From 4af2c10781d1a84a5827c63a83363e6fd8a365bc Mon Sep 17 00:00:00 2001 From: Andy Vandenberghe Date: Sun, 4 Oct 2026 22:13:59 +0200 Subject: [PATCH] Bundle the Patreon button instead of loading it from Patreon's CDN The ACP page loaded its "Become a patron" image from c5.patreon.com, so every view of the page made the admin's browser call a third party, and the button would break if Patreon moved the file. Ship the image with the extension and pass its URL to the template as U_PATREON_BUTTON. Closes #200 Co-Authored-By: Claude Opus 5.5 --- acp/recenttopics_module.php | 6 ++- adm/style/acp_recenttopics.html | 2 +- adm/style/images/become_a_patron_button.png | Bin 0 -> 3086 bytes tests/acp/acp_template_test.php | 47 ++++++++++++++++++++ tests/functional/recenttopics_test.php | 19 ++++++++ tests/tests.md | 20 ++++++++- 6 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 adm/style/images/become_a_patron_button.png create mode 100644 tests/acp/acp_template_test.php diff --git a/acp/recenttopics_module.php b/acp/recenttopics_module.php index 8b0efa0..11d9655 100644 --- a/acp/recenttopics_module.php +++ b/acp/recenttopics_module.php @@ -45,7 +45,7 @@ class recenttopics_module */ public function main($id, $mode) { - global $phpbb_container; + global $phpbb_container, $phpbb_root_path; $config = $phpbb_container->get('config'); $request = $phpbb_container->get('request'); @@ -298,7 +298,9 @@ public function main($id, $mode) 'RT_SHOW_LIKES' => (int) $config['rt_show_likes'], 'RT_SIDE_SHOW_DATE' => (int) $config['rt_side_show_date'], 'S_POSTLOVE' => $phpbb_container->has('avathar.postlove.topic_likes'), - 'S_RT_OK' => version_compare($ext_version, $latest_version, '=='), + // Bundled copy, so the ACP page makes no request to Patreon's CDN (issue #200) + 'U_PATREON_BUTTON' => $phpbb_root_path . 'ext/avathar/recenttopics/adm/style/images/become_a_patron_button.png', + 'S_RT_OK' => version_compare($ext_version, $latest_version, '=='), 'S_RT_OLD' => version_compare($ext_version, $latest_version, '<'), 'S_RT_DEV' => version_compare($ext_version, $latest_version, '>'), 'EXT_VERSION' => $ext_version, diff --git a/adm/style/acp_recenttopics.html b/adm/style/acp_recenttopics.html index ba24c1b..99d54d3 100644 --- a/adm/style/acp_recenttopics.html +++ b/adm/style/acp_recenttopics.html @@ -171,7 +171,7 @@

{{ lang('RECENT_TOPICS') }}

{{ lang('RT_DONATE') }}

