feat(web/image_hosting): image upload

This commit is contained in:
Baoshuo Ren 2022-10-13 13:34:55 +08:00
parent 2d9d99e1a6
commit 0a7a6cbe9e
Signed by: baoshuo
GPG Key ID: 00CB9680AB29F51A
21 changed files with 291 additions and 17 deletions

View File

@ -837,16 +837,20 @@ UNLOCK TABLES;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `users_images` (
`id` varchar(30) NOT NULL,
`username` varchar(20) NOT NULL,
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`path` varchar(100) NOT NULL,
`uploader` varchar(20) NOT NULL,
`width` int(11) NOT NULL,
`height` int(11) NOT NULL,
`upload_time` datetime NOT NULL,
`size` int(11) NOT NULL,
`hash` varchar(70) NOT NULL,
PRIMARY KEY (`id`),
KEY `username` (`username`),
KEY `uploader` (`uploader`),
KET `path` (`path`),
KEY `upload_time` (`upload_time`),
KEY `size` (`size`)
KEY `size` (`size`),
KEY `hash` (`hash`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;

View File

@ -11,6 +11,14 @@ services:
environment:
- MYSQL_DATABASE=app_uoj233
- MYSQL_ROOT_PASSWORD=root
phpmyadmin:
image: phpmyadmin
restart: always
ports:
- 28080:80
environment:
- PMA_ARBITRARY=1
uoj-judger:
build:

View File

@ -1,21 +1,279 @@
<?php
requirePHPLib('form');
use Gregwar\Captcha\PhraseBuilder;
use Gregwar\Captcha\CaptchaBuilder;
if (!Auth::check() && UOJConfig::$data['switch']['force-login']) {
requirePHPLib('form');
requireLib('bootstrap5');
if (!Auth::check()) {
redirectToLogin();
}
if (!isNormalUser($myUser) && UOJConfig::$data['switch']['force-login']) {
if (!isNormalUser($myUser)) {
become403Page();
}
$REQUIRE_LIB['bootstrap5'] = '';
$limit = $myUser['images_size_limit'];
$used = DB::select("SELECT SUM(size) FROM `users_images` WHERE uploader = {$myUser['username']}")["SUM(size)"];
function throwError($msg) {
die(json_encode(['status' => 'error', 'message' => $msg]));
}
$allowedTypes = [IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF];
if ($_POST['image_upload_file_submit'] == 'submit') {
header('Content-Type: application/json');
if (!crsf_check()) {
throwError('expired');
}
if (!isset($_SESSION['phrase']) || !PhraseBuilder::comparePhrases($_SESSION['phrase'], $_POST['captcha'])) {
throwError("bad_captcha");
}
if ($_FILES["image_upload_file"]["error"] > 0) {
throwError($_FILES["image_upload_file"]["error"]);
}
if ($_FILES["image_upload_file"]["size"] > 5242880) { // 5 MB
throwError('too_large');
}
if ($used + $_FILES["image_upload_file"]["size"] > $limit) {
throwError('storage_limit_exceeded');
}
$size = getimagesize($_FILES['image_upload_file']['tmp_name']);
if (!$size || !in_array($size[2], $allowedTypes)) {
throwError('not_a_image');
}
list($width, $height, $type) = $size;
$hash = hash_file("sha256", $_FILES['image_upload_file']['tmp_name']);
$watermark_text = UOJConfig::$data['profile']['oj-name-short'];
if (isSuperUser($myUser) && $_POST['watermark'] == 'no_watermark') {
$watermark_text = "";
$hash += "__no";
} elseif ($_POST['watermark'] == 'site_shortname_and_username') {
$watermark_text .= ' @'.Auth::id();
$hash += "__id";
}
$existing_image = DB::selectFirst("SELECT * FROM users_images WHERE `hash` = '$hash'");
if ($existing_image) {
die(json_encode(['status' => 'success', 'path' => $existing_image['path']]));
}
$img = imagecreatefromstring(file_get_contents($_FILES["image_upload_file"]["tmp_name"]));
$white = imagecolorallocate($img, 255, 255, 255);
$black = imagecolorallocate($img, 150, 150, 150);
imagettftext($img, '16', 0, 10 + 1, max(0, $height - 20) + 1, $black, UOJContext::documentRoot().'/fonts/roboto-mono/RobotoMono-Bold.ttf', $watermark_text);
imagettftext($img, '16', 0, 10, max(0, $height - 20), $white, UOJContext::documentRoot().'/fonts/roboto-mono/RobotoMono-Bold.ttf', $watermark_text);
imagepng($img, $_FILES["image_upload_file"]["tmp_name"]);
imagedestroy($img);
$filename = uojRandAvaiableFileName('/image_hosting/', 10, '.png');
if (!move_uploaded_file($_FILES["image_upload_file"]["tmp_name"], UOJContext::storagePath().$filename)) {
throwError('unknown error');
}
DB::insert("INSERT INTO users_images (`path`, uploader, width, height, upload_time, size, `hash`) VALUES ('$filename', '{$myUser['username']}', $width, $height, now(), {$_FILES["image_upload_file"]["size"]}, '$hash')");
die(json_encode(['status' => 'success', 'path' => $filename]));
}
?>
<?php echoUOJPageHeader(UOJLocale::get('image hosting')) ?>
<style>
.drop {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
align-self: center;
flex-grow: 0 !important;
width: 9em;
height: 8.75em;
user-select: none;
cursor: pointer;
margin-left: 0;
background: #fafafa;
border: 1px solid #e8e8e8;
box-sizing: border-box;
border-radius: 4px;
}
.drop:hover {
border-color: #89d1f5;
}
</style>
<h1 class="h2">
<?= UOJLocale::get('image hosting') ?>
</h1>
<div class="card card-default">
<div class="card-body">
<form class="row m-0" id="image-upload-form" method="post" enctype="multipart/form-data">
<div class="col-12 col-md-3 drop mx-auto" id="image-upload-form-drop">
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="56" class="mb-2">
<g>
<path fill="#3498db" d="M424.49 120.48a12 12 0 0 0-17 0L272 256l-39.51-39.52a12 12 0 0 0-17 0L160 272v48h352V208zM64 336V128H48a48 48 0 0 0-48 48v256a48 48 0 0 0 48 48h384a48 48 0 0 0 48-48v-16H144a80.09 80.09 0 0 1-80-80z"></path>
<path fill="#89d1f5" d="M528 32H144a48 48 0 0 0-48 48v256a48 48 0 0 0 48 48h384a48 48 0 0 0 48-48V80a48 48 0 0 0-48-48zM208 80a48 48 0 1 1-48 48 48 48 0 0 1 48-48zm304 240H160v-48l55.52-55.52a12 12 0 0 1 17 0L272 256l135.52-135.52a12 12 0 0 1 17 0L512 208z"></path>
</g>
</svg>
<span class="small">点击此处选择图片</span>
</div>
<input id="image_upload_file" name="image_upload_file" type="file" accept="image/*" style="display: none;" />
<div class="modal fade" id="image-upload-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">上传图片</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
您确定要上传图片吗?
</div>
<div class="mb-3" id="modal-file-info"></div>
<div class="input-group">
<input type="text" class="form-control" id="input-captcha" name="captcha" placeholder="<?= UOJLocale::get('enter verification code') ?>" maxlength="20" />
<span class="input-group-text p-0 overflow-hidden rounded-0" style="border-bottom-right-radius: var(--bs-border-radius) !important">
<img id="captcha" class="col w-100 h-100" src="/captcha">
</span>
</div>
<div class="mt-3" id="modal-help-message" style="display: none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">确定</button>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-3 mt-3 mt-md-0 ms-md-2">
<h2 class="h4">水印</h2>
<?php if (isSuperUser($myUser)): ?>
<div class="form-check d-inline-block d-md-block me-2">
<input class="form-check-input" type="radio" name="watermark" id="watermark-no_watermark" data-value="no_watermark">
<label class="form-check-label" for="watermark-no_watermark">
无水印
</label>
</div>
<?php endif ?>
<div class="form-check d-inline-block d-md-block me-2">
<input class="form-check-input" type="radio" name="watermark" id="watermark-site_shortname" data-value="site_shortname" checked>
<label class="form-check-label" for="watermark-site_shortname">
<?= UOJConfig::$data['profile']['oj-name-short'] ?>
</label>
</div>
<div class="form-check d-inline-block d-md-block me-2">
<input class="form-check-input" type="radio" name="watermark" id="watermark-site_shortname_and_username" data-value="site_shortname_and_username">
<label class="form-check-label" for="watermark-site_shortname_and_username">
<?= UOJConfig::$data['profile']['oj-name-short'] ?> @<?= Auth::id() ?>
</label>
</div>
</div>
<div class="col mt-3 mt-md-0 ms-md-2">
<h2 class="h4">上传须知</h2>
<ul>
<li>上传的图片必须符合法律与社会道德;</li>
<li>图床仅供 S2OJ 站内使用;</li>
<li>在合适的地方插入图片即可引用。</li>
</ul>
</div>
</form>
</div>
</div>
<script>
var image_upload_modal = new bootstrap.Modal('#image-upload-modal');
function refreshCaptcha() {
var timestamp = new Date().getTime();
$("#captcha").attr("src", "/captcha" + '?' + timestamp);
}
$("#captcha").click(function(e) {
refreshCaptcha();
});
$('#image-upload-form').submit(function(event) {
event.preventDefault();
var data = new FormData();
data.append('_token', "<?= crsf_token() ?>");
data.append('image_upload_file_submit', 'submit');
data.append('image_upload_file', $('#image_upload_file').prop('files')[0]);
data.append('watermark', $('input[name=watermark]:checked', this).data('value'));
data.append('captcha', $('#input-captcha').val());
if ($('#image_upload_file').prop('files')[0].size > 5242880) {
$('#modal-help-message').html('图片大小不能超过 5 MB。').show();
return false;
}
$('#modal-help-message').html('上传中...').show();
$.ajax({
method: 'POST',
processData: false,
contentType: false,
data: data,
success: function(data) {
if (data.status === 'success') {
image_upload_modal.hide();
location.reload();
} else {
if (data.message === 'bad_captcha') {
refreshCaptcha();
$('#modal-help-message').html('验证码错误。').show();
} else if (data.message === 'expired') {
$('#modal-help-message').html('页面过期,请刷新重试。').show();
} else if (data.message === 'storage_limit_exceeded') {
$('#modal-help-message').html('存储超限,请联系管理员提升限制。').show();
} else if (data.message === 'not_a_image') {
$('#modal-help-message').html('文件格式不受支持。').show();
} else if (data.message === 'too_large') {
$('#modal-help-message').html('图片大小不能超过 5 MB。').show();
}
}
},
});
return false;
});
$('#image-upload-form-drop').click(function() {
$('#image_upload_file').click();
});
$('#image_upload_file').change(function() {
if ($(this).prop('files')) {
refreshCaptcha();
var watermark_type = $('input[name=watermark]:checked', '#image-upload-form').data('value');
var html = '';
html += '大小:<b>'+($(this).prop('files')[0].size / 1024).toFixed(2)+'</b> KB。';
if (watermark_type === 'no_watermark') {
html += '不添加水印。';
} else if (watermark_type === 'site_shortname_and_username') {
html += '使用水印:<?= UOJConfig::$data['profile']['oj-name-short'] ?> @<?= Auth::id() ?>。';
} else {
html += '使用水印:<?= UOJConfig::$data['profile']['oj-name-short'] ?>。';
}
$('#modal-file-info').html(html);
$('#modal-help-message').html('').hide();
image_upload_modal.show();
}
});
</script>
<?php echoUOJPageFooter() ?>

View File

@ -17,7 +17,7 @@ function uojRandAvaiableFileName($dir, $length = 20, $suffix = '') {
do {
$fileName = $dir . uojRandString($length);
} while (file_exists(UOJContext::storagePath().$fileName.$suffix));
return $fileName;
return $fileName.$suffix;
}
function uojRandAvaiableTmpFileName() {

View File

@ -81,8 +81,8 @@ Route::group([
Route::any('/click-zan', '/click_zan.php');
// Image Hosting
Route::any('/image-hosting', '/image_hosting/index.php');
Route::get('/image-hosting/{image_name}.png', '/image_hosting/get_image.php');
Route::any('/image_hosting', '/image_hosting/index.php');
Route::get('/image_hosting/{image_name}.png', '/image_hosting/get_image.php');
}
);

View File

@ -6,16 +6,20 @@ ALTER TABLE `user_info` ADD COLUMN `images_size_limit` int(11) UNSIGNED NOT NULL
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE IF NOT EXISTS `users_images` (
`id` varchar(30) NOT NULL,
`username` varchar(20) NOT NULL,
CREATE TABLE `users_images` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`path` varchar(100) NOT NULL,
`uploader` varchar(20) NOT NULL,
`width` int(11) NOT NULL,
`height` int(11) NOT NULL,
`upload_time` datetime NOT NULL,
`size` int(11) NOT NULL,
`hash` varchar(70) NOT NULL,
PRIMARY KEY (`id`),
KEY `username` (`username`),
KEY `uploader` (`uploader`),
KEY `path` (`path`),
KEY `upload_time` (`upload_time`),
KEY `size` (`size`)
KEY `size` (`size`),
KEY `hash` (`hash`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;

View File

@ -111,7 +111,7 @@ mb-4" role="navigation">
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="<?= HTML::url('/image-hosting') ?>">
<a class="dropdown-item" href="<?= HTML::url('/image_hosting') ?>">
<i class="bi bi-images"></i>
<?= UOJLocale::get('image hosting') ?>
</a>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.