From cdc713301f630b1b9f11b3b13bc00a7ba07ec9bc Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 3 Mar 2026 18:02:42 +0900 Subject: [PATCH 01/30] Prevent saving layout HTML/CSS if it was not previously edited --- modules/layout/layout.admin.controller.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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); From bf0899973ab66a39071bd83086e8152ddfd39419 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 3 Mar 2026 18:07:56 +0900 Subject: [PATCH 02/30] Fix missing validation of xe_run_method --- modules/addon/addon.admin.controller.php | 4 ++++ modules/addon/addon.controller.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) 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\');'; From a03c33381f2c5af5864098af66e994af9c59aad0 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 4 Mar 2026 18:51:15 +0900 Subject: [PATCH 03/30] Fix error when updating a document with a required file #2685 --- modules/extravar/models/Value.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'])) { From 4fe87edd1d4ca348395b0ae77f6947cd3ee2728f Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Mon, 9 Mar 2026 20:29:18 +0900 Subject: [PATCH 04/30] Don't call API class if HTTP status code is 4xx or 5xx --- classes/module/ModuleObject.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)) From 8b8dc99431d07100f94f44e668fc6b866b679b74 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Mon, 9 Mar 2026 20:29:59 +0900 Subject: [PATCH 05/30] Replace $oDocument with empty DocumentItem if access is not allowed --- modules/board/board.api.php | 35 +++++++++++++++++++++++++---------- modules/board/board.view.php | 2 ++ 2 files changed, 27 insertions(+), 10 deletions(-) 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 a24350ef5..f5cc7e84a 100644 --- a/modules/board/board.view.php +++ b/modules/board/board.view.php @@ -318,6 +318,7 @@ class BoardView extends Board { if (abs($oDocument->get('member_srl')) != $this->user->member_srl) { + $oDocument = DocumentModel::getDocument(0); 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::getDocument(0); Context::set('document_srl', null, true); $this->dispBoardMessage('msg_not_founded', 404); } From 3ca12cca6fc2163588305b61a937d460826ff444 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Mon, 9 Mar 2026 20:34:34 +0900 Subject: [PATCH 06/30] Always set correct module_srl, even on empty documents --- modules/board/board.view.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/board/board.view.php b/modules/board/board.view.php index f5cc7e84a..198cee836 100644 --- a/modules/board/board.view.php +++ b/modules/board/board.view.php @@ -319,6 +319,7 @@ class BoardView extends Board if (abs($oDocument->get('member_srl')) != $this->user->member_srl) { $oDocument = DocumentModel::getDocument(0); + $oDocument->add('module_srl', $this->module_srl); Context::set('document_srl', null, true); $this->dispBoardMessage('msg_not_founded', 404); } @@ -328,6 +329,7 @@ class BoardView extends Board if($oDocument->getStatus() == 'TEMP') { $oDocument = DocumentModel::getDocument(0); + $oDocument->add('module_srl', $this->module_srl); Context::set('document_srl', null, true); $this->dispBoardMessage('msg_not_founded', 404); } @@ -357,6 +359,7 @@ class BoardView extends Board if(!$this->grant->view && !$oDocument->isGranted()) { $oDocument = DocumentModel::getDocument(0); + $oDocument->add('module_srl', $this->module_srl); Context::set('document_srl', null, true); $this->dispBoardMessage($this->user->isMember() ? 'msg_not_permitted' : 'msg_not_logged'); } From 8901cb6e3617c6c92072c35925778ab3552e3e34 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Mon, 9 Mar 2026 21:28:51 +0900 Subject: [PATCH 07/30] Fix duplicate message content when document is moved #2686 --- modules/document/document.controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/document/document.controller.php b/modules/document/document.controller.php index 72608b49f..5c3fca3c4 100644 --- a/modules/document/document.controller.php +++ b/modules/document/document.controller.php @@ -3574,7 +3574,7 @@ class DocumentController extends Document { // Set message $title = sprintf(lang('default_message_format'), $actions[$obj->type]); - $content = <<{$title}

