From 02696024a7975a75c3ea00d64244e5f6d5729f41 Mon Sep 17 00:00:00 2001 From: Jordi Kroon Date: Mon, 6 Jul 2026 23:21:50 +0200 Subject: [PATCH 1/7] migrate doc redirects to redirects.json sidecar from doc-en --- error.php | 527 ++++++--------------------------------------- include/errors.inc | 419 ----------------------------------- 2 files changed, 71 insertions(+), 875 deletions(-) diff --git a/error.php b/error.php index 11a60745e5..9a50ec4e92 100644 --- a/error.php +++ b/error.php @@ -140,7 +140,6 @@ // ============================================================================ // Nice URLs for download files, so wget works completely well with download links if (preg_match("!^get/([^/]+)/from/([^/]+)(/mirror)?$!", $URI, $dlinfo)) { - $df = $dlinfo[1]; if (strpos($df, "7-LATEST") !== false) { include_once __DIR__ . "/include/version.inc"; @@ -198,155 +197,60 @@ // Work with lowercased URI from now $URI = strtolower($URI); -// Redirection hack, see error.inc for detailed description -// These expect -foo not _foo -$term = str_replace('_', '-', $URI); - -if ($path = is_known_ini($term)) { - status_header(301); - mirror_redirect("/manual/$LANG/$path"); - exit; -} -if ($path = is_known_variable($term)) { - status_header(301); - mirror_redirect("/manual/$LANG/$path"); - exit; -} -if ($path = is_known_term($term)) { - status_header(301); - mirror_redirect("/manual/$LANG/$path"); - exit; -} - // ============================================================================ -// Major manual page modifications (need to handle shortcuts and pages in all languages) -// Used permanent HTTP redirects, so search engines will be able to pick up the correct -// new URLs for these pages. -$manual_page_moves = [ - // entry point changed - 'installation' => 'install', - - // XML Id changed (see https://github.com/php/doc-en/commit/0d51ca45814bbc60d7a1e6cf6fc7213f0b49c8d5) - 'enum.random.intervalboundary' => 'enum.random-intervalboundary', - 'enum.random.intervalboundary.intro' => 'enum.random-intervalboundary.intro', - 'enum.random.intervalboundary.synopsis' => 'enum.random-intervalboundary.synopsis', - - // was split among platforms (don't know where to redirect) - 'install.apache' => 'install', - 'install.apache2' => 'install', - 'install.netscape-enterprise' => 'install', - 'install.otherhttpd' => 'install', - - // moved to platform sections - 'install.caudium' => 'install.unix.caudium', - 'install.commandline' => 'install.unix.commandline', - 'install.fhttpd' => 'install.unix.fhttpd', - 'install.hpux' => 'install.unix.hpux', - 'install.iis' => 'install.windows.iis', - 'install.linux' => 'install.unix', - 'install.omnihttpd' => 'install.windows.omnihttpd', - 'install.openbsd' => 'install.unix.openbsd', - 'install.sambar' => 'install.windows.sambar', - 'install.solaris' => 'install.unix.solaris', - 'install.xitami' => 'install.windows.xitami', - 'install.windows.installer.msi' => 'install.windows', - 'install.windows.installer' => 'install.windows', - 'install.windows.tools' => 'install.windows', - - // Internals docs where moved - 'zend' => 'internals2.ze1.zendapi', - 'zend-api' => 'internals2.ze1.zendapi', - 'internals.pdo' => 'internals2.pdo', - 'phpdevel' => 'internals2.ze1.php3devel', - 'tsrm' => 'internals2.ze1.tsrm', - - // Replaced extensions - 'aspell' => 'pspell', - - // Refactored - 'regexp.reference' => 'regexp.introduction', - "security" => "manual/security", - - // MongoDB converted from set to book - 'set.mongodb' => 'book.mongodb', - 'mongodb.installation.homebrew' => 'mongodb.installation#mongodb.installation.homebrew', - 'mongodb.installation.manual' => 'mongodb.installation#mongodb.installation.manual', - 'mongodb.installation.pecl' => 'mongodb.installation#mongodb.installation.pecl', - 'mongodb.installation.windows' => 'mongodb.installation#mongodb.installation.windows', - 'mongodb.persistence.deserialization' => 'mongodb.persistence#mongodb.persistence.deserialization', - 'mongodb.persistence.serialization' => 'mongodb.persistence#mongodb.persistence.serialization', -]; - -if (isset($manual_page_moves[$URI])) { - status_header(301); - mirror_redirect("/" . $manual_page_moves[$URI]); -} elseif (preg_match("!^manual/([^/]+)/([^/]+).php$!", $URI, $match) && - isset($manual_page_moves[$match[2]])) { - status_header(301); - - $parts = explode('#', $manual_page_moves[$match[2]], 2); - if (count($parts) === 1) { - mirror_redirect("/manual/{$match[1]}/{$parts[0]}.php"); - } else { - mirror_redirect("/manual/{$match[1]}/{$parts[0]}.php#{$parts[1]}"); +// Doc-page redirect map generated by phd from doc-en/redirects.xml, +// shipped as manual/en/redirects.json. +// 'shortcut' - bare URI (php.net/{from}) -> /manual/{LANG}/{to}.php +// 'in_manual' - manual URI (/manual/{lang}/{from}[.php]) -> /manual/{lang}/{to}.php +// 'both' - matches at either entry point + +$docRedirects = is_file(__DIR__ . '/manual/en/redirects.json') + ? json_decode(file_get_contents(__DIR__ . '/manual/en/redirects.json'), true) + : ['shortcut' => [], 'in_manual' => [], 'both' => []]; + +if (preg_match("!^manual/([^/]+)/([^/]+?)(?:\.php)?$!", $URI, $match)) { + $target = $docRedirects['both'][$match[2]] ?? $docRedirects['in_manual'][$match[2]] ?? null; + if ($target !== null) { + [$id, $anchor] = array_pad(explode('#', $target, 2), 2, ''); + status_header(301); + mirror_redirect("/manual/$match[1]/$id.php" . ($anchor !== '' ? "#$anchor" : '')); } } -$manual_redirections = [ - 'class.oci-lob' => 'class.ocilob', - 'oci-lob.append' => 'ocilob.append', - 'oci-lob.close' => 'ocilob.close', - 'oci-lob.eof' => 'ocilob.eof', - 'oci-lob.erase' => 'ocilob.erase', - 'oci-lob.export' => 'ocilob.export', - 'oci-lob.flush' => 'ocilob.flush', - 'oci-lob.free' => 'ocilob.free', - 'oci-lob.getbuffering' => 'ocilob.getbuffering', - 'oci-lob.import' => 'ocilob.import', - 'oci-lob.load' => 'ocilob.load', - 'oci-lob.read' => 'ocilob.read', - 'oci-lob.rewind' => 'ocilob.rewind', - 'oci-lob.save' => 'ocilob.save', - 'oci-lob.seek' => 'ocilob.seek', - 'oci-lob.setbuffering' => 'ocilob.setbuffering', - 'oci-lob.size' => 'ocilob.size', - 'oci-lob.tell' => 'ocilob.tell', - 'oci-lob.truncate' => 'ocilob.truncate', - 'oci-lob.write' => 'ocilob.write', - 'oci-lob.writetemporary' => 'ocilob.writetemporary', - 'class.oci-collection' => 'class.ocicollection', - 'oci-collection.append' => 'ocicollection.append', - 'oci-collection.assign' => 'ocicollection.assign', - 'oci-collection.assignelem' => 'ocicollection.assignelem', - 'oci-collection.free' => 'ocicollection.free', - 'oci-collection.getelem' => 'ocicollection.getelem', - 'oci-collection.max' => 'ocicollection.max', - 'oci-collection.size' => 'ocicollection.size', - 'oci-collection.trim' => 'ocicollection.trim', - 'splstack.setiteratormode' => 'spldoublylinkedlist.setiteratormode', - 'splqueue.setiteratormode' => 'spldoublylinkedlist.setiteratormode', - 'class.allow-dynamic-properties' => 'class.allowdynamicproperties', - 'class.return-type-will-change' => 'class.returntypewillchange', - 'class.sensitive-parameter' => 'class.sensitiveparameter', - 'function.curl-file-create' => 'curlfile.construct', - 'simplexmliterator.current' => 'simplexmlelement.current', - 'simplexmliterator.getchildren' => 'simplexmlelement.getchildren', - 'simplexmliterator.haschildren' => 'simplexmlelement.haschildren', - 'simplexmliterator.key' => 'simplexmlelement.key', - 'simplexmliterator.next' => 'simplexmlelement.next', - 'simplexmliterator.rewind' => 'simplexmlelement.rewind', - 'simplexmliterator.valid' => 'simplexmlelement.valid', -]; +// Bare-URI shortcut lookup. Try three forms in order +// 1. raw lowercased $URI ("if", "mysqli_get_client_version") +// 2. underscore -> hyphen $term ("short-open-tag" via /short_open_tag) +// 3. $-stripped $term ("-server" via /$_SERVER) +$target = null; +$term = str_replace('_', '-', $URI); +foreach ([$URI, $term, ltrim($term, '$')] as $key) { + if (isset($docRedirects['both'][$key])) { + $target = $docRedirects['both'][$key]; + break; + } + if (isset($docRedirects['shortcut'][$key])) { + $target = $docRedirects['shortcut'][$key]; + break; + } +} -if (preg_match("!^manual/([^/]+)/([^/]+?)(?:\.php)?$!", $URI, $match) && isset($manual_redirections[$match[2]])) { +if ($target !== null) { + [$id, $anchor] = array_pad(explode('#', $target, 2), 2, ''); + $url = "/manual/$LANG/$id.php"; + if ($queryString !== '') { + $url .= '?' . $queryString; + } + if ($anchor !== '') { + $url .= "#$anchor"; + } status_header(301); - mirror_redirect("/manual/$match[1]/" . $manual_redirections[$match[2]]); + mirror_redirect($url); } // ============================================================================ -// Define shortcuts for PHP files, manual pages and external redirects -$uri_aliases = [ - +// Site-level shortcuts to non-manual pages and external URL keys. +// (Doc-page shortcuts live in doc-en/redirects.xml.) +$site_aliases = [ # PHP page shortcuts "download" => "downloads", "getphp" => "downloads", @@ -361,224 +265,38 @@ "subscribe" => "mailing-lists", "logos" => "download-logos", - # manual shortcuts - "intro" => "introduction", - "whatis" => "introduction", - "whatisphp" => "introduction", - "what_is_php" => "introduction", - "composer" => "install.composer.intro", - - "windows" => "install.windows", - "win32" => "install.windows", - - "globals" => "language.variables.predefined", - "register_globals" => "security.globals", - "registerglobals" => "security.globals", - "manual/en/security.registerglobals.php" => "security.globals", // fix for 4.3.8 configure - "magic_quotes" => "security.magicquotes", - "magicquotes" => "security.magicquotes", - "gd" => "image", - "bcmath" => "bc", - 'streams' => 'book.stream', - "mongodb" => "book.mongodb", - "hrtime" => "function.hrtime", // Prefer function over PECL ext - - "callback" => "language.pseudo-types", - "number" => "language.pseudo-types", - "mixed" => "language.pseudo-types", - "bool" => "language.types.boolean", - "boolean" => "language.types.boolean", - "int" => "language.types.integer", - "integer" => "language.types.integer", - "float" => "language.types.float", - "string" => "language.types.string", - "heredoc" => "language.types.string", - "<<<" => "language.types.string", - "object" => "language.types.object", - "null" => "language.types.null", - 'callable' => 'language.types.callable', - - "htaccess" => "configuration.changes", - "php_value" => "configuration.changes", - - "operator.precedence" => "language.operators.precedence", - "operator-precedence" => "language.operators.precedence", - "precedence" => "language.operators.precedence", - "arithmetic.operators" => "language.operators.arithmetic", - "arithmetic-operators" => "language.operators.arithmetic", - "assignment.operators" => "language.operators.assignment", - "assignment-operators" => "language.operators.assignment", - "bitwise.operators" => "language.operators.bitwise", - "bitwise-operators" => "language.operators.bitwise", - "comparison.operators" => "language.operators.comparison", - "comparison-operators" => "language.operators.comparison", - "error.control" => "language.operators.errorcontrol", - "error-control" => "language.operators.errorcontrol", - "backtick" => "language.operators.execution", - "backtick.operator" => "language.operators.execution", - "backtick-operator" => "language.operators.execution", - "execution.operator" => "language.operators.execution", - "increment.operators" => "language.operators.increment", - "increment-operators" => "language.operators.increment", - "logical.operators" => "language.operators.logical", - "logical-operators" => "language.operators.logical", - "string.operators" => "language.operators.string", - "string-operators" => "language.operators.string", - "type.operators" => "language.operators.type", - "type-operators" => "language.operators.type", - "string.type" => "language.types.string", - "array.type" => "language.types.array", - "object.type" => "language.types.object", - "statements" => "control-structures.intro", - "control.structures" => "control-structures.intro", - "control-structures" => "control-structures.intro", - - "ternary" => "language.operators.comparison", - "instanceof" => "language.operators.type", - "if" => "language.control-structures", - "static" => "language.variables.scope", - "global" => "language.variables.scope", - "@" => "language.operators.errorcontrol", - "&" => "language.references", - "**" => "language.operators.arithmetic", - "..." => "functions.arguments", - "splat" => "functions.arguments", - "arrow" => "functions.arrow", - "fn" => "functions.arrow", - "pipe" => "operators.functional", - "|>" => "operators.functional", - // ?:, ??, ??= - // These shortcuts can not be captured here since they - // don't actually produce a 404 error. - // Instead, we have a small check in index.php directly. - - "dowhile" => "control-structures.do.while", - - "tut" => "tutorial", - "tut.php" => "tutorial", // BC - - "faq.php" => "faq", // BC - "bugs.php" => "bugs", // BC - "bugstats.php" => "bugstats", // BC - "docs-echm.php" => "download-docs", // BC - - "odbc" => "uodbc", // BC - - "links" => "support", // BC - "links.php" => "support", // BC - "oracle" => "oci8", - "cli" => "features.commandline", - - "oop" => "language.oop5", - "enum" => "language.enumerations", - "enums" => "language.enumerations", - - "const" => "language.constants.syntax", - "class" => "language.oop5.basic", - "new" => "language.oop5.basic", - "extends" => "language.oop5.basic", - "clone" => "language.oop5.cloning", - "construct" => "language.oop5.decon", - "destruct" => "language.oop5.decon", - "public" => "language.oop5.visibility", - "private" => "language.oop5.visibility", - "protected" => "language.oop5.visibility", - "var" => "language.oop5.visibility", - "abstract" => "language.oop5.abstract", - "interface" => "language.oop5.interfaces", - "interfaces" => "language.oop5.interfaces", - "autoload" => "language.oop5.autoload", - "__autoload" => "language.oop5.autoload", - "language.oop5.reflection" => "book.reflection", // BC - "::" => "language.oop5.paamayim-nekudotayim", - - "__construct" => "language.oop5.decon", - "__destruct" => "language.oop5.decon", - "__call" => "language.oop5.overloading", - "__callstatic" => "language.oop5.overloading", - "__get" => "language.oop5.overloading", - "__set" => "language.oop5.overloading", - "__isset" => "language.oop5.overloading", - "__unset" => "language.oop5.overloading", - "__sleep" => "language.oop5.magic", - "__wakeup" => "language.oop5.magic", - "__tostring" => "language.oop5.magic", - "__invoke" => "language.oop5.magic", - "__set_state" => "language.oop5.magic", - "__debuginfo" => "language.oop5.magic", - "__clone" => "language.oop5.cloning", - - "throw" => "language.exceptions", - "try" => "language.exceptions", - "catch" => "language.exceptions", - "lsb" => "language.oop5.late-static-bindings", - "namespace" => "language.namespaces", - "use" => "language.namespaces.using", - - "factory" => "language.oop5.patterns", - "singleton" => "language.oop5.patterns", - - "trait" => "language.oop5.traits", - "traits" => "language.oop5.traits", - - "news.php" => "archive/index", // BC - "readme.mirror" => "mirroring", // BC - - "php5" => "language.oop5", - "zend_changes.txt" => "language.oop5", // BC - "zend2_example.phps" => "language.oop5", // BC - "zend_changes_php_5_0_0b2.txt" => "language.oop5", // BC - "zend-engine-2" => "language.oop5", // BC - "zend-engine-2.php" => "language.oop5", // BC - - "news_php_5_0_0b2.txt" => "ChangeLog-5", // BC - "news_php_5_0_0b3.txt" => "ChangeLog-5", // BC - - "manual/about-notes.php" => "manual/add-note", // BC - "software/index.php" => "software", // BC - "releases.php" => "releases/index", // BC - - "migration7" => "migration70", // Consistent with migration5 - "update_5_2.txt" => "migration52", // BC - "readme_upgrade_51.php" => "migration51", // BC - "internals" => "internals2", // BC - "configuration.directives" => "ini.core", // BC - - # regexp. BC - "regexp.reference.backslash" => "regexp.reference.escape", - "regexp.reference.circudollar" => "regexp.reference.anchors", - "regexp.reference.squarebrackets" => "regexp.reference.character-classes", - "regexp.reference.verticalbar" => "regexp.reference.alternation", - - # external shortcut aliases ;) - "dochowto" => "phpdochowto", + # BC extensions + "bugs.php" => "bugs", // routed through $external_redirects + "bugstats.php" => "bugstats", // routed through $external_redirects + "docs-echm.php" => "download-docs", + "links" => "support", + "links.php" => "support", + + # News / releases / software + "news.php" => "archive/index", + "news_php_5_0_0b2.txt" => "ChangeLog-5", + "news_php_5_0_0b3.txt" => "ChangeLog-5", + "software/index.php" => "software", + "releases.php" => "releases/index", + "release-candidates.php" => "pre-release-builds", + "changelog-8.php" => "ChangeLog-8", - # CVS -> SVN + # Mirroring / VCS legacy + "readme.mirror" => "mirroring", + "cvsup" => "mirroring", "anoncvs.php" => "git", "cvs-php.php" => "git-php", - - # SVN -> Git "svn" => "git", "svn.php" => "git", "svn-php" => "git-php", "svn-php.php" => "git-php", - # CVSUp -> Nada - "cvsup" => "mirroring", - - # Other items - "security/crypt" => "security/crypt_blowfish", - - # Bugfixes - "array_sort" => "sort", // #64743 - "array-sort" => "sort", // #64743 + # External shortcut aliases (resolved via $external_redirects) + "dochowto" => "phpdochowto", # Removed pages - "tips.php" => "urlhowto", "tips" => "urlhowto", - "release-candidates.php" => "pre-release-builds", - - "changelog-8.php" => "ChangeLog-8", + "tips.php" => "urlhowto", ]; $external_redirects = [ @@ -607,116 +325,13 @@ "sunglasses" => "https://www.youtube.com/watch?v=dQw4w9WgXcQ", // Temporary easter egg for bug#66144 ]; -// Temporary hack to fix bug #49956 for mysqli -- Please don't hate me for this. Data taken from mysqli/summary.xml -$mysqli_redirects = [ - "mysqli_affected_rows" => "mysqli.affected-rows", - "mysqli_get_client_version" => "mysqli.client-version", - "mysqli_connect_errno" => "mysqli.connect-errno", - "mysqli_connect_error" => "mysqli.connect-error", - "mysqli_errno" => "mysqli.errno", - "mysqli_error" => "mysqli.error", - "mysqli_field_count" => "mysqli.field-count", - "mysqli_get_host_info" => "mysqli.host-info", - "mysqli_get_proto_info" => "mysqli.protocol-version", - "mysqli_get_server_version" => "mysqli.server-version", - "mysqli_info" => "mysqli.info", - "mysqli_insert_id" => "mysqli.insert-id", - "mysqli_sqlstate" => "mysqli.sqlstate", - "mysqli_warning_count" => "mysqli.warning-count", - "mysqli_autocommit" => "mysqli.autocommit", - "mysqli_change_user" => "mysqli.change-user", - "mysqli_character_set_name" => "mysqli.character-set-name", - "mysqli_close" => "mysqli.close", - "mysqli_commit" => "mysqli.commit", - "mysqli_connect" => "mysqli.construct", - "mysqli_debug" => "mysqli.debug", - "mysqli_dump_debug_info" => "mysqli.dump-debug-info", - "mysqli_get_charset" => "mysqli.get-charset", - "mysqli_get_connection_stats" => "mysqli.get-connection-stats", - "mysqli_get_client_info" => "mysqli.get-client-info", - "mysqli_get_client_stats" => "mysqli.get-client-stats", - "mysqli_get_cache_stats" => "mysqli.get-cache-stats", - "mysqli_get_server_info" => "mysqli.get-server-info", - "mysqli_get_warnings" => "mysqli.get-warnings", - "mysqli_init" => "mysqli.init", - "mysqli_kill" => "mysqli.kill", - "mysqli_more_results" => "mysqli.more-results", - "mysqli_multi_query" => "mysqli.multi-query", - "mysqli_next_result" => "mysqli.next-result", - "mysqli_options" => "mysqli.options", - "mysqli_ping" => "mysqli.ping", - "mysqli_prepare" => "mysqli.prepare", - "mysqli_query" => "mysqli.query", - "mysqli_real_connect" => "mysqli.real-connect", - "mysqli_real_escape_string" => "mysqli.real-escape-string", - "mysqli_real_query" => "mysqli.real-query", - "mysqli_refresh" => "mysqli.refresh", - "mysqli_rollback" => "mysqli.rollback", - "mysqli_select_db" => "mysqli.select-db", - "mysqli_set_charset" => "mysqli.set-charset", - "mysqli_set_local_infile_default" => "mysqli.set-local-infile-default", - "mysqli_set_local_infile_handler" => "mysqli.set-local-infile-handler", - "mysqli_ssl_set" => "mysqli.ssl-set", - "mysqli_stat" => "mysqli.stat", - "mysqli_stmt_init" => "mysqli.stmt-init", - "mysqli_store_result" => "mysqli.store-result", - "mysqli_thread_id" => "mysqli.thread-id", - "mysqli_thread_safe" => "mysqli.thread-safe", - "mysqli_use_result" => "mysqli.use-result", - "mysqli_stmt_affected_rows" => "mysqli-stmt.affected-rows", - "mysqli_stmt_errno" => "mysqli-stmt.errno", - "mysqli_stmt_error" => "mysqli-stmt.error", - "mysqli_stmt_field_count" => "mysqli-stmt.field-count", - "mysqli_stmt_insert_id" => "mysqli-stmt.insert-id", - "mysqli_stmt_param_count" => "mysqli-stmt.param-count", - "mysqli_stmt_sqlstate" => "mysqli-stmt.sqlstate", - "mysqli_stmt_attr_get" => "mysqli-stmt.attr-get", - "mysqli_stmt_attr_set" => "mysqli-stmt.attr-set", - "mysqli_stmt_bind_param" => "mysqli-stmt.bind-param", - "mysqli_stmt_bind_result" => "mysqli-stmt.bind-result", - "mysqli_stmt_close" => "mysqli-stmt.close", - "mysqli_stmt_data_seek" => "mysqli-stmt.data-seek", - "mysqli_stmt_execute" => "mysqli-stmt.execute", - "mysqli_stmt_fetch" => "mysqli-stmt.fetch", - "mysqli_stmt_free_result" => "mysqli-stmt.free-result", - "mysqli_stmt_get_result" => "mysqli-stmt.get-result", - "mysqli_stmt_get_warnings" => "mysqli-stmt.get-warnings", - "mysqli_stmt_more_results" => "mysqli-stmt.more-results", - "mysqli_stmt_next_result" => "mysqli-stmt.next-result", - "mysqli_stmt_num_rows" => "mysqli-stmt.num-rows", - "mysqli_stmt_prepare" => "mysqli-stmt.prepare", - "mysqli_stmt_reset" => "mysqli-stmt.reset", - "mysqli_stmt_result_metadata" => "mysqli-stmt.result-metadata", - "mysqli_stmt_send_long_data" => "mysqli-stmt.send-long-data", - "mysqli_stmt_store_result" => "mysqli-stmt.store-result", - "mysqli_field_tell" => "mysqli-result.current-field", - "mysqli_num_fields" => "mysqli-result.field-count", - "mysqli_fetch_lengths" => "mysqli-result.lengths", - "mysqli_num_rows" => "mysqli-result.num-rows", - "mysqli_data_seek" => "mysqli-result.data-seek", - "mysqli_fetch_all" => "mysqli-result.fetch-all", - "mysqli_fetch_array" => "mysqli-result.fetch-array", - "mysqli_fetch_assoc" => "mysqli-result.fetch-assoc", - "mysqli_fetch_field_direct" => "mysqli-result.fetch-field-direct", - "mysqli_fetch_field" => "mysqli-result.fetch-field", - "mysqli_fetch_fields" => "mysqli-result.fetch-fields", - "mysqli_fetch_object" => "mysqli-result.fetch-object", - "mysqli_fetch_row" => "mysqli-result.fetch-row", - "mysqli_field_seek" => "mysqli-result.field-seek", - "mysqli_free_result" => "mysqli-result.free", - "mysqli_embedded_server_end" => "mysqli-driver.embedded-server-end", - "mysqli_embedded_server_start" => "mysqli-driver.embedded-server-start", -]; - -// Merge this temporary hack with $uri_aliases so it'll be treated as such -$uri_aliases = array_merge($uri_aliases, $mysqli_redirects); - // ============================================================================ -// "Rewrite" the URL, if it was a shortcut +// "Rewrite" the URL, if it was a site-level shortcut. If the target +// resolves to a real .php in web-php, redirect immediately; otherwise +// mutate $URI and fall through so $external_redirects can catch it. -if (isset($uri_aliases[$URI])) { - $URI = $uri_aliases[$URI]; - /* If it was a page alias, redirect right away */ +if (isset($site_aliases[$URI])) { + $URI = $site_aliases[$URI]; if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/$URI.php")) { $target = "/$URI.php"; if ($queryString !== '') { diff --git a/include/errors.inc b/include/errors.inc index 6bc9cf6c74..5d60931b3c 100644 --- a/include/errors.inc +++ b/include/errors.inc @@ -112,425 +112,6 @@ function status_header(int $status): bool return true; } -/****************************************************************************** -In the future every mirror will have SQLite instead of hacks like this. Or, some -other means like a ginormous array! But the point is we'll end up storing -everything like functions, variables, constants, common search terms, etc. - -This exists today because as of PHP 5.3.0 there are over 200 links within the -php.ini files so these create nicer urls and allow language detection to work. - -The most commonly searched terms have also been added. - -TODO: Determine if we want to continue 301 -OR- make these official URLs. -******************************************************************************/ - -function is_known_ini(string $ini): ?string { - $inis = [ - 'engine' => 'apache.configuration.php#ini.engine', - 'short-open-tag' => 'ini.core.php#ini.short-open-tag', - 'asp-tags' => 'ini.core.php#ini.asp-tags', - 'precision' => 'ini.core.php#ini.precision', - 'y2k-compliance' => 'ini.core.php#ini.y2k-compliance', - 'output-buffering' => 'outcontrol.configuration.php#ini.output-buffering', - 'output-handler' => 'outcontrol.configuration.php#ini.output-handler', - 'zlib.output-compression' => 'zlib.configuration.php#ini.zlib.output-compression', - 'zlib.output-compression-level' => 'zlib.configuration.php#ini.zlib.output-compression-level', - 'zlib.output-handler' => 'zlib.configuration.php#ini.zlib.output-handler', - 'implicit-flush' => 'outcontrol.configuration.php#ini.implicit-flush', - 'allow-call-time-pass-reference' => 'ini.core.php#ini.allow-call-time-pass-reference', - 'open-basedir' => 'ini.core.php#ini.open-basedir', - 'disable-functions' => 'ini.core.php#ini.disable-functions', - 'disable-classes' => 'ini.core.php#ini.disable-classes', - 'zend.assertions' => 'ini.core.php#ini.zend.assertions', - 'syntax-highlighting' => 'misc.configuration.php#ini.syntax-highlighting', - 'ignore-user-abort' => 'misc.configuration.php#ini.ignore-user-abort', - 'realpath-cache-size' => 'ini.core.php#ini.realpath-cache-size', - 'realpath-cache-ttl' => 'ini.core.php#ini.realpath-cache-ttl', - 'expose-php' => 'ini.core.php#ini.expose-php', - 'max-execution-time' => 'info.configuration.php#ini.max-execution-time', - 'max-input-time' => 'info.configuration.php#ini.max-input-time', - 'max-input-vars' => 'info.configuration.php#ini.max-input-vars', - 'max-input-nesting-level' => 'info.configuration.php#ini.max-input-nesting-level', - 'memory-limit' => 'ini.core.php#ini.memory-limit', - 'error-reporting' => 'errorfunc.configuration.php#ini.error-reporting', - 'display-errors' => 'errorfunc.configuration.php#ini.display-errors', - 'display-startup-errors' => 'errorfunc.configuration.php#ini.display-startup-errors', - 'log-errors' => 'errorfunc.configuration.php#ini.log-errors', - 'log-errors-max-len' => 'errorfunc.configuration.php#ini.log-errors-max-len', - 'ignore-repeated-errors' => 'errorfunc.configuration.php#ini.ignore-repeated-errors', - 'ignore-repeated-source' => 'errorfunc.configuration.php#ini.ignore-repeated-source', - 'report-memleaks' => 'errorfunc.configuration.php#ini.report-memleaks', - 'track-errors' => 'errorfunc.configuration.php#ini.track-errors', - 'xmlrpc-errors' => 'errorfunc.configuration.php#ini.xmlrpc-errors', - 'html-errors' => 'errorfunc.configuration.php#ini.html-errors', - 'docref-root' => 'errorfunc.configuration.php#ini.docref-root', - 'docref-ext' => 'errorfunc.configuration.php#ini.docref-ext', - 'error-prepend-string' => 'errorfunc.configuration.php#ini.error-prepend-string', - 'error-append-string' => 'errorfunc.configuration.php#ini.error-append-string', - 'error-log' => 'errorfunc.configuration.php#ini.error-log', - 'syslog.facility' => 'errorfunc.configuration.php#ini.syslog.facility', - 'syslog.filter' => 'errorfunc.configuration.php#ini.syslog.filter', - 'syslog.ident' => 'errorfunc.configuration.php#ini.syslog.ident', - 'arg-separator.output' => 'ini.core.php#ini.arg-separator.output', - 'arg-separator.input' => 'ini.core.php#ini.arg-separator.input', - 'variables-order' => 'ini.core.php#ini.variables-order', - 'request-order' => 'ini.core.php#ini.request-order', - 'register-globals' => 'ini.core.php#ini.register-globals', - 'register-long-arrays' => 'ini.core.php#ini.register-long-arrays', - 'register-argc-argv' => 'ini.core.php#ini.register-argc-argv', - 'auto-globals-jit' => 'ini.core.php#ini.auto-globals-jit', - 'post-max-size' => 'ini.core.php#ini.post-max-size', - 'magic-quotes-gpc' => 'info.configuration.php#ini.magic-quotes-gpc', - 'magic-quotes-runtime' => 'info.configuration.php#ini.magic-quotes-runtime', - 'auto-prepend-file' => 'ini.core.php#ini.auto-prepend-file', - 'auto-append-file' => 'ini.core.php#ini.auto-append-file', - 'default-mimetype' => 'ini.core.php#ini.default-mimetype', - 'default-charset' => 'ini.core.php#ini.default-charset', - 'always-populate-raw-post-data' => 'ini.core.php#ini.always-populate-raw-post-data', - 'include-path' => 'ini.core.php#ini.include-path', - 'doc-root' => 'ini.core.php#ini.doc-root', - 'user-dir' => 'ini.core.php#ini.user-dir', - 'extension-dir' => 'ini.core.php#ini.extension-dir', - 'enable-dl' => 'info.configuration.php#ini.enable-dl', - 'cgi.force-redirect' => 'ini.core.php#ini.cgi.force-redirect', - 'cgi.redirect-status-env' => 'ini.core.php#ini.cgi.redirect-status-env', - 'cgi.fix-pathinfo' => 'ini.core.php#ini.cgi.fix-pathinfo', - 'fastcgi.impersonate' => 'ini.core.php#ini.fastcgi.impersonate', - 'cgi.rfc2616-headers' => 'ini.core.php#ini.cgi.rfc2616-headers', - 'file-uploads' => 'ini.core.php#ini.file-uploads', - 'upload-tmp-dir' => 'ini.core.php#ini.upload-tmp-dir', - 'upload-max-filesize' => 'ini.core.php#ini.upload-max-filesize', - 'allow-url-fopen' => 'filesystem.configuration.php#ini.allow-url-fopen', - 'allow-url-include' => 'filesystem.configuration.php#ini.allow-url-include', - 'from' => 'filesystem.configuration.php#ini.from', - 'user-agent' => 'filesystem.configuration.php#ini.user-agent', - 'default-socket-timeout' => 'filesystem.configuration.php#ini.default-socket-timeout', - 'auto-detect-line-endings' => 'filesystem.configuration.php#ini.auto-detect-line-endings', - 'date.timezone' => 'datetime.configuration.php#ini.date.timezone', - 'date.default-latitude' => 'datetime.configuration.php#ini.date.default-latitude', - 'date.default-longitude' => 'datetime.configuration.php#ini.date.default-longitude', - 'date.sunrise-zenith' => 'datetime.configuration.php#ini.date.sunrise-zenith', - 'date.sunset-zenith' => 'datetime.configuration.php#ini.date.sunset-zenith', - 'filter.default' => 'filter.configuration.php#ini.filter.default', - 'filter.default-flags' => 'filter.configuration.php#ini.filter.default-flags', - 'pcre.backtrack-limit' => 'pcre.configuration.php#ini.pcre.backtrack-limit', - 'pcre.recursion-limit' => 'pcre.configuration.php#ini.pcre.recursion-limit', - 'pdo-odbc.connection-pooling' => 'ref.pdo-odbc.php#ini.pdo-odbc.connection-pooling', - 'phar.readonly' => 'phar.configuration.php#ini.phar.readonly', - 'phar.require-hash' => 'phar.configuration.php#ini.phar.require-hash', - 'define-syslog-variables' => 'network.configuration.php#ini.define-syslog-variables', - 'smtp' => 'mail.configuration.php#ini.smtp', - 'smtp-port' => 'mail.configuration.php#ini.smtp-port', - 'sendmail-from' => 'mail.configuration.php#ini.sendmail-from', - 'sendmail-path' => 'mail.configuration.php#ini.sendmail-path', - 'sql.safe-mode' => 'ini.core.php#ini.sql.safe-mode', - 'odbc.default-db' => 'odbc.configuration.php#ini.uodbc.default-db', - 'odbc.default-user' => 'odbc.configuration.php#ini.uodbc.default-user', - 'odbc.default-pw' => 'odbc.configuration.php#ini.uodbc.default-pw', - 'odbc.allow-persistent' => 'odbc.configuration.php#ini.uodbc.allow-persistent', - 'odbc.check-persistent' => 'odbc.configuration.php#ini.uodbc.check-persistent', - 'odbc.max-persistent' => 'odbc.configuration.php#ini.uodbc.max-persistent', - 'odbc.max-links' => 'odbc.configuration.php#ini.uodbc.max-links', - 'odbc.defaultlrl' => 'odbc.configuration.php#ini.uodbc.defaultlrl', - 'odbc.defaultbinmode' => 'odbc.configuration.php#ini.uodbc.defaultbinmode', - 'mysql.allow-local-infile' => 'mysql.configuration.php#ini.mysql.allow-local-infile', - 'mysql.allow-persistent' => 'mysql.configuration.php#ini.mysql.allow-persistent', - 'mysql.max-persistent' => 'mysql.configuration.php#ini.mysql.max-persistent', - 'mysql.max-links' => 'mysql.configuration.php#ini.mysql.max-links', - 'mysql.default-port' => 'mysql.configuration.php#ini.mysql.default-port', - 'mysql.default-socket' => 'mysql.configuration.php#ini.mysql.default-socket', - 'mysql.default-host' => 'mysql.configuration.php#ini.mysql.default-host', - 'mysql.default-user' => 'mysql.configuration.php#ini.mysql.default-user', - 'mysql.default-password' => 'mysql.configuration.php#ini.mysql.default-password', - 'mysql.connect-timeout' => 'mysql.configuration.php#ini.mysql.connect-timeout', - 'mysql.trace-mode' => 'mysql.configuration.php#ini.mysql.trace-mode', - 'mysqli.allow-local-infile' => 'mysqli.configuration.php#ini.mysqli.allow-local-infile', - 'mysqli.max-links' => 'mysqli.configuration.php#ini.mysqli.max-links', - 'mysqli.allow-persistent' => 'mysqli.configuration.php#ini.mysqli.allow-persistent', - 'mysqli.default-port' => 'mysqli.configuration.php#ini.mysqli.default-port', - 'mysqli.default-socket' => 'mysqli.configuration.php#ini.mysqli.default-socket', - 'mysqli.default-host' => 'mysqli.configuration.php#ini.mysqli.default-host', - 'mysqli.default-user' => 'mysqli.configuration.php#ini.mysqli.default-user', - 'mysqli.default-pw' => 'mysqli.configuration.php#ini.mysqli.default-pw', - 'oci8.privileged-connect' => 'oci8.configuration.php#ini.oci8.privileged-connect', - 'oci8.max-persistent' => 'oci8.configuration.php#ini.oci8.max-persistent', - 'oci8.persistent-timeout' => 'oci8.configuration.php#ini.oci8.persistent-timeout', - 'oci8.ping-interval' => 'oci8.configuration.php#ini.oci8.ping-interval', - 'oci8.statement-cache-size' => 'oci8.configuration.php#ini.oci8.statement-cache-size', - 'oci8.default-prefetch' => 'oci8.configuration.php#ini.oci8.default-prefetch', - 'oci8.old-oci-close-semantics' => 'oci8.configuration.php#ini.oci8.old-oci-close-semantics', - 'opcache.preload' => 'opcache.configuration.php#ini.opcache.preload', - 'pgsql.allow-persistent' => 'pgsql.configuration.php#ini.pgsql.allow-persistent', - 'pgsql.auto-reset-persistent' => 'pgsql.configuration.php#ini.pgsql.auto-reset-persistent', - 'pgsql.max-persistent' => 'pgsql.configuration.php#ini.pgsql.max-persistent', - 'pgsql.max-links' => 'pgsql.configuration.php#ini.pgsql.max-links', - 'pgsql.ignore-notice' => 'pgsql.configuration.php#ini.pgsql.ignore-notice', - 'pgsql.log-notice' => 'pgsql.configuration.php#ini.pgsql.log-notice', - 'sqlite3.extension-dir' => 'sqlite3.configuration.php#ini.sqlite3.extension-dir', - 'bcmath.scale' => 'bc.configuration.php#ini.bcmath.scale', - 'browscap' => 'misc.configuration.php#ini.browscap', - 'session.save-handler' => 'session.configuration.php#ini.session.save-handler', - 'session.save-path' => 'session.configuration.php#ini.session.save-path', - 'session.use-cookies' => 'session.configuration.php#ini.session.use-cookies', - 'session.cookie-secure' => 'session.configuration.php#ini.session.cookie-secure', - 'session.use-only-cookies' => 'session.configuration.php#ini.session.use-only-cookies', - 'session.name' => 'session.configuration.php#ini.session.name', - 'session.auto-start' => 'session.configuration.php#ini.session.auto-start', - 'session.cookie-lifetime' => 'session.configuration.php#ini.session.cookie-lifetime', - 'session.cookie-path' => 'session.configuration.php#ini.session.cookie-path', - 'session.cookie-domain' => 'session.configuration.php#ini.session.cookie-domain', - 'session.cookie-httponly' => 'session.configuration.php#ini.session.cookie-httponly', - 'session.serialize-handler' => 'session.configuration.php#ini.session.serialize-handler', - 'session.gc-probability' => 'session.configuration.php#ini.session.gc-probability', - 'session.gc-divisor' => 'session.configuration.php#ini.session.gc-divisor', - 'session.gc-maxlifetime' => 'session.configuration.php#ini.session.gc-maxlifetime', - 'session.bug-compat-42' => 'session.configuration.php#ini.session.bug-compat-42', - 'session.bug-compat-warn' => 'session.configuration.php#ini.session.bug-compat-warn', - 'session.referer-check' => 'session.configuration.php#ini.session.referer-check', - 'session.entropy-length' => 'session.configuration.php#ini.session.entropy-length', - 'session.entropy-file' => 'session.configuration.php#ini.session.entropy-file', - 'session.cache-limiter' => 'session.configuration.php#ini.session.cache-limiter', - 'session.cache-expire' => 'session.configuration.php#ini.session.cache-expire', - 'session.sid-length' => 'session.configuration.php#ini.session.sid-length', - 'session.use-trans-sid' => 'session.configuration.php#ini.session.use-trans-sid', - 'session.hash-function' => 'session.configuration.php#ini.session.hash-function', - 'session.hash-bits-per-character' => 'session.configuration.php#ini.session.hash-bits-per-character', - 'session.upload-progress.enabled' => 'session.configuration.php#ini.session.upload-progress.enabled', - 'session.upload-progress.cleanup' => 'session.configuration.php#ini.session.upload-progress.cleanup', - 'session.upload-progress.prefix' => 'session.configuration.php#ini.session.upload-progress.prefix', - 'session.upload-progress.name' => 'session.configuration.php#ini.session.upload-progress.name', - 'session.upload-progress.freq' => 'session.configuration.php#ini.session.upload-progress.freq', - 'session.upload-progress.min-freq' => 'session.configuration.php#ini.session.upload-progress.min-freq', - 'url-rewriter.tags' => 'session.configuration.php#ini.url-rewriter.tags', - 'assert.active' => 'info.configuration.php#ini.assert.active', - 'assert.exception' => 'info.configuration.php#ini.assert.exception', - 'assert.warning' => 'info.configuration.php#ini.assert.warning', - 'assert.bail' => 'info.configuration.php#ini.assert.bail', - 'assert.callback' => 'info.configuration.php#ini.assert.callback', - 'assert.quiet-eval' => 'info.configuration.php#ini.assert.quiet-eval', - 'zend.enable-gc' => 'info.configuration.php#ini.zend.enable-gc', - 'com.typelib-file' => 'com.configuration.php#ini.com.typelib-file', - 'com.allow-dcom' => 'com.configuration.php#ini.com.allow-dcom', - 'com.autoregister-typelib' => 'com.configuration.php#ini.com.autoregister-typelib', - 'com.autoregister-casesensitive' => 'com.configuration.php#ini.com.autoregister-casesensitive', - 'com.autoregister-verbose' => 'com.configuration.php#ini.com.autoregister-verbose', - 'mbstring.language' => 'mbstring.configuration.php#ini.mbstring.language', - 'mbstring.internal-encoding' => 'mbstring.configuration.php#ini.mbstring.internal-encoding', - 'mbstring.http-input' => 'mbstring.configuration.php#ini.mbstring.http-input', - 'mbstring.http-output' => 'mbstring.configuration.php#ini.mbstring.http-output', - 'mbstring.encoding-translation' => 'mbstring.configuration.php#ini.mbstring.encoding-translation', - 'mbstring.detect-order' => 'mbstring.configuration.php#ini.mbstring.detect-order', - 'mbstring.substitute-character' => 'mbstring.configuration.php#ini.mbstring.substitute-character', - 'mbstring.func-overload' => 'mbstring.configuration.php#ini.mbstring.func-overload', - 'gd.jpeg-ignore-warning' => 'image.configuration.php#ini.image.jpeg-ignore-warning', - 'exif.encode-unicode' => 'exif.configuration.php#ini.exif.encode-unicode', - 'exif.decode-unicode-motorola' => 'exif.configuration.php#ini.exif.decode-unicode-motorola', - 'exif.decode-unicode-intel' => 'exif.configuration.php#ini.exif.decode-unicode-intel', - 'exif.encode-jis' => 'exif.configuration.php#ini.exif.encode-jis', - 'exif.decode-jis-motorola' => 'exif.configuration.php#ini.exif.decode-jis-motorola', - 'exif.decode-jis-intel' => 'exif.configuration.php#ini.exif.decode-jis-intel', - 'tidy.default-config' => 'tidy.configuration.php#ini.tidy.default-config', - 'tidy.clean-output' => 'tidy.configuration.php#ini.tidy.clean-output', - 'soap.wsdl-cache-enabled' => 'soap.configuration.php#ini.soap.wsdl-cache-enabled', - 'soap.wsdl-cache-dir' => 'soap.configuration.php#ini.soap.wsdl-cache-dir', - 'soap.wsdl-cache-ttl' => 'soap.configuration.php#ini.soap.wsdl-cache-ttl', - ]; - - return $inis[$ini] ?? null; -} - -function is_known_variable(string $variable): ?string { - $variables = [ - // Variables - 'globals' => 'reserved.variables.globals.php', - '-server' => 'reserved.variables.server.php', - '-get' => 'reserved.variables.get.php', - '-post' => 'reserved.variables.post.php', - '-files' => 'reserved.variables.files.php', - '-request' => 'reserved.variables.request.php', - '-session' => 'reserved.variables.session.php', - '-cookie' => 'reserved.variables.cookies.php', - '-env' => 'reserved.variables.environment.php', - 'this' => 'language.oop5.basic.php', - 'php-errormsg' => 'reserved.variables.phperrormsg.php', - 'argv' => 'reserved.variables.argv.php', - 'argc' => 'reserved.variables.argc.php', - 'http-response-header' => 'reserved.variables.httpresponseheader.php', - 'http-server-vars' => 'reserved.variables.server.php', - 'http-get-vars' => 'reserved.variables.get.php', - 'http-post-vars' => 'reserved.variables.post.php', - 'http-session-vars' => 'reserved.variables.session.php', - 'http-post-files' => 'reserved.variables.files.php', - 'http-cookie-vars' => 'reserved.variables.cookies.php', - ]; - - return $variables[ltrim($variable, '$')] ?? null; -} - -function is_known_term(string $term): ?string { - $terms = [ - '<>' => 'language.operators.comparison.php', - '<=>' => 'language.operators.comparison.php', - 'spaceship' => 'language.operators.comparison.php', - '==' => 'language.operators.comparison.php', - '===' => 'language.operators.comparison.php', - '@' => 'language.operators.errorcontrol.php', - '__halt_compiler' => 'function.halt-compiler.php', - '__PHP_Incomplete_Class' => 'function.unserialize.php', - 'and' => 'language.operators.logical.php', - 'apache' => 'install.php', - 'array' => 'language.types.array.php', - 'arrays' => 'language.types.array.php', - 'as' => 'control-structures.foreach.php', - 'case' => 'control-structures.switch.php', - 'catch' => 'language.exceptions.php', - 'checkbox' => 'faq.html.php', - 'class' => 'language.oop5.basic.php', - 'classes' => 'language.oop5.basic.php', - 'closures' => 'functions.anonymous.php', - 'cookie' => 'features.cookies.php', - 'date' => 'function.date.php', - 'default' => 'control-structures.switch.php', - 'do' => 'control-structures.do.while.php', - 'enddeclare' => 'control-structures.declare.php', - 'endfor' => 'control-structures.alternative-syntax.php', - 'endforeach' => 'control-structures.alternative-syntax.php', - 'endif' => 'control-structures.alternative-syntax.php', - 'endswitch' => 'control-structures.alternative-syntax.php', - 'endwhile' => 'control-structures.alternative-syntax.php', - 'exception' => 'language.exceptions.php', - 'extends' => 'language.oop5.basic.php#language.oop5.basic.extends', - 'false' => 'language.types.boolean.php', - 'file' => 'function.file.php', - 'final' => 'language.oop5.final.php', - 'finally' => 'language.exceptions.php', - 'fopen' => 'function.fopen.php', - 'for' => 'control-structures.for.php', - 'foreach' => 'control-structures.foreach.php', - 'form' => 'language.variables.external.php', - 'forms' => 'language.variables.external.php', - 'function' => 'language.functions.php', - 'gd' => 'book.image.php', - 'get' => 'reserved.variables.get.php', - 'global' => 'language.variables.scope.php', - 'globals' => 'language.variables.scope.php', - 'header' => 'function.header.php', - 'heredoc' => 'language.types.string.php#language.types.string.syntax.heredoc', - 'htaccess' => 'configuration.file.php', - 'if' => 'control-structures.if.php', - 'implements' => 'language.oop5.interfaces.php', - 'include' => 'function.include.php', - 'insteadof' => 'language.oop5.traits.php#language.oop5.traits.conflict', - 'int' => 'language.types.integer.php', - 'ip' => 'reserved.variables.server.php', - 'iterable' => 'language.types.iterable.php', - 'juggling' => 'language.types.type-juggling.php', - 'location' => 'function.header.php', - 'mail' => 'function.mail.php', - 'mixed' => 'language.types.mixed.php', - 'modulo' => 'language.operators.arithmetic.php', - 'mysql' => 'mysql.php', - 'never' => 'language.types.never.php', - 'new' => 'language.oop5.basic.php#language.oop5.basic.new', - 'nowdoc' => 'language.types.string.php#language.types.string.syntax.nowdoc', - 'null' => 'language.types.null.php', - 'numeric' => 'reserved.other-reserved-words.php', - 'object' => 'language.types.object.php', - 'operator' => 'language.operators.php', - 'operators' => 'language.operators.php', - 'or' => 'language.operators.logical.php', - 'parent' => 'reserved.classes.php#reserved.classes.special', - 'php.ini' => 'configuration.file.php', - 'php-mysql.dll' => 'book.mysql.php', - 'php-self' => 'reserved.variables.server.php', - 'query-string' => 'reserved.variables.server.php', - 'readonly' => 'language.oop5.properties.php#language.oop5.properties.readonly-properties', - 'redirect' => 'function.header.php', - 'reference' => 'index.php', - 'referer' => 'reserved.variables.server.php', - 'referrer' => 'reserved.variables.server.php', - 'remote-addr' => 'reserved.variables.server.php', - 'request' => 'reserved.variables.request.php', - 'self' => 'reserved.classes.php#reserved.classes.special', - 'session' => 'features.sessions.php', - 'smtp' => 'book.mail.php', - 'ssl' => 'book.openssl.php', - 'static' => 'language.oop5.static.php', - 'stdin' => 'wrappers.php.php', - 'string' => 'language.types.string.php', - 'superglobal' => 'language.variables.superglobals.php', - 'superglobals' => 'language.variables.superglobals.php', - 'switch' => 'control-structures.switch.php', - 'timestamp' => 'function.time.php', - 'true' => 'language.types.boolean.php', - 'try' => 'language.exceptions.php', - 'upload' => 'features.file-upload.php', - 'use' => 'language.namespaces.php', - 'void' => 'language.types.void.php', - 'xor' => 'language.operators.logical.php', - 'yield from' => 'language.generators.syntax.php#control-structures.yield.from', - 'yield' => 'language.generators.syntax.php#control-structures.yield', - - '__COMPILER_HALT_OFFSET__' => 'function.halt-compiler.php', - 'DEFAULT_INCLUDE_PATH' => 'reserved.constants.php', - 'E_ALL' => 'errorfunc.constants.php', - 'E_COMPILE_ERROR' => 'errorfunc.constants.php', - 'E_COMPILE_WARNING' => 'errorfunc.constants.php', - 'E_CORE_ERROR' => 'errorfunc.constants.php', - 'E_CORE_WARNING' => 'errorfunc.constants.php', - 'E_DEPRECATED' => 'errorfunc.constants.php', - 'E_ERROR' => 'errorfunc.constants.php', - 'E_NOTICE' => 'errorfunc.constants.php', - 'E_PARSE' => 'errorfunc.constants.php', - 'E_RECOVERABLE_ERROR' => 'errorfunc.constants.php', - 'E_STRICT' => 'errorfunc.constants.php', - 'E_USER_DEPRECATED' => 'errorfunc.constants.php', - 'E_USER_ERROR' => 'errorfunc.constants.php', - 'E_USER_NOTICE' => 'errorfunc.constants.php', - 'E_USER_WARNING' => 'errorfunc.constants.php', - 'E_WARNING' => 'errorfunc.constants.php', - 'PEAR_EXTENSION_DIR' => 'reserved.constants.php', - 'PEAR_INSTALL_DIR' => 'reserved.constants.php', - 'PHP_BINARY' => 'reserved.constants.php', - 'PHP_BINDIR' => 'reserved.constants.php', - 'PHP_CONFIG_FILE_PATH' => 'reserved.constants.php', - 'PHP_CONFIG_FILE_SCAN_DIR' => 'reserved.constants.php', - 'PHP_DATADIR' => 'reserved.constants.php', - 'PHP_DEBUG' => 'reserved.constants.php', - 'PHP_EOL' => 'reserved.constants.php', - 'PHP_EXTENSION_DIR' => 'reserved.constants.php', - 'PHP_EXTRA_VERSION' => 'reserved.constants.php', - 'PHP_FD_SETSIZE' => 'reserved.constants.php', - 'PHP_FLOAT_DIG' => 'reserved.constants.php', - 'PHP_FLOAT_EPSILON' => 'reserved.constants.php', - 'PHP_FLOAT_MAX' => 'reserved.constants.php', - 'PHP_FLOAT_MIN' => 'reserved.constants.php', - 'PHP_INT_MAX' => 'reserved.constants.php', - 'PHP_INT_MIN' => 'reserved.constants.php', - 'PHP_INT_SIZE' => 'reserved.constants.php', - 'PHP_LIBDIR' => 'reserved.constants.php', - 'PHP_LOCALSTATEDIR' => 'reserved.constants.php', - 'PHP_MAJOR_VERSION' => 'reserved.constants.php', - 'PHP_MANDIR' => 'reserved.constants.php', - 'PHP_MAXPATHLEN' => 'reserved.constants.php', - 'PHP_MINOR_VERSION' => 'reserved.constants.php', - 'PHP_OS_FAMILY' => 'reserved.constants.php', - 'PHP_OS' => 'reserved.constants.php', - 'PHP_PREFIX' => 'reserved.constants.php', - 'PHP_RELEASE_VERSION' => 'reserved.constants.php', - 'PHP_SAPI' => 'reserved.constants.php', - 'PHP_SHLIB_SUFFIX' => 'reserved.constants.php', - 'PHP_SYSCONFDIR' => 'reserved.constants.php', - 'PHP_VERSION_ID' => 'reserved.constants.php', - 'PHP_VERSION' => 'reserved.constants.php', - 'PHP_WINDOWS_EVENT_CTRL_BREAK' => 'reserved.constants.php', - 'PHP_WINDOWS_EVENT_CTRL_C' => 'reserved.constants.php', - 'PHP_ZTS' => 'reserved.constants.php', - ]; - - return $terms[$term] ?? null; -} /* Search snippet provider: A dirty proof-of-concept: From 13d7c8fc3b38b1a440ee9f5e5b9803c832c1e6cd Mon Sep 17 00:00:00 2001 From: lacatoire Date: Thu, 22 Oct 2026 15:44:47 +0200 Subject: [PATCH 2/7] Serve the pages of the moved extensions from /manual/extensions/ The third-party extensions manual is English only and lives next to the language manuals. Pages that moved there keep their names: - error.php loads manual/extensions/redirects.json, next to the doc redirects, and answers a 301 to /manual/extensions/ whatever the language of the old URL. A missing or corrupted redirects.json gives an empty map. - manual_setup() builds the base URL, canonical URL and last modification time from manual/extensions/, and shows neither a language chooser nor the hreflang alternates. - Shortcuts (php.net/apcu_add) fall back to the extensions manual when the language manual and the English one do not have the page. --- include/manual-lookup.inc | 7 +++++++ include/shared-manual.inc | 11 +++++++++-- public/error.php | 29 ++++++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/include/manual-lookup.inc b/include/manual-lookup.inc index 8d988041d1..23fbe0b9ce 100644 --- a/include/manual-lookup.inc +++ b/include/manual-lookup.inc @@ -89,6 +89,13 @@ function find_manual_page_slow($lang, $keyword) } } + // Fall back to the third-party extensions manual (English only) for the + // pages that moved to php/doc-extensions + foreach ($sections as $section) { + $found = tryprefix("extensions", $keyword, $section); + if ($found) { return $found; } + } + // BC: Few references pages where moved to book. if (strpos($keyword, "ref.") === 0) { $kw = substr_replace($keyword, "book.", 0, 4); diff --git a/include/shared-manual.inc b/include/shared-manual.inc index 4d37f98125..26e6bdfe56 100644 --- a/include/shared-manual.inc +++ b/include/shared-manual.inc @@ -282,7 +282,10 @@ function manual_setup($setup): void { } $PGI = $setup; // Set base href for this manual page - $base = 'manual/' . (new Languages())->convert($setup['head'][1]) . "/"; + // The third-party extensions manual (php/doc-extensions) is English only + // and served from manual/extensions/, which is not a language. + $isExtensionsManual = $setup['head'][1] === 'extensions'; + $base = 'manual/' . ($isExtensionsManual ? 'extensions' : (new Languages())->convert($setup['head'][1])) . "/"; $_SERVER['BASE_PAGE'] = $base . $setup['this'][0]; $_SERVER['BASE_HREF'] = $MYSITE . $_SERVER['BASE_PAGE']; @@ -314,7 +317,7 @@ function manual_setup($setup): void { $config = [ "current" => "docs", "breadcrumbs" => $breadcrumbs, - "languages" => array_keys(Languages::ACTIVE_ONLINE_LANGUAGES), + "languages" => $isExtensionsManual ? [] : array_keys(Languages::ACTIVE_ONLINE_LANGUAGES), "meta-navigation" => [ "contents" => $base . $setup["home"][0], "index" => $base . $setup["up"][0], @@ -331,6 +334,10 @@ function manual_setup($setup): void { ]; site_header($setup["this"][1] . " - Manual", $config); + if ($isExtensionsManual) { + return; + } + $languageChooser = manual_language_chooser($config['lang'], $config['thispage']); echo << /manual/{lang}/{to}.php // 'both' - matches at either entry point -$docRedirects = is_file(__DIR__ . '/manual/en/redirects.json') - ? json_decode(file_get_contents(__DIR__ . '/manual/en/redirects.json'), true) - : ['shortcut' => [], 'in_manual' => [], 'both' => []]; +// A missing, unreadable or corrupted file gives an empty map. +$loadRedirects = static function (string $file): array { + $empty = ['shortcut' => [], 'in_manual' => [], 'both' => []]; + if (!is_file($file)) { + return $empty; + } + + $maps = json_decode((string) file_get_contents($file), true); + + return is_array($maps) ? $maps + $empty : $empty; +}; + +$docRedirects = $loadRedirects(__DIR__ . '/manual/en/redirects.json'); +// Pages of the extensions moved to the third-party extensions manual +// (php/doc-extensions), same format. That manual is English only. +$extRedirects = $loadRedirects(__DIR__ . '/manual/extensions/redirects.json'); if (preg_match("!^manual/([^/]+)/([^/]+?)(?:\.php)?$!", $URI, $match)) { $target = $docRedirects['both'][$match[2]] ?? $docRedirects['in_manual'][$match[2]] ?? null; @@ -218,6 +231,16 @@ } } +// Whatever the language of the requested page, it moves to the English +// extensions manual. Requests already in that manual are not redirected. +if (preg_match("#^manual/(?!extensions/)[^/]+/([^/]+?)(?:\.php)?$#", $URI, $match)) { + $target = $extRedirects['in_manual'][$match[1]] ?? null; + if ($target !== null) { + status_header(301); + mirror_redirect("/manual/extensions/$target.php"); + } +} + // Bare-URI shortcut lookup. Try three forms in order // 1. raw lowercased $URI ("if", "mysqli_get_client_version") // 2. underscore -> hyphen $term ("short-open-tag" via /short_open_tag) From acad8de9754f032d403b846310d12eec5a990166 Mon Sep 17 00:00:00 2001 From: lacatoire Date: Thu, 22 Oct 2026 16:10:23 +0200 Subject: [PATCH 3/7] Update the PHPStan baseline for the doc redirects in error.php error.php reads $LANG fewer times now that the doc redirect tables left it. --- phpstan-baseline.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1be2fec354..01aacf834b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1713,7 +1713,7 @@ parameters: - message: '#^Variable \$LANG might not be defined\.$#' identifier: variable.undefined - count: 8 + count: 6 path: public/error.php - From 99383a265c2c8aa537a6a3b44f7d5943b9ab8c2e Mon Sep 17 00:00:00 2001 From: lacatoire Date: Thu, 22 Oct 2026 16:13:16 +0200 Subject: [PATCH 4/7] Add an end-to-end test of the move of the extensions to the extensions manual The script builds the extensions manual and a language manual, serves them with the built-in web server and checks the redirects, the links in both directions and the shortcuts. Any maintainer can run it with the six repositories side by side. --- tests/extensions-migration.sh | 244 ++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100755 tests/extensions-migration.sh diff --git a/tests/extensions-migration.sh b/tests/extensions-migration.sh new file mode 100755 index 0000000000..91d92b43cf --- /dev/null +++ b/tests/extensions-migration.sh @@ -0,0 +1,244 @@ +#!/bin/sh +# +# End-to-end test of the move of the third-party extensions to the extensions +# manual (php/doc-extensions). +# +# It builds the extensions manual and a language manual, serves both with the +# built-in web server and checks the way a visitor gets from one to the other: +# +# - the old URL of a moved page answers a 301 to /manual/extensions/.php +# - the pages of the language manual link to the extensions manual, and back +# - the shortcuts (php.net/apcu_add) find the pages of the extensions manual +# - pages that stayed in the language manual are untouched +# +# The directories below must sit side by side, each on the branch to test: +# +# doc-base/ doc-base +# phd/ phd +# en/ doc-en (fallback of the language manual) +# fr/ doc-fr (the language manual that is built) +# extensions/ doc-extensions +# web-php/ web-php (this repository) +# +# Usage: sh web-php/tests/extensions-migration.sh [ROOT] +# +# ROOT directory that holds the six directories (default: the parent of +# this repository) +# +# Environment: +# PHP php command, needs the dom, intl, mbstring, sqlite3, xmlreader and +# xmlwriter extensions (default: php) +# PORT port of the web server (default: 8080) +# KEEP set to 1 to leave the web server running at the end, to browse +# http://localhost:$PORT/manual/fr/extensions.alphabetical.php +# +# The built pages are written to extensions/output and fr/output. The links of +# the extensions manual to the main manual are rewritten in extensions/output, +# so that "back" stays on this server instead of going to www.php.net. + +set -eu + +ROOT=${1:-$(cd "$(dirname "$0")/../.." && pwd)} +PHP=${PHP:-php} +PORT=${PORT:-8080} +KEEP=${KEEP:-0} +SITE="$ROOT/web-php" +LOG="$SITE/.build/extensions-migration-server.log" +BUILD_LOG="$SITE/.build/extensions-migration-build.log" +BASE="http://localhost:$PORT" +FAILURES=0 + +for directory in doc-base phd en fr extensions web-php; do + if [ ! -d "$ROOT/$directory" ]; then + echo "Missing directory: $ROOT/$directory" >&2 + exit 2 + fi +done + +step() { + printf '\n== %s\n' "$1" +} + +pass() { + printf ' ok %s\n' "$1" +} + +fail() { + printf ' FAIL %s\n' "$1" + FAILURES=$((FAILURES + 1)) +} + +# check_status URL EXPECTED_CODE [ACCEPT_LANGUAGE] +check_status() { + code=$(curl -s -o /dev/null -w '%{http_code}' -H "Accept-Language: ${3:-fr}" "$BASE$1") + if [ "$code" = "$2" ]; then pass "$2 $1"; else fail "$1: expected $2, got $code"; fi +} + +# check_redirect URL EXPECTED_LOCATION_PATH +check_redirect() { + result=$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' "$BASE$1") + if [ "$result" = "301 $BASE$2" ]; then pass "301 $1 -> $2"; else fail "$1: expected 301 $2, got $result"; fi +} + +# check_page_contains URL TEXT DESCRIPTION +check_page_contains() { + if curl -s -H 'Accept-Language: fr' "$BASE$1" | grep -q -F -- "$2"; then pass "$3"; else fail "$3 ($1)"; fi +} + +# check_page_lacks URL TEXT DESCRIPTION +check_page_lacks() { + if curl -s -H 'Accept-Language: fr' "$BASE$1" | grep -q -F -- "$2"; then fail "$3 ($1)"; else pass "$3"; fi +} + +# run COMMAND...: the output goes to the build log, shown if the command fails +run() { + if ! "$@" >>"$BUILD_LOG" 2>&1; then + echo "Command failed: $*" >&2 + tail -n 30 "$BUILD_LOG" >&2 + exit 1 + fi +} + +cleanup() { + if [ "${SERVER_PID:-}" != "" ] && [ "$KEEP" != "1" ]; then + kill "$SERVER_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +mkdir -p "$SITE/.build" +: >"$BUILD_LOG" + +step "Build the extensions manual (log: $BUILD_LOG)" +run $PHP -d memory_limit=-1 "$ROOT/doc-base/configure.php" --with-base-lang=extensions +run $PHP -d memory_limit=-1 "$ROOT/phd/render.php" --docbook "$ROOT/doc-base/.manual.xml" \ + --output="$ROOT/extensions/output" --package PHP --format php --lang extensions + +step "Build the language manual (fr)" +run $PHP -d memory_limit=-1 "$ROOT/doc-base/configure.php" --with-lang=fr +run $PHP -d memory_limit=-1 "$ROOT/phd/render.php" --docbook "$ROOT/doc-base/.manual.xml" \ + --output="$ROOT/fr/output" --package PHP --format php --lang fr + +step "Install both manuals in web-php" +ln -sfn "$ROOT/fr/output/php-web" "$SITE/public/manual/fr" +ln -sfn "$ROOT/extensions/output/php-web" "$SITE/public/manual/extensions" +find "$ROOT/extensions/output/php-web" -name '*.php' \ + -exec sed -i "s#https://www.php.net/manual/en/#$BASE/manual/fr/#g" {} + +echo " fr -> $ROOT/fr/output/php-web" +echo " extensions -> $ROOT/extensions/output/php-web" + +step "Start the web server on $BASE" +: >"$LOG" +(cd "$SITE/public" && exec $PHP -d display_errors=1 -d error_reporting=E_ALL \ + -S "localhost:$PORT" -t "$SITE/public" "$SITE/public/.router.php") >"$LOG" 2>&1 & +SERVER_PID=$! +for attempt in 1 2 3 4 5 6 7 8 9 10; do + if curl -s -o /dev/null "$BASE/manual/fr/index.php"; then break; fi + sleep 1 +done + +step "Old URLs of moved pages are redirected (any language)" +check_redirect /manual/fr/function.apcu-add.php /manual/extensions/function.apcu-add.php +check_redirect /manual/fr/book.apcu.php /manual/extensions/book.apcu.php +check_redirect /manual/en/book.imagick.php /manual/extensions/book.imagick.php +check_redirect /manual/ja/ref.apcu.php /manual/extensions/ref.apcu.php +# A few identifiers are mixed case, the requested name is lowercased first. +check_redirect /manual/de/pool.submitto.php /manual/extensions/pool.submitTo.php +check_redirect /manual/fr/pool.submitTo.php /manual/extensions/pool.submitTo.php + +step "The extensions manual is served from /manual/extensions/" +check_status /manual/extensions/index.php 200 +check_status /manual/extensions/book.apcu.php 200 +check_status /manual/extensions/function.apcu-add.php 200 +check_status /manual/extensions/function.does-not-exist.php 404 +check_page_contains /manual/extensions/function.apcu-add.php 'lang="en"' "the page is English" +check_page_contains /manual/extensions/function.apcu-add.php 'rel="canonical" href="https://www.php.net/manual/extensions/function.apcu-add.php"' "canonical URL is under manual/extensions" +check_page_lacks /manual/extensions/function.apcu-add.php 'id="changelang"' "no language chooser" +check_page_lacks /manual/extensions/function.apcu-add.php 'hreflang="de"' "no alternate language" + +step "Pages of the language manual are untouched" +check_status /manual/fr/function.strlen.php 200 +check_status /manual/fr/class.exception.php 200 +check_status /manual/fr/function.does-not-exist.php 404 +check_page_contains /manual/fr/function.strlen.php 'lang="fr"' "the page is French" +check_page_contains /manual/fr/function.strlen.php 'id="changelang"' "language chooser is there" + +step "Shortcuts find the pages of the extensions manual" +check_status /apcu_add 200 +check_status /apcu-add 200 en +check_status /imagick 200 +check_page_contains /apcu_add 'PHP: apcu_add - Manual' "/apcu_add shows the page of apcu_add" +check_status /strlen 200 + +step "From the language manual to the extensions manual" +# The PDO drivers have no page yet: their references wait for a wrapper book +# in doc-extensions (see the comment at the end of its manual.xml). The links +# already point at the address the pages will have. +known_missing="extensions/ref.pdo-cubrid.php extensions/ref.pdo-ibm.php extensions/ref.pdo-informix.php extensions/ref.pdo-sqlsrv.php" +links=$(grep -h -o 'href="\.\./extensions/[^"#]*' "$ROOT"/fr/output/php-web/*.php | sed 's#href="\.\./##' | sort -u) +count=0 +bad=0 +for link in $links; do + count=$((count + 1)) + code=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/manual/$link") + if [ "$code" != "200" ]; then + missing=0 + for name in $known_missing; do + if [ "$link" = "$name" ]; then missing=1; fi + done + if [ "$missing" -eq 1 ]; then + printf ' known /manual/%s: %s\n' "$link" "$code" + else + bad=$((bad + 1)) + fail "/manual/$link: $code" + fi + fi +done +if [ "$count" -eq 0 ]; then fail "no page of the language manual links to the extensions manual"; fi +if [ "$bad" -eq 0 ] && [ "$count" -gt 0 ]; then pass "$count distinct links, all answer 200 but the known ones"; fi + +step "Back, from the extensions manual to the language manual" +# These two pages were renamed in the main manual and the summaries of +# doc-extensions still point to the old names. +known_stale="language.exceptions.php reserved.constants.core.php" +links=$(grep -h -o "href=\"$BASE/manual/fr/[^\"#]*" "$ROOT"/extensions/output/php-web/*.php | sed "s#href=\"$BASE##" | sort -u) +count=0 +bad=0 +for link in $links; do + count=$((count + 1)) + code=$(curl -s -o /dev/null -w '%{http_code}' "$BASE$link") + if [ "$code" != "200" ]; then + stale=0 + for name in $known_stale; do + if [ "$link" = "/manual/fr/$name" ]; then stale=1; fi + done + if [ "$stale" -eq 1 ]; then + printf ' known %s: %s\n' "$link" "$code" + else + bad=$((bad + 1)) + fail "$link: $code" + fi + fi +done +if [ "$count" -eq 0 ]; then fail "no page of the extensions manual links back"; fi +if [ "$bad" -eq 0 ] && [ "$count" -gt 0 ]; then pass "$count distinct links, all answer 200 but the known ones"; fi + +step "The server raised no PHP warning" +if grep -q -E 'PHP (Warning|Fatal error|Deprecated|Notice)' "$LOG"; then + fail "see $LOG" + grep -E 'PHP (Warning|Fatal error|Deprecated|Notice)' "$LOG" | sort | uniq -c | head -5 +else + pass "no warning in $LOG" +fi + +printf '\n' +if [ "$FAILURES" -eq 0 ]; then + echo "All checks passed." +else + echo "$FAILURES check(s) failed." +fi +if [ "$KEEP" = "1" ]; then + echo "Server left running on $BASE (pid $SERVER_PID)." +fi + +[ "$FAILURES" -eq 0 ] From d3f0a078d661b1bfa281b5dab61dcc4a557a1501 Mon Sep 17 00:00:00 2001 From: lacatoire Date: Thu, 22 Oct 2026 16:38:11 +0200 Subject: [PATCH 5/7] Build the main manual without the extensions manual next to it in the test doc-base defines an entity for every book of every sibling directory, which hid the includes of books that moved. The test now builds the main manual as its own CI does, and validates the English manual too. --- tests/extensions-migration.sh | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/tests/extensions-migration.sh b/tests/extensions-migration.sh index 91d92b43cf..d8ce11539e 100755 --- a/tests/extensions-migration.sh +++ b/tests/extensions-migration.sh @@ -29,6 +29,7 @@ # PHP php command, needs the dom, intl, mbstring, sqlite3, xmlreader and # xmlwriter extensions (default: php) # PORT port of the web server (default: 8080) +# WORKERS workers of the web server (default: 8) # KEEP set to 1 to leave the web server running at the end, to browse # http://localhost:$PORT/manual/fr/extensions.alphabetical.php # @@ -99,7 +100,21 @@ run() { fi } +# The main manual must build without the extensions manual next to it, as it +# does in its own CI: doc-base defines an entity for every book of every +# sibling directory, which would hide an include of a book that moved. +hide_extensions() { + mv "$ROOT/extensions" "$ROOT/.extensions.hidden" +} + +restore_extensions() { + if [ -e "$ROOT/.extensions.hidden" ]; then + mv "$ROOT/.extensions.hidden" "$ROOT/extensions" + fi +} + cleanup() { + restore_extensions if [ "${SERVER_PID:-}" != "" ] && [ "$KEEP" != "1" ]; then kill "$SERVER_PID" 2>/dev/null || true fi @@ -109,15 +124,18 @@ trap cleanup EXIT mkdir -p "$SITE/.build" : >"$BUILD_LOG" -step "Build the extensions manual (log: $BUILD_LOG)" -run $PHP -d memory_limit=-1 "$ROOT/doc-base/configure.php" --with-base-lang=extensions -run $PHP -d memory_limit=-1 "$ROOT/phd/render.php" --docbook "$ROOT/doc-base/.manual.xml" \ - --output="$ROOT/extensions/output" --package PHP --format php --lang extensions - -step "Build the language manual (fr)" +step "Build the main manual without the extensions manual next to it (log: $BUILD_LOG)" +hide_extensions +run $PHP -d memory_limit=-1 "$ROOT/doc-base/configure.php" --with-lang=en run $PHP -d memory_limit=-1 "$ROOT/doc-base/configure.php" --with-lang=fr run $PHP -d memory_limit=-1 "$ROOT/phd/render.php" --docbook "$ROOT/doc-base/.manual.xml" \ --output="$ROOT/fr/output" --package PHP --format php --lang fr +restore_extensions + +step "Build the extensions manual" +run $PHP -d memory_limit=-1 "$ROOT/doc-base/configure.php" --with-base-lang=extensions +run $PHP -d memory_limit=-1 "$ROOT/phd/render.php" --docbook "$ROOT/doc-base/.manual.xml" \ + --output="$ROOT/extensions/output" --package PHP --format php --lang extensions step "Install both manuals in web-php" ln -sfn "$ROOT/fr/output/php-web" "$SITE/public/manual/fr" @@ -129,7 +147,7 @@ echo " extensions -> $ROOT/extensions/output/php-web" step "Start the web server on $BASE" : >"$LOG" -(cd "$SITE/public" && exec $PHP -d display_errors=1 -d error_reporting=E_ALL \ +(cd "$SITE/public" && PHP_CLI_SERVER_WORKERS=${WORKERS:-8} exec $PHP -d display_errors=1 -d error_reporting=E_ALL \ -S "localhost:$PORT" -t "$SITE/public" "$SITE/public/.router.php") >"$LOG" 2>&1 & SERVER_PID=$! for attempt in 1 2 3 4 5 6 7 8 9 10; do From 7d5ca2fd0a0053dc8aba792b128fb6307f3984ff Mon Sep 17 00:00:00 2001 From: lacatoire Date: Thu, 22 Oct 2026 16:39:46 +0200 Subject: [PATCH 6/7] Let the test build another language than French LANGUAGE selects the language manual to build, fr by default. --- tests/extensions-migration.sh | 51 ++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/tests/extensions-migration.sh b/tests/extensions-migration.sh index d8ce11539e..46a8382d3c 100755 --- a/tests/extensions-migration.sh +++ b/tests/extensions-migration.sh @@ -16,7 +16,7 @@ # doc-base/ doc-base # phd/ phd # en/ doc-en (fallback of the language manual) -# fr/ doc-fr (the language manual that is built) +# fr/ doc-fr (the language manual that is built, see LANGUAGE) # extensions/ doc-extensions # web-php/ web-php (this repository) # @@ -30,10 +30,12 @@ # xmlwriter extensions (default: php) # PORT port of the web server (default: 8080) # WORKERS workers of the web server (default: 8) +# LANGUAGE language manual to build: fr or es, or any language whose repository +# is side by side under its code (default: fr) # KEEP set to 1 to leave the web server running at the end, to browse -# http://localhost:$PORT/manual/fr/extensions.alphabetical.php +# http://localhost:$PORT/manual/$LANGUAGE/aliases.php # -# The built pages are written to extensions/output and fr/output. The links of +# The built pages are written to extensions/output and /output. The links of # the extensions manual to the main manual are rewritten in extensions/output, # so that "back" stays on this server instead of going to www.php.net. @@ -43,13 +45,14 @@ ROOT=${1:-$(cd "$(dirname "$0")/../.." && pwd)} PHP=${PHP:-php} PORT=${PORT:-8080} KEEP=${KEEP:-0} +LANGUAGE=${LANGUAGE:-fr} SITE="$ROOT/web-php" LOG="$SITE/.build/extensions-migration-server.log" BUILD_LOG="$SITE/.build/extensions-migration-build.log" BASE="http://localhost:$PORT" FAILURES=0 -for directory in doc-base phd en fr extensions web-php; do +for directory in doc-base phd en "$LANGUAGE" extensions web-php; do if [ ! -d "$ROOT/$directory" ]; then echo "Missing directory: $ROOT/$directory" >&2 exit 2 @@ -71,7 +74,7 @@ fail() { # check_status URL EXPECTED_CODE [ACCEPT_LANGUAGE] check_status() { - code=$(curl -s -o /dev/null -w '%{http_code}' -H "Accept-Language: ${3:-fr}" "$BASE$1") + code=$(curl -s -o /dev/null -w '%{http_code}' -H "Accept-Language: ${3:-$LANGUAGE}" "$BASE$1") if [ "$code" = "$2" ]; then pass "$2 $1"; else fail "$1: expected $2, got $code"; fi } @@ -83,12 +86,12 @@ check_redirect() { # check_page_contains URL TEXT DESCRIPTION check_page_contains() { - if curl -s -H 'Accept-Language: fr' "$BASE$1" | grep -q -F -- "$2"; then pass "$3"; else fail "$3 ($1)"; fi + if curl -s -H "Accept-Language: $LANGUAGE" "$BASE$1" | grep -q -F -- "$2"; then pass "$3"; else fail "$3 ($1)"; fi } # check_page_lacks URL TEXT DESCRIPTION check_page_lacks() { - if curl -s -H 'Accept-Language: fr' "$BASE$1" | grep -q -F -- "$2"; then fail "$3 ($1)"; else pass "$3"; fi + if curl -s -H "Accept-Language: $LANGUAGE" "$BASE$1" | grep -q -F -- "$2"; then fail "$3 ($1)"; else pass "$3"; fi } # run COMMAND...: the output goes to the build log, shown if the command fails @@ -127,9 +130,9 @@ mkdir -p "$SITE/.build" step "Build the main manual without the extensions manual next to it (log: $BUILD_LOG)" hide_extensions run $PHP -d memory_limit=-1 "$ROOT/doc-base/configure.php" --with-lang=en -run $PHP -d memory_limit=-1 "$ROOT/doc-base/configure.php" --with-lang=fr +run $PHP -d memory_limit=-1 "$ROOT/doc-base/configure.php" --with-lang=$LANGUAGE run $PHP -d memory_limit=-1 "$ROOT/phd/render.php" --docbook "$ROOT/doc-base/.manual.xml" \ - --output="$ROOT/fr/output" --package PHP --format php --lang fr + --output="$ROOT/$LANGUAGE/output" --package PHP --format php --lang "$LANGUAGE" restore_extensions step "Build the extensions manual" @@ -138,11 +141,11 @@ run $PHP -d memory_limit=-1 "$ROOT/phd/render.php" --docbook "$ROOT/doc-base/.ma --output="$ROOT/extensions/output" --package PHP --format php --lang extensions step "Install both manuals in web-php" -ln -sfn "$ROOT/fr/output/php-web" "$SITE/public/manual/fr" +ln -sfn "$ROOT/$LANGUAGE/output/php-web" "$SITE/public/manual/$LANGUAGE" ln -sfn "$ROOT/extensions/output/php-web" "$SITE/public/manual/extensions" find "$ROOT/extensions/output/php-web" -name '*.php' \ - -exec sed -i "s#https://www.php.net/manual/en/#$BASE/manual/fr/#g" {} + -echo " fr -> $ROOT/fr/output/php-web" + -exec sed -i "s#https://www.php.net/manual/en/#$BASE/manual/$LANGUAGE/#g" {} + +echo " $LANGUAGE -> $ROOT/$LANGUAGE/output/php-web" echo " extensions -> $ROOT/extensions/output/php-web" step "Start the web server on $BASE" @@ -151,18 +154,18 @@ step "Start the web server on $BASE" -S "localhost:$PORT" -t "$SITE/public" "$SITE/public/.router.php") >"$LOG" 2>&1 & SERVER_PID=$! for attempt in 1 2 3 4 5 6 7 8 9 10; do - if curl -s -o /dev/null "$BASE/manual/fr/index.php"; then break; fi + if curl -s -o /dev/null "$BASE/manual/$LANGUAGE/index.php"; then break; fi sleep 1 done step "Old URLs of moved pages are redirected (any language)" -check_redirect /manual/fr/function.apcu-add.php /manual/extensions/function.apcu-add.php -check_redirect /manual/fr/book.apcu.php /manual/extensions/book.apcu.php +check_redirect /manual/$LANGUAGE/function.apcu-add.php /manual/extensions/function.apcu-add.php +check_redirect /manual/$LANGUAGE/book.apcu.php /manual/extensions/book.apcu.php check_redirect /manual/en/book.imagick.php /manual/extensions/book.imagick.php check_redirect /manual/ja/ref.apcu.php /manual/extensions/ref.apcu.php # A few identifiers are mixed case, the requested name is lowercased first. check_redirect /manual/de/pool.submitto.php /manual/extensions/pool.submitTo.php -check_redirect /manual/fr/pool.submitTo.php /manual/extensions/pool.submitTo.php +check_redirect /manual/$LANGUAGE/pool.submitTo.php /manual/extensions/pool.submitTo.php step "The extensions manual is served from /manual/extensions/" check_status /manual/extensions/index.php 200 @@ -175,11 +178,11 @@ check_page_lacks /manual/extensions/function.apcu-add.php 'id="changelang"' "no check_page_lacks /manual/extensions/function.apcu-add.php 'hreflang="de"' "no alternate language" step "Pages of the language manual are untouched" -check_status /manual/fr/function.strlen.php 200 -check_status /manual/fr/class.exception.php 200 -check_status /manual/fr/function.does-not-exist.php 404 -check_page_contains /manual/fr/function.strlen.php 'lang="fr"' "the page is French" -check_page_contains /manual/fr/function.strlen.php 'id="changelang"' "language chooser is there" +check_status /manual/$LANGUAGE/function.strlen.php 200 +check_status /manual/$LANGUAGE/class.exception.php 200 +check_status /manual/$LANGUAGE/function.does-not-exist.php 404 +check_page_contains /manual/$LANGUAGE/function.strlen.php "lang=\"$LANGUAGE\"" "the page is in $LANGUAGE" +check_page_contains /manual/$LANGUAGE/function.strlen.php 'id="changelang"' "language chooser is there" step "Shortcuts find the pages of the extensions manual" check_status /apcu_add 200 @@ -193,7 +196,7 @@ step "From the language manual to the extensions manual" # in doc-extensions (see the comment at the end of its manual.xml). The links # already point at the address the pages will have. known_missing="extensions/ref.pdo-cubrid.php extensions/ref.pdo-ibm.php extensions/ref.pdo-informix.php extensions/ref.pdo-sqlsrv.php" -links=$(grep -h -o 'href="\.\./extensions/[^"#]*' "$ROOT"/fr/output/php-web/*.php | sed 's#href="\.\./##' | sort -u) +links=$(grep -h -o 'href="\.\./extensions/[^"#]*' "$ROOT"/$LANGUAGE/output/php-web/*.php | sed 's#href="\.\./##' | sort -u) count=0 bad=0 for link in $links; do @@ -219,7 +222,7 @@ step "Back, from the extensions manual to the language manual" # These two pages were renamed in the main manual and the summaries of # doc-extensions still point to the old names. known_stale="language.exceptions.php reserved.constants.core.php" -links=$(grep -h -o "href=\"$BASE/manual/fr/[^\"#]*" "$ROOT"/extensions/output/php-web/*.php | sed "s#href=\"$BASE##" | sort -u) +links=$(grep -h -o "href=\"$BASE/manual/$LANGUAGE/[^\"#]*" "$ROOT"/extensions/output/php-web/*.php | sed "s#href=\"$BASE##" | sort -u) count=0 bad=0 for link in $links; do @@ -228,7 +231,7 @@ for link in $links; do if [ "$code" != "200" ]; then stale=0 for name in $known_stale; do - if [ "$link" = "/manual/fr/$name" ]; then stale=1; fi + if [ "$link" = "/manual/$LANGUAGE/$name" ]; then stale=1; fi done if [ "$stale" -eq 1 ]; then printf ' known %s: %s\n' "$link" "$code" From cb0effa5ce23a5a24a39a386db48f96de839212b Mon Sep 17 00:00:00 2001 From: lacatoire Date: Thu, 22 Oct 2026 16:59:54 +0200 Subject: [PATCH 7/7] Add Playwright tests for the move of the extensions to the extensions manual The tests read the built manuals: every old URL in the language manual, a sample in six other languages, variants of the URL (no .php, upper case, trailing slash, query string, HEAD), pages that must not be redirected, the links and the navigation in both directions, and journeys in Chromium. They only exist when EXTENSIONS_MIGRATION=1, so the existing end-to-end job, which does not build the manuals, is unchanged. tests/extensions-migration.sh runs them with PLAYWRIGHT=1. --- playwright.config.ts | 31 ++++++ .../journeys.browser.spec.ts | 90 ++++++++++++++++ .../ExtensionsMigration/links.api.spec.ts | 101 ++++++++++++++++++ tests/EndToEnd/ExtensionsMigration/manuals.ts | 71 ++++++++++++ .../ExtensionsMigration/negatives.api.spec.ts | 83 ++++++++++++++ .../performance.api.spec.ts | 40 +++++++ .../ExtensionsMigration/redirects.api.spec.ts | 54 ++++++++++ .../ExtensionsMigration/variants.api.spec.ts | 42 ++++++++ tests/extensions-migration.sh | 16 +++ 9 files changed, 528 insertions(+) create mode 100644 tests/EndToEnd/ExtensionsMigration/journeys.browser.spec.ts create mode 100644 tests/EndToEnd/ExtensionsMigration/links.api.spec.ts create mode 100644 tests/EndToEnd/ExtensionsMigration/manuals.ts create mode 100644 tests/EndToEnd/ExtensionsMigration/negatives.api.spec.ts create mode 100644 tests/EndToEnd/ExtensionsMigration/performance.api.spec.ts create mode 100644 tests/EndToEnd/ExtensionsMigration/redirects.api.spec.ts create mode 100644 tests/EndToEnd/ExtensionsMigration/variants.api.spec.ts diff --git a/playwright.config.ts b/playwright.config.ts index a138099278..4e9df757d7 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,12 @@ import {defineConfig, devices} from '@playwright/test'; +/** + * The tests of the move of the extensions to the extensions manual need the + * manuals built by tests/extensions-migration.sh, so they only exist when + * EXTENSIONS_MIGRATION=1 (that script sets it). + */ +const extensionsMigration = process.env.EXTENSIONS_MIGRATION === '1'; + /** * See https://playwright.dev/docs/test-configuration. */ @@ -34,6 +41,30 @@ export default defineConfig({ name: 'End-to-End Chromium', use: {...devices['Desktop Chrome']}, testDir: './tests/EndToEnd', + testIgnore: '**/ExtensionsMigration/**', }, + ...(extensionsMigration + ? [ + { + name: 'Extensions migration (API)', + testDir: './tests/EndToEnd/ExtensionsMigration', + testMatch: '*.api.spec.ts', + timeout: 120_000, + use: {baseURL: process.env.HTTP_HOST ?? 'http://localhost:8080'}, + }, + { + name: 'Extensions migration (Chromium)', + testDir: './tests/EndToEnd/ExtensionsMigration', + testMatch: '*.browser.spec.ts', + timeout: 30_000, + expect: {timeout: 10_000}, + use: { + ...devices['Desktop Chrome'], + baseURL: process.env.HTTP_HOST ?? 'http://localhost:8080', + actionTimeout: 10_000, + }, + }, + ] + : []), ], }); diff --git a/tests/EndToEnd/ExtensionsMigration/journeys.browser.spec.ts b/tests/EndToEnd/ExtensionsMigration/journeys.browser.spec.ts new file mode 100644 index 0000000000..794a81b8f8 --- /dev/null +++ b/tests/EndToEnd/ExtensionsMigration/journeys.browser.spec.ts @@ -0,0 +1,90 @@ +import {devices, expect, test} from '@playwright/test'; +import {language} from './manuals'; + +/** + * What a visitor does in a browser: follow an old link, go from one manual to + * the other and come back, type a shortcut. + */ + +test('an old URL lands on the page of the extensions manual after a 301', async ({page}) => { + const response = await page.goto(`/manual/${language}/function.apcu-add.php`); + + await expect(page).toHaveURL(/\/manual\/extensions\/function\.apcu-add\.php$/); + await expect(page.locator('h1.refname')).toHaveText('apcu_add'); + expect(response?.status()).toBe(200); + + const previous = response?.request().redirectedFrom(); + expect(previous, 'the browser was redirected').not.toBeNull(); + expect((await previous?.response())?.status()).toBe(301); +}); + +test('from the language manual to the extensions manual, and back', async ({page}) => { + await page.goto(`/manual/${language}/aliases.php`); + await expect(page.locator('#changelang'), 'the language manual has a chooser').toHaveCount(1); + + await page.locator('a[href*="../extensions/"]').first().click(); + await expect(page).toHaveURL(/\/manual\/extensions\//); + await expect(page.locator('#changelang'), 'the extensions manual has none').toHaveCount(0); + await expect(page.locator('#breadcrumbs-inner')).toContainText('Third-Party Extension'); + + // The way back: a page of the extensions manual that links to the main manual. + await page.goto('/manual/extensions/apcu.installation.php'); + await page.locator(`a[href$="/manual/${language}/install.pecl.php"]`).first().click(); + await expect(page).toHaveURL(new RegExp(`/manual/${language}/install\\.pecl\\.php$`)); + await expect(page.locator('#changelang')).toHaveCount(1); + await expect(page.locator('html')).toHaveAttribute('lang', language); +}); + +test('the breadcrumbs of the extensions manual stay inside it', async ({page}) => { + await page.goto('/manual/extensions/function.apcu-add.php'); + + await page.locator('#breadcrumbs-inner a', {hasText: 'Third-Party Extensions'}).click(); + await expect(page).toHaveURL(/\/manual\/extensions\/extensions\.php$/); + + await page.locator('#breadcrumbs-inner a', {hasText: 'Third-Party Extension Manual'}).click(); + await expect(page).toHaveURL(/\/manual\/extensions\/index\.php$/); +}); + +test('a shortcut finds the page of a function that moved', async ({page}) => { + await page.goto('/apcu_add'); + + await expect(page).toHaveTitle(/apcu_add/); + await expect(page.locator('h1.refname')).toHaveText('apcu_add'); +}); + +test('the extensions manual reads without JavaScript', async ({browser}) => { + const context = await browser.newContext({javaScriptEnabled: false}); + const page = await context.newPage(); + + await page.goto('/manual/extensions/function.apcu-add.php'); + await expect(page.locator('h1.refname')).toBeVisible(); + await page.locator('#breadcrumbs-inner a', {hasText: 'Third-Party Extensions'}).click(); + await expect(page).toHaveURL(/\/manual\/extensions\/extensions\.php$/); + + await context.close(); +}); + +test('a page of the extensions manual does not overflow on a phone', async ({browser}) => { + const context = await browser.newContext({...devices['Pixel 5']}); + const page = await context.newPage(); + + await page.goto('/manual/extensions/function.apcu-add.php'); + await expect(page.locator('h1.refname')).toBeVisible(); + + const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth); + expect(overflow, 'horizontal overflow in pixels').toBeLessThanOrEqual(1); + + await test.info().attach('phone', {body: await page.screenshot(), contentType: 'image/png'}); + await context.close(); +}); + +test('the pages of the two manuals carry the right language and canonical URL', async ({page}) => { + await page.goto('/manual/extensions/book.apcu.php'); + await expect(page.locator('html')).toHaveAttribute('lang', 'en'); + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', 'https://www.php.net/manual/extensions/book.apcu.php'); + await expect(page.locator('link[hreflang]'), 'no alternate language').toHaveCount(1); // only x-default + + await page.goto(`/manual/${language}/function.strlen.php`); + await expect(page.locator('html')).toHaveAttribute('lang', language); + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', `https://www.php.net/manual/${language}/function.strlen.php`); +}); diff --git a/tests/EndToEnd/ExtensionsMigration/links.api.spec.ts b/tests/EndToEnd/ExtensionsMigration/links.api.spec.ts new file mode 100644 index 0000000000..62cadcb108 --- /dev/null +++ b/tests/EndToEnd/ExtensionsMigration/links.api.spec.ts @@ -0,0 +1,101 @@ +import {expect, test} from '@playwright/test'; +import { + extensionPages, + inParallel, + knownMissingExtensionPages, + knownStaleLanguagePages, + language, + languagePages, + pageContent, + sample, +} from './manuals'; + +/** + * The links between the two manuals, in both directions, and the navigation + * links of the pages of the extensions manual. + */ + +function distinct(pattern: RegExp, pages: string[], directory: 'extensions' | 'language'): string[] { + const links = new Set(); + for (const page of pages) { + for (const match of pageContent(directory, page).matchAll(pattern)) { + links.add(match[1]); + } + } + + return [...links].sort(); +} + +test(`from the ${language} manual to the extensions manual`, async ({request}) => { + const links = distinct(/href="\.\.\/extensions\/([^"#]+)\.php/g, languagePages, 'language'); + expect(links.length, 'the language manual links to the extensions manual').toBeGreaterThan(10); + + const failures: string[] = []; + await inParallel(links, 20, async (page) => { + const response = await request.get(`/manual/extensions/${page}.php`, {maxRedirects: 0}); + if (response.status() !== 200 && !knownMissingExtensionPages.includes(page)) { + failures.push(`${page}: ${response.status()}`); + } + }); + + expect(failures, `${links.length} distinct links`).toEqual([]); +}); + +test(`from the extensions manual back to the ${language} manual`, async ({request}) => { + const links = distinct(new RegExp(`href="[^"]*/manual/${language}/([^"#]+)\\.php`, 'g'), extensionPages, 'extensions'); + expect(links.length, 'the extensions manual links back').toBeGreaterThan(50); + + const failures: string[] = []; + await inParallel(links, 20, async (page) => { + const response = await request.get(`/manual/${language}/${page}.php`, {maxRedirects: 0}); + if (response.status() !== 200 && !knownStaleLanguagePages.includes(page)) { + failures.push(`${page}: ${response.status()}`); + } + }); + + expect(failures, `${links.length} distinct links`).toEqual([]); +}); + +test('the navigation links of the pages of the extensions manual answer 200', async ({request}) => { + const failures: string[] = []; + + await inParallel(sample(extensionPages, 25), 10, async (page) => { + const response = await request.get(`/manual/extensions/${page}.php`); + const html = await response.text(); + + for (const match of html.matchAll(/ { + const failures: string[] = []; + const withoutBreadcrumbs: string[] = []; + + await inParallel(sample(extensionPages, 60), 10, async (page) => { + const url = `/manual/extensions/${page}.php`; + const html = await (await request.get(url)).text(); + const crumbs = html.match(/