diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 938956a87..bed5e2e3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,11 +2,11 @@ name: PHP Lint & Codeception on: [ push, pull_request ] jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - php: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3' ] + php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] name: PHP ${{ matrix.php }} steps: diff --git a/README.md b/README.md index b85c5ca17..b892e26e1 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Rhymix는 개발자와 사용자가 서로의 권리와 책임을 존중하는 ### 설치 환경 -Rhymix를 사용하려면 PHP 7.2.5 이상, MySQL 또는 MariaDB가 필요합니다. +Rhymix를 사용하려면 PHP 7.4 이상, MySQL 또는 MariaDB가 필요합니다. 자세한 설치 환경은 [매뉴얼](https://rhymix.org/manual/introduction/requirements)을 참고하십시오. ### 개발 참여 @@ -108,7 +108,7 @@ This requires the most convenience for the average user over any other CMS. ### Installation Environment -Rhymix requires PHP 7.2.5 or higher, and MySQL or MariaDB. +Rhymix requires PHP 7.4 or higher, and MySQL or MariaDB. Please see the [online manual](https://rhymix.org/manual/introduction/requirements) for more information on server requirements. ### Participation in Development diff --git a/classes/context/Context.class.php b/classes/context/Context.class.php index 1d5dcc37c..e1edbf63c 100644 --- a/classes/context/Context.class.php +++ b/classes/context/Context.class.php @@ -2879,14 +2879,10 @@ class Context { return isset(self::$_instance->meta_tags[$name]) ? self::$_instance->meta_tags[$name]['content'] : null; } - - $ret = array(); - foreach(self::$_instance->meta_tags as $name => $content) + else { - $ret[] = array('name' => $name, 'is_http_equiv' => $content['is_http_equiv'], 'content' => escape($content['content'], false)); + return array_values(self::$_instance->meta_tags); } - - return $ret; } /** @@ -2894,14 +2890,17 @@ class Context * * @param string $name name of meta tag * @param string $content content of meta tag - * @param mixed $is_http_equiv value of http_equiv + * @param bool $is_http_equiv + * @param bool $is_before_title * @return void */ - public static function addMetaTag($name, $content, $is_http_equiv = false) + public static function addMetaTag($name, $content, $is_http_equiv = false, $is_before_title = true) { self::$_instance->meta_tags[$name] = array( + 'name' => $name, + 'content' => escape(self::replaceUserLang($content, true), false), 'is_http_equiv' => (bool)$is_http_equiv, - 'content' => self::replaceUserLang($content, true), + 'is_before_title' => (bool)$is_before_title, ); } diff --git a/classes/display/HTMLDisplayHandler.php b/classes/display/HTMLDisplayHandler.php index 52e5f697d..c851b09c7 100644 --- a/classes/display/HTMLDisplayHandler.php +++ b/classes/display/HTMLDisplayHandler.php @@ -138,7 +138,7 @@ class HTMLDisplayHandler { // handle separately if the layout is faceoff - if($layout_info && $layout_info->type == 'faceoff') + if($layout_info && isset($layout_info->type) && $layout_info->type == 'faceoff') { $oLayoutModel->doActivateFaceOff($layout_info); Context::set('layout_info', $layout_info); @@ -196,6 +196,12 @@ class HTMLDisplayHandler Context::set('favicon_url', $favicon_url); Context::set('mobicon_url', $mobicon_url); + // Only print the X-UA-Compatible meta tag if somebody is still using IE + if (preg_match('!Trident/7\.0!', $_SERVER['HTTP_USER_AGENT'] ?? '')) + { + Context::addMetaTag('X-UA-Compatible', 'IE=edge', true); + } + return $output; } @@ -641,7 +647,7 @@ class HTMLDisplayHandler { if ($tag !== '') { - Context::addOpenGraphData('og:article:tag', $tag, false); + Context::addOpenGraphData('og:article:tag', $tag); } } @@ -683,21 +689,21 @@ class HTMLDisplayHandler function _addTwitterMetadata() { $card_type = $this->_image_type === 'document' ? 'summary_large_image' : 'summary'; - Context::addMetaTag('twitter:card', $card_type); + Context::addMetaTag('twitter:card', $card_type, false, false); foreach(Context::getOpenGraphData() as $val) { if ($val['property'] === 'og:title') { - Context::addMetaTag('twitter:title', $val['content']); + Context::addMetaTag('twitter:title', $val['content'], false, false); } if ($val['property'] === 'og:description') { - Context::addMetaTag('twitter:description', $val['content']); + Context::addMetaTag('twitter:description', $val['content'], false, false); } if ($val['property'] === 'og:image' && $this->_image_type === 'document') { - Context::addMetaTag('twitter:image', $val['content']); + Context::addMetaTag('twitter:image', $val['content'], false, false); } } } diff --git a/classes/module/ModuleHandler.class.php b/classes/module/ModuleHandler.class.php index 7d518befa..95c535c3d 100644 --- a/classes/module/ModuleHandler.class.php +++ b/classes/module/ModuleHandler.class.php @@ -693,7 +693,8 @@ class ModuleHandler extends Handler } } - if ($kind === 'admin') { + if ($kind === 'admin') + { Context::addMetaTag('robots', 'noindex'); } diff --git a/classes/module/ModuleObject.class.php b/classes/module/ModuleObject.class.php index a203bf9aa..c235289bf 100644 --- a/classes/module/ModuleObject.class.php +++ b/classes/module/ModuleObject.class.php @@ -370,28 +370,36 @@ class ModuleObject extends BaseObject } } // If permission is 'manager', check 'is user have manager privilege(granted)' - else if(preg_match('/^(manager|([a-z0-9\_]+)-managers)$/', $permission, $type)) + else if(preg_match('/^(manager(?::(.+))?|([a-z0-9\_]+)-managers)$/', $permission, $type)) { - if($grant->manager) + // If permission is manager(:scope), check manager privilege and scope + if ($grant->manager) { - return true; + if (empty($type[2])) + { + return true; + } + elseif ($grant->can($type[2])) + { + return true; + } } // If permission is '*-managers', search modules to find manager privilege of the member - if(Context::get('is_logged') && isset($type[2])) + if(Context::get('is_logged') && isset($type[3])) { // Manager privilege of the member is found by search all modules, Pass - if($type[2] == 'all' && ModuleModel::findManagerPrivilege($member_info) !== false) + if($type[3] == 'all' && ModuleModel::findManagerPrivilege($member_info) !== false) { return true; } // Manager privilege of the member is found by search same module as this module, Pass - elseif($type[2] == 'same' && ModuleModel::findManagerPrivilege($member_info, $this->module) !== false) + elseif($type[3] == 'same' && ModuleModel::findManagerPrivilege($member_info, $this->module) !== false) { return true; } // Manager privilege of the member is found by search same module as the module, Pass - elseif(ModuleModel::findManagerPrivilege($member_info, $type[2]) !== false) + elseif(ModuleModel::findManagerPrivilege($member_info, $type[3]) !== false) { return true; } diff --git a/common/autoload.php b/common/autoload.php index 8d7f98b3d..233ce1b83 100644 --- a/common/autoload.php +++ b/common/autoload.php @@ -11,10 +11,10 @@ if (defined('RX_VERSION')) /** * Check PHP version. */ -if (PHP_VERSION_ID < 70205) +if (PHP_VERSION_ID < 70400) { header('HTTP/1.1 500 Internal Server Error'); - echo 'Rhymix requires PHP 7.2.5 or higher.'; + echo 'Rhymix requires PHP 7.4 or higher.'; exit(1); } diff --git a/common/constants.php b/common/constants.php index da9ab15e6..6cce49864 100644 --- a/common/constants.php +++ b/common/constants.php @@ -3,7 +3,7 @@ /** * RX_VERSION is the version number of the Rhymix CMS. */ -define('RX_VERSION', '2.1.18'); +define('RX_VERSION', '2.1.19'); /** * RX_MICROTIME is the startup time of the current script, in microseconds since the Unix epoch. @@ -125,7 +125,7 @@ else /** * RX_WINDOWS is true if the operating system is Windows. */ -define('RX_WINDOWS', strncasecmp(PHP_OS, 'WIN', 3) === 0); +define('RX_WINDOWS', PHP_OS_FAMILY === 'Windows'); /** * XE core compatibility constants (may be used by XE-compatible plugins and themes). @@ -143,7 +143,7 @@ define('__XE_VERSION_ALPHA__', false); define('__XE_VERSION_BETA__', false); define('__XE_VERSION_RC__', false); define('__XE_VERSION_STABLE__', true); -define('__XE_MIN_PHP_VERSION__', '7.2.5'); +define('__XE_MIN_PHP_VERSION__', '7.4.0'); define('__XE_RECOMMEND_PHP_VERSION__', '7.4.0'); define('__ZBXE_VERSION__', RX_VERSION); define('_XE_LOCATION_', 'ko'); diff --git a/common/defaults/config.php b/common/defaults/config.php index f28768d7c..8351d0056 100644 --- a/common/defaults/config.php +++ b/common/defaults/config.php @@ -151,5 +151,7 @@ return array( ], 'use_rewrite' => true, 'use_sso' => false, - 'other' => array(), + 'other' => [ + 'proxy' => null, + ], ); diff --git a/common/framework/Cookie.php b/common/framework/Cookie.php index 997b39410..2374be6c8 100644 --- a/common/framework/Cookie.php +++ b/common/framework/Cookie.php @@ -79,24 +79,7 @@ class Cookie $options['samesite'] = config('cookie.samesite') ?? 'Lax'; } - // PHP 7.3+ supports the samesite attribute natively. PHP 7.2 requires a hack. - if (\PHP_VERSION_ID >= 70300) - { - $result = setcookie($name, $value, $options); - } - else - { - $expires = $options['expires']; - $path = $options['path'] ?? '/'; - $domain = $options['domain'] ?? null; - $secure = $options['secure'] ?? false; - $httponly = $options['httponly'] ?? false; - if (!empty($options['samesite'])) - { - $path = ($path ?: '/') . '; SameSite=' . $options['samesite']; - } - $result = setcookie($name, $value, $expires, $path, $domain, $secure, $httponly); - } + $result = setcookie($name, $value, $options); // Make the cookie immediately available server-side. if ($result && $options['expires'] >= 0) diff --git a/common/framework/HTTP.php b/common/framework/HTTP.php index 51e0b79ce..1948dee49 100644 --- a/common/framework/HTTP.php +++ b/common/framework/HTTP.php @@ -209,9 +209,10 @@ class HTTP ]; // Add proxy settings. - if (defined('__PROXY_SERVER__')) + $proxy = config('other.proxy') ?: (defined('__PROXY_SERVER__') ? constant('__PROXY_SERVER__') : ''); + if ($proxy !== '') { - $proxy = parse_url(constant('__PROXY_SERVER__')); + $proxy = parse_url($proxy); $proxy_scheme = preg_match('/^(https|socks)/', $proxy['scheme'] ?? '') ? ($proxy['scheme'] . '://') : 'http://'; $proxy_auth = (!empty($proxy['user']) && !empty($proxy['pass'])) ? ($proxy['user'] . ':' . $proxy['pass'] . '@') : ''; $settings['proxy'] = sprintf('%s%s%s:%s', $proxy_scheme, $proxy_auth, $proxy['host'], $proxy['port']); diff --git a/common/framework/Session.php b/common/framework/Session.php index c1e92a067..e88de76d3 100644 --- a/common/framework/Session.php +++ b/common/framework/Session.php @@ -79,17 +79,7 @@ class Session ini_set('session.use_cookies', 1); ini_set('session.use_only_cookies', 1); ini_set('session.use_strict_mode', 1); - if ($samesite) - { - if (PHP_VERSION_ID >= 70300) - { - ini_set('session.cookie_samesite', $samesite); - } - else - { - $path = ($path ?: '/') . '; SameSite=' . $samesite; - } - } + ini_set('session.cookie_samesite', $samesite ? 1 : 0); session_set_cookie_params($lifetime, $path, $domain, $secure, $httponly); session_name($session_name = Config::get('session.name') ?: session_name()); diff --git a/common/framework/Template.php b/common/framework/Template.php index 90302bab9..0049e68d9 100644 --- a/common/framework/Template.php +++ b/common/framework/Template.php @@ -899,19 +899,23 @@ class Template protected function _v2_checkCapability(int $check_type, $capability): bool { $grant = \Context::get('grant'); - if ($check_type === 1) + if (!($grant instanceof \Rhymix\Modules\Module\Models\Permission)) { - return isset($grant->$capability) ? boolval($grant->$capability) : false; + return false; + } + elseif ($check_type === 1) + { + return $grant->can($capability); } elseif ($check_type === 2) { - return isset($grant->$capability) ? !boolval($grant->$capability) : true; + return !$grant->can($capability); } elseif (is_array($capability)) { foreach ($capability as $cap) { - if (isset($grant->$cap) && $grant->$cap) + if ($grant->can($cap)) { return true; } diff --git a/common/framework/drivers/mail/mailfunction.php b/common/framework/drivers/mail/mailfunction.php index 6e11d03c2..545031106 100644 --- a/common/framework/drivers/mail/mailfunction.php +++ b/common/framework/drivers/mail/mailfunction.php @@ -7,15 +7,6 @@ namespace Rhymix\Framework\Drivers\Mail; */ class MailFunction extends Base implements \Rhymix\Framework\Drivers\MailInterface { - /** - * Direct invocation of the constructor is not permitted. - */ - protected function __construct() - { - include_once \RX_BASEDIR . 'common/libraries/swift_mail.php'; - $this->_mailer = new \Swift_Mailer(new \Swift_MailTransport); - } - /** * Get the human-readable name of this mail driver. * @@ -58,6 +49,12 @@ class MailFunction extends Base implements \Rhymix\Framework\Drivers\MailInterfa */ public function send(\Rhymix\Framework\Mail $message) { + if ($this->_mailer === null) + { + include_once \RX_BASEDIR . 'common/libraries/swift_mail.php'; + $this->_mailer = new \Swift_Mailer(new \Swift_MailTransport); + } + try { $errors = []; diff --git a/common/framework/drivers/mail/mailgun.php b/common/framework/drivers/mail/mailgun.php index cc1f339e9..118b6345e 100644 --- a/common/framework/drivers/mail/mailgun.php +++ b/common/framework/drivers/mail/mailgun.php @@ -116,12 +116,13 @@ class Mailgun extends Base implements \Rhymix\Framework\Drivers\MailInterface // Send the API request. $url = self::$_url . '/' . $this->_config['api_domain'] . '/messages.mime'; $request = \Rhymix\Framework\HTTP::post($url, $data, $headers, [], $settings); - $result = @json_decode($request->getBody()->getContents()); + $result_text = $request->getBody()->getContents(); + $result = @json_decode($result_text); // Parse the result. if (!$result) { - $message->errors[] = 'Mailgun: API error: ' . $request->getBody()->getContents(); + $message->errors[] = 'Mailgun: API error: ' . $result_text; return false; } elseif (!$result->id) diff --git a/common/framework/drivers/mail/smtp.php b/common/framework/drivers/mail/smtp.php index a15f05f68..7a762b2a9 100644 --- a/common/framework/drivers/mail/smtp.php +++ b/common/framework/drivers/mail/smtp.php @@ -7,23 +7,6 @@ namespace Rhymix\Framework\Drivers\Mail; */ class SMTP extends Base implements \Rhymix\Framework\Drivers\MailInterface { - /** - * Direct invocation of the constructor is not permitted. - */ - protected function __construct(array $config) - { - $security = in_array($config['smtp_security'], ['ssl', 'tls']) ? $config['smtp_security'] : null; - $transport = new \Swift_SmtpTransport($config['smtp_host'], $config['smtp_port'], $security); - $transport->setUsername($config['smtp_user']); - $transport->setPassword($config['smtp_pass']); - $local_domain = $transport->getLocalDomain(); - if (preg_match('/^\*\.(.+)$/', $local_domain, $matches)) - { - $transport->setLocalDomain($matches[1]); - } - $this->mailer = new \Swift_Mailer($transport); - } - /** * Get the list of configuration fields required by this mail driver. * @@ -56,9 +39,32 @@ class SMTP extends Base implements \Rhymix\Framework\Drivers\MailInterface */ public function send(\Rhymix\Framework\Mail $message) { + if ($this->_mailer === null) + { + if (isset($this->_config['smtp_security']) && in_array($this->_config['smtp_security'], ['ssl', 'tls'])) + { + $security = $this->_config['smtp_security']; + } + else + { + $security = null; + } + + $transport = new \Swift_SmtpTransport($this->_config['smtp_host'], $this->_config['smtp_port'], $security); + $transport->setUsername($this->_config['smtp_user']); + $transport->setPassword($this->_config['smtp_pass']); + $local_domain = $transport->getLocalDomain(); + if (preg_match('/^\*\.(.+)$/', $local_domain, $matches)) + { + $transport->setLocalDomain($matches[1]); + } + $this->_mailer = new \Swift_Mailer($transport); + } + try { - $result = $this->mailer->send($message->message, $errors); + $errors = []; + $result = $this->_mailer->send($message->message, $errors); } catch(\Exception $e) { diff --git a/common/framework/drivers/push/fcmv1.php b/common/framework/drivers/push/fcmv1.php index 6cd947749..390161af0 100644 --- a/common/framework/drivers/push/fcmv1.php +++ b/common/framework/drivers/push/fcmv1.php @@ -151,7 +151,8 @@ class FCMv1 extends Base implements PushInterface foreach ($responses as $i => $response) { $status_code = $response->getStatusCode(); - $result = @json_decode($response->getBody()->getContents()); + $result_text = $response->getBody()->getContents(); + $result = @json_decode($result_text); if ($status_code === 200) { $output->success[$tokens[$i]] = $result->name ?? ''; @@ -164,6 +165,10 @@ class FCMv1 extends Base implements PushInterface { $output->invalid[$tokens[$i]] = $tokens[$i]; } + elseif (str_contains($error_message, 'Requested entity was not found')) + { + $output->invalid[$tokens[$i]] = $tokens[$i]; + } } else { @@ -196,7 +201,8 @@ class FCMv1 extends Base implements PushInterface foreach ($responses as $i => $response) { $status_code = $response->getStatusCode(); - $result = @json_decode($response->getBody()->getContents()); + $result_text = $response->getBody()->getContents(); + $result = @json_decode($result_text); if ($status_code === 200) { $output->success[$topics[$i]] = $result->name ?? ''; diff --git a/common/framework/drivers/queue/db.php b/common/framework/drivers/queue/db.php index c45eedc5d..a55bd71ad 100644 --- a/common/framework/drivers/queue/db.php +++ b/common/framework/drivers/queue/db.php @@ -93,8 +93,8 @@ class DB implements QueueInterface public function addTask(string $handler, ?object $args = null, ?object $options = null): int { $oDB = RFDB::getInstance(); - $stmt = $oDB->prepare('INSERT INTO task_queue (handler, args, options) VALUES (?, ?, ?)'); - $result = $stmt->execute([$handler, serialize($args), serialize($options)]); + $stmt = $oDB->prepare('INSERT INTO task_queue (handler, args, options, regdate) VALUES (?, ?, ?, ?)'); + $result = $stmt->execute([$handler, serialize($args), serialize($options), date('Y-m-d H:i:s')]); return $result ? $oDB->getInsertID() : 0; } diff --git a/common/framework/drivers/sms/apistore.php b/common/framework/drivers/sms/apistore.php deleted file mode 100644 index 2b294a0a5..000000000 --- a/common/framework/drivers/sms/apistore.php +++ /dev/null @@ -1,141 +0,0 @@ - 500, - 'sms_max_length' => 90, - 'sms_max_length_in_charset' => 'CP949', - 'lms_supported' => true, - 'lms_supported_country_codes' => array(82), - 'lms_max_length' => 2000, - 'lms_max_length_in_charset' => 'CP949', - 'lms_subject_supported' => true, - 'lms_subject_max_length' => 60, - 'mms_supported' => false, - 'delay_supported' => true, - ); - - /** - * Config keys used by this driver are stored here. - */ - protected static $_required_config = array('api_user', 'api_key'); - - /** - * Check if the current SMS driver is supported on this server. - * - * This method returns true on success and false on failure. - * - * @return bool - */ - public static function isSupported() - { - return true; - } - - /** - * Store the last response. - */ - protected $_last_response = ''; - - /** - * Send a message. - * - * This method returns true on success and false on failure. - * - * @param array $messages - * @param object $original - * @return bool - */ - public function send(array $messages, \Rhymix\Framework\SMS $original) - { - $status = true; - - foreach ($messages as $i => $message) - { - $data = array(); - $data['send_phone'] = $message->from; - $data['dest_phone'] = implode(',', $message->to); - $data['msg_body'] = strval($message->content); - if ($message->type !== 'SMS' && $message->subject) - { - $data['subject'] = $message->subject; - } - - $result = $this->_apiCall(sprintf('message/%s', strtolower($message->type)), $data); - if (!$result) - { - $message->errors[] = 'ApiStore API returned invalid response: ' . $this->_getLastResponse(); - $status = false; - } - if ($result->result_message !== 'OK') - { - $message->errors[] = 'ApiStore API error: ' . $result->result_code . ' ' . $result->result_message; - } - } - - return $status; - } - - /** - * API call. - * - * @param string $url - * @param array $data - * @param string $method (optional) - * @return object|false - */ - protected function _apiCall(string $url, array $data, string $method = 'POST') - { - // Build the request URL. - if ($data['version']) - { - $version = $data['version']; - unset($data['version']); - } - else - { - $version = 1; - } - $url = sprintf('http://api.apistore.co.kr/ppurio/%d/%s/%s', $version, trim($url, '/'), $this->_config['api_user']); - - // Set the API key in the header. - $headers = array( - 'x-waple-authorization' => $this->_config['api_key'], - ); - - // Send the API reqeust. - if ($method === 'GET') - { - if ($data) - { - $url .= '?' . http_build_query($data); - } - $this->_last_response = \FileHandler::getRemoteResource($url, null, 5, $method, null, $headers) ?: ''; - } - else - { - $this->_last_response = \FileHandler::getRemoteResource($url, $data, 5, $method, null, $headers) ?: ''; - } - $result = @json_decode($this->_last_response); - return $result ?: false; - } - - /** - * Fetch the last API response. - * - * @return string - */ - protected function _getLastResponse() - { - return $this->_last_response; - } -} diff --git a/common/functions.php b/common/functions.php index ae2616abe..d2b2d0d23 100644 --- a/common/functions.php +++ b/common/functions.php @@ -740,20 +740,6 @@ function is_empty_html_content($str): bool return $str === ''; } -/** - * Polyfill for is_countable() in PHP < 7.3 - * - * @param mixed $var - * @return bool -**/ -if (!function_exists('is_countable')) -{ - function is_countable($var) - { - return is_array($var) || $var instanceof Countable; - } -} - /** * Polyfill for str_starts_with() in PHP < 8.0 * diff --git a/common/scripts/cron.php b/common/scripts/cron.php index 563f71c70..19ea7b1c9 100644 --- a/common/scripts/cron.php +++ b/common/scripts/cron.php @@ -33,6 +33,7 @@ else } // Get queue configuration set by the administrator. +$display_errors = config('queue.display_errors') === false ? false : true; $timeout = (config('queue.interval') ?? 1) * 60; $process_count = config('queue.process_count') ?? 1; @@ -41,6 +42,14 @@ if (PHP_SAPI !== 'cli') { ignore_user_abort(true); set_time_limit(max(60, $timeout)); + if ($display_errors) + { + ini_set('display_errors', true); + } + if (Rhymix\Framework\Session::checkStart()) + { + Rhymix\Framework\Session::close(); + } } // Create multiple processes if configured. @@ -89,3 +98,9 @@ else { Rhymix\Framework\Queue::process($timeout); } + +// If called over the network, display a simple OK message to indicate success. +if (PHP_SAPI !== 'cli') +{ + echo "OK\n"; +} diff --git a/common/tpl/common_layout.html b/common/tpl/common_layout.html index 04a7e326d..b4d64176c 100644 --- a/common/tpl/common_layout.html +++ b/common/tpl/common_layout.html @@ -7,11 +7,12 @@ - -@foreach (Context::getMetaTag() as $val) - -@endforeach +@foreach (Context::getMetaTag() as $val) +@if ($val['is_before_title']) + +@endif +@endforeach {{ Context::getBrowserTitle() }} @@ -55,6 +56,11 @@ @endforeach +@foreach (Context::getMetaTag() as $val) +@if (!$val['is_before_title']) + +@endif +@endforeach @foreach (Context::getOpenGraphData() as $og_metadata) @endforeach diff --git a/modules/admin/controllers/ServerEnv.php b/modules/admin/controllers/ServerEnv.php index 6305ef128..79a393aa2 100644 --- a/modules/admin/controllers/ServerEnv.php +++ b/modules/admin/controllers/ServerEnv.php @@ -20,7 +20,7 @@ class ServerEnv extends Base $info = array(); $skip = array( 'phpext' => array('core', 'session', 'spl', 'standard', 'date', 'ctype', 'tokenizer', 'apache2handler', 'filter', 'reflection'), - 'module' => array('addon', 'admin', 'adminlogging', 'advanced_mailer', 'autoinstall', 'board', 'comment', 'communication', 'counter', 'document', 'editor', 'file', 'importer', 'install', 'integration_search', 'krzip', 'layout', 'member', 'menu', 'message', 'module', 'ncenterlite', 'opage', 'page', 'point', 'poll', 'rss', 'session', 'spamfilter', 'tag', 'trackback', 'trash', 'widget'), + 'module' => array('addon', 'admin', 'adminlogging', 'advanced_mailer', 'autoinstall', 'board', 'comment', 'communication', 'counter', 'document', 'editor', 'extravar', 'file', 'importer', 'install', 'integration_search', 'krzip', 'layout', 'member', 'menu', 'message', 'module', 'ncenterlite', 'opage', 'page', 'point', 'poll', 'rss', 'session', 'spamfilter', 'tag', 'trackback', 'trash', 'widget'), 'addon' => array('adminlogging', 'autolink', 'counter', 'member_extra_info', 'point_level_icon', 'photoswipe', 'resize_image'), 'layout' => array('default', 'user_layout', 'xedition'), 'widget' => array('content', 'counter_status', 'language_select', 'login_info', 'mcontent', 'pollWidget'), diff --git a/modules/admin/controllers/maintenance/CacheReset.php b/modules/admin/controllers/maintenance/CacheReset.php index b40845f27..c287dbce0 100644 --- a/modules/admin/controllers/maintenance/CacheReset.php +++ b/modules/admin/controllers/maintenance/CacheReset.php @@ -95,7 +95,7 @@ class CacheReset extends Base // If possible, use system command to speed up recursive deletion if (function_exists('exec') && !preg_match('/(?checkInstalled(); + // Opcache reset + if (function_exists('opcache_reset')) + { + opcache_reset(); + } + $this->setMessage('success_updated'); } } diff --git a/modules/admin/controllers/maintenance/Cleanup.php b/modules/admin/controllers/maintenance/Cleanup.php index e1870720b..7e5e3e45e 100644 --- a/modules/admin/controllers/maintenance/Cleanup.php +++ b/modules/admin/controllers/maintenance/Cleanup.php @@ -172,11 +172,7 @@ class Cleanup extends Base } // Return default values for most common operating systems. - if (preg_match('/Linux/', \PHP_OS)) - { - return $cache = true; - } - if (preg_match('/Win/i', \PHP_OS)) + if (\RX_WINDOWS) { return $cache = false; } diff --git a/modules/admin/controllers/systemconfig/Advanced.php b/modules/admin/controllers/systemconfig/Advanced.php index 4135a0a0f..8622d1599 100644 --- a/modules/admin/controllers/systemconfig/Advanced.php +++ b/modules/admin/controllers/systemconfig/Advanced.php @@ -109,6 +109,7 @@ class Advanced extends Base Context::set('minify_scripts', Config::get('view.minify_scripts')); Context::set('concat_scripts', Config::get('view.concat_scripts')); Context::set('jquery_version', Config::get('view.jquery_version')); + Context::set('outgoing_proxy', Config::get('other.proxy')); $this->setTemplateFile('config_advanced'); } @@ -215,6 +216,13 @@ class Advanced extends Base Config::set('locale.auto_select_lang', $vars->auto_select_lang === 'Y'); Config::set('locale.default_timezone', $vars->default_timezone); + // Proxy + $proxy = trim($vars->outgoing_proxy ?? ''); + if ($proxy !== '' && !preg_match('!^(https?|socks)://.+!', $proxy)) + { + throw new Exception('msg_invalid_outgoing_proxy'); + } + // Other settings Config::set('url.rewrite', intval($vars->use_rewrite)); Config::set('use_rewrite', $vars->use_rewrite > 0); @@ -226,6 +234,7 @@ class Advanced extends Base Config::set('view.concat_scripts', $vars->concat_scripts ?: 'none'); Config::set('view.delay_compile', intval($vars->delay_template_compile)); Config::set('view.jquery_version', $vars->jquery_version == 3 ? 3 : 2); + Config::set('other.proxy', $proxy); // Save if (!Config::save()) diff --git a/modules/admin/controllers/systemconfig/Queue.php b/modules/admin/controllers/systemconfig/Queue.php index 1422dbbc5..6ccc67d71 100644 --- a/modules/admin/controllers/systemconfig/Queue.php +++ b/modules/admin/controllers/systemconfig/Queue.php @@ -84,6 +84,9 @@ class Queue extends Base $driver_config[$conf_name] = $conf_value === '' ? null : $conf_value; } + // Validate error display setting. + $display_errors = Context::get('webcron_display_errors') === 'Y' ? true : false; + // Validate the interval. $interval = intval($vars->queue_interval ?? 1); if ($interval < 1 || $interval > 10) @@ -116,10 +119,10 @@ class Queue extends Base throw new Exception('msg_queue_driver_not_usable'); } - // Save system config. Config::set("queue.enabled", $enabled); Config::set("queue.driver", $driver); + Config::set("queue.display_errors", $display_errors); Config::set("queue.interval", $interval); Config::set("queue.process_count", $process_count); Config::set("queue.key", $key); diff --git a/modules/admin/lang/en.php b/modules/admin/lang/en.php index a2ff0abbb..1e54bc591 100644 --- a/modules/admin/lang/en.php +++ b/modules/admin/lang/en.php @@ -203,6 +203,9 @@ $lang->cache_truncate_method_empty = 'Delete content of cache folder'; $lang->about_cache_truncate_method = 'It is faster and more reliable to delete the cache folder itself.
Choose the option to delete content only if the cache folder cannot be deleted, e.g. it is a mountpoint.'; $lang->cache_control_header = 'Cache-Control header'; $lang->about_cache_control_header = 'Select the Cache-Control header to apply to HTML pages that generally should not be cached.
Deselecting some of these options may help in certain circumstances, but at the cost of displaying outdated information.'; +$lang->outgoing_proxy = 'Proxy Outgoing Requests'; +$lang->about_outgoing_proxy = 'Use a proxy to hide the server\'s IP when making requests to other sites.
This setting does not apply to modules that implement their own HTTP clients.'; +$lang->msg_invalid_outgoing_proxy = 'Proxy URL must begin with http://, https:// or socks://'; $lang->msg_cache_handler_not_supported = 'Your server does not support the selected cache method, or Rhymix is unable to use the cache with the given settings.'; $lang->msg_invalid_default_url = 'The default URL is invalid.'; $lang->msg_default_url_ssl_inconsistent = 'In order to use SSL always, the default URL must also begin with https://'; @@ -288,6 +291,8 @@ $lang->cmd_queue_enabled = 'Use Task Queue'; $lang->cmd_queue_enabled_help = 'The task queue will stop accepting new tasks if you uncheck the above.'; $lang->cmd_queue_driver = 'Queue Driver'; $lang->cmd_queue_driver_help = 'Select the driver for the task queue that suits your hosting environment and website needs.
Some drivers such as Redis will need the corresponding program to be installed on the server.'; +$lang->cmd_queue_webcron_display_errors = 'Display Webcron Errors'; +$lang->cmd_queue_webcron_display_errors_help = 'Show webcron errors to help with debugging, if you have difficulty accessing server error logs.'; $lang->cmd_queue_interval = 'Calling Interval'; $lang->cmd_queue_interval_help = 'Use a scheduler such as crontab or systemd timer to call the script on a set interval.
All tasks are processed as soon as possible regardless of the interval, but a short interval means quick recovery from any error.
For web-based cron, this should not exceed the max_execution_time setting in php.ini.
The max_execution_time on this server is %d seconds.'; $lang->cmd_queue_process_count = 'Process Count'; @@ -300,8 +305,8 @@ $lang->cmd_queue_config_keys['user'] = 'User'; $lang->cmd_queue_config_keys['pass'] = 'Password'; $lang->cmd_queue_config_keys['dbnum'] = 'DB Number'; $lang->msg_queue_instructions['same_as_php'] = '(same as PHP)'; -$lang->msg_queue_instructions['crontab1'] = 'Log into the server as the %s account and run crontab -e to paste the following content into your crontab. (DO NOT run it as root!)
The %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['crontab1'] = 'Log into the server as the %s account and run crontab -e to paste the following content into your crontab. (DO NOT run it as root!)
If the account shown above cannot be logged into, such as apache or www-data, try running sudo crontab -e -u %s from a different account.'; +$lang->msg_queue_instructions['crontab2'] = 'The %s directory in the example should be replaced with a path where logs can be recorded.
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.
Check your logs to make sure that the cron service is reaching your website.'; $lang->msg_queue_instructions['systemd1'] = 'Put the following content in /etc/systemd/system/rhymix-queue.service'; $lang->msg_queue_instructions['systemd2'] = 'Put the following content in /etc/systemd/system/rhymix-queue.timer'; diff --git a/modules/admin/lang/ko.php b/modules/admin/lang/ko.php index 7fd335612..3cb78e0e1 100644 --- a/modules/admin/lang/ko.php +++ b/modules/admin/lang/ko.php @@ -204,6 +204,9 @@ $lang->cache_truncate_method_empty = '캐시 내용만 삭제'; $lang->about_cache_truncate_method = '캐시 폴더를 삭제하는 방법이 더 빠르고 안정적입니다.
내용만 삭제하는 방법은 램디스크를 캐시 폴더로 사용하는 등 폴더 자체를 삭제해서는 안 되는 경우에만 선택하십시오.'; $lang->cache_control_header = '캐시 컨트롤 헤더'; $lang->about_cache_control_header = '브라우저 캐시를 적용하지 않을 일반 HTML 페이지에 적용할 Cache-Control 헤더 내용을 선택할 수 있습니다.
선택을 해제하면 뒤로가기 등 특정한 상황에서 성능이 개선될 수도 있지만, 오래된 정보가 노출되는 등 부작용이 발생할 수도 있습니다.'; +$lang->outgoing_proxy = '외부 요청 프록시'; +$lang->about_outgoing_proxy = '외부 요청시 프록시를 사용하여 서버 IP 노출을 방지합니다.
코어에서 제공하는 클래스와 함수를 사용하지 않고 외부 요청을 자체 구현한 서드파티 자료에는 적용되지 않습니다.'; +$lang->msg_invalid_outgoing_proxy = '프록시 주소는 http://, https:// 또는 socks://로 시작해야 합니다.'; $lang->msg_cache_handler_not_supported = '선택하신 캐시 방식을 서버에서 지원하지 않거나, 주어진 정보로 캐시에 접속할 수 없습니다.'; $lang->msg_invalid_default_url = '기본 URL이 올바르지 않습니다.'; $lang->msg_default_url_ssl_inconsistent = 'SSL을 항상 사용하실 경우 기본 URL도 https://로 시작해야 합니다.'; @@ -284,6 +287,8 @@ $lang->cmd_queue_enabled = '비동기 작업 사용'; $lang->cmd_queue_enabled_help = '체크를 해제하면 더이상 작업을 접수하지 않습니다.'; $lang->cmd_queue_driver = '비동기 드라이버'; $lang->cmd_queue_driver_help = '비동기 작업을 관리할 방법을 설정합니다. 호스팅 환경과 사이트의 필요에 맞추어 선택하세요.
Redis 등 일부 드라이버는 서버에 해당 기능이 설치되어 있어야 사용할 수 있습니다.'; +$lang->cmd_queue_webcron_display_errors = '웹크론 오류 표시'; +$lang->cmd_queue_webcron_display_errors_help = '에러 로그를 확인하기 어려운 서버 환경인 경우, 웹크론 에러를 화면에 표시하여 문제 파악을 돕습니다.'; $lang->cmd_queue_interval = '호출 간격'; $lang->cmd_queue_interval_help = 'crontab, systemd timer, 웹크론 등을 사용하여 일정한 주기로 스크립트를 호출해 주십시오.
모든 비동기 작업은 호출 간격과 무관하게 실시간으로 처리되나, 호출 간격이 짧으면 장애 발생시 신속하게 복구됩니다.
웹크론 사용시에는 php.ini의 실행 시간 제한을 초과하지 않는 것이 좋습니다.
이 서버의 max_execution_time은 %d초로 설정되어 있습니다.'; $lang->cmd_queue_process_count = '프로세스 갯수'; @@ -296,8 +301,8 @@ $lang->cmd_queue_config_keys['user'] = '아이디'; $lang->cmd_queue_config_keys['pass'] = '암호'; $lang->cmd_queue_config_keys['dbnum'] = 'DB 번호'; $lang->msg_queue_instructions['same_as_php'] = 'PHP를 실행하는 계정과 동일한'; -$lang->msg_queue_instructions['crontab1'] = '%s 계정으로 서버에 로그인하여 crontab -e 명령을 실행한 후, 아래의 내용을 붙여넣으십시오. (root 권한으로 실행하지 마십시오.)
예제의 %s 디렉토리는 로그를 기록할 수 있는 경로로 변경하여 사용하십시오.'; -$lang->msg_queue_instructions['crontab2'] = '스크립트 호출 간격을 변경할 경우, 설정에 맞추어 crontab 실행 간격도 조절하여야 합니다.'; +$lang->msg_queue_instructions['crontab1'] = '%s 계정으로 서버에 로그인하여 crontab -e 명령을 실행한 후, 아래의 내용을 붙여넣으십시오. (root 권한으로 실행하지 마십시오!)
만약 apachewww-data처럼 로그인할 수 없는 계정이라면, 다른 계정에서 sudo crontab -e -u %s 명령을 실행해 볼 수 있습니다.'; +$lang->msg_queue_instructions['crontab2'] = '예제의 %s 디렉토리는 로그를 기록할 권한이 있는 경로로 변경하여 사용하십시오.
스크립트 호출 간격을 변경할 경우, 설정에 맞추어 crontab 실행 간격도 조절하여야 합니다.'; $lang->msg_queue_instructions['webcron'] = '아래의 URL을 1분 간격 또는 아래에서 설정한 호출 간격에 맞추어 GET으로 호출하도록 합니다.
웹크론 서비스가 방화벽이나 CDN 등에 의해 차단되지 않도록 주의하고, 정상적으로 호출되는지 서버 로그를 확인하십시오.'; $lang->msg_queue_instructions['systemd1'] = '/etc/systemd/system/rhymix-queue.service 파일에 아래와 같은 내용을 넣습니다.'; $lang->msg_queue_instructions['systemd2'] = '/etc/systemd/system/rhymix-queue.timer 파일에 아래와 같은 내용을 넣습니다.'; diff --git a/modules/admin/tpl/config_advanced.html b/modules/admin/tpl/config_advanced.html index 3378ea501..6373337fe 100644 --- a/modules/admin/tpl/config_advanced.html +++ b/modules/admin/tpl/config_advanced.html @@ -201,6 +201,13 @@