{$obj->manager_message}


@@ -3604,7 +3604,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); } } From 44cf008ac788ed2ea4f2360bdc959b3f28c5cd29 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 10 Mar 2026 13:32:42 +0900 Subject: [PATCH 08/30] Allow setting list_count in various admin list pages #2549 --- modules/comment/comment.admin.view.php | 24 +++++++------- modules/document/document.admin.view.php | 40 +++++++++++------------- modules/file/file.admin.view.php | 12 +++---- modules/member/member.admin.model.php | 28 ++++++----------- 4 files changed, 45 insertions(+), 59 deletions(-) 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/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/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); } /** From 94008fbe9bda41a378875e2edbc15bbfca254515 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 10 Mar 2026 19:45:38 +0900 Subject: [PATCH 09/30] Allow larger images/videos to be uploaded if they are going to be converted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 변환 대상인 이미지나 동영상 파일은 용량 제한을 더 느슨하게 설정할 수 있도록 함 - 변환 후에 다시 용량을 체크하여 각 게시판의 업로드 정책 적용 - https://rhymix.org/qna/1926104 --- .../js/plugins/jquery.fileupload/js/main.js | 9 +++- modules/editor/editor.model.php | 8 +++- .../editor/skins/ckeditor/file_upload.html | 2 + .../editor/skins/ckeditor/js/file_upload.js | 2 + modules/file/file.admin.controller.php | 46 +++++++++++++++++-- modules/file/file.controller.php | 9 ++++ modules/file/lang/en.php | 2 + modules/file/lang/ko.php | 2 + modules/file/tpl/file_module_config.html | 7 +++ modules/file/tpl/upload_config.html | 7 +++ 10 files changed, 87 insertions(+), 7 deletions(-) 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/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..98687ef16 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="{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/file/file.admin.controller.php b/modules/file/file.admin.controller.php index 1e5ae2e6d..3cb11c989 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 = []; @@ -147,6 +148,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 +229,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 +298,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) diff --git a/modules/file/file.controller.php b/modules/file/file.controller.php index 3f24cae0c..81d1419db 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) { diff --git a/modules/file/lang/en.php b/modules/file/lang/en.php index 64190e13f..152af21e4 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'; diff --git a/modules/file/lang/ko.php b/modules/file/lang/ko.php index 3014e884c..d8d27ee9a 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 = '문서로 이동'; 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..bbdf9248e 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}

