From be6af8fab8dc83e93d6cf0b65a396e2cfbad785b Mon Sep 17 00:00:00 2001 From: William Jacoby Date: Thu, 12 Nov 2026 00:09:34 -0600 Subject: [PATCH 1/2] Add an optional introduction above the Knowledge Base index The ACP configuration page gets a switch, a text box and a Markdown option for a short introduction shown under the Knowledge Base title, on the hierarchical index and the flat index. It is written in BBCode or Markdown and stored through the same article_text service as articles. Migration version_2_0_33 adds it switched off, holding the text kinerity/knowledgebase showed above its index, and switches it on on a board imported from kinerity, so its visitors keep seeing it. Tests: kb_intro_test.php (default text, display switch, saving BBCode and Markdown, emptying, the migration and its kinerity check). Co-Authored-By: Claude Opus 5.5 --- README.md | 2 + adm/style/acp_knowledgebase_config.html | 29 ++++ config/services.yml | 11 ++ controller/acp_controller.php | 19 ++- controller/category.php | 12 +- controller/index.php | 8 +- docs/testing.md | 20 +++ docs/upgrading-from-kinerity.md | 5 + inc/intro.php | 150 ++++++++++++++++++ language/en/acp_knowledgebase.php | 6 + language/en/default_content.php | 1 + migrations/version_2_0_33.php | 65 ++++++++ styles/all/template/kb_cat_body.html | 1 + styles/all/template/kb_index_body.html | 1 + styles/all/template/kb_intro.html | 3 + .../all/theme/phpbbmodders_knowledgebase.css | 7 + tests/kb_intro_test.php | 129 +++++++++++++++ 17 files changed, 466 insertions(+), 3 deletions(-) create mode 100644 inc/intro.php create mode 100644 migrations/version_2_0_33.php create mode 100644 styles/all/template/kb_intro.html create mode 100644 tests/kb_intro_test.php diff --git a/README.md b/README.md index 1e07524..7f0538a 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ The Knowledge Base follows each member's board style. It starts as a flat articl index (one list of every article, with a category dropdown); hierarchical category browsing and an optional colour stylesheet (a bundled one, or your own CSS file) are set on the same ACP page; see the [styling guide](docs/TODO/archive/styling/README.md). +The same page can switch on a short introduction above the article index, written in +BBCode or Markdown; it is off until you switch it on. A search of the board also shows the matching articles in a **Knowledge Base** block above the forum results, with an **Also search the Knowledge Base** option in the advanced search. diff --git a/adm/style/acp_knowledgebase_config.html b/adm/style/acp_knowledgebase_config.html index 0c1b2c5..13320c2 100644 --- a/adm/style/acp_knowledgebase_config.html +++ b/adm/style/acp_knowledgebase_config.html @@ -75,6 +75,35 @@

{{ lang('ACP_KNOWLEDGE_BASE_CONFIGURE') }}

+
+
+
+ {{ lang('KB_INTRO_ENABLE_EXPLAIN') }} +
+
+ +
+
+
+
+
+
+ {{ lang('KB_INTRO_TEXT_EXPLAIN') }} +
+
+
+
+
+
+ {{ lang('KB_INTRO_MARKDOWN_EXPLAIN') }} +
+
+ +
+
+

