Merge branch 'rhymix:master' into develop

This commit is contained in:
Lastorder 2024-11-22 09:24:12 +09:00 committed by GitHub
commit 6e84829da4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
80 changed files with 656 additions and 440 deletions

View file

@ -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'),

View file

@ -95,7 +95,7 @@ class CacheReset extends Base
// If possible, use system command to speed up recursive deletion
if (function_exists('exec') && !preg_match('/(?<!_)exec/', ini_get('disable_functions')))
{
if (strncasecmp(\PHP_OS, 'win', 3) == 0)
if (\RX_WINDOWS)
{
@exec('rmdir /S /Q ' . escapeshellarg($tmp_dir));
}
@ -115,6 +115,12 @@ class CacheReset extends Base
$oAutoinstallAdminController = getAdminController('autoinstall');
$oAutoinstallAdminController->checkInstalled();
// Opcache reset
if (function_exists('opcache_reset'))
{
opcache_reset();
}
$this->setMessage('success_updated');
}
}

View file

@ -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;
}

View file

@ -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())

View file

@ -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);

View file

@ -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.<br />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.<br>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.<br>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.<br>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.<br>All tasks are processed as soon as possible regardless of the interval, but a short interval means quick recovery from any error.<br>For web-based cron, this should not exceed the max_execution_time setting in php.ini.<br>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 <code>%s</code> account and run <code>crontab -e</code> to paste the following content into your crontab. (DO NOT run it as root!)<br>The <code>%s</code> 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 <code>%s</code> account and run <code>crontab -e</code> to paste the following content into your crontab. <span style="color:red">(DO NOT run it as root!)</span><br>If the account shown above cannot be logged into, such as <code>apache</code> or <code>www-data</code>, try running <code>sudo crontab -e -u %s</code> from a different account.';
$lang->msg_queue_instructions['crontab2'] = 'The <code>%s</code> directory in the example should be replaced with a path where logs can be recorded.<br>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.<br>Check your logs to make sure that the cron service is reaching your website.';
$lang->msg_queue_instructions['systemd1'] = 'Put the following content in <code>/etc/systemd/system/rhymix-queue.service</code>';
$lang->msg_queue_instructions['systemd2'] = 'Put the following content in <code>/etc/systemd/system/rhymix-queue.timer</code>';

View file

@ -204,6 +204,9 @@ $lang->cache_truncate_method_empty = '캐시 내용만 삭제';
$lang->about_cache_truncate_method = '캐시 폴더를 삭제하는 방법이 더 빠르고 안정적입니다.<br />내용만 삭제하는 방법은 램디스크를 캐시 폴더로 사용하는 등 폴더 자체를 삭제해서는 안 되는 경우에만 선택하십시오.';
$lang->cache_control_header = '캐시 컨트롤 헤더';
$lang->about_cache_control_header = '브라우저 캐시를 적용하지 않을 일반 HTML 페이지에 적용할 Cache-Control 헤더 내용을 선택할 수 있습니다.<br>선택을 해제하면 뒤로가기 등 특정한 상황에서 성능이 개선될 수도 있지만, 오래된 정보가 노출되는 등 부작용이 발생할 수도 있습니다.';
$lang->outgoing_proxy = '외부 요청 프록시';
$lang->about_outgoing_proxy = '외부 요청시 프록시를 사용하여 서버 IP 노출을 방지합니다.<br>코어에서 제공하는 클래스와 함수를 사용하지 않고 외부 요청을 자체 구현한 서드파티 자료에는 적용되지 않습니다.';
$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 = '비동기 작업을 관리할 방법을 설정합니다. 호스팅 환경과 사이트의 필요에 맞추어 선택하세요.<br>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, 웹크론 등을 사용하여 일정한 주기로 스크립트를 호출해 주십시오.<br>모든 비동기 작업은 호출 간격과 무관하게 실시간으로 처리되나, 호출 간격이 짧으면 장애 발생시 신속하게 복구됩니다.<br>웹크론 사용시에는 php.ini의 실행 시간 제한을 초과하지 않는 것이 좋습니다.<br>이 서버의 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 계정으로 서버에 로그인하여 <code>crontab -e</code> 명령을 실행한 후, 아래의 내용을 붙여넣으십시오. (root 권한으로 실행하지 마십시오.)<br>예제의 <code>%s</code> 디렉토리는 로그를 기록할 수 있는 경로로 변경하여 사용하십시오.';
$lang->msg_queue_instructions['crontab2'] = '스크립트 호출 간격을 변경할 경우, 설정에 맞추어 crontab 실행 간격도 조절하여야 합니다.';
$lang->msg_queue_instructions['crontab1'] = '<code>%s</code> 계정으로 서버에 로그인하여 <code>crontab -e</code> 명령을 실행한 후, 아래의 내용을 붙여넣으십시오. <span style="color:red">(root 권한으로 실행하지 마십시오!)</span><br>만약 <code>apache</code>나 <code>www-data</code>처럼 로그인할 수 없는 계정이라면, 다른 계정에서 <code>sudo crontab -e -u %s</code> 명령을 실행해 볼 수 있습니다.';
$lang->msg_queue_instructions['crontab2'] = '예제의 <code>%s</code> 디렉토리는 로그를 기록할 권한이 있는 경로로 변경하여 사용하십시오.<br>스크립트 호출 간격을 변경할 경우, 설정에 맞추어 crontab 실행 간격도 조절하여야 합니다.';
$lang->msg_queue_instructions['webcron'] = '아래의 URL을 1분 간격 또는 아래에서 설정한 호출 간격에 맞추어 GET으로 호출하도록 합니다.<br>웹크론 서비스가 방화벽이나 CDN 등에 의해 차단되지 않도록 주의하고, 정상적으로 호출되는지 서버 로그를 확인하십시오.';
$lang->msg_queue_instructions['systemd1'] = '<code>/etc/systemd/system/rhymix-queue.service</code> 파일에 아래와 같은 내용을 넣습니다.';
$lang->msg_queue_instructions['systemd2'] = '<code>/etc/systemd/system/rhymix-queue.timer</code> 파일에 아래와 같은 내용을 넣습니다.';