+
+
From 00c9a5316ca6ee4d5236d2aec3a8e4fe8b6d174e Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 19 Mar 2026 17:33:08 +0900 Subject: [PATCH 10/30] Fix RVE-2026-5 unconfigured domain in auth email --- modules/member/member.controller.php | 45 +++++++++++++++++-- .../default/confirm_member_account_mail.html | 2 +- .../default/confirm_member_new_email.html | 2 +- .../default/find_member_account_mail.html | 2 +- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index 0bdfceb13..a9b2735dc 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -1876,7 +1876,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'); @@ -2108,7 +2108,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'); @@ -2165,7 +2165,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'); @@ -3725,7 +3725,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'); @@ -4058,6 +4058,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]}
  • From c906eae5d32c2354b39bbdad241b86bd4549e7e9 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 19 Mar 2026 17:35:42 +0900 Subject: [PATCH 11/30] Disallow GET requests to procMemberFindAccount, and add route to procMemberAuthEmailAddress --- modules/member/conf/module.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/member/conf/module.xml b/modules/member/conf/module.xml index a6aabadb5..df37dde24 100644 --- a/modules/member/conf/module.xml +++ b/modules/member/conf/module.xml @@ -40,10 +40,10 @@ - - + + - + From 4ee0699dea8abadd3df564e537f1c22c6b3b6108 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 19 Mar 2026 17:50:44 +0900 Subject: [PATCH 12/30] Fix RVE-2026-6 possible command injection via magick --- modules/file/file.admin.controller.php | 4 ++-- modules/file/file.controller.php | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/file/file.admin.controller.php b/modules/file/file.admin.controller.php index 3cb11c989..9229e2ff0 100644 --- a/modules/file/file.admin.controller.php +++ b/modules/file/file.admin.controller.php @@ -426,7 +426,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)) @@ -434,7 +434,7 @@ 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, + (preg_match('![^a-z0-9/._-]!', $config->magick_command) || \RX_WINDOWS) ? escapeshellarg($config->magick_command) : $config->magick_command, escapeshellarg(FileHandler::getRealPath($file->uploaded_filename)), $width, $height, $quality, '-auto-orient -strip', diff --git a/modules/file/file.controller.php b/modules/file/file.controller.php index 81d1419db..d414635c1 100644 --- a/modules/file/file.controller.php +++ b/modules/file/file.controller.php @@ -1162,9 +1162,9 @@ 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 = (preg_match('![^a-z0-9/._-]!', $config->magick_command) || \RX_WINDOWS) ? escapeshellarg($config->magick_command) : $config->magick_command; $command .= ' identify ' . escapeshellarg($file_info['tmp_name']); @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)) @@ -1356,7 +1356,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, + (preg_match('![^a-z0-9/._-]!', $config->magick_command) || \RX_WINDOWS) ? escapeshellarg($config->magick_command) : $config->magick_command, escapeshellarg($file_info['tmp_name']), $adjusted['width'], $adjusted['height'], @@ -1374,10 +1374,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, + (preg_match('![^a-z0-9/._-]!', $config->magick_command) || \RX_WINDOWS) ? escapeshellarg($config->magick_command) : $config->magick_command, escapeshellarg($file_info['tmp_name']), $adjusted['width'], $adjusted['height'], From e61ccf84b85d6d8132a5ebf4775e514194d6a804 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Sun, 22 Mar 2026 14:59:01 +0900 Subject: [PATCH 13/30] Always cast module_srl to int when parsing include_modules --- modules/board/board.view.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/board/board.view.php b/modules/board/board.view.php index a24350ef5..dff9a3c42 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; From 63d2582c8e55ba12a989d463462234e76edc0e77 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Sun, 22 Mar 2026 15:09:45 +0900 Subject: [PATCH 14/30] Fix invalid query when search contains zero conditions #2687 --- .../parsers/dbquery/VariableBase.php | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/common/framework/parsers/dbquery/VariableBase.php b/common/framework/parsers/dbquery/VariableBase.php index 419c74e95..8db20c13b 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 : '?'); @@ -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]; + } } } From 7a3d759e62eff0a994ea12ee3343d6f4fcbcfc96 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Sun, 22 Mar 2026 15:17:50 +0900 Subject: [PATCH 15/30] Fix undefined property when XML query has without #2688 --- common/framework/DB.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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); From 057507d3d7a17af5e152d598aba2e712ddb710ee Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Sun, 29 Mar 2026 16:23:38 +0900 Subject: [PATCH 16/30] Fix member extra field being reset to public after editing --- modules/member/member.admin.controller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/member/member.admin.controller.php b/modules/member/member.admin.controller.php index 1bc72fe41..17859439f 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; } } From 5c858806b2e7cde9152c08ee97e620204846ee1c Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Sun, 29 Mar 2026 16:26:10 +0900 Subject: [PATCH 17/30] Treat comma as a regular character when parsing search string #2687 --- common/framework/parsers/dbquery/VariableBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/framework/parsers/dbquery/VariableBase.php b/common/framework/parsers/dbquery/VariableBase.php index 8db20c13b..6ee29a2cb 100644 --- a/common/framework/parsers/dbquery/VariableBase.php +++ b/common/framework/parsers/dbquery/VariableBase.php @@ -503,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'); From 7c30af23c5ba1180d6ceb09ead7c7c18ddf386a6 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Sun, 29 Mar 2026 16:37:23 +0900 Subject: [PATCH 18/30] Allow admin to delete comment placeholders from list #967 --- modules/comment/comment.admin.controller.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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) From ba49fe7b7013123ece49f4569ffaac86cf49fa6d Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 31 Mar 2026 19:30:09 +0900 Subject: [PATCH 19/30] Fix warnings when migrating XE config to Rhymix format --- common/framework/parsers/ConfigParser.php | 34 +++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) 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); From 81b32378cadafd414fabea9e6cff23f07306b6ca Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 31 Mar 2026 19:53:08 +0900 Subject: [PATCH 20/30] Support timeouts for ffmpeg and magick commands https://rhymix.org/qna/1935749 --- modules/file/file.admin.controller.php | 8 ++++++++ modules/file/file.controller.php | 24 ++++++++++++++++++++++++ modules/file/lang/en.php | 16 ++++++++++------ modules/file/lang/ko.php | 4 ++++ modules/file/tpl/upload_config.html | 16 ++++++++++++++++ 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/modules/file/file.admin.controller.php b/modules/file/file.admin.controller.php index 3cb11c989..ef3f8d523 100644 --- a/modules/file/file.admin.controller.php +++ b/modules/file/file.admin.controller.php @@ -122,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) { @@ -441,6 +445,10 @@ class FileAdminController extends File '-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.controller.php b/modules/file/file.controller.php index 81d1419db..6c73d5cb9 100644 --- a/modules/file/file.controller.php +++ b/modules/file/file.controller.php @@ -1166,6 +1166,10 @@ class FileController extends File { $command = \RX_WINDOWS ? escapeshellarg($config->magick_command) : $config->magick_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)) { @@ -1335,6 +1339,10 @@ class FileController extends File $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; @@ -1365,6 +1373,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; } @@ -1386,6 +1398,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; } @@ -1572,6 +1588,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; @@ -1604,6 +1624,10 @@ class FileController extends File $command = \RX_WINDOWS ? escapeshellarg($config->ffmpeg_command) : $config->ffmpeg_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 152af21e4..9598448d8 100644 --- a/modules/file/lang/en.php +++ b/modules/file/lang/en.php @@ -104,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'; @@ -122,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 d8d27ee9a..58392862f 100644 --- a/modules/file/lang/ko.php +++ b/modules/file/lang/ko.php @@ -128,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/upload_config.html b/modules/file/tpl/upload_config.html index bbdf9248e..932b6eb41 100644 --- a/modules/file/tpl/upload_config.html +++ b/modules/file/tpl/upload_config.html @@ -283,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}