diff --git a/config/services.yml b/config/services.yml index 7810dd4..28c4fce 100644 --- a/config/services.yml +++ b/config/services.yml @@ -18,6 +18,7 @@ services: - '@phpbbmodders.knowledgebase.inc' - '%tables.articles_table%' - '%tables.categories_table%' + - '@phpbbmodders.knowledgebase.intro' phpbbmodders.knowledgebase.controller.category: class: phpbbmodders\knowledgebase\controller\category @@ -34,6 +35,7 @@ services: - '@phpbbmodders.knowledgebase.inc' - '%tables.articles_table%' - '%tables.categories_table%' + - '@phpbbmodders.knowledgebase.intro' phpbbmodders.knowledgebase.controller.recent_changes: class: phpbbmodders\knowledgebase\controller\recent_changes @@ -262,6 +264,14 @@ services: # Argument order differs between 3.3.x and 4.0; the renderer class copes with either. - [configure_user, ['@user', '@config', '@auth']] + phpbbmodders.knowledgebase.intro: + class: phpbbmodders\knowledgebase\inc\intro + arguments: + - '@config' + - '@config_text' + - '@phpbbmodders.knowledgebase.article_text' + - '@template' + phpbbmodders.knowledgebase.article_text: class: phpbbmodders\knowledgebase\textformatter\article_text arguments: @@ -346,6 +356,7 @@ services: - '%tables.kb_users_table%' - '%tables.kb_groups_table%' - '@phpbbmodders.knowledgebase.alternative_styles' + - '@phpbbmodders.knowledgebase.intro' phpbbmodders.knowledgebase.controller.set_order: class: phpbbmodders\knowledgebase\controller\set_order diff --git a/controller/acp_controller.php b/controller/acp_controller.php index bdbb410..2f29702 100644 --- a/controller/acp_controller.php +++ b/controller/acp_controller.php @@ -34,6 +34,7 @@ use phpbbmodders\knowledgebase\inc\field\kb_field_type_factory; use phpbbmodders\knowledgebase\inc\icon; use phpbbmodders\knowledgebase\inc\functions_kb; +use phpbbmodders\knowledgebase\inc\intro; use phpbbmodders\knowledgebase\inc\upload_directory; use phpbbmodders\knowledgebase\search\kb_search_backend_factory; @@ -165,6 +166,9 @@ protected functions_kb $kb; * @param string $kb_groups_table * @param alternative_styles $alt_styles */ + /** @var intro|null The index introduction text; optional so older callers still build */ + protected intro|null $intro; + public function __construct( driver_interface $db, config $config, db_text $config_text, helper $helper, manager $ext_manager, language $language, auth $auth, request_interface $request, template $template, user $user, @@ -173,9 +177,10 @@ public function __construct( kb_field_type_factory $field_factory, string $phpbb_root_path, string $php_ext, string $articles_table, string $categories_table, string $logs_table, string $attachments_table, string $options_table, string $kb_users_table, - string $kb_groups_table, alternative_styles $alt_styles + string $kb_groups_table, alternative_styles $alt_styles, intro|null $intro = null ) { + $this->intro = $intro; $this->db = $db; $this->config = $config; $this->config_text = $config_text; @@ -282,6 +287,10 @@ public function settings(): void 'EXTGROUP_FILESIZE' => $max_filesize, 'ADVANCED_FORM' => $announce_forum_id ? '' : 'none', 'HIERARCHICAL_INDEX_ON' => $this->config['kb_hierarchical_index'] ? 'checked="checked"' : '', + 'INTRO_ON' => $this->intro && $this->intro->is_enabled() ? 'checked="checked"' : '', + 'INTRO_DISPLAY' => $this->intro && $this->intro->is_enabled() ? '' : 'none', + 'INTRO_TEXT' => $this->intro ? $this->intro->for_edit() : '', + 'INTRO_MARKDOWN_ON' => $this->intro && $this->intro->is_markdown() ? 'checked="checked"' : '', 'ALT_STYLE_ON' => $this->config['kb_alt_style_enabled'] ? 'checked="checked"' : '', 'ALT_STYLE_DISPLAY' => $this->config['kb_alt_style_enabled'] ? '' : 'none', 'S_ALT_STYLE_OPTIONS' => $this->alternative_style_options((string) $this->config['kb_alt_style']), @@ -360,6 +369,14 @@ protected function set_settings(): void $this->config->set('kb_font_icon', $kb_font_icon); $this->config->set('kb_hierarchical_index', $this->request->variable('hierarchical_index', 0) ? 1 : 0); + if ($this->intro) + { + $this->intro->save( + (bool) $this->request->variable('intro_enabled', 0), + utf8_normalize_nfc($this->request->variable('intro_text', '', true)), + (bool) $this->request->variable('intro_markdown', 0) + ); + } $this->config->set('kb_feed_format', $feed_format); $this->save_alternative_style(); $this->save_announcement_forum(); diff --git a/controller/category.php b/controller/category.php index dd75335..8b88a7b 100644 --- a/controller/category.php +++ b/controller/category.php @@ -22,6 +22,7 @@ use phpbb\template\template; use phpbb\user; use phpbbmodders\knowledgebase\inc\functions_kb; +use phpbbmodders\knowledgebase\inc\intro; class category { @@ -77,12 +78,16 @@ protected functions_kb $kb; * @param string $articles_table * @param string $categories_table */ + /** @var intro|null The index introduction text */ + protected intro|null $intro; + public function __construct( driver_interface $db, config $config, helper $helper, language $language, auth $auth, request_interface $request, template $template, user $user, pagination $pagination, functions_kb $kb, - string $articles_table, string $categories_table + string $articles_table, string $categories_table, intro|null $intro = null ) { + $this->intro = $intro; $this->db = $db; $this->config = $config; $this->helper = $helper; @@ -473,6 +478,11 @@ public function cat(): \Symfony\Component\HttpFoundation\Response } $this->kb->assign_nav_tabs_vars('index', $cat_id); + // The flat index (all categories) is the Knowledge Base index in its default layout + if ($flat_all) + { + $this->intro?->assign(); + } return $this->helper->render('@phpbbmodders_knowledgebase/kb_cat_body.html', ($this->language->lang('LIBRARY') . ' » ' . $this->language->lang('CATEGORY'))); } diff --git a/controller/index.php b/controller/index.php index e8b6cff..b5cdf23 100644 --- a/controller/index.php +++ b/controller/index.php @@ -22,6 +22,7 @@ use phpbb\template\template; use phpbb\user; use phpbbmodders\knowledgebase\inc\functions_kb; +use phpbbmodders\knowledgebase\inc\intro; class index { @@ -77,12 +78,16 @@ protected functions_kb $kb; * @param string $articles_table * @param string $categories_table */ + /** @var intro|null The index introduction text */ + protected intro|null $intro; + public function __construct( driver_interface $db, config $config, helper $helper, language $language, auth $auth, request_interface $request, template $template, user $user, pagination $pagination, functions_kb $kb, - string $articles_table, string $categories_table + string $articles_table, string $categories_table, intro|null $intro = null ) { + $this->intro = $intro; $this->db = $db; $this->config = $config; $this->helper = $helper; @@ -178,6 +183,7 @@ public function main(): \Symfony\Component\HttpFoundation\Response ]); $this->kb->assign_nav_tabs_vars('index'); + $this->intro?->assign(); return $this->helper->render('@phpbbmodders_knowledgebase/kb_index_body.html', $this->language->lang('LIBRARY')); } diff --git a/docs/testing.md b/docs/testing.md index 509a2c7..7ba6c52 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1487,3 +1487,23 @@ is a separate piece of work, not part of this change. **Not covered.** Boards upgraded from phpBB 3.3 to 4.0 with kinerity data (the sequence case is covered by PHPUnit only); a custom style; kinerity versions before 1.2.1 on a board (1.0.x's missing column is covered by PHPUnit only); MariaDB. + +## Introduction text on the index - 11/12/2026 + +`inc/intro.php` shows an administrator's introduction under the Knowledge Base title, on the +hierarchical index and on the flat index (all categories). It is set on the ACP configuration page: +a switch, the text, and whether it is Markdown; BBCode, smilies and links work as in an article, +through the same `article_text` service. Migration `version_2_0_33` adds it switched off, holding +the text kinerity/knowledgebase showed, and switches it on on a board imported from kinerity. + +`tests/kb_intro_test.php` covers the stored default text, nothing shown while switched off or empty, +saving BBCode and Markdown, emptying it, and the migration's steps and its kinerity check. + +Claude ran the migration on the phpBB 3.3 PostgreSQL board imported from kinerity: it came up +switched on with kinerity's text, in the same place as on the kinerity board beside it. In the ACP, +the text was saved as BBCode (bold, a link, a smiley, `&` and quotes) and came back unchanged in the +editor, then as Markdown (bold, a link, inline code); both displayed formatted on the flat index. It +also showed on the hierarchical index, and nothing showed once it was switched off. + +Not covered: a board that never ran kinerity in a browser (the migration's off-by-default path is +covered by PHPUnit), and other languages (the default text is English, as kinerity's was). diff --git a/docs/upgrading-from-kinerity.md b/docs/upgrading-from-kinerity.md index c1c32d8..daf2798 100644 --- a/docs/upgrading-from-kinerity.md +++ b/docs/upgrading-from-kinerity.md @@ -85,6 +85,11 @@ tables are kept, unchanged. A group or user that already has a setting for `u_kb_view` or `a_manage_kb` keeps it. +- **The introduction text** kinerity showed above its index. This extension's + own introduction (Extensions → Knowledge Base → Configuration) starts out + with kinerity's text and is switched on after an import, so visitors still + see it. Edit it or switch it off there. + ## What is removed After the import, kinerity's permissions (with their settings in roles, diff --git a/inc/intro.php b/inc/intro.php new file mode 100644 index 0000000..4485cb2 --- /dev/null +++ b/inc/intro.php @@ -0,0 +1,150 @@ +/default_content.php (the text kinerity/knowledgebase + * showed on its index) in the board's default language. + */ +class intro +{ + protected config $config; + protected db_text $config_text; + protected article_text $article_text; + protected template $template; + + /** + * @param config $config + * @param db_text $config_text + * @param article_text $article_text + * @param template $template + */ + public function __construct(config $config, db_text $config_text, article_text $article_text, template $template) + { + $this->config = $config; + $this->config_text = $config_text; + $this->article_text = $article_text; + $this->template = $template; + } + + /** + * The stored form of plain text with no BBCode, as phpBB's parser would + * store it, for the default text a migration writes. + * + * @param string $text Plain text, not escaped + * @return string + */ + public static function stored_plain_text(string $text): string + { + return '' . htmlspecialchars($text, ENT_COMPAT, 'UTF-8') . ''; + } + + /** + * Whether the introduction is switched on. + * + * @return bool + */ + public function is_enabled(): bool + { + return !empty($this->config['kb_intro_enabled']); + } + + /** + * Whether the stored text is Markdown. + * + * @return bool + */ + public function is_markdown(): bool + { + return !empty($this->config['kb_intro_markdown']); + } + + /** + * Put the introduction on the page being built, when it is switched on + * and not empty: KB_INTRO (HTML) and S_KB_INTRO. + * + * @return void + */ + public function assign(): void + { + $html = $this->is_enabled() ? $this->html() : ''; + + $this->template->assign_vars([ + 'S_KB_INTRO' => $html !== '', + 'KB_INTRO' => $html, + ]); + } + + /** + * The introduction as HTML, whether or not it is switched on. + * + * @return string Empty when there is no text + */ + public function html(): string + { + $stored = (string) $this->config_text->get('kb_intro_text'); + if (trim(strip_tags($stored)) === '') + { + return ''; + } + + return $this->article_text->display($stored, (string) ($this->config['kb_intro_uid'] ?? ''), $this->is_markdown()); + } + + /** + * The introduction as the administrator typed it, escaped for a textarea. + * + * @return string + */ + public function for_edit(): string + { + return $this->article_text->for_edit((string) $this->config_text->get('kb_intro_text')); + } + + /** + * Save the introduction from the ACP form. + * + * @param bool $enabled + * @param string $text Submitted text, escaped by the request layer + * @param bool $markdown True to parse it as Markdown, false for BBCode + * @return void + */ + public function save(bool $enabled, string $text, bool $markdown): void + { + $uid = $bitfield = ''; + $options = 0; + if (trim($text) !== '') + { + $this->article_text->store($text, $markdown, $uid, $bitfield, $options); + } + else + { + $text = ''; + } + + $this->config_text->set('kb_intro_text', $text); + $this->config->set('kb_intro_uid', (string) $uid); + $this->config->set('kb_intro_markdown', $markdown ? 1 : 0); + $this->config->set('kb_intro_enabled', $enabled ? 1 : 0); + } +} diff --git a/language/en/acp_knowledgebase.php b/language/en/acp_knowledgebase.php index cc508bc..f69c1d1 100644 --- a/language/en/acp_knowledgebase.php +++ b/language/en/acp_knowledgebase.php @@ -92,6 +92,12 @@ 'KB_FEED_FORMAT_INVALID' => 'The feed format you chose is not one of the choices.', 'KB_HIERARCHICAL_INDEX' => 'Hierarchical category browsing', 'KB_HIERARCHICAL_INDEX_EXPLAIN' => 'Off by default: the Knowledge Base then shows the flat article index, a single dropdown listing every category and one list of every article across every category, with a compact layout for the article tables. When enabled, the index shows the category tree with its subcategories instead. The colours stay those of the board style either way.', +'KB_INTRO_ENABLE' => 'Show an introduction', +'KB_INTRO_ENABLE_EXPLAIN' => 'Shows the text below at the top of the Knowledge Base index, under its title.', +'KB_INTRO_TEXT' => 'Introduction text', +'KB_INTRO_TEXT_EXPLAIN' => 'BBCode, smilies and links work as in an article. Leave it empty to show nothing.', +'KB_INTRO_MARKDOWN' => 'Write the introduction in Markdown', +'KB_INTRO_MARKDOWN_EXPLAIN' => 'Formats the text as Markdown, as for a Markdown article. BBCode still works alongside it.', 'KB_ALT_STYLE_ENABLE' => 'Use an alternative stylesheet', 'KB_ALT_STYLE_ENABLE_EXPLAIN' => 'Recolours the Knowledge Base’s tabs, header bars and article tables over the board style. Off by default: the Knowledge Base then uses the colours of each member’s own board style. The stylesheet you choose below is remembered while this is off.', 'KB_ALT_STYLE' => 'Stylesheet', diff --git a/language/en/default_content.php b/language/en/default_content.php index 36a6146..c63d777 100644 --- a/language/en/default_content.php +++ b/language/en/default_content.php @@ -25,4 +25,5 @@ $lang = array_merge($lang, [ 'KB_IMPORTED_NO_CATEGORY' => 'Imported articles without a category', + 'KB_INTRO_DEFAULT_TEXT' => 'This section contains detailed articles elaborating on some of the common issues users encounter. Articles submitted by members of the community are checked for accuracy by the team. If you do not find the answer to your question here, we recommend looking through the forums as well as using the search.', ]); diff --git a/migrations/version_2_0_33.php b/migrations/version_2_0_33.php new file mode 100644 index 0000000..d7d4a8f --- /dev/null +++ b/migrations/version_2_0_33.php @@ -0,0 +1,65 @@ +config['kb_intro_enabled']); + } + + public function update_data() + { + return [ + ['config.add', ['kb_intro_enabled', 0]], + ['config.add', ['kb_intro_markdown', 0]], + ['config.add', ['kb_intro_uid', '']], + ['config_text.add', ['kb_intro_text', intro::stored_plain_text(default_content::lang((string) ($this->config['default_lang'] ?? 'en'), 'KB_INTRO_DEFAULT_TEXT'))]], + ['if', [ + ['custom', [[$this, 'came_from_kinerity']]], + ['config.update', ['kb_intro_enabled', 1]], + ]], + ['config.update', ['kb_version', '2.0.33']], + ]; + } + + public function revert_data() + { + return [ + ['config.update', ['kb_version', '2.0.32']], + ]; + } + + /** + * @return bool Whether this board's data was imported from kinerity/knowledgebase + */ + public function came_from_kinerity() + { + return (new kinerity_import($this->db, $this->db_tools, $this->table_prefix))->has_backup(); + } +} diff --git a/styles/all/template/kb_cat_body.html b/styles/all/template/kb_cat_body.html index 4237463..7c324dd 100644 --- a/styles/all/template/kb_cat_body.html +++ b/styles/all/template/kb_cat_body.html @@ -1,5 +1,6 @@ {% INCLUDE 'overall_header.html' %}

