Address review findings on 1.7.0: denial headers, API status code, hook entry points - #66
Merged
Conversation
…ocs and tests Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com>
Copilot created this pull request from a session on behalf of
jeffw16
July 30, 2026 20:15
View session
jeffw16
marked this pull request as ready for review
July 30, 2026 20:16
Contributor
There was a problem hiding this comment.
Pull request overview
This PR is a follow-up to prior review feedback, tightening denial behavior across all entry points (index.php/API/REST) by ensuring crawler directives are consistently applied, Action API denials use an explicit 403, and the CrawlerProtectionShouldDeny hook is extended to cover API/REST with an entry-point identifier.
Changes:
- Add
ResponseFactory::markDenied()and use it from API/REST denial paths to sendX-Robots-Tag: noindex,nofollow, plus an explicit403for Action API denials. - Extend
CrawlerProtectionShouldDenywith an$entryPointargument (constants for index/api/rest) and run it for API and REST paths. - Expand unit/integration tests and update docs/tooling (including phpunit script scoping and the i18n qqq checker rewrite).
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| includes/ResponseFactory.php | Adds consistent robot directives for non-OutputPage denials via markDenied() and extends raw denial headers. |
| includes/CrawlerProtectionService.php | Runs the should-deny hook for API/REST entry points and marks API/REST denials with robots header (and 403 for API). |
| includes/HookRunner.php | Updates hook runner signature to pass the new $entryPoint argument. |
| includes/Hook/CrawlerProtectionShouldDenyHook.php | Extends the hook contract with entry-point constants and a new $entryPoint parameter. |
| tests/phpunit/unit/ResponseFactoryTest.php | Adds unit coverage for markDenied() and strengthens raw-denial fallback assertions. |
| tests/phpunit/unit/CrawlerProtectionServiceTest.php | Updates hook-handler signatures and adds unit tests for API/REST hook propagation and denial header marking. |
| tests/phpunit/namespaced-stubs.php | Extends the WebResponse stub with statusHeader() for tests. |
| tests/phpunit/integration/CrawlerProtectionIntegrationTest.php | Adds integration coverage for API/REST hook registration and basic end-to-end API/REST denial behavior. |
| README.md | Updates denial/header behavior documentation and clarifies reverse-proxy/XFF trust guidance; documents new hook signature. |
| TESTING.md | Documents separate unit vs integration test commands. |
| composer.json | Scopes composer phpunit to unit tests and adds phpunit:integration. |
| .github/scripts/check-i18n-qqq.sh | Rewrites the i18n qqq checker in PHP and adds orphaned-key detection. |
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
includes/Hook/CrawlerProtectionShouldDenyHook.php:67
- The hook docblock currently claims
$user/$requestare always the namespaced classes (\MediaWiki\User\User,\MediaWiki\Request\WebRequest). Across supported MediaWiki versions these can be the legacy global classes (\User,\WebRequest), so the doc can mislead hook implementers into adding incompatible type hints. Document both possible types to reflect actual runtime values.
* @param \MediaWiki\User\User $user The user making the request
* @param \MediaWiki\Request\WebRequest|null $request The current request,
* or null when the entry point cannot supply one
includes/CrawlerProtectionService.php:440
- When
fnmatch()is unavailable, this warning will be emitted on every call toisProtectedRestPath(). Under crawler load that can create very high log volume, even though the message is static. Consider emitting it only once (per PHP process) to avoid flooding logs while still surfacing the configuration problem.
if ( !function_exists( 'fnmatch' ) ) {
$this->logger->warning(
'CrawlerProtection: fnmatch() is unavailable, so ' .
'CrawlerProtectedRestPaths cannot be evaluated.'
);
jeffw16
approved these changes
Jul 30, 2026
Merged
jeffw16
added a commit
that referenced
this pull request
Jul 31, 2026
* i18n: add qqq.json, descriptionmsg, localised raw-denial responses, and CI qqq completeness check (#55) * Initial plan * Add i18n improvements: qqq.json, descriptionmsg, localised raw-denial, CI check * tests: add unit tests for i18n fallback in denyAccess and denyAccessWith418 * tests: refactor i18n fallback tests to AAA format with explicit assertions * fix: use ->plain() instead of ->text() to resolve SecurityCheck-XSS phan errors * fix: suppress SecurityCheck-XSS for trusted message bodies in raw denial --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> * Protect api.php and rest.php entry points from crawlers (#56) * Initial plan * feat: protect api.php (ApiCheckCanExecute) and rest.php (RestCheckCanExecute) entry points - Add CrawlerProtectedApiModules config (default []) for Action API module protection - Add CrawlerProtectedRestPaths config (default []) for REST API path protection via fnmatch glob patterns - Register ApiCheckCanExecute and RestCheckCanExecute hooks in extension.json - Restructure Hooks.php with namespace-block compat stubs so the class implements both new hook interfaces on all supported MW versions; REST protection is silently skipped on MW < 1.42 where RestCheckCanExecute does not fire - Add checkApiModule, isProtectedApiModule, checkRestPath, isProtectedRestPath to CrawlerProtectionService - Add stub interfaces for ApiCheckCanExecuteHook, RestCheckCanExecuteHook, HttpException in namespaced-stubs.php - Add unit tests for all new service methods and hook handlers - Update README with entry-point coverage table and new config documentation Closes #48 * Operationalize Copilot guidance with repo-specific rules, correct scope, and agent setup workflow (#61) * Initial plan * Overhaul Copilot instructions with repo-specific CI architecture and testing guidance * Clarify trimmed guidance and Copilot setup workflow intent * Restore comprehensive MediaWiki instruction guidance --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: use FNM_PATHNAME for REST path globs, fix docblock, bump version Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * fix: drop core hook interface stubs and use LocalizedHttpException for REST denial Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * ci: pin actions to SHAs, extend matrix to MW 1.44/1.45, add Dependabot, enforce minus-x (#59) * Initial plan * ci: hygiene improvements - pin actions, extend matrix, add dependabot * ci: skip minus-x on REL1_39 where MediaWiki does not ship it Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * Bump actions/checkout from 4.1.7 to 7.0.1 (#65) Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.7 to 7.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](actions/checkout@v4.1.7...v7.0.1) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Merge main; cover query sub-modules and correct REST hook MW version Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * test: extract REST handler stub helper with property docs (MW 1.45 phpcs) Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Jeffrey Wang <jeffw16@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Fix IP allowlist to use WebRequest::getIP(); add temp-account policy; normalize array configs (#57) * Initial plan * Fix IP allowlist to use WebRequest::getIP(), add TreatTempUsersAsAnon config, normalize array configs - CrawlerProtectionService: derive client IP from $request->getIP() (not $user->getName()) in both checkPerformAction() and checkSpecialPage() - Add $request parameter to checkSpecialPage(); update Hooks.php to pass $special->getContext()->getRequest() - Add CrawlerProtectionTreatTempUsersAsAnon config (default false) and isUserAllowed() helper; temp-account users (isRegistered+isTemp) are treated as anonymous when the flag is true - Pre-normalise all array-valued configs at construction time: coerce scalars to single-element arrays, drop non-string entries, log warnings (prevents fatal on misconfigured scalar values) - Validate CrawlerProtectionAllowedIPs entries at construction time and log a warning for invalid IP/range strings - Inject LoggerInterface via constructor and ServiceWiring.php - Add CrawlerProtectionTreatTempUsersAsAnon to extension.json - Update tests: fix IP tests to use request->getIP(), add $request param to checkSpecialPage() calls, add tests for temp accounts, IP-from- request, and scalar misconfiguration - Add getIP() to WebRequest stub for unit tests - Document new config options in README.md * Operationalize Copilot guidance with repo-specific rules, correct scope, and agent setup workflow (#61) * Initial plan * Overhaul Copilot instructions with repo-specific CI architecture and testing guidance * Clarify trimmed guidance and Copilot setup workflow intent * Restore comprehensive MediaWiki instruction guidance --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> * Validate IP allowlist entries with IPUtils only; make temp-user mocks version-safe Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * ci: pin actions to SHAs, extend matrix to MW 1.44/1.45, add Dependabot, enforce minus-x (#59) * Initial plan * ci: hygiene improvements - pin actions, extend matrix, add dependabot * ci: skip minus-x on REL1_39 where MediaWiki does not ship it Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * tests: give the Hooks mock context a getRequest() so special-page tests pass Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> Co-authored-by: Jeffrey Wang <jeffw16@users.noreply.github.com> * test: add integration tests and fix denyAccessPretty branch testability (#58) * Initial plan * test: add integration tests and fix denyAccessPretty testability - Replace version_compare(MW_VERSION,...) with method_exists() in ResponseFactory::denyAccessPretty() so both title-setter branches (setPageTitle for MW<1.41, setPageTitleMsg for MW>=1.41) are reachable in tests without redefining the MW_VERSION constant. - Add tests/phpunit/integration/CrawlerProtectionIntegrationTest.php (MediaWikiIntegrationTestCase) covering: * Service-container wiring: both services resolve from the real container * Hook registration: MediaWikiPerformAction and SpecialPageBeforeExecute are registered * Real OutputPage state: denyAccessPretty() sets HTTP 403 on a genuine OutputPage (CI matrix runs REL1_39 + REL1_43, covering both method_exists() branches) * End-to-end behaviour: anonymous users are blocked on protected actions and special pages; registered users are not - Add unit tests for both branches of the new method_exists() logic in ResponseFactoryTest: modern path (setPageTitleMsg called, setPageTitle never) and legacy path (anonymous stub without setPageTitleMsg triggers setPageTitle). - composer.json: restrict standalone `composer phpunit` to unit/ only so integration tests (which need a real MediaWiki install) are not run in isolation; register integration test namespace in autoload-dev. - ci.yml: convert phpunit matrix to explicit include entries and enable pcov coverage on the REL1_43/PHP 8.2 job, passing --coverage-text to phpunit when coverage != none. Closes #50 * Operationalize Copilot guidance with repo-specific rules, correct scope, and agent setup workflow (#61) * Initial plan * Overhaul Copilot instructions with repo-specific CI architecture and testing guidance * Clarify trimmed guidance and Copilot setup workflow intent * Restore comprehensive MediaWiki instruction guidance --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> * ci: pin actions to SHAs, extend matrix to MW 1.44/1.45, add Dependabot, enforce minus-x (#59) * Initial plan * ci: hygiene improvements - pin actions, extend matrix, add dependabot * ci: skip minus-x on REL1_39 where MediaWiki does not ship it Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * Bump actions/checkout from 4.1.7 to 7.0.1 (#65) Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.7 to 7.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](actions/checkout@v4.1.7...v7.0.1) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * test: make integration tests version-agnostic for Title/FauxRequest/User Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jeffrey Wang <jeffw16@users.noreply.github.com> * Add CrawlerProtectionShouldDeny hook for bespoke access policy (#62) * Initial plan * Add CrawlerProtectionShouldDeny hook * Merge candidate/1.7.0 and fix unit tests to not construct HookContainer Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * tests: assert the hook still runs for allowlisted IPs Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Jeffrey Wang <jeffw16@users.noreply.github.com> Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * Mark denial responses as noindex,nofollow (#63) * Initial plan * Send X-Robots-Tag on denials and robot policy on pretty denial page * Use WebResponse::header() for X-Robots-Tag on the pretty denial path Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * Scope X-Robots-Tag to the pretty denial path only Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * Resolve merge conflicts with candidate/1.7.0 and bump to 1.7.1 Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * Downgrade version from 1.7.1 to 1.7.0 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Jeffrey Wang <jeffw16@users.noreply.github.com> Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Use canonical request IP for API/REST allowlist; run denial tests under MediaWiki Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * Add opt-in X-Forwarded-For allowlist matching for wikis behind a reverse proxy Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * Add missing CrawlerProtectionTrustXForwardedFor key to scalar-config unit tests Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * Address cicalese review: denial headers, API 403, hook entry point, docs and tests (#66) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Address cicalese follow-up: signature test names, real ApiMain coverage, REST path docs Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * Make ApiMain integration assertions version-agnostic Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> * Soften ApiMain integration test status assertion comments Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to @cicalese's review of #64. The findings were: denial responses were only marked
noindexon the HTML path, Action API denials returned200 OK, andCrawlerProtectionShouldDenywas shipping as@stable to implementwith anindex.php-only signature.Denial responses
denyAccessRaw()now sendsX-Robots-Tag: noindex,nofollow, covering the raw-403 and 418 strategies — the ones operators actually enable under crawler load.ResponseFactory::markDenied( $response, ?int $statusCode )marks denials that aren't rendered throughOutputPage. Core answers anApiCheckCanExecuteveto withdieWithError()and no HTTP code, soApiMain::handleException()readsgetCode() === 0and emits200 OK;checkApiModules()now sets 403 explicitly. REST keeps its 403 fromLocalizedHttpExceptionand only gains the robot directive.CrawlerProtectionShouldDenyAdded a
string $entryPointargument (constantsENTRY_POINT_INDEX/ENTRY_POINT_API/ENTRY_POINT_REST) before the interface stabilizes, and wired the hook into the API and REST paths rather than deferring them.$specialPageNameis alwaysnullforapi/rest;$requestis documented nullable.Docs
WebRequest::getIP()behaves as it does everywhere in MediaWiki, including reporting the proxy address when the proxy is unregistered.$wgCrawlerProtectionTrustXForwardedForwarning now requires a proxy that unconditionally sets or appends the header, names HAProxy'soption forwardfor if-noneas the gap, and gives correct HAProxy/nginx directives.Tests
ApiCheckCanExecute/RestCheckCanExecuteregistration, a reflection check of handler signatures against core's hook interfaces (the class deliberately doesn't implement them, so PHP can't catch drift), and end-to-end API/REST service coverage. A realApiMain::execute()isn't viable — PHPUnit runs as CLI, where the container service is built withcliMode = true.markDenied()behavior, andassertNotSame( '', … )alongside the previously tautological i18n fallback assertions.Smaller items
fnmatch()guarded withfunction_exists(); absent, no REST path is treated as protected and a warning is logged.normalizeArrayConfig()no longer warns twice for a single non-string scalar.check-i18n-qqq.shrewritten in PHP (drops the Python dependency) and now also fails on qqq keys orphaned fromen.json.composer phpunitscoped totests/phpunit/unit/, matching the documented behavior; addedcomposer phpunit:integration. The CI job still runs the full directory.