From b213fc196885af2a4f96fc7e2d882e8ed8b11df9 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 13 Mar 2025 22:38:05 +0900 Subject: [PATCH 01/41] Fix module_srl not being updated for files attached to comments, when a document is moved to a different module --- modules/file/file.controller.php | 12 ++++++++++-- modules/file/queries/updateFileModuleComment.xml | 10 +++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/modules/file/file.controller.php b/modules/file/file.controller.php index fb8c9a076..3bdd7c785 100644 --- a/modules/file/file.controller.php +++ b/modules/file/file.controller.php @@ -1971,8 +1971,16 @@ class FileController extends File function triggerMoveDocument($obj) { $obj->upload_target_srls = $obj->document_srls; - executeQuery('file.updateFileModule', $obj); - executeQuery('file.updateFileModuleComment', $obj); + $output = executeQuery('file.updateFileModule', $obj); + if (!$output->toBool()) + { + return $output; + } + $output = executeQuery('file.updateFileModuleComment', $obj); + if (!$output->toBool()) + { + return $output; + } } function triggerAddCopyDocument(&$obj) diff --git a/modules/file/queries/updateFileModuleComment.xml b/modules/file/queries/updateFileModuleComment.xml index f053a1d4b..52c4e4b47 100644 --- a/modules/file/queries/updateFileModuleComment.xml +++ b/modules/file/queries/updateFileModuleComment.xml @@ -1,13 +1,13 @@ - -
+
+
- + - - + + From beec50f217f63f8d3c1dc9d4b319c8c8cf7fc24e Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 13 Mar 2025 22:57:38 +0900 Subject: [PATCH 02/41] Prevent files from being deleted upon module deletion if they belong to documents or comments that have since been moved to a different module --- modules/file/file.controller.php | 12 ++++--- modules/file/queries/getModuleFilesProper.xml | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 modules/file/queries/getModuleFilesProper.xml diff --git a/modules/file/file.controller.php b/modules/file/file.controller.php index 3bdd7c785..f77462546 100644 --- a/modules/file/file.controller.php +++ b/modules/file/file.controller.php @@ -1723,14 +1723,18 @@ class FileController extends File // Get a full list of attachments $args = new stdClass; $args->module_srl = $module_srl; - $output = executeQueryArray('file.getModuleFiles', $args); - if(!$output->toBool() || empty($file_list = $output->data)) + $output = executeQueryArray('file.getModuleFilesProper', $args); + if (!$output->toBool()) { return $output; } + if (!$output->data) + { + return; + } - // Delete the file - return $this->deleteFile($file_list); + // Delete each file. + return $this->deleteFile($output->data); } /** diff --git a/modules/file/queries/getModuleFilesProper.xml b/modules/file/queries/getModuleFilesProper.xml new file mode 100644 index 000000000..bbfb72108 --- /dev/null +++ b/modules/file/queries/getModuleFilesProper.xml @@ -0,0 +1,31 @@ + + +
+
+ + + +
+ + + + +
+
+ + + + + + + + + + + + + + + + +
From fd103ba67d3062f87746a3e44a15a3255329326d Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 13 Mar 2025 23:21:33 +0900 Subject: [PATCH 03/41] Fix #2522 browser titles for member pages not following admin template --- classes/module/ModuleHandler.class.php | 7 +-- modules/communication/communication.view.php | 14 +++--- modules/member/member.view.php | 50 +++++++++++++------- modules/ncenterlite/ncenterlite.view.php | 8 ++-- 4 files changed, 46 insertions(+), 33 deletions(-) diff --git a/classes/module/ModuleHandler.class.php b/classes/module/ModuleHandler.class.php index 8d5ebfe70..0a00ad038 100644 --- a/classes/module/ModuleHandler.class.php +++ b/classes/module/ModuleHandler.class.php @@ -848,15 +848,10 @@ class ModuleHandler extends Handler $seo_title = config('seo.subpage_title') ?: '$SITE_TITLE - $SUBPAGE_TITLE'; } $seo_title = Context::replaceUserLang($seo_title); - $subpage_title = $module_info->browser_title; - if (in_array($module_info->module, ['member'])) - { - $subpage_title = ''; - } Context::setBrowserTitle($seo_title, array( 'site_title' => Context::getSiteTitle(), 'site_subtitle' => Context::getSiteSubtitle(), - 'subpage_title' => $subpage_title, + 'subpage_title' => $module_info->browser_title, 'page' => Context::get('page') ?: 1, )); diff --git a/modules/communication/communication.view.php b/modules/communication/communication.view.php index f5b6cb1e4..22b90a54c 100644 --- a/modules/communication/communication.view.php +++ b/modules/communication/communication.view.php @@ -139,11 +139,11 @@ class CommunicationView extends communication if ($message) { - Context::addBrowserTitle($message->title); + MemberView::setMemberPageBrowserTitle($message->title); } else { - Context::addBrowserTitle(lang('communication.message_box.' . $message_type)); + MemberView::setMemberPageBrowserTitle(lang('communication.message_box.' . $message_type)); } $this->setTemplateFile($template_filename); @@ -186,7 +186,7 @@ class CommunicationView extends communication Context::set('message', $message); } - Context::addBrowserTitle($message->title ?? lang('cmd_view_message_box')); + MemberView::setMemberPageBrowserTitle($message->title ?? lang('cmd_view_message_box')); $this->setTemplateFile('new_message'); } @@ -313,7 +313,7 @@ class CommunicationView extends communication $editor = $oEditorModel->getEditor(getNextSequence(), $option); $editor = $editor . "\n" . '' . "\n"; Context::set('editor', $editor); - Context::addBrowserTitle(lang('cmd_send_message')); + MemberView::setMemberPageBrowserTitle(lang('cmd_send_message')); $this->setTemplateFile('send_message'); // Fix for skins that don't support window_type=self @@ -387,7 +387,7 @@ class CommunicationView extends communication Context::set('friend_list', $output->data); Context::set('page_navigation', $output->page_navigation); - Context::addBrowserTitle(lang('cmd_view_friend')); + MemberView::setMemberPageBrowserTitle(lang('cmd_view_friend')); $this->setTemplateFile('friends'); } @@ -460,7 +460,7 @@ class CommunicationView extends communication $friend_group_list = $oCommunicationModel->getFriendGroups(); Context::set('friend_group_list', $friend_group_list); - Context::addBrowserTitle(lang('cmd_add_friend')); + MemberView::setMemberPageBrowserTitle(lang('cmd_add_friend')); $this->setTemplateFile('add_friend'); // Fix for skins that don't support window_type=self @@ -524,7 +524,7 @@ class CommunicationView extends communication } } - Context::addBrowserTitle(lang('cmd_add_friend_group')); + MemberView::setMemberPageBrowserTitle(lang('cmd_add_friend_group')); $this->setTemplateFile('add_friend_group'); // Fix for skins that don't support window_type=self diff --git a/modules/member/member.view.php b/modules/member/member.view.php index 19727f83f..d6afba4af 100644 --- a/modules/member/member.view.php +++ b/modules/member/member.view.php @@ -114,6 +114,24 @@ class MemberView extends Member } } + /** + * Set the browser title for a page belonging to the member menu. + * + * @param string $title + * @return void + */ + public static function setMemberPageBrowserTitle(string $title): void + { + $seo_title = config('seo.subpage_title') ?: '$SITE_TITLE - $SUBPAGE_TITLE'; + $seo_title = Context::replaceUserLang($seo_title); + Context::setBrowserTitle($seo_title, array( + 'site_title' => Context::getSiteTitle(), + 'site_subtitle' => Context::getSiteSubtitle(), + 'subpage_title' => $title, + 'page' => Context::get('page') ?: 1, + )); + } + /** * Module index */ @@ -181,7 +199,7 @@ class MemberView extends Member $member_info->group_list[$key] = Context::replaceUserLang($val, true); } - Context::addBrowserTitle(lang('cmd_view_member_info')); + self::setMemberPageBrowserTitle(lang('cmd_view_member_info')); Context::set('memberInfo', get_object_vars($member_info)); $extendForm = MemberModel::getCombineJoinForm($member_info); @@ -376,7 +394,7 @@ class MemberView extends Member $member_config->agreement = $member_config->agreements[1]->content ?? ''; // Set a template file - Context::addBrowserTitle(lang('cmd_signup')); + self::setMemberPageBrowserTitle(lang('cmd_signup')); $this->setTemplateFile('signup_form'); } @@ -413,7 +431,7 @@ class MemberView extends Member Context::set('identifierValue', $logged_info->user_id); } - Context::addBrowserTitle(lang('cmd_modify_member_info')); + self::setMemberPageBrowserTitle(lang('cmd_modify_member_info')); $this->setTemplateFile('rechecked_password'); } @@ -499,7 +517,7 @@ class MemberView extends Member $this->addExtraFormValidatorMessage(); // Set a template file - Context::addBrowserTitle(lang('cmd_modify_member_info')); + self::setMemberPageBrowserTitle(lang('cmd_modify_member_info')); $this->setTemplateFile('modify_info'); } @@ -550,7 +568,7 @@ class MemberView extends Member $oSecurity = new Security(); $oSecurity->encodeHTML('document_list...title', 'search_target', 'search_keyword'); - Context::addBrowserTitle(lang('cmd_view_own_document')); + self::setMemberPageBrowserTitle(lang('cmd_view_own_document')); $this->setTemplateFile('document_list'); } @@ -598,7 +616,7 @@ class MemberView extends Member $oSecurity = new Security(); $oSecurity->encodeHTML('search_target', 'search_keyword'); - Context::addBrowserTitle(lang('cmd_view_own_comment')); + self::setMemberPageBrowserTitle(lang('cmd_view_own_comment')); $this->setTemplateFile('comment_list'); } @@ -708,7 +726,7 @@ class MemberView extends Member $security = new Security($output->data); $security->encodeHTML('..nick_name'); - Context::addBrowserTitle(lang('cmd_view_scrapped_document')); + self::setMemberPageBrowserTitle(lang('cmd_view_scrapped_document')); $this->setTemplateFile('scrapped_list'); } @@ -743,7 +761,7 @@ class MemberView extends Member Context::set('document_list', $output->data); Context::set('page_navigation', $output->page_navigation); - Context::addBrowserTitle(lang('cmd_view_saved_document')); + self::setMemberPageBrowserTitle(lang('cmd_view_saved_document')); $this->setTemplateFile('saved_list'); } @@ -783,7 +801,7 @@ class MemberView extends Member $output = executeQueryArray('member.getMemberDevice', $args); Context::set('registered_devices', $output->data); - Context::addBrowserTitle(lang('cmd_view_active_logins')); + self::setMemberPageBrowserTitle(lang('cmd_view_active_logins')); $this->setTemplateFile('active_logins'); } @@ -822,7 +840,7 @@ class MemberView extends Member } // Set a template file - Context::addBrowserTitle(lang('cmd_login')); + self::setMemberPageBrowserTitle(lang('cmd_login')); $this->setTemplateFile('login_form'); } @@ -858,7 +876,7 @@ class MemberView extends Member Context::set('formValue', $member_info->email_address); } // Set a template file - Context::addBrowserTitle(lang('cmd_modify_member_password')); + self::setMemberPageBrowserTitle(lang('cmd_modify_member_password')); $this->setTemplateFile('modify_password'); } @@ -893,7 +911,7 @@ class MemberView extends Member Context::set('formValue', $member_info->email_address); } // Set a template file - Context::addBrowserTitle(lang('msg_leave_member')); + self::setMemberPageBrowserTitle(lang('msg_leave_member')); $this->setTemplateFile('leave_form'); } @@ -948,7 +966,7 @@ class MemberView extends Member Context::set('identifier', $this->member_config->identifier); Context::set('enable_find_account_question', 'N'); - Context::addBrowserTitle(lang('cmd_find_member_account')); + self::setMemberPageBrowserTitle(lang('cmd_find_member_account')); $this->setTemplateFile('find_member_account'); } @@ -967,7 +985,7 @@ class MemberView extends Member return; } - Context::addBrowserTitle(lang('cmd_resend_auth_mail')); + self::setMemberPageBrowserTitle(lang('cmd_resend_auth_mail')); $this->setTemplateFile('resend_auth_mail'); } @@ -987,7 +1005,7 @@ class MemberView extends Member $_SESSION['rechecked_password_step'] = 'INPUT_DATA'; - Context::addBrowserTitle(lang('cmd_modify_member_email_address')); + self::setMemberPageBrowserTitle(lang('cmd_modify_member_email_address')); $this->setTemplateFile('modify_email_address'); } @@ -1106,7 +1124,7 @@ class MemberView extends Member Context::set('nickname_list', $output->data); Context::set('page_navigation', $output->page_navigation); - Context::addBrowserTitle(lang('cmd_modify_nickname_log')); + self::setMemberPageBrowserTitle(lang('cmd_modify_nickname_log')); $this->setTemplateFile('member_nick'); } } diff --git a/modules/ncenterlite/ncenterlite.view.php b/modules/ncenterlite/ncenterlite.view.php index 9d5954a20..72d090eca 100644 --- a/modules/ncenterlite/ncenterlite.view.php +++ b/modules/ncenterlite/ncenterlite.view.php @@ -28,7 +28,7 @@ class NcenterliteView extends Ncenterlite Context::set('ncenterlite_list', $output->data); Context::set('page_navigation', $output->page_navigation); - Context::addBrowserTitle(lang('ncenterlite_my_list')); + MemberView::setMemberPageBrowserTitle(lang('ncenterlite_my_list')); $this->setTemplateFileOrDefault('NotifyList'); } @@ -86,7 +86,7 @@ class NcenterliteView extends Ncenterlite Context::set('sms_available', Rhymix\Framework\SMS::getDefaultDriver()->getName() !== 'Dummy'); Context::set('push_available', count(Rhymix\Framework\Config::get('push.types') ?? []) > 0); - Context::addBrowserTitle(lang('ncenterlite_my_settings')); + MemberView::setMemberPageBrowserTitle(lang('ncenterlite_my_settings')); $this->setTemplateFileOrDefault('userconfig'); } @@ -140,7 +140,7 @@ class NcenterliteView extends Ncenterlite Context::set('unsubscribe_list', $output->data); Context::set('page_navigation', $output->page_navigation); - Context::addBrowserTitle(lang('unsubscribe_list')); + MemberView::setMemberPageBrowserTitle(lang('unsubscribe_list')); $this->setTemplateFileOrDefault('unsubscribeList'); } @@ -230,7 +230,7 @@ class NcenterliteView extends Ncenterlite Context::set('text', $text); Context::set('type', $type); - Context::addBrowserTitle(lang('unsubscribe_list')); + MemberView::setMemberPageBrowserTitle(lang('unsubscribe_list')); $this->setTemplateFileOrDefault('unsubscribe'); } From bb3ea5c9aa2f6be5f56e51e681dba5a4a5dd2b3d Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Fri, 14 Mar 2025 01:22:32 +0900 Subject: [PATCH 04/41] Fix overlapping pagination links #2521 --- modules/admin/tpl/css/admin.css | 5 +++++ modules/comment/tpl/comment_list.html | 1 - modules/document/tpl/document_list.html | 15 +++++++-------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/modules/admin/tpl/css/admin.css b/modules/admin/tpl/css/admin.css index c3a09a36a..8ef7ff6a5 100644 --- a/modules/admin/tpl/css/admin.css +++ b/modules/admin/tpl/css/admin.css @@ -185,6 +185,11 @@ body>.x, height: 24px; padding: 0 6px; } +@media screen and (max-width: 800px) { + .x .x_pagination { + clear: both; + } +} .x .btn { color: #333; } diff --git a/modules/comment/tpl/comment_list.html b/modules/comment/tpl/comment_list.html index 838df8911..ff14e4691 100644 --- a/modules/comment/tpl/comment_list.html +++ b/modules/comment/tpl/comment_list.html @@ -83,7 +83,6 @@ xe.lang.msg_empty_search_keyword = '{$lang->msg_empty_search_keyword}'; -
diff --git a/modules/document/tpl/document_list.html b/modules/document/tpl/document_list.html index d9f53ed3f..20ca7a103 100644 --- a/modules/document/tpl/document_list.html +++ b/modules/document/tpl/document_list.html @@ -72,16 +72,15 @@ xe.lang.msg_empty_search_keyword = '{$lang->msg_empty_search_keyword}'; -
- + -
+ + From 3f553d4d9cce8e975353195a1c36c9f707410d9f Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Fri, 14 Mar 2025 01:37:51 +0900 Subject: [PATCH 05/41] Hide vote/blame log if respective setting is disabled #2523 --- modules/board/board.view.php | 20 ++++++++++++++++++-- modules/board/models/Features.php | 8 ++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/modules/board/board.view.php b/modules/board/board.view.php index 84ac81c11..06b6077ff 100644 --- a/modules/board/board.view.php +++ b/modules/board/board.view.php @@ -1523,6 +1523,12 @@ class BoardView extends Board throw new Rhymix\Framework\Exception('msg_not_target'); } + $features = Rhymix\Modules\Board\Models\Features::fromModuleInfo($this->module_info); + if (!$features->{$target}->vote_log) + { + throw new Rhymix\Framework\Exceptions\FeatureDisabled; + } + $output = executeQueryArray($queryId, $args); if(!$output->toBool()) { @@ -1537,7 +1543,11 @@ class BoardView extends Board { if($log->point > 0) { - if($log->member_srl == $vote_member_infos[$log->member_srl]->member_srl) + if (isset($vote_member_infos[$log->member_srl])) + { + continue; + } + if (!$features->{$target}->vote_up_log) { continue; } @@ -1545,7 +1555,11 @@ class BoardView extends Board } else { - if($log->member_srl == $blame_member_infos[$log->member_srl]->member_srl) + if (isset($blame_member_infos[$log->member_srl])) + { + continue; + } + if (!$features->{$target}->vote_down_log) { continue; } @@ -1553,6 +1567,8 @@ class BoardView extends Board } } } + + Context::set('board_features', $features); Context::set('vote_member_info', $vote_member_infos); Context::set('blame_member_info', $blame_member_infos); $this->setTemplateFile('vote_log'); diff --git a/modules/board/models/Features.php b/modules/board/models/Features.php index 6bbc567ec..5f815be70 100644 --- a/modules/board/models/Features.php +++ b/modules/board/models/Features.php @@ -61,8 +61,10 @@ class Features // Document features $features->document->vote_up = ($document_config->use_vote_up ?? 'Y') !== 'N'; + $features->document->vote_up_log = ($document_config->use_vote_up ?? 'Y') === 'S'; $features->document->vote_down = ($document_config->use_vote_down ?? 'Y') !== 'N'; - $features->document->vote_log = ($document_config->use_vote_up ?? 'Y') === 'S' || ($document_config->use_vote_down ?? 'Y') === 'S'; + $features->document->vote_down_log = ($document_config->use_vote_down ?? 'Y') === 'S'; + $features->document->vote_log = $features->document->vote_up_log || $features->document->vote_down_log; if (isset($document_config->allow_vote_cancel)) { $features->document->cancel_vote = $document_config->allow_vote_cancel === 'Y'; @@ -92,8 +94,10 @@ class Features // Comment features $features->comment->vote_up = ($comment_config->use_vote_up ?? 'Y') !== 'N'; + $features->comment->vote_up_log = ($comment_config->use_vote_up ?? 'Y') === 'S'; $features->comment->vote_down = ($comment_config->use_vote_down ?? 'Y') !== 'N'; - $features->comment->vote_log = ($comment_config->use_vote_up ?? 'Y') === 'S' || ($comment_config->use_vote_down ?? 'Y') === 'S'; + $features->comment->vote_down_log = ($comment_config->use_vote_down ?? 'Y') === 'S'; + $features->comment->vote_log = $features->comment->vote_up_log || $features->comment->vote_down_log; if (isset($comment_config->allow_vote_cancel)) { $features->comment->cancel_vote = $comment_config->allow_vote_cancel === 'Y'; From 2276110b0feb9e87cfc70905fc30704f6550d402 Mon Sep 17 00:00:00 2001 From: dewekk <60457472+dewekk@users.noreply.github.com> Date: Fri, 14 Mar 2025 09:46:10 +0900 Subject: [PATCH 06/41] =?UTF-8?q?utf8=5Fnormalize=5Fspaces=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=EC=97=90=EC=84=9C=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EA=B3=B5=EB=B0=B1=20=EB=AC=B8=EC=9E=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/functions.php b/common/functions.php index 84147358d..17c243e46 100644 --- a/common/functions.php +++ b/common/functions.php @@ -680,7 +680,7 @@ function utf8_mbencode($str): string */ function utf8_normalize_spaces($str, bool $multiline = false): string { - return $multiline ? preg_replace('/((?!\x0A)[\pZ\pC])+/u', ' ', (string)$str) : preg_replace('/[\pZ\pC]+/u', ' ', (string)$str); + return $multiline ? preg_replace(['/((?!\x0A)[\pZ\pC])+/u', '/\x20(?=\x0A)/u'], [' ', ''], (string)$str) : preg_replace('/[\pZ\pC]+/u', ' ', (string)$str); } /** From 2c05d3f0c76409facc4de8b833aafb0927509d5c Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 18 Mar 2025 22:37:51 +0900 Subject: [PATCH 07/41] =?UTF-8?q?#2527=20=EB=B3=B4=EC=99=84:=20\n=20?= =?UTF-8?q?=EB=92=A4=EC=97=90=20=EA=B3=B5=EB=B0=B1=20=EB=82=A8=EC=9D=84=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/functions.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/common/functions.php b/common/functions.php index 17c243e46..84753ca64 100644 --- a/common/functions.php +++ b/common/functions.php @@ -680,7 +680,14 @@ function utf8_mbencode($str): string */ function utf8_normalize_spaces($str, bool $multiline = false): string { - return $multiline ? preg_replace(['/((?!\x0A)[\pZ\pC])+/u', '/\x20(?=\x0A)/u'], [' ', ''], (string)$str) : preg_replace('/[\pZ\pC]+/u', ' ', (string)$str); + if ($multiline) + { + return preg_replace(['/((?!\x0A)[\pZ\pC])+/u', '/\x20*\x0A\x20*/'], [' ', "\n"], (string)$str); + } + else + { + return preg_replace('/[\pZ\pC]+/u', ' ', (string)$str); + } } /** From 82195c3e14f173e3b3a64e06a76f2a0256cd670f Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 18 Mar 2025 22:38:07 +0900 Subject: [PATCH 08/41] =?UTF-8?q?#2527=20=EB=B3=B4=EC=99=84:=20=EC=9C=A0?= =?UTF-8?q?=EB=8B=9B=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/functions/FunctionsTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/functions/FunctionsTest.php b/tests/unit/functions/FunctionsTest.php index 0c6ed1e15..d9657c2f5 100644 --- a/tests/unit/functions/FunctionsTest.php +++ b/tests/unit/functions/FunctionsTest.php @@ -226,6 +226,8 @@ class FunctionsTest extends \Codeception\Test\Unit $this->assertEquals("Weird spaces are in this string", utf8_normalize_spaces("Weird\x20spaces\xe2\x80\x80are\xe2\x80\x84in\xe2\x80\x86\xe2\x80\x8bthis\x0astring")); $this->assertEquals("Weird spaces are in this\nstring", utf8_normalize_spaces("Weird\x20spaces\xe2\x80\x80are\xe2\x80\x84in\xe2\x80\x86\xe2\x80\x8bthis\x0astring", true)); + $this->assertEquals("Stupid Windows\nLine Breaks", utf8_normalize_spaces("Stupid Windows \r\n Line Breaks", true)); + $this->assertEquals("Multiple\nCRLF\n\nsequences", utf8_normalize_spaces("Multiple \r\nCRLF\r\n\t\r\n sequences", true)); $this->assertEquals("Trimmed", utf8_trim("\x20\xe2\x80\x80Trimmed\xe2\x80\x84\xe2\x80\x86\xe2\x80\x8b")); $this->assertEquals("Trimmed", utf8_trim("\x20\xe2\x80\x80Trimmed\x0a\x0c\x07\x09")); } From 800eb2f444a2ea960d4dfd09d0853062c47bd974 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 18 Mar 2025 22:51:22 +0900 Subject: [PATCH 09/41] Change behavior of \@mobile directive in template v2 #2510 --- common/framework/Template.php | 10 ++++++++++ .../framework/parsers/template/TemplateParser_v2.php | 4 ++-- tests/_data/template/v2example.compiled.html | 2 +- tests/unit/framework/parsers/TemplateParserV2Test.php | 4 ++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/common/framework/Template.php b/common/framework/Template.php index 8330aae8a..a94c8fbc6 100644 --- a/common/framework/Template.php +++ b/common/framework/Template.php @@ -945,6 +945,16 @@ class Template return count($args) ? in_array((string)$validator_id, $args, true) : true; } + /** + * Check if the current visitor is using a mobile device for v2. + * + * @return bool + */ + protected function _v2_isMobile(): bool + { + return UA::isMobile() && (config('mobile.tablets') || !UA::isTablet()); + } + /** * Lang shortcut for v2. * diff --git a/common/framework/parsers/template/TemplateParser_v2.php b/common/framework/parsers/template/TemplateParser_v2.php index b1d6c0c46..7e6d1dcbf 100644 --- a/common/framework/parsers/template/TemplateParser_v2.php +++ b/common/framework/parsers/template/TemplateParser_v2.php @@ -104,8 +104,8 @@ class TemplateParser_v2 'cannot' => ['if ($this->_v2_checkCapability(2, %s)):', 'endif;'], 'canany' => ['if ($this->_v2_checkCapability(3, %s)):', 'endif;'], 'guest' => ['if (!$this->user->isMember()):', 'endif;'], - 'desktop' => ["if (!\\Context::get('m')):", 'endif;'], - 'mobile' => ["if (\\Context::get('m')):", 'endif;'], + 'desktop' => ['if (!$this->_v2_isMobile()):', 'endif;'], + 'mobile' => ['if ($this->_v2_isMobile()):', 'endif;'], 'env' => ['if (!empty($_ENV[%s])):', 'endif;'], 'else' => ['else:'], 'elseif' => ['elseif (%s):'], diff --git a/tests/_data/template/v2example.compiled.html b/tests/_data/template/v2example.compiled.html index d4742e251..ef60e4677 100644 --- a/tests/_data/template/v2example.compiled.html +++ b/tests/_data/template/v2example.compiled.html @@ -42,7 +42,7 @@ _v2_include("include", $__filename, [(string)$__varname => $__var]); endforeach; })('incl/eachtest', $__Context->bar, 'var'); ?> _v2_include("include", $__filename, [(string)$__varname => $__var]); endforeach; })('incl/eachtest', [], 'anything', 'incl/empty'); ?> - +_v2_isMobile()): ?>