{$lang->about_cache_control_header}

+
+ +
+ +

{$lang->about_outgoing_proxy}

+
+
diff --git a/modules/admin/tpl/config_queue.html b/modules/admin/tpl/config_queue.html index eda100a42..4838a80f7 100644 --- a/modules/admin/tpl/config_queue.html +++ b/modules/admin/tpl/config_queue.html @@ -116,11 +116,11 @@ endif; }

- {sprintf($lang->msg_queue_instructions['crontab1'], $user_info['name'] ?? 'PHP', $user_info['dir'] . 'logs')|noescape} + {sprintf($lang->msg_queue_instructions['crontab1'], $user_info['name'] ?? 'PHP', $user_info['name'] ?? 'PHP')|noescape}

-
* * * * * php {\RX_BASEDIR}index.php common.cron >> {$user_info['dir']}logs{\DIRECTORY_SEPARATOR}queue.log 2>&1
+
* * * * * /usr/bin/php {\RX_BASEDIR}index.php common.cron >> {$user_info['dir']}logs{\DIRECTORY_SEPARATOR}queue.log 2>&1

- {$lang->msg_queue_instructions['crontab2']|noescape} + {sprintf($lang->msg_queue_instructions['crontab2'], $user_info['dir'] . 'logs')|noescape}