View file

@ -201,6 +201,13 @@
<p class="x_help-block">{$lang->about_cache_control_header}</p>
</div>
</div>
<div class="x_control-group">
<label class="x_control-label" for="outgoing_proxy">{$lang->outgoing_proxy}</label>
<div class="x_controls">
<input type="text" name="outgoing_proxy" id="outgoing_proxy" value="{$outgoing_proxy}" />
<p class="x_help-block">{$lang->about_outgoing_proxy}</p>
</div>
</div>
<div class="x_control-group">
<label class="x_control-label" for="partial_page_rendering">{$lang->cmd_partial_page_rendering}</label>
<div class="x_controls">

View file

@ -116,11 +116,11 @@
endif;
}
<p class="qss-instruction">
{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}
</p>
<pre><code>* * * * * php {\RX_BASEDIR}index.php common.cron &gt;&gt; {$user_info['dir']}logs{\DIRECTORY_SEPARATOR}queue.log 2&gt;&amp;1</code></pre>
<pre><code>* * * * * /usr/bin/php {\RX_BASEDIR}index.php common.cron &gt;&gt; {$user_info['dir']}logs{\DIRECTORY_SEPARATOR}queue.log 2&gt;&amp;1</code></pre>
<p class="qss-instruction">
{$lang->msg_queue_instructions['crontab2']|noescape}
{sprintf($lang->msg_queue_instructions['crontab2'], $user_info['dir'] . 'logs')|noescape}
</p>
</div>
<div class="qss-content webcron">
@ -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']}</code></pre>
<p class="qss-instruction">
{$lang->msg_queue_instructions['systemd2']|noescape}
@ -167,6 +167,21 @@ systemctl enable rhymix-queue.timer</code></pre>
</div>
</div>
<div class="x_control-group">
<label class="x_control-label">{$lang->cmd_queue_webcron_display_errors}</label>
<div class="x_controls">
<label class="x_inline">
<input type="radio" name="webcron_display_errors" value="Y" checked="checked"|cond="config('queue.display_errors') !== false" />
{$lang->cmd_yes}
</label>
<label class="x_inline">
<input type="radio" name="webcron_display_errors" value="N" checked="checked"|cond="config('queue.display_errors') === false" />
{$lang->cmd_no}
</label>
<p class="x_help-block">{$lang->cmd_queue_webcron_display_errors_help}</p>
</div>
</div>
<div class="x_control-group">
<label class="x_control-label" for="queue_interval">{$lang->cmd_queue_interval}</label>
<div class="x_controls">

View file

@ -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()
{
}
}

View file

@ -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 */

View file

@ -3,11 +3,12 @@
<table name="admin_log" />
</tables>
<columns>
<column name="ipaddress" var="ipaddress" notnull="notnull" />
<column name="regdate" var="regdate" />
<column name="site_srl" var="siteSrl" filter="number" default="0" />
<column name="site_srl" var="site_srl" filter="number" default="0" />
<column name="member_srl" var="member_srl" filter="number" default="0" />
<column name="module" var="module" />
<column name="act" var="act" />
<column name="request_vars" var="requestVars" />
<column name="request_vars" var="request_vars" />
<column name="regdate" var="regdate" default="curdate()" />
<column name="ipaddress" var="ipaddress" default="ipaddress()" />
</columns>
</query>