The full class name is , config->context === 'JS' ? escape_js(Rhymix\Framework\Push::class) : htmlspecialchars(Rhymix\Framework\Push::class, \ENT_QUOTES, 'UTF-8', false); ?> really.

diff --git a/tests/unit/framework/parsers/TemplateParserV2Test.php b/tests/unit/framework/parsers/TemplateParserV2Test.php index e6c7b0d9c..57d849151 100644 --- a/tests/unit/framework/parsers/TemplateParserV2Test.php +++ b/tests/unit/framework/parsers/TemplateParserV2Test.php @@ -858,10 +858,10 @@ class TemplateParserV2Test extends \Codeception\Test\Unit '@endmobile', ]); $target = implode("\n", [ - "", + '_v2_isMobile()): ?>', '

4K or GTFO!

', '', - "", + '_v2_isMobile()): ?>', '

USB C is the way to go~

', '', ]); From 62eb6b2aaecb473fba143e46c1d952b5dcb0212c Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 18 Mar 2025 23:17:42 +0900 Subject: [PATCH 10/41] Add more context switches for inline scripts and styles --- .../parsers/template/TemplateParser_v2.php | 33 ++++++++++++-- tests/_data/template/v2example.compiled.html | 4 +- .../parsers/TemplateParserV2Test.php | 45 ++++++++++++++++--- 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/common/framework/parsers/template/TemplateParser_v2.php b/common/framework/parsers/template/TemplateParser_v2.php index 7e6d1dcbf..5700f9fdf 100644 --- a/common/framework/parsers/template/TemplateParser_v2.php +++ b/common/framework/parsers/template/TemplateParser_v2.php @@ -179,20 +179,45 @@ class TemplateParser_v2 */ protected function _addContextSwitches(string $content): string { - return preg_replace_callback('#(]*)|config->context = \'CSS\'; ?>' . $match[2] . 'config->context = \'HTML\'; ?>"'; + }, $content); + + // Inline scripts. + $content = preg_replace_callback('#(?<=\s)(href="javascript:|on[a-z]+=")([^"]*?)"#i', function($match) { + return $match[1] . 'config->context = \'JS\'; ?>' . $match[2] . 'config->context = \'HTML\'; ?>"'; + }, $content); + + // '; + $target = 'config->context = \'CSS\'; ?>> body { font-size: 16px; } config->context = \'HTML\'; ?>'; + $this->assertEquals($target, $this->_parse($source, true, false)); + + // Inline style + $source = '
'; + $target = '
'; + $this->assertEquals($target, $this->_parse($source, true, false)); + } + public function testEchoStatements() { // Basic usage of XE-style single braces @@ -366,11 +394,6 @@ class TemplateParserV2Test extends \Codeception\Test\Unit $target = "foo ?? ''); ?>"; $this->assertEquals($target, $this->_parse($source)); - // Context-aware escape - $source = ''; - $target = ''; - $this->assertEquals($target, $this->_parse($source)); - // JSON using context-aware escape $source = '{{ $foo|json }}'; $target = implode('', [ @@ -573,7 +596,7 @@ class TemplateParserV2Test extends \Codeception\Test\Unit // Script tag with external path $source = ''; - $target = ''; + $target = ''; $this->assertEquals($target, $this->_parse($source)); // Absolute URL @@ -1295,9 +1318,10 @@ class TemplateParserV2Test extends \Codeception\Test\Unit * * @param string $source * @param bool $force_v2 Disable version detection + * @param bool $remove_context_switches Remove context switches that make code difficult to read * @return string */ - protected function _parse(string $source, bool $force_v2 = true): string + protected function _parse(string $source, bool $force_v2 = true, bool $remove_context_switches = true): string { $tmpl = new \Rhymix\Framework\Template('./tests/_data/template', 'empty.html'); if ($force_v2) @@ -1309,6 +1333,13 @@ class TemplateParserV2Test extends \Codeception\Test\Unit { $result = substr($result, strlen($this->prefix)); } + + // Remove context switches. + if ($remove_context_switches) + { + $result = preg_replace('#<\?php \$this->config->context = \'[A-Z]+\'; \?>#', '', $result); + } + return $result; } From baadb36e378e651cd36eef78a52b0a872a42ed13 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 18 Mar 2025 23:53:06 +0900 Subject: [PATCH 11/41] Improve escape_css() to accept most common CSS expressions --- common/functions.php | 2 +- tests/unit/functions/FunctionsTest.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/common/functions.php b/common/functions.php index 84753ca64..8680c6f39 100644 --- a/common/functions.php +++ b/common/functions.php @@ -205,7 +205,7 @@ function escape($str, bool $double_escape = true, bool $except_lang_code = false */ function escape_css(string $str): string { - return preg_replace('/[^a-zA-Z0-9_.#\/-]/', '', (string)$str); + return preg_replace('/[^a-zA-Z0-9_.,#%\/\'()\x20-]/', '', (string)$str); } /** diff --git a/tests/unit/functions/FunctionsTest.php b/tests/unit/functions/FunctionsTest.php index d9657c2f5..000fe451f 100644 --- a/tests/unit/functions/FunctionsTest.php +++ b/tests/unit/functions/FunctionsTest.php @@ -50,8 +50,10 @@ class FunctionsTest extends \Codeception\Test\Unit $this->assertEquals('$user_lang->userLang1234567890', escape('$user_lang->userLang1234567890', true, false)); $this->assertEquals('$user_lang->userLang1234567890', escape('$user_lang->userLang1234567890', true, true)); - $this->assertEquals('expressionalertXSS', escape_css('expression:alert("XSS")')); + $this->assertEquals('expressionalert(XSS)', escape_css('expression:alert("XSS")')); $this->assertEquals('#123456', escape_css('#123456')); + $this->assertEquals('16px/160% Segoe UI, sans-serif font-style', escape_css('16px/160% Segoe UI, sans-serif; font-style')); + $this->assertEquals('box-shadow(0 1px 2px rgba(0, 0, 0, 0.15)', escape_css('box-shadow(0 1px 2px rgba(0, 0, 0, "0.15")')); $this->assertEquals('hello\\\\world', escape_js('hello\\world')); $this->assertEquals('\u003Cbr \/\u003E', escape_js('
')); From 9689a1ed683aa733ed40a65f0d04b3fbbfae26e9 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 19 Mar 2025 00:12:39 +0900 Subject: [PATCH 12/41] Implement better contextual escape for template v2 --- common/framework/Template.php | 17 +++++++++++++++++ .../parsers/template/TemplateParser_v2.php | 8 ++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/common/framework/Template.php b/common/framework/Template.php index a94c8fbc6..b0e2c9a06 100644 --- a/common/framework/Template.php +++ b/common/framework/Template.php @@ -955,6 +955,23 @@ class Template return UA::isMobile() && (config('mobile.tablets') || !UA::isTablet()); } + /** + * Contextual escape function for v2. + * + * @param string $str + * @param string $type + * @return string + */ + protected function _v2_escape(string $str, string $type = ''): string + { + switch ($this->config->context) + { + case 'CSS': return escape_css($str); + case 'JS': return escape_js($str); + default: return escape($str); + } + } + /** * Lang shortcut for v2. * diff --git a/common/framework/parsers/template/TemplateParser_v2.php b/common/framework/parsers/template/TemplateParser_v2.php index 5700f9fdf..926c3e0f4 100644 --- a/common/framework/parsers/template/TemplateParser_v2.php +++ b/common/framework/parsers/template/TemplateParser_v2.php @@ -782,7 +782,7 @@ class TemplateParser_v2 'json_encode(%s, self::$_json_options2) : ' . 'htmlspecialchars(json_encode(%s, self::$_json_options), \ENT_QUOTES, \'UTF-8\', false); ?>', $args, $args); case 'lang': - return sprintf('config->context === \'JS\' ? escape_js($this->_v2_lang(%s)) : $this->_v2_lang(%s); ?>', $args, $args); + return sprintf('config->context === \'HTML\' ? $this->_v2_lang(%s) : $this->_v2_escape($this->_v2_lang(%s)); ?>', $args, $args); case 'dump': return sprintf('', $args); case 'dd': @@ -790,7 +790,7 @@ class TemplateParser_v2 case 'stack': return sprintf('', $args); case 'url': - return sprintf('config->context === \'JS\' ? escape_js(getNotEncodedUrl(%s)) : getUrl(%s); ?>', $args, $args); + return sprintf('config->context === \'HTML\' ? getUrl(%s) : $this->_v2_escape(getNotEncodedUrl(%s)); ?>', $args, $args); default: return $match[0]; } @@ -968,11 +968,11 @@ class TemplateParser_v2 switch($option) { case 'autocontext': - return "\$this->config->context === 'JS' ? escape_js({$str2}) : htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false)"; + return "\$this->config->context === 'HTML' ? htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape({$str2})"; case 'autocontext_json': return "\$this->config->context === 'JS' ? {$str2} : htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false)"; case 'autocontext_lang': - return "\$this->config->context === 'JS' ? escape_js({$str2}) : ({$str})"; + return "\$this->config->context === 'HTML' ? ({$str}) : \$this->_v2_escape({$str2})"; case 'autoescape': return "htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false)"; case 'autolang': From 0bc3635b6ce11cc9868b484269e0884238903954 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 19 Mar 2025 00:12:57 +0900 Subject: [PATCH 13/41] Update unit tests for improved contextual escape --- tests/_data/template/v2example.compiled.html | 12 ++--- .../parsers/TemplateParserV2Test.php | 50 +++++++++---------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/tests/_data/template/v2example.compiled.html b/tests/_data/template/v2example.compiled.html index 0929af928..2ad7c4bb9 100644 --- a/tests/_data/template/v2example.compiled.html +++ b/tests/_data/template/v2example.compiled.html @@ -15,16 +15,16 @@ {{ $foo }} - + get('foo')): ?> required="required"> - bar[3] === 'da'): ?> required="required" /> + bar[3] === 'da'): ?> required="required" /> baz))): ?> class="foobar"> foo || $__Context->bar): ?>