@@ -137,7 +137,7 @@ Description=Rhymix Queue Service [Service] -ExecStart=php {\RX_BASEDIR}index.php common.cron +ExecStart=/usr/bin/php {\RX_BASEDIR}index.php common.cron User={$user_info['name']}

{$lang->msg_queue_instructions['systemd2']|noescape} @@ -167,6 +167,21 @@ systemctl enable rhymix-queue.timer

+
+ +
+ + +

{$lang->cmd_queue_webcron_display_errors_help}

+
+
+
diff --git a/modules/adminlogging/adminlogging.class.php b/modules/adminlogging/adminlogging.class.php index 21cd2bc3a..bf58737fb 100644 --- a/modules/adminlogging/adminlogging.class.php +++ b/modules/adminlogging/adminlogging.class.php @@ -18,7 +18,7 @@ class adminlogging extends ModuleObject */ function moduleInstall() { - + } /** @@ -27,7 +27,23 @@ class adminlogging extends ModuleObject */ function checkUpdate() { - return FALSE; + $oDB = DB::getInstance(); + if (!$oDB->isColumnExists('admin_log', 'member_srl')) + { + return true; + } + if (!$oDB->isIndexExists('admin_log', 'idx_member_srl')) + { + return true; + } + + $column_info = $oDB->getColumnInfo('admin_log', 'request_vars'); + if ($column_info->xetype !== 'bigtext') + { + return true; + } + + return false; } /** @@ -36,7 +52,21 @@ class adminlogging extends ModuleObject */ function moduleUpdate() { - + $oDB = DB::getInstance(); + if (!$oDB->isColumnExists('admin_log', 'member_srl')) + { + $oDB->addColumn('admin_log', 'member_srl', 'number', null, 0, true, 'site_srl'); + } + if (!$oDB->isIndexExists('admin_log', 'idx_member_srl')) + { + $oDB->addIndex('admin_log', 'idx_member_srl', ['member_srl']); + } + + $column_info = $oDB->getColumnInfo('admin_log', 'request_vars'); + if ($column_info->xetype !== 'bigtext') + { + $oDB->modifyColumn('admin_log', 'request_vars', 'bigtext'); + } } /** @@ -45,7 +75,7 @@ class adminlogging extends ModuleObject */ function recompileCache() { - + } } diff --git a/modules/adminlogging/adminlogging.controller.php b/modules/adminlogging/adminlogging.controller.php index 7b35ee58f..1d9b239ce 100644 --- a/modules/adminlogging/adminlogging.controller.php +++ b/modules/adminlogging/adminlogging.controller.php @@ -11,43 +11,26 @@ */ class adminloggingController extends adminlogging { - - /** - * Initialization - * @return void - */ - function init() - { - // forbit access if the user is not an administrator - $oMemberModel = getModel('member'); - $logged_info = $oMemberModel->getLoggedInfo(); - if($logged_info->is_admin != 'Y') - { - throw new Rhymix\Framework\Exceptions\NotPermitted('admin.msg_is_not_administrator'); - } - } - /** * Insert log * @return void */ - function insertLog($module, $act) + public function insertLog($module, $act) { - if(!$module || !$act) + if (!$module || !$act) { return; } $args = new stdClass(); + $args->member_srl = $this->user->member_srl; $args->module = $module; $args->act = $act; - $args->ipaddress = \RX_CLIENT_IP; + $args->request_vars = json_encode(Context::getRequestVars(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); $args->regdate = date('YmdHis'); - $args->requestVars = print_r(Context::getRequestVars(), TRUE); - + $args->ipaddress = \RX_CLIENT_IP; $output = executeQuery('adminlogging.insertLog', $args); } - } /* End of file adminlogging.controller.php */ /* Location: ./modules/adminlogging/adminlogging.controller.php */ diff --git a/modules/adminlogging/queries/insertLog.xml b/modules/adminlogging/queries/insertLog.xml index 9c4f6ab06..8a0f95e0c 100644 --- a/modules/adminlogging/queries/insertLog.xml +++ b/modules/adminlogging/queries/insertLog.xml @@ -3,11 +3,12 @@ - - - + + - + + + diff --git a/modules/adminlogging/schemas/admin_log.xml b/modules/adminlogging/schemas/admin_log.xml index 02ba0af03..31a022493 100644 --- a/modules/adminlogging/schemas/admin_log.xml +++ b/modules/adminlogging/schemas/admin_log.xml @@ -1,8 +1,10 @@
- - - + + + - + + +
diff --git a/modules/board/board.view.php b/modules/board/board.view.php index 0d363080e..4ff4672d0 100644 --- a/modules/board/board.view.php +++ b/modules/board/board.view.php @@ -289,9 +289,6 @@ class BoardView extends Board } } - // check the manage grant - if($this->grant->manager) $oDocument->setGrant(); - // if the consultation function is enabled, and the document is not a notice if($this->consultation && !$oDocument->isNotice()) { @@ -904,7 +901,7 @@ class BoardView extends Board } } - if ($this->module_info->protect_admin_content_update !== 'N') + if (($this->module_info->protect_admin_content_update ?? 'N') !== 'N') { $member_info = MemberModel::getMemberInfo($oDocument->get('member_srl')); if(isset($member_info->is_admin) && $member_info->is_admin == 'Y' && $this->user->is_admin != 'Y') diff --git a/modules/board/conf/module.xml b/modules/board/conf/module.xml index 717fb91cb..da65fbfaa 100644 --- a/modules/board/conf/module.xml +++ b/modules/board/conf/module.xml @@ -116,21 +116,21 @@ - - - - - - - + + + + + + + - - + + - + diff --git a/modules/board/lang/en.php b/modules/board/lang/en.php index c26790353..e85952457 100644 --- a/modules/board/lang/en.php +++ b/modules/board/lang/en.php @@ -97,7 +97,7 @@ $lang->cmd_do_not_message = 'Never'; $lang->delete_placeholder = 'Delete Placeholder'; $lang->msg_document_notify_mail = '[%s] The new post : %s'; $lang->cmd_board_combined_board = 'Combined Board'; -$lang->about_board_combined_board = 'You can use this board to view documents from other boards. Press the Ctrl key and click to select multiple boards.
Caution: view permissions for the current board will apply to all affected documents.'; +$lang->about_board_combined_board = 'You can use this board to view documents from other boards. Press the Ctrl key and click to select multiple boards.
Warning: permissions for the current board will apply to all affected documents and comments.'; $lang->cmd_board_include_modules = 'Include Boards'; $lang->cmd_board_include_modules_none = '(None)'; $lang->cmd_board_include_days = 'Include Duration'; diff --git a/modules/board/lang/ko.php b/modules/board/lang/ko.php index 778004a5f..5f73c5afa 100644 --- a/modules/board/lang/ko.php +++ b/modules/board/lang/ko.php @@ -109,7 +109,7 @@ $lang->cmd_document_vote_user = '이 글의 추천인 목록'; $lang->cmd_comment_vote_user = '이 댓글의 추천인 목록'; $lang->msg_not_target = '문서 또는 댓글의 추천인목록만 조회가능합니다.'; $lang->cmd_board_combined_board = '통합 게시판'; -$lang->about_board_combined_board = '다른 게시판의 글을 모아서 볼 수 있습니다. 여러 게시판을 선택하려면 Ctrl 키를 누르고 클릭하세요.
주의: 현재 게시판의 읽기 권한 설정이 모든 글에 적용됩니다.'; +$lang->about_board_combined_board = '다른 게시판의 글을 모아서 볼 수 있습니다. 여러 게시판을 선택하려면 Ctrl 키를 누르고 클릭하세요.
주의: 현재 게시판의 권한 설정이 모든 글에 적용됩니다.'; $lang->cmd_board_include_modules = '포함할 게시판 선택'; $lang->cmd_board_include_modules_none = '(포함하지 않음)'; $lang->cmd_board_include_days = '포함할 기간'; diff --git a/modules/board/skins/xedition/_comment.html b/modules/board/skins/xedition/_comment.html index c87c379dc..f5f52e574 100644 --- a/modules/board/skins/xedition/_comment.html +++ b/modules/board/skins/xedition/_comment.html @@ -91,8 +91,10 @@ - - + + + +
{$captcha} diff --git a/modules/board/skins/xedition/comment_form.html b/modules/board/skins/xedition/comment_form.html index e8b8de3b7..2d0310b08 100644 --- a/modules/board/skins/xedition/comment_form.html +++ b/modules/board/skins/xedition/comment_form.html @@ -29,8 +29,10 @@ - - + + + +
{$captcha} diff --git a/modules/board/tpl/index.html b/modules/board/tpl/index.html index 3d4a90dac..2be552def 100644 --- a/modules/board/tpl/index.html +++ b/modules/board/tpl/index.html @@ -22,7 +22,7 @@ - {$no} + {$val->module_srl} {$lang->virtual_site} @@ -32,7 +32,7 @@ {$val->domain ?? ''}{\RX_BASEURL} {$val->mid} - {$val->browser_title} + {$val->browser_title} {zdate($val->regdate,"Y-m-d")} {$lang->cmd_setup}   diff --git a/modules/comment/comment.item.php b/modules/comment/comment.item.php index 3be2d3244..096a2f887 100644 --- a/modules/comment/comment.item.php +++ b/modules/comment/comment.item.php @@ -126,7 +126,7 @@ class CommentItem extends BaseObject } $grant = ModuleModel::getGrant(ModuleModel::getModuleInfoByModuleSrl($this->get('module_srl')), $logged_info); - if ($grant->manager) + if ($grant->manager && $grant->can('moderate:comment')) { return $this->grant_cache = true; } diff --git a/modules/comment/conf/module.xml b/modules/comment/conf/module.xml index 83841a17a..00e6efbe0 100644 --- a/modules/comment/conf/module.xml +++ b/modules/comment/conf/module.xml @@ -13,8 +13,8 @@ - - + + @@ -23,9 +23,9 @@ - - - + + + diff --git a/modules/document/conf/module.xml b/modules/document/conf/module.xml index 7f7fe8faf..b6e6105d2 100644 --- a/modules/document/conf/module.xml +++ b/modules/document/conf/module.xml @@ -22,14 +22,14 @@ - - - - - - - - + + + + + + + + @@ -44,11 +44,11 @@ - + - - - + + + diff --git a/modules/document/document.item.php b/modules/document/document.item.php index 4f83caf43..36fdf4c09 100644 --- a/modules/document/document.item.php +++ b/modules/document/document.item.php @@ -218,7 +218,7 @@ class DocumentItem extends BaseObject } $grant = ModuleModel::getGrant(ModuleModel::getModuleInfoByModuleSrl($this->get('module_srl')), $logged_info); - if ($grant->manager) + if ($grant->manager && $grant->can('moderate:document')) { return $this->grant_cache = true; } diff --git a/modules/editor/conf/module.xml b/modules/editor/conf/module.xml index 61169cbf1..aea50d220 100644 --- a/modules/editor/conf/module.xml +++ b/modules/editor/conf/module.xml @@ -12,7 +12,7 @@ - + diff --git a/modules/file/conf/module.xml b/modules/file/conf/module.xml index 3b0d1da9c..4c5c5f598 100644 --- a/modules/file/conf/module.xml +++ b/modules/file/conf/module.xml @@ -23,7 +23,7 @@ - + diff --git a/modules/member/member.admin.view.php b/modules/member/member.admin.view.php index a573d6c18..dbb4cd74a 100644 --- a/modules/member/member.admin.view.php +++ b/modules/member/member.admin.view.php @@ -58,6 +58,10 @@ class MemberAdminView extends Member // retrieve group list $this->group_list = $oMemberModel->getGroups(); + foreach ($this->group_list as $group) + { + $group->title = Context::replaceUserLang($group->title, true); + } Context::set('group_list', $this->group_list); $security = new Security(); diff --git a/modules/member/member.class.php b/modules/member/member.class.php index 6b9a3335a..8e1c59bf9 100644 --- a/modules/member/member.class.php +++ b/modules/member/member.class.php @@ -14,16 +14,6 @@ class Member extends ModuleObject public const NOUSE_EXTRA_VARS = ['error_return_url', 'success_return_url', '_rx_ajax_compat', '_rx_ajax_form', '_rx_csrf_token', 'ruleset', 'captchaType', 'use_editor', 'use_html']; public const STATUS_LIST = ['APPROVED', 'DENIED', 'UNAUTHED', 'SUSPENDED', 'DELETED']; - /** - * constructor - * - * @return void - */ - function __construct() - { - parent::__construct(); - } - /** * Implement if additional tasks are necessary when installing * diff --git a/modules/member/member.view.php b/modules/member/member.view.php index 2731ad480..b11318e26 100644 --- a/modules/member/member.view.php +++ b/modules/member/member.view.php @@ -176,6 +176,11 @@ class MemberView extends Member $member_info->email_address = sprintf('%s@%s', $protect_id, $email_host); } + foreach ($member_info->group_list ?? [] as $key => $val) + { + $member_info->group_list[$key] = Context::replaceUserLang($val, true); + } + Context::set('memberInfo', get_object_vars($member_info)); $extendForm = MemberModel::getCombineJoinForm($member_info); diff --git a/modules/member/tpl/member_list.html b/modules/member/tpl/member_list.html index 0b2bb461b..dfd63161a 100644 --- a/modules/member/tpl/member_list.html +++ b/modules/member/tpl/member_list.html @@ -61,7 +61,7 @@ ? - {@ $member_info['group_list'] = implode(', ', $member_info['group_list'])} + {@ $member_info['group_list'] = Context::replaceUserLang(implode(', ', $member_info['group_list']), true)} {getEncodeEmailAddress($member_info['email_address'])} diff --git a/modules/module/lang/en.php b/modules/module/lang/en.php index 6a81ee07f..d9172183d 100644 --- a/modules/module/lang/en.php +++ b/modules/module/lang/en.php @@ -97,6 +97,10 @@ $lang->about_mobile_page_count = 'You can set the number of page links to move p $lang->about_admin_id = 'You can grant someone permission to manage this module. Please enter the user ID or email address of the person you wish to add.'; $lang->about_grant_deatil = 'Registered users mean users who signed-up to the virtual sites (e.g., cafeXE).'; $lang->about_module = 'Rhymix consists of modules except the basic library. [Module Manage] module will show all installed modules and help you to manage them.'; +$lang->admin_scope = 'Scope of Admin Powers'; +$lang->admin_scopes['moderate:document'] = 'Manage documents'; +$lang->admin_scopes['moderate:comment'] = 'Manage comments'; +$lang->admin_scopes['config:*'] = 'Change settings'; $lang->extra_vars_is_strict = 'Specified values only'; $lang->extra_vars_options = 'Options'; $lang->about_extra_vars_is_strict = 'In single and multiple choice fields, only allow the values specified below. If you change the allowed values, it may affect previous posts.'; diff --git a/modules/module/lang/ko.php b/modules/module/lang/ko.php index 4b4ffe96a..212f0a39a 100644 --- a/modules/module/lang/ko.php +++ b/modules/module/lang/ko.php @@ -96,6 +96,10 @@ $lang->about_mobile_page_count = '목록 하단, 페이지를 이동하는 링 $lang->about_admin_id = '특정 회원에게 이 모듈의 관리 권한을 부여할 수 있습니다. 권한을 부여할 회원의 아이디 또는 이메일 주소를 입력해 주세요.'; $lang->about_grant_deatil = '가입한 사용자는 cafeXE 등 분양형 가상 사이트에 가입을 한 로그인 사용자를 의미합니다.'; $lang->about_module = 'Rhymix는 기본 라이브러리를 제외한 나머지는 모두 모듈로 구성되어 있습니다. 모듈 관리 모듈은 설치된 모든 모듈을 보여주고 관리를 돕습니다.'; +$lang->admin_scope = '관리자 권한 범위'; +$lang->admin_scopes['moderate:document'] = '문서 관리'; +$lang->admin_scopes['moderate:comment'] = '댓글 관리'; +$lang->admin_scopes['config:*'] = '모듈 설정 변경'; $lang->extra_vars_is_strict = '임의입력 금지'; $lang->extra_vars_options = '선택지'; $lang->about_extra_vars_is_strict = '단일/다중 선택에서 미리 주어진 선택지만 입력할 수 있도록 합니다. 선택지를 변경할 경우 기존 게시물에 영향을 줄 수 있습니다.'; diff --git a/modules/module/models/Permission.php b/modules/module/models/Permission.php new file mode 100644 index 000000000..5a3daa591 --- /dev/null +++ b/modules/module/models/Permission.php @@ -0,0 +1,64 @@ +{$scope}) && $scope !== 'scopes') + { + return boolval($this->{$scope}); + } + + if ($this->manager && $this->scopes && preg_match('/^(\w+):(.+)$/', $scope, $matches)) + { + if ($this->scopes === true) + { + return true; + } + if (is_array($this->scopes) && in_array($scope, $this->scopes)) + { + return true; + } + if (is_array($this->scopes) && in_array($matches[1] . ':*', $this->scopes)) + { + return true; + } + } + + return false; + } +} diff --git a/modules/module/module.admin.controller.php b/modules/module/module.admin.controller.php index cc7f6b577..8e4338cfb 100644 --- a/modules/module/module.admin.controller.php +++ b/modules/module/module.admin.controller.php @@ -292,6 +292,11 @@ class ModuleAdminController extends Module // Register Admin ID $oModuleController->deleteAdminId($module_srl); $admin_member = Context::get('admin_member'); + $scopes = Context::get('admin_scopes') ?: null; + if(is_string($scopes) && $scopes !== '') + { + $scopes = explode('|@|', $scopes); + } if($admin_member) { $admin_members = explode(',',$admin_member); @@ -299,7 +304,7 @@ class ModuleAdminController extends Module { $admin_id = trim($admin_id); if(!$admin_id) continue; - $oModuleController->insertAdminId($module_srl, $admin_id); + $oModuleController->insertAdminId($module_srl, $admin_id, $scopes); } } diff --git a/modules/module/module.admin.model.php b/modules/module/module.admin.model.php index 72cd82bf1..d2e80c83f 100644 --- a/modules/module/module.admin.model.php +++ b/modules/module/module.admin.model.php @@ -197,6 +197,8 @@ class ModuleAdminModel extends Module // Extract admin ID set in the current module $admin_member = ModuleModel::getAdminId($module_srl) ?: []; Context::set('admin_member', $admin_member); + // Get defined scopes + Context::set('manager_scopes', $this->getModuleAdminScopes()); // Get a list of groups $group_list = MemberModel::getGroups(); Context::set('group_list', $group_list); @@ -286,6 +288,19 @@ class ModuleAdminModel extends Module $this->add('grantList', $grantList); } + /** + * Get defined scopes of module admin. + * + * @return array + */ + public function getModuleAdminScopes(): array + { + $obj = new \stdClass; + $obj->scopes = lang('module.admin_scopes')->getArrayCopy(); + ModuleHandler::triggerCall('module.getModuleAdminScopes', 'after', $obj); + return $obj->scopes; + } + /** * @brief Common:: skin setting page for the module */ diff --git a/modules/module/module.class.php b/modules/module/module.class.php index e19fc2dbb..8845a2ddd 100644 --- a/modules/module/module.class.php +++ b/modules/module/module.class.php @@ -148,6 +148,12 @@ class Module extends ModuleObject { return true; } + + // check scope column on module_admins table + if (!$oDB->isColumnExists('module_admins', 'scopes')) + { + return true; + } } /** @@ -311,6 +317,12 @@ class Module extends ModuleObject return $output; } } + + // check scope column on module_admins table + if (!$oDB->isColumnExists('module_admins', 'scopes')) + { + $oDB->addColumn('module_admins', 'scopes', 'text', null, null, false, 'member_srl'); + } } /** diff --git a/modules/module/module.controller.php b/modules/module/module.controller.php index 1178cc8ce..93e76cfac 100644 --- a/modules/module/module.controller.php +++ b/modules/module/module.controller.php @@ -806,7 +806,7 @@ class ModuleController extends Module /** * @brief Specify the admin ID to a module */ - function insertAdminId($module_srl, $admin_id) + function insertAdminId($module_srl, $admin_id, $scopes = null) { if (strpos($admin_id, '@') !== false) { @@ -824,6 +824,14 @@ class ModuleController extends Module $args = new stdClass(); $args->module_srl = intval($module_srl); $args->member_srl = $member_info->member_srl; + if (is_array($scopes)) + { + $args->scopes = json_encode(array_values($scopes)); + } + else + { + $args->scopes = new Rhymix\Framework\Parsers\DBQuery\NullValue; + } $output = executeQuery('module.insertAdminId', $args); Rhymix\Framework\Cache::delete("site_and_module:module_admins:" . intval($module_srl)); diff --git a/modules/module/module.model.php b/modules/module/module.model.php index 473185703..4034a53e1 100644 --- a/modules/module/module.model.php +++ b/modules/module/module.model.php @@ -1853,7 +1853,9 @@ class ModuleModel extends Module } /** - * @brief Check if a member is a module administrator + * Check if a member is a module administrator + * + * @return array|bool */ public static function isModuleAdmin($member_info, $module_srl = null) { @@ -1882,14 +1884,22 @@ class ModuleModel extends Module $module_admins = array(); foreach ($output->data as $module_admin) { - $module_admins[$module_admin->member_srl] = true; + $module_admins[$module_admin->member_srl] = $module_admin->scopes ? json_decode($module_admin->scopes) : true; } if ($output->toBool()) { Rhymix\Framework\Cache::set("site_and_module:module_admins:$module_srl", $module_admins, 0, true); } } - return isset($module_admins[$member_info->member_srl]); + + if (isset($module_admins[$member_info->member_srl])) + { + return $module_admins[$member_info->member_srl]; + } + else + { + return false; + } } /** @@ -1900,8 +1910,14 @@ class ModuleModel extends Module $obj = new stdClass(); $obj->module_srl = $module_srl; $output = executeQueryArray('module.getAdminID', $obj); - if(!$output->toBool() || !$output->data) return; - + if (!$output->toBool() || !$output->data) + { + return; + } + foreach ($output->data as $row) + { + $row->scopes = !empty($row->scopes) ? json_decode($row->scopes) : null; + } return $output->data; } @@ -2129,7 +2145,12 @@ class ModuleModel extends Module } /** - * @brief Return privileges(granted) information by using module info, xml info and member info + * Get privileges(granted) information by using module info, xml info and member info + * + * @param object $module_info + * @param object $member_info + * @param ?object $xml_info + * @return Rhymix\Modules\Module\Models\Permission */ public static function getGrant($module_info, $member_info, $xml_info = null) { @@ -2148,8 +2169,6 @@ class ModuleModel extends Module } } - $grant = new stdClass; - // Get information of module.xml if(!$xml_info) { @@ -2172,6 +2191,7 @@ class ModuleModel extends Module $privilege_list = array_unique($privilege_list, SORT_STRING); // Grant first + $grant = new Rhymix\Modules\Module\Models\Permission; foreach($privilege_list as $val) { // If an administrator, grant all @@ -2180,7 +2200,7 @@ class ModuleModel extends Module $grant->{$val} = true; } // If a module manager, grant all (except 'root', 'is_admin') - else if($is_module_admin === true && $val !== 'root' && $val !== 'is_admin') + elseif ($is_module_admin && $val !== 'root' && $val !== 'is_admin') { $grant->{$val} = true; } @@ -2196,6 +2216,20 @@ class ModuleModel extends Module } } + // If module admin, add scopes + if ($member_info && $member_info->is_admin == 'Y') + { + $grant->scopes = true; + } + elseif ($is_module_admin) + { + $grant->scopes = $is_module_admin; + } + else + { + $grant->scopes = []; + } + // If access were not granted, check more if(!$grant->access) { diff --git a/modules/module/queries/getModuleAdmin.xml b/modules/module/queries/getModuleAdmin.xml index 55f163796..ef91b3f6c 100644 --- a/modules/module/queries/getModuleAdmin.xml +++ b/modules/module/queries/getModuleAdmin.xml @@ -2,6 +2,9 @@ + + + diff --git a/modules/module/queries/insertAdminId.xml b/modules/module/queries/insertAdminId.xml index 60f1ab088..ed510fcb0 100644 --- a/modules/module/queries/insertAdminId.xml +++ b/modules/module/queries/insertAdminId.xml @@ -5,6 +5,7 @@ + diff --git a/modules/module/schemas/module_admins.xml b/modules/module/schemas/module_admins.xml index 7de4e5545..b98994a83 100644 --- a/modules/module/schemas/module_admins.xml +++ b/modules/module/schemas/module_admins.xml @@ -1,5 +1,6 @@
+
diff --git a/modules/module/schemas/task_queue.xml b/modules/module/schemas/task_queue.xml index 73e18ab33..0a0d4cd18 100644 --- a/modules/module/schemas/task_queue.xml +++ b/modules/module/schemas/task_queue.xml @@ -3,5 +3,5 @@ - + diff --git a/modules/module/tpl/module_grants.html b/modules/module/tpl/module_grants.html index 630c44748..3fd88cc3d 100644 --- a/modules/module/tpl/module_grants.html +++ b/modules/module/tpl/module_grants.html @@ -8,7 +8,7 @@
- +

