From e682c7207e982881744fb92d95ff29719930e543 Mon Sep 17 00:00:00 2001 From: Matt Friedman Date: Thu, 17 Sep 2026 17:28:47 -0700 Subject: [PATCH 1/5] Store ZIP bytes in response body; remove premature header/output sending --- controller/main.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/controller/main.php b/controller/main.php index 7ab7bae..dc83197 100644 --- a/controller/main.php +++ b/controller/main.php @@ -104,12 +104,10 @@ public function handle() $this->packager->create_extension($this->data); $filename = $this->packager->create_zip($this->data); - $response = new Response($filename); + $response = new Response(file_get_contents($filename)); $response->headers->set('Content-type', 'application/octet-stream'); $response->headers->set('Content-Disposition', 'attachment; filename="' . basename($filename) . '";'); $response->headers->set('Content-length', filesize($filename)); - $response->sendHeaders(); - $response->setContent(readfile($filename)); return $response; } From d6a3e0c00db1ee1a7a8903b3a2d1fd13d66182af Mon Sep 17 00:00:00 2001 From: Matt Friedman Date: Thu, 17 Sep 2026 17:29:19 -0700 Subject: [PATCH 2/5] Fix console command execute return value expectation --- skeleton/console/command/sample.php.twig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/skeleton/console/command/sample.php.twig b/skeleton/console/command/sample.php.twig index 20aea26..ee17340 100644 --- a/skeleton/console/command/sample.php.twig +++ b/skeleton/console/command/sample.php.twig @@ -50,9 +50,13 @@ class sample extends \phpbb\console\command\command * * @param InputInterface $input An InputInterface instance * @param OutputInterface $output An OutputInterface instance + * + * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln($this->user->lang('CLI_{{ EXTENSION.extension_name|upper }}_HELLO')); + + return 0; } } From 0efd27da6a9c24a8e99065394716df16c7b9a1e2 Mon Sep 17 00:00:00 2001 From: Matt Friedman Date: Thu, 17 Sep 2026 17:29:45 -0700 Subject: [PATCH 3/5] =?UTF-8?q?Fix=20generated=20notification=E2=80=99s=20?= =?UTF-8?q?get=5Furl=20usage=20of=20the=20route=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- skeleton/notification/type/sample.php.twig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skeleton/notification/type/sample.php.twig b/skeleton/notification/type/sample.php.twig index 9afebe4..666c30f 100644 --- a/skeleton/notification/type/sample.php.twig +++ b/skeleton/notification/type/sample.php.twig @@ -129,7 +129,9 @@ class sample extends \phpbb\notification\type\base */ public function get_url() { - return $this->helper->route('{{ EXTENSION.vendor_name }}_{{ EXTENSION.extension_name }}_controller', $this->get_data('{{ EXTENSION.extension_name|lower }}_sample_name')); + return $this->helper->route('{{ EXTENSION.vendor_name }}_{{ EXTENSION.extension_name }}_controller', [ + 'name' => $this->get_data('{{ EXTENSION.extension_name|lower }}_sample_name'), + ]); } /** From 0dd275824324f8e167d64043807564bc52b6baf0 Mon Sep 17 00:00:00 2001 From: Matt Friedman Date: Thu, 17 Sep 2026 17:32:00 -0700 Subject: [PATCH 4/5] Strengthen guards against user input characters that could break generated JSON/PHP files --- helper/validator.php | 69 ++++++++++++++++++++++++++++++++- language/en/common.php | 3 ++ tests/controller/main_test.php | 2 + tests/helper/validator_test.php | 17 ++++++++ 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/helper/validator.php b/helper/validator.php index 8153116..7c5b31d 100644 --- a/helper/validator.php +++ b/helper/validator.php @@ -75,14 +75,26 @@ public function validate_extension_name($value) */ public function validate_extension_display_name($value) { - if ((string) $value !== '' && strpos($value, '"') === false) + if ((string) $value !== '') { - return htmlspecialchars_decode($value, ENT_NOQUOTES); + return $this->validate_generated_string($value, 'SKELETON_INVALID_DISPLAY_NAME'); } throw new runtime_exception($this->language->lang('SKELETON_INVALID_DISPLAY_NAME')); } + /** + * Validate the extension description for insertion into composer.json + * + * @param string $value The value to validate + * @return string The valid value + * @throws runtime_exception + */ + public function validate_extension_description($value) + { + return $this->validate_generated_string($value, 'SKELETON_INVALID_EXTENSION_DESCRIPTION'); + } + /** * Validate and require the extension date/time * @@ -143,6 +155,8 @@ public function validate_vendor_name($value) */ public function validate_extension_homepage($value) { + $value = $this->validate_generated_string($value, 'SKELETON_INVALID_EXTENSION_URL'); + if ((string) $value !== '' && filter_var($value, FILTER_VALIDATE_URL) === false) { throw new runtime_exception($this->language->lang('SKELETON_INVALID_EXTENSION_URL')); @@ -151,6 +165,41 @@ public function validate_extension_homepage($value) return $value; } + /** + * Validate a string inserted without escaping into generated files. + * Values must be safe inside a JSON string and must not contain a PHP comment + * terminator. Request values are HTML escaped, so inspect their decoded form + * and return safe characters without HTML entities. + * + * @param string $value The value to validate + * @param string $error Language key used for invalid input + * @return string The valid value + * @throws runtime_exception + */ + protected function validate_generated_string($value, $error) + { + $decoded_value = htmlspecialchars_decode((string) $value, ENT_QUOTES); + + if (preg_match('/["\\\\\x00-\x1F]/', $decoded_value) || strpos($decoded_value, '*/') !== false) + { + throw new runtime_exception($this->language->lang($error)); + } + + return htmlspecialchars_decode((string) $value, ENT_NOQUOTES); + } + + /** + * Validate the author name + * + * @param string $value The value to validate + * @return string The valid value + * @throws runtime_exception + */ + public function validate_author_name($value) + { + return $this->validate_generated_string($value, 'SKELETON_INVALID_AUTHOR_NAME'); + } + /** * Validate the author homepage URL * @@ -160,6 +209,8 @@ public function validate_extension_homepage($value) */ public function validate_author_homepage($value) { + $value = $this->validate_generated_string($value, 'SKELETON_INVALID_AUTHOR_URL'); + if ((string) $value !== '' && filter_var($value, FILTER_VALIDATE_URL) === false) { throw new runtime_exception($this->language->lang('SKELETON_INVALID_AUTHOR_URL')); @@ -177,6 +228,8 @@ public function validate_author_homepage($value) */ public function validate_author_email($value) { + $value = $this->validate_generated_string($value, 'SKELETON_INVALID_AUTHOR_EMAIL'); + if ((string) $value !== '' && filter_var($value, FILTER_VALIDATE_EMAIL) === false) { throw new runtime_exception($this->language->lang('SKELETON_INVALID_AUTHOR_EMAIL')); @@ -185,6 +238,18 @@ public function validate_author_email($value) return $value; } + /** + * Validate the author role + * + * @param string $value The value to validate + * @return string The valid value + * @throws runtime_exception + */ + public function validate_author_role($value) + { + return $this->validate_generated_string($value, 'SKELETON_INVALID_AUTHOR_ROLE'); + } + /** * Validate and require the phpBB minimum version * diff --git a/language/en/common.php b/language/en/common.php index 372b5f4..d61740f 100644 --- a/language/en/common.php +++ b/language/en/common.php @@ -148,8 +148,11 @@ 'SKELETON_TITLE_COMPONENT_INFO' => 'Components', 'SKELETON_INVALID_AUTHOR_EMAIL' => 'An author email is invalid', + 'SKELETON_INVALID_AUTHOR_NAME' => 'An author name is invalid', + 'SKELETON_INVALID_AUTHOR_ROLE' => 'An author role is invalid', 'SKELETON_INVALID_AUTHOR_URL' => 'An author homepage URL is invalid', 'SKELETON_INVALID_DISPLAY_NAME' => 'The display name you provided is invalid', + 'SKELETON_INVALID_EXTENSION_DESCRIPTION'=> 'The extension description you provided is invalid', 'SKELETON_INVALID_EXTENSION_TIME' => 'The extension date you provided is invalid', 'SKELETON_INVALID_EXTENSION_URL' => 'The extension homepage URL is invalid', 'SKELETON_INVALID_EXTENSION_VERSION'=> 'The extension version you provided is invalid', diff --git a/tests/controller/main_test.php b/tests/controller/main_test.php index 3a5ab30..620e462 100644 --- a/tests/controller/main_test.php +++ b/tests/controller/main_test.php @@ -324,6 +324,8 @@ public function test_submit_success() $response = $this->get_controller($this->packager_mock)->handle(); $this->assertInstanceOf('\Symfony\Component\HttpFoundation\Response', $response); + $this->assertSame('', $response->getContent()); + $this->assertSame('0', $response->headers->get('Content-length')); } public function test_submit_exception() diff --git a/tests/helper/validator_test.php b/tests/helper/validator_test.php index b06fa1b..97caffb 100644 --- a/tests/helper/validator_test.php +++ b/tests/helper/validator_test.php @@ -42,6 +42,9 @@ public function valid_data(): array ['extension_display_name', 'Foo bar'], ['extension_display_name', 'Foo >bar\'s< & world'], + ['extension_description', 'Foo >bar\'s< & world'], + ['author_name', 'Foo Bar'], + ['author_role', 'Developer'], ['extension_time', '0000-00-00'], @@ -145,6 +148,13 @@ public function invalid_data(): array ['extension_display_name', '', 'SKELETON_INVALID_DISPLAY_NAME'], ['extension_display_name', null, 'SKELETON_INVALID_DISPLAY_NAME'], ['extension_display_name', 'Foo bar"s world', 'SKELETON_INVALID_DISPLAY_NAME'], + ['extension_display_name', 'Foo */ bar', 'SKELETON_INVALID_DISPLAY_NAME'], + ['extension_display_name', 'Foo \\ bar', 'SKELETON_INVALID_DISPLAY_NAME'], + + ['extension_description', 'Foo "bar"', 'SKELETON_INVALID_EXTENSION_DESCRIPTION'], + ['extension_description', 'Foo \\ bar', 'SKELETON_INVALID_EXTENSION_DESCRIPTION'], + ['extension_description', "Foo\nbar", 'SKELETON_INVALID_EXTENSION_DESCRIPTION'], + ['extension_description', 'Foo */ bar', 'SKELETON_INVALID_EXTENSION_DESCRIPTION'], ['extension_time', '00-00-0000', 'SKELETON_INVALID_EXTENSION_TIME'], ['extension_time', 'FOO', 'SKELETON_INVALID_EXTENSION_TIME'], @@ -175,13 +185,20 @@ public function invalid_data(): array ['extension_homepage', 'www.web.com', 'SKELETON_INVALID_EXTENSION_URL'], ['extension_homepage', 'foo', 'SKELETON_INVALID_EXTENSION_URL'], + ['extension_homepage', 'https://example.com/\\bad', 'SKELETON_INVALID_EXTENSION_URL'], + + ['author_name', 'Foo */ Bar', 'SKELETON_INVALID_AUTHOR_NAME'], + ['author_name', 'Foo "Bar"', 'SKELETON_INVALID_AUTHOR_NAME'], + ['author_role', 'Developer \\ Maintainer', 'SKELETON_INVALID_AUTHOR_ROLE'], ['author_homepage', 'www.web.com', 'SKELETON_INVALID_AUTHOR_URL'], ['author_homepage', 'foo', 'SKELETON_INVALID_AUTHOR_URL'], + ['author_homepage', 'https://example.com/*/', 'SKELETON_INVALID_AUTHOR_URL'], ['author_email', 'mail.com', 'SKELETON_INVALID_AUTHOR_EMAIL'], ['author_email', 'user@mail', 'SKELETON_INVALID_AUTHOR_EMAIL'], ['author_email', 'foo', 'SKELETON_INVALID_AUTHOR_EMAIL'], + ['author_email', 'foo"@example.com', 'SKELETON_INVALID_AUTHOR_EMAIL'], ['phpbb_version_min', '', 'SKELETON_INVALID_PHPBB_MIN_VERSION'], ['phpbb_version_min', null, 'SKELETON_INVALID_PHPBB_MIN_VERSION'], From 63cf1326ebb38de21aae5aadf6b317408c77cb42 Mon Sep 17 00:00:00 2001 From: Matt Friedman Date: Thu, 17 Sep 2026 17:32:09 -0700 Subject: [PATCH 5/5] Remove unused lang keys --- helper/validator.php | 46 +++++++++++++++++++++--------------------- language/en/common.php | 1 - 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/helper/validator.php b/helper/validator.php index 7c5b31d..dbec69d 100644 --- a/helper/validator.php +++ b/helper/validator.php @@ -165,29 +165,6 @@ public function validate_extension_homepage($value) return $value; } - /** - * Validate a string inserted without escaping into generated files. - * Values must be safe inside a JSON string and must not contain a PHP comment - * terminator. Request values are HTML escaped, so inspect their decoded form - * and return safe characters without HTML entities. - * - * @param string $value The value to validate - * @param string $error Language key used for invalid input - * @return string The valid value - * @throws runtime_exception - */ - protected function validate_generated_string($value, $error) - { - $decoded_value = htmlspecialchars_decode((string) $value, ENT_QUOTES); - - if (preg_match('/["\\\\\x00-\x1F]/', $decoded_value) || strpos($decoded_value, '*/') !== false) - { - throw new runtime_exception($this->language->lang($error)); - } - - return htmlspecialchars_decode((string) $value, ENT_NOQUOTES); - } - /** * Validate the author name * @@ -304,6 +281,29 @@ public function validate_php_version($value) } + /** + * Validate a string inserted without escaping into generated files. + * Values must be safe inside a JSON string and must not contain a PHP comment + * terminator. Request values are HTML escaped, so inspect their decoded form + * and return safe characters without HTML entities. + * + * @param string $value The value to validate + * @param string $error Language key used for invalid input + * @return string The valid value + * @throws runtime_exception + */ + protected function validate_generated_string($value, $error) + { + $decoded_value = htmlspecialchars_decode((string) $value, ENT_QUOTES); + + if (preg_match('/["\\\\\x00-\x1F]/', $decoded_value) || strpos($decoded_value, '*/') !== false) + { + throw new runtime_exception($this->language->lang($error)); + } + + return htmlspecialchars_decode((string) $value, ENT_NOQUOTES); + } + /** * Version value check. Checks for values like: * 1.0.0-RC1 diff --git a/language/en/common.php b/language/en/common.php index d61740f..1309766 100644 --- a/language/en/common.php +++ b/language/en/common.php @@ -58,7 +58,6 @@ 'SKELETON_QUESTION_EXTENSION_TIME_EXPLAIN' => 'YYYY-MM-DD, default: today', 'SKELETON_QUESTION_NUM_AUTHORS' => 'How many authors does the extension have', - 'SKELETON_QUESTION_NUM_AUTHORS_EXPLAIN' => 'default: 1', 'SKELETON_QUESTION_AUTHOR_NAME' => 'Please enter the author name', 'SKELETON_QUESTION_AUTHOR_NAME_UI' => 'Author name',