Hello bar): ?>foo ?? ''; ?>

-

config->context === 'JS' ? escape_js(implode('|', array_map(function($i) { return strtoupper($i); }, $__Context->bar))) : htmlspecialchars(implode('|', array_map(function($i) { return strtoupper($i); }, $__Context->bar)), \ENT_QUOTES, 'UTF-8', false); ?>

+

config->context === 'HTML' ? htmlspecialchars(implode('|', array_map(function($i) { return strtoupper($i); }, $__Context->bar)), \ENT_QUOTES, 'UTF-8', false) : $this->_v2_escape(implode('|', array_map(function($i) { return strtoupper($i); }, $__Context->bar))); ?>

@@ -33,7 +33,7 @@
nosuchvar)): ?> unit tests are cool - k >= 2): ?>class="config->context === 'JS' ? escape_js($__Context->val ?? '') : htmlspecialchars($__Context->val ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"> + k >= 2): ?>class="config->context === 'HTML' ? htmlspecialchars($__Context->val ?? '', \ENT_QUOTES, 'UTF-8', false) : $this->_v2_escape($__Context->val ?? ''); ?>">
_v2_incrLoopVar($__loop_RANDOM_LOOP_ID); endforeach; $this->_v2_removeLoopVar($__loop_RANDOM_LOOP_ID); unset($__loop_RANDOM_LOOP_ID); else: ?>
Nothing here...
@@ -43,7 +43,7 @@ _v2_include("include", $__filename, [(string)$__varname => $__var]); endforeach; })('incl/eachtest', [], 'anything', 'incl/empty'); ?> _v2_isMobile()): ?> -