View file

@ -1,8 +1,10 @@
<table name="admin_log">
<column name="ipaddress" type="varchar" size="60" notnull="notnull" index="idx_admin_ip" />
<column name="regdate" type="date" index="idx_admin_date" />
<column name="site_srl" type="number" size="11" default="0" />
<column name="id" type="number" notnull="notnull" primary_key="primary_key" auto_increment="auto_increment" />
<column name="site_srl" type="number" default="0" />
<column name="member_srl" type="number" default="0" index="idx_member_srl" />
<column name="module" type="varchar" size="100" />
<column name="act" type="varchar" size="100" />
<column name="request_vars" type="text" />
<column name="request_vars" type="bigtext" />
<column name="regdate" type="date" index="idx_admin_date" />
<column name="ipaddress" type="varchar" size="60" notnull="notnull" index="idx_admin_ip" />
</table>

View file

@ -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')

View file

@ -116,21 +116,21 @@
<action name="dispBoardAdminContent" type="view" admin_index="true" menu_name="board" menu_index="true" />
<action name="dispBoardAdminInsertBoard" type="view" setup_index="true" menu_name="board" />
<action name="dispBoardAdminDeleteBoard" type="view" menu_name="board" />
<action name="dispBoardAdminBoardInfo" type="view" permission="manager" menu_name="board" />
<action name="dispBoardAdminCategoryInfo" type="view" permission="manager" menu_name="board" />
<action name="dispBoardAdminExtraVars" type="view" permission="manager" menu_name="board" />
<action name="dispBoardAdminGrantInfo" type="view" permission="manager" menu_name="board" />
<action name="dispBoardAdminBoardAdditionSetup" type="view" permission="manager" menu_name="board" />
<action name="dispBoardAdminSkinInfo" type="view" permission="manager" menu_name="board" />
<action name="dispBoardAdminMobileSkinInfo" type="view" permission="manager" menu_name="board" />
<action name="dispBoardAdminBoardInfo" type="view" permission="manager:config:*" menu_name="board" />
<action name="dispBoardAdminCategoryInfo" type="view" permission="manager:config:*" menu_name="board" />
<action name="dispBoardAdminExtraVars" type="view" permission="manager:config:*" menu_name="board" />
<action name="dispBoardAdminGrantInfo" type="view" permission="manager:config:*" menu_name="board" />
<action name="dispBoardAdminBoardAdditionSetup" type="view" permission="manager:config:*" menu_name="board" />
<action name="dispBoardAdminSkinInfo" type="view" permission="manager:config:*" menu_name="board" />
<action name="dispBoardAdminMobileSkinInfo" type="view" permission="manager:config:*" menu_name="board" />
<action name="getBoardAdminSimpleSetup" type="model" simple_setup_index="true" />
<action name="procBoardAdminInsertBoard" type="controller" permission="manager" check_var="module_srl" ruleset="insertBoard" />
<action name="procBoardAdminDeleteBoard" type="controller" permission="manager" check_var="module_srl" />
<action name="procBoardAdminInsertBoard" type="controller" permission="manager:config:*" check_var="module_srl" ruleset="insertBoard" />
<action name="procBoardAdminDeleteBoard" type="controller" permission="manager:config:*" check_var="module_srl" />
<action name="procBoardAdminUpdateBoard" type="controller" ruleset="insertBoardForBasic" />
<action name="procBoardAdminInsertCombinedConfig" type="controller" />
<action name="procBoardAdminSaveCategorySettings" type="controller" permission="manager" check_var="module_srl" ruleset="saveCategorySettings" />
<action name="procBoardAdminSaveCategorySettings" type="controller" permission="manager:config:*" check_var="module_srl" ruleset="saveCategorySettings" />
</actions>
<eventHandlers>
<eventHandler after="member.getMemberMenu" class="controller" method="triggerMemberMenu" />

View file

@ -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.<br />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.<br /><span style="color:red">Warning: permissions for the current board will apply to all affected documents and comments.</span>';
$lang->cmd_board_include_modules = 'Include Boards';
$lang->cmd_board_include_modules_none = '(None)';
$lang->cmd_board_include_days = 'Include Duration';

View file

@ -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 키를 누르고 클릭하세요.<br />주의: 현재 게시판의 읽기 권한 설정이 모든 글에 적용됩니다.';
$lang->about_board_combined_board = '다른 게시판의 글을 모아서 볼 수 있습니다. 여러 게시판을 선택하려면 Ctrl 키를 누르고 클릭하세요.<br /><span style="color:red">주의: 현재 게시판의 권한 설정이 모든 글에 적용됩니다.</span>';
$lang->cmd_board_include_modules = '포함할 게시판 선택';
$lang->cmd_board_include_modules_none = '(포함하지 않음)';
$lang->cmd_board_include_days = '포함할 기간';

