diff --git a/web/app/controllers/hack.php b/web/app/controllers/hack.php index 572a3c2..1d81d73 100644 --- a/web/app/controllers/hack.php +++ b/web/app/controllers/hack.php @@ -38,7 +38,7 @@ if (UOJHack::cur()->userCanReview(Auth::user())) { $new_in = "{$input}_in"; $new_out = "{$input}_out"; $reason = null; - $err = dataAddHackPoint(UOJHack::cur()->problem->info, $new_in, $new_out, $reason, Auth::user()); + $err = UOJHack::cur()->problem->addHackPoint($new_in, $new_out, $reason, Auth::user()); $err === '' || UOJResponse::message($err); unlink($new_in); unlink($new_out); diff --git a/web/app/controllers/judge/submit.php b/web/app/controllers/judge/submit.php index 59e25c0..b8ed8e3 100644 --- a/web/app/controllers/judge/submit.php +++ b/web/app/controllers/judge/submit.php @@ -72,7 +72,7 @@ function hackJudged() { $up_out = $_FILES["std_output"]['tmp_name']; if (!UOJHack::cur()->problem->needToReviewHack()) { - $err = dataAddHackPoint(UOJHack::cur()->problem->info, $up_in, $up_out); + $err = UOJHack::cur()->problem->addHackPoint($up_in, $up_out); if ($err === '') { unlink($input); DB::update([ diff --git a/web/app/controllers/problem_data_manage.php b/web/app/controllers/problem_data_manage.php index 381c768..863f25e 100644 --- a/web/app/controllers/problem_data_manage.php +++ b/web/app/controllers/problem_data_manage.php @@ -70,33 +70,12 @@ if ($_POST['problem_data_file_submit'] == 'submit') { $zip_mime_types = ['application/zip', 'application/x-zip', 'application/x-zip-compressed']; if (in_array($_FILES["problem_data_file"]["type"], $zip_mime_types) || $_FILES["problem_data_file"]["type"] == 'application/octet-stream' && substr($_FILES["problem_data_file"]["name"], -4) == '.zip') { - $zip = new ZipArchive; + $errmsg = UOJProblem::cur()->uploadDataViaZipFile($_FILES["problem_data_file"]["tmp_name"]); - try { - if ($zip->open($_FILES["problem_data_file"]["tmp_name"]) !== true) { - throw new UOJUploadFailedException('压缩文件打开失败'); - } - - if (!$zip->extractTo(UOJProblem::cur()->getUploadFolderPath())) { - throw new UOJUploadFailedException('压缩文件解压失败'); - } - - if (!$zip->close()) { - throw new UOJUploadFailedException('压缩文件关闭失败'); - } - } catch (Exception $e) { - becomeMsgPage('
\n" . HTML::escape(uojFilePreview("{$this->prepare_dir}/compiler_result.txt", 10000)) . "\n"); - } elseif ($rstype == 7) { - throw new Exception("$name : compile error. No comment"); - } else { - throw new Exception("$name : compile error. Compiler " . judgerCodeStr($rstype)); - } - } - - unlink("{$this->prepare_dir}/compiler_result.txt"); - - if (isset($config['path'])) { - rename("{$this->prepare_dir}/{$config['path']}/{$src['path']}", "{$this->prepare_dir}/{$src['path']}"); - rename("{$this->prepare_dir}/{$config['path']}/$name", "{$this->prepare_dir}/$name"); - } - } - - private function makefile_at_prepare() { - $include_path = UOJLocalRun::$judger_include_path; - - $res = UOJLocalRun::exec(['/usr/bin/make', "INCLUDE_PATH={$include_path}"], [ - ['in', '/dev/null'], - ['out', 'stderr'], - ['err', "{$this->prepare_dir}/makefile_result.txt"], - ['tl', 60], - ['ml', 512], - ['ol', 64], - ['type', 'compiler'], - ['work-path', $this->prepare_dir], - ['add-readable-raw', "{$include_path}/"] - ]); - $rstype = isset($res['rstype']) ? $res['rstype'] : 7; - - if ($rstype != 0 || $res['exit_code'] != 0) { - if ($rstype == 0) { - throw new Exception("Makefile : compile error
\n" . HTML::escape(uojFilePreview("{$this->prepare_dir}/makefile_result.txt", 10000)) . "\n"); - } elseif ($rstype == 7) { - throw new Exception("Makefile : compile error. No comment"); - } else { - throw new Exception("Makefile : compile error. Compiler " . judgerCodeStr($rstype)); - } - } - - unlink("{$this->prepare_dir}/makefile_result.txt"); - } - - public function _updateProblemConf($new_problem_conf) { - try { - putUOJConf("{$this->upload_dir}/problem.conf", $new_problem_conf); - - $this->_sync(); - return ''; - } catch (Exception $e) { - return $e->getMessage(); - } - } - public function updateProblemConf($new_problem_conf) { - return $this->lock(LOCK_EX, fn () => $this->_updateProblemConf($new_problem_conf)); - } - - private function _addHackPoint($uploaded_input_file, $uploaded_output_file, $reason) { - try { - switch ($this->problem->getExtraConfig('add_hack_as')) { - case 'test': - $key_num = 'n_tests'; - $msg = 'add new test'; - $gen_in_name = 'getUOJProblemInputFileName'; - $gen_out_name = 'getUOJProblemOutputFileName'; - break; - case 'ex_test': - $key_num = 'n_ex_tests'; - $msg = 'add new extra test'; - $gen_in_name = 'getUOJProblemExtraInputFileName'; - $gen_out_name = 'getUOJProblemExtraOutputFileName'; - break; - default: - return 'add hack to data failed: add_hack_as should be either "ex_test" or "test"'; - } - - $new_problem_conf = $this->problem->getProblemConfArray(); - if ($new_problem_conf == -1 || $new_problem_conf == -2) { - return $new_problem_conf; - } - $new_problem_conf[$key_num] = getUOJConfVal($new_problem_conf, $key_num, 0) + 1; - - putUOJConf("{$this->upload_dir}/problem.conf", $new_problem_conf); - - $new_input_name = $gen_in_name($new_problem_conf, $new_problem_conf[$key_num]); - $new_output_name = $gen_out_name($new_problem_conf, $new_problem_conf[$key_num]); - - if (!copy($uploaded_input_file, "{$this->upload_dir}/$new_input_name")) { - return "input file not found"; - } - if (!copy($uploaded_output_file, "{$this->upload_dir}/$new_output_name")) { - return "output file not found"; - } - } catch (Exception $e) { - return $e->getMessage(); - } - - $ret = $this->_sync(); - if ($ret !== '') { - return "hack successfully but sync failed: $ret"; - } - - if (isset($reason['hack_url'])) { - UOJSystemUpdate::updateProblem($this->problem, [ - 'text' => 'Hack 成功,自动添加数据', - 'url' => $reason['hack_url'] - ]); - } - UOJSubmission::rejudgeProblemAC($this->problem, [ - 'reason_text' => $reason['rejudge'], - 'requestor' => '' - ]); - return ''; - } - - public function addHackPoint($uploaded_input_file, $uploaded_output_file, $reason = []) { - return $this->lock(LOCK_EX, fn () => $this->_addHackPoint($uploaded_input_file, $uploaded_output_file, $reason)); - } - - public function fast_hackable_check() { - if (!$this->problem->info['hackable']) { - return; - } - if (!$this->check_conf_on('use_builtin_judger')) { - return; - } - - if ($this->check_conf_on('submit_answer')) { - throw new UOJProblemConfException("提交答案题不可 Hack,请先停用本题的 Hack 功能。"); - } else { - if (UOJLang::findSourceCode('std', $this->upload_dir) === false) { - throw new UOJProblemConfException("找不到本题的 std。请上传 std 代码文件,或停用本题的 Hack 功能。"); - } - if (UOJLang::findSourceCode('val', $this->upload_dir) === false) { - throw new UOJProblemConfException("找不到本题的 val。请上传 val 代码文件,或停用本题的 Hack 功能。"); - } - } - } - - private function _sync() { - try { - if (!$this->create_prepare_folder()) { - throw new UOJSyncFailedException('创建临时文件夹失败'); - } - - $this->requirement = []; - $this->problem_extra_config = $this->problem->getExtraConfig();; - if (!is_file("{$this->upload_dir}/problem.conf")) { - throw new UOJFileNotFoundException("problem.conf"); - } - - $this->problem_conf = getUOJConf("{$this->upload_dir}/problem.conf"); - $this->final_problem_conf = $this->problem_conf; - if ($this->problem_conf === -1) { - throw new UOJFileNotFoundException("problem.conf"); - } elseif ($this->problem_conf === -2) { - throw new UOJProblemConfException("syntax error: duplicate keys"); - } - - $this->allow_files = array_flip(FS::scandir($this->upload_dir)); - - $zip_file = new ZipArchive(); - if ($zip_file->open("{$this->prepare_dir}/download.zip", ZipArchive::CREATE) !== true) { - throw new Exception("download.zip : failed to create the zip file"); - } - - if (isset($this->allow_files['require']) && is_dir("{$this->upload_dir}/require")) { - $this->copy_to_prepare('require'); - } - - if (isset($this->allow_files['testlib.h']) && is_file("{$this->upload_dir}/testlib.h")) { - $this->copy_file_to_prepare('testlib.h'); - } - - $this->fast_hackable_check(); - - if ($this->check_conf_on('use_builtin_judger')) { - $n_tests = getUOJConfVal($this->problem_conf, 'n_tests', 10); - if (!validateUInt($n_tests) || $n_tests <= 0) { - throw new UOJProblemConfException("n_tests must be a positive integer"); - } - for ($num = 1; $num <= $n_tests; $num++) { - $input_file_name = getUOJProblemInputFileName($this->problem_conf, $num); - $output_file_name = getUOJProblemOutputFileName($this->problem_conf, $num); - - $this->copy_file_to_prepare($input_file_name); - $this->copy_file_to_prepare($output_file_name); - } - - if (!$this->check_conf_on('interaction_mode')) { - if (isset($this->problem_conf['use_builtin_checker'])) { - if (!preg_match('/^[a-zA-Z0-9_]{1,20}$/', $this->problem_conf['use_builtin_checker'])) { - throw new Exception("" . HTML::escape($this->problem_conf['use_builtin_checker']) . " is not a valid checker"); - } - } else { - $this->copy_source_code_to_prepare('chk'); - $this->compile_at_prepare('chk', ['need_include_header' => true]); - } - } - - if ($this->check_conf_on('submit_answer')) { - if (!isset($this->problem_extra_config['dont_download_input'])) { - for ($num = 1; $num <= $n_tests; $num++) { - $input_file_name = getUOJProblemInputFileName($this->problem_conf, $num); - $zip_file->addFile("{$this->prepare_dir}/$input_file_name", "$input_file_name"); - } - } - - $n_output_files = 0; - for ($num = 1; $num <= $n_tests; $num++) { - $output_file_id = getUOJConfVal($this->problem_conf, ["output_file_id_{$num}", "output_file_id"], "$num"); - if (!validateUInt($output_file_id) || $output_file_id < 0 || $output_file_id > $n_tests) { - throw new UOJProblemConfException("output_file_id/output_file_id_{$num} must be in [1, n_tests]"); - } - $n_output_files = max($n_output_files, $output_file_id); - } - for ($num = 1; $num <= $n_output_files; $num++) { - $output_file_name = getUOJProblemOutputFileName($this->problem_conf, $num); - $this->requirement[] = ['name' => "output$num", 'type' => 'text', 'file_name' => $output_file_name]; - } - } else { - $n_ex_tests = getUOJConfVal($this->problem_conf, 'n_ex_tests', 0); - if (!validateUInt($n_ex_tests) || $n_ex_tests < 0) { - throw new UOJProblemConfException('n_ex_tests must be a non-negative integer. Current value: ' . HTML::escape($n_ex_tests)); - } - - for ($num = 1; $num <= $n_ex_tests; $num++) { - $input_file_name = getUOJProblemExtraInputFileName($this->problem_conf, $num); - $output_file_name = getUOJProblemExtraOutputFileName($this->problem_conf, $num); - - $this->copy_file_to_prepare($input_file_name); - $this->copy_file_to_prepare($output_file_name); - } - - if ($this->problem->info['hackable']) { - $this->copy_source_code_to_prepare('std'); - if (isset($this->problem_conf['with_implementer']) && $this->problem_conf['with_implementer'] == 'on') { - $this->compile_at_prepare('std', [ - 'implementer' => 'implementer', - 'path' => 'require' - ]); - } else { - $this->compile_at_prepare('std'); - } - $this->copy_source_code_to_prepare('val'); - $this->compile_at_prepare('val', ['need_include_header' => true]); - } - - if ($this->check_conf_on('interaction_mode')) { - $this->copy_source_code_to_prepare('interactor'); - $this->compile_at_prepare('interactor', ['need_include_header' => true]); - } - - $n_sample_tests = getUOJConfVal($this->problem_conf, 'n_sample_tests', $n_tests); - if (!validateUInt($n_sample_tests) || $n_sample_tests < 0) { - throw new UOJProblemConfException('n_sample_tests must be a non-negative integer. Current value: ' . HTML::escape($n_sample_tests)); - } - if ($n_sample_tests > $n_ex_tests) { - throw new UOJProblemConfException("n_sample_tests can't be greater than n_ex_tests"); - } - - if (!isset($this->problem_extra_config['dont_download_sample'])) { - for ($num = 1; $num <= $n_sample_tests; $num++) { - $input_file_name = getUOJProblemExtraInputFileName($this->problem_conf, $num); - $output_file_name = getUOJProblemExtraOutputFileName($this->problem_conf, $num); - $zip_file->addFile("{$this->prepare_dir}/{$input_file_name}", "$input_file_name"); - if (!isset($this->problem_extra_config['dont_download_sample_output'])) { - $zip_file->addFile("{$this->prepare_dir}/{$output_file_name}", "$output_file_name"); - } - } - } - - $this->requirement[] = ['name' => 'answer', 'type' => 'source code', 'file_name' => 'answer.code']; - } - } else { - if (!isSuperUser($this->user)) { - throw new UOJProblemConfException("use_builtin_judger must be on."); - } else { - foreach ($this->allow_files as $file_name => $file_num) { - $this->copy_to_prepare($file_name); - } - $this->makefile_at_prepare(); - - $this->requirement[] = ['name' => 'answer', 'type' => 'source code', 'file_name' => 'answer.code']; - } - } - putUOJConf("{$this->prepare_dir}/problem.conf", $this->final_problem_conf); - - if (isset($this->allow_files['download']) && is_dir("{$this->upload_dir}/download")) { - $download_dir = "{$this->upload_dir}/download"; - foreach (FS::scandir_r($download_dir) as $file_name) { - if (is_file("{$download_dir}/{$file_name}")) { - $zip_file->addFile("{$download_dir}/{$file_name}", $file_name); - } - } - } - - $zip_file->close(); - - $orig_requirement = $this->problem->getSubmissionRequirement(); - if (!$orig_requirement) { - DB::update([ - "update problems", - "set", ["submission_requirement" => json_encode($this->requirement)], - "where", ["id" => $this->id] - ]); - } - - UOJSystemUpdate::updateProblemInternally($this->problem, [ - 'text' => 'sync', - 'requestor' => Auth::check() ? Auth::id() : null - ]); - } catch (Exception $e) { - $this->remove_prepare_folder(); - return $e->getMessage(); - } - - UOJLocalRun::exec(['rm', $this->data_dir, '-r']); - rename($this->prepare_dir, $this->data_dir); - - UOJLocalRun::execAnd([ - ['cd', '/var/uoj_data'], - ['zip', "{$this->id}.next.zip", $this->id, '-r', '-q'], - ['mv', "{$this->id}.next.zip", "{$this->id}.zip", '-f'], - ]); - - return ''; - } - - public function sync() { - return $this->lock(LOCK_EX, fn () => $this->_sync()); - } -} - -function dataSyncProblemData($problem, $user = null) { - return (new SyncProblemDataHandler($problem, $user))->sync(); -} - -function dataAddHackPoint($problem, $uploaded_input_file, $uploaded_output_file, $reason = null, $user = null) { - if ($reason === null) { - if (UOJHack::cur()) { - $reason = [ - 'rejudge' => '自动重测本题所有获得 100 分的提交记录', - 'hack_url' => HTML::url(UOJHack::cur()->getUri()) - ]; - } else { - $reason = []; - } - } - - return (new SyncProblemDataHandler($problem, $user))->addHackPoint($uploaded_input_file, $uploaded_output_file, $reason); -} - -function dataUpdateProblemConf($problem, $new_problem_conf) { - return (new SyncProblemDataHandler($problem))->updateProblemConf($new_problem_conf); -} diff --git a/web/app/models/FS.php b/web/app/models/FS.php index 5fda198..07d52ec 100644 --- a/web/app/models/FS.php +++ b/web/app/models/FS.php @@ -3,11 +3,11 @@ class FS { public static function scandir(string $directory, $cfg = []) { $cfg += [ - 'exclude_dots' => true + 'exclude_dots' => true ]; $entries = scandir($directory); if ($cfg['exclude_dots']) { - $entries = array_filter($entries, fn($name) => $name !== '.' && $name !== '..'); + $entries = array_values(array_filter($entries, fn ($name) => $name !== '.' && $name !== '..')); } return $entries; } @@ -27,17 +27,17 @@ class FS { /** * @param int $type lock type. can be either LOCK_SH or LOCK_EX - */ + */ public static function lock_file(string $path, int $type, callable $func) { $lock_fp = fopen($path, 'c'); - + if (!flock($lock_fp, $type | LOCK_NB)) { UOJLog::error("lock failed: {$path}"); return false; } - + $ret = $func(); - + flock($lock_fp, LOCK_UN | LOCK_NB); return $ret; @@ -46,7 +46,7 @@ class FS { public static function randomAvailableFileName($dir, $suffix = '') { do { $name = $dir . uojRandString(20) . $suffix; - } while (file_exists(UOJContext::storagePath().$name)); + } while (file_exists(UOJContext::storagePath() . $name)); return $name; } @@ -56,9 +56,18 @@ class FS { public static function randomAvailableSubmissionFileName() { $num = uojRand(1, 10000); - if (!file_exists(UOJContext::storagePath()."/submission/$num")) { - system("mkdir ".UOJContext::storagePath()."/submission/$num"); + if (!file_exists(UOJContext::storagePath() . "/submission/$num")) { + system("mkdir " . UOJContext::storagePath() . "/submission/$num"); } return static::randomAvailableFileName("/submission/$num/"); } + + public static function moveFilesInDir(string $src, string $dest) { + foreach (FS::scandir($src) as $name) { + if (!rename("{$src}/{$name}", "{$dest}/{$name}")) { + return false; + } + } + return true; + } } diff --git a/web/app/models/UOJProblem.php b/web/app/models/UOJProblem.php index bc592bc..cc97442 100644 --- a/web/app/models/UOJProblem.php +++ b/web/app/models/UOJProblem.php @@ -721,6 +721,33 @@ class UOJProblem { } return $conf->getNonTraditionalJudgeType(); } + + public function syncData($user = null) { + return (new UOJProblemDataSynchronizer($this, $user))->sync(); + } + + public function addHackPoint($uploaded_input_file, $uploaded_output_file, $reason = null, $user = null) { + if ($reason === null) { + if (UOJHack::cur()) { + $reason = [ + 'rejudge' => '自动重测本题所有获得100分的提交记录', + 'hack_url' => HTML::url(UOJHack::cur()->getUri()) + ]; + } else { + $reason = []; + } + } + + return (new UOJProblemDataSynchronizer($this, $user))->addHackPoint($uploaded_input_file, $uploaded_output_file, $reason); + } + + public function uploadDataViaZipFile($new_data_zip) { + return (new UOJProblemDataSynchronizer($this))->upload($new_data_zip); + } + + public function updateProblemConf($new_problem_conf) { + return (new UOJProblemDataSynchronizer($this))->updateProblemConf($new_problem_conf); + } } UOJProblem::$table_for_content = 'problems_contents'; diff --git a/web/app/models/UOJProblemConfigure.php b/web/app/models/UOJProblemConfigure.php index bfa9a87..6e528cd 100644 --- a/web/app/models/UOJProblemConfigure.php +++ b/web/app/models/UOJProblemConfigure.php @@ -357,7 +357,7 @@ class UOJProblemConfigure { } } - $err = dataUpdateProblemConf($this->problem->info, $conf); + $err = $this->problem->updateProblemConf($conf); if ($err) { UOJResponse::message('
\n" . HTML::escape(uojFilePreview("{$this->prepare_dir}/compiler_result.txt", 10000)) . "\n"); + } elseif ($rstype == 7) { + throw new Exception("$name : compile error. No comment"); + } else { + throw new Exception("$name : compile error. Compiler " . judgerCodeStr($rstype)); + } + } + + unlink("{$this->prepare_dir}/compiler_result.txt"); + + if (isset($config['path'])) { + rename("{$this->prepare_dir}/{$config['path']}/{$src['path']}", "{$this->prepare_dir}/{$src['path']}"); + rename("{$this->prepare_dir}/{$config['path']}/$name", "{$this->prepare_dir}/$name"); + } + } + + private function makefile_at_prepare() { + $include_path = UOJLocalRun::$judger_include_path; + + $res = UOJLocalRun::exec(['/usr/bin/make', "INCLUDE_PATH={$include_path}"], [ + ['in', '/dev/null'], + ['out', 'stderr'], + ['err', "{$this->prepare_dir}/makefile_result.txt"], + ['tl', 60], + ['ml', 512], + ['ol', 64], + ['type', 'compiler'], + ['work-path', $this->prepare_dir], + ['add-readable-raw', "{$include_path}/"] + ]); + $rstype = isset($res['rstype']) ? $res['rstype'] : 7; + + if ($rstype != 0 || $res['exit_code'] != 0) { + if ($rstype == 0) { + throw new Exception("Makefile : compile error
\n" . HTML::escape(uojFilePreview("{$this->prepare_dir}/makefile_result.txt", 10000)) . "\n"); + } elseif ($rstype == 7) { + throw new Exception("Makefile : compile error. No comment"); + } else { + throw new Exception("Makefile : compile error. Compiler " . judgerCodeStr($rstype)); + } + } + + unlink("{$this->prepare_dir}/makefile_result.txt"); + } + + private function _upload($new_data_zip) { + try { + // Clear upload dir + foreach (FS::scandir_r($this->upload_dir) as $file_name) { + unlink("{$this->upload_dir}/{$file_name}"); + } + + $zip = new ZipArchive(); + if ($zip->open($new_data_zip) !== true) { + throw new UOJUploadFailedException('压缩文件打开失败'); + } + if (!$zip->extractTo($this->upload_dir)) { + throw new UOJUploadFailedException('压缩文件解压失败'); + } + if (!$zip->close()) { + throw new UOJUploadFailedException('压缩文件关闭失败'); + } + + $files = FS::scandir($this->upload_dir); + + if (count($files) == 1 && is_dir("{$this->upload_dir}/{$files[0]}")) { + if (!FS::moveFilesInDir("{$this->upload_dir}/{$files[0]}", $this->upload_dir)) { + throw new UOJUploadFailedException('操作解压后的文件时发生错误'); + } + } + + return ''; + } catch (Exception $e) { + return $e->getMessage(); + } finally { + $this->remove_prepare_folder(); + } + } + public function upload($new_data_zip) { + return $this->lock(LOCK_EX, fn () => $this->_upload($new_data_zip)); + } + + public function _updateProblemConf($new_problem_conf) { + try { + putUOJConf("{$this->upload_dir}/problem.conf", $new_problem_conf); + + $this->_sync(); + return ''; + } catch (Exception $e) { + return $e->getMessage(); + } + } + public function updateProblemConf($new_problem_conf) { + return $this->lock(LOCK_EX, fn () => $this->_updateProblemConf($new_problem_conf)); + } + + private function _addHackPoint($uploaded_input_file, $uploaded_output_file, $reason) { + try { + switch ($this->problem->getExtraConfig('add_hack_as')) { + case 'test': + $key_num = 'n_tests'; + $msg = 'add new test'; + $gen_in_name = 'getUOJProblemInputFileName'; + $gen_out_name = 'getUOJProblemOutputFileName'; + break; + case 'ex_test': + $key_num = 'n_ex_tests'; + $msg = 'add new extra test'; + $gen_in_name = 'getUOJProblemExtraInputFileName'; + $gen_out_name = 'getUOJProblemExtraOutputFileName'; + break; + default: + return 'add hack to data failed: add_hack_as should be either "ex_test" or "test"'; + } + + $new_problem_conf = $this->problem->getProblemConfArray(); + if ($new_problem_conf == -1 || $new_problem_conf == -2) { + return $new_problem_conf; + } + $new_problem_conf[$key_num] = getUOJConfVal($new_problem_conf, $key_num, 0) + 1; + + putUOJConf("{$this->upload_dir}/problem.conf", $new_problem_conf); + + $new_input_name = $gen_in_name($new_problem_conf, $new_problem_conf[$key_num]); + $new_output_name = $gen_out_name($new_problem_conf, $new_problem_conf[$key_num]); + + if (!copy($uploaded_input_file, "{$this->upload_dir}/$new_input_name")) { + return "input file not found"; + } + if (!copy($uploaded_output_file, "{$this->upload_dir}/$new_output_name")) { + return "output file not found"; + } + } catch (Exception $e) { + return $e->getMessage(); + } + + $ret = $this->_sync(); + if ($ret !== '') { + return "hack successfully but sync failed: $ret"; + } + + if (isset($reason['hack_url'])) { + UOJSystemUpdate::updateProblem($this->problem, [ + 'text' => 'Hack 成功,自动添加数据', + 'url' => $reason['hack_url'] + ]); + } + UOJSubmission::rejudgeProblemAC($this->problem, [ + 'reason_text' => $reason['rejudge'], + 'requestor' => '' + ]); + return ''; + } + + public function addHackPoint($uploaded_input_file, $uploaded_output_file, $reason = []) { + return $this->lock(LOCK_EX, fn () => $this->_addHackPoint($uploaded_input_file, $uploaded_output_file, $reason)); + } + + public function fast_hackable_check() { + if (!$this->problem->info['hackable']) { + return; + } + if (!$this->check_conf_on('use_builtin_judger')) { + return; + } + + if ($this->check_conf_on('submit_answer')) { + throw new UOJProblemConfException("提交答案题不可 Hack,请先停用本题的 Hack 功能。"); + } else { + if (UOJLang::findSourceCode('std', $this->upload_dir) === false) { + throw new UOJProblemConfException("找不到本题的 std。请上传 std 代码文件,或停用本题的 Hack 功能。"); + } + if (UOJLang::findSourceCode('val', $this->upload_dir) === false) { + throw new UOJProblemConfException("找不到本题的 val。请上传 val 代码文件,或停用本题的 Hack 功能。"); + } + } + } + + private function _sync() { + try { + if (!$this->create_prepare_folder()) { + throw new UOJSyncFailedException('创建临时文件夹失败'); + } + + $this->requirement = []; + $this->problem_extra_config = $this->problem->getExtraConfig();; + if (!is_file("{$this->upload_dir}/problem.conf")) { + throw new UOJFileNotFoundException("problem.conf"); + } + + $this->problem_conf = getUOJConf("{$this->upload_dir}/problem.conf"); + $this->final_problem_conf = $this->problem_conf; + if ($this->problem_conf === -1) { + throw new UOJFileNotFoundException("problem.conf"); + } elseif ($this->problem_conf === -2) { + throw new UOJProblemConfException("syntax error: duplicate keys"); + } + + $this->allow_files = array_flip(FS::scandir($this->upload_dir)); + + $zip_file = new ZipArchive(); + if ($zip_file->open("{$this->prepare_dir}/download.zip", ZipArchive::CREATE) !== true) { + throw new Exception("download.zip : failed to create the zip file"); + } + + if (isset($this->allow_files['require']) && is_dir("{$this->upload_dir}/require")) { + $this->copy_to_prepare('require'); + } + + if (isset($this->allow_files['testlib.h']) && is_file("{$this->upload_dir}/testlib.h")) { + $this->copy_file_to_prepare('testlib.h'); + } + + $this->fast_hackable_check(); + + if ($this->check_conf_on('use_builtin_judger')) { + $n_tests = getUOJConfVal($this->problem_conf, 'n_tests', 10); + if (!validateUInt($n_tests) || $n_tests <= 0) { + throw new UOJProblemConfException("n_tests must be a positive integer"); + } + for ($num = 1; $num <= $n_tests; $num++) { + $input_file_name = getUOJProblemInputFileName($this->problem_conf, $num); + $output_file_name = getUOJProblemOutputFileName($this->problem_conf, $num); + + $this->copy_file_to_prepare($input_file_name); + $this->copy_file_to_prepare($output_file_name); + } + + if (!$this->check_conf_on('interaction_mode')) { + if (isset($this->problem_conf['use_builtin_checker'])) { + if (!preg_match('/^[a-zA-Z0-9_]{1,20}$/', $this->problem_conf['use_builtin_checker'])) { + throw new Exception("" . HTML::escape($this->problem_conf['use_builtin_checker']) . " is not a valid checker"); + } + } else { + $this->copy_source_code_to_prepare('chk'); + $this->compile_at_prepare('chk', ['need_include_header' => true]); + } + } + + if ($this->check_conf_on('submit_answer')) { + if (!isset($this->problem_extra_config['dont_download_input'])) { + for ($num = 1; $num <= $n_tests; $num++) { + $input_file_name = getUOJProblemInputFileName($this->problem_conf, $num); + $zip_file->addFile("{$this->prepare_dir}/$input_file_name", "$input_file_name"); + } + } + + $n_output_files = 0; + for ($num = 1; $num <= $n_tests; $num++) { + $output_file_id = getUOJConfVal($this->problem_conf, ["output_file_id_{$num}", "output_file_id"], "$num"); + if (!validateUInt($output_file_id) || $output_file_id < 0 || $output_file_id > $n_tests) { + throw new UOJProblemConfException("output_file_id/output_file_id_{$num} must be in [1, n_tests]"); + } + $n_output_files = max($n_output_files, $output_file_id); + } + for ($num = 1; $num <= $n_output_files; $num++) { + $output_file_name = getUOJProblemOutputFileName($this->problem_conf, $num); + $this->requirement[] = ['name' => "output$num", 'type' => 'text', 'file_name' => $output_file_name]; + } + } else { + $n_ex_tests = getUOJConfVal($this->problem_conf, 'n_ex_tests', 0); + if (!validateUInt($n_ex_tests) || $n_ex_tests < 0) { + throw new UOJProblemConfException('n_ex_tests must be a non-negative integer. Current value: ' . HTML::escape($n_ex_tests)); + } + + for ($num = 1; $num <= $n_ex_tests; $num++) { + $input_file_name = getUOJProblemExtraInputFileName($this->problem_conf, $num); + $output_file_name = getUOJProblemExtraOutputFileName($this->problem_conf, $num); + + $this->copy_file_to_prepare($input_file_name); + $this->copy_file_to_prepare($output_file_name); + } + + if ($this->problem->info['hackable']) { + $this->copy_source_code_to_prepare('std'); + if (isset($this->problem_conf['with_implementer']) && $this->problem_conf['with_implementer'] == 'on') { + $this->compile_at_prepare('std', [ + 'implementer' => 'implementer', + 'path' => 'require' + ]); + } else { + $this->compile_at_prepare('std'); + } + $this->copy_source_code_to_prepare('val'); + $this->compile_at_prepare('val', ['need_include_header' => true]); + } + + if ($this->check_conf_on('interaction_mode')) { + $this->copy_source_code_to_prepare('interactor'); + $this->compile_at_prepare('interactor', ['need_include_header' => true]); + } + + $n_sample_tests = getUOJConfVal($this->problem_conf, 'n_sample_tests', $n_tests); + if (!validateUInt($n_sample_tests) || $n_sample_tests < 0) { + throw new UOJProblemConfException('n_sample_tests must be a non-negative integer. Current value: ' . HTML::escape($n_sample_tests)); + } + if ($n_sample_tests > $n_ex_tests) { + throw new UOJProblemConfException("n_sample_tests can't be greater than n_ex_tests"); + } + + if (!isset($this->problem_extra_config['dont_download_sample'])) { + for ($num = 1; $num <= $n_sample_tests; $num++) { + $input_file_name = getUOJProblemExtraInputFileName($this->problem_conf, $num); + $output_file_name = getUOJProblemExtraOutputFileName($this->problem_conf, $num); + $zip_file->addFile("{$this->prepare_dir}/{$input_file_name}", "$input_file_name"); + if (!isset($this->problem_extra_config['dont_download_sample_output'])) { + $zip_file->addFile("{$this->prepare_dir}/{$output_file_name}", "$output_file_name"); + } + } + } + + $this->requirement[] = ['name' => 'answer', 'type' => 'source code', 'file_name' => 'answer.code']; + } + } else { + if ($this->user !== 'root' && !isSuperUser($this->user)) { + throw new UOJProblemConfException("use_builtin_judger must be on."); + } else { + foreach ($this->allow_files as $file_name => $file_num) { + $this->copy_to_prepare($file_name); + } + $this->makefile_at_prepare(); + + $this->requirement[] = ['name' => 'answer', 'type' => 'source code', 'file_name' => 'answer.code']; + } + } + putUOJConf("{$this->prepare_dir}/problem.conf", $this->final_problem_conf); + + if (isset($this->allow_files['download']) && is_dir("{$this->upload_dir}/download")) { + $download_dir = "{$this->upload_dir}/download"; + foreach (FS::scandir_r($download_dir) as $file_name) { + if (is_file("{$download_dir}/{$file_name}")) { + $zip_file->addFile("{$download_dir}/{$file_name}", $file_name); + } + } + } + + $zip_file->close(); + + $orig_requirement = $this->problem->getSubmissionRequirement(); + if (!$orig_requirement) { + DB::update([ + "update problems", + "set", ["submission_requirement" => json_encode($this->requirement)], + "where", ["id" => $this->id] + ]); + } + + UOJSystemUpdate::updateProblemInternally($this->problem, [ + 'text' => 'sync', + 'requestor' => Auth::check() ? Auth::id() : null + ]); + } catch (Exception $e) { + $this->remove_prepare_folder(); + return $e->getMessage(); + } + + UOJLocalRun::exec(['rm', $this->data_dir, '-r']); + rename($this->prepare_dir, $this->data_dir); + + UOJLocalRun::execAnd([ + ['cd', '/var/uoj_data'], + ['zip', "{$this->id}.next.zip", $this->id, '-r', '-q'], + ['mv', "{$this->id}.next.zip", "{$this->id}.zip", '-f'], + ]); + + return ''; + } + + public function sync() { + return $this->lock(LOCK_EX, fn () => $this->_sync()); + } +}