The full class name is , config->context === 'JS' ? escape_js(Rhymix\Framework\Push::class) : htmlspecialchars(Rhymix\Framework\Push::class, \ENT_QUOTES, 'UTF-8', false); ?> really.

+

The full class name is , config->context === 'HTML' ? htmlspecialchars(Rhymix\Framework\Push::class, \ENT_QUOTES, 'UTF-8', false) : $this->_v2_escape(Rhymix\Framework\Push::class); ?> really.

@@ -61,6 +61,6 @@
diff --git a/tests/unit/framework/parsers/TemplateParserV2Test.php b/tests/unit/framework/parsers/TemplateParserV2Test.php index 2d6a7a763..4892fbb87 100644 --- a/tests/unit/framework/parsers/TemplateParserV2Test.php +++ b/tests/unit/framework/parsers/TemplateParserV2Test.php @@ -280,7 +280,7 @@ class TemplateParserV2Test extends \Codeception\Test\Unit { // Basic usage of XE-style single braces $source = '{$var}'; - $target = "config->context === 'JS' ? escape_js(\$__Context->var ?? '') : htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(\$__Context->var ?? ''); ?>"; $this->assertEquals($target, $this->_parse($source)); // Single braces with space at beginning will not be parsed @@ -290,22 +290,22 @@ class TemplateParserV2Test extends \Codeception\Test\Unit // Single braces with space at end are OK $source = '{$var }'; - $target = "config->context === 'JS' ? escape_js(\$__Context->var ?? '') : htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(\$__Context->var ?? ''); ?>"; $this->assertEquals($target, $this->_parse($source)); // Correct handling of object property and array access $source = '{Context::getRequestVars()->$foo[$bar]}'; - $target = "config->context === 'JS' ? escape_js(Context::getRequestVars()->{\$__Context->foo}[\$__Context->bar]) : htmlspecialchars(Context::getRequestVars()->{\$__Context->foo}[\$__Context->bar], \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(Context::getRequestVars()->{\$__Context->foo}[\$__Context->bar], \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(Context::getRequestVars()->{\$__Context->foo}[\$__Context->bar]); ?>"; $this->assertEquals($target, $this->_parse($source)); // Basic usage of Blade-style double braces $source = '{{ $var }}'; - $target = "config->context === 'JS' ? escape_js(\$__Context->var ?? '') : htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(\$__Context->var ?? ''); ?>"; $this->assertEquals($target, $this->_parse($source)); // Double braces without spaces are OK $source = '{{$var}}'; - $target = "config->context === 'JS' ? escape_js(\$__Context->var ?? '') : htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(\$__Context->var ?? ''); ?>"; $this->assertEquals($target, $this->_parse($source)); // Literal double braces @@ -325,7 +325,7 @@ class TemplateParserV2Test extends \Codeception\Test\Unit // Multiline echo statement $source = '{{ $foo ?' . "\n" . ' date($foo) :' . "\n" . ' toBool($bar) }}'; - $target = "config->context === 'JS' ? escape_js(\$__Context->foo ? date(\$__Context->foo) : toBool(\$__Context->bar)) : htmlspecialchars(\$__Context->foo ?\n date(\$__Context->foo) :\n toBool(\$__Context->bar), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(\$__Context->foo ?\n date(\$__Context->foo) :\n toBool(\$__Context->bar), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(\$__Context->foo ? date(\$__Context->foo) : toBool(\$__Context->bar)); ?>"; $this->assertEquals($target, $this->_parse($source)); } @@ -367,11 +367,11 @@ class TemplateParserV2Test extends \Codeception\Test\Unit $this->assertEquals($target, $this->_parse($source)); $source = '{{ $lang->cmd_hello_world }}'; - $target = "config->context === 'JS' ? escape_js(\$__Context->lang->cmd_hello_world) : (\$__Context->lang->cmd_hello_world); ?>"; + $target = "config->context === 'HTML' ? (\$__Context->lang->cmd_hello_world) : \$this->_v2_escape(\$__Context->lang->cmd_hello_world); ?>"; $this->assertEquals($target, $this->_parse($source)); $source = '{{ $user_lang->user_lang_1234567890 }}'; - $target = "config->context === 'JS' ? escape_js(\$__Context->user_lang->user_lang_1234567890 ?? '') : (\$__Context->user_lang->user_lang_1234567890 ?? ''); ?>"; + $target = "config->context === 'HTML' ? (\$__Context->user_lang->user_lang_1234567890 ?? '') : \$this->_v2_escape(\$__Context->user_lang->user_lang_1234567890 ?? ''); ?>"; $this->assertEquals($target, $this->_parse($source)); // Escape @@ -405,12 +405,12 @@ class TemplateParserV2Test extends \Codeception\Test\Unit // strip_tags $source = '{{ $foo|strip }}'; - $target = "config->context === 'JS' ? escape_js(strip_tags(\$__Context->foo ?? '')) : htmlspecialchars(strip_tags(\$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(strip_tags(\$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(strip_tags(\$__Context->foo ?? '')); ?>"; $this->assertEquals($target, $this->_parse($source)); // strip_tags (alternate name) $source = '{{ $foo|upper|strip_tags }}'; - $target = "config->context === 'JS' ? escape_js(strip_tags(strtoupper(\$__Context->foo ?? ''))) : htmlspecialchars(strip_tags(strtoupper(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(strip_tags(strtoupper(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(strip_tags(strtoupper(\$__Context->foo ?? ''))); ?>"; $this->assertEquals($target, $this->_parse($source)); // Trim @@ -420,12 +420,12 @@ class TemplateParserV2Test extends \Codeception\Test\Unit // URL encode $source = '{{ $foo|trim|urlencode }}'; - $target = "config->context === 'JS' ? escape_js(rawurlencode(trim(\$__Context->foo ?? ''))) : htmlspecialchars(rawurlencode(trim(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(rawurlencode(trim(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(rawurlencode(trim(\$__Context->foo ?? ''))); ?>"; $this->assertEquals($target, $this->_parse($source)); // Lowercase $source = '{{ $foo|trim|lower }}'; - $target = "config->context === 'JS' ? escape_js(strtolower(trim(\$__Context->foo ?? ''))) : htmlspecialchars(strtolower(trim(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(strtolower(trim(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(strtolower(trim(\$__Context->foo ?? ''))); ?>"; $this->assertEquals($target, $this->_parse($source)); // Uppercase @@ -445,37 +445,37 @@ class TemplateParserV2Test extends \Codeception\Test\Unit // Array join (default joiner is comma) $source = '{{ $foo|join }}'; - $target = "config->context === 'JS' ? escape_js(implode(', ', \$__Context->foo ?? '')) : htmlspecialchars(implode(', ', \$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(implode(', ', \$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(implode(', ', \$__Context->foo ?? '')); ?>"; $this->assertEquals($target, $this->_parse($source)); // Array join (custom joiner) $source = '{{ $foo|join:"!@!" }}'; - $target = "config->context === 'JS' ? escape_js(implode(\"!@!\", \$__Context->foo ?? '')) : htmlspecialchars(implode(\"!@!\", \$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(implode(\"!@!\", \$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(implode(\"!@!\", \$__Context->foo ?? '')); ?>"; $this->assertEquals($target, $this->_parse($source)); // Date conversion (default format) $source = '{{ $item->regdate | date }}'; - $target = "config->context === 'JS' ? escape_js(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'Y-m-d H:i:s')) : htmlspecialchars(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'Y-m-d H:i:s'), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'Y-m-d H:i:s'), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'Y-m-d H:i:s')); ?>"; $this->assertEquals($target, $this->_parse($source)); // Date conversion (custom format) $source = "{{ \$item->regdate | date:'n/j H:i' }}"; - $target = "config->context === 'JS' ? escape_js(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'n/j H:i')) : htmlspecialchars(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'n/j H:i'), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'n/j H:i'), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'n/j H:i')); ?>"; $this->assertEquals($target, $this->_parse($source)); // Date conversion (custom format in variable) $source = "{{ \$item->regdate | date:\$format }}"; - $target = "config->context === 'JS' ? escape_js(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), \$__Context->format)) : htmlspecialchars(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), \$__Context->format), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), \$__Context->format), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), \$__Context->format)); ?>"; $this->assertEquals($target, $this->_parse($source)); // Number format $source = '{{ $num | format }}'; - $target = "config->context === 'JS' ? escape_js(number_format(\$__Context->num ?? '')) : htmlspecialchars(number_format(\$__Context->num ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(number_format(\$__Context->num ?? ''), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(number_format(\$__Context->num ?? '')); ?>"; $this->assertEquals($target, $this->_parse($source)); // Number format (alternate name) $source = '{{ $num | number_format }}'; - $target = "config->context === 'JS' ? escape_js(number_format(\$__Context->num ?? '')) : htmlspecialchars(number_format(\$__Context->num ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'HTML' ? htmlspecialchars(number_format(\$__Context->num ?? ''), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(number_format(\$__Context->num ?? '')); ?>"; $this->assertEquals($target, $this->_parse($source)); // Number format (custom format) @@ -820,7 +820,7 @@ class TemplateParserV2Test extends \Codeception\Test\Unit ]); $target = implode("\n", [ "_v2_errorExists('email', 'login')): ?>", - "config->context === 'JS' ? escape_js(\$__Context->message ?? '') : htmlspecialchars(\$__Context->message ?? '', \ENT_QUOTES, 'UTF-8', false); ?>", + "config->context === 'HTML' ? htmlspecialchars(\$__Context->message ?? '', \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(\$__Context->message ?? ''); ?>", '', ]); $this->assertEquals($target, $this->_parse($source)); @@ -999,17 +999,17 @@ class TemplateParserV2Test extends \Codeception\Test\Unit // Lang code with variable as name $source = '@lang($var->name)'; - $target = 'config->context === \'JS\' ? escape_js($this->_v2_lang($__Context->var->name)) : $this->_v2_lang($__Context->var->name); ?>'; + $target = 'config->context === \'HTML\' ? $this->_v2_lang($__Context->var->name) : $this->_v2_escape($this->_v2_lang($__Context->var->name)); ?>'; $this->assertEquals($target, $this->_parse($source)); // Lang code with literal name and variable $source = "@lang('board.cmd_list_items', \$var)"; - $target = "config->context === 'JS' ? escape_js(\$this->_v2_lang('board.cmd_list_items', \$__Context->var)) : \$this->_v2_lang('board.cmd_list_items', \$__Context->var); ?>"; + $target = "config->context === 'HTML' ? \$this->_v2_lang('board.cmd_list_items', \$__Context->var) : \$this->_v2_escape(\$this->_v2_lang('board.cmd_list_items', \$__Context->var)); ?>"; $this->assertEquals($target, $this->_parse($source)); // Lang code with class alias $source = "@use('Rhymix\Framework\Lang', 'Lang')\n" . '

@lang(Lang::getLang())

'; - $target = "\n" . '

config->context === \'JS\' ? escape_js($this->_v2_lang(Rhymix\Framework\Lang::getLang())) : $this->_v2_lang(Rhymix\Framework\Lang::getLang()); ?>

'; + $target = "\n" . '

config->context === \'HTML\' ? $this->_v2_lang(Rhymix\Framework\Lang::getLang()) : $this->_v2_escape($this->_v2_lang(Rhymix\Framework\Lang::getLang())); ?>

'; $this->assertEquals($target, $this->_parse($source)); // Dump one variable @@ -1024,12 +1024,12 @@ class TemplateParserV2Test extends \Codeception\Test\Unit // URL $source = "@url(['mid' => 'foo', 'act' => 'dispBoardWrite'])"; - $target = "config->context === 'JS' ? escape_js(getNotEncodedUrl(['mid' => 'foo', 'act' => 'dispBoardWrite'])) : getUrl(['mid' => 'foo', 'act' => 'dispBoardWrite']); ?>"; + $target = "config->context === 'HTML' ? getUrl(['mid' => 'foo', 'act' => 'dispBoardWrite']) : \$this->_v2_escape(getNotEncodedUrl(['mid' => 'foo', 'act' => 'dispBoardWrite'])); ?>"; $this->assertEquals($target, $this->_parse($source)); // URL old-style with variables $source = "@url('', 'mid', \$mid, 'act', \$act])"; - $target = "config->context === 'JS' ? escape_js(getNotEncodedUrl('', 'mid', \$__Context->mid, 'act', \$__Context->act])) : getUrl('', 'mid', \$__Context->mid, 'act', \$__Context->act]); ?>"; + $target = "config->context === 'HTML' ? getUrl('', 'mid', \$__Context->mid, 'act', \$__Context->act]) : \$this->_v2_escape(getNotEncodedUrl('', 'mid', \$__Context->mid, 'act', \$__Context->act])); ?>"; $this->assertEquals($target, $this->_parse($source)); } From 49f57980dd78f14717e8edcee5c7c6f59280ac3c Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 19 Mar 2025 00:16:08 +0900 Subject: [PATCH 14/41] Add unit test for inline styles in template v2 --- tests/_data/template/v2example.compiled.html | 4 ++++ tests/_data/template/v2example.executed.html | 4 ++++ tests/_data/template/v2example.html | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/tests/_data/template/v2example.compiled.html b/tests/_data/template/v2example.compiled.html index 2ad7c4bb9..1c9dce8a3 100644 --- a/tests/_data/template/v2example.compiled.html +++ b/tests/_data/template/v2example.compiled.html @@ -64,3 +64,7 @@ const foo = 'config->context === 'HTML' ? htmlspecialchars($__Context->foo ?? '', \ENT_QUOTES, 'UTF-8', false) : $this->_v2_escape($__Context->foo ?? ''); ?>'; const bar = config->context === 'JS' ? json_encode($__Context->bar, self::$_json_options2) : htmlspecialchars(json_encode($__Context->bar, self::$_json_options), \ENT_QUOTES, 'UTF-8', false); ?>; config->context = 'HTML'; ?> + +config->context = 'CSS'; ?>> + body { background-color: config->context === 'HTML' ? htmlspecialchars('#ffffff', \ENT_QUOTES, 'UTF-8', false) : $this->_v2_escape('#ffffff'); ?>; } +config->context = 'HTML'; ?> diff --git a/tests/_data/template/v2example.executed.html b/tests/_data/template/v2example.executed.html index 1baff6af2..3224403f0 100644 --- a/tests/_data/template/v2example.executed.html +++ b/tests/_data/template/v2example.executed.html @@ -60,3 +60,7 @@ const foo = 'FOOFOO\u003C\u0022FOO\u0022\u003EBAR'; const bar = ["Rhy","miX","is","da","BEST!"]; + + diff --git a/tests/_data/template/v2example.html b/tests/_data/template/v2example.html index 2b57d75d1..8335308bf 100644 --- a/tests/_data/template/v2example.html +++ b/tests/_data/template/v2example.html @@ -64,3 +64,7 @@ const foo = '{{ $foo }}'; const bar = @json($bar); + + From 01f34781df5474e4feebff6a1afccc2303316c9f Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 19 Mar 2025 00:42:01 +0900 Subject: [PATCH 15/41] Don't parse template v1 syntax (single braces) in CSS/JS context --- common/framework/parsers/template/TemplateParser_v2.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common/framework/parsers/template/TemplateParser_v2.php b/common/framework/parsers/template/TemplateParser_v2.php index 926c3e0f4..be9d4088c 100644 --- a/common/framework/parsers/template/TemplateParser_v2.php +++ b/common/framework/parsers/template/TemplateParser_v2.php @@ -822,6 +822,13 @@ class TemplateParser_v2 return $this->_arrangeOutputFilters($match); }, $content); + // Exclude {single} curly braces in non-HTML contexts. + $content = preg_replace_callback('#(<\?php \$this->config->context = \'(?:CSS|JS)\'; \?>)(.*?)(<\?php \$this->config->context = \'HTML\'; \?>)#s', function($match) { + $warning = ''; + $match[2] = preg_replace('#(? Date: Wed, 19 Mar 2025 11:23:42 +0900 Subject: [PATCH 16/41] Use regular escape() for CSS contexts --- common/framework/Template.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/common/framework/Template.php b/common/framework/Template.php index b0e2c9a06..49ffb831b 100644 --- a/common/framework/Template.php +++ b/common/framework/Template.php @@ -959,14 +959,12 @@ class Template * Contextual escape function for v2. * * @param string $str - * @param string $type * @return string */ - protected function _v2_escape(string $str, string $type = ''): string + protected function _v2_escape(string $str): string { switch ($this->config->context) { - case 'CSS': return escape_css($str); case 'JS': return escape_js($str); default: return escape($str); } From 04a7734b2e98be727f0f78d643ea8882e837850a Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 19 Mar 2025 11:23:56 +0900 Subject: [PATCH 17/41] Add unit test for contextual escape --- .../_data/template/v2contextual.executed.html | 27 +++++++++++++++++ tests/_data/template/v2contextual.html | 30 +++++++++++++++++++ .../parsers/TemplateParserV2Test.php | 18 +++++++++++ 3 files changed, 75 insertions(+) create mode 100644 tests/_data/template/v2contextual.executed.html create mode 100644 tests/_data/template/v2contextual.html diff --git a/tests/_data/template/v2contextual.executed.html b/tests/_data/template/v2contextual.executed.html new file mode 100644 index 000000000..69847da11 --- /dev/null +++ b/tests/_data/template/v2contextual.executed.html @@ -0,0 +1,27 @@ + + +Hello <"world"> ('string') variable.jpg

+ +

+ +Hello <"world"> ('string') variable.jpg +

+ + + + + +
    +
  • Hello <"world"> ('string') variable.jpg
  • +
  • Hello <"world"> ('string') variable.jpg
  • +
diff --git a/tests/_data/template/v2contextual.html b/tests/_data/template/v2contextual.html new file mode 100644 index 000000000..7ca89745a --- /dev/null +++ b/tests/_data/template/v2contextual.html @@ -0,0 +1,30 @@ +@version(2) + +
+ {{ $var }} +

+ +

+ + {{ $var }} + +

+ + + + + +
    +
  • {{ $var }}
  • +
  • {{ $var|noescape }}
  • +
diff --git a/tests/unit/framework/parsers/TemplateParserV2Test.php b/tests/unit/framework/parsers/TemplateParserV2Test.php index 4892fbb87..75a9e4bb5 100644 --- a/tests/unit/framework/parsers/TemplateParserV2Test.php +++ b/tests/unit/framework/parsers/TemplateParserV2Test.php @@ -1220,6 +1220,24 @@ class TemplateParserV2Test extends \Codeception\Test\Unit $this->assertStringContainsString('/tests/_data/template/css/style.scss', array_first($list)['file']); } + public function testCompileContextualEscape() + { + // Contextual escape + $tmpl = new \Rhymix\Framework\Template('./tests/_data/template', 'v2contextual.html'); + $tmpl->disableCache(); + $tmpl->setVars([ + 'var' => 'Hello <"world"> (\'string\') variable.jpg' + ]); + + $executed_output = $tmpl->compile(); + //Rhymix\Framework\Storage::write(\RX_BASEDIR . 'tests/_data/template/v2contextual.executed.html', $executed_output); + $expected = file_get_contents(\RX_BASEDIR . 'tests/_data/template/v2contextual.executed.html'); + $this->assertEquals( + $this->_normalizeWhitespace($expected), + $this->_normalizeWhitespace($executed_output) + ); + } + public function testCompileLang() { // Lang From 3e052d2d0087369f95855006897bb4c4435eab9a Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 19 Mar 2025 12:44:17 +0900 Subject: [PATCH 18/41] Improve detection of template v1-style syntax in CSS/JS contexts --- common/framework/parsers/template/TemplateParser_v2.php | 6 ++++-- tests/_data/template/v2contextual.executed.html | 9 ++++----- tests/_data/template/v2contextual.html | 9 ++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/common/framework/parsers/template/TemplateParser_v2.php b/common/framework/parsers/template/TemplateParser_v2.php index be9d4088c..84b010e5d 100644 --- a/common/framework/parsers/template/TemplateParser_v2.php +++ b/common/framework/parsers/template/TemplateParser_v2.php @@ -824,8 +824,10 @@ class TemplateParser_v2 // Exclude {single} curly braces in non-HTML contexts. $content = preg_replace_callback('#(<\?php \$this->config->context = \'(?:CSS|JS)\'; \?>)(.*?)(<\?php \$this->config->context = \'HTML\'; \?>)#s', function($match) { - $warning = ''; - $match[2] = preg_replace('#(?' : ''; + return '{' . $warning . $m[1] . '}'; + }, $match[2]); return $match[1] . $match[2] . $match[3]; }, $content); diff --git a/tests/_data/template/v2contextual.executed.html b/tests/_data/template/v2contextual.executed.html index 69847da11..f36d54714 100644 --- a/tests/_data/template/v2contextual.executed.html +++ b/tests/_data/template/v2contextual.executed.html @@ -8,17 +8,16 @@ Hello <"world"> ('string') variable.jpg