diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f71f1f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +* text=auto eol=lf +*.png binary +*.jpg binary +*.zip binary +/.github export-ignore +/.gitignore export-ignore +/.gitattributes export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore +/docs/images export-ignore diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..e8d5e9c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,29 @@ +name: Bug report +description: Report a reproducible problem with Admin Activation Details. +labels: ["bug"] +body: + - type: textarea + id: problem + attributes: + label: What happened? + description: Describe the result and what you expected. Remove real email addresses, IPs and activation keys. + validations: + required: true + - type: input + id: versions + attributes: + label: phpBB, PHP and extension versions + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + description: Include whether this was registration or an activation resend, and whether mail was queued. + validations: + required: true + - type: textarea + id: errors + attributes: + label: Error messages and relevant extensions + description: Remove credentials and private account details before posting. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8577a72 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Questions, feature requests, and general discussion + url: https://github.com/phpbbmodders/adminactivationdetails/discussions + about: For anything that is not a bug report, please use Discussions instead. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..7999ad0 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,42 @@ +name: Tests +on: + push: + pull_request: +permissions: + contents: read +jobs: + tests: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + phpbb: ['3.3.x', 'master'] + php: ['8.3', '8.4'] + name: phpBB ${{ matrix.phpbb }} / PHP ${{ matrix.php }} + steps: + - name: Checkout phpBB + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: phpbb/phpbb + ref: ${{ matrix.phpbb }} + - name: Checkout extension + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: phpBB/ext/phpbbmodders/adminactivationdetails + - name: Set up PHP + uses: shivammathur/setup-php@d52fc211a0436f8839a587b3b5b8efddab4744ea # v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, gd, intl, mbstring, mysqli, pdo_sqlite, sqlite3, zip + coverage: none + - name: Install phpBB test dependencies + run: .github/setup-phpbb.sh none '${{ matrix.php }}' 1 + - name: Run regression tests + run: phpBB/vendor/bin/phpunit --bootstrap tests/bootstrap.php --configuration phpBB/ext/phpbbmodders/adminactivationdetails/phpunit.xml.dist + - name: Check extension coding standards + run: phpBB/vendor/bin/phpcs -s --standard=build/code_sniffer/ruleset-php-extensions.xml --extensions=php --ignore='*/tests/*' phpBB/ext/phpbbmodders/adminactivationdetails + epv: + uses: phpbbmodders/.github/.github/workflows/epv.yml@v1 + with: + EXTNAME: phpbbmodders/adminactivationdetails + PHPBB_BRANCHES: '["3.3.x", "master"]' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..246c41d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor/ +/composer.lock +/.phpunit.result.cache +/tests/runtime/board-33.yml +/tests/runtime/board-40.yml +/tests/runtime/__pycache__/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..af65640 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# Admin Activation Details + +Adds the registering account's email address, stored registration IP and +registration date to phpBB's administrator activation emails. Valid IPv4 +and IPv6 addresses include WHOIS/RDAP and Stop Forum Spam lookup links. + +Requires phpBB 3.3.19 or phpBB 4.0 and PHP 8.3.36 or later. phpBB 4.0 is +still a development branch; see [testing](docs/testing.md) for tested revisions. + +## Installation + +Copy this extension into `ext/phpbbmodders/adminactivationdetails/`, then +open **ACP → Customise → Manage extensions** and enable **Admin Activation Details**. +Set **Account activation** to **By admin** in User registration settings. +Administrators must enable email for the existing **User requiring activation** +notification in their UCP notification settings. + +There are no additional settings or database migrations. Disable the extension +to restore phpBB's original activation notification and email template. + +## Email behavior + +- Covers administrator activation notifications from registration and resends. +- Uses the account's stored registration IP, not the current visitor's address. +- Keeps phpBB's existing recipients, notification preferences, profile link, + activation link and mail queue behavior. +- Uses phpBB's current email-rendering user's date format and timezone, shown + explicitly beside the date. A queued email keeps that rendered date. +- Missing or invalid IPs show **Not available** and omit lookup links. +- Sends no passwords and makes no requests to lookup services. Clicking a lookup + link submits the IP to that service. A registration IP may identify a proxy or VPN. + +The extension supplies a plain-text English email template; phpBB's normal +language fallback applies. Translations can provide +`language//email/admin_activate.txt` inside this extension. Existing +core-template customizations and HTML-email modifications do not automatically +carry over to the extension's template. + +## Screenshots + +Browser previews of captured test emails with synthetic details. Click an image +to see it full size; see the [gallery](docs/images/screenshots/README.md). + +[IPv4 registration email example](docs/images/screenshots/ipv4.png) + +[IPv6 registration email example](docs/images/screenshots/ipv6.png) + +[Missing or invalid IP email example](docs/images/screenshots/missing-ip.png) + +## Development + +See [implementation and testing](docs/testing.md). The extension listens to +`core.modify_notification_template` and changes only the `admin_activate` email. +Another extension replacing that template may conflict. +No phpBB core files need editing. If using phpBB's CLI to enable or disable +the extension, run `cache:purge` afterward to rebuild its cached services. + +The feature was requested in this [phpBB discussion](https://www.phpbb.com/community/viewtopic.php?t=2667817). + +Synthetic [IPv4](samples/admin-activation-ipv4.txt) and +[IPv6](samples/admin-activation-ipv6.txt) email examples are included. Their example activation keys do not work. + +## Contributing + +Contributions are welcome! + +- **Bug reports**: [Open an issue](https://github.com/phpbbmodders/adminactivationdetails/issues). +- **Everything else** (questions, feature requests, ideas, general discussion): [Use Discussions](https://github.com/phpbbmodders/adminactivationdetails/discussions). +- Pull requests are welcome for bug fixes or discussed features. + +## Acknowledgments + +- Implementation, testing and documentation assisted by [Codex](https://openai.com/codex/). + +## License + +This extension is licensed under the **GNU General Public License v2.0**. +See [license.txt](license.txt). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d66abfc --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security policy + +Report suspected vulnerabilities privately to security@williamjacoby.cc. +Include the organization name **phpbbmodders**, repository name +**adminactivationdetails**, affected versions, and reproduction steps. + +Do not post activation keys or account details in a public issue. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8af6c35 --- /dev/null +++ b/composer.json @@ -0,0 +1,26 @@ +{ + "name": "phpbbmodders/adminactivationdetails", + "type": "phpbb-extension", + "description": "Include registration IP, account details and IP lookup links in administrator activation emails.", + "homepage": "https://www.phpbbmodders.com/", + "version": "1.0.0-dev", + "license": "GPL-2.0-only", + "authors": [ + { + "name": "phpBB Modders", + "homepage": "https://www.phpbbmodders.com/", + "role": "Extension Developer" + } + ], + "require": { + "php": ">=8.3.36", + "phpbb/phpbb": ">=3.3.19,<4.1.0@dev", + "composer/installers": "^1.0 || ^2.0" + }, + "extra": { + "display-name": "Admin Activation Details", + "soft-require": { + "phpbb/phpbb": ">=3.3.19,<4.1.0@dev" + } + } +} diff --git a/config/services.yml b/config/services.yml new file mode 100644 index 0000000..fadbfc0 --- /dev/null +++ b/config/services.yml @@ -0,0 +1,8 @@ +services: + phpbbmodders.adminactivationdetails.listener: + class: phpbbmodders\adminactivationdetails\event\listener + arguments: + - '@user_loader' + - '@user' + tags: + - { name: event.listener } diff --git a/docs/images/screenshots/README.md b/docs/images/screenshots/README.md new file mode 100644 index 0000000..3982f55 --- /dev/null +++ b/docs/images/screenshots/README.md @@ -0,0 +1,17 @@ +# Email examples + +Browser previews of captured plain-text test emails. All account details are +synthetic, and the example activation keys do not work. These previews show the +email contents; the surrounding frame is not part of the sent email. + +### IPv4 registration + +[IPv4 registration email example](ipv4.png) + +### IPv6 registration + +[IPv6 registration email example](ipv6.png) + +### Missing or invalid IP + +[Missing or invalid IP email example](missing-ip.png) diff --git a/docs/images/screenshots/ipv4.png b/docs/images/screenshots/ipv4.png new file mode 100644 index 0000000..ff8c20c Binary files /dev/null and b/docs/images/screenshots/ipv4.png differ diff --git a/docs/images/screenshots/ipv6.png b/docs/images/screenshots/ipv6.png new file mode 100644 index 0000000..9655f50 Binary files /dev/null and b/docs/images/screenshots/ipv6.png differ diff --git a/docs/images/screenshots/missing-ip.png b/docs/images/screenshots/missing-ip.png new file mode 100644 index 0000000..2452aea Binary files /dev/null and b/docs/images/screenshots/missing-ip.png differ diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..8919fbd --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,83 @@ +# Implementation and testing + +## Implementation + +The extension listens to `core.modify_notification_template` on phpBB 3.3 and +4.0. Only emails using `admin_activate.txt` are affected. Core's activation URL +identifies the account; the extension loads its stored registration details. +Missing accounts cannot expose Anonymous account details. + +This event covers notification-manager mail from new registrations and direct +mail from activation resends. Core still controls recipients, preferences, +activation keys, and queuing. The extension does not replace a core service. + +phpBB's resend path sends directly to administrators. It does not use the +registration notification-manager subscription preferences; the extension +preserves that existing behavior rather than changing who receives mail. + +## Results — 10/26/2026 + +Both live boards used PHP 8.4.27 with SQLite and a localhost-only SMTP capture. + +| Board | Source revision | Unit tests | Browser and delivery checks | +| --- | --- | --- | --- | +| phpBB 3.3.19 (3.3.x source) | `1b1ffdf89c033f584a1daf0c6995c13e59dcb81b` | 8 tests, 42 assertions | Passed | +| phpBB 4.0.0-a3-dev | `2a2dad29a4d47d26d8f01f4bf4851c98f809c481` | 8 tests, 42 assertions | Passed | + +Source inspection also compared the current 3.3.x (`d8e558ca55`) and master +(`a4cea6406f`) revisions: the exercised UCP, notification and messenger paths +are unchanged from the runtime snapshots above. The phpBB 4.0 claim is for the +measured development snapshot, not a future final release. + +Verified on each board: + +- Real registration through Playwright, with separate applicant and admin mail. +- Correct account email, stored registration IP, registration date/timezone, + and existing profile/activation URLs; no password or unexpanded variables. +- IPv6 output and encoded lookup URLs; invalid addresses omit lookup links. +- Real activation resend from localhost retains the stored synthetic IPv6 IP. +- Mail remains queued until processing and retains its enriched contents. +- Registration notification email opt-out is respected. +- Disable plus cache purge restores the original email; re-enable plus cache + purge restores the enriched email. +- Administrator login and the email's activation key activate the correct account. + +The installable package passes EPV with **0 fatal, 0 error, 0 warning, 0 notice**. +The production PHP passes both branches' phpBB extension coding standards. +Composer metadata validates; its explicit extension version produces Composer's +usual Packagist advisory. GitHub CI is configured but has not been run remotely. + +## Repeat unit tests + +Use the committed `phpbb-release-tools/phpbb-test-board/run-unit-tests.sh` +runner against this checkout, once with `-b 3.3.x` and once with `-b master`. +It uses phpBB's own test bootstrap and supported PHPUnit dependencies. + +CI repeats the regression suite and code standards on both phpBB branches, +using PHP 8.3 and 8.4. The organization EPV workflow validates both branches. +The local runtime tests above used PHP 8.4; PHP 8.3 CI has not run yet. + +## Repeat browser/delivery tests + +Use `seed-forum` with a copy of the appropriate `tests/runtime/board-*.yml.example`. +Change the source and disposable board paths, choose free ports, and install +with `reset-board.sh --config YOUR_CONFIG --reset`. These fixtures are strictly +for disposable local boards. Mark the new board root by creating an empty +`.adminactivationdetails-test-board` file; the PHP fixture refuses unmarked boards. + +Start `python3 tests/runtime/smtp_capture.py /path/to/artifacts/mail` in a +separate terminal. It listens only on `127.0.0.1:18250` and never relays mail. +Configure the board with `sudo -u www-data php tests/runtime/fixture.php BOARD_ROOT setup`. +Keep test-board CLI cache operations under the web-server user so Twig cache +permissions remain consistent. Run `cache:purge` after CLI extension state changes. + +Run `tests/runtime/browser.js` through the Playwright skill runner, or with a +locally installed Playwright and Chromium, setting `TEST_URL`, `TEST_BOARD_ROOT` +and `TEST_ARTIFACT_DIR`. The script uses the local SMTP capture and invokes the +PHP fixture through noninteractive `sudo -u www-data`. + +The browser test creates only synthetic `@example.org` accounts. It tests +registration, resend, IPv6, invalid IPs, queueing and opt-out; results are written +as JSON to the artifact directory. The test restores the admin subscription. +After testing, use the fixture's `mail-off` operation and stop the capture server. +Restore or remove only the disposable boards and their dedicated nginx sites. diff --git a/event/listener.php b/event/listener.php new file mode 100644 index 0000000..a376e7f --- /dev/null +++ b/event/listener.php @@ -0,0 +1,83 @@ +user_loader = $user_loader; + $this->user = $user; + } + + /** + * @return array Events shared by both supported phpBB branches + */ + public static function getSubscribedEvents() + { + return array('core.modify_notification_template' => 'activation_email'); + } + + /** + * Enrich only admin_activate emails, including direct activation resends. + * Read the account ID from core's activation URL, never from request data. + * + * @param \phpbb\event\data $event Email template before rendering + * @return void + */ + public function activation_email($event) + { + $template = $event['template']; + if (basename($template->get_source_file_for_handle('body')) !== 'admin_activate.txt') + { + return; + } + + $query = parse_url(html_entity_decode($template->retrieve_var('U_ACTIVATE'), ENT_COMPAT, 'UTF-8'), PHP_URL_QUERY); + parse_str(is_string($query) ? $query : '', $params); + $user_id = filter_var($params['u'] ?? null, FILTER_VALIDATE_INT, array('options' => array('min_range' => 2))); + if ($user_id === false) + { + return; + } + + $row = $this->user_loader->get_user($user_id, true); + // user_loader falls back to Anonymous for a missing account. + if (!$row || (int) $row['user_id'] !== $user_id) + { + $row = array(); + } + + $ip = $row['user_ip'] ?? ''; + $ip = filter_var($ip, FILTER_VALIDATE_IP) !== false ? $ip : ''; + $encoded_ip = rawurlencode($ip); + $regdate = (int) ($row['user_regdate'] ?? 0); + + $template->assign_vars(array( + 'USER_IP' => $ip, + 'USER_EMAIL' => html_entity_decode($row['user_email'] ?? '', ENT_COMPAT, 'UTF-8'), + 'REG_DATE' => $regdate > 0 ? $this->user->format_date($regdate, '', true) : '', + 'REG_TIMEZONE' => $this->user->timezone ? $this->user->timezone->getName() : 'UTC', + 'U_WHOIS' => $ip !== '' ? 'https://search.arin.net/rdap/?query=' . $encoded_ip : '', + 'U_STOPFORUMSPAM' => $ip !== '' ? 'https://www.stopforumspam.com/search/?q=' . $encoded_ip : '', + )); + $template->set_filenames(array('body' => '@phpbbmodders_adminactivationdetails/admin_activate.txt')); + } +} diff --git a/ext.php b/ext.php new file mode 100644 index 0000000..a4e6c74 --- /dev/null +++ b/ext.php @@ -0,0 +1,30 @@ += 80336 && + phpbb_version_compare(PHPBB_VERSION, '3.3.19', '>=') && + phpbb_version_compare(PHPBB_VERSION, '4.1.0-dev', '<')) + { + return true; + } + + $language = $this->container->get('language'); + $language->add_lang('install', 'phpbbmodders/adminactivationdetails'); + + return $language->lang('ADMINACTIVATIONDETAILS_NOT_ENABLEABLE'); + } +} diff --git a/language/en/email/admin_activate.txt b/language/en/email/admin_activate.txt new file mode 100644 index 0000000..29cc5ee --- /dev/null +++ b/language/en/email/admin_activate.txt @@ -0,0 +1,39 @@ +Subject: Activate user account + +Hello, + +The account owned by "{USERNAME}" has been deactivated or newly created. Check the account details before deciding whether to activate it. + +Username: {USERNAME} + +Email address: {USER_EMAIL} + +Email address: Not available + + + +Registration IP: {USER_IP} + +Registration IP: Not available + + + +Registration date: {REG_DATE} ({REG_TIMEZONE}) + +Registration date: Not available + + + +WHOIS / RDAP lookup: +{U_WHOIS} + +Stop Forum Spam lookup: +{U_STOPFORUMSPAM} + +View the user's profile: +{U_USER_DETAILS} + +Activate the account: +{U_ACTIVATE} + +{EMAIL_SIG} diff --git a/language/en/install.php b/language/en/install.php new file mode 100644 index 0000000..53addaf --- /dev/null +++ b/language/en/install.php @@ -0,0 +1,20 @@ + 'Admin Activation Details requires phpBB 3.3.19 or phpBB 4.0, and PHP 8.3.36 or later.', +)); diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..d8cf7d4 --- /dev/null +++ b/license.txt @@ -0,0 +1,280 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d9a4d80 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,8 @@ + + + + + ./tests + + + diff --git a/samples/admin-activation-ipv4.txt b/samples/admin-activation-ipv4.txt new file mode 100644 index 0000000..7282556 --- /dev/null +++ b/samples/admin-activation-ipv4.txt @@ -0,0 +1,26 @@ +Subject: Activate user account + +Hello, + +The account owned by "ExampleApplicant" has been deactivated or newly created. Check the account details before deciding whether to activate it. + +Username: ExampleApplicant +Email address: ExampleApplicant@example.org + +Registration IP: 192.0.2.41 + +Registration date: Mon Oct 26, 2026 3:17 pm (UTC) + +WHOIS / RDAP lookup: +https://search.arin.net/rdap/?query=192.0.2.41 + +Stop Forum Spam lookup: +https://www.stopforumspam.com/search/?q=192.0.2.41 +View the user's profile: +https://forum.example.org/memberlist.php?mode=viewprofile&u=84 + +Activate the account: +https://forum.example.org/ucp.php?mode=activate&u=84&k=EXAMPLE_ACTIVATION_KEY + +-- +Thanks, The Management diff --git a/samples/admin-activation-ipv6.txt b/samples/admin-activation-ipv6.txt new file mode 100644 index 0000000..b2f1a88 --- /dev/null +++ b/samples/admin-activation-ipv6.txt @@ -0,0 +1,26 @@ +Subject: Activate user account + +Hello, + +The account owned by "ExampleApplicant" has been deactivated or newly created. Check the account details before deciding whether to activate it. + +Username: ExampleApplicant +Email address: ExampleApplicant@example.org + +Registration IP: 2001:db8::12 + +Registration date: Mon Oct 26, 2026 3:17 pm (UTC) + +WHOIS / RDAP lookup: +https://search.arin.net/rdap/?query=2001%3Adb8%3A%3A12 + +Stop Forum Spam lookup: +https://www.stopforumspam.com/search/?q=2001%3Adb8%3A%3A12 +View the user's profile: +https://forum.example.org/memberlist.php?mode=viewprofile&u=84 + +Activate the account: +https://forum.example.org/ucp.php?mode=activate&u=84&k=EXAMPLE_ACTIVATION_KEY + +-- +Thanks, The Management diff --git a/samples/admin-activation-missing-ip.txt b/samples/admin-activation-missing-ip.txt new file mode 100644 index 0000000..ca9a4b2 --- /dev/null +++ b/samples/admin-activation-missing-ip.txt @@ -0,0 +1,21 @@ +Subject: Activate user account + +Hello, + +The account owned by "ExampleApplicant" has been deactivated or newly created. Check the account details before deciding whether to activate it. + +Username: ExampleApplicant +Email address: ExampleApplicant@example.org + +Registration IP: Not available + +Registration date: Mon Oct 26, 2026 3:26 pm (UTC) + +View the user's profile: +https://forum.example.org/memberlist.php?mode=viewprofile&u=85 + +Activate the account: +https://forum.example.org/ucp.php?mode=activate&u=85&k=EXAMPLE_ACTIVATION_KEY + +-- +Thanks, The Management diff --git a/tests/functional/.gitkeep b/tests/functional/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/notification_test.php b/tests/notification_test.php new file mode 100644 index 0000000..bd7c52b --- /dev/null +++ b/tests/notification_test.php @@ -0,0 +1,95 @@ +getMockBuilder(\phpbb\user::class)->disableOriginalConstructor() + ->onlyMethods(array('format_date'))->getMock(); + $user->timezone = new \DateTimeZone('America/Chicago'); + $user->ip = '198.51.100.200'; + $user->method('format_date')->willReturn('10/26/2026 9:00 AM'); + $loader = $this->getMockBuilder(\phpbb\user_loader::class)->disableOriginalConstructor() + ->onlyMethods(array('get_user'))->getMock(); + $should_load = $filename === 'admin_activate.txt' && strpos($url, 'u=42') !== false; + $loader->expects($should_load ? $this->once() : $this->never())->method('get_user') + ->with(42, true)->willReturn($row); + $template = $this->createMock(\phpbb\template\template::class); + $template->method('get_source_file_for_handle')->with('body')->willReturn('/language/en/email/' . $filename); + $template->method('retrieve_var')->with('U_ACTIVATE')->willReturn($url); + $vars = array(); + $template->method('assign_vars')->willReturnCallback(function ($added) use (&$vars) { $vars = $added; }); + $template->expects($should_load ? $this->once() : $this->never())->method('set_filenames') + ->with(array('body' => '@phpbbmodders_adminactivationdetails/admin_activate.txt')); + $listener = new listener($loader, $user); + $listener->activation_email(new \phpbb\event\data(array('template' => $template))); + return $vars; + } + + public function test_uses_applicant_details_and_keeps_core_links_untouched() + { + $vars = $this->render(array('user_id' => 42, 'user_ip' => '192.0.2.12', + 'user_email' => 'applicant@example.org', 'user_regdate' => 1793023200)); + $this->assertSame('192.0.2.12', $vars['USER_IP']); + $this->assertSame('applicant@example.org', $vars['USER_EMAIL']); + $this->assertSame('10/26/2026 9:00 AM', $vars['REG_DATE']); + $this->assertSame('America/Chicago', $vars['REG_TIMEZONE']); + $this->assertStringEndsWith('q=192.0.2.12', $vars['U_STOPFORUMSPAM']); + foreach (array('U_ACTIVATE', 'U_USER_DETAILS', 'USERNAME', 'PASSWORD') as $core_var) + { + $this->assertArrayNotHasKey($core_var, $vars); + } + } + + public function test_ipv6_arguments_are_encoded() + { + $vars = $this->render(array('user_id' => 42, 'user_ip' => '2001:db8::12')); + $this->assertSame('2001:db8::12', $vars['USER_IP']); + $this->assertSame('https://search.arin.net/rdap/?query=2001%3Adb8%3A%3A12', $vars['U_WHOIS']); + $this->assertStringEndsWith('q=2001%3Adb8%3A%3A12', $vars['U_STOPFORUMSPAM']); + } + + public function test_invalid_ip_cannot_create_lookup_links() + { + $vars = $this->render(array('user_id' => 42, 'user_ip' => "192.0.2.1\r\nInjected: value")); + $this->assertSame('', $vars['USER_IP']); + $this->assertSame('', $vars['U_WHOIS']); + $this->assertSame('', $vars['U_STOPFORUMSPAM']); + } + + public function test_missing_fields_are_safe() + { + $vars = $this->render(array('user_id' => 42)); + $this->assertSame('', $vars['USER_IP']); + $this->assertSame('', $vars['USER_EMAIL']); + $this->assertSame('', $vars['REG_DATE']); + } + + public function test_missing_account_does_not_expose_anonymous_details() + { + $vars = $this->render(array('user_id' => ANONYMOUS, 'user_ip' => '203.0.113.8', + 'user_email' => 'anonymous@example.org')); + $this->assertSame('', $vars['USER_IP']); + $this->assertSame('', $vars['USER_EMAIL']); + } + + public function test_false_loader_result_is_safe() + { + $this->assertSame('', $this->render(false)['USER_IP']); + } + + public function test_other_emails_are_not_modified() + { + $this->assertSame(array(), $this->render(false, 'user_welcome.txt')); + } + + public function test_missing_or_invalid_account_id_does_not_query_users() + { + $this->assertSame(array(), $this->render(false, 'admin_activate.txt', 'https://board.example/ucp.php?u[]=42')); + $this->assertSame(array(), $this->render(false, 'admin_activate.txt', 'https://board.example/ucp.php')); + } +} diff --git a/tests/runtime/board-33.yml.example b/tests/runtime/board-33.yml.example new file mode 100644 index 0000000..79adacf --- /dev/null +++ b/tests/runtime/board-33.yml.example @@ -0,0 +1,12 @@ +# Copy to an ignored local YAML file and set the source/root paths. +board: + root: /tmp/adminactivationdetails-33 + phpbb_branch: '3.3' + server_name: localhost + server_port: '8120' + nginx_site: /etc/nginx/sites-available/phpbb-adminactivationdetails-33-test.conf + seeds: [] +styles: {} +extensions: + adminactivationdetails: + source: /path/to/adminactivationdetails diff --git a/tests/runtime/board-40.yml.example b/tests/runtime/board-40.yml.example new file mode 100644 index 0000000..9511420 --- /dev/null +++ b/tests/runtime/board-40.yml.example @@ -0,0 +1,12 @@ +# Copy to an ignored local YAML file and set the source/root paths. +board: + root: /tmp/adminactivationdetails-40 + phpbb_branch: 'master' + server_name: localhost + server_port: '8121' + nginx_site: /etc/nginx/sites-available/phpbb-adminactivationdetails-40-test.conf + seeds: [] +styles: {} +extensions: + adminactivationdetails: + source: /path/to/adminactivationdetails diff --git a/tests/runtime/browser.js b/tests/runtime/browser.js new file mode 100644 index 0000000..672030d --- /dev/null +++ b/tests/runtime/browser.js @@ -0,0 +1,91 @@ +// Exercise registration, activation resends and local SMTP delivery on a marked test board. +// Inputs: TEST_URL, TEST_BOARD_ROOT, TEST_ARTIFACT_DIR; outputs JSON test results. +const fs = require('node:fs'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); +const assert = require('node:assert/strict'); +const base = process.env.TEST_URL; +const root = process.env.TEST_BOARD_ROOT; +const work = process.env.TEST_ARTIFACT_DIR; +if (!base || !root || !work) { + console.log('Usage: TEST_URL=http://localhost:PORT TEST_BOARD_ROOT=/path/to/marked/board TEST_ARTIFACT_DIR=/path/to/artifacts node browser.js'); + process.exit(0); +} +const fixture = path.join(__dirname, 'fixture.php'); +assert.equal(new URL(base).hostname, 'localhost'); +const { chromium } = require('playwright'); +const username = 'iptest' + Date.now().toString().slice(-10); +function cli(...args) { return execFileSync('sudo', ['-n', '-u', 'www-data', 'php', fixture, root, ...args], {encoding:'utf8'}).trim(); } +function mails() { return JSON.parse(execFileSync('python3',[path.join(__dirname,'read_mail.py'),path.join(work,'mail')],{encoding:'utf8'})).filter(m=>m.body.includes(username)); } +function adminMails() { return mails().filter(m=>m.subject === 'Activate user account'); } +(async()=>{ + const browser = await chromium.launch({headless: !process.env.DISPLAY}); + const checks = []; + try { + const page = await browser.newPage(); + await page.goto(base+'/ucp.php?mode=register'); + await page.waitForTimeout(2200); + await page.getByRole('button',{name:'I agree to these terms',exact:true}).click(); + await page.locator('#username').fill(username); + await page.locator('#new_password').fill('SyntheticTest123!'); + await page.locator('#password_confirm').fill('SyntheticTest123!'); + await page.locator('#email').fill(username+'@example.org'); + await page.waitForTimeout(2200); + await page.locator('#submit').click(); + const body = await page.locator('body').innerText(); + if (!/account has been created|account has been registered/i.test(body)) throw new Error('Registration did not succeed: '+body.slice(-4000)); + const account = JSON.parse(cli('account',username)); + let mail = adminMails().at(-1); + assert.ok(mail, 'Administrator activation email delivered'); + assert.ok(mail.to.includes('admin@example.org')); + assert.ok(mail.body.includes('Registration IP: '+account.user_ip)); + assert.ok(mail.body.includes('Email address: '+username+'@example.org')); + assert.ok(mail.body.includes('Registration date:') && !mail.body.includes('Registration date: Not available')); + assert.ok(mail.body.includes('u='+account.user_id+'&k='+account.user_actkey)); + assert.ok(!mail.body.includes('SyntheticTest123!')); + assert.ok(!mail.body.includes('