diff --git a/db/app_uoj233.sql b/db/app_uoj233.sql
index f49be87..ee08e17 100644
--- a/db/app_uoj233.sql
+++ b/db/app_uoj233.sql
@@ -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`
--
diff --git a/docker-compose.development.yml b/docker-compose.development.yml
index 531f1a5..2158beb 100644
--- a/docker-compose.development.yml
+++ b/docker-compose.development.yml
@@ -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:
diff --git a/web/app/controllers/image_hosting/get_image.php b/web/app/controllers/image_hosting/get_image.php
new file mode 100644
index 0000000..1f53f04
--- /dev/null
+++ b/web/app/controllers/image_hosting/get_image.php
@@ -0,0 +1,24 @@
+ '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 不合法。返回');
+ } else {
+ $result = DB::selectFirst("SELECT * from users_images WHERE id = $id");
+ if (!$result) {
+ becomeMsgPage('图片不存在。返回');
+ } else {
+ unlink(UOJContext::storagePath().$result['path']);
+ DB::delete("DELETE FROM users_images WHERE id = $id");
+
+ header("Location: ". UOJContext::requestURI());
+ die();
+ }
+ }
+ }
+ ?>
+
+
+
+
+
+
+ = UOJLocale::get('image hosting') ?>
+
+
+
+
+
+ 40,
+ 'col_names' => ['*'],
+ 'table_name' => 'users_images',
+ 'cond' => "uploader = '{$myUser['username']}'",
+ 'tail' => 'order by upload_time desc',
+];
+ $pag = new Paginator($pag_config);
+ ?>
+
+
+ 我的图片
+
+
+
+ get() as $idx => $row): ?>
+
+
+
+
+
+
+
+
+
+isEmpty()): ?>
+
+ = UOJLocale::get('none') ?>
+
+
+
+
+ = $pag->pagination() ?>
+
+
+
+
+
+
+
diff --git a/web/app/controllers/super_manage.php b/web/app/controllers/super_manage.php
index fd6939a..19fdb92 100644
--- a/web/app/controllers/super_manage.php
+++ b/web/app/controllers/super_manage.php
@@ -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 = <<
+ ID |
+ 上传者 |
+ 预览 |
+ 文件大小 |
+ 上传时间 |
+
+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 <<
+ {$row['id']} |
+ $user_link |
+ |
+ $size |
+ {$row['upload_time']} |
+
+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;
评测机列表
+
+ 图床管理
+
+
+
删除图片
+ printHTML() ?>
+
diff --git a/web/app/libs/uoj-rand-lib.php b/web/app/libs/uoj-rand-lib.php
index d39c527..838d777 100644
--- a/web/app/libs/uoj-rand-lib.php
+++ b/web/app/libs/uoj-rand-lib.php
@@ -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() {
diff --git a/web/app/libs/uoj-validate-lib.php b/web/app/libs/uoj-validate-lib.php
index eb991a5..a685868 100644
--- a/web/app/libs/uoj-validate-lib.php
+++ b/web/app/libs/uoj-validate-lib.php
@@ -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;
+}
diff --git a/web/app/locale/basic/en.php b/web/app/locale/basic/en.php
index 44fd378..cb2bcd2 100644
--- a/web/app/locale/basic/en.php
+++ b/web/app/locale/basic/en.php
@@ -100,4 +100,6 @@ return [
'last active at' => 'Last active at',
'online' => 'Online',
'offline' => 'Offline',
+ 'apps' => 'Apps',
+ 'image hosting' => 'Image Hosting',
];
diff --git a/web/app/locale/basic/zh-cn.php b/web/app/locale/basic/zh-cn.php
index 133eeba..135b050 100644
--- a/web/app/locale/basic/zh-cn.php
+++ b/web/app/locale/basic/zh-cn.php
@@ -100,4 +100,6 @@ return [
'online' => '在线',
'offline' => '离线',
'last active at' => '最后活动于',
+ 'apps' => '应用',
+ 'image hosting' => '图床',
];
diff --git a/web/app/route.php b/web/app/route.php
index 3ea089e..43377cb 100644
--- a/web/app/route.php
+++ b/web/app/route.php
@@ -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');
}
);
diff --git a/web/app/storage/image_hosting/.gitkeep b/web/app/storage/image_hosting/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/web/app/upgrade/4_image_hosting/README.md b/web/app/upgrade/4_image_hosting/README.md
new file mode 100644
index 0000000..a936858
--- /dev/null
+++ b/web/app/upgrade/4_image_hosting/README.md
@@ -0,0 +1 @@
+ref: https://github.com/renbaoshuo/S2OJ/issues/4
diff --git a/web/app/upgrade/4_image_hosting/down.sql b/web/app/upgrade/4_image_hosting/down.sql
new file mode 100644
index 0000000..422652d
--- /dev/null
+++ b/web/app/upgrade/4_image_hosting/down.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `user_info` DROP COLUMN IF EXISTS `images_size_limit`;
+DROP TABLE IF EXISTS `users_images`;
diff --git a/web/app/upgrade/4_image_hosting/up.sql b/web/app/upgrade/4_image_hosting/up.sql
new file mode 100644
index 0000000..2471909
--- /dev/null
+++ b/web/app/upgrade/4_image_hosting/up.sql
@@ -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 */;
diff --git a/web/app/views/main-nav.php b/web/app/views/main-nav.php
index eb6cc71..ee77856 100644
--- a/web/app/views/main-nav.php
+++ b/web/app/views/main-nav.php
@@ -103,6 +103,22 @@ mb-4" role="navigation">
= UOJLocale::get('blogs') ?>
+
+
+
+
+ = UOJLocale::get('apps') ?>
+
+
+
+
diff --git a/web/fonts/roboto-mono/RobotoMono-Bold.ttf b/web/fonts/roboto-mono/RobotoMono-Bold.ttf
new file mode 100644
index 0000000..8eff26c
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-Bold.ttf differ
diff --git a/web/fonts/roboto-mono/RobotoMono-BoldItalic.ttf b/web/fonts/roboto-mono/RobotoMono-BoldItalic.ttf
new file mode 100644
index 0000000..9929964
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-BoldItalic.ttf differ
diff --git a/web/fonts/roboto-mono/RobotoMono-ExtraLight.ttf b/web/fonts/roboto-mono/RobotoMono-ExtraLight.ttf
new file mode 100644
index 0000000..e788efe
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-ExtraLight.ttf differ
diff --git a/web/fonts/roboto-mono/RobotoMono-ExtraLightItalic.ttf b/web/fonts/roboto-mono/RobotoMono-ExtraLightItalic.ttf
new file mode 100644
index 0000000..142bb80
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-ExtraLightItalic.ttf differ
diff --git a/web/fonts/roboto-mono/RobotoMono-Italic.ttf b/web/fonts/roboto-mono/RobotoMono-Italic.ttf
new file mode 100644
index 0000000..cf2b5b3
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-Italic.ttf differ
diff --git a/web/fonts/roboto-mono/RobotoMono-Light.ttf b/web/fonts/roboto-mono/RobotoMono-Light.ttf
new file mode 100644
index 0000000..f03a2b9
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-Light.ttf differ
diff --git a/web/fonts/roboto-mono/RobotoMono-LightItalic.ttf b/web/fonts/roboto-mono/RobotoMono-LightItalic.ttf
new file mode 100644
index 0000000..acda3b8
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-LightItalic.ttf differ
diff --git a/web/fonts/roboto-mono/RobotoMono-Medium.ttf b/web/fonts/roboto-mono/RobotoMono-Medium.ttf
new file mode 100644
index 0000000..752d0fa
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-Medium.ttf differ
diff --git a/web/fonts/roboto-mono/RobotoMono-MediumItalic.ttf b/web/fonts/roboto-mono/RobotoMono-MediumItalic.ttf
new file mode 100644
index 0000000..03c2f69
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-MediumItalic.ttf differ
diff --git a/web/fonts/roboto-mono/RobotoMono-Regular.ttf b/web/fonts/roboto-mono/RobotoMono-Regular.ttf
new file mode 100644
index 0000000..d9371a1
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-Regular.ttf differ
diff --git a/web/fonts/roboto-mono/RobotoMono-SemiBold.ttf b/web/fonts/roboto-mono/RobotoMono-SemiBold.ttf
new file mode 100644
index 0000000..06aea97
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-SemiBold.ttf differ
diff --git a/web/fonts/roboto-mono/RobotoMono-SemiBoldItalic.ttf b/web/fonts/roboto-mono/RobotoMono-SemiBoldItalic.ttf
new file mode 100644
index 0000000..72b0fc6
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-SemiBoldItalic.ttf differ
diff --git a/web/fonts/roboto-mono/RobotoMono-Thin.ttf b/web/fonts/roboto-mono/RobotoMono-Thin.ttf
new file mode 100644
index 0000000..388700f
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-Thin.ttf differ
diff --git a/web/fonts/roboto-mono/RobotoMono-ThinItalic.ttf b/web/fonts/roboto-mono/RobotoMono-ThinItalic.ttf
new file mode 100644
index 0000000..b33bd28
Binary files /dev/null and b/web/fonts/roboto-mono/RobotoMono-ThinItalic.ttf differ
diff --git a/web/install.sh b/web/install.sh
index e86112f..c385272 100644
--- a/web/install.sh
+++ b/web/install.sh
@@ -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