diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22f2f79f1..938956a87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: PHP Lint & Codeception on: [ push, pull_request ] jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93fd25c06..2951a7f63 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,3 +4,4 @@ - [이슈 및 PR 작성 방법](https://rhymix.org/manual/contrib/github) - [코딩 규칙](https://rhymix.org/manual/contrib/coding-standards) +- [GPL: 개발자, 디자이너, 사용자 등의 권리와 의무](https://rhymix.org/manual/contrib/license) diff --git a/classes/context/Context.class.php b/classes/context/Context.class.php index 6b6d23905..1d5dcc37c 100644 --- a/classes/context/Context.class.php +++ b/classes/context/Context.class.php @@ -773,9 +773,10 @@ class Context } if (count($vars)) { - $title = trim(trim(preg_replace_callback('/\\$(\w+)/', function($matches) use($vars) { + $title = trim(preg_replace_callback('/\\$(\w+)/', function($matches) use($vars) { return isset($vars[strtolower($matches[1])]) ? $vars[strtolower($matches[1])] : $matches[0]; - }, $title), ' -')); + }, $title)); + $title = preg_replace('/(-\s+)+-/', '-', trim($title, ' -')); } self::$_instance->browser_title = $title; } @@ -1368,7 +1369,9 @@ class Context $file['tmp_name'] = $val['tmp_name'][$i]; $file['error'] = $val['error'][$i]; $file['size'] = $val['size'][$i]; - $files[] = $file; + + $subkey = (is_int($i) || ctype_digit($i)) ? intval($i) : preg_replace('/[^a-z0-9:+=_-]/i', '', (string)$i); + $files[$subkey] = $file; } if(count($files)) { diff --git a/classes/display/HTMLDisplayHandler.php b/classes/display/HTMLDisplayHandler.php index ad1ab48c2..52e5f697d 100644 --- a/classes/display/HTMLDisplayHandler.php +++ b/classes/display/HTMLDisplayHandler.php @@ -573,19 +573,32 @@ class HTMLDisplayHandler foreach ($document_files as $file) { - if ($file->isvalid !== 'Y' || !preg_match('/\.(?:bmp|gif|jpe?g|png|webp)$/i', $file->uploaded_filename)) + if ($file->isvalid !== 'Y' || !preg_match('/\.(?:bmp|gif|jpe?g|png|webp|mp4)$/i', $file->uploaded_filename)) { continue; } - list($width, $height) = @getimagesize($file->uploaded_filename); - if ($width < 100 && $height < 100) + if (str_starts_with($file->mime_type, 'video/')) { - continue; + if ($file->thumbnail_filename) + { + list($width, $height) = @getimagesize($file->thumbnail_filename); + if ($width >= 100 || $height >= 100) + { + $document_images[] = array('filepath' => $file->thumbnail_filename, 'width' => $width, 'height' => $height); + break; + } + } + } + else + { + list($width, $height) = @getimagesize($file->uploaded_filename); + if ($width >= 100 || $height >= 100) + { + $document_images[] = array('filepath' => $file->uploaded_filename, 'width' => $width, 'height' => $height); + break; + } } - - $document_images[] = array('filepath' => $file->uploaded_filename, 'width' => $width, 'height' => $height); - break; } } Rhymix\Framework\Cache::set("seo:document_images:$document_srl", $document_images); diff --git a/classes/display/JSONDisplayHandler.php b/classes/display/JSONDisplayHandler.php index 72dc6f02d..eb09bad3a 100644 --- a/classes/display/JSONDisplayHandler.php +++ b/classes/display/JSONDisplayHandler.php @@ -15,7 +15,16 @@ class JSONDisplayHandler $variables['message'] = $oModule->getMessage(); self::_convertCompat($variables, Context::getRequestMethod()); - return json_encode($variables) . "\n"; + $result = json_encode($variables) . "\n"; + if (json_last_error() != \JSON_ERROR_NONE) + { + trigger_error('JSON encoding error: ' . json_last_error_msg(), E_USER_WARNING); + return json_encode([ + 'error' => -1, + 'message' => 'JSON encoding error', + ]) . "\n"; + } + return $result; } /** diff --git a/classes/module/ModuleHandler.class.php b/classes/module/ModuleHandler.class.php index b7c893b95..7d518befa 100644 --- a/classes/module/ModuleHandler.class.php +++ b/classes/module/ModuleHandler.class.php @@ -703,7 +703,7 @@ class ModuleHandler extends Handler $procResult = $oModule->proc(); $methodList = array('XMLRPC' => 1, 'JSON' => 1, 'JS_CALLBACK' => 1); - if(!$oModule->stop_proc && !isset($methodList[Context::getRequestMethod()]) && !isset($_SERVER['HTTP_X_AJAX_TARGET']) && !isset($_POST['_rx_ajax_form'])) + if(!$oModule->stop_proc && !isset($methodList[Context::getRequestMethod()])) { $error = $oModule->getError(); $message = $oModule->getMessage(); @@ -1014,28 +1014,6 @@ class ModuleHandler extends Handler $methodList = array('XMLRPC' => 1, 'JSON' => 1, 'JS_CALLBACK' => 1); if(!isset($methodList[Context::getRequestMethod()])) { - // Handle iframe form submissions. - $ajax_form_target = strval($_SERVER['HTTP_X_AJAX_TARGET'] ?? ($_POST['_rx_ajax_form'] ?? '')); - if($ajax_form_target !== '' && starts_with('_rx_temp_iframe_', $ajax_form_target)) - { - $data = []; - if ($this->error) - { - $data['error'] = -1; - $data['message'] = lang($this->error); - } - else - { - $data['error'] = $oModule->error; - $data['message'] = lang($oModule->message); - } - $data = array_merge($data, $oModule->getVariables()); - - ob_end_clean(); - echo sprintf('
', json_encode($ajax_form_target), json_encode($data)); - return; - } - // Handle redirects. if($oModule->getRedirectUrl()) { @@ -1122,7 +1100,7 @@ class ModuleHandler extends Handler if($layout_info) { // Input extra_vars into $layout_info - if($layout_info->extra_var_count) + if(isset($layout_info->extra_var_count) && $layout_info->extra_var_count) { foreach($layout_info->extra_var as $var_id => $val) @@ -1138,7 +1116,7 @@ class ModuleHandler extends Handler } } // Set menus into context - if($layout_info->menu_count) + if(isset($layout_info->menu_count) && $layout_info->menu_count) { $oMenuAdminController = getAdminController('menu'); $homeMenuCacheFile = null; diff --git a/classes/object/Object.class.php b/classes/object/Object.class.php index 728e0acb3..6c574b4be 100644 --- a/classes/object/Object.class.php +++ b/classes/object/Object.class.php @@ -210,8 +210,7 @@ class BaseObject */ public function add($key, $val) { - $this->variables[$key] = $val; - return $this; + return $this->set($key, $val); } /** diff --git a/common/composer.json b/common/composer.json index 6eec88035..73bd37b07 100644 --- a/common/composer.json +++ b/common/composer.json @@ -33,6 +33,7 @@ "ezyang/htmlpurifier": "4.16.*", "google/auth": "1.26.*", "guzzlehttp/guzzle": "7.8.*", + "guzzlehttp/psr7": "2.6.*", "jbbcode/jbbcode": "1.4.*", "leafo/lessphp": "dev-master", "league/html-to-markdown": "5.1.*", diff --git a/common/framework/DB.php b/common/framework/DB.php index 03d3b0a85..62d6d31ef 100644 --- a/common/framework/DB.php +++ b/common/framework/DB.php @@ -463,7 +463,9 @@ class DB if ($this->isError()) { - return $this->getError(); + $output = $this->getError(); + $output->add('_count', $query_string); + return $output; } else { @@ -474,11 +476,13 @@ class DB catch (Exceptions\DBError $e) { $output = $this->setError(-1, $e->getMessage()); + $output->add('_count', $query_string); return $output; } catch (\PDOException $e) { $output = $this->setError(-1, $e->getMessage()); + $output->add('_count', $query_string); return $output; } diff --git a/common/framework/Mail.php b/common/framework/Mail.php index 7502c6df9..bcf9b62a3 100644 --- a/common/framework/Mail.php +++ b/common/framework/Mail.php @@ -560,6 +560,13 @@ class Mail */ public function send(): bool { + // If queue is enabled, send asynchronously. + if (config('queue.enabled') && !defined('RXQUEUE_CRON')) + { + Queue::addTask(self::class . '::' . 'sendAsync', $this); + return true; + } + // Get caller information. $backtrace = debug_backtrace(0); if(count($backtrace) && isset($backtrace[0]['file'])) @@ -600,6 +607,17 @@ class Mail return $this->sent; } + /** + * Send an email asynchronously (for Queue integration). + * + * @param self $mail + * @return void + */ + public static function sendAsync(self $mail): void + { + $mail->send(); + } + /** * Check if the message was sent. * diff --git a/common/framework/Push.php b/common/framework/Push.php index 547a2295e..bc37cd2c7 100644 --- a/common/framework/Push.php +++ b/common/framework/Push.php @@ -402,6 +402,13 @@ class Push */ public function send(): bool { + // If queue is enabled, send asynchronously. + if (config('queue.enabled') && !defined('RXQUEUE_CRON')) + { + Queue::addTask(self::class . '::' . 'sendAsync', $this); + return true; + } + // Get caller information. $backtrace = debug_backtrace(0); if(count($backtrace) && isset($backtrace[0]['file'])) @@ -464,6 +471,17 @@ class Push return $this->sent > 0 ? true : false; } + /** + * Send asynchronously (for Queue integration). + * + * @param self $sms + * @return void + */ + public static function sendAsync(self $push): void + { + $push->send(); + } + /** * Get the device token * diff --git a/common/framework/Queue.php b/common/framework/Queue.php new file mode 100644 index 000000000..56410e13f --- /dev/null +++ b/common/framework/Queue.php @@ -0,0 +1,302 @@ + $class_name::getName(), + 'required' => $class_name::getRequiredConfig(), + 'optional' => $class_name::getOptionalConfig(), + ]; + } + } + foreach (self::$_drivers as $driver_name => $driver) + { + if ($driver->isSupported()) + { + $result[$driver_name] = [ + 'name' => $driver->getName(), + 'required' => $driver->getRequiredConfig(), + 'optional' => $driver->getOptionalConfig(), + ]; + } + } + ksort($result); + return $result; + } + + /** + * Add a task. + * + * The handler can be in one of the following formats: + * - Global function, e.g. myHandler + * - ClassName::staticMethodName + * - ClassName::getInstance()->methodName + * - new ClassName()->methodName + * + * Once identified and/or instantiated, the handler will be passed $args + * and $options, in that order. Each of them must be a single object. + * + * It is strongly recommended that you write a dedicated method to handle + * queued tasks, rather than reusing an existing method with a potentially + * incompatible structure. If you must to call an existing method, + * you should consider writing a wrapper. + * + * Any value returned by the handler will be discarded. If you throw an + * exception, it may be logged, but it will not cause a fatal error. + * + * @param string $handler + * @param ?object $args + * @param ?object $options + * @return int + */ + public static function addTask(string $handler, ?object $args = null, ?object $options = null): int + { + $driver_name = config('queue.driver'); + if (!$driver_name) + { + throw new Exceptions\FeatureDisabled('Queue not configured'); + } + + $driver = self::getDriver($driver_name); + if (!$driver) + { + throw new Exceptions\FeatureDisabled('Queue not configured'); + } + + return $driver->addTask($handler, $args, $options); + } + + /** + * Get the first task to execute immediately. + * + * If no tasks are pending, this method will return null. + * Detailed scheduling of tasks will be handled by each driver. + * + * @param int $blocking + * @return ?object + */ + public static function getTask(int $blocking = 0): ?object + { + $driver_name = config('queue.driver'); + if (!$driver_name) + { + throw new Exceptions\FeatureDisabled('Queue not configured'); + } + + $driver = self::getDriver($driver_name); + if (!$driver) + { + throw new Exceptions\FeatureDisabled('Queue not configured'); + } + + return $driver->getTask($blocking); + } + + /** + * Check the process key. + * + * @param string $key + * @return bool + */ + public static function checkKey(string $key): bool + { + return config('queue.key') === $key; + } + + /** + * Process the queue. + * + * This will usually be called by a separate script, run every minute + * through an external scheduler such as crontab or systemd. + * + * If you are on a shared hosting service, you may also call a URL + * using a "web cron" service provider. + * + * @param int $timeout + * @return void + */ + public static function process(int $timeout): void + { + // This part will run in a loop until timeout. + $process_start_time = microtime(true); + while (true) + { + // Get a task from the driver. + $loop_start_time = microtime(true); + $task = self::getTask(1); + + // Wait 1 second and loop back. + if ($task) + { + // Find the handler for the task. + $task->handler = trim($task->handler, '\\()'); + $handler = null; + try + { + if (preg_match('/^(?:\\\\)?([\\\\\\w]+)::(\\w+)$/', $task->handler, $matches)) + { + $class_name = '\\' . $matches[1]; + $method_name = $matches[2]; + if (class_exists($class_name) && method_exists($class_name, $method_name)) + { + $handler = [$class_name, $method_name]; + } + else + { + error_log('RxQueue: task handler not found: ' . $task->handler); + } + } + elseif (preg_match('/^(?:\\\\)?([\\\\\\w]+)::(\\w+)(?:\(\))?->(\\w+)$/', $task->handler, $matches)) + { + $class_name = '\\' . $matches[1]; + $initializer_name = $matches[2]; + $method_name = $matches[3]; + if (class_exists($class_name) && method_exists($class_name, $initializer_name)) + { + $obj = $class_name::$initializer_name(); + if (method_exists($obj, $method_name)) + { + $handler = [$obj, $method_name]; + } + else + { + error_log('RxQueue: task handler not found: ' . $task->handler); + } + } + else + { + error_log('RxQueue: task handler not found: ' . $task->handler); + } + } + elseif (preg_match('/^new (?:\\\\)?([\\\\\\w]+)(?:\(\))?->(\\w+)$/', $task->handler, $matches)) + { + $class_name = '\\' . $matches[1]; + $method_name = $matches[2]; + if (class_exists($class_name) && method_exists($class_name, $method_name)) + { + $obj = new $class_name(); + $handler = [$obj, $method_name]; + } + else + { + error_log('RxQueue: task handler not found: ' . $task->handler); + } + } + else + { + if (function_exists('\\' . $task->handler)) + { + $handler = '\\' . $task->handler; + } + else + { + error_log('RxQueue: task handler not found: ' . $task->handler); + } + } + } + catch (\Throwable $th) + { + error_log(vsprintf('RxQueue: task handler %s could not be accessed: %s in %s:%d', [ + $task->handler, + get_class($th), + $th->getFile(), + $th->getLine(), + ])); + } + + // Call the handler. + try + { + if ($handler) + { + call_user_func($handler, $task->args, $task->options); + } + } + catch (\Throwable $th) + { + error_log(vsprintf('RxQueue: task handler %s threw %s in %s:%d', [ + $task->handler, + get_class($th), + $th->getFile(), + $th->getLine(), + ])); + } + } + + // If the timeout is imminent, break the loop. + $process_elapsed_time = microtime(true) - $process_start_time; + if ($process_elapsed_time > $timeout - 2) + { + break; + } + + // If there was no task, wait 1 second to make sure that the loop isn't too tight. + $loop_elapsed_time = microtime(true) - $loop_start_time; + if (!$task && $loop_elapsed_time < 1) + { + usleep(intval((1 - $loop_elapsed_time) * 1000000)); + } + } + } +} diff --git a/common/framework/SMS.php b/common/framework/SMS.php index 0d347bc83..4bb65fd60 100644 --- a/common/framework/SMS.php +++ b/common/framework/SMS.php @@ -511,6 +511,13 @@ class SMS */ public function send(): bool { + // If queue is enabled, send asynchronously. + if (config('queue.enabled') && !defined('RXQUEUE_CRON')) + { + Queue::addTask(self::class . '::' . 'sendAsync', $this); + return true; + } + // Get caller information. $backtrace = debug_backtrace(0); if(count($backtrace) && isset($backtrace[0]['file'])) @@ -566,6 +573,17 @@ class SMS return $this->sent; } + /** + * Send an SMS asynchronously (for Queue integration). + * + * @param self $sms + * @return void + */ + public static function sendAsync(self $sms): void + { + $sms->send(); + } + /** * Check if the message was sent. * diff --git a/common/framework/UA.php b/common/framework/UA.php index 136a44ed5..14e14b87b 100644 --- a/common/framework/UA.php +++ b/common/framework/UA.php @@ -171,7 +171,7 @@ class UA } // Look for common search engine names and the 'bot' keyword. - if (preg_match('/bot|spider|crawler|archiver|wget|curl|php|slurp|wordpress|facebook|teoma|yeti|daum|apachebench|mediapartners-google|[(<+]https?:|@/i', $ua)) + if (preg_match('/bot|spider|crawler|archiver|wget|curl|php|slurp|wordpress|facebook|external(agent|fetcher)|teoma|yeti|daum|apachebench|googleother|mediapartners-google|[(<+]https?:|@/i', $ua)) { return self::$_robot_cache[$ua] = true; } diff --git a/common/framework/drivers/QueueInterface.php b/common/framework/drivers/QueueInterface.php new file mode 100644 index 000000000..71d71a452 --- /dev/null +++ b/common/framework/drivers/QueueInterface.php @@ -0,0 +1,71 @@ +prepare('INSERT INTO task_queue (handler, args, options) VALUES (?, ?, ?)'); + $result = $stmt->execute([$handler, serialize($args), serialize($options)]); + return $result ? $oDB->getInsertID() : 0; + } + + /** + * Get the first task. + * + * @param int $blocking + * @return ?object + */ + public function getTask(int $blocking = 0): ?object + { + $oDB = RFDB::getInstance(); + $oDB->beginTransaction(); + $stmt = $oDB->query('SELECT * FROM task_queue ORDER BY id LIMIT 1 FOR UPDATE'); + $result = $stmt->fetchObject(); + $stmt->closeCursor(); + + if ($result) + { + $stmt = $oDB->prepare('DELETE FROM task_queue WHERE id = ?'); + $stmt->execute([$result->id]); + $oDB->commit(); + + $result->args = unserialize($result->args); + $result->options = unserialize($result->options); + return $result; + } + else + { + $oDB->commit(); + return null; + } + } +} diff --git a/common/framework/drivers/queue/dummy.php b/common/framework/drivers/queue/dummy.php new file mode 100644 index 000000000..0abb74792 --- /dev/null +++ b/common/framework/drivers/queue/dummy.php @@ -0,0 +1,119 @@ +_dummy_queue = (object)[ + 'handler' => $handler, + 'args' => $args, + 'options' => $options, + ]; + return 0; + } + + /** + * Get the first task. + * + * @param int $blocking + * @return ?object + */ + public function getTask(int $blocking = 0): ?object + { + $result = $this->_dummy_queue; + $this->_dummy_queue = null; + return $result; + } +} diff --git a/common/framework/drivers/queue/redis.php b/common/framework/drivers/queue/redis.php new file mode 100644 index 000000000..58c122184 --- /dev/null +++ b/common/framework/drivers/queue/redis.php @@ -0,0 +1,197 @@ +connect($config['host'], $config['port'] ?? 6379); + if (isset($config['user']) || isset($config['pass'])) + { + $auth = []; + if (isset($config['user']) && $config['user']) $auth[] = $config['user']; + if (isset($config['pass']) && $config['pass']) $auth[] = $config['pass']; + $test->auth(count($auth) > 1 ? $auth : $auth[0]); + } + if (isset($config['dbnum'])) + { + $test->select(intval($config['dbnum'])); + } + return true; + } + catch (\Throwable $th) + { + return false; + } + } + + /** + * Constructor. + * + * @param array $config + */ + public function __construct(array $config) + { + try + { + $this->_conn = new \Redis; + $this->_conn->connect($config['host'], $config['port'] ?? 6379); + if (isset($config['user']) || isset($config['pass'])) + { + $auth = []; + if (isset($config['user']) && $config['user']) $auth[] = $config['user']; + if (isset($config['pass']) && $config['pass']) $auth[] = $config['pass']; + $this->_conn->auth(count($auth) > 1 ? $auth : $auth[0]); + } + if (isset($config['dbnum'])) + { + $this->_conn->select(intval($config['dbnum'])); + } + $this->_key = 'rxQueue_' . substr(hash_hmac('sha1', 'rxQueue_', config('crypto.authentication_key')), 0, 24); + } + catch (\RedisException $e) + { + $this->_conn = null; + } + } + + /** + * Add a task. + * + * @param string $handler + * @param ?object $args + * @param ?object $options + * @return int + */ + public function addTask(string $handler, ?object $args = null, ?object $options = null): int + { + $value = serialize((object)[ + 'handler' => $handler, + 'args' => $args, + 'options' => $options, + ]); + + if ($this->_conn) + { + $result = $this->_conn->rPush($this->_key, $value); + return intval($result); + } + else + { + return 0; + } + } + + /** + * Get the first task. + * + * @param int $blocking + * @return ?object + */ + public function getTask(int $blocking = 0): ?object + { + if ($this->_conn) + { + if ($blocking > 0) + { + $result = $this->_conn->blpop($this->_key, $blocking); + if (is_array($result) && isset($result[1])) + { + return unserialize($result[1]); + } + else + { + return null; + } + } + else + { + $result = $this->_conn->lpop($this->_key); + if ($result) + { + return unserialize($result); + } + else + { + return null; + } + } + } + else + { + return null; + } + } +} diff --git a/common/framework/drivers/sms/solapi.php b/common/framework/drivers/sms/solapi.php index 9cef5dbd7..b87f8e6cf 100644 --- a/common/framework/drivers/sms/solapi.php +++ b/common/framework/drivers/sms/solapi.php @@ -132,6 +132,11 @@ class SolAPI extends Base implements \Rhymix\Framework\Drivers\SMSInterface } } + foreach ($original->getExtraVars() as $key => $value) + { + $options->$key = $value; + } + $data['messages'][] = $options; } } diff --git a/common/framework/filters/FilenameFilter.php b/common/framework/filters/FilenameFilter.php index a07b997b0..228f9227c 100644 --- a/common/framework/filters/FilenameFilter.php +++ b/common/framework/filters/FilenameFilter.php @@ -15,6 +15,9 @@ class FilenameFilter */ public static function clean(string $filename): string { + // Clean up unnecessary encodings. + $filename = strtr($filename, ['&' => '&', ''' => "'"]); + // Replace dangerous characters with safe alternatives, maintaining meaning as much as possible. $illegal = array('\\', '/', '<', '>', '{', '}', ':', ';', '|', '"', '~', '`', '$', '%', '^', '*', '?'); $replace = array('', '', '(', ')', '(', ')', '_', ',', '_', '', '_', '\'', '_', '_', '_', '', ''); @@ -31,9 +34,6 @@ class FilenameFilter $filename = preg_replace('/__+/', '_', $filename); $filename = preg_replace('/\.\.+/', '.', $filename); - // Clean up unnecessary encodings. - $filename = strtr($filename, array('&' => '&')); - // Change .php files to .phps to make them non-executable. if (strtolower(substr($filename, strlen($filename) - 4)) === '.php') { diff --git a/common/js/plugins/jquery.fileupload/js/main.js b/common/js/plugins/jquery.fileupload/js/main.js index 645f115ef..8acdb7b53 100644 --- a/common/js/plugins/jquery.fileupload/js/main.js +++ b/common/js/plugins/jquery.fileupload/js/main.js @@ -353,12 +353,12 @@ unselectNonImageFiles: function() {}, generateHtml: function($container, file) { - var filename = String(file.source_filename); + var filename = String(file.source_filename).escape(); var html = ''; var data = $container.data(); if (filename.match(/\.(gif|jpe?g|png|webp)$/i)) { - html = '%s account and run crontab -e to paste the following content into your crontab. (DO NOT run it as root!)%s directory in the example should be replaced with a path where logs can be recorded.';
+$lang->msg_queue_instructions['crontab2'] = 'If you change the calling interval below, the crontab interval must be adjusted accordingly.';
+$lang->msg_queue_instructions['webcron'] = 'Configure an external cron service to make a GET request to the following URL every minute, or following the interval set below./etc/systemd/system/rhymix-queue.service';
+$lang->msg_queue_instructions['systemd2'] = 'Put the following content in /etc/systemd/system/rhymix-queue.timer';
+$lang->msg_queue_instructions['systemd3'] = 'Execute the following commands to enable the timer, and monitor your journal to make sure that it is operating as scheduled.';
+$lang->msg_queue_driver_not_found = 'The selected task queue driver is not supported on this server.';
+$lang->msg_queue_driver_not_usable = 'The selected task queue driver failed to initialize using the configuration values you entered.';
+$lang->msg_queue_driver_cannot_be_dummy = 'In otder to use the task queue, you must select a driver other than "Not use"';
+$lang->msg_queue_invalid_config = 'Missing or invalid configuration for the selected queue driver.';
+$lang->msg_queue_invalid_interval = 'The calling interval must be between 1 and 10 minutes.';
+$lang->msg_queue_invalid_process_count = 'The process count must be between 1 and 10.';
+$lang->msg_queue_invalid_key = 'The webcron auth key must be at least 16 characters long, and only consist of alphanumeric characters.';
$lang->autoinstall = 'EasyInstall';
$lang->last_week = 'Last Week';
$lang->this_week = 'This Week';
diff --git a/modules/admin/lang/ko.php b/modules/admin/lang/ko.php
index 2f754dac0..7fd335612 100644
--- a/modules/admin/lang/ko.php
+++ b/modules/admin/lang/ko.php
@@ -7,6 +7,7 @@ $lang->subtitle_notification = '알림 설정';
$lang->subtitle_security = '보안 설정';
$lang->subtitle_advanced = '고급 설정';
$lang->subtitle_debug = '디버그 설정';
+$lang->subtitle_queue = '비동기 작업';
$lang->subtitle_seo = 'SEO 설정';
$lang->subtitle_etc = '기타';
$lang->current_state = '현황';
@@ -264,7 +265,7 @@ $lang->about_seo_main_title = '사이트 메인 화면에 표시되는 제목
$lang->seo_subpage_title = '서브페이지 제목';
$lang->about_seo_subpage_title = '문서 목록, 페이지 등 주요 메뉴를 방문하면 표시되는 제목 형태입니다. 위의 변수들과 함께 $PAGE (페이지)도 사용할 수 있습니다.';
$lang->seo_document_title = '개별 문서 페이지 제목';
-$lang->about_seo_document_title = '게시물을 읽는 화면에서 표시되는 제목 형태입니다. 위에 변수들과 함께 $DOCUMENT_TITLE (문서 제목)도 사용할 수 있습니다.';
+$lang->about_seo_document_title = '게시물을 읽는 화면에서 표시되는 제목 형태입니다. 위에 변수들과 함께 $CATEGORY (카테고리명), $DOCUMENT_TITLE (문서 제목)도 사용할 수 있습니다.';
$lang->site_meta_keywords = 'SEO 키워드';
$lang->about_site_meta_keywords = '별도의 키워드를 지정하지 않은 페이지에서는 이 키워드 목록이 표시됩니다.';
$lang->site_meta_description = 'SEO 설명';
@@ -278,6 +279,36 @@ $lang->og_extract_images_fallback = '사이트 대표 이미지 사용';
$lang->og_extract_hashtags = '본문에서 해시태그 추출';
$lang->og_use_nick_name = '글 작성자 이름 표시';
$lang->og_use_timestamps = '글 작성/수정 시각 표시';
+$lang->cmd_queue_description = '메일 발송, 푸시알림 등 시간이 오래 걸리거나 외부 서비스와 연동하는 작업을 비동기 처리하여 응답 속도를 개선합니다.crontab -e 명령을 실행한 후, 아래의 내용을 붙여넣으십시오. (root 권한으로 실행하지 마십시오.)%s 디렉토리는 로그를 기록할 수 있는 경로로 변경하여 사용하십시오.';
+$lang->msg_queue_instructions['crontab2'] = '스크립트 호출 간격을 변경할 경우, 설정에 맞추어 crontab 실행 간격도 조절하여야 합니다.';
+$lang->msg_queue_instructions['webcron'] = '아래의 URL을 1분 간격 또는 아래에서 설정한 호출 간격에 맞추어 GET으로 호출하도록 합니다./etc/systemd/system/rhymix-queue.service 파일에 아래와 같은 내용을 넣습니다.';
+$lang->msg_queue_instructions['systemd2'] = '/etc/systemd/system/rhymix-queue.timer 파일에 아래와 같은 내용을 넣습니다.';
+$lang->msg_queue_instructions['systemd3'] = '아래의 명령을 실행하여 타이머를 활성화하고, 정상 작동하는지 모니터링하십시오.';
+$lang->msg_queue_driver_not_found = '이 서버에서 지원하지 않는 비동기 드라이버입니다.';
+$lang->msg_queue_driver_not_usable = '입력하신 정보로 비동기 드라이버와 연동하는 데 실패했습니다. 드라이버 설정을 확인해 주십시오.';
+$lang->msg_queue_driver_cannot_be_dummy = '비동기 작업을 사용하려면 "미사용" 이외의 드라이버를 선택해야 합니다.';
+$lang->msg_queue_invalid_config = '비동기 드라이버의 필수 설정이 누락되었습니다.';
+$lang->msg_queue_invalid_interval = '호출 간격은 1~10분 이내여야 합니다.';
+$lang->msg_queue_invalid_process_count = '프로세스 갯수는 1~10개 이내여야 합니다.';
+$lang->msg_queue_invalid_key = '웹크론 인증키는 16자 이상으로, 알파벳 대소문자와 숫자만으로 이루어져야 합니다.';
$lang->autoinstall = '쉬운 설치';
$lang->last_week = '지난주';
$lang->this_week = '이번주';
diff --git a/modules/admin/tpl/config_header.html b/modules/admin/tpl/config_header.html
index a8e7e7a1c..84e8b3a25 100644
--- a/modules/admin/tpl/config_header.html
+++ b/modules/admin/tpl/config_header.html
@@ -12,5 +12,6 @@
+ {$lang->cmd_comment_vote_user} + + {$lang->cmd_vote}{$comment->get('voted_count')} {$lang->cmd_vote}{$comment->get('voted_count')} + + {$lang->cmd_vote_down}{$comment->get('blamed_count')} {$lang->cmd_vote_down}{$comment->get('blamed_count')} + + {$lang->cmd_reply} + {$lang->cmd_modify} {$lang->cmd_delete} {$lang->cmd_comment_do} diff --git a/modules/board/skins/xedition/_read.html b/modules/board/skins/xedition/_read.html index 385f19b40..29fb9de98 100644 --- a/modules/board/skins/xedition/_read.html +++ b/modules/board/skins/xedition/_read.html @@ -83,11 +83,11 @@
{$lang->about_customize_bottom_list}
@@ -289,10 +289,18 @@{$lang->about_comment_length_limit}
{$lang->about_inline_data_url_limit}
+{$lang->about_consultation}
{$lang->about_comment_page_count}
{$lang->about_max_thread_depth}
+