View file

@ -91,8 +91,10 @@
</span>
<input cond="$is_logged" type="checkbox" name="notify_message" value="Y" id="notify_message" class="iCheck" />
<label cond="$is_logged" for="notify_message">{$lang->notify}</label>
<input cond="$module_info->secret=='Y'" type="checkbox" name="is_secret" value="Y" id="is_secret" class="iCheck" />
<label cond="$module_info->secret=='Y'" for="is_secret">{$lang->secret}</label>
<!--@if(isset($module_info->secret) && $module_info->secret === 'Y')-->
<input type="checkbox" name="is_secret" value="Y" id="is_secret" class="iCheck" />
<label for="is_secret">{$lang->secret}</label>
<!--@endif-->
</div>
<div class="write_captcha" cond="isset($captcha) && $captcha && $captcha->isTargetAction('comment')">
{$captcha}

View file

@ -29,8 +29,10 @@
</span>
<input cond="$is_logged" type="checkbox" name="notify_message" value="Y" checked="checked"|cond="$oComment->get('notify_message')=='Y'" id="notify_message" class="iCheck" />
<label cond="$is_logged" for="notify_message">{$lang->notify}</label>
<input cond="$module_info->secret=='Y'" type="checkbox" name="is_secret" value="Y" id="is_secret" checked="checked"|cond="$oComment->get('is_secret')=='Y'" class="iCheck" />
<label cond="$module_info->secret=='Y'" for="is_secret">{$lang->secret}</label>
<!--@if(isset($module_info->secret) && $module_info->secret === 'Y')-->
<input type="checkbox" name="is_secret" value="Y" id="is_secret" checked="checked"|cond="$oComment->get('is_secret')=='Y'" class="iCheck" />
<label for="is_secret">{$lang->secret}</label>
<!--@endif-->
</div>
<div class="write_captcha" cond="isset($captcha) && $captcha && $captcha->isTargetAction('comment')">
{$captcha}

View file

@ -22,7 +22,7 @@
</thead>
<tbody>
<tr loop="$board_list => $no,$val">
<td>{$no}</td>
<td>{$val->module_srl}</td>
<td>
<block cond="!$val->module_category_srl">
<block cond="$val->site_srl">{$lang->virtual_site}</block>
@ -32,7 +32,7 @@
</td>
<td class="domain_prefix"><span class="domain">{$val->domain ?? ''}</span>{\RX_BASEURL}</td>
<td>{$val->mid}</td>
<td><a href="{getSiteUrl($val->domain,'','mid',$val->mid)}">{$val->browser_title}</a></td>
<td><a href="{getSiteUrl($val->domain,'','mid',$val->mid)}" target="_blank">{$val->browser_title}</a></td>
<td>{zdate($val->regdate,"Y-m-d")}</td>
<td>
<a href="{getUrl('act','dispBoardAdminBoardInfo','module_srl',$val->module_srl)}"><i class="x_icon-cog"></i> {$lang->cmd_setup}</a> &nbsp;

View file

@ -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;
}

View file

@ -13,8 +13,8 @@
<action name="procCommentVoteDownCancel" type="controller" />
<action name="procCommentDeclare" type="controller" permission="member" />
<action name="procCommentDeclareCancel" type="controller" permission="member" />
<action name="procCommentGetList" type="controller" permission="manager" check_type="comment" check_var="comment_srls" />
<action name="procCommentInsertModuleConfig" type="controller" permission="manager" check_var="target_module_srl" ruleset="insertCommentModuleConfig" />
<action name="procCommentGetList" type="controller" permission="manager:moderate:comment" check_type="comment" check_var="comment_srls" />
<action name="procCommentInsertModuleConfig" type="controller" permission="manager:config:comment" check_var="target_module_srl" ruleset="insertCommentModuleConfig" />
<action name="dispCommentAdminList" type="view" admin_index="true" menu_name="comment" menu_index="true" />
<action name="dispCommentAdminDeclared" type="view" menu_name="comment" />
@ -23,9 +23,9 @@
<action name="procCommentAdminChangeStatus" type="controller"/>
<action name="procCommentAdminChangePublishedStatusChecked" type="controller" />
<action name="procCommentAdminCancelDeclare" type="controller" />
<action name="procCommentAdminAddCart" type="controller" permission="manager" check_type="comment" check_var="comment_srl" />
<action name="procCommentAdminDeleteChecked" type="controller" permission="manager" check_type="comment" check_var="cart" ruleset="deleteChecked" />
<action name="procCommentAdminMoveToTrash" type="controller" permission="manager" check_type="comment" check_var="comment_srl" />
<action name="procCommentAdminAddCart" type="controller" permission="manager:moderate:comment" check_type="comment" check_var="comment_srl" />
<action name="procCommentAdminDeleteChecked" type="controller" permission="manager:moderate:comment" check_type="comment" check_var="cart" ruleset="deleteChecked" />
<action name="procCommentAdminMoveToTrash" type="controller" permission="manager:moderate:comment" check_type="comment" check_var="comment_srl" />
</actions>
<eventHandlers>
<eventHandler after="document.deleteDocument" class="controller" method="triggerDeleteDocumentComments" />

