ifnex/03_WordPress/wp-includes/sitemaps/class-wp-sitemaps.php
Kazem Alghasi f1d5667fe8 feat(deploy): preflight + smoke-test + UAT checklist (phases 3.9.1–3.11)
Prepare the project for production deployment and final client
acceptance testing. Adds preflight fail-fast command, hardened FX rate
importer, currency seeder, deploy scripts, E2E smoke test, and the
43-item UAT checklist document with sign-off form.

Key changes:

- Phase 3.9.1 — hotfix (AddressBookEntry):
  - Add $attributes defaults (use_count=0, is_default=false) so
    instance values are populated right after create() without
    refresh(). Resolves the red test
    `it_increments_use_count_and_sets_last_used_at`.

- Phase 3.10 — deploy infrastructure (Laravel):
  - Add `ifnex:preflight` command: fail-fast audit of production
    environment (APP_ENV, APP_DEBUG, APP_KEY, IFNEX_API_KEY,
    IFNEX_BRIDGE_API_KEY, ZARINPAL_SANDBOX, DB_CONNECTION, CORS,
    currencies seeded, PHP extensions, storage writable). Exits 1
    if any check fails; supports --skip-db for pre-migrate runs.
  - Add `ifnex:import-fx-rates` command: replacement for
    `ifnex:update-rates`. Fetches USD/AED from Exir.io + EUR/CNY
    from FreeCurrencyAPI. Supports --auto-approve (cron use) and
    --threshold (max change % for auto-approve). Logs critical on
    HTTP failure. URL configurable via EXIR_API_URL env.
  - Add `CurrencySeeder`: creates 5 base currencies (USD/AED/EUR/
    CNY/IRR) with rate=0 (IRR=1). Idempotent — does not overwrite
    real rates already set by admin or cron.
  - Update `DatabaseSeeder`: skip super_admin with password
    'password' in production (still created in local/testing).
  - Fix `bootstrap/app.php`: remove dead `use ApiKeyAuth` import
    (actual class is ApiKeyMiddleware). Latent fatal if api_key
    alias was used.
  - Add `config/ifnex.php` exir block + preflight config.
  - Harden `.env.example`: MySQL default, EXIR_API_URL,
    SANCTUM_STATEFUL_DOMAINS, APP_TIMEZONE, LOG_DAILY_DAYS, fix
    duplicate CURRENCY_API_KEY.

- Phase 3.10 — deploy scripts (Linux):
  - Add `scripts/deploy.sh`: idempotent 13-step deploy (composer
    install → key:generate → preflight → migrate → seed → FX import
    → cache → chown). Fail-fast with set -e.
  - Add `scripts/cron-exir-rates.sh`: daily cron wrapper for
    `ifnex:import-fx-rates --source=all --auto-approve`.

