feat(web): add image_hosting (#5)
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Baoshuo Ren 2022-10-13 20:33:42 +08:00 committed by GitHub
commit 0ecf295a38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 622 additions and 5 deletions

View File

@ -815,6 +815,7 @@ CREATE TABLE `user_info` (
`motto` varchar(200) NOT NULL,
`last_login` timestamp NOT NULL DEFAULT 0,
`last_visited` timestamp NOT NULL DEFAULT 0,
`images_size_limit` int(11) UNSIGNED NOT NULL DEFAULT 104857600, /* 100 MiB */
PRIMARY KEY (`username`),
KEY `ac_num` (`ac_num`,`username`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
@ -829,6 +830,39 @@ LOCK TABLES `user_info` WRITE;
/*!40000 ALTER TABLE `user_info` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `users_images`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
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(100) NOT NULL,
PRIMARY KEY (`id`),
KEY `uploader` (`uploader`),
KEY `path` (`path`),
KEY `upload_time` (`upload_time`),
KEY `size` (`size`),
KEY `hash` (`hash`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `users_images`
--
LOCK TABLES `users_images` WRITE;
/*!40000 ALTER TABLE `users_images` DISABLE KEYS */;
/*!40000 ALTER TABLE `users_images` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `user_msg`
--

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

@ -0,0 +1,24 @@
<?php
requirePHPLib('form');
if (!Auth::check() && UOJConfig::$data['switch']['force-login']) {
redirectToLogin();
}
$name = $_GET['image_name'];
if (!validateString($name)) {
become404Page();
}
$file_name = UOJContext::storagePath()."/image_hosting/$name.png";
$finfo = finfo_open(FILEINFO_MIME);
$mimetype = finfo_file($finfo, $file_name);
if ($mimetype === false) {
become404Page();
}
finfo_close($finfo);
header("X-Sendfile: $file_name");
header("Content-type: $mimetype");
header("Cache-Control: max-age=604800", true);

View File

@ -0,0 +1,430 @@
<?php
use Gregwar\Captcha\PhraseBuilder;
use Gregwar\Captcha\CaptchaBuilder;
requirePHPLib('form');
requireLib('bootstrap5');
if (!Auth::check()) {
redirectToLogin();
}
if (!isNormalUser($myUser)) {
become403Page();
}
$limit = $myUser['images_size_limit'];
$_result = DB::selectFirst("SELECT SUM(size), count(*) FROM `users_images` WHERE uploader = '{$myUser['username']}'");
$used = $_result["SUM(size)"];
$count = $_result["count(*)"];
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_watermark";
} elseif ($_POST['watermark'] == 'site_shortname_and_username') {
$watermark_text .= ' @'.Auth::id();
$hash .= "__id_".Auth::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 = imagecolorallocatealpha($img, 255, 255, 255, 30);
$black = imagecolorallocatealpha($img, 50, 50, 50, 70);
$scale = ceil($width / 750.0);
imagettftext($img, strval($scale * 16), 0, ($scale * 16) + $scale, max(0, $height - ($scale * 16) + 5) + $scale, $black, UOJContext::documentRoot().'/fonts/roboto-mono/RobotoMono-Bold.ttf', $watermark_text);
imagefilter($img, IMG_FILTER_GAUSSIAN_BLUR);
imagettftext($img, strval($scale * 16), 0, ($scale * 16), max(0, $height - ($scale * 16) + 5), $white, UOJContext::documentRoot().'/fonts/roboto-mono/RobotoMono-Bold.ttf', $watermark_text);
imagepng($img, $_FILES["image_upload_file"]["tmp_name"]);
imagedestroy($img);
if (filesize($_FILES["image_upload_file"]["tmp_name"]) > 5242880) { // 5 MB
throwError('too_large');
}
$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]));
} elseif ($_POST['image_delete_submit'] == 'submit') {
crsf_defend();
$id = $_POST['image_delete_id'];
if (!validateUInt($id)) {
becomeMsgPage('ID 不合法。<a href="'.UOJContext::requestURI().'">返回</a>');
} else {
$result = DB::selectFirst("SELECT * from users_images WHERE id = $id");
if (!$result) {
becomeMsgPage('图片不存在。<a href="'.UOJContext::requestURI().'">返回</a>');
} else {
unlink(UOJContext::storagePath().$result['path']);
DB::delete("DELETE FROM users_images WHERE id = $id");
header("Location: ". UOJContext::requestURI());
die();
}
}
}
?>
<?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 col-lg-3 order-1 drop mx-auto mx-md-0" 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 id="select-image-text" 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" id="cancel-upload">取消</button>
<button type="submit" class="btn btn-primary">确定</button>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-4 col-lg-2 order-2 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 order-3 order-md-4 order-lg-3 mt-3 mt-lg-0 ms-lg-2">
<h2 class="h4">上传须知</h2>
<ul>
<li>上传的图片必须符合法律与社会道德;</li>
<li>图床仅供 S2OJ 站内使用,校外用户无法查看;</li>
<li>在合适的地方插入图片即可引用。</li>
</ul>
</div>
<div class="col-12 col-md-5 col-lg-3 order-4 order-md-3 order-lg-4 mt-3 mt-md-0 ms-md-2">
<h2 class="h4">使用统计</h2>
<div class="d-flex justify-content-between">
<span class="small">已用空间</span>
<span><?= round($used * 1.0 / 1024 / 1024, 2) ?> MB / <?= round($limit * 1.0 / 1024 / 1024, 2) ?> MB</span>
</div>
<div class="d-flex justify-content-between">
<span class="small">上传总数</span>
<span><?= $count ?> 张</span>
</div>
</div>
</form>
</div>
</div>
<script>
var image_upload_modal = new bootstrap.Modal('#image-upload-modal');
var droppedFiles = false;
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();
}
}
},
error: function() {
$('#modal-help-message').html('上传失败,请刷新页面重试。').addClass('text-danger').show();
}
});
return false;
});
$('#image-upload-form-drop').click(function() {
$('#image_upload_file').click();
});
$('#image-upload-form-drop').on('drag dragstart dragend dragover dragenter dragleave drop', function(e) {
e.preventDefault();
e.stopPropagation();
}).on('dragover dragenter', function() {
$('#select-image-text').html('松开以上传');
}).on('dragleave dragend drop', function() {
$('#select-image-text').html('点击此处选择图片');
}).on('drop', function(e) {
$('#image_upload_file').prop('files', e.originalEvent.dataTransfer.files);
$('#image_upload_file').trigger('change');
});
$('#image-upload-modal').on('hide.bs.modal', function() {
$('#image-upload-form').trigger('reset');
});
$('#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
$pag_config = [
'page_len' => 40,
'col_names' => ['*'],
'table_name' => 'users_images',
'cond' => "uploader = '{$myUser['username']}'",
'tail' => 'order by upload_time desc',
];
$pag = new Paginator($pag_config);
?>
<h2 class="h3 mt-4 mb-3">
我的图片
</h2>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-4">
<?php foreach ($pag->get() as $idx => $row): ?>
<div class="col">
<div class="card">
<img src="<?= $row['path'] ?>" class="card-img-top" height="200" style="object-fit: contain">
<div class="card-footer bg-transparent small px-2">
<div class="d-flex flex-wrap justify-content-between">
<time><?= $row['upload_time'] ?></time>
<span>
<?php if ($row['size'] < 1024 * 512): ?>
<?= round($row['size'] * 1.0 / 1024, 1) ?> KB
<?php else: ?>
<?= round($row['size'] * 1.0 / 1024 / 1024, 1) ?> MB
<?php endif ?>
</span>
</div>
<div class="d-flex flex-wrap justify-content-between mt-2">
<form method="post" onsubmit="return confirm('您确定要删除这张图片吗?');">
<input type="hidden" name="image_delete_submit" value="submit">
<input type="hidden" name="image_delete_id" value="<?= $row['id'] ?>">
<input type="hidden" name="_token" value="<?= crsf_token() ?>">
<button class="btn btn-sm btn-outline-danger image-delete-button" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="删除">
<i class="bi bi-trash3"></i>
</button>
</form>
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary image-copy-url-button" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="复制链接" data-image-path="<?= $row['path'] ?>">
<i class="bi bi-clipboard"></i>
</button>
<button class="btn btn-sm btn-outline-secondary image-copy-md-button" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="复制 Markdown 源码" data-image-path="<?= $row['path'] ?>">
<i class="bi bi-markdown"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<?php endforeach ?>
</div>
<?php if ($pag->isEmpty()): ?>
<div class="mt-4 text-muted">
<?= UOJLocale::get('none') ?>
</div>
<?php endif ?>
<div class="text-end">
<?= $pag->pagination() ?>
</div>
<div class="toast-container position-fixed bottom-0 start-0 ms-3 mb-4">
<div id="copy-url-toast" class="toast text-bg-success align-items-center border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
复制成功!
</div>
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
<script>
$(document).ready(function() {
[...document.querySelectorAll('[data-bs-toggle="tooltip"]')].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
});
var copy_url_toast = new bootstrap.Toast('#copy-url-toast', { delay: 2000 });
$('.image-copy-url-button').click(function() {
var url = new URL($(this).data('image-path'), location.origin);
navigator.clipboard.writeText(url);
copy_url_toast.show();
});
$('.image-copy-md-button').click(function() {
var url = new URL($(this).data('image-path'), location.origin);
navigator.clipboard.writeText('![](' + url + ')');
copy_url_toast.show();
});
</script>
<?php echoUOJPageFooter() ?>

View File

@ -603,6 +603,58 @@ EOD;
EOD;
};
$image_hosting_cols = ['*'];
$image_hosting_config = ['page_len' => 20, 'table_classes' => ['table', 'table-bordered', 'table-hover', 'table-striped']];
$image_hosting_header_row = <<<EOD
<tr>
<th>ID</th>
<th>上传者</th>
<th>预览</th>
<th style="width: 3em">文件大小</th>
<th style="width: 12em">上传时间</th>
</tr>
EOD;
$image_hosting_print_row = function($row) {
$user_link = getUserLink($row['uploader']);
if ($row['size'] < 1024 * 512) {
$size = strval(round($row['size'] * 1.0 / 1024, 1)) . ' KB';
} else {
$size = strval(round($row['size'] * 1.0 / 1024 / 1024, 1)) . ' MB';
}
echo <<<EOD
<tr>
<td>{$row['id']}</td>
<td>$user_link</td>
<td><img src="{$row['path']}" width="250"></td>
<td>$size</td>
<td>{$row['upload_time']}</td>
</tr>
EOD;
};
$image_deleter = new UOJForm('image_deleter');
$image_deleter->addInput('image_deleter_id', 'text', '图片 ID', '',
function ($x, &$vdata) {
if (!validateUInt($x)) {
return 'ID 不合法';
}
if (!DB::selectCount("select count(*) from users_images where id = $x")) {
return '图片不存在';
}
$vdata['id'] = $x;
return '';
},
null
);
$image_deleter->handle = function(&$vdata) {
$id = $vdata['id'];
$result = DB::selectFirst("SELECT * from users_images WHERE id = $id");
unlink(UOJContext::storagePath().$result['path']);
DB::delete("DELETE FROM users_images WHERE id = $id");
};
$image_deleter->runAtServer();
$tabs_info = array(
'users' => array(
'name' => '用户管理',
@ -627,6 +679,10 @@ EOD;
'judger' => array(
'name' => '评测机管理',
'url' => '/super-manage/judger'
),
'image_hosting' => array(
'name' => '图床管理',
'url' => '/super-manage/image_hosting'
)
);
@ -758,6 +814,13 @@ EOD;
</div>
<h3>评测机列表</h3>
<?php echoLongTable($judgerlist_cols, 'judger_info', "1=1", '', $judgerlist_header_row, $judgerlist_print_row, $judgerlist_config) ?>
<?php elseif ($cur_tab === 'image_hosting'): ?>
<h3>图床管理</h3>
<?php echoLongTable($image_hosting_cols, 'users_images', "1=1", 'order by id desc', $image_hosting_header_row, $image_hosting_print_row, $image_hosting_config) ?>
<div>
<h4>删除图片</h4>
<?php $image_deleter->printHTML() ?>
</div>
<?php endif ?>
</div>
</div>

View File

@ -4,7 +4,7 @@ function uojRand($l, $r) {
return mt_rand($l, $r);
}
function uojRandString($len, $charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') {
function uojRandString($len, $charset = '0123456789abcdefghijklmnopqrstuvwxyz') {
$n_chars = strlen($charset);
$str = '';
for ($i = 0; $i < $len; $i++) {
@ -13,11 +13,11 @@ function uojRandString($len, $charset = '0123456789abcdefghijklmnopqrstuvwxyzABC
return $str;
}
function uojRandAvaiableFileName($dir) {
function uojRandAvaiableFileName($dir, $length = 20, $suffix = '') {
do {
$fileName = $dir . uojRandString(20);
} while (file_exists(UOJContext::storagePath().$fileName));
return $fileName;
$fileName = $dir . uojRandString($length);
} while (file_exists(UOJContext::storagePath().$fileName.$suffix));
return $fileName.$suffix;
}
function uojRandAvaiableTmpFileName() {

View File

@ -51,3 +51,7 @@ function validateIP($ip) {
function validateURL($url) {
return filter_var($url, FILTER_VALIDATE_URL) !== false;
}
function validateString($str) {
return preg_match('/[^0-9a-zA-Z]/', $str) !== true;
}

View File

@ -100,4 +100,6 @@ return [
'last active at' => 'Last active at',
'online' => 'Online',
'offline' => 'Offline',
'apps' => 'Apps',
'image hosting' => 'Image Hosting',
];

View File

@ -100,4 +100,6 @@ return [
'online' => '在线',
'offline' => '离线',
'last active at' => '最后活动于',
'apps' => '应用',
'image hosting' => '图床',
];

View File

@ -5,6 +5,7 @@ Route::pattern('id', '[1-9][0-9]{0,9}');
Route::pattern('contest_id', '[1-9][0-9]{0,9}');
Route::pattern('tab', '\S{1,20}');
Route::pattern('rand_str_id', '[0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]{20}');
Route::pattern('image_name', '[0-9a-z]{1,20}');
Route::pattern('upgrade_name', '[a-zA-Z0-9_]{1,50}');
Route::group([
@ -78,6 +79,10 @@ Route::group([
Route::any('/download.php', '/download.php');
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');
}
);

View File

View File

@ -0,0 +1 @@
ref: https://github.com/renbaoshuo/S2OJ/issues/4

View File

@ -0,0 +1,2 @@
ALTER TABLE `user_info` DROP COLUMN IF EXISTS `images_size_limit`;
DROP TABLE IF EXISTS `users_images`;

View File

@ -0,0 +1,25 @@
ALTER TABLE `user_info` ADD COLUMN `images_size_limit` int(11) UNSIGNED NOT NULL DEFAULT 104857600 /* 100 MiB */;
--
-- Table structure for table `users_images`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
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(100) NOT NULL,
PRIMARY KEY (`id`),
KEY `uploader` (`uploader`),
KEY `path` (`path`),
KEY `upload_time` (`upload_time`),
KEY `size` (`size`),
KEY `hash` (`hash`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;

View File

@ -103,6 +103,22 @@ mb-4" role="navigation">
<?= UOJLocale::get('blogs') ?>
</a>
</li>
<?php if (isset($REQUIRE_LIB['bootstrap5'])): ?>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-grid-3x3-gap"></i>
<?= UOJLocale::get('apps') ?>
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="<?= HTML::url('/image_hosting') ?>">
<i class="bi bi-images"></i>
<?= UOJLocale::get('image hosting') ?>
</a>
</li>
</ul>
</li>
<?php endif ?>
<li class="nav-item">
<a class="nav-link" href="<?= HTML::url('/faq') ?>">
<?php if (isset($REQUIRE_LIB['bootstrap5'])): ?>

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.

View File

@ -89,6 +89,7 @@ initProgress(){
service apache2 restart
mkdir -p /opt/uoj/web/app/storage/submission
mkdir -p /opt/uoj/web/app/storage/tmp
mkdir -p /opt/uoj/web/app/storage/image_hosting
chmod -R 777 /opt/uoj/web/app/storage
#Using cli upgrade to latest
php7.4 /var/www/uoj/app/cli.php upgrade:latest