View file

@ -22,14 +22,14 @@
<action name="procDocumentDeclare" type="controller" permission="member" />
<action name="procDocumentDeclareCancel" type="controller" permission="member" />
<action name="procDocumentDeleteTempSaved" type="controller" permission="member" />
<action name="procDocumentGetList" type="controller" permission="manager" check_type="document" check_var="document_srls" />
<action name="procDocumentAddCart" type="controller" permission="manager" check_type="document" check_var="srls" />
<action name="procDocumentManageCheckedDocument" type="controller" permission="manager" check_type="document" check_var="cart" />
<action name="procDocumentInsertModuleConfig" type="controller" permission="manager" check_var="target_module_srl" />
<action name="procDocumentInsertCategory" type="controller" permission="manager" check_var="module_srl" ruleset="insertCategory" />
<action name="procDocumentDeleteCategory" type="controller" permission="manager" check_var="module_srl" />
<action name="procDocumentMoveCategory" type="controller" permission="manager" check_var="module_srl" />
<action name="procDocumentMakeXmlFile" type="controller" permission="manager" check_var="module_srl" />
<action name="procDocumentGetList" type="controller" permission="manager:moderate:document" check_type="document" check_var="document_srls" />
<action name="procDocumentAddCart" type="controller" permission="manager:moderate:document" check_type="document" check_var="srls" />
<action name="procDocumentManageCheckedDocument" type="controller" permission="manager:moderate:document" check_type="document" check_var="cart" />
<action name="procDocumentInsertModuleConfig" type="controller" permission="manager:config:*" check_var="target_module_srl" />
<action name="procDocumentInsertCategory" type="controller" permission="manager:config:*" check_var="module_srl" ruleset="insertCategory" />
<action name="procDocumentDeleteCategory" type="controller" permission="manager:config:*" check_var="module_srl" />
<action name="procDocumentMoveCategory" type="controller" permission="manager:config:*" check_var="module_srl" />
<action name="procDocumentMakeXmlFile" type="controller" permission="manager:config:*" check_var="module_srl" />
<action name="dispDocumentAdminList" type="view" admin_index="true" menu_name="document" menu_index="true" />
<action name="dispDocumentAdminConfig" type="view" menu_name="document" />
@ -44,11 +44,11 @@
<action name="procDocumentAdminCancelDeclare" type="controller" />
<action name="procDocumentAdminInsertAlias" type="controller" ruleset="insertAlias" />
<action name="procDocumentAdminDeleteAlias" type="controller" ruleset="deleteAlias" />
<action name="procDocumentAdminMoveToTrash" type="controller" permission="manager" check_type="document" check_var="document_srl" />
<action name="procDocumentAdminMoveToTrash" type="controller" permission="manager:moderate:document" check_type="document" check_var="document_srl" />
<action name="procDocumentAdminRestoreTrash" type="controller" />
<action name="procDocumentAdminInsertExtraVar" type="controller" permission="manager" check_var="module_srl" ruleset="insertExtraVar" />
<action name="procDocumentAdminDeleteExtraVar" type="controller" permission="manager" check_var="module_srl" />
<action name="procDocumentAdminMoveExtraVar" type="controller" permission="manager" check_var="module_srl" />
<action name="procDocumentAdminInsertExtraVar" type="controller" permission="manager:config:*" check_var="module_srl" ruleset="insertExtraVar" />
<action name="procDocumentAdminDeleteExtraVar" type="controller" permission="manager:config:*" check_var="module_srl" />
<action name="procDocumentAdminMoveExtraVar" type="controller" permission="manager:config:*" check_var="module_srl" />
</actions>
<eventHandlers>
<eventHandler after="module.deleteModule" class="controller" method="triggerDeleteModuleDocuments" />

View file

@ -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;
}

View file

@ -12,7 +12,7 @@
<action name="procEditorSaveDoc" type="controller" />
<action name="procEditorRemoveSavedDoc" type="controller" />
<action name="procEditorLoadSavedDocument" type="controller" />
<action name="procEditorInsertModuleConfig" type="controller" permission="manager" check_var="target_module_srl" />
<action name="procEditorInsertModuleConfig" type="controller" permission="manager:config:*" check_var="target_module_srl" />
<action name="dispEditorAdminIndex" type="view" menu_name="editor" menu_index="true" admin_index="true" />
<action name="dispEditorAdminSetupComponent" type="view" menu_name="editor" />

