', '', $ret);
- }
- */
- $ret .= '';
- return $ret;
- }
-
- /**
- * Render support for XoopsFormFile
- *
- * @param XoopsFormFile $element form element
- *
- * @return string rendered form element
- */
- public function renderFormFile(XoopsFormFile $element)
- {
- return '
'
- . '
getExtra() . '>'
- . '
';
- }
-
- /**
- * Render support for XoopsFormLabel
- *
- * @param XoopsFormLabel $element form element
- *
- * @return string rendered form element
- */
- public function renderFormLabel(XoopsFormLabel $element)
- {
- return '
' . $element->getValue() . '
';
- }
-
- /**
- * Render support for XoopsFormPassword
- *
- * @param XoopsFormPassword $element form element
- *
- * @return string rendered form element
- */
- public function renderFormPassword(XoopsFormPassword $element)
- {
- return '
getExtra() . ' ' . ($element->autoComplete ? '' : 'autocomplete="off" ') . '/>';
- }
-
- /**
- * Render support for XoopsFormRadio
- *
- * @param XoopsFormRadio $element form element
- *
- * @return string rendered form element
- */
- public function renderFormRadio(XoopsFormRadio $element)
- {
-
- $elementName = $element->getName();
- $elementId = $elementName;
-
- switch ((int) ($element->columns)) {
- case 0:
- return $this->renderCheckedInline($element, 'radio', $elementId, $elementName);
- case 1:
- return $this->renderCheckedOneColumn($element, 'radio', $elementId, $elementName);
- default:
- return $this->renderCheckedColumnar($element, 'radio', $elementId, $elementName);
- }
- }
-
- /**
- * Render support for XoopsFormSelect
- *
- * @param XoopsFormSelect $element form element
- *
- * @return string rendered form element
- */
- public function renderFormSelect(XoopsFormSelect $element)
- {
- $ele_name = $element->getName();
- $ele_title = $element->getTitle();
- $ele_value = $element->getValue();
- $ele_options = $element->getOptions();
- $ret = '
getExtra();
- if ($element->isMultiple() != false) {
- $ret .= ' name="' . $ele_name . '[]" id="' . $ele_name . '" title="' . $ele_title
- . '" multiple="multiple">';
- } else {
- $ret .= ' name="' . $ele_name . '" id="' . $ele_name . '" title="' . $ele_title . '">';
- }
- foreach ($ele_options as $value => $name) {
- $ret .= ' 0 && in_array($value, $ele_value)) {
- $ret .= ' selected';
- }
- $ret .= '>' . $name . ' ';
- }
- $ret .= ' ';
-
- return $ret;
- }
- /**
- * Render support for XoopsFormText
- *
- * @param XoopsFormText $element form element
- *
- * @return string rendered form element
- */
- public function renderFormText(XoopsFormText $element)
- {
- return "
getExtra() . '>';
- }
-
- /**
- * Render support for XoopsFormTextArea
- *
- * @param XoopsFormTextArea $element form element
- *
- * @return string rendered form element
- */
- public function renderFormTextArea(XoopsFormTextArea $element)
- {
- return "
';
- }
-
- /**
- * Render support for XoopsFormTextDateSelect
- *
- * @param XoopsFormTextDateSelect $element form element
- *
- * @return string rendered form element
- */
- public function renderFormTextDateSelect(XoopsFormTextDateSelect $element)
- {
- static $included = false;
- if (file_exists(XOOPS_ROOT_PATH . '/language/' . $GLOBALS['xoopsConfig']['language'] . '/calendar.php')) {
- include_once XOOPS_ROOT_PATH . '/language/' . $GLOBALS['xoopsConfig']['language'] . '/calendar.php';
- } else {
- include_once XOOPS_ROOT_PATH . '/language/english/calendar.php';
- }
-
- $ele_name = $element->getName();
- $ele_value = $element->getValue(false);
- if (is_string($ele_value)) {
- $display_value = $ele_value;
- $ele_value = time();
- } elseif ($ele_value === 0) {
- $display_value = '';
- $ele_value = time();
- } else {
- $display_value = date(_SHORTDATESTRING, $ele_value);
- }
-
- $jstime = formatTimestamp($ele_value, 'm/d/Y');
- if (isset($GLOBALS['xoTheme']) && is_object($GLOBALS['xoTheme'])) {
- $GLOBALS['xoTheme']->addScript('include/calendar.js');
- $GLOBALS['xoTheme']->addStylesheet('include/calendar-blue.css');
- if (!$included) {
- $included = true;
- $GLOBALS['xoTheme']->addScript('', '', '
- var calendar = null;
-
- function selected(cal, date)
- {
- cal.sel.value = date;
- }
-
- function closeHandler(cal)
- {
- cal.hide();
- Calendar.removeEvent(document, "mousedown", checkCalendar);
- }
-
- function checkCalendar(ev)
- {
- var el = Calendar.is_ie ? Calendar.getElement(ev) : Calendar.getTargetElement(ev);
- for (; el != null; el = el.parentNode)
- if (el == calendar.element || el.tagName == "A") break;
- if (el == null) {
- calendar.callCloseHandler(); Calendar.stopEvent(ev);
- }
- }
- function showCalendar(id)
- {
- var el = xoopsGetElementById(id);
- if (calendar != null) {
- calendar.hide();
- } else {
- var cal = new Calendar(true, "' . $jstime . '", selected, closeHandler);
- calendar = cal;
- cal.setRange(1900, 2100);
- calendar.create();
- }
- calendar.sel = el;
- calendar.parseDate(el.value);
- calendar.showAtElement(el);
- Calendar.addEvent(document, "mousedown", checkCalendar);
-
- return false;
- }
-
- Calendar._DN = new Array
- ("' . _CAL_SUNDAY . '",
- "' . _CAL_MONDAY . '",
- "' . _CAL_TUESDAY . '",
- "' . _CAL_WEDNESDAY . '",
- "' . _CAL_THURSDAY . '",
- "' . _CAL_FRIDAY . '",
- "' . _CAL_SATURDAY . '",
- "' . _CAL_SUNDAY . '");
- Calendar._MN = new Array
- ("' . _CAL_JANUARY . '",
- "' . _CAL_FEBRUARY . '",
- "' . _CAL_MARCH . '",
- "' . _CAL_APRIL . '",
- "' . _CAL_MAY . '",
- "' . _CAL_JUNE . '",
- "' . _CAL_JULY . '",
- "' . _CAL_AUGUST . '",
- "' . _CAL_SEPTEMBER . '",
- "' . _CAL_OCTOBER . '",
- "' . _CAL_NOVEMBER . '",
- "' . _CAL_DECEMBER . '");
-
- Calendar._TT = {};
- Calendar._TT["TOGGLE"] = "' . _CAL_TGL1STD . '";
- Calendar._TT["PREV_YEAR"] = "' . _CAL_PREVYR . '";
- Calendar._TT["PREV_MONTH"] = "' . _CAL_PREVMNTH . '";
- Calendar._TT["GO_TODAY"] = "' . _CAL_GOTODAY . '";
- Calendar._TT["NEXT_MONTH"] = "' . _CAL_NXTMNTH . '";
- Calendar._TT["NEXT_YEAR"] = "' . _CAL_NEXTYR . '";
- Calendar._TT["SEL_DATE"] = "' . _CAL_SELDATE . '";
- Calendar._TT["DRAG_TO_MOVE"] = "' . _CAL_DRAGMOVE . '";
- Calendar._TT["PART_TODAY"] = "(' . _CAL_TODAY . ')";
- Calendar._TT["MON_FIRST"] = "' . _CAL_DISPM1ST . '";
- Calendar._TT["SUN_FIRST"] = "' . _CAL_DISPS1ST . '";
- Calendar._TT["CLOSE"] = "' . _CLOSE . '";
- Calendar._TT["TODAY"] = "' . _CAL_TODAY . '";
-
- // date formats
- Calendar._TT["DEF_DATE_FORMAT"] = "' . _SHORTDATESTRING . '";
- Calendar._TT["TT_DATE_FORMAT"] = "' . _SHORTDATESTRING . '";
-
- Calendar._TT["WK"] = "";
- ');
- }
- }
- return '
';
- }
-
- /**
- * Render support for XoopsThemeForm
- *
- * @param XoopsThemeForm $form form to render
- *
- * @return string rendered form
- */
- public function renderThemeForm(XoopsThemeForm $form)
- {
- $ele_name = $form->getName();
-
- $ret = '
';
- $ret .= $form->renderValidationJS(true);
-
- return $ret;
- }
-
- /**
- * Support for themed addBreak
- *
- * @param XoopsThemeForm $form
- * @param string $extra pre-rendered content for break row
- * @param string $class class for row
- *
- * @return void
- */
- public function addThemeFormBreak(XoopsThemeForm $form, $extra, $class)
- {
- $class = ($class != '') ? preg_replace('/[^A-Za-z0-9\s\s_-]/i', '', $class) : '';
- $form->addElement('
' . $extra . '
');
- }
-}
diff --git a/htdocs/class/xoopseditor/tinymce7/js/tinymce/plugins/xoopsimagemanager/XoopsFormRendererBootstrap5.php b/htdocs/class/xoopseditor/tinymce7/js/tinymce/plugins/xoopsimagemanager/XoopsFormRendererBootstrap5.php
deleted file mode 100644
index c0a5f34a6..000000000
--- a/htdocs/class/xoopseditor/tinymce7/js/tinymce/plugins/xoopsimagemanager/XoopsFormRendererBootstrap5.php
+++ /dev/null
@@ -1,771 +0,0 @@
-, updated for BS5
- * @copyright 2000-2026 XOOPS Project[](https://xoops.org)
- * @license GNU GPL 2 or later[](https://www.gnu.org/licenses/gpl-2.0.html)
- */
-class XoopsFormRendererBootstrap5 implements XoopsFormRendererInterface
-{
- /**
- * Render support for XoopsFormButton
- *
- * @param XoopsFormButton $element form element
- *
- * @return string rendered form element
- */
- public function renderFormButton(XoopsFormButton $element)
- {
- return '
getExtra() . '>' . $element->getValue() . ' ';
- }
-
- /**
- * Render support for XoopsFormButtonTray
- *
- * @param XoopsFormButtonTray $element form element
- *
- * @return string rendered form element
- */
- public function renderFormButtonTray(XoopsFormButtonTray $element)
- {
- $ret = '';
- if ($element->_showDelete) {
- $ret .= '
' . _DELETE
- . ' ';
- }
- $ret .= '
'
- . _CANCEL . ' '
- . '
' . _RESET . ' '
- . '
getExtra()
- . '>' . $element->getValue() . ' ';
-
- return $ret;
- }
-
- /**
- * Render support for XoopsFormCheckBox
- *
- * @param XoopsFormCheckBox $element form element
- *
- * @return string rendered form element
- */
- public function renderFormCheckBox(XoopsFormCheckBox $element)
- {
- $elementName = $element->getName();
- $elementId = $elementName;
- $elementOptions = $element->getOptions();
- if (count($elementOptions) > 1 && substr($elementName, -2, 2) !== '[]') {
- $elementName .= '[]';
- $element->setName($elementName);
- }
-
- switch ((int) ($element->columns)) {
- case 0:
- return $this->renderCheckedInline($element, 'checkbox', $elementId, $elementName);
- case 1:
- return $this->renderCheckedOneColumn($element, 'checkbox', $elementId, $elementName);
- default:
- return $this->renderCheckedColumnar($element, 'checkbox', $elementId, $elementName);
- }
- }
-
- /**
- * Render a inline checkbox or radio element
- *
- * @param XoopsFormCheckBox|XoopsFormRadio $element element being rendered
- * @param string $type 'checkbox' or 'radio'
- * @param string $elementId input 'id' attribute of element
- * @param string $elementName input 'name' attribute of element
- * @return string
- */
- protected function renderCheckedInline($element, $type, $elementId, $elementName)
- {
- $ret = '';
-
- $idSuffix = 0;
- $elementValue = $element->getValue();
- $elementOptions = $element->getOptions();
- foreach ($elementOptions as $value => $name) {
- ++$idSuffix;
-
- $ret .= '
';
- $ret .= " getExtra() . '>';
- $ret .= '' . $name . $element->getDelimeter() . ' ';
- $ret .= '
';
- }
-
- return $ret;
- }
-
- /**
- * Render a single column checkbox or radio element
- *
- * @param XoopsFormCheckBox|XoopsFormRadio $element element being rendered
- * @param string $type 'checkbox' or 'radio'
- * @param string $elementId input 'id' attribute of element
- * @param string $elementName input 'name' attribute of element
- * @return string
- */
- protected function renderCheckedOneColumn($element, $type, $elementId, $elementName)
- {
- $ret = '';
-
- $idSuffix = 0;
- $elementValue = $element->getValue();
- $elementOptions = $element->getOptions();
- foreach ($elementOptions as $value => $name) {
- ++$idSuffix;
- $ret .= '
';
- $ret .= '';
- $ret .= " getExtra() . '>' . $name . $element->getDelimeter();
- $ret .= ' ';
- $ret .= '
';
- }
-
- return $ret;
- }
-
- /**
- * Render a multicolumn checkbox or radio element
- *
- * @param XoopsFormCheckBox|XoopsFormRadio $element element being rendered
- * @param string $type 'checkbox' or 'radio'
- * @param string $elementId input 'id' attribute of element
- * @param string $elementName input 'name' attribute of element
- * @return string
- */
- protected function renderCheckedColumnar($element, $type, $elementId, $elementName)
- {
- $ret = '';
-
- $idSuffix = 0;
- $elementValue = $element->getValue();
- $elementOptions = $element->getOptions();
- foreach ($elementOptions as $value => $name) {
- ++$idSuffix;
-
- $ret .= '
';
- $ret .= " getExtra() . '>';
- $ret .= '' . $name . $element->getDelimeter() . ' ';
- $ret .= '
';
- }
-
- return $ret;
- }
-
- /**
- * Render support for XoopsFormColorPicker
- *
- * @param XoopsFormColorPicker $element form element
- *
- * @return string rendered form element
- */
- public function renderFormColorPicker(XoopsFormColorPicker $element)
- {
- if (isset($GLOBALS['xoTheme'])) {
- $GLOBALS['xoTheme']->addScript('include/spectrum.js');
- $GLOBALS['xoTheme']->addStylesheet('include/spectrum.css');
- } else {
- echo '';
- echo '
';
- }
- return '
getExtra() . '>';
- }
-
- /**
- * Render support for XoopsFormDhtmlTextArea
- *
- * @param XoopsFormDhtmlTextArea $element form element
- *
- * @return string rendered form element
- */
- public function renderFormDhtmlTextArea(XoopsFormDhtmlTextArea $element)
- {
- static $js_loaded;
-
- xoops_loadLanguage('formdhtmltextarea');
- $ret = '';
- // actions
- $ret .= $this->renderFormDhtmlTAXoopsCode($element) . "
\n";
- // fonts
- $ret .= $this->renderFormDhtmlTATypography($element);
- // length checker
-
- $ret .= "
\n";
- // the textarea box
- $ret .= "
\n";
-
- if (empty($element->skipPreview)) {
- if (empty($GLOBALS['xoTheme'])) {
- $element->js .= implode('', file(XOOPS_ROOT_PATH . '/class/textsanitizer/image/image.js'));
- } else {
- $GLOBALS['xoTheme']->addScript(
- '/class/textsanitizer/image/image.js',
- ['type' => 'text/javascript'],
- );
- }
- $button = "
getName() . "','" . XOOPS_URL . "/images', " . (int) $element->doHtml . ", '"
- . $GLOBALS['xoopsSecurity']->createToken() . "')\" title='" . _PREVIEW . "'>" . _PREVIEW . " ";
-
- $ret .= '
' . "
"
- . '
' . ' ' . $button . ' '
- . " " . _XOOPS_FORM_PREVIEW_CONTENT
- . '
' . ' ' . '
';
- }
- // Load javascript
- if (empty($js_loaded)) {
- $javascript = ($element->js ? '' : '')
- . '';
- $ret = $javascript . $ret;
- $js_loaded = true;
- }
-
- return $ret;
- }
-
- /**
- * Render xoopscode buttons for editor, include calling text sanitizer extensions
- *
- * @param XoopsFormDhtmlTextArea $element form element
- *
- * @return string rendered buttons for xoopscode assistance
- */
- protected function renderFormDhtmlTAXoopsCode(XoopsFormDhtmlTextArea $element)
- {
- $textarea_id = $element->getName();
- $code = '';
- $code .= "
";
- $code .= " ";
- $code .= " ";
- $code .= " ";
- $code .= " Manager ";
- $code .= " ";
-
- $myts = \MyTextSanitizer::getInstance();
-
- $extensions = array_filter($myts->config['extensions']);
- foreach (array_keys($extensions) as $key) {
- $extension = $myts->loadExtension($key);
- @[$encode, $js] = $extension->encode($textarea_id);
- if (empty($encode)) {
- continue;
- }
- // TODO - MyTextSanitizer button rendering should go through XoopsFormRenderer
- $encode = str_replace('btn-default', 'btn-secondary', $encode);
-
- $code .= $encode;
- if (!empty($js)) {
- $element->js .= $js;
- }
- }
- $code .= " ";
- $code .= " ";
- $code .= "
";
-
- $xoopsPreload = XoopsPreload::getInstance();
- $xoopsPreload->triggerEvent('core.class.xoopsform.formdhtmltextarea.codeicon', [&$code]);
-
- return $code;
- }
-
- /**
- * Render typography controls for editor (font, size, color)
- *
- * @param XoopsFormDhtmlTextArea $element form element
- *
- * @return string rendered typography controls
- */
- protected function renderFormDhtmlTATypography(XoopsFormDhtmlTextArea $element)
- {
- $textarea_id = $element->getName();
- $hiddentext = $element->_hiddenText;
-
- $fontarray = !empty($GLOBALS['formtextdhtml_fonts']) ? $GLOBALS['formtextdhtml_fonts'] : [
- 'Arial',
- 'Courier',
- 'Georgia',
- 'Helvetica',
- 'Impact',
- 'Verdana',
- 'Haettenschweiler',
- ];
-
- $colorArray = [
- 'Black' => '000000',
- 'Blue' => '38AAFF',
- 'Brown' => '987857',
- 'Green' => '79D271',
- 'Grey' => '888888',
- 'Orange' => 'FFA700',
- 'Paper' => 'E0E0E0',
- 'Purple' => '363E98',
- 'Red' => 'FF211E',
- 'White' => 'FEFEFE',
- 'Yellow' => 'FFD628',
- ];
-
- $fontStr = '
';
- $fontStr .= '
'
- . ''
- . ' '
- . '
';
-
- $fontStr .= '
'
- . ''
- . ' '
- . '
';
-
- $fontStr .= '
'
- . ''
- . ' '
- . '
';
- $fontStr .= '
';
-
- $styleStr = "
";
- $styleStr .= " ";
- $styleStr .= " ";
- $styleStr .= "" . ' ';
- $styleStr .= "" . ' ';
- $styleStr .= "
";
-
- $alignStr = "
";
- $alignStr .= " ";
- $alignStr .= " ";
- $alignStr .= " ";
- $alignStr .= "
";
-
- $fontStr .= " {$styleStr} {$alignStr} \n";
-
- $fontStr .= "
getName() . "', '" . ($element->configs['maxlength'] ?? '') . "', '"
- . _XOOPS_FORM_ALT_LENGTH . "', '" . _XOOPS_FORM_ALT_LENGTH_MAX . "');\" title='"
- . _XOOPS_FORM_ALT_CHECKLENGTH . "'> ";
- $fontStr .= "
";
-
- return $fontStr;
- }
-
- /**
- * Render support for XoopsFormElementTray
- *
- * @param XoopsFormElementTray $element form element
- *
- * @return string rendered form element
- */
- public function renderFormElementTray(XoopsFormElementTray $element)
- {
- $count = 0;
- $ret = '
';
- foreach ($element->getElements() as $ele) {
- if ($count > 0) {
- $ret .= $element->getDelimeter();
- }
- if ($ele->getCaption() != '') {
- $ret .= $ele->getCaption() . ' ';
- }
- $ret .= $ele->render() . NWLINE;
- if (!$ele->isHidden()) {
- ++$count;
- }
- }
- $ret .= ' ';
- return $ret;
- }
-
- /**
- * Render support for XoopsFormFile
- *
- * @param XoopsFormFile $element form element
- *
- * @return string rendered form element
- */
- public function renderFormFile(XoopsFormFile $element)
- {
- return '
'
- . '
getExtra() . '>'
- . '
';
- }
-
- /**
- * Render support for XoopsFormLabel
- *
- * @param XoopsFormLabel $element form element
- *
- * @return string rendered form element
- */
- public function renderFormLabel(XoopsFormLabel $element)
- {
- return '
' . $element->getValue() . '
';
- }
-
- /**
- * Render support for XoopsFormPassword
- *
- * @param XoopsFormPassword $element form element
- *
- * @return string rendered form element
- */
- public function renderFormPassword(XoopsFormPassword $element)
- {
- return '
getExtra() . ' ' . ($element->autoComplete ? '' : 'autocomplete="off" ') . '/>';
- }
-
- /**
- * Render support for XoopsFormRadio
- *
- * @param XoopsFormRadio $element form element
- *
- * @return string rendered form element
- */
- public function renderFormRadio(XoopsFormRadio $element)
- {
- $elementName = $element->getName();
- $elementId = $elementName;
-
- switch ((int) ($element->columns)) {
- case 0:
- return $this->renderCheckedInline($element, 'radio', $elementId, $elementName);
- case 1:
- return $this->renderCheckedOneColumn($element, 'radio', $elementId, $elementName);
- default:
- return $this->renderCheckedColumnar($element, 'radio', $elementId, $elementName);
- }
- }
-
- /**
- * Render support for XoopsFormSelect
- *
- * @param XoopsFormSelect $element form element
- *
- * @return string rendered form element
- */
- public function renderFormSelect(XoopsFormSelect $element)
- {
- $ele_name = $element->getName();
- $ele_title = $element->getTitle();
- $ele_value = $element->getValue();
- $ele_options = $element->getOptions();
- $ret = '
getExtra();
- if ($element->isMultiple() != false) {
- $ret .= ' name="' . $ele_name . '[]" id="' . $ele_name . '" title="' . $ele_title
- . '" multiple>';
- } else {
- $ret .= ' name="' . $ele_name . '" id="' . $ele_name . '" title="' . $ele_title . '">';
- }
- foreach ($ele_options as $value => $name) {
- $ret .= ' 0 && in_array($value, $ele_value)) {
- $ret .= ' selected';
- }
- $ret .= '>' . $name . ' ';
- }
- $ret .= ' ';
-
- return $ret;
- }
-
- /**
- * Render support for XoopsFormText
- *
- * @param XoopsFormText $element form element
- *
- * @return string rendered form element
- */
- public function renderFormText(XoopsFormText $element)
- {
- return "
getExtra() . '>';
- }
-
- /**
- * Render support for XoopsFormTextArea
- *
- * @param XoopsFormTextArea $element form element
- *
- * @return string rendered form element
- */
- public function renderFormTextArea(XoopsFormTextArea $element)
- {
- return "
';
- }
-
- /**
- * Render support for XoopsFormTextDateSelect
- *
- * @param XoopsFormTextDateSelect $element form element
- *
- * @return string rendered form element
- */
- public function renderFormTextDateSelect(XoopsFormTextDateSelect $element)
- {
- static $included = false;
- if (file_exists(XOOPS_ROOT_PATH . '/language/' . $GLOBALS['xoopsConfig']['language'] . '/calendar.php')) {
- include_once XOOPS_ROOT_PATH . '/language/' . $GLOBALS['xoopsConfig']['language'] . '/calendar.php';
- } else {
- include_once XOOPS_ROOT_PATH . '/language/english/calendar.php';
- }
-
- $ele_name = $element->getName();
- $ele_value = $element->getValue(false);
- if (is_string($ele_value)) {
- $display_value = $ele_value;
- $ele_value = time();
- } elseif ($ele_value === 0) {
- $display_value = '';
- $ele_value = time();
- } else {
- $display_value = date(_SHORTDATESTRING, $ele_value);
- }
-
- $jstime = formatTimestamp($ele_value, 'm/d/Y');
- if (isset($GLOBALS['xoTheme']) && is_object($GLOBALS['xoTheme'])) {
- $GLOBALS['xoTheme']->addScript('include/calendar.js');
- $GLOBALS['xoTheme']->addStylesheet('include/calendar-blue.css');
- if (!$included) {
- $included = true;
- $GLOBALS['xoTheme']->addScript('', '', '
- var calendar = null;
-
- function selected(cal, date)
- {
- cal.sel.value = date;
- }
-
- function closeHandler(cal)
- {
- cal.hide();
- Calendar.removeEvent(document, "mousedown", checkCalendar);
- }
-
- function checkCalendar(ev)
- {
- var el = Calendar.is_ie ? Calendar.getElement(ev) : Calendar.getTargetElement(ev);
- for (; el != null; el = el.parentNode)
- if (el == calendar.element || el.tagName == "A") break;
- if (el == null) {
- calendar.callCloseHandler(); Calendar.stopEvent(ev);
- }
- }
- function showCalendar(id)
- {
- var el = xoopsGetElementById(id);
- if (calendar != null) {
- calendar.hide();
- } else {
- var cal = new Calendar(true, "' . $jstime . '", selected, closeHandler);
- calendar = cal;
- cal.setRange(1900, 2100);
- calendar.create();
- }
- calendar.sel = el;
- calendar.parseDate(el.value);
- calendar.showAtElement(el);
- Calendar.addEvent(document, "mousedown", checkCalendar);
-
- return false;
- }
-
- Calendar._DN = new Array
- ("' . _CAL_SUNDAY . '",
- "' . _CAL_MONDAY . '",
- "' . _CAL_TUESDAY . '",
- "' . _CAL_WEDNESDAY . '",
- "' . _CAL_THURSDAY . '",
- "' . _CAL_FRIDAY . '",
- "' . _CAL_SATURDAY . '",
- "' . _CAL_SUNDAY . '");
- Calendar._MN = new Array
- ("' . _CAL_JANUARY . '",
- "' . _CAL_FEBRUARY . '",
- "' . _CAL_MARCH . '",
- "' . _CAL_APRIL . '",
- "' . _CAL_MAY . '",
- "' . _CAL_JUNE . '",
- "' . _CAL_JULY . '",
- "' . _CAL_AUGUST . '",
- "' . _CAL_SEPTEMBER . '",
- "' . _CAL_OCTOBER . '",
- "' . _CAL_NOVEMBER . '",
- "' . _CAL_DECEMBER . '");
-
- Calendar._TT = {};
- Calendar._TT["TOGGLE"] = "' . _CAL_TGL1STD . '";
- Calendar._TT["PREV_YEAR"] = "' . _CAL_PREVYR . '";
- Calendar._TT["PREV_MONTH"] = "' . _CAL_PREVMNTH . '";
- Calendar._TT["GO_TODAY"] = "' . _CAL_GOTODAY . '";
- Calendar._TT["NEXT_MONTH"] = "' . _CAL_NXTMNTH . '";
- Calendar._TT["NEXT_YEAR"] = "' . _CAL_NEXTYR . '";
- Calendar._TT["SEL_DATE"] = "' . _CAL_SELDATE . '";
- Calendar._TT["DRAG_TO_MOVE"] = "' . _CAL_DRAGMOVE . '";
- Calendar._TT["PART_TODAY"] = "(' . _CAL_TODAY . ')";
- Calendar._TT["MON_FIRST"] = "' . _CAL_DISPM1ST . '";
- Calendar._TT["SUN_FIRST"] = "' . _CAL_DISPS1ST . '";
- Calendar._TT["CLOSE"] = "' . _CLOSE . '";
- Calendar._TT["TODAY"] = "' . _CAL_TODAY . '";
-
- // date formats
- Calendar._TT["DEF_DATE_FORMAT"] = "' . _SHORTDATESTRING . '";
- Calendar._TT["TT_DATE_FORMAT"] = "' . _SHORTDATESTRING . '";
-
- Calendar._TT["WK"] = "";
- ');
- }
- }
- return '
'
- . ' getExtra() . '>'
- . ''
- . ' '
- . ' '
- . '
';
- }
-
- /**
- * Render support for XoopsThemeForm
- *
- * @param XoopsThemeForm $form form to render
- *
- * @return string rendered form
- */
- public function renderThemeForm(XoopsThemeForm $form)
- {
- $ele_name = $form->getName();
-
- $ret = '
';
- $ret .= $form->renderValidationJS(true);
-
- return $ret;
- }
-
- /**
- * Support for themed addBreak
- *
- * @param XoopsThemeForm $form
- * @param string $extra pre-rendered content for break row
- * @param string $class class for row
- *
- * @return void
- */
- public function addThemeFormBreak(XoopsThemeForm $form, $extra, $class)
- {
- $class = ($class != '') ? preg_replace('/[^A-Za-z0-9\s\s_-]/i', '', $class) : '';
- $form->addElement('
' . $extra . '
');
- }
-}
diff --git a/htdocs/class/xoopseditor/tinymce7/js/tinymce/plugins/xoopsimagemanager/xoopsimagemanager.php b/htdocs/class/xoopseditor/tinymce7/js/tinymce/plugins/xoopsimagemanager/xoopsimagemanager.php
index e6495f261..051507620 100644
--- a/htdocs/class/xoopseditor/tinymce7/js/tinymce/plugins/xoopsimagemanager/xoopsimagemanager.php
+++ b/htdocs/class/xoopseditor/tinymce7/js/tinymce/plugins/xoopsimagemanager/xoopsimagemanager.php
@@ -67,7 +67,7 @@
//xoops_load("xoopsmodule");
include_once XOOPS_ROOT_PATH . '/include/cp_functions.php';
include_once XOOPS_ROOT_PATH . '/modules/system/constants.php';
-include_once __DIR__ . '/XoopsFormRendererBootstrap5.php';
+xoops_load('xoopsformrendererbootstrap5');
XoopsFormRenderer::getInstance()->set(new XoopsFormRendererBootstrap5());
@@ -171,6 +171,10 @@
// Add new category - start
if ($op === 'addcat' && \Xmf\Request::hasVar('op', 'POST')) {
+ if (!$isadmin) {
+ redirect_header($current_file . '?target=' . $target, 3, _NOPERM);
+ }
+
if (!$GLOBALS['xoopsSecurity']->check()) {
redirect_header($current_file . '?target=' . $target, 3, implode('
', $GLOBALS['xoopsSecurity']->getErrors()));
}
@@ -226,6 +230,10 @@
// Update category - start
if ($op === 'updatecat' && \Xmf\Request::hasVar('op', 'POST')) {
+ if (!$isadmin) {
+ redirect_header($current_file . '?target=' . $target, 3, _NOPERM);
+ }
+
if (!$GLOBALS['xoopsSecurity']->check() || $imgcat_id <= 0) {
redirect_header($current_file . '?target=' . $target, 3, implode('
', $GLOBALS['xoopsSecurity']->getErrors()));
}
@@ -287,6 +295,10 @@
// Confirm delete category - start
if ($op === 'delcat' && \Xmf\Request::hasVar('op', 'GET')) {
+ if (!$isadmin) {
+ redirect_header($current_file . '?target=' . $target, 3, _NOPERM);
+ }
+
xoops_header();
echo "
";
xoops_confirm(['op' => 'delcatok', 'imgcat_id' => $imgcat_id, 'target' => $target], $current_file, _MD_RUDELIMGCAT);
@@ -297,6 +309,10 @@
// Delete category - start
if ($op === 'delcatok' && \Xmf\Request::hasVar('op', 'POST')) {
+ if (!$isadmin) {
+ redirect_header($current_file . '?target=' . $target, 3, _NOPERM);
+ }
+
if (!$GLOBALS['xoopsSecurity']->check()) {
redirect_header($current_file . '?target=' . $target, 3, implode('
', $GLOBALS['xoopsSecurity']->getErrors()));
}
@@ -506,6 +522,10 @@
}
if ($op === 'editcat') {
+ if (!$isadmin) {
+ redirect_header($current_file . '?target=' . $target, 3, _NOPERM);
+ }
+
if ($imgcat_id <= 0) {
redirect_header($current_file . '?target=' . $target, 1);
}
diff --git a/htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php b/htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php
index 8eb88ed7e..9738459aa 100644
--- a/htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php
+++ b/htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php
@@ -9,6 +9,8 @@
*/
require_once __DIR__ . '/XoopsFormTabRendererInterface.php';
+require_once __DIR__ . '/../../xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php';
+require_once __DIR__ . '/XoopsFormRendererValueEscapeTrait.php';
/**
* Bootstrap3 style form renderer
@@ -21,6 +23,8 @@
*/
class XoopsFormRendererBootstrap3 implements XoopsFormRendererInterface, XoopsFormTabRendererInterface
{
+ use XoopsFormRendererValueEscapeTrait;
+
/**
* Counter giving each rendered tab tray a unique DOM id.
*
@@ -37,8 +41,8 @@ class XoopsFormRendererBootstrap3 implements XoopsFormRendererInterface, XoopsFo
public function renderFormButton(XoopsFormButton $element)
{
return "
getExtra() . ' />';
+ . $element->getName() . "' id='" . $element->getName() . "' value='" . $this->escapeElementValue($element->getValue())
+ . "' title='" . $this->escapeElementValue($element->getValue()) . "'" . $element->getExtra() . ' />';
}
/**
@@ -59,7 +63,7 @@ public function renderFormButtonTray(XoopsFormButtonTray $element)
. '" onClick="history.go(-1);return true;" /> '
. '
'
. '
getExtra()
+ . '" id="' . $element->getName() . '" value="' . $this->escapeElementValue($element->getValue()) . '"' . $element->getExtra()
. ' />';
return $ret;
@@ -215,7 +219,7 @@ public function renderFormColorPicker(XoopsFormColorPicker $element)
}
return '
getExtra() . ' />';
+ . '" size="7" maxlength="7" value="' . $this->escapeElementValue($element->getValue()) . '"' . $element->getExtra() . ' />';
}
/**
@@ -229,20 +233,16 @@ public function renderFormDhtmlTextArea(XoopsFormDhtmlTextArea $element)
{
xoops_loadLanguage('formdhtmltextarea');
$ret = '';
- // actions
- $ret .= $this->renderFormDhtmlTAXoopsCode($element) . "
\n";
- // fonts
- $ret .= $this->renderFormDhtmlTATypography($element);
- // length checker
-
- $ret .= "
\n";
+ // toolbar: xoopscode buttons, typography, check-length — shared across all renderers
+ $toolbar = new \XoopsDhtmlToolbar();
+ $ret .= $toolbar->render($element) . "
\n";
// the textarea box
$ret .= "
\n";
+ . '>' . $this->escapeElementValue($element->getValue()) . "\n";
if (empty($element->skipPreview)) {
if (empty($GLOBALS['xoTheme'])) {
@@ -285,146 +285,35 @@ public function renderFormDhtmlTextArea(XoopsFormDhtmlTextArea $element)
/**
* Render xoopscode buttons for editor, include calling text sanitizer extensions
*
+ * Thin delegate to the shared {@see XoopsDhtmlToolbar}. Kept (rather than removed) because
+ * this method is `protected`, not part of {@see XoopsFormRendererInterface}, and a third-party
+ * subclass of this renderer may still call or override it.
+ *
* @param XoopsFormDhtmlTextArea $element form element
*
* @return string rendered buttons for xoopscode assistance
*/
protected function renderFormDhtmlTAXoopsCode(XoopsFormDhtmlTextArea $element)
{
- $textarea_id = $element->getName();
- $code = '';
- $code .= "
";
- $code .= " ";
- $code .= " ";
- $code .= " ";
- $code .= " Manager ";
- $code .= " ";
-
- $myts = \MyTextSanitizer::getInstance();
-
- $extensions = array_filter($myts->config['extensions']);
- foreach (array_keys($extensions) as $key) {
- $extension = $myts->loadExtension($key);
- $result = $extension->encode($textarea_id);
- $encode = $result[0] ?? '';
- $js = $result[1] ?? '';
- if (empty($encode)) {
- continue;
- }
- $code .= $encode;
- if (!empty($js)) {
- $element->js .= $js;
- }
- }
- $code .= " ";
- $code .= " ";
- $code .= "
";
-
- $xoopsPreload = XoopsPreload::getInstance();
- $xoopsPreload->triggerEvent('core.class.xoopsform.formdhtmltextarea.codeicon', [&$code]);
-
- return $code;
+ return (new \XoopsDhtmlToolbar())->renderCodeButtons($element);
}
/**
* Render typography controls for editor (font, size, color)
*
+ * Thin delegate to the shared {@see XoopsDhtmlToolbar}. Kept (rather than removed) because
+ * this method is `protected`, not part of {@see XoopsFormRendererInterface}, and a third-party
+ * subclass of this renderer may still call or override it.
+ *
* @param XoopsFormDhtmlTextArea $element form element
*
* @return string rendered typography controls
*/
protected function renderFormDhtmlTATypography(XoopsFormDhtmlTextArea $element)
{
- $textarea_id = $element->getName();
- $hiddentext = $element->_hiddenText;
-
- $fontarray = !empty($GLOBALS['formtextdhtml_fonts']) ? $GLOBALS['formtextdhtml_fonts'] : [
- 'Arial',
- 'Courier',
- 'Georgia',
- 'Helvetica',
- 'Impact',
- 'Verdana',
- 'Haettenschweiler',
- ];
-
- $colorArray = [
- 'Black' => '000000',
- 'Blue' => '38AAFF',
- 'Brown' => '987857',
- 'Green' => '79D271',
- 'Grey' => '888888',
- 'Orange' => 'FFA700',
- 'Paper' => 'E0E0E0',
- 'Purple' => '363E98',
- 'Red' => 'FF211E',
- 'White' => 'FEFEFE',
- 'Yellow' => 'FFD628',
- ];
-
- $fontStr = '
';
- $fontStr .= '
'
- . ''
- . ' '
- . '
';
-
- $fontStr .= '
'
- . ''
- . ' '
- . '
';
-
- $fontStr .= '
'
- . ''
- . ' '
- . '
';
- $fontStr .= '
';
-
- //$styleStr = "
";
- $styleStr = "
";
- $styleStr .= " ";
- $styleStr .= " ";
- $styleStr .= "" . ' ';
- $styleStr .= "" . ' ';
- $styleStr .= "
";
-
- $alignStr = "
";
- $alignStr .= " ";
- $alignStr .= " ";
- $alignStr .= " ";
- $alignStr .= "
";
-
- $fontStr .= " {$styleStr} {$alignStr} \n";
-
- $maxlength = $element->configs['maxlength'] ?? 0;
- $fontStr .= "
getName() . "', '" . $maxlength . "', '"
- . _XOOPS_FORM_ALT_LENGTH . "', '" . _XOOPS_FORM_ALT_LENGTH_MAX . "');\" title='"
- . _XOOPS_FORM_ALT_CHECKLENGTH . "'> ";
- $fontStr .= "
";
-
- return $fontStr;
+ $toolbar = new \XoopsDhtmlToolbar();
+
+ return $toolbar->renderTypography($element) . $toolbar->renderCheckLength($element);
}
/**
@@ -507,7 +396,7 @@ public function renderFormPassword(XoopsFormPassword $element)
{
return '
getExtra() . ' ' . ($element->autoComplete ? '' : 'autocomplete="off" ') . '/>';
}
@@ -578,7 +467,7 @@ public function renderFormText(XoopsFormText $element)
return "
getExtra() . ' />';
+ . "' value='" . $this->escapeElementValue($element->getValue()) . "'" . $element->getExtra() . ' />';
}
/**
@@ -593,7 +482,7 @@ public function renderFormTextArea(XoopsFormTextArea $element)
return "
';
+ . $element->getExtra() . '>' . $this->escapeElementValue($element->getValue()) . '';
}
/**
diff --git a/htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php b/htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php
index 76d953e05..7f70fea9b 100644
--- a/htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php
+++ b/htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php
@@ -9,6 +9,8 @@
*/
require_once __DIR__ . '/XoopsFormTabRendererInterface.php';
+require_once __DIR__ . '/../../xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php';
+require_once __DIR__ . '/XoopsFormRendererValueEscapeTrait.php';
/**
* Bootstrap4 style form renderer
@@ -22,6 +24,8 @@
*/
class XoopsFormRendererBootstrap4 implements XoopsFormRendererInterface, XoopsFormTabRendererInterface
{
+ use XoopsFormRendererValueEscapeTrait;
+
/**
* Counter giving each rendered tab tray a unique DOM id.
*
@@ -39,8 +43,8 @@ public function renderFormButton(XoopsFormButton $element)
{
return '
getExtra() . '>' . $element->getValue() . ' ';
}
@@ -220,7 +224,7 @@ public function renderFormColorPicker(XoopsFormColorPicker $element)
}
return '
getExtra() . '>';
+ . '" size="7" maxlength="7" value="' . $this->escapeElementValue($element->getValue()) . '"' . $element->getExtra() . '>';
}
/**
@@ -234,20 +238,16 @@ public function renderFormDhtmlTextArea(XoopsFormDhtmlTextArea $element)
{
xoops_loadLanguage('formdhtmltextarea');
$ret = '';
- // actions
- $ret .= $this->renderFormDhtmlTAXoopsCode($element) . "
\n";
- // fonts
- $ret .= $this->renderFormDhtmlTATypography($element);
- // length checker
-
- $ret .= "
\n";
+ // toolbar: xoopscode buttons, typography, check-length — shared across all renderers
+ $toolbar = new \XoopsDhtmlToolbar();
+ $ret .= $toolbar->render($element) . "
\n";
// the textarea box
$ret .= "
\n";
+ . '>' . $this->escapeElementValue($element->getValue()) . "\n";
if (empty($element->skipPreview)) {
if (empty($GLOBALS['xoTheme'])) {
@@ -290,149 +290,35 @@ public function renderFormDhtmlTextArea(XoopsFormDhtmlTextArea $element)
/**
* Render xoopscode buttons for editor, include calling text sanitizer extensions
*
+ * Thin delegate to the shared {@see XoopsDhtmlToolbar}. Kept (rather than removed) because
+ * this method is `protected`, not part of {@see XoopsFormRendererInterface}, and a third-party
+ * subclass of this renderer may still call or override it.
+ *
* @param XoopsFormDhtmlTextArea $element form element
*
* @return string rendered buttons for xoopscode assistance
*/
protected function renderFormDhtmlTAXoopsCode(XoopsFormDhtmlTextArea $element)
{
- $textarea_id = $element->getName();
- $code = '';
- $code .= "
";
- $code .= " ";
- $code .= " ";
- $code .= " ";
- $code .= " Manager ";
- $code .= " ";
-
- $myts = \MyTextSanitizer::getInstance();
-
- $extensions = array_filter($myts->config['extensions']);
- foreach (array_keys($extensions) as $key) {
- $extension = $myts->loadExtension($key);
- $result = $extension->encode($textarea_id);
- $encode = $result[0] ?? '';
- $js = $result[1] ?? '';
- if (empty($encode)) {
- continue;
- }
- // TODO - MyTextSanitizer button rendering should go through XoopsFormRenderer
- $encode = str_replace('btn-default', 'btn-secondary', $encode);
-
- $code .= $encode;
- if (!empty($js)) {
- $element->js .= $js;
- }
- }
- $code .= " ";
- $code .= " ";
- $code .= "
";
-
- $xoopsPreload = XoopsPreload::getInstance();
- $xoopsPreload->triggerEvent('core.class.xoopsform.formdhtmltextarea.codeicon', [&$code]);
-
- return $code;
+ return (new \XoopsDhtmlToolbar())->renderCodeButtons($element);
}
/**
* Render typography controls for editor (font, size, color)
*
+ * Thin delegate to the shared {@see XoopsDhtmlToolbar}. Kept (rather than removed) because
+ * this method is `protected`, not part of {@see XoopsFormRendererInterface}, and a third-party
+ * subclass of this renderer may still call or override it.
+ *
* @param XoopsFormDhtmlTextArea $element form element
*
* @return string rendered typography controls
*/
protected function renderFormDhtmlTATypography(XoopsFormDhtmlTextArea $element)
{
- $textarea_id = $element->getName();
- $hiddentext = $element->_hiddenText;
-
- $fontarray = !empty($GLOBALS['formtextdhtml_fonts']) ? $GLOBALS['formtextdhtml_fonts'] : [
- 'Arial',
- 'Courier',
- 'Georgia',
- 'Helvetica',
- 'Impact',
- 'Verdana',
- 'Haettenschweiler',
- ];
-
- $colorArray = [
- 'Black' => '000000',
- 'Blue' => '38AAFF',
- 'Brown' => '987857',
- 'Green' => '79D271',
- 'Grey' => '888888',
- 'Orange' => 'FFA700',
- 'Paper' => 'E0E0E0',
- 'Purple' => '363E98',
- 'Red' => 'FF211E',
- 'White' => 'FEFEFE',
- 'Yellow' => 'FFD628',
- ];
-
- $fontStr = '
';
- $fontStr .= '
'
- . ''
- . ' '
- . '
';
-
- $fontStr .= '
'
- . ''
- . ' '
- . '
';
-
- $fontStr .= '
'
- . ''
- . ' '
- . '
';
- $fontStr .= '
';
-
- //$styleStr = "
";
- $styleStr = "
";
- $styleStr .= " ";
- $styleStr .= " ";
- $styleStr .= "" . ' ';
- $styleStr .= "" . ' ';
- $styleStr .= "
";
-
- $alignStr = "
";
- $alignStr .= " ";
- $alignStr .= " ";
- $alignStr .= " ";
- $alignStr .= "
";
-
- $fontStr .= " {$styleStr} {$alignStr} \n";
-
- $maxlength = $element->configs['maxlength'] ?? 0;
- $fontStr .= "
getName() . "', '" . $maxlength . "', '"
- . _XOOPS_FORM_ALT_LENGTH . "', '" . _XOOPS_FORM_ALT_LENGTH_MAX . "');\" title='"
- . _XOOPS_FORM_ALT_CHECKLENGTH . "'> ";
- $fontStr .= "
";
-
- return $fontStr;
+ $toolbar = new \XoopsDhtmlToolbar();
+
+ return $toolbar->renderTypography($element) . $toolbar->renderCheckLength($element);
}
/**
@@ -513,7 +399,7 @@ public function renderFormPassword(XoopsFormPassword $element)
{
return '
getExtra() . ' ' . ($element->autoComplete ? '' : 'autocomplete="off" ') . '/>';
}
@@ -585,7 +471,7 @@ public function renderFormText(XoopsFormText $element)
return "
getExtra() . '>';
+ . "' value='" . $this->escapeElementValue($element->getValue()) . "'" . $element->getExtra() . '>';
}
/**
@@ -600,7 +486,7 @@ public function renderFormTextArea(XoopsFormTextArea $element)
return "
';
+ . $element->getExtra() . '>' . $this->escapeElementValue($element->getValue()) . '';
}
/**
diff --git a/htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php b/htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php
index f4a23ccd3..3975ece98 100644
--- a/htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php
+++ b/htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php
@@ -9,6 +9,8 @@
*/
require_once __DIR__ . '/XoopsFormTabRendererInterface.php';
+require_once __DIR__ . '/../../xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php';
+require_once __DIR__ . '/XoopsFormRendererValueEscapeTrait.php';
/**
* Bootstrap5 style form renderer
@@ -22,6 +24,8 @@
*/
class XoopsFormRendererBootstrap5 implements XoopsFormRendererInterface, XoopsFormTabRendererInterface
{
+ use XoopsFormRendererValueEscapeTrait;
+
/**
* Counter giving each rendered tab tray a unique DOM id.
*
@@ -40,8 +44,8 @@ public function renderFormButton(XoopsFormButton $element)
{
return '
getExtra() . '>' . $element->getValue() . ' ';
}
@@ -221,7 +225,7 @@ public function renderFormColorPicker(XoopsFormColorPicker $element)
}
return '
getExtra() . '>';
+ . '" size="7" maxlength="7" value="' . $this->escapeElementValue($element->getValue()) . '"' . $element->getExtra() . '>';
}
/**
@@ -235,20 +239,16 @@ public function renderFormDhtmlTextArea(XoopsFormDhtmlTextArea $element)
{
xoops_loadLanguage('formdhtmltextarea');
$ret = '';
- // actions
- $ret .= $this->renderFormDhtmlTAXoopsCode($element) . "
\n";
- // fonts
- $ret .= $this->renderFormDhtmlTATypography($element);
- // length checker
-
- $ret .= "
\n";
+ // toolbar: xoopscode buttons, typography, check-length — shared across all renderers
+ $toolbar = new \XoopsDhtmlToolbar();
+ $ret .= $toolbar->render($element) . "
\n";
// the textarea box
$ret .= "
\n";
+ . '>' . $this->escapeElementValue($element->getValue()) . "\n";
if (empty($element->skipPreview)) {
if (empty($GLOBALS['xoTheme'])) {
@@ -291,149 +291,35 @@ public function renderFormDhtmlTextArea(XoopsFormDhtmlTextArea $element)
/**
* Render xoopscode buttons for editor, include calling text sanitizer extensions
*
+ * Thin delegate to the shared {@see XoopsDhtmlToolbar}. Kept (rather than removed) because
+ * this method is `protected`, not part of {@see XoopsFormRendererInterface}, and a third-party
+ * subclass of this renderer may still call or override it.
+ *
* @param XoopsFormDhtmlTextArea $element form element
*
* @return string rendered buttons for xoopscode assistance
*/
protected function renderFormDhtmlTAXoopsCode(XoopsFormDhtmlTextArea $element)
{
- $textarea_id = $element->getName();
- $code = '';
- $code .= "
";
- $code .= " ";
- $code .= " ";
- $code .= " ";
- $code .= " Manager ";
- $code .= " ";
-
- $myts = \MyTextSanitizer::getInstance();
-
- $extensions = array_filter($myts->config['extensions']);
- foreach (array_keys($extensions) as $key) {
- $extension = $myts->loadExtension($key);
- $result = $extension->encode($textarea_id);
- $encode = $result[0] ?? '';
- $js = $result[1] ?? '';
- if (empty($encode)) {
- continue;
- }
- // TODO - MyTextSanitizer button rendering should go through XoopsFormRenderer
- $encode = str_replace('btn-default', 'btn-secondary', $encode);
-
- $code .= $encode;
- if (!empty($js)) {
- $element->js .= $js;
- }
- }
- $code .= " ";
- $code .= " ";
- $code .= "
";
-
- $xoopsPreload = XoopsPreload::getInstance();
- $xoopsPreload->triggerEvent('core.class.xoopsform.formdhtmltextarea.codeicon', [&$code]);
-
- return $code;
+ return (new \XoopsDhtmlToolbar())->renderCodeButtons($element);
}
/**
* Render typography controls for editor (font, size, color)
*
+ * Thin delegate to the shared {@see XoopsDhtmlToolbar}. Kept (rather than removed) because
+ * this method is `protected`, not part of {@see XoopsFormRendererInterface}, and a third-party
+ * subclass of this renderer may still call or override it.
+ *
* @param XoopsFormDhtmlTextArea $element form element
*
* @return string rendered typography controls
*/
protected function renderFormDhtmlTATypography(XoopsFormDhtmlTextArea $element)
{
- $textarea_id = $element->getName();
- $hiddentext = $element->_hiddenText;
-
- $fontarray = !empty($GLOBALS['formtextdhtml_fonts']) ? $GLOBALS['formtextdhtml_fonts'] : [
- 'Arial',
- 'Courier',
- 'Georgia',
- 'Helvetica',
- 'Impact',
- 'Verdana',
- 'Haettenschweiler',
- ];
-
- $colorArray = [
- 'Black' => '000000',
- 'Blue' => '38AAFF',
- 'Brown' => '987857',
- 'Green' => '79D271',
- 'Grey' => '888888',
- 'Orange' => 'FFA700',
- 'Paper' => 'E0E0E0',
- 'Purple' => '363E98',
- 'Red' => 'FF211E',
- 'White' => 'FEFEFE',
- 'Yellow' => 'FFD628',
- ];
-
- $fontStr = '
';
- $fontStr .= '
'
- . ''
- . ' '
- . '
';
-
- $fontStr .= '
'
- . ''
- . ' '
- . '
';
-
- $fontStr .= '
'
- . ''
- . ' '
- . '
';
- $fontStr .= '
';
-
- //$styleStr = "
";
- $styleStr = "
";
- $styleStr .= " ";
- $styleStr .= " ";
- $styleStr .= "" . ' ';
- $styleStr .= "" . ' ';
- $styleStr .= "
";
-
- $alignStr = "
";
- $alignStr .= " ";
- $alignStr .= " ";
- $alignStr .= " ";
- $alignStr .= "
";
-
- $fontStr .= " {$styleStr} {$alignStr} \n";
-
- $maxlength = $element->configs['maxlength'] ?? 0;
- $fontStr .= "
getName() . "', '" . $maxlength . "', '"
- . _XOOPS_FORM_ALT_LENGTH . "', '" . _XOOPS_FORM_ALT_LENGTH_MAX . "');\" title='"
- . _XOOPS_FORM_ALT_CHECKLENGTH . "'> ";
- $fontStr .= "
";
-
- return $fontStr;
+ $toolbar = new \XoopsDhtmlToolbar();
+
+ return $toolbar->renderTypography($element) . $toolbar->renderCheckLength($element);
}
/**
@@ -514,7 +400,7 @@ public function renderFormPassword(XoopsFormPassword $element)
{
return '
getExtra() . ' ' . ($element->autoComplete ? '' : 'autocomplete="off" ') . '/>';
}
@@ -586,7 +472,7 @@ public function renderFormText(XoopsFormText $element)
return "
getExtra() . '>';
+ . "' value='" . $this->escapeElementValue($element->getValue()) . "'" . $element->getExtra() . '>';
}
/**
@@ -601,7 +487,7 @@ public function renderFormTextArea(XoopsFormTextArea $element)
return "
';
+ . $element->getExtra() . '>' . $this->escapeElementValue($element->getValue()) . '';
}
/**
diff --git a/htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php b/htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php
index 00a4de9ba..8e3eb6f4c 100644
--- a/htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php
+++ b/htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php
@@ -17,8 +17,13 @@
* @copyright 2000-2026 XOOPS Project (https://xoops.org)
* @license GNU GPL 2.0 or later (https://www.gnu.org/licenses/gpl-2.0.html)
*/
+require_once __DIR__ . '/../../xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php';
+require_once __DIR__ . '/XoopsFormRendererValueEscapeTrait.php';
+
class XoopsFormRendererLegacy implements XoopsFormRendererInterface
{
+ use XoopsFormRendererValueEscapeTrait;
+
/**
* Render support for XoopsFormButton
*
@@ -29,8 +34,8 @@ class XoopsFormRendererLegacy implements XoopsFormRendererInterface
public function renderFormButton(XoopsFormButton $element)
{
return "
getExtra() . ' />';
+ . "' id='" . $element->getName() . "' value='" . $this->escapeElementValue($element->getValue()) . "' title='"
+ . $this->escapeElementValue($element->getValue()) . "'" . $element->getExtra() . ' />';
}
/**
@@ -50,7 +55,7 @@ public function renderFormButtonTray(XoopsFormButtonTray $element)
$ret .= '
'
. '
'
. '
getExtra()
+ . '" id="' . $element->getName() . '" value="' . $this->escapeElementValue($element->getValue()) . '"' . $element->getExtra()
. ' />';
return $ret;
@@ -141,7 +146,7 @@ public function renderFormColorPicker(XoopsFormColorPicker $element)
}
return "
getExtra()
+ . $element->getMaxlength() . "' value='" . $this->escapeElementValue($element->getValue()) . "'" . $element->getExtra()
. ' />';
}
@@ -156,16 +161,11 @@ public function renderFormDhtmlTextArea(XoopsFormDhtmlTextArea $element)
{
xoops_loadLanguage('formdhtmltextarea');
$ret = '';
- // actions
- $ret .= $this->renderFormDhtmlTAXoopsCode($element) . "
\n";
- // fonts
- $ret .= $this->renderFormDhtmlTATypography($element);
- // length checker
- $maxlength = $element->configs['maxlength'] ?? 0;
- $ret .= "
getName() . "', '" . $maxlength . "', '" . _XOOPS_FORM_ALT_LENGTH . "', '" . _XOOPS_FORM_ALT_LENGTH_MAX . "');\" title='" . _XOOPS_FORM_ALT_CHECKLENGTH . "'> ";
- $ret .= "
\n";
+ // toolbar: xoopscode buttons, typography, check-length — shared across all renderers
+ $toolbar = new \XoopsDhtmlToolbar();
+ $ret .= $toolbar->render($element) . "
\n";
// the textarea box
- $ret .= "
\n";
+ $ret .= "
\n";
if (empty($element->skipPreview)) {
if (empty($GLOBALS['xoTheme'])) {
@@ -200,101 +200,35 @@ public function renderFormDhtmlTextArea(XoopsFormDhtmlTextArea $element)
/**
* Render xoopscode buttons for editor, include calling text sanitizer extensions
*
+ * Thin delegate to the shared {@see XoopsDhtmlToolbar}. Kept (rather than removed) because
+ * this method is `protected`, not part of {@see XoopsFormRendererInterface}, and a third-party
+ * subclass of this renderer may still call or override it.
+ *
* @param XoopsFormDhtmlTextArea $element form element
*
* @return string rendered buttons for xoopscode assistance
*/
protected function renderFormDhtmlTAXoopsCode(XoopsFormDhtmlTextArea $element)
{
- $textarea_id = $element->getName();
- $code = '';
- $code .= '
';
- $code .= "
";
- $code .= "
";
- $code .= "
";
- $code .= "
Manager ";
- $code .= "
";
-
- $myts = \MyTextSanitizer::getInstance();
-
- $extensions = array_filter($myts->config['extensions']);
- foreach (array_keys($extensions) as $key) {
- $extension = $myts->loadExtension($key);
- $result = $extension->encode($textarea_id);
- $encode = $result[0] ?? '';
- $js = $result[1] ?? '';
- if (empty($encode)) {
- continue;
- }
- $code .= $encode;
- if (!empty($js)) {
- $element->js .= $js;
- }
- }
- $code .= "
";
- $code .= "
";
-
- $xoopsPreload = XoopsPreload::getInstance();
- $xoopsPreload->triggerEvent('core.class.xoopsform.formdhtmltextarea.codeicon', [&$code]);
-
- return $code;
+ return (new \XoopsDhtmlToolbar())->renderCodeButtons($element);
}
/**
* Render typography controls for editor (font, size, color)
*
+ * Thin delegate to the shared {@see XoopsDhtmlToolbar}. Kept (rather than removed) because
+ * this method is `protected`, not part of {@see XoopsFormRendererInterface}, and a third-party
+ * subclass of this renderer may still call or override it.
+ *
* @param XoopsFormDhtmlTextArea $element form element
*
* @return string rendered typography controls
*/
protected function renderFormDhtmlTATypography(XoopsFormDhtmlTextArea $element)
{
- $textarea_id = $element->getName();
- $hiddentext = $element->_hiddenText;
- $fontStr = "';
-
- $styleStr = "
";
- $styleStr .= "
";
- $styleStr .= "
";
- $styleStr .= "
";
-
- $alignStr = "
";
- $alignStr .= "
";
- $alignStr .= "
";
+ $toolbar = new \XoopsDhtmlToolbar();
- $fontStr .= "
\n{$styleStr} {$alignStr} \n";
- return $fontStr;
+ return $toolbar->renderTypography($element) . $toolbar->renderCheckLength($element);
}
/**
@@ -366,7 +300,7 @@ public function renderFormLabel(XoopsFormLabel $element)
public function renderFormPassword(XoopsFormPassword $element)
{
return '
getExtra() . ' ' . ($element->autoComplete ? '' : 'autocomplete="off" ') . '/>';
}
@@ -468,7 +402,7 @@ public function renderFormText(XoopsFormText $element)
{
return "
getExtra()
+ . $element->getMaxlength() . "' value='" . $this->escapeElementValue($element->getValue()) . "'" . $element->getExtra()
. ' />';
}
@@ -483,7 +417,7 @@ public function renderFormTextArea(XoopsFormTextArea $element)
{
return "
';
+ . "'" . $element->getExtra() . '>' . $this->escapeElementValue($element->getValue()) . '';
}
/**
diff --git a/htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php b/htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php
index fc03919ab..dacf279db 100644
--- a/htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php
+++ b/htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php
@@ -20,6 +20,8 @@
defined('XOOPS_ROOT_PATH') || exit('Restricted access');
require_once __DIR__ . '/XoopsFormTabRendererInterface.php';
+require_once __DIR__ . '/../../xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php';
+require_once __DIR__ . '/XoopsFormRendererValueEscapeTrait.php';
/**
* Tailwind CSS + DaisyUI form renderer
@@ -43,6 +45,8 @@
*/
class XoopsFormRendererTailwind implements XoopsFormRendererInterface, XoopsFormTabRendererInterface
{
+ use XoopsFormRendererValueEscapeTrait;
+
/**
* Counter giving each rendered tab tray a unique DOM id / radio group.
*
@@ -409,16 +413,16 @@ public function renderFormDhtmlTextArea(XoopsFormDhtmlTextArea $element)
$savePositionJs = $this->buildJsCall('xoopsSavePosition', [$nameRaw]);
- $ret = $this->renderFormDhtmlTAXoopsCode($element) . "
\n";
- $ret .= $this->renderFormDhtmlTATypography($element);
- $ret .= "
\n";
+ // toolbar: xoopscode buttons, typography, check-length — shared across all renderers
+ $toolbar = new \XoopsDhtmlToolbar();
+ $ret = $toolbar->render($element) . "
\n";
$ret .= '
\n";
+ . $this->renderExtra($element) . '>' . $this->escapeElementValue($element->getValue()) . "\n";
if (empty($element->skipPreview)) {
if (empty($GLOBALS['xoTheme'])) {
@@ -496,142 +500,35 @@ protected function renderEditorButton(string $class, string $onclickJs, string $
/**
* Render xoopscode buttons for editor, include calling text sanitizer extensions
*
+ * Thin delegate to the shared {@see XoopsDhtmlToolbar}. Kept (rather than removed) because
+ * this method is `protected`, not part of {@see XoopsFormRendererInterface}, and a third-party
+ * subclass of this renderer may still call or override it.
+ *
* @param XoopsFormDhtmlTextArea $element form element
*
* @return string rendered buttons for xoopscode assistance
*/
protected function renderFormDhtmlTAXoopsCode(XoopsFormDhtmlTextArea $element)
{
- $textareaIdRaw = (string) $element->getName(false);
- $textareaIdParam = rawurlencode($textareaIdRaw);
- $urlImageMgr = XOOPS_URL . '/imagemanager.php?target=' . $textareaIdParam;
- $urlSmilies = XOOPS_URL . '/misc.php?action=showpopups&type=smilies&target=' . $textareaIdParam;
- $btn = self::BTN_NEUTRAL_SM;
-
- $code = "
";
- $code .= $this->renderEditorButton($btn, $this->buildJsCall('xoopsCodeUrl', [$textareaIdRaw, _ENTERURL, _ENTERWEBTITLE]), _XOOPS_FORM_ALT_URL, 'fa-solid fa-link');
- $code .= $this->renderEditorButton($btn, $this->buildJsCall('xoopsCodeEmail', [$textareaIdRaw, _ENTEREMAIL, _ENTERWEBTITLE]), _XOOPS_FORM_ALT_EMAIL, 'fa-solid fa-envelope');
- $code .= $this->renderEditorButton($btn, $this->buildJsCall('xoopsCodeImg', [$textareaIdRaw, _ENTERIMGURL, _ENTERIMGPOS, _IMGPOSRORL, _ERRORIMGPOS, _XOOPS_FORM_ALT_ENTERWIDTH]), _XOOPS_FORM_ALT_IMG, 'fa-solid fa-file-image');
- $code .= $this->renderEditorButton($btn, $this->buildJsCall('openWithSelfMain', [$urlImageMgr, 'imgmanager', 400, 430]), _XOOPS_FORM_ALT_IMAGE, 'fa-solid fa-file-image', ' Manager ');
- $code .= $this->renderEditorButton($btn, $this->buildJsCall('openWithSelfMain', [$urlSmilies, 'smilies', 300, 475]), _XOOPS_FORM_ALT_SMILEY, 'fa-solid fa-face-smile');
-
- $myts = \MyTextSanitizer::getInstance();
- $extensions = array_filter($myts->config['extensions']);
- foreach (array_keys($extensions) as $key) {
- $extension = $myts->loadExtension($key);
- $result = $extension->encode($textareaIdRaw);
- $encode = $result[0] ?? '';
- $js = $result[1] ?? '';
- if (empty($encode)) {
- continue;
- }
- // Extensions output Bootstrap classes — remap the common ones to DaisyUI.
- $encode = str_replace(['btn-default', 'btn-secondary'], self::BTN_NEUTRAL_SM, $encode);
- $code .= $encode;
- if (!empty($js)) {
- $element->js .= $js;
- }
- }
- $code .= $this->renderEditorButton($btn, $this->buildJsCall('xoopsCodeCode', [$textareaIdRaw, _ENTERCODE]), _XOOPS_FORM_ALT_CODE, 'fa-solid fa-code');
- $code .= $this->renderEditorButton($btn, $this->buildJsCall('xoopsCodeQuote', [$textareaIdRaw, _ENTERQUOTE]), _XOOPS_FORM_ALT_QUOTE, 'fa-solid fa-quote-right');
- $code .= '
';
-
- $xoopsPreload = XoopsPreload::getInstance();
- $xoopsPreload->triggerEvent('core.class.xoopsform.formdhtmltextarea.codeicon', [&$code]);
-
- return $code;
+ return (new \XoopsDhtmlToolbar())->renderCodeButtons($element);
}
/**
* Render typography controls for editor (font, size, color)
*
+ * Thin delegate to the shared {@see XoopsDhtmlToolbar}. Kept (rather than removed) because
+ * this method is `protected`, not part of {@see XoopsFormRendererInterface}, and a third-party
+ * subclass of this renderer may still call or override it.
+ *
* @param XoopsFormDhtmlTextArea $element form element
*
* @return string rendered typography controls
*/
protected function renderFormDhtmlTATypography(XoopsFormDhtmlTextArea $element)
{
- $textareaIdRaw = (string) $element->getName(false);
- $hiddentextRaw = (string) $element->_hiddenText;
- $btn = self::BTN_NEUTRAL_SM;
- $menuCls = self::DROPDOWN_MENU_CLS;
-
- $fontarray = !empty($GLOBALS['formtextdhtml_fonts']) ? $GLOBALS['formtextdhtml_fonts'] : [
- 'Arial', 'Courier', 'Georgia', 'Helvetica', 'Impact', 'Verdana', 'Haettenschweiler',
- ];
-
- $colorArray = [
- 'Black' => '000000', 'Blue' => '38AAFF', 'Brown' => '987857',
- 'Green' => '79D271', 'Grey' => '888888', 'Orange' => 'FFA700',
- 'Paper' => 'E0E0E0', 'Purple' => '363E98', 'Red' => 'FF211E',
- 'White' => 'FEFEFE', 'Yellow' => 'FFD628',
- ];
-
- $fontStr = "
";
-
- // Size dropdown — each link uses href='#' onclick='...; return false;'
- // so the JavaScript runs through a real handler context instead of a
- // `javascript:` URL (which adds URL decoding on top of HTML entity
- // decoding before the JS engine sees it).
- $sizes = $GLOBALS['formtextdhtml_sizes'] ?? [];
- $fontStr .= "
';
-
- // Font dropdown
- $fontStr .= "
';
-
- // Color dropdown
- $fontStr .= "
';
-
- // Style buttons
- $styleBtn = self::BTN_NEUTRAL_SM . ' join-item';
- $fontStr .= "
";
- $fontStr .= $this->renderEditorButton($styleBtn, $this->buildJsCall('xoopsMakeBold', [$hiddentextRaw, $textareaIdRaw]), _XOOPS_FORM_ALT_BOLD, 'fa-solid fa-bold');
- $fontStr .= $this->renderEditorButton($styleBtn, $this->buildJsCall('xoopsMakeItalic', [$hiddentextRaw, $textareaIdRaw]), _XOOPS_FORM_ALT_ITALIC, 'fa-solid fa-italic');
- $fontStr .= $this->renderEditorButton($styleBtn, $this->buildJsCall('xoopsMakeUnderline', [$hiddentextRaw, $textareaIdRaw]), _XOOPS_FORM_ALT_UNDERLINE, 'fa-solid fa-underline');
- $fontStr .= $this->renderEditorButton($styleBtn, $this->buildJsCall('xoopsMakeLineThrough', [$hiddentextRaw, $textareaIdRaw]), _XOOPS_FORM_ALT_LINETHROUGH, 'fa-solid fa-strikethrough');
- $fontStr .= '
';
-
- // Align buttons
- $fontStr .= "
";
- $fontStr .= $this->renderEditorButton($styleBtn, $this->buildJsCall('xoopsMakeLeft', [$hiddentextRaw, $textareaIdRaw]), _XOOPS_FORM_ALT_LEFT, 'fa-solid fa-align-left');
- $fontStr .= $this->renderEditorButton($styleBtn, $this->buildJsCall('xoopsMakeCenter', [$hiddentextRaw, $textareaIdRaw]), _XOOPS_FORM_ALT_CENTER, 'fa-solid fa-align-center');
- $fontStr .= $this->renderEditorButton($styleBtn, $this->buildJsCall('xoopsMakeRight', [$hiddentextRaw, $textareaIdRaw]), _XOOPS_FORM_ALT_RIGHT, 'fa-solid fa-align-right');
- $fontStr .= '
';
-
- // Length check button — configs is a legacy dynamic property on some
- // editor instances; guard the access to avoid PHP 8.2 dynamic property warnings
- $maxlength = 0;
- if (property_exists($element, 'configs') && is_array($element->configs) && isset($element->configs['maxlength'])) {
- $maxlength = (int) $element->configs['maxlength'];
- }
- $lengthOnclick = $this->buildJsCall('XoopsCheckLength', [$textareaIdRaw, (string) $maxlength, _XOOPS_FORM_ALT_LENGTH, _XOOPS_FORM_ALT_LENGTH_MAX]);
- $checkLengthLabel = $this->esc(_XOOPS_FORM_ALT_CHECKLENGTH);
- $fontStr .= "
";
- $fontStr .= '
';
+ $toolbar = new \XoopsDhtmlToolbar();
- return $fontStr;
+ return $toolbar->renderTypography($element) . $toolbar->renderCheckLength($element);
}
/**
@@ -824,7 +721,7 @@ public function renderFormTextArea(XoopsFormTextArea $element)
. ' rows="' . (int) $element->getRows() . '"'
. ' cols="' . (int) $element->getCols() . '"'
. $this->renderExtra($element) . '>'
- . $this->esc($element->getValue()) . '';
+ . $this->escapeElementValue($element->getValue()) . '';
}
/**
diff --git a/htdocs/class/xoopsform/renderer/XoopsFormRendererValueEscapeTrait.php b/htdocs/class/xoopsform/renderer/XoopsFormRendererValueEscapeTrait.php
new file mode 100644
index 000000000..025b1901c
--- /dev/null
+++ b/htdocs/class/xoopsform/renderer/XoopsFormRendererValueEscapeTrait.php
@@ -0,0 +1,47 @@
+
+ * @copyright 2000-2026 XOOPS Project (https://xoops.org)
+ * @license GNU GPL 2.0 or later (https://www.gnu.org/licenses/gpl-2.0.html)
+ * @link https://xoops.org
+ * @since 2.7.3
+ */
+
+defined('XOOPS_ROOT_PATH') || exit('Restricted access');
+
+trait XoopsFormRendererValueEscapeTrait
+{
+ /**
+ * Escape a form element value for HTML text/attribute context, idempotently.
+ *
+ * XOOPS callers historically hand elements an ALREADY-escaped value
+ * (getVar($k, 'e'|'E'), MyTextSanitizer::htmlSpecialChars()), and roughly 95% of
+ * core does. Escaping again here would double-escape them and rewrite stored text
+ * on the next save. Decoding first makes this a no-op for those callers while
+ * still neutralising the ones that pass raw user input -- which core itself does
+ * in at least two places (kernel/menusitems.php fetches with 'n', and
+ * include/comment_form.php re-displays raw $_POST on preview).
+ *
+ * Flags match kernel/object.php:478 so the round-trip is exact.
+ *
+ * @param mixed $value
+ * @return string
+ */
+ protected function escapeElementValue($value): string
+ {
+ return htmlspecialchars(
+ htmlspecialchars_decode((string) $value, ENT_QUOTES | ENT_HTML5),
+ ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE,
+ 'UTF-8'
+ );
+ }
+}
diff --git a/htdocs/include/formdhtmltextarea.js b/htdocs/include/formdhtmltextarea.js
index e936ff8af..65f0d7590 100644
--- a/htdocs/include/formdhtmltextarea.js
+++ b/htdocs/include/formdhtmltextarea.js
@@ -131,13 +131,23 @@ function xoopsCodeCode(id, enterCodePhrase) {
domobj.focus();
}
+// Legacy helper from an older toolbar layout that had a separate "add text" box alongside
+// the Font/Colour/Size selects. No renderer has emitted an "
Addtext" element or an
+// element with the hiddentext id for a long time, and nothing in core or any bundled
+// extension calls this function, so every path through it dereferenced null. It is kept
+// only because it is a global that a third-party module could still call; it now returns
+// harmlessly instead of throwing a TypeError.
function xoopsCodeText(id, hiddentext, enterTextboxPhrase) {
var textareaDom = xoopsGetElementById(id);
var textDom = xoopsGetElementById(id + "Addtext");
var fontDom = xoopsGetElementById(id + "Font");
var colorDom = xoopsGetElementById(id + "Color");
var sizeDom = xoopsGetElementById(id + "Size");
- var xoopsHiddenTextDomStyle = xoopsGetElementById(hiddentext).style;
+ var hiddenDom = xoopsGetElementById(hiddentext);
+ if (!textareaDom || !textDom || !fontDom || !colorDom || !sizeDom || !hiddenDom) {
+ return;
+ }
+ var xoopsHiddenTextDomStyle = hiddenDom.style;
var selection = xoopsGetSelect(id);
if (selection.length > 0) {
var textDomValue = selection;
@@ -223,12 +233,19 @@ function xoopsGetSelect(id) {
function xoopsSetElementAttribute(key, val, id, eid) {
+ // xoopsGetSelect() returns null on browsers with no selection API (its final else
+ // branch), so normalise before use: otherwise text.length throws and the tag pair
+ // below would contain the literal string "null".
var text = xoopsGetSelect(id);
- if (text.length <= 0) {
- setVisible("xoopsHiddenText");
- eval("setElement" + key.substr(0, 1).toUpperCase() + key.substr(1, key.length) + "(eid, val)");
- return;
- }
+ if (text === null || text === undefined) {
+ text = "";
+ }
+ // With no selection this used to call setVisible("xoopsHiddenText") and then eval a
+ // setElementSize/Font/Color() helper. That path was dead twice over: no renderer emits
+ // an element with that id (so setVisible() threw a TypeError on null), and no
+ // setElement* function has ever existed in this file. Size/Font/Colour therefore did
+ // nothing at all unless text was selected. Insert an empty tag pair instead, which is
+ // the behaviour every other editor has and what the user is asking for by clicking.
var domobj = xoopsGetElementById(id);
xoopsInsertText(domobj, "[" + key + "=" + val + "]" + text + "[/" + key + "]");
domobj.focus();
@@ -270,11 +287,31 @@ function makeLineThrough(id) {
}
}
+// Explicit dispatch for the legacy live-preview helpers. Replaces an eval() of a
+// caller-supplied function name: only these four are ever valid targets, so a lookup is
+// both safer and clearer than evaluating a string.
+var xoopsStylePreviewFuncs = {
+ makeBold: makeBold,
+ makeItalic: makeItalic,
+ makeUnderline: makeUnderline,
+ makeLineThrough: makeLineThrough
+};
+
function xoopsMakeStyle(id, eid, val, func) {
+ // See xoopsSetElementAttribute(): xoopsGetSelect() can return null.
var text = xoopsGetSelect(id);
- if (text.length <= 0 && func.length > 0 && eid.length > 0) {
+ if (text === null || text === undefined) {
+ text = "";
+ }
+ // The no-selection branch toggles a hidden live-preview element. No current renderer
+ // emits one, so setVisible() dereferenced null and threw — B/I/U/S did nothing unless
+ // text was selected. Take it only when the element genuinely exists (kept for any
+ // custom renderer that still emits it); otherwise fall through and insert an empty
+ // tag pair so the click does something useful.
+ if (text.length <= 0 && func.length > 0 && eid.length > 0
+ && xoopsGetElementById(eid) && xoopsStylePreviewFuncs[func]) {
setVisible(eid);
- eval(func + "(eid)");
+ xoopsStylePreviewFuncs[func](eid);
return;
}
var domobj = xoopsGetElementById(id);
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index adbe5bfd1..9b880e507 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -62,9 +62,30 @@
if (!defined('_DB_QUERY_ERROR')) {
define('_DB_QUERY_ERROR', 'DB Query Error: %s');
}
-if (!defined('_MSC_ORIGINAL_IMAGE')) {
- define('_MSC_ORIGINAL_IMAGE', 'Original Image');
-}
+// `class/textsanitizer/image/image.php` includes its language file at FILE scope, keyed on
+// $xoopsConfig['language'], and then uses _MSC_ORIGINAL_IMAGE inside load() -- a fatal in PHP 8 if
+// the include failed. Two separate problems follow from that, and both are fixed here:
+//
+// 1. Nothing set $xoopsConfig, so the path resolved to `language//misc.php` and the include
+// always failed. The previous bootstrap papered over it by stubbing _MSC_ORIGINAL_IMAGE --
+// the one constant the reachable branch happens to use. The other _MSC_* constants in that
+// file were still undefined, so a config change (allowimage + a theme) turns a passing test
+// into a fatal.
+//
+// 2. Setting $xoopsConfig['language'] alone does NOT fix it. Roughly thirty tests under
+// class/auth/ assign `$GLOBALS['xoopsConfig'] = ['debug_mode' => n]` -- replacing the whole
+// array -- and unset() it in tearDown. Whether the include succeeds then depends on which
+// test happens to load image.php first, which is not a property a bootstrap may have.
+//
+// So load the real language file HERE, unconditionally. The require_once is the load-bearing
+// part: the constants exist before any test can touch $xoopsConfig. The assignment below is only
+// so image.php's own include_once resolves to the same realpath and is a no-op rather than a
+// warning. misc.php defines its 21 constants unguarded, which is also why stubbing any of them
+// here would collide.
+if (!isset($GLOBALS['xoopsConfig']['language'])) {
+ $GLOBALS['xoopsConfig']['language'] = 'english';
+}
+require_once XOOPS_ROOT_PATH . '/language/english/misc.php';
if (!defined('_QUOTEC')) {
define('_QUOTEC', '"');
}
@@ -374,6 +395,53 @@
if (!defined('_XOOPS_FORM_ENTERYOUTUBEURL')) {
define('_XOOPS_FORM_ENTERYOUTUBEURL', 'Enter YouTube URL');
}
+if (!defined('_XOOPS_FORM_ALT_ENTERHEIGHT')) {
+ define('_XOOPS_FORM_ALT_ENTERHEIGHT', 'Height:');
+}
+
+// TextSanitizer extension button constants.
+//
+// Only the youtube ones were defined before, because config.dist.php enables youtube and
+// disables mp3/wmp/mms/rtsp/soundcloud (wiki is conditional on the mediawiki module) -- so a
+// test that rendered the whole toolbar only ever reached youtube. XoopsDhtmlToolbarExtensionEscapingTest
+// calls each extension's encode() directly to cover all seven regardless of configuration,
+// which needs every extension's constants defined here.
+if (!defined('_XOOPS_FORM_ALTMP3')) {
+ define('_XOOPS_FORM_ALTMP3', 'MP3');
+}
+if (!defined('_XOOPS_FORM_ALTWMP')) {
+ define('_XOOPS_FORM_ALTWMP', 'Windows Media');
+}
+if (!defined('_XOOPS_FORM_ENTERWMPURL')) {
+ define('_XOOPS_FORM_ENTERWMPURL', 'Enter Windows Media URL');
+}
+if (!defined('_XOOPS_FORM_ALTMMS')) {
+ define('_XOOPS_FORM_ALTMMS', 'MMS');
+}
+if (!defined('_XOOPS_FORM_ENTERMMSURL')) {
+ define('_XOOPS_FORM_ENTERMMSURL', 'Enter MMS URL');
+}
+if (!defined('_XOOPS_FORM_ALTRTSP')) {
+ define('_XOOPS_FORM_ALTRTSP', 'RTSP');
+}
+if (!defined('_XOOPS_FORM_ENTERRTSPURL')) {
+ define('_XOOPS_FORM_ENTERRTSPURL', 'Enter RTSP URL');
+}
+if (!defined('_XOOPS_FORM_ALT_SOUNDCLOUD')) {
+ define('_XOOPS_FORM_ALT_SOUNDCLOUD', 'SoundCloud');
+}
+if (!defined('_XOOPS_FORM_ENTER_SOUNDCLOUD_URL')) {
+ define('_XOOPS_FORM_ENTER_SOUNDCLOUD_URL', 'Enter SoundCloud URL');
+}
+if (!defined('_XOOPS_FORM_ALTWIKI')) {
+ define('_XOOPS_FORM_ALTWIKI', 'Wiki');
+}
+if (!defined('_XOOPS_FORM_ENTERWIKITERM')) {
+ define('_XOOPS_FORM_ENTERWIKITERM', 'Enter Wiki term');
+}
+if (!defined('_XOOPS_FORM_ALTYOUTUBE')) {
+ define('_XOOPS_FORM_ALTYOUTUBE', 'Youtube');
+}
// Multibyte constants
if (!defined('XOOPS_USE_MULTIBYTES')) {
@@ -971,3 +1039,13 @@ function xoops_load($name, $type = 'core')
{
return XoopsLoad::load($name, $type);
}
+
+// Xmf\I18n\Xoops\Registry\LocaleRegistry caches a snapshot under XOOPS_VAR_PATH, which in this
+// suite resolves to the repo's own xoops_data/. A developer who also runs a live site from this
+// tree leaves a populated snapshot there, and tests would then negotiate locales and build language
+// selectors from THAT site's installed packs — passing or failing differently per machine. Purge it
+// once per test process so every run starts from an unreadable (empty) registry. Nothing here has a
+// database, so no test rewrites the file.
+if (class_exists(\Xmf\I18n\Xoops\Registry\LocaleRegistry::class)) {
+ (new \Xmf\I18n\Xoops\Registry\LocaleRegistry(null))->purgeCache();
+}
diff --git a/tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarExtensionEscapingTest.php b/tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarExtensionEscapingTest.php
new file mode 100644
index 000000000..41d99298f
--- /dev/null
+++ b/tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarExtensionEscapingTest.php
@@ -0,0 +1,287 @@
+ label => [directory, class]
+ */
+ public static function extensionProvider(): array
+ {
+ return [
+ 'youtube' => ['youtube', 'MytsYoutube'],
+ 'mp3' => ['mp3', 'MytsMp3'],
+ 'wmp' => ['wmp', 'MytsWmp'],
+ 'mms' => ['mms', 'MytsMms'],
+ 'rtsp' => ['rtsp', 'MytsRtsp'],
+ 'soundcloud' => ['soundcloud', 'MytsSoundcloud'],
+ 'wiki' => ['wiki', 'MytsWiki'],
+ ];
+ }
+
+ /** Load an extension class directly, sidestepping MyTextSanitizer's config gate. */
+ private function loadExtension(string $dir, string $class): object
+ {
+ $file = XOOPS_ROOT_PATH . '/class/textsanitizer/' . $dir . '/' . $dir . '.php';
+ self::assertFileExists($file, "Extension $dir is missing from the tree.");
+
+ require_once XOOPS_ROOT_PATH . '/class/module.textsanitizer.php';
+ require_once $file;
+
+ self::assertTrue(class_exists($class, false), "Extension $dir did not declare $class.");
+
+ // MyTextSanitizerExtension::__construct() takes the sanitizer; core builds
+ // extensions as `new $class($this)` from inside MyTextSanitizer.
+ return new $class(\MyTextSanitizer::getInstance());
+ }
+
+ /** `encode()` returns [buttonHtml, javascript]; only the button markup carries handlers. */
+ private function buttonHtmlFor(string $dir, string $class): string
+ {
+ $encoded = $this->loadExtension($dir, $class)->encode(self::MALICIOUS_NAME);
+ $html = is_array($encoded) ? (string) ($encoded[0] ?? '') : (string) $encoded;
+
+ self::assertNotSame('', $html, "$dir::encode() produced no button markup.");
+
+ return $html;
+ }
+
+ /**
+ * Strip JS string literals, leaving the code around them.
+ *
+ * This is what separates "the payload is present" from "the payload is executable". A
+ * correctly escaped handler still CONTAINS the characters `alert(1)` -- inside a string
+ * literal, as inert data:
+ *
+ * xoopsCodeYoutube("x\u0022);alert(1);\/\/","Enter YouTube URL","Height:","Enter Width");
+ *
+ * So asserting that `alert(1)` is absent from the raw handler FAILS ON CORRECT OUTPUT, and
+ * the shortest way to make such an assertion pass is to weaken the escaping. It is the same
+ * mistake as grepping rendered HTML for `onfocus=` and calling it a breakout when the output
+ * was `value='x' onfocus='...'` -- correctly escaped and inert.
+ *
+ * Removing the literals first asks the question that matters: is the payload CODE? A payload
+ * that closed its literal leaves `);alert(1);//` behind here; one that did not leaves an
+ * empty argument list.
+ *
+ * Deliberately small: it tracks the opening quote and honours backslash escapes, which is all
+ * a one-line call expression needs. An unterminated literal swallows the rest of the string,
+ * which is itself the signature of a break-out and shows up as a missing `)`.
+ */
+ private function codeOutsideStringLiterals(string $js): string
+ {
+ $out = '';
+ $quote = null;
+ $length = strlen($js);
+
+ for ($i = 0; $i < $length; ++$i) {
+ $char = $js[$i];
+
+ if (null !== $quote) {
+ if ('\\' === $char) {
+ ++$i; // skip the escaped character, whatever it is
+ continue;
+ }
+ if ($char === $quote) {
+ $quote = null;
+ }
+ continue;
+ }
+
+ if ('"' === $char || "'" === $char) {
+ $quote = $char;
+ continue;
+ }
+
+ $out .= $char;
+ }
+
+ return $out;
+ }
+
+ /** @return array the onclick attribute bodies in the given markup */
+ private function onclickBodies(string $html): array
+ {
+ preg_match_all("/onclick='([^']*)'/", $html, $matches);
+
+ return $matches[1];
+ }
+
+ #[Test]
+ #[DataProvider('extensionProvider')]
+ public function eachExtensionEmitsInertJavaScriptForAHostileTextareaId(string $dir, string $class): void
+ {
+ $handlers = $this->onclickBodies($this->buttonHtmlFor($dir, $class));
+ self::assertNotEmpty($handlers, "$dir::encode() produced no single-quoted onclick handler.");
+
+ foreach ($handlers as $handler) {
+ // What the JS parser actually receives: the HTML attribute decode happens first.
+ $decoded = html_entity_decode($handler, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+
+ self::assertDoesNotMatchRegularExpression(
+ '/"\s*\)\s*;\s*alert\s*\(/',
+ $decoded,
+ "$dir: the injected payload closed its JS string literal, making alert( callable"
+ );
+ self::assertStringNotContainsString(
+ 'alert',
+ $this->codeOutsideStringLiterals($decoded),
+ "$dir: the injected payload escaped its JS string literal and became executable "
+ . "code. The payload is EXPECTED to appear inside the literal -- that is what "
+ . "correct escaping looks like -- so this fires only when it appears outside one."
+ );
+ }
+ }
+
+ #[Test]
+ #[DataProvider('extensionProvider')]
+ public function eachExtensionEmitsAWellFormedCallExpression(string $dir, string $class): void
+ {
+ foreach ($this->onclickBodies($this->buttonHtmlFor($dir, $class)) as $handler) {
+ $decoded = html_entity_decode($handler, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+
+ self::assertMatchesRegularExpression(
+ '/^\s*[A-Za-z_$][\w$]*\s*\(.*\)\s*;?\s*$/s',
+ $decoded,
+ "$dir: the onclick handler is not a single well-formed call expression"
+ );
+ // Counted OUTSIDE string literals. The hostile textarea id carries its own
+ // parentheses, so a naive count over the whole handler reports an imbalance on
+ // perfectly correct output -- and the shortest way to make that pass is to stop
+ // escaping.
+ $code = $this->codeOutsideStringLiterals($decoded);
+
+ self::assertSame(
+ substr_count($code, '('),
+ substr_count($code, ')'),
+ "$dir: unbalanced parentheses in the onclick handler -- it would throw on click"
+ );
+ }
+ }
+
+ /** The delimiter invariant the json_encode design silently depends on. See the class docblock. */
+ #[Test]
+ #[DataProvider('extensionProvider')]
+ public function eachExtensionUsesSingleQuotedOnclickAttributes(string $dir, string $class): void
+ {
+ self::assertDoesNotMatchRegularExpression(
+ '/onclick\s*=\s*"/',
+ $this->buttonHtmlFor($dir, $class),
+ "$dir: onclick is double-quoted. json_encode wraps its output in double quotes, so the "
+ . "first argument closes the attribute and JSON_HEX_QUOT does not help. Use onclick='...'."
+ );
+ }
+
+ /** The same invariant for the toolbar's own buttons and dropdowns. */
+ #[Test]
+ public function theToolbarItselfUsesSingleQuotedOnclickAttributes(): void
+ {
+ $element = new XoopsFormDhtmlTextArea('Caption', self::MALICIOUS_NAME, 'value', 5, 50, 'xoopsHiddenText');
+ $toolbar = new XoopsDhtmlToolbar();
+ $html = $toolbar->renderCodeButtons($element) . $toolbar->renderTypography($element);
+
+ self::assertDoesNotMatchRegularExpression('/onclick\s*=\s*"/', $html, 'Toolbar emitted a double-quoted onclick.');
+ self::assertNotEmpty($this->onclickBodies($html), 'Toolbar emitted no single-quoted onclick handlers.');
+ }
+
+ /**
+ * Whole-toolbar smoke test. Kept because it exercises the core buttons the per-extension
+ * tests do not reach -- but it is NOT the coverage guarantee; see the class docblock.
+ */
+ #[Test]
+ public function toolbarOnclickHandlersSurviveAttributeDecodeWithoutExecutingInjectedCode(): void
+ {
+ $element = new XoopsFormDhtmlTextArea('Caption', self::MALICIOUS_NAME, 'value', 5, 50, 'xoopsHiddenText');
+ $toolbar = new XoopsDhtmlToolbar();
+ $handlers = $this->onclickBodies($toolbar->renderCodeButtons($element));
+
+ self::assertNotEmpty($handlers, 'No onclick handlers were found in the rendered toolbar markup.');
+
+ foreach ($handlers as $handler) {
+ $decoded = html_entity_decode($handler, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+
+ preg_match('/^([A-Za-z0-9_]+)\(/', $decoded, $fnMatch);
+ $handlerName = $fnMatch[1] ?? $handler;
+
+ self::assertDoesNotMatchRegularExpression(
+ '/"\s*\)\s*;\s*alert\s*\(/',
+ $decoded,
+ "{$handlerName}: injected payload closed its JS string literal, letting alert( become callable"
+ );
+ self::assertStringNotContainsString(
+ 'alert',
+ $this->codeOutsideStringLiterals($decoded),
+ "{$handlerName}: the injected payload escaped its JS string literal and became "
+ . "executable code"
+ );
+ }
+ }
+}
diff --git a/tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php b/tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php
new file mode 100644
index 000000000..145e87dc3
--- /dev/null
+++ b/tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php
@@ -0,0 +1,349 @@
+3rd';
+ }
+}
+
+/**
+ * Unit tests for the shared {@see XoopsDhtmlToolbar}.
+ *
+ * Test 1 is the acceptance test for the whole refactor: all five form renderers must delegate to
+ * the shared toolbar and therefore produce byte-identical toolbar markup for the same element.
+ */
+class XoopsDhtmlToolbarTest extends TestCase
+{
+ private const RENDERER_CLASSES = [
+ 'XoopsFormRendererLegacy',
+ 'XoopsFormRendererBootstrap3',
+ 'XoopsFormRendererBootstrap4',
+ 'XoopsFormRendererBootstrap5',
+ 'XoopsFormRendererTailwind',
+ ];
+
+ /** @var array */
+ private $originalPreloadEvents;
+
+ protected function setUp(): void
+ {
+ $GLOBALS['formtextdhtml_sizes'] = [
+ 'xx-small' => 'xx-Small',
+ 'small' => 'Small',
+ 'medium' => 'Medium',
+ ];
+ $GLOBALS['formtextdhtml_fonts'] = ['Arial', 'Courier', 'Georgia'];
+
+ $instance = XoopsPreload::getInstance();
+ $ref = new ReflectionClass($instance);
+ $prop = $ref->getProperty('_events');
+ $prop->setAccessible(true);
+ $this->originalPreloadEvents = $prop->getValue($instance);
+
+ ToolbarCodeiconTestPreload::$called = false;
+
+ $this->resetStylesheetGuard();
+ }
+
+ protected function tearDown(): void
+ {
+ $instance = XoopsPreload::getInstance();
+ $ref = new ReflectionClass($instance);
+ $prop = $ref->getProperty('_events');
+ $prop->setAccessible(true);
+ $prop->setValue($instance, $this->originalPreloadEvents);
+
+ unset($GLOBALS['formtextdhtml_sizes'], $GLOBALS['formtextdhtml_fonts']);
+ }
+
+ // =========================================================================
+ // Test 1 — the acceptance test
+ // =========================================================================
+
+ /**
+ * All five renderers must produce the IDENTICAL toolbar string for the same element. This is
+ * the whole point of the refactor.
+ */
+ public function testAllFiveRenderersProduceIdenticalToolbarOutput(): void
+ {
+ $results = [];
+ foreach (self::RENDERER_CLASSES as $class) {
+ $element = $this->makeElement();
+ $renderer = new $class();
+ $code = $this->callProtected($renderer, 'renderFormDhtmlTAXoopsCode', [$element]);
+ $typography = $this->callProtected($renderer, 'renderFormDhtmlTATypography', [$element]);
+ $results[$class] = $code . $typography;
+ }
+
+ $expected = reset($results);
+ $this->assertNotSame('', $expected, 'Precondition: toolbar output must not be empty');
+ $this->assertStringContainsString('xo-edtb-btn', $expected);
+
+ foreach ($results as $class => $actual) {
+ $this->assertSame($expected, $actual, "{$class}'s toolbar output diverges from the others");
+ }
+ }
+
+ /**
+ * Prove genuine delegation, not coincidence: the per-renderer combined output must equal the
+ * shared toolbar's own rows built directly (renderCodeButtons + renderTypography +
+ * renderCheckLength), for every one of the five renderers.
+ */
+ public function testEveryRendererDelegatesToTheSameSharedToolbarInstanceShape(): void
+ {
+ $toolbarElement = $this->makeElement();
+ $toolbar = new XoopsDhtmlToolbar();
+ $direct = $toolbar->renderCodeButtons($toolbarElement)
+ . $toolbar->renderTypography($toolbarElement)
+ . $toolbar->renderCheckLength($toolbarElement);
+
+ foreach (self::RENDERER_CLASSES as $class) {
+ $element = $this->makeElement();
+ $renderer = new $class();
+ $code = $this->callProtected($renderer, 'renderFormDhtmlTAXoopsCode', [$element]);
+ $typography = $this->callProtected($renderer, 'renderFormDhtmlTATypography', [$element]);
+ $this->assertSame($direct, $code . $typography, "{$class} did not delegate to XoopsDhtmlToolbar");
+ }
+ }
+
+ // =========================================================================
+ // Test 2 — the codeicon preload event contract
+ // =========================================================================
+
+ /**
+ * The `core.class.xoopsform.formdhtmltextarea.codeicon` event fires with the code row by
+ * reference, after the core buttons and TextSanitizer extensions, immediately before the code
+ * row is returned — so a listener's appended markup must be present, and at the tail, of the
+ * returned string.
+ */
+ public function testCodeiconPreloadEventFiresByReferenceAtSameLogicalPoint(): void
+ {
+ $this->injectCodeiconHandler();
+
+ $element = $this->makeElement();
+ $toolbar = new XoopsDhtmlToolbar();
+ $code = $toolbar->renderCodeButtons($element);
+
+ $this->assertTrue(ToolbarCodeiconTestPreload::$called, 'codeicon preload handler was not invoked');
+ $this->assertStringContainsString('data-testid="thirdparty-codeicon"', $code);
+ $this->assertStringEndsWith('3rd ', $code);
+ }
+
+ // =========================================================================
+ // Test 3 — TextSanitizer extension contract
+ // =========================================================================
+
+ /**
+ * Extension-supplied 'btn btn-default btn-sm' markup is rewritten to the toolbar's neutral
+ * classes, and any $js an extension returns lands on $element->js. Exercised end to end via
+ * the bundled 'youtube' extension, which is enabled in the shipped textsanitizer config and
+ * hardcodes those Bootstrap classes in its own encode() output.
+ */
+ public function testExtensionButtonClassesAreRewrittenAndJsLandsOnElement(): void
+ {
+ $element = $this->makeElement();
+ $toolbar = new XoopsDhtmlToolbar();
+ $code = $toolbar->renderCodeButtons($element);
+
+ $this->assertStringNotContainsString('btn-default', $code);
+ $this->assertStringContainsString('xo-edtb-btn xo-edtb-btn-sm', $code);
+ $this->assertStringContainsString('xoopsCodeYoutube', $code);
+ $this->assertStringContainsString('function xoopsCodeYoutube', $element->js);
+ }
+
+ /**
+ * Focused unit test of the rewrite rule itself, independent of which bundled extension
+ * happens to be enabled.
+ */
+ public function testRewriteExtensionButtonClassesReplacesKnownBootstrapClasses(): void
+ {
+ $toolbar = new XoopsDhtmlToolbar();
+ $input = "x y ";
+
+ $output = $this->callProtected($toolbar, 'rewriteExtensionButtonClasses', [$input]);
+
+ $this->assertStringNotContainsString('btn-default', $output);
+ $this->assertStringNotContainsString('btn-secondary', $output);
+ $this->assertStringContainsString("class='xo-edtb-btn xo-edtb-btn-sm'", $output);
+ $this->assertStringContainsString("class='xo-edtb-btn'>y", $output);
+ }
+
+ // =========================================================================
+ // Test 4 — no document.write, no eval(
+ // =========================================================================
+
+ public function testOutputContainsNoDocumentWriteOrEval(): void
+ {
+ $element = $this->makeElement();
+ $toolbar = new XoopsDhtmlToolbar();
+ $full = $toolbar->render($element);
+
+ $this->assertStringNotContainsString('document.write', $full);
+ $this->assertStringNotContainsString('eval(', $full);
+ }
+
+ // =========================================================================
+ // Test 5 — $GLOBALS['formtextdhtml_sizes']/['formtextdhtml_fonts'] overrides
+ // =========================================================================
+
+ public function testGlobalSizeAndFontOverridesAreReflectedInOutput(): void
+ {
+ $GLOBALS['formtextdhtml_sizes'] = ['huge' => 'Huge Size'];
+ $GLOBALS['formtextdhtml_fonts'] = ['CustomFontXYZ'];
+
+ $element = $this->makeElement();
+ $toolbar = new XoopsDhtmlToolbar();
+ $typography = $toolbar->renderTypography($element);
+
+ $this->assertStringContainsString('Huge Size', $typography);
+ $this->assertStringContainsString('huge', $typography);
+ $this->assertStringContainsString('CustomFontXYZ', $typography);
+ }
+
+ // =========================================================================
+ // Test 6 — check-length button
+ // =========================================================================
+
+ public function testCheckLengthButtonCarriesConfiguredMaxlength(): void
+ {
+ $element = $this->makeElement();
+ $element->configs = ['maxlength' => 12345];
+
+ $toolbar = new XoopsDhtmlToolbar();
+ $checkLength = $toolbar->renderCheckLength($element);
+
+ $this->assertStringContainsString('XoopsCheckLength', $checkLength);
+ $this->assertStringContainsString('12345', $checkLength);
+ $this->assertStringContainsString($element->getName(), $checkLength);
+ }
+
+ public function testCheckLengthButtonDefaultsToZeroWhenConfigsNotSet(): void
+ {
+ $element = $this->makeElement();
+ $toolbar = new XoopsDhtmlToolbar();
+
+ $checkLength = $toolbar->renderCheckLength($element);
+
+ $this->assertStringContainsString('XoopsCheckLength', $checkLength);
+ $this->assertStringContainsString('"0"', $checkLength);
+ }
+
+ // =========================================================================
+ // Stylesheet injection — Part 3 contract
+ // =========================================================================
+
+ public function testStylesheetIsInjectedOnceAcrossMultipleEditorsOnOnePage(): void
+ {
+ $toolbar = new XoopsDhtmlToolbar();
+
+ $first = $toolbar->render($this->makeElement('editorOne'));
+ $second = $toolbar->render($this->makeElement('editorTwo'));
+
+ $this->assertStringContainsString('toolbar.css', $first);
+ $this->assertStringNotContainsString('toolbar.css', $second);
+ }
+
+ // =========================================================================
+ // Helpers
+ // =========================================================================
+
+ private function makeElement(string $name = 'toolbarfield'): XoopsFormDhtmlTextArea
+ {
+ return new XoopsFormDhtmlTextArea('Caption', $name, 'value', 5, 50, 'xoopsHiddenText');
+ }
+
+ /**
+ * @param object $obj
+ * @param string $method
+ * @param array $args
+ *
+ * @return mixed
+ */
+ private function callProtected(object $obj, string $method, array $args = [])
+ {
+ $ref = new ReflectionMethod($obj, $method);
+ $ref->setAccessible(true);
+
+ return $ref->invokeArgs($obj, $args);
+ }
+
+ private function resetStylesheetGuard(): void
+ {
+ $ref = new ReflectionClass(XoopsDhtmlToolbar::class);
+ $prop = $ref->getProperty('styleInjected');
+ $prop->setAccessible(true);
+ $prop->setValue(null, false);
+ }
+
+ private function injectCodeiconHandler(): void
+ {
+ $instance = XoopsPreload::getInstance();
+ $ref = new ReflectionClass($instance);
+ $prop = $ref->getProperty('_events');
+ $prop->setAccessible(true);
+ $events = $prop->getValue($instance);
+
+ // Normalized form of 'core.class.xoopsform.formdhtmltextarea.codeicon': dots stripped,
+ // lowercased — see XoopsPreload::triggerEvent().
+ $events['coreclassxoopsformformdhtmltextareacodeicon'][] = [
+ 'class_name' => ToolbarCodeiconTestPreload::class,
+ 'method' => 'eventCodeiconHandler',
+ ];
+ $prop->setValue($instance, $events);
+ }
+}
diff --git a/tests/unit/htdocs/class/xoopsform/XoopsFormRendererEscapingTest.php b/tests/unit/htdocs/class/xoopsform/XoopsFormRendererEscapingTest.php
new file mode 100644
index 000000000..affd0c39f
--- /dev/null
+++ b/tests/unit/htdocs/class/xoopsform/XoopsFormRendererEscapingTest.php
@@ -0,0 +1,454 @@
+ body.
+ *
+ * Every renderer interpolated $element->getValue() straight into a quoted attribute (button,
+ * color picker, password, text) and straight into a ",
+ * broke out of the element and the rest parsed as markup. Any field whose value can come
+ * from user input -- a search box redisplaying its term, a profile field after a validation
+ * failure, a comment preview -- was an injection point.
+ *
+ * WHAT IS ASSERTED
+ * ----------------
+ * Attribute sites: containment, measured against a benign baseline render of the same
+ * element. A hostile value may not ADD an element, a ` must not
+ * reach the output as `',
+ 'entity then image' => '`"> ',
+ 'whitespace and amp' => "a\nb\tc & d",
+ ];
+
+ /**
+ * Payloads carrying no '<', so they are inert in element TEXT but still close a quoted
+ * attribute.
+ *
+ * KNOWN_TEXT_CONTEXT_GAPS excludes a METHOD; the gap it documents is a CONTEXT. Bootstrap4
+ * and Bootstrap5 renderFormButton also write value= and title= attributes, so skipping the
+ * whole method dropped the containment assertion from those too -- reverting just those two
+ * attribute sites to raw getValue() left this suite green with a live breakout reintroduced.
+ * These payloads restore attribute coverage on the pinned methods without asserting anything
+ * about the text gap they legitimately document.
+ */
+ private const ATTRIBUTE_ONLY_PAYLOADS = [
+ 'single quote breakout' => "x' onfocus='alert(1)' autofocus x",
+ 'double quote breakout' => 'x" onfocus="alert(1)" autofocus x',
+ ];
+
+ /** Value that closes a