diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 668f1e4c4..1787dd0f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install PHP run: chmod +x .github/workflows/setup-php.sh && .github/workflows/setup-php.sh ${{ matrix.php }} diff --git a/classes/module/ModuleObject.class.php b/classes/module/ModuleObject.class.php index e2ede1a5b..687c07bad 100644 --- a/classes/module/ModuleObject.class.php +++ b/classes/module/ModuleObject.class.php @@ -921,7 +921,7 @@ class ModuleObject extends BaseObject // execute api methods of the module if view action is and result is XMLRPC or JSON if(isset($this->module_info->module_type) && in_array($this->module_info->module_type, ['view', 'mobile'])) { - if(Context::getResponseMethod() == 'XMLRPC' || Context::getResponseMethod() == 'JSON') + if ($this->getHttpStatusCode() < 400 && in_array(Context::getResponseMethod(), ['JSON', 'XMLRPC'])) { $oAPI = getAPI($this->module_info->module); if($oAPI instanceof ModuleObject && method_exists($oAPI, $this->act)) diff --git a/common/constants.php b/common/constants.php index 96b81da1e..ee9fab27d 100644 --- a/common/constants.php +++ b/common/constants.php @@ -3,7 +3,7 @@ /** * RX_VERSION is the version number of the Rhymix CMS. */ -define('RX_VERSION', '2.1.31'); +define('RX_VERSION', '2.1.32'); /** * RX_MICROTIME is the startup time of the current script, in microseconds since the Unix epoch. diff --git a/common/framework/DB.php b/common/framework/DB.php index 084299617..468c0ecd9 100644 --- a/common/framework/DB.php +++ b/common/framework/DB.php @@ -496,9 +496,9 @@ class DB } // Collect various counts used in the page calculation. - $list_count = $query->navigation->list_count->getValue($args)[0]; - $page_count = $query->navigation->page_count->getValue($args)[0]; - $page = $query->navigation->page->getValue($args)[0]; + $list_count = $query->navigation->list_count ? $query->navigation->list_count->getValue($args)[0] : 10; + $page_count = $query->navigation->page_count ? $query->navigation->page_count->getValue($args)[0] : 10; + $page = $query->navigation->page ? $query->navigation->page->getValue($args)[0] : 1; $total_count = intval($count); $total_page = max(1, intval(ceil($total_count / $list_count))); $last_index = $total_count - (($page - 1) * $list_count); diff --git a/common/framework/Security.php b/common/framework/Security.php index bf406894b..5616ff51d 100644 --- a/common/framework/Security.php +++ b/common/framework/Security.php @@ -37,13 +37,19 @@ class Security case 'filename': if (!utf8_check($input)) return false; return Filters\FilenameFilter::clean($input); - + // Clean up SVG content to prevent various attacks. case 'svg': if (!utf8_check($input)) return false; $sanitizer = new \enshrined\svgSanitize\Sanitizer(); return strval($sanitizer->sanitize($input)); + // Clean up a path to prevent argument injection. + case 'command': + if (!utf8_check($input)) return false; + if (\RX_WINDOWS || preg_match('![^a-z0-9/._-]!', $input)) return escapeshellarg($input); + return strval($input); + // Unknown filters. default: throw new Exception('Unknown filter type for sanitize: ' . $type); diff --git a/common/framework/parsers/ConfigParser.php b/common/framework/parsers/ConfigParser.php index 19378874c..a12e3b882 100644 --- a/common/framework/parsers/ConfigParser.php +++ b/common/framework/parsers/ConfigParser.php @@ -84,7 +84,7 @@ class ConfigParser $config['db']['master']['prefix'] .= '_'; } - $config['db']['master']['charset'] = $db_info->master_db['db_charset'] ?: 'utf8'; + $config['db']['master']['charset'] = empty($db_info->master_db['db_charset']) ? 'utf8' : $db_info->master_db['db_charset']; if (strpos($config['db']['master']['type'], 'innodb') !== false) { @@ -105,7 +105,7 @@ class ConfigParser $slave_id = 'slave' . $slave_id; $config['db'][$slave_id]['type'] = strtolower($slave_db['db_type']); $config['db'][$slave_id]['host'] = $slave_db['db_hostname']; - $config['db'][$slave_id]['port'] = $slave_db['db_type']; + $config['db'][$slave_id]['port'] = $slave_db['db_port']; $config['db'][$slave_id]['user'] = $slave_db['db_userid']; $config['db'][$slave_id]['pass'] = $slave_db['db_password']; $config['db'][$slave_id]['database'] = $slave_db['db_database']; @@ -116,7 +116,7 @@ class ConfigParser $config['db'][$slave_id]['prefix'] .= '_'; } - $config['db'][$slave_id]['charset'] = $slave_db['db_charset'] ?: 'utf8'; + $config['db'][$slave_id]['charset'] = empty($slave_db['db_charset']) ? 'utf8' : $slave_db['db_charset']; if (strpos($config['db'][$slave_id]['type'], 'innodb') !== false) { @@ -145,7 +145,7 @@ class ConfigParser // Create new crypto keys. $config['crypto']['encryption_key'] = Security::getRandom(64, 'alnum'); - $config['crypto']['authentication_key'] = $db_info->secret_key ?: Security::getRandom(64, 'alnum'); + $config['crypto']['authentication_key'] = empty($db_info->secret_key) ? Security::getRandom(64, 'alnum') : $db_info->secret_key; $config['crypto']['session_key'] = Security::getRandom(64, 'alnum'); // Convert language configuration. @@ -177,8 +177,8 @@ class ConfigParser $default_url = \Context::decodeIdna($default_url); } $config['url']['default'] = $default_url ?: (\RX_SSL ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . \RX_BASEURL; - $config['url']['http_port'] = $db_info->http_port ?: null; - $config['url']['https_port'] = $db_info->https_port ?: null; + $config['url']['http_port'] = $db_info->http_port ?? null; + $config['url']['https_port'] = $db_info->https_port ?? null; // Convert SSL configuration. if (isset($db_info->use_ssl) && in_array($db_info->use_ssl, ['always', 'optional'])) @@ -193,11 +193,11 @@ class ConfigParser } // Convert session configuration. - $config['session']['delay'] = $db_info->delay_session === 'Y' ? true : false; - $config['session']['use_db'] = $db_info->use_db_session === 'Y' ? true : false; + $config['session']['delay'] = ($db_info->delay_session ?? 'N') === 'Y' ? true : false; + $config['session']['use_db'] = ($db_info->use_db_session ?? 'N') === 'Y' ? true : false; // Convert view configuration. - $config['view']['minify_scripts'] = $db_info->minify_scripts ?: 'common'; + $config['view']['minify_scripts'] = $db_info->minify_scripts ?? 'common'; // Convert admin IP whitelist. if (isset($db_info->admin_ip_list) && is_array($db_info->admin_ip_list) && count($db_info->admin_ip_list)) @@ -206,9 +206,9 @@ class ConfigParser } // Convert sitelock configuration. - $config['lock']['locked'] = $db_info->use_sitelock === 'Y' ? true : false; - $config['lock']['title'] = strval($db_info->sitelock_title); - $config['lock']['message'] = strval($db_info->sitelock_message); + $config['lock']['locked'] = ($db_info->use_sitelock ?? 'N') === 'Y' ? true : false; + $config['lock']['title'] = strval($db_info->sitelock_title ?? ''); + $config['lock']['message'] = strval($db_info->sitelock_message ?? ''); if (!is_array($db_info->sitelock_whitelist)) { $db_info->sitelock_whitelist = $db_info->sitelock_whitelist ? array_map('trim', explode(',', trim($db_info->sitelock_whitelist))) : array(); @@ -220,7 +220,7 @@ class ConfigParser $config['lock']['allow'] = array_values($db_info->sitelock_whitelist); // Convert media filter configuration. - if (is_array($db_info->embed_white_iframe)) + if (is_array($db_info->embed_white_iframe ?? null)) { $whitelist = array_unique(array_map(function($item) { return preg_match('@^https?://(.*)$@i', $item, $matches) ? $matches[1] : $item; @@ -228,7 +228,7 @@ class ConfigParser natcasesort($whitelist); $config['mediafilter']['iframe'] = $whitelist; } - if (is_array($db_info->embed_white_object)) + if (is_array($db_info->embed_white_object ?? null)) { $whitelist = array_unique(array_map(function($item) { return preg_match('@^https?://(.*)$@i', $item, $matches) ? $matches[1] : $item; @@ -240,9 +240,9 @@ class ConfigParser // Convert miscellaneous configuration. $config['file']['folder_structure'] = 1; $config['file']['umask'] = Storage::recommendUmask(); - $config['mobile']['enabled'] = $db_info->use_mobile_view === 'N' ? false : true; - $config['use_rewrite'] = $db_info->use_rewrite === 'Y' ? true : false; - $config['use_sso'] = $db_info->use_sso === 'Y' ? true : false; + $config['mobile']['enabled'] = ($db_info->use_mobile_view ?? 'N') === 'N' ? false : true; + $config['use_rewrite'] = ($db_info->use_rewrite ?? 'N') === 'Y' ? true : false; + $config['use_sso'] = ($db_info->use_sso ?? 'N') === 'Y' ? true : false; // Copy other configuration. unset($db_info->master_db, $db_info->slave_db); diff --git a/common/framework/parsers/dbquery/VariableBase.php b/common/framework/parsers/dbquery/VariableBase.php index 419c74e95..6ee29a2cb 100644 --- a/common/framework/parsers/dbquery/VariableBase.php +++ b/common/framework/parsers/dbquery/VariableBase.php @@ -254,8 +254,11 @@ class VariableBase break; case 'search': $parsed_keywords = $this->_parseSearchKeywords($column, $value); - $where = $parsed_keywords[0]; - $params = array_merge($params, $parsed_keywords[1]); + if (count($parsed_keywords)) + { + $where = $parsed_keywords[0]; + $params = array_merge($params, $parsed_keywords[1]); + } break; case 'plus': $where = sprintf('%s = %s + %s', $column, $column, $is_expression ? $value : '?'); @@ -500,7 +503,7 @@ class VariableBase // parse the value (text); $value = str_replace('"', '"', $value); - $keywords = preg_split('/(\([^\)]*?\))|(\-?\"[^\"]*?\")|[\s,]+/', trim($value), 10, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); + $keywords = preg_split('/(\([^\)]*?\))|(\-?\"[^\"]*?\")|[\s]+/', trim($value), 10, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); $conditions = array(); $operators = array('AND' => 'AND', 'OR' => 'OR', '|' => 'OR'); @@ -521,9 +524,12 @@ class VariableBase if ($item !== '') { $parsed_keywords = $this->_parseSearchKeywords($column, $item); - $conditions[] = $parsed_keywords[0]; - $conditions[] = 'AND'; - $params = array_merge($params, $parsed_keywords[1]); + if (count($parsed_keywords)) + { + $conditions[] = $parsed_keywords[0]; + $conditions[] = 'AND'; + $params = array_merge($params, $parsed_keywords[1]); + } } continue; } @@ -563,9 +569,17 @@ class VariableBase // remove the last point (would be an operator) array_pop($conditions); - $conditions = implode(' ', $conditions); - $where = count($keywords) === 1 ? $conditions : "($conditions)"; - return [$where, $params]; + // combine the conditions and return + if (count($params) === 0) + { + return []; + } + else + { + $conditions = implode(' ', $conditions); + $where = count($params) === 1 ? $conditions : "($conditions)"; + return [$where, $params]; + } } } diff --git a/common/js/plugins/jquery.fileupload/js/main.js b/common/js/plugins/jquery.fileupload/js/main.js index 6cd681f1c..e1269c1d9 100644 --- a/common/js/plugins/jquery.fileupload/js/main.js +++ b/common/js/plugins/jquery.fileupload/js/main.js @@ -104,7 +104,14 @@ var dfd = jQuery.Deferred(); $.each(item.files, function(index, file) { - if(data.settings.maxFileSize > 0 && data.settings.maxFileSize < file.size) { + var extension = file.name.split('.').pop().toLowerCase(); + var preConversionTypes = data.settings.preConversionTypes || []; + var limit = data.settings.maxFileSize; + if (preConversionTypes.length > 0 && preConversionTypes.indexOf(extension) > -1) { + limit = data.settings.preConversionSize || limit; + } + console.log('file size: ' + file.size + ', limit: ' + limit); + if (limit > 0 && limit < file.size) { dfd.reject(); alert(window.xe.lang.msg_exceeds_limit_size); return false; diff --git a/modules/addon/addon.admin.controller.php b/modules/addon/addon.admin.controller.php index 6d4cc630f..392f80db9 100644 --- a/modules/addon/addon.admin.controller.php +++ b/modules/addon/addon.admin.controller.php @@ -209,6 +209,10 @@ class addonAdminController extends addonController $args->{$key} = $vars->{$key} ?? ''; } $args->xe_run_method = $vars->xe_run_method ?? ''; + if (!in_array($args->xe_run_method, ['run_selected', 'no_run_selected'])) + { + $args->xe_run_method = 'run_selected'; + } $args->mid_list = $vars->mid_list ?? []; $output = $this->doSetup($addon_name, $args, $site_module_info->site_srl, 'site'); diff --git a/modules/addon/addon.controller.php b/modules/addon/addon.controller.php index c8225c69a..19b95395c 100644 --- a/modules/addon/addon.controller.php +++ b/modules/addon/addon.controller.php @@ -102,8 +102,8 @@ class addonController extends addon $buff[] = '$before_time = microtime(true);'; // Run method and mid list - $run_method = ($extra_vars->xe_run_method ?? null) ?: 'run_selected'; - $buff[] = '$rm = \'' . $run_method . "';"; + $run_method = strval($extra_vars->xe_run_method ?? 'run_selected'); + $buff[] = '$rm = ' . var_export($run_method, true) . ';'; $buff[] = '$ml = ' . var_export(array_fill_keys($mid_list, true), true) . ';'; $buff[] = '$_m = Context::get(\'mid\');'; diff --git a/modules/board/board.api.php b/modules/board/board.api.php index 6ca21a452..96519ef89 100644 --- a/modules/board/board.api.php +++ b/modules/board/board.api.php @@ -56,12 +56,19 @@ class BoardAPI extends Board public function dispBoardContentView($oModule) { $oDocument = Context::get('oDocument'); - if($oDocument->isGranted()) + if ($oDocument->isExists() && $oDocument->isAccessible()) { - $extra_vars = $oDocument->getExtraVars() ?: []; - $oDocument->add('extra_vars', $this->_arrangeExtraVars($extra_vars)); + if ($oDocument->isGranted()) + { + $extra_vars = $oDocument->getExtraVars() ?: []; + $oDocument->add('extra_vars', $this->_arrangeExtraVars($extra_vars)); + } + $oModule->add('oDocument', $this->_arrangeContent($oDocument, $oModule->grant)); + } + else + { + $oModule->add('oDocument', null); } - $oModule->add('oDocument', $this->_arrangeContent($oDocument, $oModule->grant)); } /** @@ -70,13 +77,13 @@ class BoardAPI extends Board public function dispBoardContentFileList($oModule) { $oDocument = Context::get('oDocument'); - if($oDocument->isAccessible()) + if ($oDocument->isExists() && $oDocument->isAccessible()) { $oModule->add('file_list', $this->_arrangeFiles(Context::get('file_list') ?: [])); } else { - $oModule->add('file_list', array()); + $oModule->add('file_list', []); } } @@ -93,12 +100,20 @@ class BoardAPI extends Board **/ public function dispBoardContentCommentList($oModule) { - $comment_list = Context::get('comment_list'); - if (!is_array($comment_list)) + $oDocument = Context::get('oDocument'); + if ($oDocument->isExists() && $oDocument->isAccessible()) { - $comment_list = []; + $comment_list = Context::get('comment_list'); + if (!is_array($comment_list)) + { + $comment_list = []; + } + $oModule->add('comment_list', $this->_arrangeComments($comment_list)); + } + else + { + $oModule->add('comment_list', []); } - $oModule->add('comment_list', $this->_arrangeComments($comment_list)); } /** diff --git a/modules/board/board.view.php b/modules/board/board.view.php index 8fd071460..1a7a33fae 100644 --- a/modules/board/board.view.php +++ b/modules/board/board.view.php @@ -30,7 +30,7 @@ class BoardView extends Board $this->search_list_count = $m ? ($this->module_info->mobile_search_list_count ?? 20) : ($this->module_info->search_list_count ?? 20); $this->page_count = $m ? ($this->module_info->mobile_page_count ?? 5) : ($this->module_info->page_count ?? 10); $this->except_notice = ($this->module_info->except_notice ?? '') == 'N' ? FALSE : TRUE; - $this->include_modules = ($this->module_info->include_modules ?? []) ? explode(',', $this->module_info->include_modules) : []; + $this->include_modules = ($this->module_info->include_modules ?? []) ? array_map('intval', explode(',', $this->module_info->include_modules)) : []; if (count($this->include_modules) && !in_array($this->module_info->module_srl, $this->include_modules)) { $this->include_modules[] = $this->module_info->module_srl; @@ -318,6 +318,7 @@ class BoardView extends Board { if (abs($oDocument->get('member_srl')) != $this->user->member_srl) { + $oDocument = DocumentModel::getBlankDocument($this->module_srl); Context::set('document_srl', null, true); $this->dispBoardMessage('msg_not_founded', 404); } @@ -326,6 +327,7 @@ class BoardView extends Board // if the document is TEMP saved, pretend that it doesn't exist. if($oDocument->getStatus() == 'TEMP') { + $oDocument = DocumentModel::getBlankDocument($this->module_srl); Context::set('document_srl', null, true); $this->dispBoardMessage('msg_not_founded', 404); } @@ -343,8 +345,7 @@ class BoardView extends Board */ else { - $oDocument = DocumentModel::getDocument(0); - $oDocument->add('module_srl', $this->module_srl); + $oDocument = DocumentModel::getBlankDocument($this->module_srl); } /** @@ -354,7 +355,7 @@ class BoardView extends Board { if(!$this->grant->view && !$oDocument->isGranted()) { - $oDocument = DocumentModel::getDocument(0); + $oDocument = DocumentModel::getBlankDocument($this->module_srl); Context::set('document_srl', null, true); $this->dispBoardMessage($this->user->isMember() ? 'msg_not_permitted' : 'msg_not_logged'); } diff --git a/modules/comment/comment.admin.controller.php b/modules/comment/comment.admin.controller.php index c9e2cb875..3e7b55283 100644 --- a/modules/comment/comment.admin.controller.php +++ b/modules/comment/comment.admin.controller.php @@ -237,7 +237,8 @@ class CommentAdminController extends Comment $module_infos[$module_srl] = ModuleModel::getModuleInfoByModuleSrl($module_srl)->comment_delete_message ?? ''; } - if($module_infos[$module_srl] === 'yes') + $policy = $module_infos[$module_srl]; + if ($policy === 'yes' && !in_array($comment->get('status'), [\RX_STATUS_DELETED, \RX_STATUS_DELETED_BY_ADMIN])) { $output = $oCommentController->updateCommentByDelete($comment, true); if(!$output->toBool() && $output->error !== -2) @@ -246,7 +247,7 @@ class CommentAdminController extends Comment return $output; } } - elseif(starts_with('only_comm', $module_infos[$module_srl])) + elseif (starts_with('only_comm', $policy) && !in_array($comment->get('status'), [\RX_STATUS_DELETED, \RX_STATUS_DELETED_BY_ADMIN])) { $childs = CommentModel::getChildComments($comment_srl); if(count($childs) > 0) diff --git a/modules/comment/comment.admin.view.php b/modules/comment/comment.admin.view.php index c3f229ca0..683a0685d 100644 --- a/modules/comment/comment.admin.view.php +++ b/modules/comment/comment.admin.view.php @@ -29,13 +29,12 @@ class CommentAdminView extends Comment { // option to get a list $args = new stdClass(); - $args->page = Context::get('page'); // /< Page - $args->list_count = 30; // / the number of postings to appear on a single page - $args->page_count = 5; // / the number of pages to appear on the page navigation - + $args->list_count = intval(Context::get('list_count')) ?: 20; + $args->page_count = 5; + $args->page = max(1, intval(Context::get('page'))); $args->sort_index = 'list_order'; // /< Sorting values - $args->module_srl = Context::get('module_srl'); + /* $search_target = Context::get('search_target'); $search_keyword = Context::get('search_keyword'); @@ -118,10 +117,10 @@ class CommentAdminView extends Comment { // option to get a blacklist $args = new stdClass(); - $args->page = Context::get('page'); // /< Page - $args->list_count = 30; // /< the number of comment postings to appear on a single page - $args->page_count = 10; // /< the number of pages to appear on the page navigation - $args->order_type = 'desc'; // /< sorted value + $args->list_count = intval(Context::get('list_count')) ?: 20; + $args->page_count = 5; + $args->page = max(1, intval(Context::get('page'))); + $args->order_type = 'desc'; // select sort method $sort_index = Context::get('sort_index'); @@ -214,11 +213,10 @@ class CommentAdminView extends Comment { // option for a list $args = new stdClass; - $args->page = Context::get('page'); // /< Page - $args->list_count = 30; // /< the number of posts to display on a single page - $args->page_count = 10; // /< the number of pages that appear in the page navigation $args->comment_srl = intval(Context::get('target_srl')); - + $args->list_count = intval(Context::get('list_count')) ?: 20; + $args->page_count = 5; + $args->page = max(1, intval(Context::get('page'))); // get Status name list $oCommentModel = getModel('comment'); diff --git a/modules/document/document.admin.view.php b/modules/document/document.admin.view.php index 5db78c216..b21214243 100644 --- a/modules/document/document.admin.view.php +++ b/modules/document/document.admin.view.php @@ -37,10 +37,15 @@ class DocumentAdminView extends Document { // option to get a list $args = new stdClass(); - $args->page = Context::get('page'); // /< Page - $args->list_count = 30; // /< the number of posts to display on a single page - $args->page_count = 5; // /< the number of pages that appear in the page navigation + $args->list_count = intval(Context::get('list_count')) ?: 30; + $args->page_count = 5; + $args->page = max(1, intval(Context::get('page'))); + $args->sort_index = 'list_order'; + $args->module_srl = Context::get('module_srl'); + $args->statusList = []; + $args->use_division = false; + // search options $args->search_target = Context::get('search_target'); // /< search (title, contents ...) $args->search_keyword = Context::get('search_keyword'); // /< keyword to search if ($args->search_target === 'member_srl') @@ -53,11 +58,6 @@ class DocumentAdminView extends Document } } - $args->sort_index = 'list_order'; // /< sorting value - $args->module_srl = Context::get('module_srl'); - $args->statusList = []; - $args->use_division = false; - // get a list $columnList = array('document_srl', 'module_srl', 'category_srl', 'member_srl', 'title', 'nick_name', 'comment_count', 'trackback_count', 'readed_count', 'voted_count', 'blamed_count', 'regdate', 'ipaddress', 'status'); $output = DocumentModel::getDocumentList($args, false, true, $columnList); @@ -160,9 +160,9 @@ class DocumentAdminView extends Document // option for a list $args = new stdClass(); - $args->page = intval(Context::get('page')) ?: 1; // /< Page - $args->list_count = 20; // /< the number of posts to display on a single page - $args->page_count = 10; // /< the number of pages that appear in the page navigation + $args->list_count = intval(Context::get('list_count')) ?: 20; + $args->page_count = 5; + $args->page = max(1, intval(Context::get('page'))); $args->order_type = strtolower(Context::get('order_type')) === 'asc' ? 'asc' : 'desc'; // select sort method @@ -254,11 +254,10 @@ class DocumentAdminView extends Document { // option for a list $args = new stdClass; - $args->page = Context::get('page'); // /< Page - $args->list_count = 30; // /< the number of posts to display on a single page - $args->page_count = 10; // /< the number of pages that appear in the page navigation $args->document_srl = intval(Context::get('target_srl')); - + $args->list_count = intval(Context::get('list_count')) ?: 20; + $args->page_count = 5; + $args->page = max(1, intval(Context::get('page'))); // get Status name list $oDocumentModel = getModel('document'); @@ -330,12 +329,11 @@ class DocumentAdminView extends Document { // options for a list $args = new stdClass(); - $args->page = Context::get('page'); // /< Page - $args->list_count = 30; // /< the number of posts to display on a single page - $args->page_count = 10; // /< the number of pages that appear in the page navigation - - $args->sort_index = 'list_order'; // /< sorting values - $args->order_type = 'desc'; // /< sorting values by order + $args->list_count = intval(Context::get('list_count')) ?: 30; + $args->page_count = 5; + $args->page = max(1, intval(Context::get('page'))); + $args->sort_index = 'list_order'; + $args->order_type = 'desc'; $args->module_srl = Context::get('module_srl'); diff --git a/modules/document/document.controller.php b/modules/document/document.controller.php index 826e6f4c3..a6a1e0cb1 100644 --- a/modules/document/document.controller.php +++ b/modules/document/document.controller.php @@ -3585,7 +3585,7 @@ class DocumentController extends Document { // Set message $title = sprintf(lang('default_message_format'), $actions[$obj->type]); - $content = <<{$title}