View file

@ -23,7 +23,7 @@
<action name="procFileAdminInsertUploadConfig" type="controller" ruleset="insertConfig" />
<action name="procFileAdminInsertDownloadConfig" type="controller" />
<action name="procFileAdminInsertOtherConfig" type="controller" />
<action name="procFileAdminInsertModuleConfig" type="controller" permission="manager" check_var="target_module_srl" ruleset="fileModuleConfig" />
<action name="procFileAdminInsertModuleConfig" type="controller" permission="manager:config:*" check_var="target_module_srl" ruleset="fileModuleConfig" />
</actions>
<eventHandlers>
<eventHandler after="document.deleteDocument" class="controller" method="triggerDeleteAttached" />

View file

@ -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();

View file

@ -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
*

View file

@ -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);

View file

@ -61,7 +61,7 @@
<i class="no_profile">?</i>
<!--@end-->
</td>
{@ $member_info['group_list'] = implode(', ', $member_info['group_list'])}
{@ $member_info['group_list'] = Context::replaceUserLang(implode(', ', $member_info['group_list']), true)}
<td class="nowr" loop="$usedIdentifiers=>$name,$title">
<!--@if($name === 'email_address')-->
<a href="#popup_menu_area" class="member_{$member_info['member_srl']}">{getEncodeEmailAddress($member_info['email_address'])}</a>

View file

@ -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.';

View file

@ -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 = '단일/다중 선택에서 미리 주어진 선택지만 입력할 수 있도록 합니다. 선택지를 변경할 경우 기존 게시물에 영향을 줄 수 있습니다.';

View file

@ -0,0 +1,64 @@
<?php
namespace Rhymix\Modules\Module\Models;
#[\AllowDynamicProperties]
class Permission
{
/**
* Default properties.
*
* Note that $is_admin is an alias to $root,
* and $is_site_admin is an alias to $manager.
*/
public $access;
public $root;
public $manager;
public $scopes;
/**
* Alias to $root, kept for backward compatibility only.
*
* @deprecated
*/
public $is_admin;
/**
* Alias to $manager, kept for backward compatibility only.
*
* @deprecated
*/
public $is_site_admin;
/**
* Primary method to determine whether a user is allowed to do something.
*
* @param string $scope
* @return bool
*/
public function can(string $scope): bool
{
if (isset($this->{$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;
}
}

View file

@ -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);
}
}

View file

@ -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
*/

View file

