diff --git a/classes/context/Context.class.php b/classes/context/Context.class.php index 95b5b8776..ef96466d0 100644 --- a/classes/context/Context.class.php +++ b/classes/context/Context.class.php @@ -1120,6 +1120,14 @@ class Context self::$_instance->security_check = 'DENY ALL'; self::$_instance->security_check_detail = 'ERR_UNSAFE_ENV'; } + + if (PHP_VERSION_ID < 80000) + { + libxml_disable_entity_loader(true); + } + libxml_set_external_entity_loader(function($a, $b, $c) { + return null; + }); } /** @@ -1261,10 +1269,6 @@ class Context $GLOBALS['HTTP_RAW_POST_DATA'] = ''; return; } - if (PHP_VERSION_ID < 80000) - { - libxml_disable_entity_loader(true); - } $params = Rhymix\Framework\Parsers\XMLRPCParser::parse($GLOBALS['HTTP_RAW_POST_DATA']) ?: []; } elseif($request_method === 'JSON') @@ -1339,7 +1343,7 @@ class Context unset($_FILES[$key]); continue; } - $val['name'] = str_replace('&', '&', escape($val['name'], false)); + $val['name'] = Rhymix\Framework\Filters\FilenameFilter::clean($val['name']); self::set($key, $val, true); self::set('is_uploaded', true); self::$_instance->is_uploaded = true; @@ -1365,7 +1369,7 @@ class Context break; } $file = array(); - $file['name'] = str_replace('&', '&', escape($val['name'][$i], false)); + $file['name'] = Rhymix\Framework\Filters\FilenameFilter::clean($val['name'][$i]); $file['type'] = $val['type'][$i]; $file['tmp_name'] = $val['tmp_name'][$i]; $file['error'] = $val['error'][$i]; diff --git a/classes/display/DisplayHandler.class.php b/classes/display/DisplayHandler.class.php index 1d2fd8194..be853571b 100644 --- a/classes/display/DisplayHandler.class.php +++ b/classes/display/DisplayHandler.class.php @@ -257,7 +257,7 @@ class DisplayHandler extends Handler case 'HTML': $json_options = defined('JSON_PRETTY_PRINT') ? (JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : 0; $panel_script = sprintf('', RX_BASEURL, 'common/js/debug.js', filemtime(RX_BASEDIR . 'common/js/debug.js')); - $panel_script .= "\n"; + $panel_script .= "\n"; $body_end_position = strrpos($output, '') ?: strlen($output); $output = substr($output, 0, $body_end_position) . "\n$panel_script\n" . substr($output, $body_end_position); break; diff --git a/classes/module/ModuleObject.class.php b/classes/module/ModuleObject.class.php index c235289bf..f77996e99 100644 --- a/classes/module/ModuleObject.class.php +++ b/classes/module/ModuleObject.class.php @@ -249,7 +249,7 @@ class ModuleObject extends BaseObject */ public function setPrivileges() { - if(!$this->user->isAdmin()) + if (!$this->user->isAdmin()) { // Get privileges(granted) information for target module by of module.xml if(($permission = $this->xml_info->action->{$this->act}->permission) && $permission->check_var) @@ -278,33 +278,28 @@ class ModuleObject extends BaseObject foreach($check_module_srl as $target_srl) { // Get privileges(granted) information of current user for target module - if(($grant = ModuleModel::getInstance()->getPrivilegesBySrl($target_srl, $permission->check_type)) === false) + $check_grant = ModuleModel::getPrivilegesBySrl($target_srl, $permission->check_type); + if ($check_grant === false) { return false; } // Check permission - if(!$this->checkPermission($grant, $this->user)) + if(!$this->checkPermission($check_grant, $this->user, $failed_requirement)) { - $this->stop($this->user->isMember() ? 'msg_not_permitted_act' : 'msg_not_logged'); + $this->stop($this->_generatePermissionError($failed_requirement)); return false; } } } } - // If no privileges(granted) information, check permission by privileges(granted) information for current module - if(!isset($grant)) + // Check permission based on the grant information for the current module. + $grant = ModuleModel::getInstance()->getGrant($this->module_info, $this->user, $this->xml_info); + if(!$this->checkPermission($grant, $this->user, $failed_requirement)) { - // Get privileges(granted) information of current user for current module - $grant = ModuleModel::getInstance()->getGrant($this->module_info, $this->user, $this->xml_info); - - // Check permission - if(!$this->checkPermission($grant, $this->user)) - { - $this->stop($this->user->isMember() ? 'msg_not_permitted_act' : 'msg_not_logged'); - return false; - } + $this->stop($this->_generatePermissionError($failed_requirement)); + return false; } // If member action, grant access for log-in, sign-up, member pages @@ -313,7 +308,7 @@ class ModuleObject extends BaseObject $grant->access = true; } - // Set privileges(granted) variables + // Set aliases to grant object $this->grant = $grant; Context::set('grant', $grant); @@ -325,9 +320,10 @@ class ModuleObject extends BaseObject * * @param object $grant privileges(granted) information of user * @param object $member_info member information + * @param string|array &$failed_requirement * @return bool */ - public function checkPermission($grant = null, $member_info = null) + public function checkPermission($grant = null, $member_info = null, &$failed_requirement = '') { // Get logged-in member information if(!$member_info) @@ -356,21 +352,50 @@ class ModuleObject extends BaseObject $permission = 'root'; } - // If permission is not or 'guest', Pass - if(empty($permission) || $permission == 'guest') + // If there is no permission or eveyone is allowed, pass + if (empty($permission) || $permission === 'guest' || $permission === 'everyone') { return true; } - // If permission is 'member', check logged-in - else if($permission == 'member') + + // If permission is 'member', the user must be logged in + if ($permission === 'member') { - if($member_info->member_srl) + if ($member_info->member_srl) { return true; } + else + { + $failed_requirement = 'member'; + return false; + } } + + // If permission is 'not_member', the user must be logged out + if ($permission === 'not_member' || $permission === 'not-member') + { + if (!$member_info->member_srl || $grant->manager) + { + return true; + } + else + { + $failed_requirement = 'not_member'; + return false; + } + } + + // If permission is 'root', false + // Because an administrator who have root privilege(granted) was passed already + if ($permission == 'root') + { + $failed_requirement = 'root'; + return false; + } + // If permission is 'manager', check 'is user have manager privilege(granted)' - else if(preg_match('/^(manager(?::(.+))?|([a-z0-9\_]+)-managers)$/', $permission, $type)) + if (preg_match('/^(manager(?::(.+))?|([a-z0-9\_]+)-managers)$/', $permission, $type)) { // If permission is manager(:scope), check manager privilege and scope if ($grant->manager) @@ -404,32 +429,71 @@ class ModuleObject extends BaseObject return true; } } - } - // If permission is 'root', false - // Because an administrator who have root privilege(granted) was passed already - else if($permission == 'root') - { + + $failed_requirement = 'manager'; return false; } - // If grant name, check the privilege(granted) of the user - else if($grant_names = explode(',', $permission)) - { - $privilege_list = array_keys((array) $this->xml_info->grant); - foreach($grant_names as $name) + // Check grant name + // If multiple names are given, all of them must pass. + elseif ($grant_names = array_map('trim', explode(',', $permission))) + { + foreach ($grant_names as $name) { - if(!in_array($name, $privilege_list) || !$grant->$name) + if (!isset($grant->{$name})) { return false; } + if (!$grant->{$name}) + { + $failed_requirement = $grant->whocan($name); + return false; + } } - return true; } return false; } + /** + * Generate an error message for a failed permission. + * + * @param mixed $failed_requirement + * @return string + */ + protected function _generatePermissionError($failed_requirement) + { + if ($failed_requirement === 'member' || !$this->user->isMember()) + { + return 'msg_not_logged'; + } + elseif ($failed_requirement === 'not_member') + { + return 'msg_required_not_logged'; + } + elseif ($failed_requirement === 'manager' || $failed_requirement === 'root') + { + return 'msg_administrator_only'; + } + elseif (is_array($failed_requirement) && count($failed_requirement)) + { + if (class_exists('PointModel')) + { + $min_level = PointModel::getMinimumLevelForGroup($failed_requirement); + if ($min_level) + { + return sprintf(lang('member.msg_required_minimum_level'), $min_level); + } + } + return 'member.msg_required_specific_group'; + } + else + { + return 'msg_not_permitted_act'; + } + } + /** * Stop processing this module instance. * diff --git a/classes/validator/Validator.class.php b/classes/validator/Validator.class.php index 531872b92..b8f563648 100644 --- a/classes/validator/Validator.class.php +++ b/classes/validator/Validator.class.php @@ -30,31 +30,25 @@ class Validator * rule list * @var array */ - var $_rules; + var $_rules = []; /** * filter list * @var array */ - var $_filters; + var $_filters = []; /** * custom message list * @var array */ - var $_message; + var $_message = []; /** * custom field name list * @var array */ - var $_fieldNames; - - /** - * Can usable status for multibyte string function - * @var boolean - */ - var $_has_mb_func; + var $_fieldNames = []; /** * validator version @@ -75,12 +69,10 @@ class Validator */ function __construct($xml_path = '') { - $this->_rules = array(); - $this->_filters = array(); - $this->_xml_ruleset = NULL; - - if($xml_path) + if ($xml_path) + { $this->load($xml_path); + } // predefined rules $this->addRule(array( @@ -93,8 +85,7 @@ class Validator 'float' => '/^\d+(\.\d+)?$/' )); - $this->_has_mb_func = is_callable('mb_strlen'); - $this->setCacheDir(RX_BASEDIR . 'files/cache'); + $this->_cache_dir = RX_BASEDIR . 'files/cache'; } /** @@ -114,112 +105,26 @@ class Validator */ function load($xml_path) { - $this->_xml_ruleset = NULL; - if(!is_readable($xml_path)) + if (!file_exists($xml_path) || !is_file($xml_path) || !is_readable($xml_path)) { - return FALSE; + return false; } - $parser = new XeXmlParser(); - $xml = $parser->loadXmlFile($xml_path); - if(!isset($xml->ruleset) || !isset($xml->ruleset->fields) || !isset($xml->ruleset->fields->field)) + $output = Rhymix\Framework\Parsers\RulesetParser::loadXML($xml_path); + if (!$output) { - return FALSE; + return false; } - $rules = array(); - $messages = array(); - - // custom rules - if(isset($xml->ruleset->customrules) && isset($xml->ruleset->customrules->rule)) + if ($output->rules) { - $customrules = $xml->ruleset->customrules->rule; - if(!is_array($customrules)) - { - $customrules = array($customrules); - } - - foreach($customrules as $rule) - { - if(!isset($rule->attrs) || !isset($rule->attrs->name)) - { - continue; - } - - $message = $rule->message ? $rule->message->body : NULL; - $rule = (array) $rule->attrs; - $rule['message'] = $message; - $name = $rule['name']; - unset($rule['name']); - - $rules[$name] = $rule; - if(isset($message)) - { - $messages['invalid_' . $name] = $message; - } - } - if(count($rules)) - { - $this->addRule($rules); - } + $this->addRule($output->rules); } - - // filters - $fields = $xml->ruleset->fields->field; - if(!is_array($fields)) - { - $fields = array($fields); - } - - $filters = array(); - $fieldsNames = array(); - foreach($fields as $field) - { - $name = ''; - $filter = array(); - - if(!isset($field->attrs) || !isset($field->attrs->name)) - { - continue; - } - - $title = $field->title ? $field->title->body : NULL; - $filter = (array) $field->attrs; - $filter['title'] = $title; - - $name = $filter['name']; - if(isset($title)) - { - $fieldsNames[$name] = $title; - } - - unset($filter['name']); - - // conditional statement - if(isset($field->if)) - { - $if = $field->if; - if(!is_array($if)) - { - $if = array($if); - } - foreach($if as $idx => $cond) - { - $if[$idx] = (array) $cond->attrs; - } - $filter['if'] = $if; - } - - $filters[$name] = $filter; - } - - $this->_xml_ruleset = $xml->ruleset; - $this->_filters = $filters; - $this->_message = $messages; - $this->_fieldNames = $fieldsNames; + $this->_filters = $output->filters; + $this->_message = $output->messages; + $this->_fieldNames = $output->fieldsNames; $this->_xml_path = $xml_path; - - return TRUE; + return true; } /** @@ -389,7 +294,7 @@ class Validator $strbytes = strlen($value); if(!$is_min_b || !$is_max_b) { - $strlength = $this->_has_mb_func ? mb_strlen($value, 'utf-8') : $this->mbStrLen($value); + $strlength = mb_strlen($value, 'UTF-8'); } if(($min && $min > ($is_min_b ? $strbytes : $strlength)) || ($max && $max < ($is_max_b ? $strbytes : $strlength))) @@ -630,21 +535,6 @@ class Validator return TRUE; } - /** - * if not supported 'mb_strlen' function, this method can use. - * @param string $str - * @return int - */ - function mbStrLen($str) - { - $arr = count_chars($str); - for($i = 0x80; $i < 0xc0; $i++) - { - unset($arr[$i]); - } - return array_sum($arr); - } - /** * Returns compiled javascript file path. The path begins from XE root directory. * @return string Compiled JavaScript file path diff --git a/classes/xml/XmlParser.class.php b/classes/xml/XmlParser.class.php index d08bdfd75..b4f65f102 100644 --- a/classes/xml/XmlParser.class.php +++ b/classes/xml/XmlParser.class.php @@ -1,240 +1,46 @@ { - * This class may drops unsupported xml lanuage attributes when multiple language attributes are given. - * For example, if 'xml:lang='ko, en, ch, jp..' is given in a xml file, only ko will be left ignoring all other language - * attributes when kor is only supported language. It seems to work fine now but we did not scrutinze any potential side effects, - * } + * XML Parser class from XE * - * @author NAVER (developers@xpressengine.com) - * @package /classes/xml - * @version 0.1 + * Renamed because of conflict with built-in XMLParser class in PHP 8+ + * + * @deprecated */ class XeXmlParser { - /** - * Xml parser - * @var resource + * Load an XML file. + * + * @deprecated + * @param string $filename + * @return ?object */ - var $oParser = NULL; - - /** - * Input xml - * @var string - */ - var $input = NULL; - - /** - * Output object in array - * @var array - */ - var $output = array(); - - /** - * The default language type - * @var string - */ - var $lang = "en"; - - /** - * Load a xml file specified by a filename and parse it to Return the resultant data object - * @param string $filename a file path of file - * @return object|null Returns a data object containing data extracted from a xml file or NULL if a specified file does not exist - */ - function loadXmlFile($filename) + public static function loadXmlFile($filename): ?object { - if(!file_exists($filename)) + $filename = strval($filename); + if (file_exists($filename)) { - return; - } - $buff = FileHandler::readFile($filename); - - $oXmlParser = new self(); - return $oXmlParser->parse($buff); - } - - /** - * Parse xml data to extract values from it and construct data object - * @param string $input a data buffer containing xml data - * @param mixed $arg1 ??? - * @param mixed $arg2 ??? - * @return object|null Returns a resultant data object or NULL in case of error - */ - function parse($input = '', $arg1 = NULL, $arg2 = NULL) - { - // Save the compile starting time for debugging - $start = microtime(true); - - $this->lang = Context::getLangType(); - - $this->input = $input ? $input : $GLOBALS['HTTP_RAW_POST_DATA']; - $this->input = str_replace(array('', ''), array('', ''), $this->input); - - // extracts a supported language - preg_match_all("/xml:lang=\"([^\"].+)\"/i", $this->input, $matches); - - // extracts the supported lanuage when xml:lang is used - if(count($matches[1]) && $supported_lang = array_unique($matches[1])) - { - $tmpLangList = array_flip($supported_lang); - // if lang of the first log-in user doesn't exist, apply en by default if exists. Otherwise apply the first lang. - if(!isset($tmpLangList[$this->lang])) - { - if(isset($tmpLangList['en'])) - { - $this->lang = 'en'; - } - else - { - $this->lang = array_shift($supported_lang); - } - } - // uncheck the language if no specific language is set. + return Rhymix\Framework\Parsers\XEXMLParser::loadXMLFile($filename); } else { - $this->lang = ''; - } - - $this->oParser = xml_parser_create('UTF-8'); - - //xml_set_object($this->oParser, $this); - xml_set_element_handler($this->oParser, [$this, "_tagOpen"], [$this, "_tagClosed"]); - xml_set_character_data_handler($this->oParser, [$this, "_tagBody"]); - - xml_parse($this->oParser, $this->input); - xml_parser_free($this->oParser); - - if(!count($this->output)) - { - return; - } - - $output = array_shift($this->output); - // Save compile starting time for debugging - if (!isset($GLOBALS['__xmlparse_elapsed__'])) - { - $GLOBALS['__xmlparse_elapsed__'] = 0; - } - $GLOBALS['__xmlparse_elapsed__'] += microtime(true) - $start; - - return $output; - } - - /** - * Start element handler. - * @param resource $parse an instance of parser - * @param string $node_name a name of node - * @param array $attrs attributes to be set - * @return array - */ - function _tagOpen($parser, $node_name, $attrs) - { - $obj = new Xml_Node_(); - $obj->node_name = strtolower($node_name); - $obj->attrs = $this->_arrToAttrsObj($attrs); - - $this->output[] = $obj; - } - - /** - * Character data handler - * Variable in the last element of this->output - * @param resource $parse an instance of parser - * @param string $body a data to be added - * @return void - */ - function _tagBody($parser, $body) - { - //if(!trim($body)) return; - $this->output[count($this->output) - 1]->body .= $body; - } - - /** - * End element handler - * @param resource $parse an instance of parser - * @param string $node_name name of xml node - * @return void - */ - function _tagClosed($parser, $node_name) - { - $node_name = strtolower($node_name); - $cur_obj = array_pop($this->output); - $parent_obj = &$this->output[count($this->output) - 1]; - if($this->lang && $cur_obj->attrs->{'xml:lang'} && $cur_obj->attrs->{'xml:lang'} != $this->lang) - { - return; - } - if($this->lang && ($parent_obj->{$node_name}->attrs->{'xml:lang'} ?? null) && $parent_obj->{$node_name}->attrs->{'xml:lang'} != $this->lang) - { - return; - } - - if(isset($parent_obj->{$node_name})) - { - $tmp_obj = $parent_obj->{$node_name}; - if(is_array($tmp_obj)) - { - $parent_obj->{$node_name}[] = $cur_obj; - } - else - { - $parent_obj->{$node_name} = array($tmp_obj, $cur_obj); - } - } - else - { - if(!is_object($parent_obj)) - { - $parent_obj = (object) $parent_obj; - } - - $parent_obj->{$node_name} = $cur_obj; + return null; } } /** - * Method to transfer values in an array to a data object - * @param array $arr data array - * @return Xml_Node_ object + * Load an XML string. + * + * @deprecated + * @param string $$input + * @return ?object */ - function _arrToAttrsObj($arr) + function parse($input = ''): ?object { - $output = new Xml_Node_(); - foreach($arr as $key => $val) - { - $key = strtolower($key); - $output->{$key} = $val; - } - return $output; + $input = strval($input !== '' ? $input : $GLOBALS['HTTP_RAW_POST_DATA']); + return Rhymix\Framework\Parsers\XEXMLParser::loadXMLString($input); } - } /** diff --git a/common/css/rhymix.scss b/common/css/rhymix.scss index d116b5b92..91c3e140d 100644 --- a/common/css/rhymix.scss +++ b/common/css/rhymix.scss @@ -10,6 +10,10 @@ article, aside, details, figcaption, figure, footer, header, hgroup, nav, sectio } body { position: relative; + &.rx_modal_open { + overflow: hidden; + position: fixed; + } } a img { border: 0; diff --git a/common/framework/DB.php b/common/framework/DB.php index c58d727b9..f01339d36 100644 --- a/common/framework/DB.php +++ b/common/framework/DB.php @@ -782,11 +782,11 @@ class DB $table = Parsers\DBTableParser::loadXML($filename, $content); if (!$table) { - return $this->setError(-1, 'Table creation failed.'); + return $this->setError(-1, 'Failed to load table schema file'); } if ($table->deleted) { - return new Helpers\DBResultHelper; + return new Helpers\DBResultHelper(-1, 'Table is marked as deleted'); } // Generate the CREATE TABLE query and execute it. diff --git a/common/framework/Password.php b/common/framework/Password.php index 96282d624..3182557fb 100644 --- a/common/framework/Password.php +++ b/common/framework/Password.php @@ -128,12 +128,12 @@ class Password $algorithm = $config->password_hashing_algorithm ?? ''; if (strval($algorithm) === '') { - $algorithm = 'md5'; + $algorithm = self::getBestSupportedAlgorithm(); } } else { - $algorithm = 'md5'; + $algorithm = self::getBestSupportedAlgorithm(); } return $algorithm; } diff --git a/common/framework/Queue.php b/common/framework/Queue.php index ec58a6aeb..1e33c1adb 100644 --- a/common/framework/Queue.php +++ b/common/framework/Queue.php @@ -11,6 +11,14 @@ class Queue * Static properties. */ protected static $_drivers = []; + protected static $_signal = 0; + + /** + * Priority constants. + */ + public const PRIORITY_HIGH = 'high'; + public const PRIORITY_NORMAL = 'normal'; + public const PRIORITY_LOW = 'low'; /** * Add a custom Queue driver. @@ -120,9 +128,10 @@ class Queue * @param string $handler * @param ?object $args * @param ?object $options + * @param ?string $priority * @return int */ - public static function addTask(string $handler, ?object $args = null, ?object $options = null): int + public static function addTask(string $handler, ?object $args = null, ?object $options = null, ?string $priority = null): int { $driver_name = config('queue.driver'); if (!$driver_name) @@ -136,7 +145,7 @@ class Queue throw new Exceptions\FeatureDisabled('Queue not configured'); } - return $driver->addTask($handler, $args, $options); + return $driver->addTask($handler, $args, $options, $priority); } /** @@ -149,9 +158,10 @@ class Queue * @param string $handler * @param ?object $args * @param ?object $options + * @param ?string $priority * @return int */ - public static function addTaskAt(int $time, string $handler, ?object $args = null, ?object $options = null): int + public static function addTaskAt(int $time, string $handler, ?object $args = null, ?object $options = null, ?string $priority = null): int { if (!config('queue.enabled')) { @@ -160,7 +170,7 @@ class Queue // This feature always uses the DB driver. $driver = self::getDbDriver(); - return $driver->addTaskAt($time, $handler, $args, $options); + return $driver->addTaskAt($time, $handler, $args, $options, $priority); } /** @@ -337,6 +347,11 @@ class Queue foreach ($tasks as $task) { self::_executeTask($task); + + if (self::signalReceived()) + { + return; + } } } if ($index === 1 || $count < 2) @@ -347,6 +362,11 @@ class Queue { $db_driver->updateLastRunTimestamp($task); self::_executeTask($task); + + if (self::signalReceived()) + { + return; + } } } @@ -359,6 +379,11 @@ class Queue if ($task) { self::_executeTask($task); + + if (self::signalReceived()) + { + return; + } } // If the timeout is imminent, break the loop. @@ -374,6 +399,12 @@ class Queue { usleep(intval((1 - $loop_elapsed_time) * 1000000)); } + + // Check for a signal again after the sleep. + if (self::signalReceived()) + { + break; + } } } @@ -479,4 +510,26 @@ class Queue ])); } } + + /** + * Signal handler. + * + * @param int $signal + * @param mixed $siginfo + * @return void + */ + public static function signalHandler(int $signal, $siginfo): void + { + self::$_signal = $signal; + } + + /** + * Has a signal been received? + * + * @return int + */ + public static function signalReceived(): int + { + return self::$_signal; + } } diff --git a/common/framework/drivers/QueueInterface.php b/common/framework/drivers/QueueInterface.php index ebcbafacb..242ef2812 100644 --- a/common/framework/drivers/QueueInterface.php +++ b/common/framework/drivers/QueueInterface.php @@ -57,9 +57,10 @@ interface QueueInterface * @param string $handler * @param ?object $args * @param ?object $options + * @param ?string $priority * @return int */ - public function addTask(string $handler, ?object $args = null, ?object $options = null): int; + public function addTask(string $handler, ?object $args = null, ?object $options = null, ?string $priority = null): int; /** * Get the next task from the queue. diff --git a/common/framework/drivers/queue/db.php b/common/framework/drivers/queue/db.php index 68632704d..c6ced5583 100644 --- a/common/framework/drivers/queue/db.php +++ b/common/framework/drivers/queue/db.php @@ -89,14 +89,43 @@ class DB implements QueueInterface * @param string $handler * @param ?object $args * @param ?object $options + * @param ?string $priority * @return int */ - public function addTask(string $handler, ?object $args = null, ?object $options = null): int + public function addTask(string $handler, ?object $args = null, ?object $options = null, ?string $priority = null): int { $oDB = RFDB::getInstance(); - $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; + + if ($priority === \Rhymix\Framework\Queue::PRIORITY_HIGH) + { + $stmt = $oDB->query('SELECT MIN(id) AS min_id FROM task_queue'); + $min_id = intval($stmt->fetchColumn()); + $stmt->closeCursor(); + $id = $min_id ? ($min_id - rand(1, 10)) : null; + } + else + { + $min_id = null; + $id = null; + } + + $stmt = $oDB->prepare('INSERT INTO task_queue (id, handler, args, options, regdate) VALUES (?, ?, ?, ?, ?)'); + $result = $stmt->execute([ + $id, + $handler, + serialize($args), + serialize($options), + date('Y-m-d H:i:s'), + ]); + + if ($result) + { + return $id ?? $oDB->getInsertID(); + } + else + { + return 0; + } } /** @@ -106,10 +135,20 @@ class DB implements QueueInterface * @param string $handler * @param ?object $args * @param ?object $options + * @param ?string $priority * @return int */ - public function addTaskAt(int $time, string $handler, ?object $args = null, ?object $options = null): int + public function addTaskAt(int $time, string $handler, ?object $args = null, ?object $options = null, ?string $priority = null): int { + if ($priority === \Rhymix\Framework\Queue::PRIORITY_HIGH) + { + $time = $time - 1; + } + elseif ($priority === \Rhymix\Framework\Queue::PRIORITY_LOW) + { + $time = $time + 1; + } + $oDB = RFDB::getInstance(); $task_srl = getNextSequence(); $stmt = $oDB->prepare(trim(<<execute([ $task_srl, 'once', @@ -126,6 +166,7 @@ class DB implements QueueInterface serialize($options), date('Y-m-d H:i:s'), ]); + return $result ? $task_srl : 0; } diff --git a/common/framework/drivers/queue/dummy.php b/common/framework/drivers/queue/dummy.php index 76d39dc63..156f00673 100644 --- a/common/framework/drivers/queue/dummy.php +++ b/common/framework/drivers/queue/dummy.php @@ -92,9 +92,10 @@ class Dummy implements QueueInterface * @param string $handler * @param ?object $args * @param ?object $options + * @param ?string $priority * @return int */ - public function addTask(string $handler, ?object $args = null, ?object $options = null): int + public function addTask(string $handler, ?object $args = null, ?object $options = null, ?string $priority = null): int { $this->_dummy_queue = (object)[ 'handler' => $handler, diff --git a/common/framework/drivers/queue/redis.php b/common/framework/drivers/queue/redis.php index cd4b8256e..963dc845f 100644 --- a/common/framework/drivers/queue/redis.php +++ b/common/framework/drivers/queue/redis.php @@ -133,9 +133,10 @@ class Redis implements QueueInterface * @param string $handler * @param ?object $args * @param ?object $options + * @param ?string $priority * @return int */ - public function addTask(string $handler, ?object $args = null, ?object $options = null): int + public function addTask(string $handler, ?object $args = null, ?object $options = null, ?string $priority = null): int { $value = serialize((object)[ 'handler' => $handler, @@ -145,7 +146,14 @@ class Redis implements QueueInterface if ($this->_conn) { - $result = $this->_conn->rPush($this->_key, $value); + if ($priority === \Rhymix\Framework\Queue::PRIORITY_HIGH) + { + $result = $this->_conn->lPush($this->_key, $value); + } + else + { + $result = $this->_conn->rPush($this->_key, $value); + } return intval($result); } else diff --git a/common/framework/filters/FilenameFilter.php b/common/framework/filters/FilenameFilter.php index 602b73354..83e1e2845 100644 --- a/common/framework/filters/FilenameFilter.php +++ b/common/framework/filters/FilenameFilter.php @@ -34,6 +34,9 @@ class FilenameFilter $filename = preg_replace('/__+/', '_', $filename); $filename = preg_replace('/\.\.+/', '.', $filename); + // Remove potentially misleading double extensions. + $filename = preg_replace('/\.(?:php[\d|s]?|txt|pdf|zip|com|exe|bat|msi|scr|jsp|aspx?|docx?|xlsx?|pptx?|hwpx?)\s?(\.[a-z0-9]+)$/', '$1', $filename); + // Change .php files to .phps to make them non-executable. if (strtolower(substr($filename, strlen($filename) - 4)) === '.php') { diff --git a/common/framework/parsers/AddonInfoParser.php b/common/framework/parsers/AddonInfoParser.php new file mode 100644 index 000000000..7598ed141 --- /dev/null +++ b/common/framework/parsers/AddonInfoParser.php @@ -0,0 +1,96 @@ +addon_name = $addon_name; + + // Get the XML schema version. + $version = strval($xml['version']) ?: '0.1'; + + // Parse version 0.2 + if ($version === '0.2') + { + $info->title = self::_getChildrenByLang($xml, 'title', $lang); + $info->description = self::_getChildrenByLang($xml, 'description', $lang); + $info->version = trim($xml->version); + $info->date = ($xml->date === 'RX_CORE') ? '' : date('Ymd', strtotime($xml->date . 'T12:00:00Z')); + $info->homepage = trim($xml->link); + $info->license = trim($xml->license); + $info->license_link = trim($xml->license['link'] ?? ''); + $info->author = array(); + + foreach ($xml->author as $author) + { + $author_info = new \stdClass; + $author_info->name = self::_getChildrenByLang($author, 'name', $lang); + $author_info->email_address = trim($author['email_address'] ?? ''); + $author_info->homepage = trim($author['link'] ?? ''); + $info->author[] = $author_info; + } + } + + // Parse version 0.1 + else + { + $info->title = self::_getChildrenByLang($xml, 'title', $lang); + $info->description = self::_getChildrenByLang($xml->author, 'description', $lang); + $info->version = trim($xml['version'] ?? ''); + $info->date = date('Ymd', strtotime($xml->author['date'] . 'T12:00:00Z')); + $info->homepage = trim($xml->link); + $info->license = trim($xml->license); + $info->license_link = trim($xml->license['link'] ?? ''); + $info->author = array(); + + $author_info = new \stdClass; + $author_info->name = self::_getChildrenByLang($xml->author, 'name', $lang); + $author_info->email_address = trim($xml->author['email_address']); + $author_info->homepage = trim($xml->author['link'] ?? ''); + $info->author[] = $author_info; + } + + // Get extra_vars. + if ($xml->extra_vars) + { + $info->extra_vars = get_object_vars(self::_getExtraVars($xml->extra_vars, $lang, 'addon')); + } + else + { + $info->extra_vars = []; + } + + // Prepare additional fields that will be filled in later. + $info->is_enabled = (object)['pc' => false, 'mobile' => false]; + $info->xe_run_method = 'run_selected'; + $info->mid_list = []; + + // Return the complete result. + return $info; + } +} diff --git a/common/framework/parsers/BaseParser.php b/common/framework/parsers/BaseParser.php index 1677aa67b..37fbcb0bb 100644 --- a/common/framework/parsers/BaseParser.php +++ b/common/framework/parsers/BaseParser.php @@ -10,7 +10,7 @@ abstract class BaseParser /** * Get all attributes of an element as an associative array. * - * @param SimpleXMLElement $element + * @param \SimpleXMLElement $element * @param bool $normalize * @return array */ @@ -31,7 +31,7 @@ abstract class BaseParser /** * Get the string value of an XML attribute after normalizing its name. * - * @param SimpleXMLElement $element + * @param \SimpleXMLElement $element * @param string $name * @return string */ @@ -55,7 +55,7 @@ abstract class BaseParser * A value that is identical to the name of the attribute will be treated as true. * Other values will be passed to toBool() for evaluation. * - * @param SimpleXMLElement $element + * @param \SimpleXMLElement $element * @param string $name * @return bool */ @@ -77,7 +77,7 @@ abstract class BaseParser /** * Get the contents of child elements that match a language. * - * @param SimpleXMLElement $parent + * @param \SimpleXMLElement $parent * @param string $tag_name * @param string $lang * @return string @@ -107,46 +107,113 @@ abstract class BaseParser /** * Parse extra_vars. * - * @param SimpleXMLElement $extra_vars + * @param \SimpleXMLElement $extra_vars * @param string $lang + * @param string $type * @return object */ - protected static function _getExtraVars(\SimpleXMLElement $extra_vars, string $lang): \stdClass + protected static function _getExtraVars(\SimpleXMLElement $extra_vars, string $lang, string $type = ''): \stdClass { $result = new \stdClass; + + // Recurse into groups. $group_name = $extra_vars->getName() === 'group' ? self::_getChildrenByLang($extra_vars, 'title', $lang) : null; foreach ($extra_vars->group ?: [] as $group) { - $group_result = self::_getExtraVars($group, $lang); + $group_result = self::_getExtraVars($group, $lang, $type); foreach ($group_result as $key => $val) { $result->{$key} = $val; } } + + // Parse each variable in the group. foreach ($extra_vars->var ?: [] as $var) { $item = new \stdClass; $item->group = $group_name; - $item->name = trim($var['name']); - $item->type = trim($var['type']) ?: 'text'; + + // id and name + if ($type === 'widget' || $type === 'widgetstyle') + { + $item->id = trim($var['id'] ?? '') ?: trim($var->id); + if (!$item->id) + { + $item->id = trim($var['name'] ?? ''); + } + $item->name = self::_getChildrenByLang($var, 'name', $lang); + if (!$item->name) + { + $item->name = self::_getChildrenByLang($var, 'title', $lang); + } + } + else + { + $item->name = trim($var['name'] ?? ''); + } + + // type + $item->type = trim($var['type'] ?? ''); + if (!$item->type) + { + $item->type = trim($var->type) ?: 'text'; + } + if ($item->type === 'filebox' && isset($var->type)) + { + $item->filter = trim($var->type['filter'] ?? ''); + $item->allow_multiple = trim($var->type['allow_multiple'] ?? ''); + } + + // Other common attributes $item->title = self::_getChildrenByLang($var, 'title', $lang); $item->description = str_replace('\\n', "\n", self::_getChildrenByLang($var, 'description', $lang)); - $item->default = trim($var['default']) ?: null; + $item->default = trim($var['default'] ?? '') ?: null; + if (!isset($item->default)) + { + $item->default = self::_getChildrenByLang($var, 'default', $lang); + } $item->value = null; + + // Options if ($var->options) { $item->options = array(); foreach ($var->options as $option) { - $option_item = new \stdClass; - $option_item->title = self::_getChildrenByLang($option, 'title', $lang); - $option_item->value = trim($option['value']); - $item->options[$option_item->value] = $option_item; + if ($type === 'widget' || $type === 'widgetstyle') + { + $value = trim($option->value ?? ''); + $item->options[$value] = self::_getChildrenByLang($option, 'name', $lang); + if ($option['default'] === 'true') + { + $item->default_options[$value] = true; + } + if ($option['init'] === 'true') + { + $item->init_options[$value] = true; + } + } + else + { + $option_item = new \stdClass; + $option_item->title = self::_getChildrenByLang($option, 'title', $lang); + $option_item->value = trim($option['value'] ?? '') ?: trim($option->value ?? ''); + $item->options[$option_item->value] = $option_item; + } } } - $result->{$item->name} = $item; + // Add to list of variables + if ($type === 'widget' || $type === 'widgetstyle') + { + $result->{$item->id} = $item; + } + else + { + $result->{$item->name} = $item; + } } + return $result; } } diff --git a/common/framework/parsers/DBQueryParser.php b/common/framework/parsers/DBQueryParser.php index bac0940ad..674648682 100644 --- a/common/framework/parsers/DBQueryParser.php +++ b/common/framework/parsers/DBQueryParser.php @@ -31,7 +31,7 @@ class DBQueryParser extends BaseParser /** * Parse a query. * - * @param SimpleXMLElement $xml + * @param \SimpleXMLElement $xml * @param string $name * @return object */ @@ -248,7 +248,7 @@ class DBQueryParser extends BaseParser /** * Parse conditions. * - * @param SimpleXMLElement $parent + * @param \SimpleXMLElement $parent * @return array */ protected static function _parseConditions(\SimpleXMLElement $parent): array diff --git a/common/framework/parsers/DBTableParser.php b/common/framework/parsers/DBTableParser.php index 8ba82b924..cbd50ea55 100644 --- a/common/framework/parsers/DBTableParser.php +++ b/common/framework/parsers/DBTableParser.php @@ -33,7 +33,7 @@ class DBTableParser extends BaseParser * @param string $content * @return ?object */ - public static function loadXML(string $filename = '', string $content = ''): object + public static function loadXML(string $filename = '', string $content = ''): ?object { // Load the XML content. if ($content) @@ -61,10 +61,10 @@ class DBTableParser extends BaseParser $table->name = strval($xml['name']); } - $deleted = strval($xml['deleted']); - if ($deleted !== '') + $is_deleted = strval($xml['deleted']); + if ($is_deleted !== '') { - $table->deleted = toBool($deleted); + $table->is_deleted = toBool($is_deleted); } // Load columns. diff --git a/common/framework/parsers/EditorComponentParser.php b/common/framework/parsers/EditorComponentParser.php index a50036ab6..53ea89118 100644 --- a/common/framework/parsers/EditorComponentParser.php +++ b/common/framework/parsers/EditorComponentParser.php @@ -31,29 +31,58 @@ class EditorComponentParser extends BaseParser $info = new \stdClass; $info->component_name = $component_name; - // Get basic information. - $info->title = self::_getChildrenByLang($xml, 'title', $lang); - $info->description = self::_getChildrenByLang($xml, 'description', $lang); - $info->version = trim($xml->version ?? ''); - $info->date = date('Ymd', strtotime($xml->date . 'T12:00:00Z')); - $info->homepage = trim($xml->homepage ?? ''); - $info->license = trim($xml->license ?? ''); - $info->license_link = trim($xml->license['link'] ?? ''); - $info->author = array(); + // Get the XML schema version. + $version = strval($xml['version']) ?: '0.1'; - foreach ($xml->author as $author) + // Parse version 0.2 + if ($version === '0.2') { + $info->title = self::_getChildrenByLang($xml, 'title', $lang); + $info->description = self::_getChildrenByLang($xml, 'description', $lang); + $info->version = trim($xml->version ?? ''); + $info->date = date('Ymd', strtotime($xml->date . 'T12:00:00Z')); + $info->homepage = trim($xml->link); + $info->license = trim($xml->license ?? ''); + $info->license_link = trim($xml->license['link'] ?? ''); + $info->author = array(); + + foreach ($xml->author as $author) + { + $author_info = new \stdClass; + $author_info->name = self::_getChildrenByLang($author, 'name', $lang); + $author_info->email_address = trim($author['email_address'] ?? ''); + $author_info->homepage = trim($author['link'] ?? ''); + $info->author[] = $author_info; + } + } + + // Parse version 0.1 + else + { + $info->title = self::_getChildrenByLang($xml, 'title', $lang); + $info->description = self::_getChildrenByLang($xml->author, 'description', $lang); + $info->version = trim($xml['version'] ?? ''); + $info->date = date('Ymd', strtotime($xml->author['date'] . 'T12:00:00Z')); + $info->homepage = trim($xml->link) ?: trim($xml->homepage); + $info->license = trim($xml->license); + $info->license_link = trim($xml->license['link'] ?? ''); + $info->author = array(); + $author_info = new \stdClass; - $author_info->name = self::_getChildrenByLang($author, 'name', $lang); - $author_info->email_address = trim($author['email_address'] ?? ''); - $author_info->homepage = trim($author['link'] ?? ''); + $author_info->name = self::_getChildrenByLang($xml->author, 'name', $lang); + $author_info->email_address = trim($xml->author['email_address']); + $author_info->homepage = trim($xml->author['link'] ?? ''); $info->author[] = $author_info; } // Get extra_vars. if ($xml->extra_vars) { - $info->extra_vars = self::_getExtraVars($xml->extra_vars, $lang); + $info->extra_vars = self::_getExtraVars($xml->extra_vars, $lang, 'editor_component'); + } + else + { + $info->extra_vars = new \stdClass; } // Return the complete result. diff --git a/common/framework/parsers/RulesetParser.php b/common/framework/parsers/RulesetParser.php new file mode 100644 index 000000000..5fc631524 --- /dev/null +++ b/common/framework/parsers/RulesetParser.php @@ -0,0 +1,97 @@ +rules = []; + $info->messages = []; + $info->filters = []; + $info->fieldsNames = []; + + // Parse custom rules. + if ($xml->customrules && $xml->customrules->rule) + { + foreach ($xml->customrules->rule as $rule) + { + $def = []; + foreach ($rule->attributes() as $key => $val) + { + $def[trim($key)] = trim($val); + } + $def['message'] = self::_getChildrenByLang($rule, 'message', $lang) ?: null; + unset($def['name']); + + $rule_name = trim($rule['name']); + $info->rules[$rule_name] = $def; + + if ($def['message']) + { + $info->messages['invalid_' . $rule_name] = $def['message']; + } + } + } + + // Parse field filters. + if ($xml->fields && $xml->fields->field) + { + foreach ($xml->fields->field as $field) + { + $def = []; + foreach ($field->attributes() as $key => $val) + { + $def[trim($key)] = trim($val); + } + $def['title'] = self::_getChildrenByLang($field, 'title', $lang) ?: null; + unset($def['name']); + + if ($field->if) + { + foreach ($field->if as $if) + { + $condition = []; + foreach ($if->attributes() as $key => $val) + { + $condition[trim($key)] = trim($val); + } + $def['if'][] = $condition; + } + } + + $filter_name = trim($field['name']); + $info->filters[$filter_name] = $def; + + if ($def['title']) + { + $info->fieldsNames[$filter_name] = $def['title']; + } + } + } + + return $info; + } +} diff --git a/common/framework/parsers/WidgetInfoParser.php b/common/framework/parsers/WidgetInfoParser.php new file mode 100644 index 000000000..6e89979d7 --- /dev/null +++ b/common/framework/parsers/WidgetInfoParser.php @@ -0,0 +1,96 @@ +widget = $widget_name; + $info->path = sprintf('./widgets/%s/', $widget_name); + + // Get the XML schema version. + $version = strval($xml['version']) ?: '0.1'; + + // Parse version 0.2 + if ($version === '0.2') + { + $info->title = self::_getChildrenByLang($xml, 'title', $lang); + $info->description = self::_getChildrenByLang($xml, 'description', $lang); + $info->version = trim($xml->version); + $info->date = ($xml->date === 'RX_CORE') ? '' : date('Ymd', strtotime($xml->date . 'T12:00:00Z')); + $info->homepage = trim($xml->link); + $info->license = trim($xml->license); + $info->license_link = trim($xml->license['link'] ?? ''); + $info->author = array(); + + foreach ($xml->author as $author) + { + $author_info = new \stdClass; + $author_info->name = self::_getChildrenByLang($author, 'name', $lang); + $author_info->email_address = trim($author['email_address'] ?? ''); + $author_info->homepage = trim($author['link'] ?? ''); + $info->author[] = $author_info; + } + } + + // Parse version 0.1 + else + { + $info->title = self::_getChildrenByLang($xml, 'title', $lang); + $info->description = self::_getChildrenByLang($xml->author, 'description', $lang); + $info->version = trim($xml['version'] ?? ''); + $info->date = date('Ymd', strtotime($xml->author['date'] . 'T12:00:00Z')); + $info->homepage = trim($xml->link); + $info->license = trim($xml->license); + $info->license_link = trim($xml->license['link'] ?? ''); + $info->author = array(); + + $author_info = new \stdClass; + $author_info->name = self::_getChildrenByLang($xml->author, 'name', $lang); + $author_info->email_address = trim($xml->author['email_address']); + $author_info->homepage = trim($xml->author['link'] ?? ''); + $info->author[] = $author_info; + } + + // Get extra_vars. + if ($xml->extra_vars) + { + $info->extra_var = self::_getExtraVars($xml->extra_vars, $lang, 'widget'); + } + else + { + $info->extra_var = new \stdClass; + } + + // Prepare additional fields that will be filled in later. + $info->widget_srl = null; + $info->widget_title = null; + + // Return the complete result. + return $info; + } +} diff --git a/common/framework/parsers/WidgetStyleInfoParser.php b/common/framework/parsers/WidgetStyleInfoParser.php new file mode 100644 index 000000000..f7ae4307d --- /dev/null +++ b/common/framework/parsers/WidgetStyleInfoParser.php @@ -0,0 +1,83 @@ +widgetStyle = $widgetstyle_name; + $info->path = sprintf('./widgetstyles/%s/', $widgetstyle_name); + + // Parse common fields. + $info->title = self::_getChildrenByLang($xml, 'title', $lang); + $info->description = self::_getChildrenByLang($xml, 'description', $lang); + $info->version = trim($xml->version); + $info->date = ($xml->date === 'RX_CORE') ? '' : date('Ymd', strtotime($xml->date . 'T12:00:00Z')); + $info->homepage = trim($xml->link); + $info->license = trim($xml->license); + $info->license_link = trim($xml->license['link'] ?? ''); + + // Parse the preview image. + $preview_filename = trim($xml->preview ?? 'preview.jpg'); + $preview_path = sprintf('%s%s', $info->path, $preview_filename); + if (file_exists($preview_path)) + { + $info->preview = $preview_path; + } + else + { + $info->preview = null; + } + + // Parse the author list. + $info->author = array(); + foreach ($xml->author as $author) + { + $author_info = new \stdClass; + $author_info->name = self::_getChildrenByLang($author, 'name', $lang); + $author_info->email_address = trim($author['email_address'] ?? ''); + $author_info->homepage = trim($author['link'] ?? ''); + $info->author[] = $author_info; + } + + // Get extra_vars. + if ($xml->extra_vars) + { + $info->extra_var = self::_getExtraVars($xml->extra_vars, $lang, 'widget'); + } + else + { + $info->extra_var = new \stdClass; + } + + // Count extra vars. + $info->extra_var_count = count(get_object_vars($info->extra_var)); + + // Return the complete result. + return $info; + } +} diff --git a/common/framework/parsers/XEXMLParser.php b/common/framework/parsers/XEXMLParser.php new file mode 100644 index 000000000..2f944a995 --- /dev/null +++ b/common/framework/parsers/XEXMLParser.php @@ -0,0 +1,116 @@ +getName(); + $result->$root_name = self::_recursiveConvert($xml, $lang); + return $result; + } + + /** + * Convert an XML node recursively. + * + * @param \SimpleXMLElement $element + * @param string $lang + * @return self + */ + protected static function _recursiveConvert(\SimpleXMLElement $element, string $lang): self + { + // Create the basic structure of the node. + $node = new self; + $node->node_name = $element->getName(); + $node->attrs = new self; + $node->body = trim($element->__toString()); + + // Add attributes. + $attrs = $element->attributes(); + foreach ($attrs as $key => $val) + { + $node->attrs->{$key} = trim($val); + } + $attrs = $element->attributes('xml', true); + foreach ($attrs as $key => $val) + { + $node->attrs->{"xml:$key"} = trim($val); + } + + // Recursively process child elements. + foreach ($element->children() as $child) + { + // Skip children that do not match the language. + $attrs = $child->attributes('xml', true); + if (isset($attrs['lang']) && strval($attrs['lang']) !== $lang) + { + continue; + } + + $child_name = $child->getName(); + $child_node = self::_recursiveConvert($child, $lang); + if (!isset($node->$child_name)) + { + $node->$child_name = $child_node; + } + elseif (is_array($node->$child_name)) + { + $node->$child_name[] = $child_node; + } + else + { + $node->$child_name = [$node->$child_name, $child_node]; + } + } + + return $node; + } + + /** + * Hack to prevent undefined property errors. + * + * @param string $name + */ + public function __get($name) + { + return isset($this->$name) ? $this->$name : null; + } +} diff --git a/common/framework/parsers/dbtable/Table.php b/common/framework/parsers/dbtable/Table.php index 67d1e5ca1..de5e73b43 100644 --- a/common/framework/parsers/dbtable/Table.php +++ b/common/framework/parsers/dbtable/Table.php @@ -12,7 +12,7 @@ class Table public $indexes = array(); public $primary_key = array(); public $constraints = array(); - public $deleted = false; + public $is_deleted = false; /** * Generate the CREATE TABLE query for this table. diff --git a/common/js/common.js b/common/js/common.js index 1558437b9..ee59e72d3 100644 --- a/common/js/common.js +++ b/common/js/common.js @@ -1,350 +1,842 @@ /** - * @file common.js - * @author NAVER (developers@xpressengine.com) - * @brief 몇가지 유용한 & 기본적으로 자주 사용되는 자바스크립트 함수들 모음 - **/ + * Common JavaScript library for Rhymix, based on XpressEngine 1.x + */ -(function($) { +/** + * ============= + * Rhymix object + * ============= + */ - /* OS check */ - var UA = navigator.userAgent.toLowerCase(); - $.os = { - Linux: /linux/.test(UA), - Unix: /x11/.test(UA), - Mac: /mac/.test(UA), - Windows: /win/.test(UA) - }; - $.os.name = ($.os.Windows) ? 'Windows' : - ($.os.Linux) ? 'Linux' : - ($.os.Unix) ? 'Unix' : - ($.os.Mac) ? 'Mac' : ''; +const Rhymix = { + addedDocument: [], + langCodes: {}, + loadedPopupMenus: [], + openWindowList: {}, + currentDebugData: null, + pendingDebugData: [], + showAjaxErrors: ['ALL'], + unloading: false, + modal: {}, + state: {} +}; - /* Intercept getScript error due to broken minified script URL */ - $(document).ajaxError(function(event, jqxhr, settings, thrownError) { - if(settings.dataType === "script" && (jqxhr.status >= 400 || (jqxhr.responseText && jqxhr.responseText.length < 40))) { - var match = /^(.+)\.min\.(css|js)($|\?)/.exec(settings.url); - if(match) { - $.getScript(match[1] + "." + match[2], settings.success); - } - } - }); +/** + * Check if the current device is a mobile device. + * + * @return bool + */ +Rhymix.isMobile = function() { + return String(navigator.userAgent).match(/mobile/i); +}; - /** - * @brief Check if two URLs belong to the same origin - */ - window.isSameOrigin = function(url1, url2) { - if(!url1 || !url2) { - return false; - } - if (url1.match(/^\.?\/[^\/]*/) || url2.match(/^\.?\/[^\/]*/)) { - return true; - } - if (url1.match(/^(https?:)?\/\/[^\/]*[^a-z0-9\/.:_-]/i) || url2.match(/^(https?:)?\/\/[^\/]*[^a-z0-9\/.:_-]/i)) { - return false; - } +/** + * Get the current color scheme + * + * @return string + */ +Rhymix.getColorScheme = function() { + if ($('body').hasClass('color_scheme_dark')) { + return 'dark'; + } else { + return 'light'; + } +}; - try { - url1 = window.XE.URI(url1).normalizePort().normalizeHostname().normalizePathname().origin(); - url2 = window.XE.URI(url2).normalizePort().normalizeHostname().normalizePathname().origin(); - return (url1 === url2) ? true : false; - } catch (err) { - return false; - } - }; +/** + * Set the color scheme + * + * @param string color_scheme + * @return void + */ +Rhymix.setColorScheme = function(color_scheme) { + if (color_scheme === 'dark' || color_scheme === 'light') { + $('body').addClass('color_scheme_' + color_scheme).removeClass('color_scheme_' + (color_scheme === 'dark' ? 'light' : 'dark')); + this.cookie.set('rx_color_scheme', color_scheme, { path: this.URI(default_url).pathname(), expires: 365 }); + } else { + this.cookie.remove('rx_color_scheme', { path: this.URI(default_url).pathname() }); + color_scheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme:dark)').matches) ? 'dark' : 'light'; + $('body').addClass('color_scheme_' + color_scheme).removeClass('color_scheme_' + (color_scheme === 'dark' ? 'light' : 'dark')); + } +}; - /** - * Get CSRF token for the document - */ - window.getCSRFToken = function() { - return $("meta[name='csrf-token']").attr("content"); - }; - - /** - * Set CSRF token for the document - */ - window.setCSRFToken = function(token) { - $("meta[name='csrf-token']").attr("content", token); - }; - - /* Intercept jQuery AJAX calls to add CSRF headers */ - $.ajaxPrefilter(function(options) { - if (!isSameOrigin(location.href, options.url)) return; - var token = getCSRFToken(); - if (token) { - if (!options.headers) options.headers = {}; - options.headers["X-CSRF-Token"] = token; - } - }); - - /* Add CSRF token to dynamically loaded forms */ - $.fn.addCSRFTokenToForm = function() { - var token = getCSRFToken(); - if (token) { - return $(this).each(function() { - if ($(this).data("csrf-token-checked") === "Y") return; - if ($(this).attr("action") && !isSameOrigin(location.href, $(this).attr("action"))) { - return $(this).data("csrf-token-checked", "Y"); - } - $("").attr({ type: "hidden", name: "_rx_csrf_token", value: token }).appendTo($(this)); - return $(this).data("csrf-token-checked", "Y"); - }); +/** + * Automatically detect the color scheme + * + * @return void + */ +Rhymix.detectColorScheme = function() { + // Return if a color scheme is already selected. + const body_element = $('body'); + if(body_element.hasClass('color_scheme_light') || body_element.hasClass('color_scheme_dark')) { + return; + } + // Detect the cookie. + let color_scheme = this.cookie.get('rx_color_scheme'); + // Detect the device color scheme. + let match_media = window.matchMedia ? window.matchMedia('(prefers-color-scheme:dark)') : null; + if (color_scheme !== 'light' && color_scheme !== 'dark') { + color_scheme = (match_media && match_media.matches) ? 'dark' : 'light'; + } + // Set the body class according to the detected color scheme. + body_element.addClass('color_scheme_' + color_scheme); + // Add an event listener to detect changes to the device color scheme. + match_media && match_media.addListener && match_media.addListener(function(e) { + if (e.matches) { + body_element.removeClass('color_scheme_light').addClass('color_scheme_dark'); } else { - return $(this); + body_element.removeClass('color_scheme_dark').addClass('color_scheme_light'); } + }); +}; + +/** + * Get the language + * + * @return string + */ +Rhymix.getLangType = function() { + return window.current_lang; +}; + +/** + * Set the language + * + * @param string lang_type + * @return void + */ +Rhymix.setLangType = function(lang_type) { + const baseurl = this.getBaseUrl(); + if (baseurl !== '/') { + this.cookie.remove('lang_type', { path: '/' }); + } + this.cookie.set('lang_type', lang_type, { path: baseurl, expires: 365 }); +}; + +/** + * Get CSRF token for this document + * + * @return string|null + */ +Rhymix.getCSRFToken = function() { + return $("meta[name='csrf-token']").attr("content"); +}; + +/** + * Set CSRF token for this document + * + * @param string token + * @return void + */ +Rhymix.setCSRFToken = function(token) { + $("meta[name='csrf-token']").attr("content", token); +}; + +/** + * Get the current rewrite level + * + * @return int + */ +Rhymix.getRewriteLevel = function() { + return window.rewrite_level; +}; + +/** + * Get the base URL relative to the current origin + * + * @return string + */ +Rhymix.getBaseUrl = function() { + if (!this.state.baseUrl) { + this.state.baseUrl = this.URI(default_url).pathname(); + } + return this.state.baseUrl; +}; + +/** + * Get the full default URL + * + * @return string + */ +Rhymix.getDefaultUrl = function() { + return window.default_url; +}; + +/** + * Get the current page's long URL + * + * @return string + */ +Rhymix.getCurrentUrl = function() { + return window.current_url; +}; + +/** + * Get the current page prefix (mid) + * + * @return string + */ +Rhymix.getCurrentUrlPrefix = function() { + return window.current_mid; +}; + +/** + * Check if a URL is identical to the current page URL except for the hash + * + * @param string url + * @return bool + */ +Rhymix.isCurrentUrl = function(url) { + const absolute_url = window.location.href; + const relative_url = window.location.pathname + window.location.search; + return url === absolute_url || url === relative_url || + url.indexOf(absolute_url.replace(/#.+$/, "") + "#") === 0 || + url.indexOf(relative_url.replace(/#.+$/, "") + "#") === 0; +}; + +/** + * Check if two URLs belong to the same origin + * + * @param string url1 + * @param string url2 + * @return bool + */ +Rhymix.isSameOrigin = function(url1, url2) { + if(!url1 || !url2) { + return false; + } + if (url1.match(/^\.?\/[^\/]*/) || url2.match(/^\.?\/[^\/]*/)) { + return true; + } + if (url1.match(/^(https?:)?\/\/[^\/]*[^a-z0-9\/.:_-]/i) || url2.match(/^(https?:)?\/\/[^\/]*[^a-z0-9\/.:_-]/i)) { + return false; + } + try { + url1 = this.URI(url1).normalizePort().normalizeHostname().normalizePathname().origin(); + url2 = this.URI(url2).normalizePort().normalizeHostname().normalizePathname().origin(); + return (url1 === url2) ? true : false; + } catch (err) { + return false; + } +} + +/** + * Check if a URL belongs to the same host as the current page + * + * Note that this function does not check the protocol. + * It is therefore a weaker check than isSameOrigin(). + * + * @param string url + * @return bool + */ +Rhymix.isSameHost = function(url) { + if (typeof url !== 'string') { + return false; + } + if (url.match(/^\.?\/[^\/]/)) { + return true; + } + if (url.match(/^\w+:[^\/]*$/) || url.match(/^(https?:)?\/\/[^\/]*[^a-z0-9\/.:_-]/i)) { + return false; + } + if (!this.state.partialOrigin) { + let uri = this.URI(window.request_uri).normalizePort().normalizeHostname().normalizePathname(); + this.state.partialOrigin = uri.hostname() + uri.directory(); + } + try { + let target_url = this.URI(url).normalizePort().normalizeHostname().normalizePathname(); + if (target_url.is('urn')) { + return false; + } + if (!target_url.hostname()) { + target_url = target_url.absoluteTo(window.request_uri); + } + target_url = target_url.hostname() + target_url.directory(); + return target_url.indexOf(this.state.partialOrigin) === 0; + } catch(err) { + return false; + } +}; + +/** + * Redirect to a URL, but reload instead if the target is the same as the current page + * + * @param string url + * @return void + */ +Rhymix.redirectToUrl = function(url) { + if (this.isCurrentUrl(url)) { + window.location.href = url; + window.location.reload(); + } else { + window.location.href = url; + } +}; + +/** + * Open a new window and focus it + * + * @param string url + * @param string target + * @param string features + */ +Rhymix.openWindow = function(url, target, features) { + + // Fill default values + if (typeof target === 'undefined') { + target = '_blank'; + } + if (typeof features === 'undefined') { + features = ''; + } + + // Close any existing window with the same target name + try { + if (target !== '_blank' && this.openWindowList[target]) { + this.openWindowList[target].close(); + delete this.openWindowList[target]; + } + } catch(e) {} + + // Open using Blankshield if the target is a different site + if (!this.isSameHost(url)) { + window.blankshield.open(url, target, features); + } else { + const win = window.open(url, target, features); + win.focus(); + if (target !== '_blank') { + this.openWindowList[target] = win; + } + } +}; + +/** + * Open a popup with standard features, for backward compatibility + * + * @param string url + * @param string target + * @return void + */ +Rhymix.openPopup = function(url, target) { + const features = 'width=800,height=600,toolbars=no,scrollbars=yes,resizable=yes'; + this.openWindow(url, target, features); +}; + +/** + * Save background scroll position + * + * @param bool pushState + * @return void + */ +Rhymix.modal.saveBackgroundPosition = function(modal_id, pushState) { + const body = $(document.body); + if (!body.data('rx_scroll_position')) { + body.data('rx_scroll_position', { + left: $(window).scrollLeft(), + top: $(window).scrollTop() + }); + } + body.addClass('rx_modal_open'); + if (pushState) { + history.pushState({ modal: modal_id }, '', location.href); + } +}; + +/** + * Open an HTML element as a modal + * + * @param string id + * @return void + */ +Rhymix.modal.open = function(id) { + this.saveBackgroundPosition(id, true); + $('#' + id).addClass('active'); +}; + +/** + * Open an iframe as a modal + * + * @param string url + * @param string target + * @return void + */ +Rhymix.modal.openIframe = function(url, target) { + const iframe = document.createElement('iframe'); + const iframe_sequence = String(Date.now()) + Math.round(Math.random() * 1000000); + const iframe_id = '_rx_iframe_' + iframe_sequence; + iframe.setAttribute('id', iframe_id); + iframe.setAttribute('class', 'rx_modal'); + iframe.setAttribute('name', target || ('_rx_iframe_' + iframe_sequence)) + iframe.setAttribute('src', url + '&iframe_sequence=' + iframe_sequence); + iframe.setAttribute('width', '100%'); + iframe.setAttribute('height', '100%'); + iframe.setAttribute('style', 'position:fixed; top:0; left:0; width:100%; height:100%; border:0; z-index:999999999; background-color: #fff; overflow-y:auto'); + this.saveBackgroundPosition(iframe_id, true); + $(document.body).append(iframe); +}; + +/** + * Close currently open modal + * + * @param string id + * @return void + */ +Rhymix.modal.close = function(id) { + history.back(); + /* + if (typeof id === 'string') { + $('#' + id).remove(); + } else { + $('.rx_modal').remove(); + } + */ +}; + +/** + * Make an AJAX request + * + * @param string action + * @param object params + * @param function success + * @param function error + * @return void + */ +Rhymix.ajax = function(action, params, success, error) { + + // Extract action info + if (!action) { + if (params instanceof FormData) { + action = (params.get('module') || params.get('mid')) + '.' + params.get('act'); + if (action === '.') { + action = null; + } + } else { + action = null; + } + } else { + action = action.split('.'); + params = params || {}; + params.module = action[0]; + params.act = action[1]; + action = action.join('.'); + } + + // Add action to URL if the current rewrite level supports it + let url = this.URI(window.request_uri).pathname(); + /* + if (this.getRewriteLevel() >= 2 && action !== null) { + url = url + action.replace('.', '/'); + } else { + url = url + 'index.php'; + } + */ + + // Add a CSRF token. + const headers = {}; + if (action !== null) { + headers['X-CSRF-Token'] = getCSRFToken(); }; - window.rhymix_alert_close = function() { - if($('#rhymix_alert').is(':hidden')) { + // Send the AJAX request + try { + const args = { + type: 'POST', + dataType: 'json', + url: url, + data: params, + processData: (action !== null && !(params instanceof FormData)), + headers : headers, + success : function(data, textStatus, xhr) { + Rhymix.ajaxSuccessHandler(xhr, textStatus, action, data, params, success, error); + }, + error : function(xhr, textStatus, errorThrown) { + Rhymix.ajaxErrorHandler(xhr, textStatus, action, url, params, success, error); + } + }; + if (params instanceof FormData) { + args.contentType = false; + } + $.ajax(args); + } catch(e) { + alert(e); + } +}; + +/** + * Default success handler for AJAX requests + * + * @param object xhr + * @param string textStatus + * @param string action + * @param object data + * @param object params + * @param function success + * @param function errror + * @return void + */ +Rhymix.ajaxSuccessHandler = function(xhr, textStatus, action, data, params, success, error) { + + // Add debug information. + if (data._rx_debug) { + data._rx_debug.page_title = "AJAX : " + action; + if (this.addDebugData) { + this.addDebugData(data._rx_debug); + } else { + this.pendingDebugData.push(data._rx_debug); + } + } + + // If the response contains a Rhymix error code, display the error message. + if (typeof data.error !== 'undefined' && data.error != 0) { + + // If an error callback is defined, call it. Abort if it returns false. + if ($.isFunction(error) && error(data, xhr) === false) { return; } - $('#rhymix_alert').fadeOut(500, function() { - $(this).empty(); - }); - }; - /** - * @brief display alert - */ - window.rhymix_alert = function(message, redirect_url, delay) { - if(!delay) { - delay = 2500; + // If an error message was supplied, display it. + if (data.message) { + let msg = data.message.replace(/\\n/g, "\n"); + if (data.errorDetail) { + msg += "\n\n" + data.errorDetail; + } + alert(msg); + return; } - if(!redirect_url) { - $('#rhymix_alert').text(message).show(); - setTimeout(rhymix_alert_close, delay); - } - else if(isSameOrigin(location.href, redirect_url)) { - Cookies.set('rhymix_alert_message', message, { expires: 1 / 1440, path: '/' }); - Cookies.set('rhymix_alert_delay', delay, { expires: 1 / 1440, path: '/' }); - } - else { - alert(message); - } - }; - $(document).ready(function() { - if(Cookies.get('rhymix_alert_message')) { - rhymix_alert(Cookies.get('rhymix_alert_message'), null, Cookies.get('rhymix_alert_delay')); - Cookies.remove('rhymix_alert_message', { path: '/' }); - Cookies.remove('rhymix_alert_delay', { path: '/' }); + // Rhymix should never return an error code without a message, but if someone does, we handle it here. + let msg = 'AJAX error: ' + (action || 'form submission') + "\n\n" + xhr.responseText; + if (msg.length > 1000) { + msg = msg.substring(0, 1000) + '...'; } - $('#rhymix_alert').click(rhymix_alert_close); - }); + console.error(msg.trim().replace(/\n+/g, "\n")); + if (this.showAjaxErrors.indexOf('ALL') >= 0 || this.showAjaxErrors.indexOf(xhr.status) >= 0) { + alert(msg.trim()); + } + return; + } - /* Array for pending debug data */ - window.rhymix_debug_pending_data = []; + // If a success callback was defined, call it. + if ($.isFunction(success)) { + success(data, xhr); + return; + } - /** - * @brief XE 공용 유틸리티 함수 - * @namespace XE - */ - window.XE = { - loaded_popup_menus : [], - addedDocument : [], - cookie : window.Cookies, - URI : window.URI, - URITemplate : window.URITemplate, - SecondLevelDomains : window.SecondLevelDomains, - IPv6 : window.IPv6, - baseurl : null, + // If the response contains a redirect URL, follow the redirect. + if (data.redirect_url) { + this.redirectToUrl(data.redirect_url.replace(/&/g, '&')); + return; + } +}; - /** - * @brief 특정 name을 가진 체크박스들의 checked 속성 변경 - * @param [itemName='cart',][options={}] - */ - checkboxToggleAll : function(itemName) { - if(!is_def(itemName)) itemName='cart'; - var obj; - var options = { - wrap : null, - checked : 'toggle', - doClick : false +/** + * Default error handler for AJAX requests + * + * @param object xhr + * @param string textStatus + * @param string action + * @param string url + * @param object params + * @param function success + * @param function errror + * @return void + */ +Rhymix.ajaxErrorHandler = function(xhr, textStatus, action, url, params, success, error) { + + // If the user is navigating away, don't do anything. + if (xhr.status == 0 && this.unloading) { + return; + } + + // If the response contains valid JSON, call the success callback instead. + if (xhr.status >= 400 && xhr.responseText) { + let data; + try { + data = JSON.parse(xhr.responseText); + } catch (e) { } + if (data && typeof data.error !== 'undefined') { + this.ajaxSuccessHandler(xhr, textStatus, action, data, params, success, error); + return; + } + } + + // If an error callback is defined, call it. Abort if it returns false. + if ($.isFunction(error)) { + let fakedata = { error: -3, message: textStatus }; + if (error(fakedata, xhr) === false) { + return; + } + } + + // Otherwise, generate a simple error message. + let error_info, msg; + if (xhr.status == 0) { + error_info = 'Connection failed: ' + url + "\n\n" + (xhr.responseText || ''); + } else { + error_info = 'Response code: ' + xhr.status + "\n\n" + (xhr.responseText || ''); + } + msg = 'AJAX error: ' + (action || 'form submission') + "\n\n" + error_info; + if (msg.length > 1000) { + msg = msg.substring(0, 1000) + '...'; + } + + // Print the error message. + console.error(msg.trim().replace(/\n+/g, "\n")); + if (this.showAjaxErrors.indexOf('ALL') >= 0 || this.showAjaxErrors.indexOf(xhr.status) >= 0) { + alert(msg.trim()); + } +}; + +/** + * Submit a form using AJAX instead of navigating away + * + * @param HTMLElement form + * @param function success + * @param function error + * @return void + */ +Rhymix.ajaxForm = function(form, success, error) { + const $form = $(form); + // Get success and error callback functions. + if (typeof success === 'undefined') { + success = $form.data('callbackSuccess'); + if (success && $.isFunction(success)) { + // no-op + } else if (success && window[success] && $.isFunction(window[success])) { + success = window[success]; + } else { + success = function(data) { + if (data.message && data.message !== 'success') { + alert(data.message); + } + if (data.redirect_url) { + Rhymix.redirectToUrl(data.redirect_url.replace(/&/g, '&')); + } }; - - switch(arguments.length) { - case 1: - if(typeof(arguments[0]) == "string") { - itemName = arguments[0]; - } else { - $.extend(options, arguments[0] || {}); - itemName = 'cart'; - } - break; - case 2: - itemName = arguments[0]; - $.extend(options, arguments[1] || {}); - } - - if(options.doClick === true) options.checked = null; - if(typeof(options.wrap) == "string") options.wrap ='#'+options.wrap; - - if(options.wrap) { - obj = $(options.wrap).find('input[name="'+itemName+'"]:checkbox'); - } else { - obj = $('input[name="'+itemName+'"]:checkbox'); - } - - if(options.checked == 'toggle') { - obj.each(function() { - $(this).attr('checked', ($(this).attr('checked')) ? false : true); - }); - } else { - if(options.doClick === true) { - obj.click(); - } else { - obj.attr('checked', options.checked); - } - } - }, - - /** - * @brief 문서/회원 등 팝업 메뉴 출력 - */ - displayPopupMenu : function(ret_obj, response_tags, params) { - var target_srl = params.target_srl; - var menu_id = params.menu_id; - var menus = ret_obj.menus; - var html = ""; - var isMobile = navigator.userAgent.match(/mobile/i); - - if(this.loaded_popup_menus[menu_id]) { - html = this.loaded_popup_menus[menu_id]; - - } else { - if(menus) { - var item = menus.item || menus; - if(typeof(item.length)=='undefined' || item.length<1) item = new Array(item); - if(item.length) { - for(var i=0;i'+str+' '; - } - } - } - this.loaded_popup_menus[menu_id] = html; - } - - /* 레이어 출력 */ - if(html) { - var area = $('#popup_menu_area').html('
    '+html+'
'); - var areaOffset = {top:params.page_y, left:params.page_x}; - - if(area.outerHeight()+areaOffset.top > $(window).height()+$(window).scrollTop()) - areaOffset.top = $(window).height() - area.outerHeight() + $(window).scrollTop(); - if(area.outerWidth()+areaOffset.left > $(window).width()+$(window).scrollLeft()) - areaOffset.left = $(window).width() - area.outerWidth() + $(window).scrollLeft(); - - area.css({ top:areaOffset.top, left:areaOffset.left }).show().focus(); - } - }, - - /* 동일 사이트 내 주소인지 판단 (프로토콜 제외) */ - isSameHost: function(url) { - if (typeof url !== "string") { - return false; - } - if (url.match(/^\.?\/[^\/]/)) { - return true; - } - if (url.match(/^\w+:[^\/]*$/) || url.match(/^(https?:)?\/\/[^\/]*[^a-z0-9\/.:_-]/i)) { - return false; - } - - if (!window.XE.baseurl) { - window.XE.baseurl = window.XE.URI(window.request_uri).normalizePort().normalizeHostname().normalizePathname(); - window.XE.baseurl = window.XE.baseurl.hostname() + window.XE.baseurl.directory(); - } - - try { - var target_url = window.XE.URI(url).normalizePort().normalizeHostname().normalizePathname(); - if (target_url.is("urn")) { - return false; - } - if (!target_url.hostname()) { - target_url = target_url.absoluteTo(window.request_uri); - } - target_url = target_url.hostname() + target_url.directory(); - return target_url.indexOf(window.XE.baseurl) === 0; - } - catch(err) { - return false; - } - }, - - /* Format file size */ - filesizeFormat: function(size) { - if (size < 2) return size + 'Byte'; - if (size < 1024) return size + 'Bytes'; - if (size < 1048576) return (size / 1024).toFixed(1) + 'KB'; - if (size < 1073741824) return (size / 1048576).toFixed(2) + 'MB'; - if (size < 1099511627776) return (size / 1073741824).toFixed(2) + 'GB'; - return (size / 1099511627776).toFixed(2) + 'TB'; } + } + if (typeof error === 'undefined') { + error = $form.data('callbackError'); + if (error && $.isFunction(error)) { + // no-op + } else if (error && window[error] && $.isFunction(window[error])) { + error = window[error]; + } else { + error = null; + } + } + this.ajax(null, new FormData($form[0]), success, error); +}; + +/** + * Toggle all checkboxes that have the same name + * + * This is a legacy function. Do not write new code that relies on it. + * + * @param string name + * @return void + */ +Rhymix.checkboxToggleAll = function(name) { + if (!window[name]) { + name='cart'; + } + let options = { + wrap : null, + checked : 'toggle', + doClick : false }; + if (arguments.length == 1) { + if(typeof(arguments[0]) == 'string') { + name = arguments[0]; + } else { + $.extend(options, arguments[0] || {}); + name = 'cart'; + } + } else { + name = arguments[0]; + $.extend(options, arguments[1] || {}); + } + + if (options.doClick === true) { + options.checked = null; + } + if (typeof options.wrap === 'string') { + options.wrap = '#' + options.wrap; + } + + let obj; + if (options.wrap) { + obj = $(options.wrap).find('input[name="'+name+'"]:checkbox'); + } else { + obj = $('input[name="'+name+'"]:checkbox'); + } + + if (options.checked === 'toggle') { + obj.each(function() { + $(this).prop('checked', $(this).prop('checked') ? false : true); + }); + } else { + if(options.doClick === true) { + obj.click(); + } else { + obj.prop('checked', options.checked); + } + } +}; + +/** + * Display a popup menu for members, documents, etc. + * + * @param object ret_obj + * @param object response_tags + * @param object params + * @return void + */ +Rhymix.displayPopupMenu = function(ret_obj, response_tags, params) { + const menu_id = params.menu_id; + const menus = ret_obj.menus; + let html = ""; + + if (this.loadedPopupMenus[menu_id]) { + html = this.loadedPopupMenus[menu_id]; + } else { + if (menus) { + let item = menus.item || menus; + if (typeof item.length === 'undefined' || item.length < 1) { + item = new Array(item); + } + if (item.length) { + for (let i = 0; i < item.length; i++) { + var url = item[i].url; + var str = item[i].str; + var classname = item[i]['class']; + var icon = item[i].icon; + var target = item[i].target; + + // Convert self to _self #2154 + if (target === 'self') { + target = '_self'; + } + + var actmatch = url.match(/\bact=(\w+)/) || url.match(/\b((?:disp|proc)\w+)/); + var act = actmatch ? actmatch[1] : null; + var classText = 'class="' + (classname ? classname : (act ? (act + ' ') : '')); + var styleText = ""; + var click_str = ""; + var matches = []; + if (target === 'popup') { + if (this.isMobile()) { + click_str = 'onclick="openModalIframe(this.href, \''+target+'\'); return false;"'; + } else { + click_str = 'onclick="popopen(this.href, \''+target+'\'); return false;"'; + } + classText += 'popup '; + } else if (target === 'javascript') { + click_str = 'onclick="'+url+'; return false; "'; + classText += 'javascript '; + url = '#'; + } else if (target.match(/^_(self|blank|parent|top)$/)) { + click_str = 'target="' + target + '"'; + classText += 'frame_' + target + ' '; + } else if (matches = target.match(/^i?frame:([a-zA-Z0-9_]+)$/)) { + click_str = 'target="' + matches[1] + '"'; + classText += 'frame_' + matches[1] + ' '; + } else { + click_str = 'target="_blank"'; + } + classText = classText.trim() + '" '; + + html += '
  • '+str+'
  • '; + } + } + } + this.loadedPopupMenus[menu_id] = html; + } + + /* 레이어 출력 */ + if (html) { + const area = $('#popup_menu_area').html('
      '+html+'
    '); + const areaOffset = {top:params.page_y, left:params.page_x}; + if (area.outerHeight()+areaOffset.top > $(window).height()+$(window).scrollTop()) { + areaOffset.top = $(window).height() - area.outerHeight() + $(window).scrollTop(); + } + if (area.outerWidth()+areaOffset.left > $(window).width()+$(window).scrollLeft()) { + areaOffset.left = $(window).width() - area.outerWidth() + $(window).scrollLeft(); + } + area.css({ top:areaOffset.top, left:areaOffset.left }).show().focus(); + } +}; + +/** + * Format file size + * + * @param int size + * @return string + */ +Rhymix.filesizeFormat = function(size) { + if (size < 2) { + return size + 'Byte'; + } + if (size < 1024) { + return size + 'Bytes'; + } + if (size < 1048576) { + return (size / 1024).toFixed(1) + 'KB'; + } + if (size < 1073741824) { + return (size / 1048576).toFixed(2) + 'MB'; + } + if (size < 1099511627776) { + return (size / 1073741824).toFixed(2) + 'GB'; + } + return (size / 1099511627776).toFixed(2) + 'TB'; +}; + +/** + * Get or set a lang code + * + * @param string key + * @param string val + * @return string|void + */ +Rhymix.lang = function(key, val) { + if (typeof val === 'undefined') { + return this.langCodes[key] || key; + } else { + return this.langCodes[key] = val; + } +}; + +// Add aliases to loaded libraries +Rhymix.cookie = window.Cookies; +Rhymix.URI = window.URI; +Rhymix.URITemplate = window.URITemplate; +Rhymix.SecondLevelDomains = window.SecondLevelDomains; +Rhymix.IPv6 = window.IPv6; + +// Alias to XE for backward compatibility +const XE = Rhymix; + +/** + * ============================ + * Document ready event handler + * ============================ + */ + +$(function() { + /** - * Shim for Modernizr if it is not loaded + * Inject CSRF token to all POST forms */ - window.Modernizr = { - audio: true, - video: true, - canvas: true, - history: true, - postmessage: true, - geolocation: ('geolocation' in navigator), - touch: ('ontouchstart' in window) || (navigator.maxTouchPoints > 0), - webgl: !!window.WebGLRenderingContext - }; - -})(jQuery); - -/* jQuery(document).ready() */ -jQuery(function($) { - - /* CSRF token */ - $("form[method]").filter(function() { return String($(this).attr("method")).toUpperCase() == "POST"; }).addCSRFTokenToForm(); - $(document).on("submit", "form[method='post']", $.fn.addCSRFTokenToForm); - $(document).on("focus", "input,select,textarea", function() { - $(this).parents("form[method]").filter(function() { return String($(this).attr("method")).toUpperCase() == "POST"; }).addCSRFTokenToForm(); + $('form[method]').filter(function() { + return String($(this).attr('method')).toUpperCase() == 'POST'; + }).addCSRFTokenToForm(); + $(document).on('submit', 'form[method=post]', $.fn.addCSRFTokenToForm); + $(document).on('focus', 'input,select,textarea', function() { + $(this).parents('form[method]').filter(function() { + return String($(this).attr('method')).toUpperCase() == 'POST'; + }).addCSRFTokenToForm(); }); /** @@ -354,45 +846,45 @@ jQuery(function($) { * This is not required in most modern browsers. * https://caniuse.com/mdn-html_elements_a_implicit_noopener */ - var noopenerRequired = (function() { - var isChromeBased = navigator.userAgent.match(/Chrome\/([0-9]+)/); + const noopenerRequired = (function() { + const isChromeBased = navigator.userAgent.match(/Chrome\/([0-9]+)/); if (isChromeBased && parseInt(isChromeBased[1], 10) >= 72) { return false; } - var isAppleWebKit = navigator.userAgent.match(/AppleWebKit\/([0-9]+)/); + const isAppleWebKit = navigator.userAgent.match(/AppleWebKit\/([0-9]+)/); if (isAppleWebKit && parseInt(isAppleWebKit[1], 10) >= 605) { return false; } - var isFirefox = navigator.userAgent.match(/Firefox\/([0-9]+)/); + const isFirefox = navigator.userAgent.match(/Firefox\/([0-9]+)/); if (isFirefox && parseInt(isFirefox[1], 10) >= 79) { return false; } return true; })(); $('a[target]').each(function() { - var $this = $(this); - var href = String($this.attr('href')).trim(); - var target = String($this.attr('target')).trim(); + const $this = $(this); + const href = String($this.attr('href')).trim(); + const target = String($this.attr('target')).trim(); if (!href || !target || target === '_top' || target === '_self' || target === '_parent') { return; } - if (!window.XE.isSameHost(href)) { - var rel = $this.attr('rel'); + if (!Rhymix.isSameHost(href)) { + let rel = $this.attr('rel'); rel = (typeof rel === 'undefined') ? '' : String(rel); if (!rel.match(/\bnoopener\b/)) { $this.attr('rel', $.trim(rel + ' noopener')); } } }); - $('body').on('click', 'a[target]', function(event) { - var $this = $(this); - var href = String($this.attr('href')).trim(); - var target = String($this.attr('target')).trim(); + $(document).on('click', 'a[target]', function(event) { + const $this = $(this); + const href = String($this.attr('href')).trim(); + const target = String($this.attr('target')).trim(); if (!href || !target || target === '_top' || target === '_self' || target === '_parent') { return; } - if (!window.XE.isSameHost(href)) { - var rel = $this.attr('rel'); + if (!Rhymix.isSameHost(href)) { + let rel = $this.attr('rel'); rel = (typeof rel === 'undefined') ? '' : String(rel); if (!rel.match(/\bnoopener\b/)) { $this.attr('rel', $.trim(rel + ' noopener')); @@ -404,271 +896,831 @@ jQuery(function($) { } }); - /* Editor preview replacement */ - $(".editable_preview").addClass("rhymix_content xe_content").attr("tabindex", 0); - $(".editable_preview").on("click", function() { - var input = $(this).siblings(".editable_preview_content"); + /** + * Enforce max filesize on file uploaeds + */ + $(document).on('change', 'input[type=file]', function() { + const max_filesize = $(this).data('max-filesize'); + if (!max_filesize) { + return; + } + const files = $(this).get(0).files; + if (!files || !files[0]) { + return; + } + if (files[0].size > max_filesize) { + this.value = ''; + const error = String($(this).data('max-filesize-error')); + alert(error.replace('%s', Rhymix.filesizeFormat(max_filesize))); + } + }); + + /** + * Intercept form submission and handle them with AJAX + */ + $(document).on('submit', 'form.rx_ajax', function(event) { + if (!$(this).attr('target')) { + event.preventDefault(); + Rhymix.ajaxForm(this); + } + }); + + /** + * Prevent repeated click on submit button + */ + $(document).on('click', 'input[type="submit"],button[type="submit"]', function(e) { + const timeout = 3000; + setTimeout(function() { + $(this).prop('disabled', true); + }, 100); + setTimeout(function() { + $(this).prop('disabled', false); + }, timeout); + }); + + /** + * Display a popup menu for members, documents, etc. + */ + $(document).on('click', function(e) { + var $area = $('#popup_menu_area'); + if (!$area.length) { + $area = $('