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(); + } + } + } + ?> + + + + + +

+ +

+ +
+
+
+
+ + 点击此处选择图片 +
+ + +
+

水印

+ +
+ + +
+ +
+ + +
+
+ + +
+
+
+

上传须知

+
    +
  • 上传的图片必须符合法律与社会道德;
  • +
  • 图床仅供 S2OJ 站内使用,校外用户无法查看;
  • +
  • 在合适的地方插入图片即可引用。
  • +
+
+
+

使用统计

+
+ 已用空间 + MB / MB +
+
+ 上传总数 + +
+
+
+
+
+ + + 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()): ?> +
+ +
+ + +
+ 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"> + + +