mirror of
https://github.com/renbaoshuo/S2OJ.git
synced 2024-12-26 12:21:52 +00:00
feat(web): add image_hosting (#5)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
commit
0ecf295a38
@ -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`
|
||||
--
|
||||
|
@ -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:
|
||||
|
24
web/app/controllers/image_hosting/get_image.php
Normal file
24
web/app/controllers/image_hosting/get_image.php
Normal 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);
|
430
web/app/controllers/image_hosting/index.php
Normal file
430
web/app/controllers/image_hosting/index.php
Normal 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() ?>
|
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -100,4 +100,6 @@ return [
|
||||
'last active at' => 'Last active at',
|
||||
'online' => 'Online',
|
||||
'offline' => 'Offline',
|
||||
'apps' => 'Apps',
|
||||
'image hosting' => 'Image Hosting',
|
||||
];
|
||||
|
@ -100,4 +100,6 @@ return [
|
||||
'online' => '在线',
|
||||
'offline' => '离线',
|
||||
'last active at' => '最后活动于',
|
||||
'apps' => '应用',
|
||||
'image hosting' => '图床',
|
||||
];
|
||||
|
@ -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');
|
||||
}
|
||||
);
|
||||
|
||||
|
0
web/app/storage/image_hosting/.gitkeep
Normal file
0
web/app/storage/image_hosting/.gitkeep
Normal file
1
web/app/upgrade/4_image_hosting/README.md
Normal file
1
web/app/upgrade/4_image_hosting/README.md
Normal file
@ -0,0 +1 @@
|
||||
ref: https://github.com/renbaoshuo/S2OJ/issues/4
|
2
web/app/upgrade/4_image_hosting/down.sql
Normal file
2
web/app/upgrade/4_image_hosting/down.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE `user_info` DROP COLUMN IF EXISTS `images_size_limit`;
|
||||
DROP TABLE IF EXISTS `users_images`;
|
25
web/app/upgrade/4_image_hosting/up.sql
Normal file
25
web/app/upgrade/4_image_hosting/up.sql
Normal 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 */;
|
@ -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'])): ?>
|
||||
|
BIN
web/fonts/roboto-mono/RobotoMono-Bold.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-Bold.ttf
Normal file
Binary file not shown.
BIN
web/fonts/roboto-mono/RobotoMono-BoldItalic.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
web/fonts/roboto-mono/RobotoMono-ExtraLight.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
web/fonts/roboto-mono/RobotoMono-ExtraLightItalic.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
web/fonts/roboto-mono/RobotoMono-Italic.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-Italic.ttf
Normal file
Binary file not shown.
BIN
web/fonts/roboto-mono/RobotoMono-Light.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-Light.ttf
Normal file
Binary file not shown.
BIN
web/fonts/roboto-mono/RobotoMono-LightItalic.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-LightItalic.ttf
Normal file
Binary file not shown.
BIN
web/fonts/roboto-mono/RobotoMono-Medium.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-Medium.ttf
Normal file
Binary file not shown.
BIN
web/fonts/roboto-mono/RobotoMono-MediumItalic.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
web/fonts/roboto-mono/RobotoMono-Regular.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-Regular.ttf
Normal file
Binary file not shown.
BIN
web/fonts/roboto-mono/RobotoMono-SemiBold.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-SemiBold.ttf
Normal file
Binary file not shown.
BIN
web/fonts/roboto-mono/RobotoMono-SemiBoldItalic.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
web/fonts/roboto-mono/RobotoMono-Thin.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-Thin.ttf
Normal file
Binary file not shown.
BIN
web/fonts/roboto-mono/RobotoMono-ThinItalic.ttf
Normal file
BIN
web/fonts/roboto-mono/RobotoMono-ThinItalic.ttf
Normal file
Binary file not shown.
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user