{{ lang('LIBRARY') }}

+{% INCLUDE '@phpbbmodders_knowledgebase/kb_intro.html' %} {% INCLUDE '@phpbbmodders_knowledgebase/kb_nav_tabs.html' %} {% if S_FLAT_LAYOUT %}
diff --git a/styles/all/template/kb_index_body.html b/styles/all/template/kb_index_body.html index 95e55d9..56926ca 100644 --- a/styles/all/template/kb_index_body.html +++ b/styles/all/template/kb_index_body.html @@ -1,5 +1,6 @@ {% INCLUDE 'overall_header.html' %}

{{ lang('LIBRARY') }}

+{% INCLUDE '@phpbbmodders_knowledgebase/kb_intro.html' %} {% INCLUDE '@phpbbmodders_knowledgebase/kb_nav_tabs.html' %}
{% if S_IS_SEARCH %} diff --git a/styles/all/template/kb_intro.html b/styles/all/template/kb_intro.html new file mode 100644 index 0000000..c63290e --- /dev/null +++ b/styles/all/template/kb_intro.html @@ -0,0 +1,3 @@ +{% if S_KB_INTRO %} +
{{ KB_INTRO }}
+{% endif %} diff --git a/styles/all/theme/phpbbmodders_knowledgebase.css b/styles/all/theme/phpbbmodders_knowledgebase.css index a001f57..4fdb9c1 100644 --- a/styles/all/theme/phpbbmodders_knowledgebase.css +++ b/styles/all/theme/phpbbmodders_knowledgebase.css @@ -406,3 +406,10 @@ table.kb-index-table .kb-index-author { margin-right: 0; } } + +/* Introduction text under the Knowledge Base title */ +.kb-intro { + margin: 0 0 10px; + font-size: 1.1em; + line-height: 1.4em; +} diff --git a/tests/kb_intro_test.php b/tests/kb_intro_test.php new file mode 100644 index 0000000..82cc8be --- /dev/null +++ b/tests/kb_intro_test.php @@ -0,0 +1,129 @@ +createMock(\phpbb\config\db_text::class); + $config_text->method('get')->willReturnCallback(function ($key) use (&$texts) { + return $texts[$key] ?? null; + }); + $config_text->method('set')->willReturnCallback(function ($key, $value) use (&$texts) { + $texts[$key] = $value; + }); + + $article_text = $this->createMock(\phpbbmodders\knowledgebase\textformatter\article_text::class); + $article_text->method('store')->willReturnCallback(function (&$text, $markdown, &$uid, &$bitfield, &$options) { + $text = ($markdown ? '' : '') . $text . ''; + $uid = $markdown ? '' : 'abc123'; + }); + $article_text->method('display')->willReturnCallback(fn($stored, $uid, $markdown) => 'HTML[' . ($markdown ? 'md' : 'bb') . ':' . $uid . ':' . $stored . ']'); + $article_text->method('for_edit')->willReturnCallback(fn($stored) => 'EDIT[' . $stored . ']'); + + $template = $this->createMock(\phpbb\template\template::class); + $template->method('assign_vars')->willReturnCallback(function ($vars) { + $this->assigned = array_merge($this->assigned, $vars); + }); + + return new intro($config, $config_text, $article_text, $template); + } + + public function test_the_default_is_kinerity_text_stored_as_plain_text() + { + $this->assertStringStartsWith('This section contains detailed articles', default_content::lang('en', 'KB_INTRO_DEFAULT_TEXT')); + $this->assertSame('A & B "C" <D>', intro::stored_plain_text('A & B "C" ')); + } + + public function test_nothing_is_shown_while_it_is_switched_off() + { + $texts = ['kb_intro_text' => 'Hello']; + $this->intro(new \phpbb\config\config(['kb_intro_enabled' => 0]), $texts)->assign(); + + $this->assertSame(['S_KB_INTRO' => false, 'KB_INTRO' => ''], $this->assigned); + } + + public function test_it_is_shown_when_switched_on() + { + $texts = ['kb_intro_text' => 'Hello']; + $config = new \phpbb\config\config(['kb_intro_enabled' => 1, 'kb_intro_uid' => 'u1', 'kb_intro_markdown' => 0]); + $this->intro($config, $texts)->assign(); + + $this->assertSame(['S_KB_INTRO' => true, 'KB_INTRO' => 'HTML[bb:u1:Hello]'], $this->assigned); + } + + public function test_an_empty_text_shows_nothing_even_when_switched_on() + { + foreach (['', '', ' '] as $stored) + { + $this->assigned = []; + $texts = ['kb_intro_text' => $stored]; + $this->intro(new \phpbb\config\config(['kb_intro_enabled' => 1]), $texts)->assign(); + $this->assertFalse($this->assigned['S_KB_INTRO'], var_export($stored, true)); + } + } + + public function test_saving_stores_the_parsed_text_and_the_switches() + { + $texts = []; + $config = new \phpbb\config\config([]); + $intro = $this->intro($config, $texts); + + $intro->save(true, '[b]Hi[/b]', false); + $this->assertSame('[b]Hi[/b]', $texts['kb_intro_text']); + $this->assertSame(['abc123', 0, 1], [$config['kb_intro_uid'], (int) $config['kb_intro_markdown'], (int) $config['kb_intro_enabled']]); + $this->assertSame('EDIT[[b]Hi[/b]]', $intro->for_edit()); + + $intro->save(false, '# Hi', true); + $this->assertSame('# Hi', $texts['kb_intro_text']); + $this->assertSame(['', 1, 0], [$config['kb_intro_uid'], (int) $config['kb_intro_markdown'], (int) $config['kb_intro_enabled']]); + $this->assertSame('HTML[md::# Hi]', $intro->html()); + + // Emptying the box clears the text + $intro->save(true, ' ', false); + $this->assertSame('', $texts['kb_intro_text']); + $this->assertSame('', $intro->html()); + } + + public function test_the_migration_adds_it_switched_off_and_switches_it_on_after_a_kinerity_import() + { + $migration = new \phpbbmodders\knowledgebase\migrations\version_2_0_33(new \phpbb\config\config([]), $this->createMock(\phpbb\db\driver\driver_interface::class), $this->createMock(\phpbb\db\tools\tools_interface::class), '', 'php', 'phpbb_', []); + $steps = $migration->update_data(); + + $this->assertContains(['config.add', ['kb_intro_enabled', 0]], $steps); + $this->assertContains(['config_text.add', ['kb_intro_text', intro::stored_plain_text(default_content::lang('en', 'KB_INTRO_DEFAULT_TEXT'))]], $steps); + $this->assertContains(['if', [['custom', [[$migration, 'came_from_kinerity']]], ['config.update', ['kb_intro_enabled', 1]]]], $steps); + $this->assertSame(['\phpbbmodders\knowledgebase\migrations\version_2_0_32'], \phpbbmodders\knowledgebase\migrations\version_2_0_33::depends_on()); + + foreach ([true, false] as $has_backup) + { + $tools = $this->createMock(\phpbb\db\tools\tools_interface::class); + $tools->method('sql_table_exists')->willReturn($has_backup); + $check = new \phpbbmodders\knowledgebase\migrations\version_2_0_33(new \phpbb\config\config([]), $this->createMock(\phpbb\db\driver\driver_interface::class), $tools, '', 'php', 'phpbb_', []); + $this->assertSame($has_backup, $check->came_from_kinerity()); + } + } +} From 77fd8152948b2d6f7ca64c58b0da68cba95c82a6 Mon Sep 17 00:00:00 2001 From: William Jacoby Date: Thu, 12 Nov 2026 05:02:10 -0600 Subject: [PATCH 2/2] Fix the EPV findings from the kinerity import and the intro text EPV fails on htmlspecialchars() in inc/kinerity_import.php (merged in #13, so main fails its EPV check) and inc/intro.php. They now call phpBB's utf8_htmlspecialchars(), which does the same (ENT_COMPAT, UTF-8). inc/default_content.php no longer defines IN_PHPBB itself, which EPV also flagged; phpBB always defines it, and the tests do. Co-Authored-By: Claude Opus 5.5 --- inc/default_content.php | 5 +---- inc/intro.php | 2 +- inc/kinerity_import.php | 6 +++--- tests/kb_intro_test.php | 2 ++ tests/kb_kinerity_import_test.php | 1 + 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/inc/default_content.php b/inc/default_content.php index cd9639c..125849f 100644 --- a/inc/default_content.php +++ b/inc/default_content.php @@ -33,11 +33,8 @@ public static function lang(string $language, string $key): string continue; } + // The language file checks IN_PHPBB, which phpBB always defines $lang = []; - if (!defined('IN_PHPBB')) - { - define('IN_PHPBB', true); - } include $file; if (isset($lang[$key])) { diff --git a/inc/intro.php b/inc/intro.php index 4485cb2..e2159f5 100644 --- a/inc/intro.php +++ b/inc/intro.php @@ -56,7 +56,7 @@ public function __construct(config $config, db_text $config_text, article_text $ */ public static function stored_plain_text(string $text): string { - return '' . htmlspecialchars($text, ENT_COMPAT, 'UTF-8') . ''; + return '' . utf8_htmlspecialchars($text) . ''; } /** diff --git a/inc/kinerity_import.php b/inc/kinerity_import.php index 69765da..de56967 100644 --- a/inc/kinerity_import.php +++ b/inc/kinerity_import.php @@ -132,7 +132,7 @@ public function __construct(driver_interface $db, tools_interface $db_tools, str */ public function fallback_category_name(): string { - return htmlspecialchars(default_content::lang($this->language, 'KB_IMPORTED_NO_CATEGORY'), ENT_COMPAT, 'UTF-8'); + return utf8_htmlspecialchars(default_content::lang($this->language, 'KB_IMPORTED_NO_CATEGORY')); } /** @@ -619,7 +619,7 @@ public static function plain_text(string $stored): string */ public static function category_details(string $stored): string { - return self::fit_escaped(self::encode_supplementary(htmlspecialchars(self::plain_text($stored), ENT_COMPAT, 'UTF-8')), self::TEXT_COLUMN_LENGTH); + return self::fit_escaped(self::encode_supplementary(utf8_htmlspecialchars(self::plain_text($stored))), self::TEXT_COLUMN_LENGTH); } /** @@ -659,7 +659,7 @@ public static function fit_escaped(string $escaped, int $length): string // Never end on part of an entity such as 🚀 (characters // phpBB stores as entities stay entities in $plain) $cut = preg_replace('/&#?[0-9a-z]*$/i', '', mb_substr($plain, 0, $keep, 'UTF-8')); - $fitted = htmlspecialchars(rtrim($cut), ENT_COMPAT, 'UTF-8') . "\u{2026}"; + $fitted = utf8_htmlspecialchars(rtrim($cut)) . "\u{2026}"; if (mb_strlen($fitted, 'UTF-8') <= $length) { return $fitted; diff --git a/tests/kb_intro_test.php b/tests/kb_intro_test.php index 82cc8be..1afee79 100644 --- a/tests/kb_intro_test.php +++ b/tests/kb_intro_test.php @@ -11,6 +11,8 @@ namespace phpbbmodders\knowledgebase\inc; require_once __DIR__ . '/kb_extensions_test.php'; +if (!defined('IN_PHPBB')) define('IN_PHPBB', true); +require_once (getenv('PHPBB_ROOT_PATH') ?: dirname(__DIR__, 4)) . '/includes/utf/utf_tools.php'; require_once dirname(__DIR__) . '/textformatter/article_text.php'; require_once dirname(__DIR__) . '/inc/default_content.php'; require_once dirname(__DIR__) . '/inc/intro.php'; diff --git a/tests/kb_kinerity_import_test.php b/tests/kb_kinerity_import_test.php index f07c23d..06d0653 100644 --- a/tests/kb_kinerity_import_test.php +++ b/tests/kb_kinerity_import_test.php @@ -12,6 +12,7 @@ require_once __DIR__ . '/kb_extensions_test.php'; if (!defined('IN_PHPBB')) define('IN_PHPBB', true); +require_once (getenv('PHPBB_ROOT_PATH') ?: dirname(__DIR__, 4)) . '/includes/utf/utf_tools.php'; if (!defined('CONFIG_TABLE')) define('CONFIG_TABLE', 'phpbb_config'); require_once (getenv('PHPBB_ROOT_PATH') ?: dirname(__DIR__, 4)) . '/includes/functions.php'; require_once dirname(__DIR__) . '/inc/functions_kb.php';