{$lang->module_admin}

@@ -34,6 +34,21 @@

{$lang->about_admin_id}

+
+ +
+ {@ $default_scopes = array_keys($manager_scopes)} + {@ $admin_scopes = $admin_member ? (array_first($admin_member)->scopes ?? $default_scopes) : $default_scopes} + + + +
+
diff --git a/modules/page/m.skins/default/mobile.html b/modules/page/m.skins/default/mobile.html index 02011ab25..1fbdf370c 100644 --- a/modules/page/m.skins/default/mobile.html +++ b/modules/page/m.skins/default/mobile.html @@ -1,2 +1,2 @@

{$oDocument->getTitle()}

-{$oDocument->getContent(($module_info->display_popupmenu ?? '') !== 'hide')} +{$oDocument->getContent(false)} diff --git a/modules/page/tpl/index.html b/modules/page/tpl/index.html index 802486361..25190e960 100644 --- a/modules/page/tpl/index.html +++ b/modules/page/tpl/index.html @@ -58,7 +58,7 @@ jQuery(function($){ - {$no} + {$val->module_srl} diff --git a/modules/point/conf/module.xml b/modules/point/conf/module.xml index 159469433..a455bbc0b 100644 --- a/modules/point/conf/module.xml +++ b/modules/point/conf/module.xml @@ -3,15 +3,15 @@ - + - + - + diff --git a/modules/rss/conf/module.xml b/modules/rss/conf/module.xml index 182f727c1..2bd09130c 100644 --- a/modules/rss/conf/module.xml +++ b/modules/rss/conf/module.xml @@ -7,7 +7,7 @@ - + diff --git a/modules/spamfilter/conf/module.xml b/modules/spamfilter/conf/module.xml index 76ed5c668..00440ac48 100644 --- a/modules/spamfilter/conf/module.xml +++ b/modules/spamfilter/conf/module.xml @@ -17,11 +17,15 @@ + - + + + + diff --git a/modules/spamfilter/lang/en.php b/modules/spamfilter/lang/en.php index af87ce170..8d09bb889 100644 --- a/modules/spamfilter/lang/en.php +++ b/modules/spamfilter/lang/en.php @@ -14,10 +14,11 @@ $lang->word = 'Keyword'; $lang->hit = 'Hit'; $lang->latest_hit = 'Latest Hits'; $lang->custom_message = 'Error Message'; +$lang->enable_description = 'Enter # as description'; $lang->about_custom_message = 'You can customize the error message that will be displayed if a spam keyword is found.
%s can be used as a placeholder for the keyword. If not used, the keyword will be hidden.'; $lang->about_interval = 'All articles attempted for posting within the assigned time will be blocked.'; $lang->about_denied_ip = 'Please enter one IP address (e.g. 127.0.0.1) or range (e.g. 127.0.0.0/24) per line. Comments may start with // or #.'; -$lang->about_denied_word = 'Please enter one keyword (2 to 180 characters) per line. Comments may start with #.
Formats such as /spam(key|word)?/ will be treated as a regular expression, and must use the proper syntax.
Spam keywords are not case sensitive.'; +$lang->about_denied_word = 'Please enter one keyword (2 to 180 characters) per line. Comments start with #. If you need to block a keyword that includes #, disable the checkbox above.
Formats such as /spam(key|word)?/ will be treated as a regular expression, and must use the proper syntax.
Spam keywords are not case sensitive.'; $lang->msg_alert_limited_by_config = 'Please do not post repeatedly within %d seconds. If you keep trying, your IP address will be blocked.'; $lang->msg_alert_limited_message_by_config = 'Please do not send messages repeatedly within %d seconds. If you keep trying, your IP address will be blocked.'; $lang->msg_alert_denied_word = 'The word "%s" is not allowed on this site.'; @@ -27,7 +28,9 @@ $lang->cmd_spamfilter_except_member = 'Except Members'; $lang->cmd_spamfilter_filter_html = 'HTML'; $lang->cmd_spamfilter_is_regexp = 'REGEXP'; $lang->cmd_interval = 'Block Post/Comment Spam'; -$lang->cmd_interval_help = 'Block IP addresses that post or comment too much in a short time. Blocked IP addresses will not be able to post, comment, or send messages.'; +$lang->cmd_interval_help = 'Block IP addresses that post or comment too much in a short time.'; +$lang->cmd_blocked_actions = 'Blocked actions'; +$lang->cmd_blocked_actions_help = 'The actions above will be disabled from blocked IP addresses.'; $lang->cmd_check_trackback = 'Block Trackback Spam'; $lang->cmd_check_trackback_help = 'Block IP addresses that send multiple trackbacks to the same document.
This only works if the trackback module is installed.'; $lang->cmd_limits_interval = 'Block Interval'; diff --git a/modules/spamfilter/lang/ko.php b/modules/spamfilter/lang/ko.php index 82f13a8f7..3b2e1dfb6 100644 --- a/modules/spamfilter/lang/ko.php +++ b/modules/spamfilter/lang/ko.php @@ -14,10 +14,11 @@ $lang->word = '키워드'; $lang->hit = '히트'; $lang->latest_hit = '최근 히트'; $lang->custom_message = '차단 메시지 설정'; +$lang->enable_description = '# 뒷부분은 설명으로 입력'; $lang->about_custom_message = '스팸 키워드 발견시 표시할 에러 메시지를 지정할 수 있습니다.
%s를 넣으면 그 자리에 해당 키워드를 표시하고, 그렇지 않으면 키워드를 숨깁니다.'; $lang->about_interval = '지정된 시간 내에 글을 등록하지 못하게 합니다.'; $lang->about_denied_ip = '한 줄에 하나씩 IP 주소 또는 대역을 입력하세요. "//" 또는 "#" 뒷부분은 설명으로 저장됩니다.
예: 127.0.0.1 // 설명, 127.0.0.1 #설명
IP 대역 표기법은 매뉴얼을 참고하십시오.'; -$lang->about_denied_word = '한 줄에 하나씩 스팸 키워드(2~180자)를 입력하세요. "#" 뒷부분은 설명으로 입력됩니다.
/스팸(키+|워드)?/ 와 같은 형태로 입력하면 정규식으로 간주하며, 올바른 정규식 문법을 사용해야 합니다.
대소문자는 구분하지 않습니다.'; +$lang->about_denied_word = '한 줄에 하나씩 스팸 키워드(2~180자)를 입력하세요. "#" 뒷부분은 설명으로 입력됩니다. "#"을 포함하는 키워드를 차단하려면 위의 설정을 해제하세요.
/스팸(키+|워드)?/ 와 같은 형태로 입력하면 정규식으로 간주하며, 올바른 정규식 문법을 사용해야 합니다.
대소문자는 구분하지 않습니다.'; $lang->msg_alert_limited_by_config = '%d초 이내에 연속 글 작성은 금지됩니다. 계속 시도하면 IP가 차단될 수 있습니다.'; $lang->msg_alert_limited_message_by_config = '%d초 이내에 연속 쪽지 발송은 금지됩니다. 계속 시도하면 IP가 차단될 수 있습니다.'; $lang->msg_alert_denied_word = '"%s"은(는) 사용이 금지된 단어입니다.'; @@ -26,8 +27,10 @@ $lang->msg_alert_trackback_denied = '한 글에는 하나의 트랙백만 허용 $lang->cmd_spamfilter_except_member = '회원 제외'; $lang->cmd_spamfilter_filter_html = 'HTML'; $lang->cmd_spamfilter_is_regexp = '정규식'; -$lang->cmd_interval = '글, 댓글 스팸 차단'; -$lang->cmd_interval_help = '지정한 시간 내에 다수의 글이나 댓글을 작성하면 스패머로 간주하고 글, 댓글 작성과 엮인글 발송, 쪽지 발송을 차단합니다.'; +$lang->cmd_interval = '단시간 다수 작성 차단'; +$lang->cmd_interval_help = '지정한 시간 내에 다수의 글이나 댓글을 작성하면 스패머로 간주하고 IP를 차단합니다.'; +$lang->cmd_blocked_actions = '차단할 행동'; +$lang->cmd_blocked_actions_help = '차단된 IP에서는 위의 행동들을 할 수 없게 됩니다.'; $lang->cmd_check_trackback = '트랙백 스팸 차단'; $lang->cmd_check_trackback_help = '하나의 글에 2회 이상 엮인글을 등록하면 스패머로 간주하고 엮인글을 차단합니다.
트랙백 모듈이 설치되어 있는 경우에만 적용됩니다.'; $lang->cmd_limits_interval = '글, 댓글 제한 시간'; diff --git a/modules/spamfilter/spamfilter.admin.controller.php b/modules/spamfilter/spamfilter.admin.controller.php index e4887f600..38e99cab0 100644 --- a/modules/spamfilter/spamfilter.admin.controller.php +++ b/modules/spamfilter/spamfilter.admin.controller.php @@ -20,7 +20,7 @@ class SpamfilterAdminController extends Spamfilter $config = ModuleModel::getModuleConfig('spamfilter') ?: new stdClass; // Get the default information - $args = Context::gets('limits', 'limits_interval', 'limits_count', 'ipv4_block_range', 'ipv6_block_range', 'except_ip', 'custom_message'); + $args = Context::gets('limits', 'limits_interval', 'limits_count', 'blocked_actions', 'ipv4_block_range', 'ipv6_block_range', 'except_ip', 'custom_message'); // Set default values if($args->limits != 'Y') @@ -38,6 +38,7 @@ class SpamfilterAdminController extends Spamfilter $args->except_ip = array_map('trim', preg_split('/[\n,]/', trim($args->except_ip ?? ''), -1, \PREG_SPLIT_NO_EMPTY)); $args->limits_interval = intval($args->limits_interval); $args->limits_count = intval($args->limits_count); + $args->blocked_actions = array_values($args->blocked_actions ?? []); $args->custom_message = escape(utf8_trim($args->custom_message)); foreach ($args as $key => $val) { @@ -177,9 +178,10 @@ class SpamfilterAdminController extends Spamfilter { //스팸 키워드 추가 $word_list = Context::get('word_list'); + $enable_description = Context::get('enable_description') ?? 'N'; if($word_list) { - $output = $this->insertWord($word_list); + $output = $this->insertWord($word_list, $enable_description); if(!$output->toBool() && !$output->get('fail_list')) return $output; if($output->get('fail_list')) $message_fail = ''.sprintf(lang('msg_faillist'),$output->get('fail_list')).''; @@ -258,7 +260,7 @@ class SpamfilterAdminController extends Spamfilter * @brief Register the spam word * The post, which contains the newly registered spam word, should be considered as a spam */ - public function insertWord($word_list) + public function insertWord($word_list, $enable_description = 'Y') { if (!is_array($word_list)) { @@ -273,7 +275,7 @@ class SpamfilterAdminController extends Spamfilter { continue; } - if (preg_match('/^(.+?)#(.+)$/', $word, $matches)) + if ($enable_description === 'Y' && preg_match('/^(.+?)#(.+)$/', $word, $matches)) { $word = trim($matches[1]); $description = trim($matches[2]); diff --git a/modules/spamfilter/spamfilter.controller.php b/modules/spamfilter/spamfilter.controller.php index 815bead6a..4aacea483 100644 --- a/modules/spamfilter/spamfilter.controller.php +++ b/modules/spamfilter/spamfilter.controller.php @@ -50,10 +50,17 @@ class SpamfilterController extends Spamfilter if($grant->manager) return; } - $oFilterModel = getModel('spamfilter'); // Check if the IP is prohibited - $output = $oFilterModel->isDeniedIP(); - if(!$output->toBool()) return $output; + $output = SpamfilterModel::isDeniedIP(); + if(!$output->toBool()) + { + $config = SpamfilterModel::getConfig(); + if (!isset($config->blocked_actions) || in_array('document', $config->blocked_actions)) + { + return $output; + } + } + // Check if there is a ban on the word $filter_targets = [$obj->title, $obj->content, $obj->tags ?? '']; if(!$is_logged) @@ -71,7 +78,7 @@ class SpamfilterController extends Spamfilter } } } - $output = $oFilterModel->isDeniedWord(implode("\n", $filter_targets)); + $output = SpamfilterModel::isDeniedWord(implode("\n", $filter_targets)); if(!$output->toBool()) { return $output; @@ -79,7 +86,7 @@ class SpamfilterController extends Spamfilter // Check the specified time beside the modificaiton time if($obj->document_srl == 0) { - $output = $oFilterModel->checkLimited(); + $output = SpamfilterModel::checkLimited(); if(!$output->toBool()) return $output; } // Save a log @@ -103,10 +110,17 @@ class SpamfilterController extends Spamfilter if($grant->manager) return; } - $oFilterModel = getModel('spamfilter'); // Check if the IP is prohibited - $output = $oFilterModel->isDeniedIP(); - if(!$output->toBool()) return $output; + $output = SpamfilterModel::isDeniedIP(); + if(!$output->toBool()) + { + $config = SpamfilterModel::getConfig(); + if (!isset($config->blocked_actions) || in_array('comment', $config->blocked_actions)) + { + return $output; + } + } + // Check if there is a ban on the word if($is_logged) { @@ -116,12 +130,12 @@ class SpamfilterController extends Spamfilter { $text = $obj->content . ' ' . $obj->nick_name . ' ' . $obj->homepage; } - $output = $oFilterModel->isDeniedWord($text); + $output = SpamfilterModel::isDeniedWord($text); if(!$output->toBool()) return $output; // If the specified time check is not modified if(!$obj->__isupdate) { - $output = $oFilterModel->checkLimited(); + $output = SpamfilterModel::checkLimited(); if(!$output->toBool()) return $output; } unset($obj->__isupdate); @@ -182,31 +196,101 @@ class SpamfilterController extends Spamfilter return $output; } + /** + * Block voting from a spam IP. + */ + function triggerVote(&$obj) + { + if (!empty($_SESSION['avoid_log'])) + { + return; + } + + if ($this->user->isAdmin() || (Context::get('grant')->manager ?? false)) + { + return; + } + + $config = SpamfilterModel::getConfig(); + if ($obj->point > 0 && isset($config->blocked_actions) && !in_array('vote_up', $config->blocked_actions)) + { + return; + } + if ($obj->point < 0 && isset($config->blocked_actions) && !in_array('vote_down', $config->blocked_actions)) + { + return; + } + + $output = SpamfilterModel::isDeniedIP(); + if (!$output->toBool()) + { + return $output; + } + } + + /** + * Block reporting from a spam IP. + */ + function triggerDeclare(&$obj) + { + if (!empty($_SESSION['avoid_log'])) + { + return; + } + + if ($this->user->isAdmin() || (Context::get('grant')->manager ?? false)) + { + return; + } + + $config = SpamfilterModel::getConfig(); + if (isset($config->blocked_actions) && !in_array('declare', $config->blocked_actions)) + { + return; + } + + $output = SpamfilterModel::isDeniedIP(); + if (!$output->toBool()) + { + return $output; + } + } + /** * @brief The routine process to check the time it takes to store a message, when writing it, and to ban IP/word */ function triggerSendMessage(&$obj) { - if($_SESSION['avoid_log']) return; + if($this->user->isAdmin() || !empty($_SESSION['avoid_log'])) + { + return; + } + if(isset($obj->use_spamfilter) && $obj->use_spamfilter === false) { return; } - $logged_info = Context::get('logged_info'); - if($logged_info->is_admin == 'Y') return; - - $oFilterModel = getModel('spamfilter'); // Check if the IP is prohibited - $output = $oFilterModel->isDeniedIP(); - if(!$output->toBool()) return $output; + $output = SpamfilterModel::isDeniedIP(); + if(!$output->toBool()) + { + $config = SpamfilterModel::getConfig(); + if (!isset($config->blocked_actions) || in_array('message', $config->blocked_actions)) + { + return $output; + } + } + // Check if there is a ban on the word $text = $obj->title . ' ' . $obj->content; - $output = $oFilterModel->isDeniedWord($text); + $output = SpamfilterModel::isDeniedWord($text); if(!$output->toBool()) return $output; + // Check the specified time - $output = $oFilterModel->checkLimited(TRUE); + $output = SpamfilterModel::checkLimited(TRUE); if(!$output->toBool()) return $output; + // Save a log $this->insertLog(); } diff --git a/modules/spamfilter/spamfilter.model.php b/modules/spamfilter/spamfilter.model.php index feee6bf42..dcd23d6cb 100644 --- a/modules/spamfilter/spamfilter.model.php +++ b/modules/spamfilter/spamfilter.model.php @@ -7,17 +7,10 @@ */ class SpamfilterModel extends Spamfilter { - /** - * @brief Initialization - */ - function init() - { - } - /** * @brief Return the user setting values of the Spam filter module */ - function getConfig() + public static function getConfig() { return ModuleModel::getModuleConfig('spamfilter') ?: new stdClass; } @@ -25,7 +18,7 @@ class SpamfilterModel extends Spamfilter /** * @brief Return the list of registered IP addresses which were banned */ - function getDeniedIPList($sort_index = 'regdate') + public static function getDeniedIPList($sort_index = 'regdate') { $args = new stdClass(); $args->sort_index = $sort_index; @@ -38,12 +31,12 @@ class SpamfilterModel extends Spamfilter /** * @brief Check if the ipaddress is in the list of banned IP addresses */ - function isDeniedIP() + public static function isDeniedIP() { $ip_list = Rhymix\Framework\Cache::get('spamfilter:denied_ip_list'); if ($ip_list === null) { - $ip_list = $this->getDeniedIPList(); + $ip_list = self::getDeniedIPList(); Rhymix\Framework\Cache::set('spamfilter:denied_ip_list', $ip_list); } if (!count($ip_list)) @@ -75,7 +68,7 @@ class SpamfilterModel extends Spamfilter /** * @brief Return the list of registered Words which were banned */ - function getDeniedWordList($sort_index = 'hit') + public static function getDeniedWordList($sort_index = 'hit') { $args = new stdClass(); $args->sort_index = $sort_index; @@ -86,12 +79,12 @@ class SpamfilterModel extends Spamfilter /** * @brief Check if the text, received as a parameter, is banned or not */ - function isDeniedWord($text) + public static function isDeniedWord($text) { $word_list = Rhymix\Framework\Cache::get('spamfilter:denied_word_list'); if ($word_list === null) { - $word_list = $this->getDeniedWordList(); + $word_list = self::getDeniedWordList(); Rhymix\Framework\Cache::set('spamfilter:denied_word_list', $word_list); } if (!count($word_list)) @@ -128,7 +121,7 @@ class SpamfilterModel extends Spamfilter $args->word = $word; executeQuery('spamfilter.updateDeniedWordHit', $args); - $config = $this->getConfig(); + $config = self::getConfig(); if($config->custom_message) { @@ -161,9 +154,9 @@ class SpamfilterModel extends Spamfilter /** * @brief Check the specified time */ - function checkLimited($isMessage = FALSE) + public static function checkLimited($isMessage = FALSE) { - $config = $this->getConfig(); + $config = self::getConfig(); if($config->limits != 'Y') return new BaseObject(); $limit_count = $config->limits_count ?: 3; @@ -177,7 +170,7 @@ class SpamfilterModel extends Spamfilter } } - $count = $this->getLogCount($interval); + $count = self::getLogCount($interval); // Ban the IP address if the interval is exceeded if($count>=$limit_count) @@ -272,7 +265,7 @@ class SpamfilterModel extends Spamfilter /** * @brief Check if the trackbacks have already been registered to a particular article */ - function isInsertedTrackback($document_srl) + public static function isInsertedTrackback($document_srl) { $oTrackbackModel = getModel('trackback'); if (is_object($oTrackbackModel) && method_exists($oTrackbackModel, 'getTrackbackCountByIPAddress')) @@ -289,7 +282,7 @@ class SpamfilterModel extends Spamfilter /** * @brief Return the number of logs recorded within the interval for the specified IPaddress */ - function getLogCount($time = 60, $ipaddress='') + public static function getLogCount($time = 60, $ipaddress='') { if(!$ipaddress) $ipaddress = \RX_CLIENT_IP; diff --git a/modules/spamfilter/tpl/config_block.html b/modules/spamfilter/tpl/config_block.html index 3ffbf8f10..d94daa11b 100644 --- a/modules/spamfilter/tpl/config_block.html +++ b/modules/spamfilter/tpl/config_block.html @@ -27,6 +27,36 @@

{$lang->cmd_interval_help}

+
+ +
+ + + + + + +

{$lang->cmd_blocked_actions_help}

+
+
diff --git a/modules/spamfilter/tpl/denied_word_list.html b/modules/spamfilter/tpl/denied_word_list.html index e1f0b9904..eb51e9bdd 100644 --- a/modules/spamfilter/tpl/denied_word_list.html +++ b/modules/spamfilter/tpl/denied_word_list.html @@ -44,6 +44,7 @@ +

{$lang->about_denied_word}

diff --git a/tests/unit/framework/SMSTest.php b/tests/unit/framework/SMSTest.php index 4c51e2e42..df947740a 100644 --- a/tests/unit/framework/SMSTest.php +++ b/tests/unit/framework/SMSTest.php @@ -16,13 +16,11 @@ class SMSTest extends \Codeception\Test\Unit { $drivers = Rhymix\Framework\SMS::getSupportedDrivers(); $this->assertTrue(isset($drivers['dummy'])); - $this->assertTrue(isset($drivers['apistore'])); $this->assertTrue(isset($drivers['coolsms'])); $this->assertTrue(isset($drivers['iwinv'])); $this->assertTrue(isset($drivers['solapi'])); $this->assertTrue(isset($drivers['ppurio'])); $this->assertEquals('Dummy', $drivers['dummy']['name']); - $this->assertTrue(in_array('api_user', $drivers['apistore']['required'])); $this->assertTrue(in_array('api_key', $drivers['coolsms']['required'])); $this->assertTrue(in_array('api_url', $drivers['iwinv']['required'])); $this->assertTrue(in_array('api_user', $drivers['ppurio']['required'])); diff --git a/widgets/content/content.class.php b/widgets/content/content.class.php index 54e281f8f..37e5594b8 100644 --- a/widgets/content/content.class.php +++ b/widgets/content/content.class.php @@ -187,7 +187,7 @@ class content extends WidgetHandler $obj->sort_index = $args->order_target; $obj->list_count = $args->list_count * $args->page_count; $obj->statusList = [1]; - if($args->show_secret != 'Y') + if(($args->show_secret ?? 'N') !== 'Y') { $obj->is_secret = 'N'; } @@ -201,7 +201,7 @@ class content extends WidgetHandler foreach($output as $key => $oComment) { $oDocument = getModel('document')->getDocument($oComment->get('document_srl'), false, false); - if(!$oDocument->isExists() || $oDocument->isSecret() && $args->show_secret != 'Y') + if(!$oDocument->isExists() || $oDocument->isSecret() && ($args->show_secret ?? 'N') !== 'Y') { continue; } @@ -256,7 +256,7 @@ class content extends WidgetHandler $obj->order_type = $args->order_type=="desc"?"desc":"asc"; } - if($args->show_secret == 'Y') + if(($args->show_secret ?? 'N') == 'Y') { $obj->statusList = array('PUBLIC', 'SECRET'); } diff --git a/widgets/content/skins/default/image_title_content.html b/widgets/content/skins/default/image_title_content.html index a3307dc6f..95e34c5f5 100644 --- a/widgets/content/skins/default/image_title_content.html +++ b/widgets/content/skins/default/image_title_content.html @@ -44,7 +44,7 @@ {$item->printExtraImages()} - + {$item->getRegdate("Y-m-d")} {$item->getRegdate("H:i")}

@@ -52,7 +52,7 @@

{$item->getContent()} - + {$item->getRegdate("Y-m-d")} {$item->getRegdate("H:i")}

@@ -60,7 +60,7 @@

{$item->getNickName($widget_info->nickname_cut_size)} - + {$item->getRegdate("Y-m-d")} {$item->getRegdate("H:i")}