@ -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');
}
}
/**

View file

@ -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));

View file

@ -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)
{

View file

@ -2,6 +2,9 @@
<tables>
<table name="module_admins" />
</tables>
<columns>
<column name="*" />
</columns>
<conditions>
<condition operation="equal" column="module_srl" var="module_srl" filter="number" />
<condition operation="equal" column="member_srl" var="member_srl" pipe="and" />

View file

@ -5,6 +5,7 @@
<columns>
<column name="module_srl" var="module_srl" notnull="notnull" />
<column name="member_srl" var="member_srl" notnull="notnull" />
<column name="scopes" var="scopes" />
<column name="regdate" default="curdate()" />
</columns>
</query>

View file

@ -1,5 +1,6 @@
<table name="module_admins">
<column name="module_srl" type="number" size="11" notnull="notnull" unique="unique_module_admin" />
<column name="member_srl" type="number" size="11" notnull="notnull" unique="unique_module_admin" />
<column name="scopes" type="text" />
<column name="regdate" type="date" index="idx_regdate" />
</table>

View file

@ -3,5 +3,5 @@
<column name="handler" type="varchar" size="191" notnull="notnull" />
<column name="args" type="longtext" notnull="notnull" />
<column name="options" type="longtext" notnull="notnull" />
<column name="regdate" type="datetime" notnull="notnull" default="current_timestamp()" index="idx_regdate" />
<column name="regdate" type="datetime" notnull="notnull" index="idx_regdate" />
</table>

View file

@ -8,7 +8,7 @@
<form action="./" method="post" onsubmit="return procFilter(this, insert_grant)" id="fo_obj">
<input type="hidden" name="module_srl" value="{$module_srl}" />
<input type="hidden" name="admin_member" value="<!--@foreach($admin_member as $key => $val)--><!--@if($member_config->identifier == 'email_address')-->{$val->email_address},<!--@else-->{$val->user_id},<!--@end--><!--@end-->" />
<div class="section x_form-horizontal">
<h1>{$lang->module_admin}</h1>
<div class="x_control-group">
@ -34,6 +34,21 @@
<p id="adminListHelp" class="x_help-block">{$lang->about_admin_id}</p>
</div>
</div>
<div class="x_control-group">
<label class="x_control-label">
{$lang->admin_scope}
</label>
<div class="x_controls">
{@ $default_scopes = array_keys($manager_scopes)}
{@ $admin_scopes = $admin_member ? (array_first($admin_member)->scopes ?? $default_scopes) : $default_scopes}
<!--@foreach($manager_scopes as $key => $val)-->
<label class="x_inline">
<input type="checkbox" name="admin_scopes[]" value="{$key}" checked="checked"|cond="in_array($key, $admin_scopes)" />
{$val}
</label>
<!--@endforeach-->
</div>
</div>
</div>
<div class="section">

View file

@ -1,2 +1,2 @@
<h1 cond="($module_info->display_mobile_title ?? '') !== 'hide'">{$oDocument->getTitle()}</h1>
{$oDocument->getContent(($module_info->display_popupmenu ?? '') !== 'hide')}
{$oDocument->getContent(false)}

View file

@ -58,7 +58,7 @@ jQuery(function($){
<tbody>
<!--@foreach($page_list as $no => $val)-->
<tr class="row{$cycle_idx}">
<td>{$no}</td>
<td>{$val->module_srl}</td>
<td>
<!--@if(!$val->module_category_srl)-->
<!--@if($val->site_srl)-->

View file

@ -3,15 +3,15 @@
<grants />
<actions>
<action name="getMembersPointInfo" type="model" permission="member" />
<action name="dispPointAdminConfig" type="view" admin_index="true" menu_name="point" menu_index="true" />
<action name="dispPointAdminModuleConfig" type="view" menu_name="point" />
<action name="dispPointAdminPointList" type="view" menu_name="point" />
<action name="procPointAdminInsertConfig" type="controller" ruleset="insertConfig" />
<action name="procPointAdminInsertModuleConfig" type="controller" />
<action name="procPointAdminUpdatePoint" type="controller" ruleset="updatePoint" />
<action name="procPointAdminInsertPointModuleConfig" type="controller" permission="manager" check_var="target_module_srl" />
<action name="procPointAdminInsertPointModuleConfig" type="controller" permission="manager:config:*" check_var="target_module_srl" />
<action name="procPointAdminReCal" type="controller" />
<action name="procPointAdminApplyPoint" type="controller" />
<action name="procPointAdminReset" type="controller" />

View file

@ -7,7 +7,7 @@
<action name="dispRssAdminIndex" type="view" admin_index="true" menu_name="rss" menu_index="true" />
<action name="procRssAdminInsertConfig" type="controller" ruleset="insertRssConfig" />
<action name="procRssAdminInsertModuleConfig" type="controller" permission="manager" check_var="target_module_srl" />
<action name="procRssAdminInsertModuleConfig" type="controller" permission="manager:config:*" check_var="target_module_srl" />
<action name="procRssAdminDeleteFeedImage" type="controller" />
</actions>
<eventHandlers>

View file

@ -17,11 +17,15 @@
<action name="procSpamfilterAdminInsertConfigCaptcha" type="controller" />
</actions>
<eventHandlers>
<eventHandler before="document.manage" class="controller" method="triggerManageDocument" />
<eventHandler before="document.insertDocument" class="controller" method="triggerInsertDocument" />
<eventHandler before="document.updateDocument" class="controller" method="triggerInsertDocument" />
<eventHandler before="document.manage" class="controller" method="triggerManageDocument" />
<eventHandler before="document.updateVotedCount" class="controller" method="triggerVote" />
<eventHandler before="document.declaredDocument" class="controller" method="triggerDeclare" />
<eventHandler before="comment.insertComment" class="controller" method="triggerInsertComment" />
<eventHandler before="comment.updateComment" class="controller" method="triggerInsertComment" />
<eventHandler before="comment.updateVotedCount" class="controller" method="triggerVote" />
<eventHandler before="comment.declaredComment" class="controller" method="triggerDeclare" />
<eventHandler before="communication.sendMessage" class="controller" method="triggerSendMessage" />
<eventHandler before="moduleObject.proc" class="controller" method="triggerCheckCaptcha" />
</eventHandlers>

View file

@ -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.<br>%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 #.<br>Formats such as /spam(key|word)?/ will be treated as a regular expression, and must use the proper syntax.<br>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.<br>Formats such as /spam(key|word)?/ will be treated as a regular expression, and must use the proper syntax.<br>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.<br>This only works if the trackback module is installed.';
$lang->cmd_limits_interval = 'Block Interval';

View file

@ -14,10 +14,11 @@ $lang->word = '키워드';
$lang->hit = '히트';
$lang->latest_hit = '최근 히트';
$lang->custom_message = '차단 메시지 설정';
$lang->enable_description = '# 뒷부분은 설명으로 입력';
$lang->about_custom_message = '스팸 키워드 발견시 표시할 에러 메시지를 지정할 수 있습니다.<br>%s를 넣으면 그 자리에 해당 키워드를 표시하고, 그렇지 않으면 키워드를 숨깁니다.';
$lang->about_interval = '지정된 시간 내에 글을 등록하지 못하게 합니다.';
$lang->about_denied_ip = '한 줄에 하나씩 IP 주소 또는 대역을 입력하세요. &quot;//&quot; 또는 &quot;#&quot; 뒷부분은 설명으로 저장됩니다.<br>예: 127.0.0.1 // 설명, 127.0.0.1 #설명<br>IP 대역 표기법은 <a href="https://github.com/rhymix/rhymix-docs/blob/master/ko/misc/ipfilter.md" target="_blank">매뉴얼</a>을 참고하십시오.';
$lang->about_denied_word = '한 줄에 하나씩 스팸 키워드(2~180자)를 입력하세요. &quot;#&quot; 뒷부분은 설명으로 입력됩니다.<br>/스팸(키+|워드)?/ 와 같은 형태로 입력하면 정규식으로 간주하며, 올바른 정규식 문법을 사용해야 합니다.<br>대소문자는 구분하지 않습니다.';
$lang->about_denied_word = '한 줄에 하나씩 스팸 키워드(2~180자)를 입력하세요. &quot;#&quot; 뒷부분은 설명으로 입력됩니다. &quot;#&quot;을 포함하는 키워드를 차단하려면 위의 설정을 해제하세요.<br>/스팸(키+|워드)?/ 와 같은 형태로 입력하면 정규식으로 간주하며, 올바른 정규식 문법을 사용해야 합니다.<br>대소문자는 구분하지 않습니다.';
$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회 이상 엮인글을 등록하면 스패머로 간주하고 엮인글을 차단합니다.<br>트랙백 모듈이 설치되어 있는 경우에만 적용됩니다.';
$lang->cmd_limits_interval = '글, 댓글 제한 시간';

View file

@ -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 = '<em>'.sprintf(lang('msg_faillist'),$output->get('fail_list')).'</em>';
@ -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]);

View file

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

View file

@ -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;

View file

@ -27,6 +27,36 @@
<p class="x_help-block">{$lang->cmd_interval_help}</p>
</div>
</div>
<div class="x_control-group">
<label class="x_control-label">{$lang->cmd_blocked_actions}</label>
<div class="x_controls">
<label class="x_inline">
<input type="checkbox" name="blocked_actions[]" value="document" checked="checked"|cond="!$config->blocked_actions || in_array('document', $config->blocked_actions)" />
{$lang->document}
</label>
<label class="x_inline">
<input type="checkbox" name="blocked_actions[]" value="comment" checked="checked"|cond="!$config->blocked_actions || in_array('comment', $config->blocked_actions)" />
{$lang->comment}
</label>
<label class="x_inline">
<input type="checkbox" name="blocked_actions[]" value="vote_up" checked="checked"|cond="!$config->blocked_actions || in_array('vote_up', $config->blocked_actions)" />
{$lang->cmd_vote}
</label>
<label class="x_inline">
<input type="checkbox" name="blocked_actions[]" value="vote_down" checked="checked"|cond="!$config->blocked_actions || in_array('vote_down', $config->blocked_actions)" />
{$lang->cmd_vote_down}
</label>
<label class="x_inline">
<input type="checkbox" name="blocked_actions[]" value="declare" checked="checked"|cond="!$config->blocked_actions || in_array('declare', $config->blocked_actions)" />
{$lang->cmd_declare}
</label>
<label class="x_inline">
<input type="checkbox" name="blocked_actions[]" value="message" checked="checked"|cond="!$config->blocked_actions || in_array('message', $config->blocked_actions)" />
{$lang->member_message}
</label>
<p class="x_help-block">{$lang->cmd_blocked_actions_help}</p>
</div>
</div>
<div class="x_control-group">
<label class="x_control-label" for="custom_message">{$lang->custom_message}</label>
<div class="x_controls">

View file

@ -44,6 +44,7 @@
<input type="hidden" name="active" value="word" />
<input type="hidden" name="xe_validator_id" value="modules/spamfilter/tpl/1" />
<textarea name="word_list" title="{$lang->add_denied_word}" rows="4" cols="42" style="width:100%"></textarea>
<label><input type="checkbox" name="enable_description" value="Y" checked="checked" /> {$lang->enable_description}</label>
<p class="x_help-block">{$lang->about_denied_word}</p>
<span class="x_pull-right" style="margin-right:-14px">
<button type="submit" class="x_btn x_btn-primary">{$lang->add_denied_word}</button>