Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.270.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Tests:

Forms and templates:
- fix TinyMCE 7 language handling to preserve case-sensitive locale codes such as zh_TW
- fix TinyMCE 7 fallback language handling to emit pack filename codes such as zh_TW/fr_FR/pt_BR and drop the invalid legacy _utf8 suffix (issue #76)

System:
- fix cloning a module block: the save handler now hydrates the module fields (mid, func_num, func_file, show_func, edit_func, template, dirname, name) from the clone form when bid=0, so the clone keeps its module binding and passes name validation; the path/function fields are validated (no traversal, identifier-only) before persisting (issue #73)
Expand Down
118 changes: 114 additions & 4 deletions htdocs/class/xoopseditor/tinymce7/formtinymce.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,80 @@
*/
class XoopsFormTinymce7 extends XoopsEditor
{
private const TINYMCE7_LANGUAGE_MAP = [
'ar' => 'ar',
'bg' => 'bg_BG',
'bg-bg' => 'bg_BG',
'ca' => 'ca',
'cs' => 'cs',
'da' => 'da',
'de' => 'de',
'el' => 'el',
'en' => 'en',
'en-us' => 'en',
'es' => 'es',
'eu' => 'eu',
'fa' => 'fa',
'fi' => 'fi',
'fr' => 'fr_FR',
'fr-fr' => 'fr_FR',
'he' => 'he_IL',
'he-il' => 'he_IL',
'hi' => 'hi',
'hr' => 'hr',
'hu' => 'hu_HU',
'hu-hu' => 'hu_HU',
'id' => 'id',
'in' => 'id',
'iw' => 'he_IL',
'it' => 'it',
'ja' => 'ja',
'kk' => 'kk',
'ko' => 'ko_KR',
'ko-kr' => 'ko_KR',
'ms' => 'ms',
'nb' => 'nb_NO',
'nb-no' => 'nb_NO',
'nl' => 'nl',
'no' => 'nb_NO',
'pl' => 'pl',
'pt' => 'pt_PT',
'pt-br' => 'pt_BR',
'pt-pt' => 'pt_PT',
'ro' => 'ro',
'ru' => 'ru',
'sk' => 'sk',
'sl' => 'sl_SI',
'sl-si' => 'sl_SI',
'sv' => 'sv_SE',
'sv-se' => 'sv_SE',
'th' => 'th_TH',
'th-th' => 'th_TH',
'tr' => 'tr',
'uk' => 'uk',
'vi' => 'vi',
'zh' => 'zh_CN',
'zh-cn' => 'zh_CN',
'zh-hans' => 'zh_CN',
'zh-hant' => 'zh_TW',
'zh-hk' => 'zh_TW',
'zh-tw' => 'zh_TW',
// Collapse the common country variants of languages whose only
// TinyMCE 7 pack is the bare code (no de_DE.js/es_ES.js/...). A
// genuine regional pack (e.g. es_MX) is intentionally NOT aliased
// here so it still resolves via the generic path.
'de-de' => 'de',
'es-es' => 'es',
'it-it' => 'it',
'ja-jp' => 'ja',
'nl-nl' => 'nl',
'pl-pl' => 'pl',
'ru-ru' => 'ru',
'tr-tr' => 'tr',
'uk-ua' => 'uk',
'vi-vn' => 'vi',
];

public $language;
public $width = '100%';
public $height = '500px';
Expand Down Expand Up @@ -99,15 +173,51 @@
if (defined('_XOOPS_EDITOR_TINYMCE7_LANGUAGE')) {
$this->language = constant('_XOOPS_EDITOR_TINYMCE7_LANGUAGE');
} else {
$this->language = str_replace('_', '-', strtolower(_LANGCODE));
if (strtolower(_CHARSET) === 'utf-8') {
$this->language .= '_utf8';
}
$langcode = defined('_LANGCODE') ? (string) constant('_LANGCODE') : 'en';
$this->language = self::normalizeLanguageCode($langcode);
}

return $this->language;
}

/**
* Convert XOOPS language codes to TinyMCE 7 language-pack filenames.
*
* TinyMCE 7 dropped the legacy TinyMCE 3 "_utf8" suffix and requires
* case-sensitive language codes matching the pack filename, e.g. zh_TW.
*/
protected static function normalizeLanguageCode(string $languageCode): string

Check warning on line 189 in htdocs/class/xoopseditor/tinymce7/formtinymce.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This method has 6 returns, which is more than the 3 allowed.

See more on https://sonarcloud.io/project/issues?id=XOOPS_XoopsCore27&issues=AZ41RD1fBrFcw9uE4ZTo&open=AZ41RD1fBrFcw9uE4ZTo&pullRequest=78
{
$languageCode = trim($languageCode);
if ($languageCode === '') {
return 'en';
}

$key = strtolower(str_replace('_', '-', $languageCode));
$key = preg_replace('/[-.]utf-?8$/', '', $key) ?? $key;

if (isset(self::TINYMCE7_LANGUAGE_MAP[$key])) {
return self::TINYMCE7_LANGUAGE_MAP[$key];
}

[$language, $region] = array_pad(explode('-', $key, 2), 2, '');

// Reject malformed tokens: a TinyMCE 7 locale is a 2-3 letter
// language, optionally with a 2-letter region. Anything else
// (symbols, digits, junk) degrades to the built-in English.
if (!preg_match('/^[a-z]{2,3}$/', $language)) {
return 'en';
}
if ($region === '') {
return $language;
}
if (!preg_match('/^[a-z]{2}$/', $region)) {
return 'en';
}

return $language . '_' . strtoupper($region);
}

/**
* prepare HTML for output
*
Expand Down
3 changes: 1 addition & 2 deletions htdocs/class/xoopseditor/tinymce7/language/english.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,5 @@
*/
// Name of the editor
define('_XOOPS_EDITOR_TINYMCE7', 'TinyMCE7');
// The value must be the same as /tinymce/jscripts/langs/your_language_code, for example, "en" for English, "fr" for French
// For details, check http://tinymce.moxiecode.com/download_i18n.php
// The value must match the TinyMCE 7 language-pack filename without ".js", for example "en", "fr_FR", or "zh_TW".
define('_XOOPS_EDITOR_TINYMCE7_LANGUAGE', 'en');
5 changes: 2 additions & 3 deletions htdocs/class/xoopseditor/tinymce7/language/french.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,5 @@
*/
// Name of the editor
define('_XOOPS_EDITOR_TINYMCE7', 'TinyMCE7');
// The value must be the same as /tinymce/jscripts/langs/your_language_code, for example, "en" for English, "fr" for French
// For details, check http://tinymce.moxiecode.com/download_i18n.php
define('_XOOPS_EDITOR_TINYMCE7_LANGUAGE', 'fr');
// The value must match the TinyMCE 7 language-pack filename without ".js", for example "en", "fr_FR", or "zh_TW".
define('_XOOPS_EDITOR_TINYMCE7_LANGUAGE', 'fr_FR');

Check failure on line 25 in htdocs/class/xoopseditor/tinymce7/language/french.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this constant "_XOOPS_EDITOR_TINYMCE7_LANGUAGE" to match the regular expression ^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$.

See more on https://sonarcloud.io/project/issues?id=XOOPS_XoopsCore27&issues=AZ41RDzqBrFcw9uE4ZTn&open=AZ41RDzqBrFcw9uE4ZTn&pullRequest=78
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace xoopseditor\tinymce7;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\PreserveGlobalState;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -49,4 +50,76 @@

self::assertSame('zh_TW', $editor->getLanguage());
}

/**
* @return array<string, array{string, string}>
*/
public static function fallbackLanguageCodeProvider(): array
{
return [
'english remains default TinyMCE code' => ['en', 'en'],
'french maps to TinyMCE 7 filename' => ['fr', 'fr_FR'],
'traditional chinese underscore' => ['zh_TW', 'zh_TW'],
'traditional chinese hyphen' => ['zh-tw', 'zh_TW'],
'legacy TinyMCE 3 utf8 suffix is stripped' => ['zh-tw_utf8', 'zh_TW'],
'brazilian portuguese preserves region case' => ['pt_BR', 'pt_BR'],
'swedish maps to TinyMCE 7 filename' => ['sv', 'sv_SE'],
'custom regional packs keep TinyMCE 7 separator style' => ['es-mx', 'es_MX'],
'germany country variant collapses to bare pack' => ['de_DE', 'de'],
'spain country variant collapses to bare pack' => ['es_ES', 'es'],
'malformed token falls back to english' => ['@@', 'en'],
'invalid region falls back to english' => ['es-123', 'en'],
'empty language falls back to english' => ['', 'en'],
];
}

#[DataProvider('fallbackLanguageCodeProvider')]
public function testFallbackLanguageCodesUseTinymce7LocaleFormat(string $langcode, string $expected): void
{
$editor = new class extends \XoopsFormTinymce7 {
public function __construct()
{
// Intentionally empty: this test only exercises the language
// formatter used by the getLanguage() fallback branch.
}

public function normalizeForTest(string $langcode): string
{
return self::normalizeLanguageCode($langcode);
}
};

self::assertSame($expected, $editor->normalizeForTest($langcode));
Comment on lines +86 to +92
}

/**
* End-to-end guard: getLanguage() must actually run _LANGCODE through
* the normalizer when _XOOPS_EDITOR_TINYMCE7_LANGUAGE is absent — i.e.
* proves the wiring of the #76 fix, not just the helper in isolation.
*
* Separate process because it has to define() the _LANGCODE global
* constant (a PHP constant cannot be unset). Local Windows PHPUnit
* process isolation has been flaky in this repo, but CI runs the
* existing isolated test reliably.
*/
#[RunInSeparateProcess]
#[PreserveGlobalState(false)]
public function testGetLanguageFallbackUsesLangcodeNormalizer(): void
{
if (defined('_XOOPS_EDITOR_TINYMCE7_LANGUAGE') || defined('_LANGCODE')) {
self::markTestSkipped('Locale constants already defined; cannot exercise the fallback branch in isolation.');
}

define('_LANGCODE', 'zh-tw_utf8');

Check failure on line 113 in tests/unit/htdocs/class/xoopseditor/tinymce7/XoopsFormTinymce7Test.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this constant "_LANGCODE" to match the regular expression ^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$.

See more on https://sonarcloud.io/project/issues?id=XOOPS_XoopsCore27&issues=AZ41UWiPGVej_5GpKgFA&open=AZ41UWiPGVej_5GpKgFA&pullRequest=78

$editor = new class extends \XoopsFormTinymce7 {
public function __construct()
{
// Intentionally empty: bypass the heavy parent constructor;
// only the getLanguage() fallback branch is under test.
}
};

self::assertSame('zh_TW', $editor->getLanguage());
}
}