+
+
@@ -291,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}

+
+
From 01d65dee7f9e75f84bbc97f87c3e7b53e0ca90c6 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 31 Mar 2026 19:54:53 +0900 Subject: [PATCH 21/30] Fix pre-conversion file size limit applying to admins --- modules/editor/skins/ckeditor/file_upload.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/editor/skins/ckeditor/file_upload.html b/modules/editor/skins/ckeditor/file_upload.html index 98687ef16..2a7874d11 100644 --- a/modules/editor/skins/ckeditor/file_upload.html +++ b/modules/editor/skins/ckeditor/file_upload.html @@ -12,7 +12,7 @@ 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="{intval($file_config->pre_conversion_filesize ?? 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'}"> From 1d8718a256900441cfeeaa446f62ce57c6182379 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 31 Mar 2026 20:50:58 +0900 Subject: [PATCH 22/30] Remove unused methods in menu module --- modules/menu/conf/module.xml | 2 - modules/menu/menu.admin.controller.php | 55 -------------------------- 2 files changed, 57 deletions(-) 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 From b1f84365a571bf778eec07c769319c1c37e07721 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 31 Mar 2026 21:02:33 +0900 Subject: [PATCH 23/30] Add 'command' type to R\F\Security::sanitize() --- common/framework/Security.php | 8 +++++++- tests/unit/framework/SecurityTest.php | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/common/framework/Security.php b/common/framework/Security.php index 6871af61e..871ce314b 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/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() From 6be98ff58c92788296be46118c1cf3289e3f2d74 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 31 Mar 2026 21:04:39 +0900 Subject: [PATCH 24/30] Simplify RVE-2026-6 patch using R\F\Security::sanitize() --- modules/file/file.admin.controller.php | 2 +- modules/file/file.controller.php | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/file/file.admin.controller.php b/modules/file/file.admin.controller.php index dacfa3e7d..52ccb1611 100644 --- a/modules/file/file.admin.controller.php +++ b/modules/file/file.admin.controller.php @@ -438,7 +438,7 @@ class FileAdminController extends File Rhymix\Framework\Storage::createDirectory($temp_dir); } $command = vsprintf('%s %s -resize %dx%d -quality %d %s %s %s', [ - (preg_match('![^a-z0-9/._-]!', $config->magick_command) || \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', diff --git a/modules/file/file.controller.php b/modules/file/file.controller.php index cc6a24ede..ab41a0003 100644 --- a/modules/file/file.controller.php +++ b/modules/file/file.controller.php @@ -1164,7 +1164,7 @@ class FileController extends File // Get image information if (in_array($file_info['extension'], ['avif', 'heic', 'heif']) && !empty($config->magick_command) && Rhymix\Framework\Storage::isExecutable($config->magick_command)) { - $command = (preg_match('![^a-z0-9/._-]!', $config->magick_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) { @@ -1334,7 +1334,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']); $command .= ' -movflags +faststart -pix_fmt yuv420p -c:v libx264 -crf 23'; $command .= sprintf(' -vf "scale=%d:%d"', $adjusted['width'], $adjusted['height']); @@ -1364,7 +1364,7 @@ class FileController extends File // Convert using magick $command = vsprintf('%s %s -resize %dx%d -quality %d %s %s %s', [ - (preg_match('![^a-z0-9/._-]!', $config->magick_command) || \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'], @@ -1389,7 +1389,7 @@ class FileController extends File 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', [ - (preg_match('![^a-z0-9/._-]!', $config->magick_command) || \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'], @@ -1434,7 +1434,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); @@ -1578,7 +1578,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']) { @@ -1621,7 +1621,7 @@ 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) From 99d6182f896b75f879f7dfcd1d20a9f60a85e0ee Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 31 Mar 2026 21:08:40 +0900 Subject: [PATCH 25/30] Update actions/checkout to v5 because github is deprecating node.js 20 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }} From cbb363671ab2ba8aac21cd24713dafbe1eafc8e3 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 31 Mar 2026 22:27:20 +0900 Subject: [PATCH 26/30] Add DocumentModel::getBlankDocument() and fix some incorrect return type comments --- modules/document/document.model.php | 32 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/modules/document/document.model.php b/modules/document/document.model.php index e95bcb32d..acf200d3d 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); } From 90f1238b23d75817b4088c0ecfadd6f0384ffefd Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 31 Mar 2026 22:28:39 +0900 Subject: [PATCH 27/30] Use DocumentModel::getBlankDocument() to obtain a dummy document with module_srl in BoardView --- modules/board/board.view.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/modules/board/board.view.php b/modules/board/board.view.php index 16bb3e970..d10a62853 100644 --- a/modules/board/board.view.php +++ b/modules/board/board.view.php @@ -318,8 +318,7 @@ class BoardView extends Board { if (abs($oDocument->get('member_srl')) != $this->user->member_srl) { - $oDocument = DocumentModel::getDocument(0); - $oDocument->add('module_srl', $this->module_srl); + $oDocument = DocumentModel::getBlankDocument($this->module_srl); Context::set('document_srl', null, true); $this->dispBoardMessage('msg_not_founded', 404); } @@ -328,8 +327,7 @@ class BoardView extends Board // if the document is TEMP saved, pretend that it doesn't exist. if($oDocument->getStatus() == 'TEMP') { - $oDocument = DocumentModel::getDocument(0); - $oDocument->add('module_srl', $this->module_srl); + $oDocument = DocumentModel::getBlankDocument($this->module_srl); Context::set('document_srl', null, true); $this->dispBoardMessage('msg_not_founded', 404); } @@ -347,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); } /** @@ -358,8 +355,7 @@ class BoardView extends Board { if(!$this->grant->view && !$oDocument->isGranted()) { - $oDocument = DocumentModel::getDocument(0); - $oDocument->add('module_srl', $this->module_srl); + $oDocument = DocumentModel::getBlankDocument($this->module_srl); Context::set('document_srl', null, true); $this->dispBoardMessage($this->user->isMember() ? 'msg_not_permitted' : 'msg_not_logged'); } From 530464289a1547f02ee752a570d2a380161094eb Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 1 Apr 2026 10:54:41 +0900 Subject: [PATCH 28/30] Version 2.1.32 --- common/constants.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From bdeffb61a511d5c0e5aadc9b7a13dff4e0b481b4 Mon Sep 17 00:00:00 2001 From: Lastorder-DC <18280396+Lastorder-DC@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:04:06 +0900 Subject: [PATCH 29/30] =?UTF-8?q?=EC=B6=94=EC=B2=9C=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/comment/comment.controller.php | 4 ++-- modules/document/document.controller.php | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/comment/comment.controller.php b/modules/comment/comment.controller.php index bdbce865d..15fac5391 100644 --- a/modules/comment/comment.controller.php +++ b/modules/comment/comment.controller.php @@ -58,9 +58,9 @@ class CommentController extends Comment } } } - $yeokka_member_srl = Rhymix\Modules\Yeokbox\Models\Config::getConfig()->yeokka_member_srl; + $logged_info = Context::get('logged_info'); - if($logged_info->member_srl != $yeokka_member_srl && $oComment->getRegdateTime() < (time() - (86400 * 7))) + if($logged_info->is_admin !== 'Y' && $oComment->getRegdateTime() < (time() - (86400 * 7))) { throw new Rhymix\Framework\Exception('작성 이후 7일 이상이 경과한 댓글은 추천할 수 없습니다.'); } diff --git a/modules/document/document.controller.php b/modules/document/document.controller.php index b2a645d6d..826e6f4c3 100644 --- a/modules/document/document.controller.php +++ b/modules/document/document.controller.php @@ -70,9 +70,8 @@ class DocumentController extends Document } } } - $yeokka_member_srl = Rhymix\Modules\Yeokbox\Models\Config::getConfig()->yeokka_member_srl; $logged_info = Context::get('logged_info'); - if($logged_info->member_srl != $yeokka_member_srl && $oDocument->getRegdateTime() < (time() - (86400 * 7))) + if($logged_info->is_admin !== 'Y' && $oDocument->getRegdateTime() < (time() - (86400 * 7))) { throw new Rhymix\Framework\Exception('작성 이후 7일 이상이 경과한 글은 추천할 수 없습니다.'); } From add448db7bd8caab5e68db92e793cab050723922 Mon Sep 17 00:00:00 2001 From: Lastorder-DC <18280396+Lastorder-DC@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:04:30 +0900 Subject: [PATCH 30/30] MemberController::getInstance --- modules/member/member.admin.controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/member/member.admin.controller.php b/modules/member/member.admin.controller.php index 54fe90c24..e75463d74 100644 --- a/modules/member/member.admin.controller.php +++ b/modules/member/member.admin.controller.php @@ -1838,7 +1838,7 @@ class MemberAdminController extends Member // Perform login as the target member // Session::login() sets the basic session variables, and setSessionInfo() populates Context with member details Rhymix\Framework\Session::login($member_info->member_srl); - $oMemberController = getController('member'); + $oMemberController = MemberController::getInstance(); $oMemberController->setSessionInfo(); $this->setRedirectUrl(getNotEncodedUrl(''));