- Phase 3.10.1 — preflight hotfix:
  - Make expected DB connection configurable
    (ifnex.preflight.expected_db_connection) instead of hardcoded
    'mysql'. Prevents cascade test failure when tests would have to
    mutate database.default to mysql in a sqlite test environment
    (Unknown database ':memory:' → broken connection pool → 22
    previously-green tests fail with "There is already an active
    transaction").
  - Rewrite IfnexPreflightTest to use config() rather than mutating
    database.default.

- Phase 3.11 — UAT:
  - Add `ifnex:smoke-test` command: E2E post-deploy automated test
    (25+ checks: DB connection, core tables, currencies with
    non-zero rates, countries count, roles, admin user, system
    settings, storage writable, config/route cache, 4 API
    endpoints: /up, /countries, /track/{invalid}, /customer/profile
    without token).
  - Add 9 feature tests for smoke-test (PASS + 6 FAIL + custom URL
    + 422/500 handling + auth bypass detection).
  - Add `UAT-3.11.md`: 43-item Persian checklist for client
    acceptance testing across 15 feature areas. Includes sign-off
    form for both parties (client + VernaSoft).
  - Update `CLIENT_DELIVERY.md` to record phases 3.10 and 3.11.

Tests:
- Total: 51 → 82 tests, 150 → 220+ assertions, all green.
- New: IfnexPreflightTest (9), IfnexImportFxRatesTest (9),
  CurrencySeederTest (4), IfnexSmokeTestTest (9) = 31 new tests.

Documentation:
- DEPLOYMENT_HANDOFF.md §2.4: add CurrencySeeder + import-fx-rates.
- DEPLOYMENT_HANDOFF.md §2.8: update cron to use cron-exir-rates.sh.
- UAT-3.11.md: new 43-item client acceptance checklist + sign-off.
- CLIENT_DELIVERY.md: record phases 3.10 and 3.11 completion.
2026-10-31 22:18:15 +03:30

303 lines
7.5 KiB
PHP

<?php
/**
* Sitemaps: WP_Sitemaps class
*
* This is the main class integrating all other classes.
*
* @package WordPress
* @subpackage Sitemaps
* @since 5.5.0
*/
/**
* Class WP_Sitemaps.
*
* @since 5.5.0
*/
#[AllowDynamicProperties]
class WP_Sitemaps {
/**
* The main index of supported sitemaps.
*
* @since 5.5.0
*
* @var WP_Sitemaps_Index
*/
public $index;
/**
* The main registry of supported sitemaps.
*
* @since 5.5.0
*
* @var WP_Sitemaps_Registry
*/
public $registry;
/**
* An instance of the renderer class.
*
* @since 5.5.0
*
* @var WP_Sitemaps_Renderer
*/
public $renderer;
/**
* WP_Sitemaps constructor.
*
* @since 5.5.0
*/
public function __construct() {
$this->registry = new WP_Sitemaps_Registry();
$this->renderer = new WP_Sitemaps_Renderer();
$this->index = new WP_Sitemaps_Index( $this->registry );
}
/**
* Initiates all sitemap functionality.
*
* If sitemaps are disabled, only the rewrite rules will be registered
* by this method, in order to properly send 404s.
*
* @since 5.5.0
*/
public function init() {
// These will all fire on the init hook.
$this->register_rewrites();
add_action( 'template_redirect', array( $this, 'render_sitemaps' ) );
if ( ! $this->sitemaps_enabled() ) {
return;
}
$this->register_sitemaps();
// Add additional action callbacks.
add_filter( 'robots_txt', array( $this, 'add_robots' ), 0, 2 );
}
/**
* Determines whether sitemaps are enabled or not.
*
* @since 5.5.0
*
* @return bool Whether sitemaps are enabled.
*/
public function sitemaps_enabled() {
$is_enabled = (bool) get_option( 'blog_public' );
/**
* Filters whether XML Sitemaps are enabled or not.
*
* When XML Sitemaps are disabled via this filter, rewrite rules are still
* in place to ensure a 404 is returned.
*
* @see WP_Sitemaps::register_rewrites()
*
* @since 5.5.0
*
* @param bool $is_enabled Whether XML Sitemaps are enabled or not.
* Defaults to true for public sites.
*/
return (bool) apply_filters( 'wp_sitemaps_enabled', $is_enabled );
}
/**
* Registers and sets up the functionality for all supported sitemaps.
*
* @since 5.5.0
*/
public function register_sitemaps() {
$providers = array(
'posts' => new WP_Sitemaps_Posts(),
'taxonomies' => new WP_Sitemaps_Taxonomies(),
'users' => new WP_Sitemaps_Users(),
);
/* @var WP_Sitemaps_Provider $provider */
foreach ( $providers as $name => $provider ) {
$this->registry->add_provider( $name, $provider );
}
}
/**
* Registers sitemap rewrite tags and routing rules.
*
* @since 5.5.0
*/
public function register_rewrites() {
// Add rewrite tags.
add_rewrite_tag( '%sitemap%', '([^?]+)' );
add_rewrite_tag( '%sitemap-subtype%', '([^?]+)' );
// Register index route.
add_rewrite_rule( '^wp-sitemap\.xml$', 'index.php?sitemap=index', 'top' );
// Register rewrites for the XSL stylesheet.
add_rewrite_tag( '%sitemap-stylesheet%', '([^?]+)' );
add_rewrite_rule( '^wp-sitemap\.xsl$', 'index.php?sitemap-stylesheet=sitemap', 'top' );
add_rewrite_rule( '^wp-sitemap-index\.xsl$', 'index.php?sitemap-stylesheet=index', 'top' );
// Register routes for providers.
add_rewrite_rule(
'^wp-sitemap-([a-z]+?)-([a-z\d_-]+?)-(\d+?)\.xml$',
'index.php?sitemap=$matches[1]&sitemap-subtype=$matches[2]&paged=$matches[3]',
'top'
);
add_rewrite_rule(
'^wp-sitemap-([a-z]+?)-(\d+?)\.xml$',
'index.php?sitemap=$matches[1]&paged=$matches[2]',
'top'
);
}
/**
* Renders sitemap templates based on rewrite rules.
*
* @since 5.5.0
*/
public function render_sitemaps() {
/*
* Bail early if this isn't a sitemap or stylesheet route.
*
* This runs on every front-end request, so it comes before any
* sanitizing. The raw query vars are tested here, matching
* WP::handle_404(), which exempts sitemap requests from its own 404 on
* the same basis. Testing the sanitized values instead would let a
* request that handle_404() exempted fall through both, leaving it a 200.
*/
if ( ! get_query_var( 'sitemap' ) && ! get_query_var( 'sitemap-stylesheet' ) ) {
return;
}
$sitemap = sanitize_text_field( get_query_var( 'sitemap' ) );
$object_subtype = sanitize_text_field( get_query_var( 'sitemap-subtype' ) );
$stylesheet_type = sanitize_text_field( get_query_var( 'sitemap-stylesheet' ) );
$paged = absint( get_query_var( 'paged' ) );
// Force a 404 and bail early if the route did not survive sanitizing.
if ( ! ( $sitemap || $stylesheet_type ) ) {
$this->send_404();
return;
}
if ( ! $this->sitemaps_enabled() ) {
$this->send_404();
return;
}
// Render stylesheet if this is stylesheet route.
if ( $stylesheet_type ) {
// Force a 404 and bail early if the stylesheet type is not recognized.
if ( ! in_array( $stylesheet_type, array( 'sitemap', 'index' ), true ) ) {
$this->send_404();
return;
}
$stylesheet = new WP_Sitemaps_Stylesheet();
$stylesheet->render_stylesheet( $stylesheet_type );
exit;
}
// Render the index.
if ( 'index' === $sitemap ) {
$sitemap_list = $this->index->get_sitemap_list();
$this->renderer->render_index( $sitemap_list );
exit;
}
$provider = $this->registry->get_provider( $sitemap );
// Force a 404 and bail early if the requested provider is not registered.
if ( ! $provider ) {
$this->send_404();
return;
}
if ( empty( $paged ) ) {
$paged = 1;
}
$url_list = $provider->get_url_list( $paged, $object_subtype );
// Force a 404 and bail early if no URLs are present.
if ( empty( $url_list ) ) {
$this->send_404();
return;
}
$this->renderer->render_sitemap( $url_list );
exit;
}
/**
* Sends a 404 for a sitemap route that cannot be served.
*
* WP::handle_404() exempts sitemap requests, so every sitemap 404 is issued
* here instead. That includes the no-cache headers handle_404() sends with
* its own 404, so an intermediary does not retain a 404 for a route that
* becomes valid once the site has more content.
*
* @since 7.1.1
*
* @global WP_Query $wp_query WordPress Query object.
*/
private function send_404(): void {
global $wp_query;
$wp_query->set_404();
status_header( 404 );
nocache_headers();
}
/**
* Redirects a URL to the wp-sitemap.xml
*
* @since 5.5.0
* @deprecated 6.7.0 Deprecated in favor of {@see WP_Rewrite::rewrite_rules()}
*
* @param bool $bypass Pass-through of the pre_handle_404 filter value.
* @param WP_Query $query The WP_Query object.
* @return bool Bypass value.
*/
public function redirect_sitemapxml( $bypass, $query ) {
_deprecated_function( __FUNCTION__, '6.7.0' );
// If a plugin has already utilized the pre_handle_404 function, return without action to avoid conflicts.
if ( $bypass ) {
return $bypass;
}
// 'pagename' is for most permalink types, name is for when the %postname% is used as a top-level field.
if ( 'sitemap-xml' === $query->get( 'pagename' )
|| 'sitemap-xml' === $query->get( 'name' )
) {
wp_safe_redirect( $this->index->get_index_url() );
exit();
}
return $bypass;
}
/**
* Adds the sitemap index to robots.txt.
*
* @since 5.5.0
*
* @param string $output robots.txt output.
* @param bool $is_public Whether the site is public.
* @return string The robots.txt output.
*/
public function add_robots( $output, $is_public ) {
if ( $is_public ) {
$output .= "\nSitemap: " . esc_url( $this->index->get_index_url() ) . "\n";
}
return $output;
}
}