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

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

View file

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

View file

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

View file

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

View file

@ -693,7 +693,8 @@ class ModuleHandler extends Handler
}
}
if ($kind === 'admin') {
if ($kind === 'admin')
{
Context::addMetaTag('robots', 'noindex');
}

View file

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

View file

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

View file

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

View file

@ -151,5 +151,7 @@ return array(
],
'use_rewrite' => true,
'use_sso' => false,
'other' => array(),
'other' => [
'proxy' => null,
],
);

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = [];

View file

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

View file

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

View file

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

View file

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

View file

@ -1,141 +0,0 @@
<?php
namespace Rhymix\Framework\Drivers\SMS;
/**
* The ApiStore SMS driver.
*/
class ApiStore extends Base implements \Rhymix\Framework\Drivers\SMSInterface
{
/**
* API specifications.
*/
protected static $_spec = array(
'max_recipients' => 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;
}
}

View file

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

View file

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

View file

@ -7,11 +7,12 @@
<meta charset="utf-8">
<meta name="generator" content="Rhymix">
<meta name="viewport" content="{{ config('mobile.viewport') ?? HTMLDisplayHandler::DEFAULT_VIEWPORT }}" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
@foreach (Context::getMetaTag() as $val)
<meta http-equiv="{{ $val['name'] }}"|if="$val['is_http_equiv']" name="{{ $val['name'] }}"|if="!$val['is_http_equiv']" content="{{ $val['content'] }}" />
@endforeach
<meta name="csrf-token" content="{!! \Rhymix\Framework\Session::getGenericToken() !!}" />
@foreach (Context::getMetaTag() as $val)
@if ($val['is_before_title'])
<meta http-equiv="{{ $val['name'] }}"|if="$val['is_http_equiv']" name="{{ $val['name'] }}"|if="!$val['is_http_equiv']" content="{!! $val['content'] !!}" />
@endif
@endforeach
<!-- TITLE -->
<title>{{ Context::getBrowserTitle() }}</title>
@ -55,6 +56,11 @@
@endforeach
<!-- OTHER HEADERS -->
@foreach (Context::getMetaTag() as $val)
@if (!$val['is_before_title'])
<meta http-equiv="{{ $val['name'] }}"|if="$val['is_http_equiv']" name="{{ $val['name'] }}"|if="!$val['is_http_equiv']" content="{!! $val['content'] !!}" />
@endif
@endforeach
@foreach (Context::getOpenGraphData() as $og_metadata)
<meta property="{{ $og_metadata['property'] }}" content="{{ $og_metadata['content'] }}" />
@endforeach

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>

View file

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

View file

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

View file

@ -44,7 +44,7 @@
<span class="icon">{$item->printExtraImages()}</span>
<!--@end-->
<!--@if($widget_info->option_view_arr[$j+1]=='regdate')-->
<!--@if(isset($widget_info->option_view_arr[$j+1]) && $widget_info->option_view_arr[$j+1]=='regdate')-->
<span class="date">{$item->getRegdate("Y-m-d")}</span> <span class="hour">{$item->getRegdate("H:i")}</span>
<!--@end-->
</p>
@ -52,7 +52,7 @@
<!--@else if($widget_info->option_view_arr[$j]=='content')-->
<p class="text" style="margin-left:{$widget_info->thumbnail_width+20}px;">
{$item->getContent()}
<!--@if($widget_info->option_view_arr[$j+1]=='regdate')-->
<!--@if(isset($widget_info->option_view_arr[$j+1]) && $widget_info->option_view_arr[$j+1]=='regdate')-->
<span class="date">{$item->getRegdate("Y-m-d")}</span> <span class="hour">{$item->getRegdate("H:i")}</span>
<!--@end-->
</p>
@ -60,7 +60,7 @@
<!--@else if($widget_info->option_view_arr[$j]=='nickname')-->
<p class="authorArea" style="margin-left:{$widget_info->thumbnail_width+20}px;">
<a href="#" onclick="return false;" class="author member_{$item->getMemberSrl()}" target="_blank"|cond="$widget_info->new_window">{$item->getNickName($widget_info->nickname_cut_size)}</a>
<!--@if($widget_info->option_view_arr[$j+1]=='regdate')-->
<!--@if(isset($widget_info->option_view_arr[$j+1]) && $widget_info->option_view_arr[$j+1]=='regdate')-->
<span class="date">{$item->getRegdate("Y-m-d")}</span> <span class="hour">{$item->getRegdate("H:i")}</span>
<!--@end-->
</p>