From 88c09e91c1da50d82910a45304bbd95abcca7fbb Mon Sep 17 00:00:00 2001 From: William Jacoby Date: Mon, 19 Oct 2026 01:07:12 -0500 Subject: [PATCH] Run EPV through the organization's strict EPV workflow EPV exits 0 on Error findings, so they never failed this workflow, and pointing it at the extension folder gave a false "Packaging structure" error. EPV now runs in an epv job that calls phpbbmodders/.github's reusable EPV workflow (v1) on phpBB 3.3.x and master, which fails on any Fatal or Error finding not listed in .github/epv-allowed.txt. The test-framework calls get EPV: 0 so EPV doesn't run twice. The 29 htmlspecialchars($value, ENT_COMPAT, 'UTF-8') calls EPV flags now use utf8_htmlspecialchars($value), which is the same call. The calls that use ENT_QUOTES or ENT_XML1 on purpose, the JavaScript-string addslashes() in the search ACP, ext.php's unserialize() and four released migrations without revert_schema() are listed in .github/epv-allowed.txt with their reasons. Co-Authored-By: Claude Opus 5.5 --- .github/epv-allowed.txt | 31 +++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 12 ++++++++++++ controller/acp_templates.php | 4 ++-- controller/category.php | 2 +- controller/template_list.php | 2 +- event/wikilink_listener.php | 4 ++-- wiki/categories.php | 2 +- wiki/edit.php | 14 +++++++------- wiki/links.php | 4 ++-- wiki/move.php | 10 +++++----- wiki/protect.php | 2 +- wiki/readable_diff.php | 4 ++-- wiki/search_results.php | 8 ++++---- wiki/view.php | 2 +- 14 files changed, 72 insertions(+), 29 deletions(-) create mode 100644 .github/epv-allowed.txt diff --git a/.github/epv-allowed.txt b/.github/epv-allowed.txt new file mode 100644 index 0000000..827de96 --- /dev/null +++ b/.github/epv-allowed.txt @@ -0,0 +1,31 @@ +# EPV findings this extension keeps on purpose, checked by the epv job in +# .github/workflows/tests.yml. One finding per line as EPV prints it, without +# "on line N", with paths relative to the extension; give the reason above it. + +# Heading anchors, the table of contents and wiki-link fragments are escaped +# with ENT_QUOTES on purpose; utf8_htmlspecialchars() would only use ENT_COMPAT. +Error: Using htmlspecialchars in /event/wikilink_listener.php +Error: Using htmlspecialchars in /wiki/markup.php +Error: Using htmlspecialchars in /wiki/templates.php + +# Discussion text is re-encoded for XML with ENT_NOQUOTES | ENT_XML1, which +# utf8_htmlspecialchars() can't do. +Error: Using htmlspecialchars in /wiki/discussion.php + +# The search index progress-bar URL is escaped for a JavaScript string, as in +# phpBB's own acp_search.php. +Error: Using addslashes in /controller/acp_search.php + +# ext.php reads phpBB's own serialized migration dependency lists and the +# reparser resume point when it moves an install made under the old vendor +# name, with allowed_classes off, so no objects can be created. +Error: Using unserialize in /ext.php + +# Released migrations that add columns to the wiki_article table, which +# initial_module's revert_schema() drops when the extension's data is deleted, +# so there is nothing for these migrations to revert. Released migrations must +# not be changed. +Error: Migration file /migrations/update_0_2_0.php is missing the revert_schema() method. +Error: Migration file /migrations/update_0_3_0.php is missing the revert_schema() method. +Error: Migration file /migrations/update_0_3_1.php is missing the revert_schema() method. +Error: Migration file /migrations/update_0_3_2.php is missing the revert_schema() method. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3919934..37f179a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,6 +18,8 @@ jobs: uses: phpbb-extensions/test-framework/.github/workflows/tests.yml@3.3.x with: EXTNAME: phpbbmodders/wiki + # EPV runs in the epv job below instead + EPV: 0 PHPBB_BRANCH: '3.3.x' RUN_FUNCTIONAL_TESTS: 0 PRIMARY_PHP_VERSION: '8.2' @@ -29,7 +31,17 @@ jobs: uses: phpbb-extensions/test-framework/.github/workflows/tests.yml@master with: EXTNAME: phpbbmodders/wiki + # EPV runs in the epv job below instead + EPV: 0 PHPBB_BRANCH: 'master' RUN_FUNCTIONAL_TESTS: 0 PRIMARY_PHP_VERSION: '8.2' PHP_VERSION_MATRIX: '["8.2", "8.3", "8.4", "8.5"]' + + # EPV (Extension Pre Validator), failing on any Fatal or Error finding not + # listed in .github/epv-allowed.txt + epv: + uses: phpbbmodders/.github/.github/workflows/epv.yml@v1 + with: + EXTNAME: phpbbmodders/wiki + PHPBB_BRANCHES: '["3.3.x", "master"]' diff --git a/controller/acp_templates.php b/controller/acp_templates.php index 9b3dc8b..a15cdc4 100644 --- a/controller/acp_templates.php +++ b/controller/acp_templates.php @@ -183,7 +183,7 @@ protected function do_check($u_action) trigger_error($this->language->lang('WIKI_STARTER_CHECK_FAILED') . adm_back_link($u_action), E_USER_WARNING); } $message = $changed - ? $this->language->lang('WIKI_STARTER_CHECK_CHANGES', count($changed), htmlspecialchars(implode(', ', $changed), ENT_COMPAT, 'UTF-8')) + ? $this->language->lang('WIKI_STARTER_CHECK_CHANGES', count($changed), utf8_htmlspecialchars(implode(', ', $changed))) : $this->language->lang('WIKI_STARTER_CHECK_NONE'); trigger_error($message . adm_back_link($u_action)); } @@ -205,7 +205,7 @@ protected function do_install($u_action) trigger_error($this->language->lang('WIKI_STARTER_NOTHING_CHOSEN') . adm_back_link($u_action), E_USER_WARNING); } $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_WIKI_STARTER_INSTALLED', false, array(implode(', ', $installed))); - trigger_error($this->language->lang('WIKI_STARTER_INSTALLED', count($installed), htmlspecialchars(implode(', ', $installed), ENT_COMPAT, 'UTF-8')) . adm_back_link($u_action)); + trigger_error($this->language->lang('WIKI_STARTER_INSTALLED', count($installed), utf8_htmlspecialchars(implode(', ', $installed))) . adm_back_link($u_action)); } /** diff --git a/controller/category.php b/controller/category.php index a8698fc..ef89f1f 100644 --- a/controller/category.php +++ b/controller/category.php @@ -238,7 +238,7 @@ public function index() 'COUNT' => $count, 'DESCRIPTION' => isset($descriptions[categories::normalise($name)]) ? $descriptions[categories::normalise($name)] : '', 'U_CATEGORY' => $u_category, - 'WRITE_IN' => $this->user->lang('WIKI_CATEGORY_WRITE_IN', htmlspecialchars('[[' . categories::PREFIX . $name . ']]', ENT_COMPAT, 'UTF-8')), + 'WRITE_IN' => $this->user->lang('WIKI_CATEGORY_WRITE_IN', utf8_htmlspecialchars('[[' . categories::PREFIX . $name . ']]')), 'MORE' => ($count > self::PREVIEW_MEMBERS) ? $this->user->lang('WIKI_CATEGORY_MORE', $count - self::PREVIEW_MEMBERS, $u_category) : '', )); diff --git a/controller/template_list.php b/controller/template_list.php index 471f628..d148a48 100644 --- a/controller/template_list.php +++ b/controller/template_list.php @@ -131,7 +131,7 @@ public function page() 'USED_ON' => count($this->links->what_links_here($row['article_url'])), 'EDIT_TIME' => $this->user->format_date($row['article_last_edit']), 'U_TEMPLATE' => $this->helper->route('phpbbmodders_wiki_article', array('article' => $row['article_url'])), - 'USAGE' => htmlspecialchars(\phpbbmodders\wiki\wiki\templates::usage($name, \phpbbmodders\wiki\wiki\templates::parameters($source)), ENT_COMPAT, 'UTF-8'), + 'USAGE' => utf8_htmlspecialchars(\phpbbmodders\wiki\wiki\templates::usage($name, \phpbbmodders\wiki\wiki\templates::parameters($source))), 'PREVIEW' => $preview, 'S_START' => strpos($name, \phpbbmodders\wiki\wiki\templates::START_PREFIX) === 0, )); diff --git a/event/wikilink_listener.php b/event/wikilink_listener.php index 99c0b3b..3b95fc2 100644 --- a/event/wikilink_listener.php +++ b/event/wikilink_listener.php @@ -407,7 +407,7 @@ public function link_wikilinks($event) } return ''; + . utf8_htmlspecialchars($this->language->lang('WIKI_LINK_NEW_ARTICLE')) . '">'; }, $html); } @@ -441,7 +441,7 @@ protected function embed_files($html) if (!isset($found[$name])) { - return '' . $alt . ''; + return '' . $alt . ''; } $url = $this->files->get_url($article, $found[$name]['file_id']); diff --git a/wiki/categories.php b/wiki/categories.php index e6e4223..01e4626 100644 --- a/wiki/categories.php +++ b/wiki/categories.php @@ -155,7 +155,7 @@ public static function names_in($stored) */ protected function members_where($category) { - $needle = 'templates->live_where('a', '') . ' AND a.article_text ' . $this->db->sql_like_expression($this->db->get_any_char() . $needle . $this->db->get_any_char()); diff --git a/wiki/edit.php b/wiki/edit.php index 9788e2a..ee27777 100644 --- a/wiki/edit.php +++ b/wiki/edit.php @@ -472,7 +472,7 @@ public function edit_article($article) $redirect_target = \phpbbmodders\wiki\wiki\markup::redirect_target(htmlspecialchars_decode($this->data['article_text'], ENT_COMPAT)); if ($redirect_target !== '' && $this->auth->acl_get('u_wiki_set_redirect') && empty($this->data['article_redirect'])) { - $this->data['article_redirect'] = htmlspecialchars($redirect_target, ENT_COMPAT, 'UTF-8'); + $this->data['article_redirect'] = utf8_htmlspecialchars($redirect_target); } \phpbbmodders\wiki\wiki\markup::set_active(true); @@ -718,7 +718,7 @@ protected function offer_start_templates($article) $text = $this->copy_text($copy_from); if ($text === null) { - $error = $this->user->lang('WIKI_START_COPY_NOT_FOUND', htmlspecialchars($copy_from, ENT_COMPAT, 'UTF-8')); + $error = $this->user->lang('WIKI_START_COPY_NOT_FOUND', utf8_htmlspecialchars($copy_from)); } } else if ($chosen !== '') @@ -757,7 +757,7 @@ protected function offer_start_templates($article) if ($text !== null) { // The text box takes text escaped as decode_message() leaves it - $this->message_parser->message = htmlspecialchars($text, ENT_COMPAT, 'UTF-8'); + $this->message_parser->message = utf8_htmlspecialchars($text); } foreach ($options as $option) @@ -771,7 +771,7 @@ protected function offer_start_templates($article) $this->template->assign_vars(array( 'S_START_PICKER' => true, - 'START_COPY_FROM' => htmlspecialchars($copy_from, ENT_COMPAT, 'UTF-8'), + 'START_COPY_FROM' => utf8_htmlspecialchars($copy_from), 'START_ERROR' => $error, 'U_START_ACTION' => $this->helper->route('phpbbmodders_wiki_article', array('article' => $article)), )); @@ -789,9 +789,9 @@ protected function ask_start_values(array $parameters) foreach ($parameters as $name => $default) { $this->template->assign_block_vars('start_values', array( - 'NAME' => htmlspecialchars((string) $name, ENT_COMPAT, 'UTF-8'), - 'LABEL' => ctype_digit((string) $name) ? $this->user->lang('WIKI_START_VALUE_NUMBERED', (int) $name) : htmlspecialchars((string) $name, ENT_COMPAT, 'UTF-8'), - 'DEFAULT' => htmlspecialchars($default, ENT_COMPAT, 'UTF-8'), + 'NAME' => utf8_htmlspecialchars((string) $name), + 'LABEL' => ctype_digit((string) $name) ? $this->user->lang('WIKI_START_VALUE_NUMBERED', (int) $name) : utf8_htmlspecialchars((string) $name), + 'DEFAULT' => utf8_htmlspecialchars($default), )); } $this->template->assign_var('S_START_VALUES', true); diff --git a/wiki/links.php b/wiki/links.php index 6e1dff9..d6336a7 100644 --- a/wiki/links.php +++ b/wiki/links.php @@ -73,13 +73,13 @@ public function what_links_here($name) // Links are stored in the parsed text as , and {{template}} calls // as (attributes in alphabetical order). $any = $this->db->get_any_char(); - $escaped = htmlspecialchars($name, ENT_COMPAT, 'UTF-8'); + $escaped = utf8_htmlspecialchars($name); $conditions = array( 'article_text ' . $this->db->sql_like_expression($any . 'db->sql_like_expression($any . '' . $any); diff --git a/wiki/move.php b/wiki/move.php index 83941b0..42ea01a 100644 --- a/wiki/move.php +++ b/wiki/move.php @@ -156,7 +156,7 @@ public function page($article) } else if ($this->taken($new_name, $article)) { - $errors[] = $this->user->lang('WIKI_MOVE_NAME_TAKEN', htmlspecialchars($new_name, ENT_COMPAT, 'UTF-8')); + $errors[] = $this->user->lang('WIKI_MOVE_NAME_TAKEN', utf8_htmlspecialchars($new_name)); } if ($new_title === '' || utf8_strlen(htmlspecialchars_decode($new_title, ENT_COMPAT)) > 255) { @@ -183,8 +183,8 @@ public function page($article) )); $this->template->assign_vars(array( 'ERROR' => implode('
', $errors), - 'ARTICLE_NAME' => htmlspecialchars($article, ENT_COMPAT, 'UTF-8'), - 'NEW_NAME' => htmlspecialchars($new_name, ENT_COMPAT, 'UTF-8'), + 'ARTICLE_NAME' => utf8_htmlspecialchars($article), + 'NEW_NAME' => utf8_htmlspecialchars($new_name), 'NEW_TITLE' => $new_title, 'S_LEAVE_REDIRECT' => $leave_redirect, 'TEMPLATE_USED_BY' => $used_by ? $this->user->lang('WIKI_MOVE_TEMPLATE_WARNING', $used_by) : '', @@ -208,7 +208,7 @@ protected function template_users($article) $sql = 'SELECT article_url FROM ' . $this->article_table . " WHERE article_approved = 1 - AND article_redirect = '" . $this->db->sql_escape(htmlspecialchars($article, ENT_COMPAT, 'UTF-8')) . "' + AND article_redirect = '" . $this->db->sql_escape(utf8_htmlspecialchars($article)) . "' AND article_url " . $this->db->sql_like_expression(templates::PREFIX . $this->db->get_any_char()); $result = $this->db->sql_query($sql); while ($row = $this->db->sql_fetchrow($result)) @@ -304,7 +304,7 @@ protected function move_article($old, $new, $title, $leave_redirect, $old_title) 'article_sources' => '', 'article_sticky' => 0, 'article_views' => 0, - 'article_redirect' => htmlspecialchars($new, ENT_COMPAT, 'UTF-8'), + 'article_redirect' => utf8_htmlspecialchars($new), 'article_description' => '', 'article_toc' => '', ))); diff --git a/wiki/protect.php b/wiki/protect.php index 3b650d3..c2cb66d 100644 --- a/wiki/protect.php +++ b/wiki/protect.php @@ -126,7 +126,7 @@ public function page($article) )); $this->template->assign_vars(array( 'ERROR' => $error, - 'ARTICLE_NAME' => htmlspecialchars($article, ENT_COMPAT, 'UTF-8'), + 'ARTICLE_NAME' => utf8_htmlspecialchars($article), 'U_ACTION' => $this->helper->route('phpbbmodders_wiki_article', array('article' => $article, 'action' => 'protect')), )); diff --git a/wiki/readable_diff.php b/wiki/readable_diff.php index d861bcf..166a936 100644 --- a/wiki/readable_diff.php +++ b/wiki/readable_diff.php @@ -288,7 +288,7 @@ protected static function word_diff($old, $new) $parts = array(); $escape = function ($words) { - return htmlspecialchars(implode(' ', $words), ENT_COMPAT, 'UTF-8'); + return utf8_htmlspecialchars(implode(' ', $words)); }; foreach ($edits as $edit) { @@ -352,7 +352,7 @@ protected function open_tag(\DOMElement $element) $tag = '<' . strtolower($element->nodeName); foreach ($element->attributes as $attribute) { - $tag .= ' ' . $attribute->nodeName . '="' . htmlspecialchars($attribute->nodeValue, ENT_COMPAT, 'UTF-8') . '"'; + $tag .= ' ' . $attribute->nodeName . '="' . utf8_htmlspecialchars($attribute->nodeValue) . '"'; } return $tag . '>'; diff --git a/wiki/search_results.php b/wiki/search_results.php index f9984bf..9eea3ed 100644 --- a/wiki/search_results.php +++ b/wiki/search_results.php @@ -167,9 +167,9 @@ protected function build_rows(array $ids, $keywords, $fields) } $row = $found[(int) $id]; - $title = htmlspecialchars($row['article_title'], ENT_COMPAT, 'UTF-8'); - $description = htmlspecialchars($row['article_description'], ENT_COMPAT, 'UTF-8'); - $snippet = ($fields === 'titleonly') ? '' : get_context(htmlspecialchars($row['article_body'], ENT_COMPAT, 'UTF-8'), $words ? array_map('stripslashes', $words) : array(), self::SNIPPET_LENGTH); + $title = utf8_htmlspecialchars($row['article_title']); + $description = utf8_htmlspecialchars($row['article_description']); + $snippet = ($fields === 'titleonly') ? '' : get_context(utf8_htmlspecialchars($row['article_body']), $words ? array_map('stripslashes', $words) : array(), self::SNIPPET_LENGTH); if ($pattern !== '') { $mark = '$1'; @@ -183,7 +183,7 @@ protected function build_rows(array $ids, $keywords, $fields) 'DESCRIPTION' => $description, 'SNIPPET' => $snippet, 'DATE' => $this->user->format_date($row['article_date']), - 'AUTHOR' => htmlspecialchars($row['author'], ENT_COMPAT, 'UTF-8'), + 'AUTHOR' => utf8_htmlspecialchars($row['author']), 'U_ARTICLE' => $this->helper->route('phpbbmodders_wiki_article', array('article' => $row['article_url'])), 'PROTECTED' => $this->protection->list_label($row['article_url']), ); diff --git a/wiki/view.php b/wiki/view.php index c7ac594..3dcd3ab 100644 --- a/wiki/view.php +++ b/wiki/view.php @@ -281,7 +281,7 @@ public function view_article($article, $id = 0) { $this->template->assign_vars(array( 'S_TEMPLATE_PAGE' => true, - 'TEMPLATE_SOURCE' => htmlspecialchars($template_source, ENT_COMPAT, 'UTF-8'), + 'TEMPLATE_SOURCE' => utf8_htmlspecialchars($template_source), 'TEMPLATE_USED_ON' => $this->user->lang('WIKI_TEMPLATE_USED_ON', count($linking_articles)), )); }