{$obj->manager_message}


@@ -3615,7 +3615,7 @@ class DocumentController extends Document $oCommunicationController = CommunicationController::getInstance(); foreach ($recipients as $member_srl => $items) { - $content = sprintf($content, implode('', $items)); + $content = sprintf($common_content, implode('', $items)); $oCommunicationController->sendMessage($this->user->member_srl, $member_srl, $title, $content, true, null, false); } } diff --git a/modules/document/document.model.php b/modules/document/document.model.php index 2cbe383fa..97e89ceaa 100644 --- a/modules/document/document.model.php +++ b/modules/document/document.model.php @@ -146,22 +146,23 @@ class DocumentModel extends Document } /** - * Import Document + * Get a document. + * * @param int $document_srl * @param bool $is_admin * @param bool $load_extra_vars * @param bool $reload_counts - * @return documentItem + * @return DocumentItem */ public static function getDocument($document_srl = 0, $is_admin = false, $load_extra_vars = true, $reload_counts = true) { if(!$document_srl) { - return new documentItem(); + return new DocumentItem(); } if(!isset($GLOBALS['XE_DOCUMENT_LIST'][$document_srl])) { - $oDocument = new documentItem($document_srl, $load_extra_vars, $reload_counts); + $oDocument = new DocumentItem($document_srl, $load_extra_vars, $reload_counts); if(!$oDocument->isExists()) { return $oDocument; @@ -176,13 +177,26 @@ class DocumentModel extends Document return $GLOBALS['XE_DOCUMENT_LIST'][$document_srl]; } + /** + * Create a blank document. + * + * @param int $module_srl + * @return DocumentItem + */ + public static function getBlankDocument($module_srl = 0): DocumentItem + { + $oDocument = new DocumentItem(); + $oDocument->add('module_srl', $module_srl); + return $oDocument; + } + /** * Bringing multiple documents (or paging) * @param array|string $document_srls * @param bool $is_admin * @param bool $load_extra_vars * @param array $columnList - * @return array value type is documentItem + * @return array value type is DocumentItem */ public static function getDocuments($document_srls, $is_admin = false, $load_extra_vars = true, $columnList = array()) { @@ -207,7 +221,7 @@ class DocumentModel extends Document { if(!isset($GLOBALS['XE_DOCUMENT_LIST'][$attribute->document_srl])) { - $oDocument = new documentItem(); + $oDocument = new DocumentItem(); $oDocument->setAttribute($attribute, false); } if($is_admin) @@ -233,7 +247,7 @@ class DocumentModel extends Document * @param bool $except_notice * @param bool $load_extra_vars * @param array $columnList - * @return Object + * @return BaseObject */ public static function getDocumentList($obj, $except_notice = false, $load_extra_vars = true, $columnList = array()) { @@ -300,7 +314,7 @@ class DocumentModel extends Document * Module_srl value, bringing the document's gongjisa Port * @param object $obj * @param array $columnList - * @return object|void + * @return BaseObject */ public static function getNoticeList($obj, $columnList = array()) { @@ -338,7 +352,7 @@ class DocumentModel extends Document { if(!isset($GLOBALS['XE_DOCUMENT_LIST'][$attribute->document_srl])) { - $oDocument = new documentItem(); + $oDocument = new DocumentItem(); $oDocument->setAttribute($attribute, false); } diff --git a/modules/editor/editor.model.php b/modules/editor/editor.model.php index a0a206b4a..f176db4fd 100644 --- a/modules/editor/editor.model.php +++ b/modules/editor/editor.model.php @@ -235,13 +235,17 @@ class EditorModel extends Editor { // Get file upload limits $file_config = FileModel::getUploadConfig(); - $file_config->allowed_attach_size = $file_config->allowed_attach_size*1024*1024; - $file_config->allowed_filesize = $file_config->allowed_filesize*1024*1024; + $file_config->allowed_attach_size = $file_config->allowed_attach_size * 1048576; + $file_config->allowed_filesize = $file_config->allowed_filesize * 1048576; if (isset($option->allowed_filesize) && $option->allowed_filesize > 0) { $file_config->allowed_attach_size = $option->allowed_filesize; $file_config->allowed_filesize = $option->allowed_filesize; } + if (isset($file_config->pre_conversion_filesize)) + { + $file_config->pre_conversion_filesize = $file_config->pre_conversion_filesize * 1048576; + } // Calculate the appropriate chunk size. $file_config->allowed_chunk_size = min(FileHandler::returnBytes(ini_get('upload_max_filesize')), FileHandler::returnBytes(ini_get('post_max_size')) * 0.95, 64 * 1024 * 1024); diff --git a/modules/editor/skins/ckeditor/file_upload.html b/modules/editor/skins/ckeditor/file_upload.html index c89df2a1c..2a7874d11 100644 --- a/modules/editor/skins/ckeditor/file_upload.html +++ b/modules/editor/skins/ckeditor/file_upload.html @@ -12,6 +12,8 @@ data-editor-status="{json_encode(FileModel::getInstance()->getFileList($editor_sequence), JSON_UNESCAPED_UNICODE)}" data-max-file-size="{$this->user->isAdmin() ? 0 : $file_config->allowed_filesize}" data-max-chunk-size="{$file_config->allowed_chunk_size ?: 0}" + data-pre-conversion-size="{$this->user->isAdmin() ? 0 : intval($file_config->pre_conversion_filesize ?? 0)}" + data-pre-conversion-types="{implode(',', $file_config->pre_conversion_types ?? [])}" data-autoinsert-types="{json_encode($editor_autoinsert_types)}" data-autoinsert-position="{$editor_autoinsert_position ?: 'paragraph'}"> diff --git a/modules/editor/skins/ckeditor/js/file_upload.js b/modules/editor/skins/ckeditor/js/file_upload.js index f21023af9..63f1b5b68 100644 --- a/modules/editor/skins/ckeditor/js/file_upload.js +++ b/modules/editor/skins/ckeditor/js/file_upload.js @@ -10,6 +10,8 @@ $(function() { container.data('instance', container.xeUploader({ maxFileSize: parseInt(data.maxFileSize, 10), maxChunkSize: parseInt(data.maxChunkSize, 10), + preConversionSize: parseInt(data.preConversionSize || 0, 10), + preConversionTypes: data.preConversionTypes ? data.preConversionTypes.split(',') : [], autoinsertTypes: data.autoinsertTypes, autoinsertPosition: data.autoinsertPosition, singleFileUploads: true diff --git a/modules/extravar/models/Value.php b/modules/extravar/models/Value.php index 4e3a21bc8..8780b8f94 100644 --- a/modules/extravar/models/Value.php +++ b/modules/extravar/models/Value.php @@ -323,7 +323,7 @@ class Value } // Check that a file value is actually an uploaded file. - if ($this->type === 'file' && $value) + if ($this->type === 'file' && $value && $value !== $old_value) { if (!isset($value['tmp_name']) || !is_uploaded_file($value['tmp_name'])) { diff --git a/modules/file/file.admin.controller.php b/modules/file/file.admin.controller.php index 1e5ae2e6d..52ccb1611 100644 --- a/modules/file/file.admin.controller.php +++ b/modules/file/file.admin.controller.php @@ -67,9 +67,10 @@ class FileAdminController extends File { // Default settings $config = getModel('module')->getModuleConfig('file') ?: new stdClass; - $config->allowed_filesize = Context::get('allowed_filesize'); - $config->allowed_attach_size = Context::get('allowed_attach_size'); + $config->allowed_filesize = intval(Context::get('allowed_filesize')); + $config->allowed_attach_size = intval(Context::get('allowed_attach_size')); $config->allowed_filetypes = Context::get('allowed_filetypes'); + $config->pre_conversion_filesize = intval(Context::get('pre_conversion_filesize')) ?: null; // Image settings $config->image_autoconv = []; @@ -121,6 +122,10 @@ class FileAdminController extends File $config->magick_command = escape(utf8_trim(Context::get('magick_command'))) ?: ''; } + // Timeouts + $config->ffmpeg_timeout = max(0, intval(Context::get('ffmpeg_timeout'))) ?: null; + $config->magick_timeout = max(0, intval(Context::get('magick_timeout'))) ?: null; + // Check maximum file size (probably not necessary anymore) if (PHP_INT_SIZE < 8) { @@ -147,6 +152,28 @@ class FileAdminController extends File $config->allowed_filetypes = '*.*'; } + // Generate pre-conversion whitelist + $config->pre_conversion_types = []; + foreach ($config->image_autoconv ?? [] as $source_type => $target_type) + { + if (!empty($target_type) && $target_type !== true) + { + $config->pre_conversion_types[] = $source_type; + if ($source_type === 'jpg') + { + $config->pre_conversion_types[] = 'jpeg'; + } + } + elseif ($source_type === 'gif2mp4' && $target_type === true) + { + $config->pre_conversion_types[] = 'gif'; + } + } + if ($config->video_autoconv['any2mp4']) + { + $config->pre_conversion_types = array_merge($config->pre_conversion_types, ['mp4', 'webm', 'ogv', 'avi', 'mkv', 'mov', 'mpg', 'mpe', 'mpeg', 'wmv', 'm4v', 'flv']); + } + // Save and redirect $output = getController('module')->insertModuleConfig('file', $config); $returnUrl = Context::get('success_return_url') ?: getNotEncodedUrl('', 'module', 'admin', 'act', 'dispFileAdminUploadConfig'); @@ -206,9 +233,10 @@ class FileAdminController extends File if(!Context::get('use_default_file_config')) { $config->use_default_file_config = 'N'; - $config->allowed_filesize = Context::get('allowed_filesize'); - $config->allowed_attach_size = Context::get('allowed_attach_size'); + $config->allowed_filesize = intval(Context::get('allowed_filesize')); + $config->allowed_attach_size = intval(Context::get('allowed_attach_size')); $config->allowed_filetypes = Context::get('allowed_filetypes'); + $config->pre_conversion_filesize = intval(Context::get('pre_conversion_filesize')) ?: null; // Check maximum file size if (PHP_INT_SIZE < 8) @@ -274,6 +302,20 @@ class FileAdminController extends File $download_grant = Context::get('download_grant'); $config->download_grant = is_array($download_grant) ? array_values($download_grant) : array($download_grant); + // Create pre-conversion whitelist + $config->pre_conversion_types = []; + foreach ($config->image_autoconv ?? [] as $source_type => $target_type) + { + if ($target_type && $target_type !== true) + { + $config->pre_conversion_types[] = $source_type; + if ($source_type === 'jpg') + { + $config->pre_conversion_types[] = 'jpeg'; + } + } + } + // Update $oModuleController = getController('module'); foreach(explode(',', Context::get('target_module_srl')) as $module_srl) @@ -388,7 +430,7 @@ class FileAdminController extends File // Resize the image using GD or ImageMagick. $config = FileModel::getFileConfig(); $result = FileHandler::createImageFile(FileHandler::getRealPath($file->uploaded_filename), $temp_filename, $width, $height, $format, 'fill', $quality); - if (!$result && !empty($config->magick_command)) + if (!$result && !empty($config->magick_command) && Rhymix\Framework\Storage::isExecutable($config->magick_command)) { $temp_dir = dirname($temp_filename); if (!Rhymix\Framework\Storage::isDirectory($temp_dir)) @@ -396,13 +438,17 @@ class FileAdminController extends File Rhymix\Framework\Storage::createDirectory($temp_dir); } $command = vsprintf('%s %s -resize %dx%d -quality %d %s %s %s', [ - \RX_WINDOWS ? escapeshellarg($config->magick_command) : $config->magick_command, + Rhymix\Framework\Security::sanitize($config->magick_command, 'command'), escapeshellarg(FileHandler::getRealPath($file->uploaded_filename)), $width, $height, $quality, '-auto-orient -strip', '-limit memory 64MB -limit map 128MB -limit disk 1GB', escapeshellarg($temp_filename), ]); + if (!\RX_WINDOWS && isset($config->magick_timeout) && $config->magick_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->magick_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); $result = $return_var === 0 ? true : false; } diff --git a/modules/file/file.admin.view.php b/modules/file/file.admin.view.php index d19fc4e8f..43dba652c 100644 --- a/modules/file/file.admin.view.php +++ b/modules/file/file.admin.view.php @@ -15,14 +15,14 @@ class FileAdminView extends File { // Options to get a list $args = new stdClass(); - $args->page = Context::get('page'); // /< Page - $args->list_count = 30; // /< Number of documents that appear on a single page - $args->page_count = 10; // /< Number of pages that appear in the page navigation - - $args->sort_index = Context::get('sort_index') ?? 'file_srl'; // /< Sorting values - $args->order_type = Context::get('order_type') ?? null; + $args->list_count = intval(Context::get('list_count')) ?: 30; + $args->page_count = 10; + $args->page = max(1, intval(Context::get('page'))); + $args->sort_index = Context::get('sort_index') ?: 'file_srl'; + $args->order_type = strtolower(Context::get('order_type')) === 'asc' ? 'asc' : 'desc'; $args->isvalid = Context::get('isvalid'); $args->module_srl = Context::get('module_srl'); + // Get a list $oFileAdminModel = getAdminModel('file'); $output = $oFileAdminModel->getFileList($args); diff --git a/modules/file/file.controller.php b/modules/file/file.controller.php index b1b83b2b0..292e56709 100644 --- a/modules/file/file.controller.php +++ b/modules/file/file.controller.php @@ -102,6 +102,15 @@ class FileController extends File $module_config = FileModel::getFileConfig($module_srl); $allowed_attach_size = $module_config->allowed_attach_size * 1024 * 1024; $allowed_filesize = $module_config->allowed_filesize * 1024 * 1024; + if (!empty($module_config->pre_conversion_filesize) && !empty($module_config->pre_conversion_types)) + { + $extension = strtolower(array_last(explode('.', $file_info['name']))); + if ($extension && in_array($extension, $module_config->pre_conversion_types)) + { + $allowed_attach_size = ($allowed_attach_size - $allowed_filesize) + ($module_config->pre_conversion_filesize * 1024 * 1024); + $allowed_filesize = $module_config->pre_conversion_filesize * 1024 * 1024; + } + } } if ($total_size > $allowed_filesize) { @@ -1158,10 +1167,14 @@ class FileController extends File public function adjustUploadedImage($file_info, $config) { // Get image information - if (in_array($file_info['extension'], ['avif', 'heic', 'heif']) && !empty($config->magick_command)) + if (in_array($file_info['extension'], ['avif', 'heic', 'heif']) && !empty($config->magick_command) && Rhymix\Framework\Storage::isExecutable($config->magick_command)) { - $command = \RX_WINDOWS ? escapeshellarg($config->magick_command) : $config->magick_command; + $command = Rhymix\Framework\Security::sanitize($config->magick_command, 'command'); $command .= ' identify ' . escapeshellarg($file_info['tmp_name']); + if (!\RX_WINDOWS && isset($config->magick_timeout) && $config->magick_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->magick_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); if ($return_var === 0 && preg_match('/([A-Z]+) ([0-9]+)x([0-9]+)/', substr(array_last($output), strlen($file_info['tmp_name'])), $matches)) { @@ -1326,11 +1339,15 @@ class FileController extends File $adjusted['height'] -= $adjusted['height'] % 2; // Convert using ffmpeg - $command = \RX_WINDOWS ? escapeshellarg($config->ffmpeg_command) : $config->ffmpeg_command; + $command = Rhymix\Framework\Security::sanitize($config->ffmpeg_command, 'command'); $command .= ' -nostdin -i ' . escapeshellarg($file_info['tmp_name']); $command .= ' -movflags +faststart -pix_fmt yuv420p -c:v libx264 -crf 23'; $command .= sprintf(' -vf "scale=%d:%d"', $adjusted['width'], $adjusted['height']); $command .= ' ' . escapeshellarg($output_name); + if (!\RX_WINDOWS && isset($config->ffmpeg_timeout) && $config->ffmpeg_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->ffmpeg_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); $result = $return_var === 0 ? true : false; @@ -1352,7 +1369,7 @@ class FileController extends File // Convert using magick $command = vsprintf('%s %s -resize %dx%d -quality %d %s %s %s', [ - \RX_WINDOWS ? escapeshellarg($config->magick_command) : $config->magick_command, + Rhymix\Framework\Security::sanitize($config->magick_command, 'command'), escapeshellarg($file_info['tmp_name']), $adjusted['width'], $adjusted['height'], @@ -1361,6 +1378,10 @@ class FileController extends File '-limit memory 64MB -limit map 128MB -limit disk 1GB', escapeshellarg($output_name), ]); + if (!\RX_WINDOWS && isset($config->magick_timeout) && $config->magick_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->magick_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); $result = $return_var === 0 ? true : false; } @@ -1370,10 +1391,10 @@ class FileController extends File $result = FileHandler::createImageFile($file_info['tmp_name'], $output_name, $adjusted['width'], $adjusted['height'], $adjusted['type'], 'fill', $adjusted['quality'], $adjusted['rotate']); // If the image cannot be resized using GD, try ImageMagick. - if (!$result && !empty($config->magick_command)) + if (!$result && !empty($config->magick_command) && Rhymix\Framework\Storage::isExecutable($config->magick_command)) { $command = vsprintf('%s %s -resize %dx%d -quality %d %s %s %s', [ - \RX_WINDOWS ? escapeshellarg($config->magick_command) : $config->magick_command, + Rhymix\Framework\Security::sanitize($config->magick_command, 'command'), escapeshellarg($file_info['tmp_name']), $adjusted['width'], $adjusted['height'], @@ -1382,6 +1403,10 @@ class FileController extends File '-limit memory 64MB -limit map 128MB -limit disk 1GB', escapeshellarg($output_name), ]); + if (!\RX_WINDOWS && isset($config->magick_timeout) && $config->magick_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->magick_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); $result = $return_var === 0 ? true : false; } @@ -1414,7 +1439,7 @@ class FileController extends File } // Analyze video file - $command = \RX_WINDOWS ? escapeshellarg($config->ffprobe_command) : $config->ffprobe_command; + $command = Rhymix\Framework\Security::sanitize($config->ffprobe_command, 'command'); $command .= ' -v quiet -print_format json -show_streams'; $command .= ' ' . escapeshellarg($file_info['tmp_name']); @exec($command, $output, $return_var); @@ -1558,7 +1583,7 @@ class FileController extends File $adjusted['height'] -= $adjusted['height'] % 2; // Convert using ffmpeg - $command = \RX_WINDOWS ? escapeshellarg($config->ffmpeg_command) : $config->ffmpeg_command; + $command = Rhymix\Framework\Security::sanitize($config->ffmpeg_command, 'command'); $command .= ' -nostdin -i ' . escapeshellarg($file_info['tmp_name']); if ($adjusted['duration'] !== $file_info['duration']) { @@ -1568,6 +1593,10 @@ class FileController extends File $command .= empty($stream_info['audio']) ? ' -an' : ' -acodec aac'; $command .= sprintf(' -vf "scale=%d:%d"', $adjusted['width'], $adjusted['height']); $command .= ' ' . escapeshellarg($output_name); + if (!\RX_WINDOWS && isset($config->ffmpeg_timeout) && $config->ffmpeg_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->ffmpeg_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); $result = $return_var === 0 ? true : false; @@ -1597,9 +1626,13 @@ class FileController extends File if ($config->video_thumbnail) { $thumbnail_name = $file_info['tmp_name'] . '.thumbnail.jpeg'; - $command = \RX_WINDOWS ? escapeshellarg($config->ffmpeg_command) : $config->ffmpeg_command; + $command = Rhymix\Framework\Security::sanitize($config->ffmpeg_command, 'command'); $command .= sprintf(' -ss 00:00:00.%d -i %s -vframes 1', mt_rand(0, 99), escapeshellarg($file_info['tmp_name'])); $command .= ' -nostdin ' . escapeshellarg($thumbnail_name); + if (!\RX_WINDOWS && isset($config->ffmpeg_timeout) && $config->ffmpeg_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->ffmpeg_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); if ($return_var === 0) { diff --git a/modules/file/lang/en.php b/modules/file/lang/en.php index 64190e13f..9598448d8 100644 --- a/modules/file/lang/en.php +++ b/modules/file/lang/en.php @@ -21,6 +21,7 @@ $lang->allowed_filesize = 'Maximum File Size'; $lang->allowed_filesize_exceeded = 'The file is too large. The maximum allowed filesize is %s.'; $lang->allowed_attach_size = 'Maximum Attachments'; $lang->allowed_filetypes = 'Allowed extentsions'; +$lang->pre_conversion_filesize = 'Pre-conversion Grace Size'; $lang->download_short_url = 'Use short URL'; $lang->inline_download_format = 'Open in current window'; $lang->inline_download_image = 'Image'; @@ -45,6 +46,7 @@ $lang->about_allowed_filesize_global = 'This is the global limit on the size of $lang->about_allowed_attach_size_global = 'This is the global limit on the combined size of all attachments in one document.'; $lang->about_allowed_size_limits = 'The file size will be limited to the value set in php.ini (%sB) in IE9 and below and older Android browsers.'; $lang->about_allowed_filetypes = 'Rhymix no longer uses the old *.* syntax. Simply list the extensions you wish to allow.
Please use a comma (,) to separate items: e.g. doc, zip, pdf'; +$lang->about_pre_conversion_filesize = 'If an image or video might be converted as configured below, it will be allowed up to this size, and checked again after conversion.
If this configuration is empty, the file must be below the allowed size both before and after conversion.'; $lang->about_save_changelog = 'Keep a log of new and deleted files in the database.'; $lang->cmd_delete_checked_file = 'Delete Selected Item(s)'; $lang->cmd_move_to_document = 'Move to Document'; @@ -102,15 +104,15 @@ $lang->max_image_size_same_format_to_jpg = 'Convert to JPG'; $lang->max_image_size_same_format_to_webp = 'Convert to WebP'; $lang->max_image_size_admin = 'Also apply to administrator'; $lang->image_quality_adjustment = 'Image Quality'; -$lang->about_image_quality_adjustment = 'adjust the quality of images that will is converted by other settings.
If set to more than 75% (Standard), the file size may be larger than the original.'; +$lang->about_image_quality_adjustment = 'Adjust the quality of images that will be converted by other settings.
If set to more than 75% (Standard), the file size may be larger than the original.'; $lang->image_autorotate = 'Fix Image Rotation'; -$lang->about_image_autorotate = 'correct images that are rotated by mobile devices.'; +$lang->about_image_autorotate = 'Correct images that are rotated by mobile devices.'; $lang->image_remove_exif_data = 'Remove EXIF'; -$lang->about_image_remove_exif_data = 'remove EXIF data including camera, GPS information, and more in image file for privacy.
Even if this option is not used, EXIF ​​data may be removed when the image is converted by other settings.'; +$lang->about_image_remove_exif_data = 'Remove EXIF data including camera, GPS information, and more in image file for privacy.
Even if this option is not used, EXIF ​​data may be removed when the image is converted by other settings.'; $lang->image_always_reencode = 'Always Reencode'; $lang->about_image_always_reencode = 'Reencode images to a constant quality even if they do not meet one of the conditions above. This may help save disk space and traffic.'; $lang->image_autoconv_gif2mp4 = 'Convert GIF to MP4'; -$lang->about_image_autoconv_gif2mp4 = 'convert animated GIF images into MP4 videos to save storage and bandwidth.
This requires ffmpeg settings below. Videos may not play properly in older browsers.'; +$lang->about_image_autoconv_gif2mp4 = 'Convert animated GIF images into MP4 videos to save storage and bandwidth.
This requires ffmpeg settings below. Videos may not play properly in older browsers.'; $lang->max_video_size = 'Limit Video Size'; $lang->about_max_video_size = 'Limit the dimensions of uploaded videos. Note that this is only indirectly related to file size.'; $lang->max_video_duration = 'Limit Video Duration'; @@ -120,15 +122,19 @@ $lang->about_video_autoconv_any2mp4 = 'Convert all other types of videos to MP4 $lang->video_always_reencode = 'Always Reencode'; $lang->about_video_always_reencode = 'Reencode videos to a constant quality even if they do not meet one of the conditions above. This may help save disk space and traffic.'; $lang->video_thumbnail = 'Video Thumbnail'; -$lang->about_video_thumbnail = 'extract a thumbnail image from uploaded video.'; +$lang->about_video_thumbnail = 'Extract a thumbnail image from uploaded video.'; $lang->video_mp4_gif_time = 'Play Like GIF'; -$lang->about_video_mp4_gif_time = 'treat silent MP4 videos with duration less than the set time as GIF images, and play with auto and loop.'; +$lang->about_video_mp4_gif_time = 'Treat silent MP4 videos with duration less than the set time as GIF images, and play with auto and loop.'; $lang->external_program_paths = 'Paths to External Programs'; $lang->ffmpeg_path = 'Absolute Path to ffmpeg'; $lang->ffprobe_path = 'Absolute Path to ffprobe'; +$lang->ffmpeg_timeout = 'ffmpeg Timeout'; $lang->magick_path = 'Absolute Path to magick'; +$lang->magick_timeout = 'magick Timeout'; $lang->about_ffmpeg_path = 'Rhymix uses ffmpeg to convert video files.'; +$lang->about_ffmpeg_timeout = 'If the video conversion task is not completed within a certain time, it will be terminated.
Proper timeout settings can help manage server load.
However, if set longer than the PHP execution time limit (%d seconds), the conversion result will not be saved.'; $lang->about_magick_path = 'Rhymix uses magick to convert newer image formats such as AVIF and HEIC.
Note that the \'convert\' command from previous versions of ImageMagick doesn\'t support these formats.
The latest version can be downloaded from their official site.'; +$lang->about_magick_timeout = 'If the image conversion task is not completed within a certain time, it will be terminated.
Proper timeout settings can help manage server load.
However, if set longer than the PHP execution time limit (%d seconds), the conversion result will not be saved.'; $lang->msg_cannot_use_exec = 'The exec() function is disabled on this server.'; $lang->msg_cannot_use_ffmpeg = 'In order to use this feature, PHP must be able to execute \'ffmpeg\' and \'ffprobe\' commands.'; $lang->msg_cannot_use_exif = 'In order to use this feature, PHP must be installed with the \'exif\' extension.'; diff --git a/modules/file/lang/ko.php b/modules/file/lang/ko.php index 3014e884c..58392862f 100644 --- a/modules/file/lang/ko.php +++ b/modules/file/lang/ko.php @@ -21,6 +21,7 @@ $lang->allowed_filesize_exceeded = '파일이 너무 큽니다. 용량 제한은 $lang->allowed_attach_size = '문서 첨부 제한'; $lang->allowed_filetypes = '허용 확장자'; $lang->allow_multimedia_direct_download = '멀티미디어 파일 직접 접근 허용'; +$lang->pre_conversion_filesize = '변환 전 유예 용량'; $lang->download_short_url = '다운로드시 짧은주소 사용'; $lang->inline_download_format = '다운로드시 현재 창 사용'; $lang->inline_download_image = '이미지'; @@ -45,6 +46,7 @@ $lang->about_allowed_filesize_global = '관리자를 포함하여 사이트 전 $lang->about_allowed_attach_size_global = '관리자를 포함하여 사이트 전체에 적용되는 문서당 총 첨부 용량 제한입니다.'; $lang->about_allowed_size_limits = 'IE9 이하, 구버전 안드로이드 등에서는 php.ini에서 지정한 %sB로 제한됩니다.'; $lang->about_allowed_filetypes = '업로드를 허용할 확장자 목록입니다. 구 버전의 *.* 문법은 사용하지 않습니다.
여러 개 입력시 쉼표(,)을 이용해서 구분해 주세요. 예) doc, zip, pdf'; +$lang->about_pre_conversion_filesize = '변환 대상인 이미지 또는 동영상은 설정한 용량만큼 우선 업로드를 허용하고, 변환 후 용량을 기준으로 다시 확인합니다.
설정을 비워 둘 경우, 변환 전후 용량을 동일하게 제한합니다.'; $lang->about_save_changelog = '파일 저장 및 삭제 내역을 DB에 기록합니다.'; $lang->cmd_delete_checked_file = '선택항목 삭제'; $lang->cmd_move_to_document = '문서로 이동'; @@ -126,9 +128,13 @@ $lang->about_video_mp4_gif_time = '설정된 시간 이하의 길이를 가진 $lang->external_program_paths = '외부 프로그램 경로'; $lang->ffmpeg_path = 'ffmpeg 절대경로'; $lang->ffprobe_path = 'ffprobe 절대경로'; +$lang->ffmpeg_timeout = 'ffmpeg 타임아웃'; $lang->magick_path = 'magick 절대경로'; +$lang->magick_timeout = 'magick 타임아웃'; $lang->about_ffmpeg_path = '동영상 변환에 사용합니다.'; +$lang->about_ffmpeg_timeout = '동영상 변환 작업이 일정 시간 안에 완료되지 않으면 강제로 종료합니다.
적절한 타임아웃 설정은 서버 부하 관리에 도움이 됩니다.
단, PHP 실행 제한 시간(%d초)보다 길게 설정할 경우 변환 결과가 저장되지 않습니다.'; $lang->about_magick_path = 'AVIF, HEIC 등 일부 이미지 변환에 사용합니다.
구 버전 ImageMagick의 convert 명령은 이러한 포맷을 지원하지 않습니다.
새 버전은 공식 사이트에서 다운받을 수 있습니다.'; +$lang->about_magick_timeout = '이미지 변환 작업이 일정 시간 안에 완료되지 않으면 강제로 종료합니다.
적절한 타임아웃 설정은 서버 부하 관리에 도움이 됩니다.
단, PHP 실행 제한 시간(%d초)보다 길게 설정할 경우 변환 결과가 저장되지 않습니다.'; $lang->msg_cannot_use_exec = '이 서버에서 exec() 함수를 사용할 수 없습니다.'; $lang->msg_cannot_use_ffmpeg = '이 기능을 사용하려면 PHP에서 ffmpeg 및 ffprobe 명령을 실행할 수 있어야 합니다.'; $lang->msg_cannot_use_exif = '이 기능을 사용하려면 PHP exif 확장모듈이 필요합니다.'; diff --git a/modules/file/tpl/file_module_config.html b/modules/file/tpl/file_module_config.html index 104580fb8..86c7b23cd 100644 --- a/modules/file/tpl/file_module_config.html +++ b/modules/file/tpl/file_module_config.html @@ -34,6 +34,13 @@

{sprintf($lang->about_allowed_attach_size, getUrl('', 'module', 'admin', 'act', 'dispFileAdminUploadConfig'))}
{sprintf($lang->about_allowed_size_limits, ini_get('upload_max_filesize'))}

+
+ +
+ MB +

{$lang->about_pre_conversion_filesize}

+
+
diff --git a/modules/file/tpl/upload_config.html b/modules/file/tpl/upload_config.html index d87571d0f..932b6eb41 100644 --- a/modules/file/tpl/upload_config.html +++ b/modules/file/tpl/upload_config.html @@ -23,6 +23,13 @@

{$lang->about_allowed_attach_size_global}
{sprintf($lang->about_allowed_size_limits, ini_get('upload_max_filesize'))}

+
+ +
+ MB +

{$lang->about_pre_conversion_filesize}

+
+
@@ -276,6 +283,14 @@

{$lang->msg_cannot_use_exec}

+
+ +
+ {$lang->unit_sec} +

{sprintf($lang->about_ffmpeg_timeout, ini_get('max_execution_time'))}

+

{$lang->msg_cannot_use_exec}

+
+
@@ -284,6 +299,14 @@

{$lang->msg_cannot_use_exec}

+
+ +
+ {$lang->unit_sec} +

{sprintf($lang->about_magick_timeout, ini_get('max_execution_time'))}

+

{$lang->msg_cannot_use_exec}

+
+
diff --git a/modules/layout/layout.admin.controller.php b/modules/layout/layout.admin.controller.php index 6587cedb6..78d7bed07 100644 --- a/modules/layout/layout.admin.controller.php +++ b/modules/layout/layout.admin.controller.php @@ -311,14 +311,22 @@ class LayoutAdminController extends Layout $layout_srl = Context::get('layout_srl'); $code = Context::get('code'); - $code_css = Context::get('code_css'); - $is_post = ($_SERVER['REQUEST_METHOD'] == 'POST'); - - if(!$layout_srl || !$code || !$is_post) + $code_css = Context::get('code_css'); + if (!$layout_srl || !$code || !\RX_POST) { throw new Rhymix\Framework\Exceptions\InvalidRequest; } + $layout_info = LayoutModel::getLayout($layout_srl); + if (!$layout_info) + { + throw new Rhymix\Framework\Exceptions\TargetNotFound; + } + if (!$layout_info->is_edited) + { + return new BaseObject(-1, 'layout.layout_editing_deprecated_p1'); + } + $oLayoutModel = getModel('layout'); $layout_file = $oLayoutModel->getUserLayoutHtml($layout_srl); FileHandler::writeFile($layout_file, $code); diff --git a/modules/member/conf/module.xml b/modules/member/conf/module.xml index 5e179fc0a..a4f5e9ca1 100644 --- a/modules/member/conf/module.xml +++ b/modules/member/conf/module.xml @@ -40,10 +40,10 @@ - - + + - + diff --git a/modules/member/member.admin.controller.php b/modules/member/member.admin.controller.php index e75463d74..defa86db9 100644 --- a/modules/member/member.admin.controller.php +++ b/modules/member/member.admin.controller.php @@ -944,6 +944,8 @@ class MemberAdminController extends Member if(!$output->toBool()) return $output; // memberConfig update + $config = MemberModel::getMemberConfig(); + $signupItem = new stdClass(); $signupItem->name = $args->column_name; $signupItem->title = $args->column_title; @@ -954,9 +956,6 @@ class MemberAdminController extends Member $signupItem->description = $args->description; $signupItem->isPublic = 'Y'; - $oMemberModel = getModel('member'); - $config = $oMemberModel->getMemberConfig(); - if($isInsert) { $config->signupForm[] = $signupItem; @@ -967,6 +966,7 @@ class MemberAdminController extends Member { if($val->member_join_form_srl == $signupItem->member_join_form_srl) { + $signupItem->isPublic = $val->isPublic ?? 'Y'; $config->signupForm[$key] = $signupItem; } } diff --git a/modules/member/member.admin.model.php b/modules/member/member.admin.model.php index 3f4a6f192..423e0630b 100644 --- a/modules/member/member.admin.model.php +++ b/modules/member/member.admin.model.php @@ -44,7 +44,7 @@ class MemberAdminModel extends Member $args = new stdClass(); $args->is_admin = Context::get('is_admin') === 'Y' ? 'Y' : null; $args->status = Context::get('is_denied') === 'Y' ? 'DENIED' : null; - $args->selected_group_srl = Context::get('selected_group_srl'); + $args->selected_group_srl = intval(Context::get('selected_group_srl')) ?: null; $filter = Context::get('filter_type'); switch($filter) @@ -128,21 +128,11 @@ class MemberAdminModel extends Member } // Change the query id if selected_group_srl exists (for table join) - $sort_order = Context::get('sort_order'); + $sort_order = Context::get('sort_order') === 'desc' ? 'desc' : 'asc'; $sort_index = Context::get('sort_index'); if(!$sort_index || !in_array($sort_index, ['user_id', 'email_address', 'phone_number', 'user_name', 'nick_name', 'regdate', 'last_login'])) { - $sort_index = "list_order"; - } - - if(!$sort_order) - { - $sort_order = 'asc'; - } - - if($sort_order != 'asc') - { - $sort_order = 'desc'; + $sort_index = 'list_order'; } if($args->selected_group_srl) @@ -158,13 +148,13 @@ class MemberAdminModel extends Member $args->sort_order = $sort_order; Context::set('sort_order', $sort_order); - // Other variables - $args->page = Context::get('page'); - $args->list_count = 40; - $args->page_count = 10; - $output = executeQuery($query_id, $args); - return $output; + // Other variables + $args->list_count = intval(Context::get('list_count')) ?: 30; + $args->page_count = 10; + $args->page = max(1, intval(Context::get('page'))); + + return executeQueryArray($query_id, $args); } /** diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index 3d3ddf6de..cf2eb2abc 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -1872,7 +1872,7 @@ class MemberController extends Member $tpl_path = sprintf('%sskins/%s', $this->module_path, $member_config->skin); if(!is_dir($tpl_path)) $tpl_path = sprintf('%sskins/%s', $this->module_path, 'default'); - $find_url = getFullUrl ('', 'module', 'member', 'act', 'procMemberAuthAccount', 'member_srl', $member_info->member_srl, 'auth_key', $args->auth_key); + $find_url = self::generateSafeAuthUrl('procMemberAuthAccount', $member_info->member_srl, $args->auth_key); Context::set('find_url', $find_url); $oTemplate = new Rhymix\Framework\Template($tpl_path, 'find_member_account_mail'); @@ -2104,7 +2104,7 @@ class MemberController extends Member $tpl_path = sprintf('%sskins/%s', $this->module_path, $member_config->skin); if(!is_dir($tpl_path)) $tpl_path = sprintf('%sskins/%s', $this->module_path, 'default'); - $auth_url = getFullUrl('','module','member','act','procMemberAuthAccount','member_srl',$member_info->member_srl, 'auth_key',$auth_info->auth_key); + $auth_url = self::generateSafeAuthUrl('procMemberAuthAccount', $member_info->member_srl, $auth_info->auth_key); Context::set('auth_url', $auth_url); $oTemplate = new Rhymix\Framework\Template($tpl_path, 'confirm_member_account_mail'); @@ -2161,7 +2161,7 @@ class MemberController extends Member $tpl_path = sprintf('%sskins/%s', $this->module_path, $member_config->skin); if(!is_dir($tpl_path)) $tpl_path = sprintf('%sskins/%s', $this->module_path, 'default'); - $auth_url = getFullUrl('','module','member','act','procMemberAuthAccount','member_srl',$member_info->member_srl, 'auth_key',$auth_args->auth_key); + $auth_url = self::generateSafeAuthUrl('procMemberAuthAccount', $member_info->member_srl, $auth_args->auth_key); Context::set('auth_url', $auth_url); $oTemplate = new Rhymix\Framework\Template($tpl_path, 'confirm_member_account_mail'); @@ -3721,7 +3721,7 @@ class MemberController extends Member Context::set('memberInfo', $memberInfo); Context::set('newEmail', $newEmail); - $auth_url = getFullUrl('','module','member','act','procMemberAuthEmailAddress','member_srl',$member_info->member_srl, 'auth_key',$auth_args->auth_key); + $auth_url = self::generateSafeAuthUrl('procMemberAuthEmailAddress', $member_info->member_srl, $auth_args->auth_key); Context::set('auth_url', $auth_url); $oTemplate = new Rhymix\Framework\Template($tpl_path, 'confirm_member_new_email'); @@ -4054,6 +4054,43 @@ class MemberController extends Member return new BaseObject(0); } + /** + * Generate a URL pointing to the main page of a properly configured domain. + * + * @return string + */ + public static function generateSafeLink(string $target = '_blank'): string + { + $domain_info = ModuleModel::getSiteInfoByDomain($_SERVER['HTTP_HOST']) ?: ModuleModel::getDefaultDomainInfo(); + $base_url = Context::getRequestUri(0, $domain_info->domain); + $title = Context::replaceUserLang($domain_info->settings->title ?? ''); + if ($title === '') + { + $title = $base_url; + } + return sprintf('%s', escape($base_url, false), escape($target, false), escape($title, false)); + } + + /** + * Generate a URL for account auth. + * + * @param string $act + * @param int $member_srl + * @param string $auth_key + * @return string + */ + public static function generateSafeAuthUrl(string $act, int $member_srl, string $auth_key): string + { + $domain_info = ModuleModel::getSiteInfoByDomain($_SERVER['HTTP_HOST']) ?: ModuleModel::getDefaultDomainInfo(); + $base_url = Context::getRequestUri(0, $domain_info->domain); + return $base_url . substr(getUrl([ + 'module' => 'member', + 'act' => $act, + 'member_srl' => $member_srl, + 'auth_key' => $auth_key, + ]), strlen(\RX_BASEURL)); + } + /** * Denied user login and write description * diff --git a/modules/member/skins/default/confirm_member_account_mail.html b/modules/member/skins/default/confirm_member_account_mail.html index 33e1ecd74..710b8239f 100644 --- a/modules/member/skins/default/confirm_member_account_mail.html +++ b/modules/member/skins/default/confirm_member_account_mail.html @@ -1,7 +1,7 @@ {$lang->msg_confirm_account_info}

    -
  • Site : {getUrl()}
  • +
  • {$lang->site} : {MemberController::generateSafeLink()}
  • {$name} : {$value}

diff --git a/modules/member/skins/default/confirm_member_new_email.html b/modules/member/skins/default/confirm_member_new_email.html index 9a2f37fff..a4de66cc8 100644 --- a/modules/member/skins/default/confirm_member_new_email.html +++ b/modules/member/skins/default/confirm_member_new_email.html @@ -1,7 +1,7 @@ {$lang->msg_confirm_account_info}

    -
  • Site : {getUrl()}
  • +
  • {$lang->site} : {MemberController::generateSafeLink()}
  • {$name} : {$value}

diff --git a/modules/member/skins/default/find_member_account_mail.html b/modules/member/skins/default/find_member_account_mail.html index 151368fa4..029f507ed 100644 --- a/modules/member/skins/default/find_member_account_mail.html +++ b/modules/member/skins/default/find_member_account_mail.html @@ -1,7 +1,7 @@ {$lang->msg_find_account_info}

    -
  • {$lang->site} : {getUrl()}
  • +
  • {$lang->site} : {MemberController::generateSafeLink()}
  • {$lang->user_id} : {$memberInfo[$lang->user_id]}
  • diff --git a/modules/menu/conf/module.xml b/modules/menu/conf/module.xml index 5bf186446..5fc2fc764 100644 --- a/modules/menu/conf/module.xml +++ b/modules/menu/conf/module.xml @@ -24,8 +24,6 @@ - - diff --git a/modules/menu/menu.admin.controller.php b/modules/menu/menu.admin.controller.php index 0c999346e..dce35bb3f 100644 --- a/modules/menu/menu.admin.controller.php +++ b/modules/menu/menu.admin.controller.php @@ -1560,61 +1560,6 @@ class MenuAdminController extends Menu $this->add('xml_file',$xml_file); } - /** - * Register a menu image button - * @return void - */ - function procMenuAdminUploadButton() - { - $menu_srl = Context::get('menu_srl'); - $menu_item_srl = Context::get('menu_item_srl'); - $target = Context::get('target'); - $target_file = Context::get($target); - // Error occurs when the target is neither a uploaded file nor a valid file - if(!$menu_srl || !$menu_item_srl) - { - Context::set('error_messge', lang('msg_invalid_request')); - - } - else if(!$target_file || !is_uploaded_file($target_file['tmp_name']) || !preg_match('/\.(jpe?g|gif|png|svg|webp)$/i',$target_file['name'])) - { - Context::set('error_messge', lang('msg_invalid_request')); - } - - // Move the file to a specific director if the uploaded file meets requirement - else - { - $tmp_arr = explode('.',$target_file['name']); - $ext = $tmp_arr[count($tmp_arr)-1]; - - $path = sprintf('./files/attach/menu_button/%d/', $menu_srl); - $filename = sprintf('%s%d.%s.%s', $path, $menu_item_srl, $target, $ext); - - if(!is_dir($path)) FileHandler::makeDir($path); - - move_uploaded_file($target_file['tmp_name'], $filename); - Context::set('filename', $filename); - } - - $this->setTemplatePath($this->module_path.'tpl'); - $this->setTemplateFile('menu_file_uploaded'); - } - - /** - * Remove the menu image button - * @return void - */ - function procMenuAdminDeleteButton() - { - $menu_srl = Context::get('menu_srl'); - $menu_item_srl = Context::get('menu_item_srl'); - $target = Context::get('target'); - $filename = Context::get('filename'); - FileHandler::removeFile($filename); - - $this->add('target', $target); - } - /** * Get all act list for admin menu * @return void diff --git a/tests/unit/framework/SecurityTest.php b/tests/unit/framework/SecurityTest.php index 9fb35a2b0..820541703 100644 --- a/tests/unit/framework/SecurityTest.php +++ b/tests/unit/framework/SecurityTest.php @@ -25,6 +25,17 @@ class SecurityTest extends \Codeception\Test\Unit $source = ''; $target = '' . "\n\n \n\n"; $this->assertEquals($target, Rhymix\Framework\Security::sanitize($source, 'svg')); + + // Command + if (!\RX_WINDOWS) + { + $source = '/usr/bin/ffmpeg'; + $target = '/usr/bin/ffmpeg'; + $this->assertEquals($target, Rhymix\Framework\Security::sanitize($source, 'command')); + $source = '/usr/bin/path with space/ffmpeg'; + $target = '\'/usr/bin/path with space/ffmpeg\''; + $this->assertEquals($target, Rhymix\Framework\Security::sanitize($source, 'command')); + } } public function testEncryption()