{{ lang('RT_DONATE_EXPLAIN') }}
-
{{ lang('PATREON_ALT')|e('html_attr') }}
+
{{ lang('PATREON_ALT')|e('html_attr') }}
diff --git a/adm/style/images/become_a_patron_button.png b/adm/style/images/become_a_patron_button.png new file mode 100644 index 0000000000000000000000000000000000000000..291b7bcf265521a3d062024bc4f7df927ae60e8f GIT binary patch literal 3086 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 S8x99PkF}+}MV*;%;{O2Ee#b2U literal 0 HcmV?d00001 diff --git a/tests/acp/acp_template_test.php b/tests/acp/acp_template_test.php new file mode 100644 index 0000000..de04d01 --- /dev/null +++ b/tests/acp/acp_template_test.php @@ -0,0 +1,47 @@ +ext_root = dirname(__DIR__, 2); + } + + /** + * Opening the ACP page must not make the admin's browser call Patreon's CDN (#200). + */ + public function test_acp_template_loads_no_external_patreon_image() + { + $template = file_get_contents($this->ext_root . '/adm/style/acp_recenttopics.html'); + + $this->assertStringNotContainsString('patreon.com/external', $template); + $this->assertStringContainsString('{{ U_PATREON_BUTTON }}', $template); + } + + /** + * The bundled button the template points to ships with the extension. + */ + public function test_patreon_button_is_bundled() + { + $file = $this->ext_root . '/adm/style/images/become_a_patron_button.png'; + + $this->assertFileExists($file); + $this->assertSame('image/png', mime_content_type($file)); + } +} diff --git a/tests/functional/recenttopics_test.php b/tests/functional/recenttopics_test.php index 0e30330..701124c 100644 --- a/tests/functional/recenttopics_test.php +++ b/tests/functional/recenttopics_test.php @@ -192,6 +192,25 @@ public function test_acp_reset_works_with_valid_form_token() 'Submitting the genuine ACP form must still reset user preferences'); } + /** + * The ACP donate button uses the extension's bundled image, not Patreon's CDN, + * and that image is actually served (#200). + */ + public function test_acp_patreon_button_is_local() + { + $this->login(); + $this->admin_login(); + + $crawler = self::request('GET', $this->acp_module_url()); + $src = $crawler->filter('a[href*="patreon.com"] img')->attr('src'); + + $this->assertStringNotContainsString('patreon.com', $src); + $this->assertStringEndsWith('ext/avathar/recenttopics/adm/style/images/become_a_patron_button.png', $src); + + self::request('GET', 'ext/avathar/recenttopics/adm/style/images/become_a_patron_button.png', array(), false); + $this->assertSame(200, self::$client->getResponse()->getStatus()); + } + // ----------------------------------------------------------------------- // Helper // ----------------------------------------------------------------------- diff --git a/tests/tests.md b/tests/tests.md index f5b9346..3c71767 100644 --- a/tests/tests.md +++ b/tests/tests.md @@ -36,6 +36,8 @@ phpBB fires named events as it runs (see `contrib/events.md`). An event dispatch ``` tests/ +├── acp/ +│ └── acp_template_test.php ACP template assets (no external images) ├── event/ │ ├── listener_test.php Main event listener (board index, ACP, WOL, permissions) │ └── ucp_listener_test.php UCP preferences listener @@ -224,7 +226,22 @@ It loads `recenttopics.php` from every language pack and checks that `VIEWING_RE --- -## 9. Functional tests (`functional/recenttopics_test.php`) +## 9. ACP template assets (`acp/acp_template_test.php`) + +**What this code does:** +The ACP page shows a "Become a patron" button. The image ships with the extension (`adm/style/images/become_a_patron_button.png`), and the ACP module passes its URL to the template as `U_PATREON_BUTTON`. + +**What this test file does:** +It reads the template and the image straight from disk. + +| Test | Scenario | What it verifies | +|------|----------|-----------------| +| `test_acp_template_loads_no_external_patreon_image` | `adm/style/acp_recenttopics.html` | No `patreon.com/external` URL; the image uses `{{ U_PATREON_BUTTON }}`. Before the fix every ACP page view made the admin's browser call Patreon's CDN (#200) | +| `test_patreon_button_is_bundled` | `adm/style/images/become_a_patron_button.png` | The file exists and is a PNG | + +--- + +## 10. Functional tests (`functional/recenttopics_test.php`) **What this code does:** These tests start a real phpBB installation (using the test framework's built-in install), enable the extension, and make HTTP requests using a real browser-like crawler (Symfony DomCrawler). They check the actual rendered HTML for specific elements. @@ -240,6 +257,7 @@ Logs in as the admin account, optionally creates topics or changes config values | `test_rt_simple_page` | `rt_page_enable` = 1; GET `/app.php/rt/simple` | `` is present | | `test_rt_page_disabled` | `rt_page_enable` = 0; GET `/app.php/rt` | Page still loads (no 500 error); `` is present; `#recent-topics-box` must NOT appear | | `test_index_has_recent_topics` | `rt_index` = 1; create a topic; GET `index.php` | `` is present; `#recent-topics-box` contains the created topic title | +| `test_acp_patreon_button_is_local` | Logged in as admin; GET the Recent Topics ACP page | The Patreon button's `src` is the bundled image, not `patreon.com`, and that image URL returns 200 (#200) | | `test_ucp_preferences` | Logged in as admin; GET `/ucp.php?i=ucp_prefs&mode=view` | `input[name="rt_enable"]` and `input[name="rt_number"]` are present on the preferences page | ---