[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> class-wp-rewrite.php (source)

   1  <?php
   2  /**
   3   * Rewrite API: WP_Rewrite class
   4   *
   5   * @package WordPress
   6   * @subpackage Rewrite
   7   * @since 1.5.0
   8   */
   9  
  10  /**
  11   * Core class used to implement a rewrite component API.
  12   *
  13   * The WordPress Rewrite class writes the rewrite module rules to the .htaccess
  14   * file. It also handles parsing the request to get the correct setup for the
  15   * WordPress Query class.
  16   *
  17   * The Rewrite along with WP class function as a front controller for WordPress.
  18   * You can add rules to trigger your page view and processing using this
  19   * component. The full functionality of a front controller does not exist,
  20   * meaning you can't define how the template files load based on the rewrite
  21   * rules.
  22   *
  23   * @since 1.5.0
  24   */
  25  #[AllowDynamicProperties]
  26  class WP_Rewrite {
  27      /**
  28       * Permalink structure for posts.
  29       *
  30       * @since 1.5.0
  31       * @var string
  32       */
  33      public $permalink_structure;
  34  
  35      /**
  36       * Whether to add trailing slashes.
  37       *
  38       * @since 2.2.0
  39       * @var bool
  40       */
  41      public $use_trailing_slashes;
  42  
  43      /**
  44       * Base for the author permalink structure (example.com/$author_base/authorname).
  45       *
  46       * @since 1.5.0
  47       * @var string
  48       */
  49      public $author_base = 'author';
  50  
  51      /**
  52       * Permalink structure for author archives.
  53       *
  54       * @since 1.5.0
  55       * @var ?string
  56       */
  57      public $author_structure;
  58  
  59      /**
  60       * Permalink structure for date archives.
  61       *
  62       * @since 1.5.0
  63       * @var ?string
  64       */
  65      public $date_structure;
  66  
  67      /**
  68       * Permalink structure for pages.
  69       *
  70       * @since 1.5.0
  71       * @var ?string
  72       */
  73      public $page_structure;
  74  
  75      /**
  76       * Base of the search permalink structure (example.com/$search_base/query).
  77       *
  78       * @since 1.5.0
  79       * @var string
  80       */
  81      public $search_base = 'search';
  82  
  83      /**
  84       * Permalink structure for searches.
  85       *
  86       * @since 1.5.0
  87       * @var ?string
  88       */
  89      public $search_structure;
  90  
  91      /**
  92       * Comments permalink base.
  93       *
  94       * @since 1.5.0
  95       * @var string
  96       */
  97      public $comments_base = 'comments';
  98  
  99      /**
 100       * Pagination permalink base.
 101       *
 102       * @since 3.1.0
 103       * @var string
 104       */
 105      public $pagination_base = 'page';
 106  
 107      /**
 108       * Comments pagination permalink base.
 109       *
 110       * @since 4.2.0
 111       * @var string
 112       */
 113      public $comments_pagination_base = 'comment-page';
 114  
 115      /**
 116       * Feed permalink base.
 117       *
 118       * @since 1.5.0
 119       * @var string
 120       */
 121      public $feed_base = 'feed';
 122  
 123      /**
 124       * Custom random content permalink base.
 125       *
 126       * Set from the 'wp_random_base' option by {@see WP_Rewrite::init()}. Empty when no custom
 127       * base is set, in which case the translated default is used.
 128       * Use {@see WP_Rewrite::get_random_base()} to retrieve the base in use.
 129       *
 130       * @since 7.2.0
 131       * @var string
 132       */
 133      public string $random_base = '';
 134  
 135      /**
 136       * Comments feed permalink structure.
 137       *
 138       * @since 1.5.0
 139       * @var ?string
 140       */
 141      public $comment_feed_structure;
 142  
 143      /**
 144       * Feed request permalink structure.
 145       *
 146       * @since 1.5.0
 147       * @var ?string
 148       */
 149      public $feed_structure;
 150  
 151      /**
 152       * The static portion of the post permalink structure.
 153       *
 154       * If the permalink structure is "/archive/%post_id%" then the front
 155       * is "/archive/". If the permalink structure is "/%year%/%postname%/"
 156       * then the front is "/".
 157       *
 158       * @since 1.5.0
 159       * @var string
 160       *
 161       * @see WP_Rewrite::init()
 162       */
 163      public $front;
 164  
 165      /**
 166       * The prefix for all permalink structures.
 167       *
 168       * If PATHINFO/index permalinks are in use then the root is the value of
 169       * `WP_Rewrite::$index` with a trailing slash appended. Otherwise the root
 170       * will be empty.
 171       *
 172       * @since 1.5.0
 173       * @var string
 174       *
 175       * @see WP_Rewrite::init()
 176       * @see WP_Rewrite::using_index_permalinks()
 177       */
 178      public $root = '';
 179  
 180      /**
 181       * The name of the index file which is the entry point to all requests.
 182       *
 183       * @since 1.5.0
 184       * @var string
 185       */
 186      public $index = 'index.php';
 187  
 188      /**
 189       * Variable name to use for regex matches in the rewritten query.
 190       *
 191       * @since 1.5.0
 192       * @var string
 193       */
 194      public $matches = '';
 195  
 196      /**
 197       * Rewrite rules to match against the request to find the redirect or query.
 198       *
 199       * @since 1.5.0
 200       * @var string[]
 201       */
 202      public $rules;
 203  
 204      /**
 205       * Additional rules added external to the rewrite class.
 206       *
 207       * Those not generated by the class, see add_rewrite_rule().
 208       *
 209       * @since 2.1.0
 210       * @var string[]
 211       */
 212      public $extra_rules = array();
 213  
 214      /**
 215       * Additional rules that belong at the beginning to match first.
 216       *
 217       * Those not generated by the class, see add_rewrite_rule().
 218       *
 219       * @since 2.3.0
 220       * @var string[]
 221       */
 222      public $extra_rules_top = array();
 223  
 224      /**
 225       * Rules that don't redirect to WordPress' index.php.
 226       *
 227       * These rules are written to the mod_rewrite portion of the .htaccess,
 228       * and are added by add_external_rule().
 229       *
 230       * @since 2.1.0
 231       * @var string[]
 232       */
 233      public $non_wp_rules = array();
 234  
 235      /**
 236       * Extra permalink structures, e.g. categories, added by add_permastruct().
 237       *
 238       * @since 2.1.0
 239       * @var array[]
 240       */
 241      public $extra_permastructs = array();
 242  
 243      /**
 244       * Endpoints (like /trackback/) added by add_rewrite_endpoint().
 245       *
 246       * @since 2.1.0
 247       * @var array[]
 248       */
 249      public $endpoints;
 250  
 251      /**
 252       * Whether to write every mod_rewrite rule for WordPress into the .htaccess file.
 253       *
 254       * This is off by default, turning it on might print a lot of rewrite rules
 255       * to the .htaccess file.
 256       *
 257       * @since 2.0.0
 258       * @var bool
 259       *
 260       * @see WP_Rewrite::mod_rewrite_rules()
 261       */
 262      public $use_verbose_rules = false;
 263  
 264      /**
 265       * Could post permalinks be confused with those of pages?
 266       *
 267       * If the first rewrite tag in the post permalink structure is one that could
 268       * also match a page name (e.g. %postname% or %author%) then this flag is
 269       * set to true. Prior to WordPress 3.3 this flag indicated that every page
 270       * would have a set of rules added to the top of the rewrite rules array.
 271       * Now it tells WP::parse_request() to check if a URL matching the page
 272       * permastruct is actually a page before accepting it.
 273       *
 274       * @since 2.5.0
 275       * @var bool
 276       *
 277       * @see WP_Rewrite::init()
 278       */
 279      public $use_verbose_page_rules = true;
 280  
 281      /**
 282       * Rewrite tags that can be used in permalink structures.
 283       *
 284       * These are translated into the regular expressions stored in
 285       * `WP_Rewrite::$rewritereplace` and are rewritten to the query
 286       * variables listed in WP_Rewrite::$queryreplace.
 287       *
 288       * Additional tags can be added with add_rewrite_tag().
 289       *
 290       * @since 1.5.0
 291       * @var string[]
 292       */
 293      public $rewritecode = array(
 294          '%year%',
 295          '%monthnum%',
 296          '%day%',
 297          '%hour%',
 298          '%minute%',
 299          '%second%',
 300          '%postname%',
 301          '%post_id%',
 302          '%author%',
 303          '%pagename%',
 304          '%search%',
 305      );
 306  
 307      /**
 308       * Regular expressions to be substituted into rewrite rules in place
 309       * of rewrite tags, see WP_Rewrite::$rewritecode.
 310       *
 311       * @since 1.5.0
 312       * @var string[]
 313       */
 314      public $rewritereplace = array(
 315          '([0-9]{4})',
 316          '([0-9]{1,2})',
 317          '([0-9]{1,2})',
 318          '([0-9]{1,2})',
 319          '([0-9]{1,2})',
 320          '([0-9]{1,2})',
 321          '([^/]+)',
 322          '([0-9]+)',
 323          '([^/]+)',
 324          '([^/]+?)',
 325          '(.+)',
 326      );
 327  
 328      /**
 329       * Query variables that rewrite tags map to, see WP_Rewrite::$rewritecode.
 330       *
 331       * @since 1.5.0
 332       * @var string[]
 333       */
 334      public $queryreplace = array(
 335          'year=',
 336          'monthnum=',
 337          'day=',
 338          'hour=',
 339          'minute=',
 340          'second=',
 341          'name=',
 342          'p=',
 343          'author_name=',
 344          'pagename=',
 345          's=',
 346      );
 347  
 348      /**
 349       * Supported default feeds.
 350       *
 351       * @since 1.5.0
 352       * @var string[]
 353       */
 354      public $feeds = array( 'feed', 'rdf', 'rss', 'rss2', 'atom' );
 355  
 356      /**
 357       * Determines whether permalinks are being used.
 358       *
 359       * This can be either rewrite module or permalink in the HTTP query string.
 360       *
 361       * @since 1.5.0
 362       *
 363       * @return bool True, if permalinks are enabled.
 364       */
 365  	public function using_permalinks() {
 366          return ! empty( $this->permalink_structure );
 367      }
 368  
 369      /**
 370       * Determines whether permalinks are being used and rewrite module is not enabled.
 371       *
 372       * Means that permalink links are enabled and index.php is in the URL.
 373       *
 374       * @since 1.5.0
 375       *
 376       * @return bool Whether permalink links are enabled and index.php is in the URL.
 377       */
 378  	public function using_index_permalinks() {
 379          if ( empty( $this->permalink_structure ) ) {
 380              return false;
 381          }
 382  
 383          // If the index is not in the permalink, we're using mod_rewrite.
 384          return preg_match( '#^/*' . $this->index . '#', $this->permalink_structure );
 385      }
 386  
 387      /**
 388       * Determines whether permalinks are being used and rewrite module is enabled.
 389       *
 390       * Using permalinks and index.php is not in the URL.
 391       *
 392       * @since 1.5.0
 393       *
 394       * @return bool Whether permalink links are enabled and index.php is NOT in the URL.
 395       */
 396  	public function using_mod_rewrite_permalinks() {
 397          return $this->using_permalinks() && ! $this->using_index_permalinks();
 398      }
 399  
 400      /**
 401       * Indexes for matches for usage in preg_*() functions.
 402       *
 403       * The format of the string is, with empty matches property value, '$NUM'.
 404       * The 'NUM' will be replaced with the value in the $number parameter. With
 405       * the matches property not empty, the value of the returned string will
 406       * contain that value of the matches property. The format then will be
 407       * '$MATCHES[NUM]', with MATCHES as the value in the property and NUM the
 408       * value of the $number parameter.
 409       *
 410       * @since 1.5.0
 411       *
 412       * @param int $number Index number.
 413       * @return string
 414       */
 415  	public function preg_index( $number ) {
 416          $match_prefix = '$';
 417          $match_suffix = '';
 418  
 419          if ( ! empty( $this->matches ) ) {
 420              $match_prefix = '$' . $this->matches . '[';
 421              $match_suffix = ']';
 422          }
 423  
 424          return "$match_prefix$number$match_suffix";
 425      }
 426  
 427      /**
 428       * Retrieves all pages and attachments for pages URIs.
 429       *
 430       * The attachments are for those that have pages as parents and will be
 431       * retrieved.
 432       *
 433       * @since 2.5.0
 434       *
 435       * @global wpdb $wpdb WordPress database abstraction object.
 436       *
 437       * @return array Array of page URIs as first element and attachment URIs as second element.
 438       */
 439  	public function page_uri_index() {
 440          global $wpdb;
 441  
 442          // Get pages in order of hierarchy, i.e. children after parents.
 443          $pages = $wpdb->get_results( "SELECT ID, post_name, post_parent FROM $wpdb->posts WHERE post_type = 'page' AND post_status != 'auto-draft'" );
 444          $posts = get_page_hierarchy( $pages );
 445  
 446          // If we have no pages get out quick.
 447          if ( ! $posts ) {
 448              return array( array(), array() );
 449          }
 450  
 451          // Now reverse it, because we need parents after children for rewrite rules to work properly.
 452          $posts = array_reverse( $posts, true );
 453  
 454          $page_uris            = array();
 455          $page_attachment_uris = array();
 456  
 457          foreach ( $posts as $id => $post ) {
 458              // URL => page name.
 459              $uri         = get_page_uri( $id );
 460              $attachments = $wpdb->get_results( $wpdb->prepare( "SELECT ID, post_name, post_parent FROM $wpdb->posts WHERE post_type = 'attachment' AND post_parent = %d", $id ) );
 461              if ( ! empty( $attachments ) ) {
 462                  foreach ( $attachments as $attachment ) {
 463                      $attach_uri                          = get_page_uri( $attachment->ID );
 464                      $page_attachment_uris[ $attach_uri ] = $attachment->ID;
 465                  }
 466              }
 467  
 468              $page_uris[ $uri ] = $id;
 469          }
 470  
 471          return array( $page_uris, $page_attachment_uris );
 472      }
 473  
 474      /**
 475       * Retrieves all of the rewrite rules for pages.
 476       *
 477       * @since 1.5.0
 478       *
 479       * @return string[] Page rewrite rules.
 480       */
 481  	public function page_rewrite_rules() {
 482          // The extra .? at the beginning prevents clashes with other regular expressions in the rules array.
 483          $this->add_rewrite_tag( '%pagename%', '(.?.+?)', 'pagename=' );
 484  
 485          return $this->generate_rewrite_rules( $this->get_page_permastruct(), EP_PAGES, true, true, false, false );
 486      }
 487  
 488      /**
 489       * Retrieves date permalink structure, with year, month, and day.
 490       *
 491       * The permalink structure for the date, if not set already depends on the
 492       * permalink structure. It can be one of three formats. The first is year,
 493       * month, day; the second is day, month, year; and the last format is month,
 494       * day, year. These are matched against the permalink structure for which
 495       * one is used. If none matches, then the default will be used, which is
 496       * year, month, day.
 497       *
 498       * Prevents post ID and date permalinks from overlapping. In the case of
 499       * post_id, the date permalink will be prepended with front permalink with
 500       * 'date/' before the actual permalink to form the complete date permalink
 501       * structure.
 502       *
 503       * @since 1.5.0
 504       *
 505       * @return string|false Date permalink structure on success, false on failure.
 506       */
 507  	public function get_date_permastruct() {
 508          if ( isset( $this->date_structure ) ) {
 509              return $this->date_structure;
 510          }
 511  
 512          if ( empty( $this->permalink_structure ) ) {
 513              $this->date_structure = '';
 514              return false;
 515          }
 516  
 517          // The date permalink must have year, month, and day separated by slashes.
 518          $endians = array( '%year%/%monthnum%/%day%', '%day%/%monthnum%/%year%', '%monthnum%/%day%/%year%' );
 519  
 520          $this->date_structure = '';
 521          $date_endian          = '';
 522  
 523          foreach ( $endians as $endian ) {
 524              if ( str_contains( $this->permalink_structure, $endian ) ) {
 525                  $date_endian = $endian;
 526                  break;
 527              }
 528          }
 529  
 530          if ( empty( $date_endian ) ) {
 531              $date_endian = '%year%/%monthnum%/%day%';
 532          }
 533  
 534          /*
 535           * Do not allow the date tags and %post_id% to overlap in the permalink
 536           * structure. If they do, move the date tags to $front/date/.
 537           */
 538          $front = $this->front;
 539          preg_match_all( '/%.+?%/', $this->permalink_structure, $tokens );
 540          $tok_index = 1;
 541          foreach ( (array) $tokens[0] as $token ) {
 542              if ( '%post_id%' === $token && ( $tok_index <= 3 ) ) {
 543                  $front = $front . 'date/';
 544                  break;
 545              }
 546              ++$tok_index;
 547          }
 548  
 549          $this->date_structure = $front . $date_endian;
 550  
 551          return $this->date_structure;
 552      }
 553  
 554      /**
 555       * Retrieves the year permalink structure without month and day.
 556       *
 557       * Gets the date permalink structure and strips out the month and day
 558       * permalink structures.
 559       *
 560       * @since 1.5.0
 561       *
 562       * @return string|false Year permalink structure on success, false on failure.
 563       */
 564  	public function get_year_permastruct() {
 565          $structure = $this->get_date_permastruct();
 566  
 567          if ( empty( $structure ) ) {
 568              return false;
 569          }
 570  
 571          $structure = str_replace( '%monthnum%', '', $structure );
 572          $structure = str_replace( '%day%', '', $structure );
 573          $structure = preg_replace( '#/+#', '/', $structure );
 574  
 575          return $structure;
 576      }
 577  
 578      /**
 579       * Retrieves the month permalink structure without day and with year.
 580       *
 581       * Gets the date permalink structure and strips out the day permalink
 582       * structures. Keeps the year permalink structure.
 583       *
 584       * @since 1.5.0
 585       *
 586       * @return string|false Year/Month permalink structure on success, false on failure.
 587       */
 588  	public function get_month_permastruct() {
 589          $structure = $this->get_date_permastruct();
 590  
 591          if ( empty( $structure ) ) {
 592              return false;
 593          }
 594  
 595          $structure = str_replace( '%day%', '', $structure );
 596          $structure = preg_replace( '#/+#', '/', $structure );
 597  
 598          return $structure;
 599      }
 600  
 601      /**
 602       * Retrieves the day permalink structure with month and year.
 603       *
 604       * Keeps date permalink structure with all year, month, and day.
 605       *
 606       * @since 1.5.0
 607       *
 608       * @return string|false Year/Month/Day permalink structure on success, false on failure.
 609       */
 610  	public function get_day_permastruct() {
 611          return $this->get_date_permastruct();
 612      }
 613  
 614      /**
 615       * Retrieves the permalink structure for categories.
 616       *
 617       * If the category_base property has no value, then the category structure
 618       * will have the front property value, followed by 'category', and finally
 619       * '%category%'. If it does, then the root property will be used, along with
 620       * the category_base property value.
 621       *
 622       * @since 1.5.0
 623       *
 624       * @return string|false Category permalink structure on success, false on failure.
 625       */
 626  	public function get_category_permastruct() {
 627          return $this->get_extra_permastruct( 'category' );
 628      }
 629  
 630      /**
 631       * Retrieves the permalink structure for tags.
 632       *
 633       * If the tag_base property has no value, then the tag structure will have
 634       * the front property value, followed by 'tag', and finally '%tag%'. If it
 635       * does, then the root property will be used, along with the tag_base
 636       * property value.
 637       *
 638       * @since 2.3.0
 639       *
 640       * @return string|false Tag permalink structure on success, false on failure.
 641       */
 642  	public function get_tag_permastruct() {
 643          return $this->get_extra_permastruct( 'post_tag' );
 644      }
 645  
 646      /**
 647       * Retrieves an extra permalink structure by name.
 648       *
 649       * @since 2.5.0
 650       *
 651       * @param string $name Permalink structure name.
 652       * @return string|false Permalink structure string on success, false on failure.
 653       */
 654  	public function get_extra_permastruct( $name ) {
 655          if ( empty( $this->permalink_structure ) ) {
 656              return false;
 657          }
 658  
 659          if ( isset( $this->extra_permastructs[ $name ] ) ) {
 660              return $this->extra_permastructs[ $name ]['struct'];
 661          }
 662  
 663          return false;
 664      }
 665  
 666      /**
 667       * Retrieves the author permalink structure.
 668       *
 669       * The permalink structure is front property, author base, and finally
 670       * '/%author%'. Will set the author_structure property and then return it
 671       * without attempting to set the value again.
 672       *
 673       * @since 1.5.0
 674       *
 675       * @return string|false Author permalink structure on success, false on failure.
 676       */
 677  	public function get_author_permastruct() {
 678          if ( isset( $this->author_structure ) ) {
 679              return $this->author_structure;
 680          }
 681  
 682          if ( empty( $this->permalink_structure ) ) {
 683              $this->author_structure = '';
 684              return false;
 685          }
 686  
 687          $this->author_structure = $this->front . $this->author_base . '/%author%';
 688  
 689          return $this->author_structure;
 690      }
 691  
 692      /**
 693       * Retrieves the search permalink structure.
 694       *
 695       * The permalink structure is root property, search base, and finally
 696       * '/%search%'. Will set the search_structure property and then return it
 697       * without attempting to set the value again.
 698       *
 699       * @since 1.5.0
 700       *
 701       * @return string|false Search permalink structure on success, false on failure.
 702       */
 703  	public function get_search_permastruct() {
 704          if ( isset( $this->search_structure ) ) {
 705              return $this->search_structure;
 706          }
 707  
 708          if ( empty( $this->permalink_structure ) ) {
 709              $this->search_structure = '';
 710              return false;
 711          }
 712  
 713          $this->search_structure = $this->root . $this->search_base . '/%search%';
 714  
 715          return $this->search_structure;
 716      }
 717  
 718      /**
 719       * Retrieves the page permalink structure.
 720       *
 721       * The permalink structure is root property, and '%pagename%'. Will set the
 722       * page_structure property and then return it without attempting to set the
 723       * value again.
 724       *
 725       * @since 1.5.0
 726       *
 727       * @return string|false Page permalink structure on success, false on failure.
 728       */
 729  	public function get_page_permastruct() {
 730          if ( isset( $this->page_structure ) ) {
 731              return $this->page_structure;
 732          }
 733  
 734          if ( empty( $this->permalink_structure ) ) {
 735              $this->page_structure = '';
 736              return false;
 737          }
 738  
 739          $this->page_structure = $this->root . '%pagename%';
 740  
 741          return $this->page_structure;
 742      }
 743  
 744      /**
 745       * Retrieves the feed permalink structure.
 746       *
 747       * The permalink structure is root property, feed base, and finally
 748       * '/%feed%'. Will set the feed_structure property and then return it
 749       * without attempting to set the value again.
 750       *
 751       * @since 1.5.0
 752       *
 753       * @return string|false Feed permalink structure on success, false on failure.
 754       */
 755  	public function get_feed_permastruct() {
 756          if ( isset( $this->feed_structure ) ) {
 757              return $this->feed_structure;
 758          }
 759  
 760          if ( empty( $this->permalink_structure ) ) {
 761              $this->feed_structure = '';
 762              return false;
 763          }
 764  
 765          $this->feed_structure = $this->root . $this->feed_base . '/%feed%';
 766  
 767          return $this->feed_structure;
 768      }
 769  
 770      /**
 771       * Retrieves the comment feed permalink structure.
 772       *
 773       * The permalink structure is root property, comment base property, feed
 774       * base and finally '/%feed%'. Will set the comment_feed_structure property
 775       * and then return it without attempting to set the value again.
 776       *
 777       * @since 1.5.0
 778       *
 779       * @return string|false Comment feed permalink structure on success, false on failure.
 780       */
 781  	public function get_comment_feed_permastruct() {
 782          if ( isset( $this->comment_feed_structure ) ) {
 783              return $this->comment_feed_structure;
 784          }
 785  
 786          if ( empty( $this->permalink_structure ) ) {
 787              $this->comment_feed_structure = '';
 788              return false;
 789          }
 790  
 791          $this->comment_feed_structure = $this->root . $this->comments_base . '/' . $this->feed_base . '/%feed%';
 792  
 793          return $this->comment_feed_structure;
 794      }
 795  
 796      /**
 797       * Adds or updates existing rewrite tags (e.g. %postname%).
 798       *
 799       * If the tag already exists, replace the existing pattern and query for
 800       * that tag, otherwise add the new tag.
 801       *
 802       * @since 1.5.0
 803       *
 804       * @see WP_Rewrite::$rewritecode
 805       * @see WP_Rewrite::$rewritereplace
 806       * @see WP_Rewrite::$queryreplace
 807       *
 808       * @param string $tag   Name of the rewrite tag to add or update.
 809       * @param string $regex Regular expression to substitute the tag for in rewrite rules.
 810       * @param string $query String to append to the rewritten query. Must end in '='.
 811       */
 812  	public function add_rewrite_tag( $tag, $regex, $query ) {
 813          $position = array_search( $tag, $this->rewritecode, true );
 814          if ( false !== $position ) {
 815              $this->rewritereplace[ $position ] = $regex;
 816              $this->queryreplace[ $position ]   = $query;
 817          } else {
 818              $this->rewritecode[]    = $tag;
 819              $this->rewritereplace[] = $regex;
 820              $this->queryreplace[]   = $query;
 821          }
 822      }
 823  
 824  
 825      /**
 826       * Removes an existing rewrite tag.
 827       *
 828       * @since 4.5.0
 829       *
 830       * @see WP_Rewrite::$rewritecode
 831       * @see WP_Rewrite::$rewritereplace
 832       * @see WP_Rewrite::$queryreplace
 833       *
 834       * @param string $tag Name of the rewrite tag to remove.
 835       */
 836  	public function remove_rewrite_tag( $tag ) {
 837          $position = array_search( $tag, $this->rewritecode, true );
 838          if ( false !== $position ) {
 839              unset( $this->rewritecode[ $position ] );
 840              unset( $this->rewritereplace[ $position ] );
 841              unset( $this->queryreplace[ $position ] );
 842          }
 843      }
 844  
 845      /**
 846       * Generates rewrite rules from a permalink structure.
 847       *
 848       * The main WP_Rewrite function for building the rewrite rule list. The
 849       * contents of the function is a mix of black magic and regular expressions,
 850       * so best just ignore the contents and move to the parameters.
 851       *
 852       * @since 1.5.0
 853       *
 854       * @param string $permalink_structure The permalink structure.
 855       * @param int    $ep_mask             Optional. Endpoint mask defining what endpoints are added to the structure.
 856       *                                    Accepts a mask of:
 857       *                                    - `EP_ALL`
 858       *                                    - `EP_NONE`
 859       *                                    - `EP_ALL_ARCHIVES`
 860       *                                    - `EP_ATTACHMENT`
 861       *                                    - `EP_AUTHORS`
 862       *                                    - `EP_CATEGORIES`
 863       *                                    - `EP_COMMENTS`
 864       *                                    - `EP_DATE`
 865       *                                    - `EP_DAY`
 866       *                                    - `EP_MONTH`
 867       *                                    - `EP_PAGES`
 868       *                                    - `EP_PERMALINK`
 869       *                                    - `EP_ROOT`
 870       *                                    - `EP_SEARCH`
 871       *                                    - `EP_TAGS`
 872       *                                    - `EP_YEAR`
 873       *                                    Default `EP_NONE`.
 874       * @param bool   $paged               Optional. Whether archive pagination rules should be added for the structure.
 875       *                                    Default true.
 876       * @param bool   $feed                Optional. Whether feed rewrite rules should be added for the structure.
 877       *                                    Default true.
 878       * @param bool   $forcomments         Optional. Whether the feed rules should be a query for a comments feed.
 879       *                                    Default false.
 880       * @param bool   $walk_dirs           Optional. Whether the 'directories' making up the structure should be walked
 881       *                                    over and rewrite rules built for each in-turn. Default true.
 882       * @param bool   $endpoints           Optional. Whether endpoints should be applied to the generated rewrite rules.
 883       *                                    Default true.
 884       * @return array<string, string> Array of rewrite rules keyed by their regex pattern.
 885       *
 886       * @phpstan-return array<non-falsy-string, non-falsy-string>
 887       */
 888  	public function generate_rewrite_rules( $permalink_structure, $ep_mask = EP_NONE, $paged = true, $feed = true, $forcomments = false, $walk_dirs = true, $endpoints = true ) {
 889          // Build a regex to match the feed section of URLs, something like (feed|atom|rss|rss2)/?
 890          $feedregex2 = '';
 891          foreach ( (array) $this->feeds as $feed_name ) {
 892              $feedregex2 .= $feed_name . '|';
 893          }
 894          $feedregex2 = '(' . trim( $feedregex2, '|' ) . ')/?$';
 895  
 896          /*
 897           * $feedregex is identical but with /feed/ added on as well, so URLs like <permalink>/feed/atom
 898           * and <permalink>/atom are both possible
 899           */
 900          $feedregex = $this->feed_base . '/' . $feedregex2;
 901  
 902          // Build a regex to match the trackback and page/xx parts of URLs.
 903          $trackbackregex = 'trackback/?$';
 904          $pageregex      = $this->pagination_base . '/?([0-9]{1,})/?$';
 905          $commentregex   = $this->comments_pagination_base . '-([0-9]{1,})/?$';
 906          $embedregex     = 'embed/?$';
 907  
 908          // Build up an array of endpoint regexes to append => queries to append.
 909          if ( $endpoints ) {
 910              $ep_query_append = array();
 911              foreach ( (array) $this->endpoints as $endpoint ) {
 912                  // Match everything after the endpoint name, but allow for nothing to appear there.
 913                  $epmatch = $endpoint[1] . '(/(.*))?/?$';
 914  
 915                  // This will be appended on to the rest of the query for each dir.
 916                  $epquery                     = '&' . $endpoint[2] . '=';
 917                  $ep_query_append[ $epmatch ] = array( $endpoint[0], $epquery );
 918              }
 919          }
 920  
 921          // Get everything up to the first rewrite tag.
 922          $front = substr( $permalink_structure, 0, strpos( $permalink_structure, '%' ) );
 923  
 924          // Build an array of the tags (note that said array ends up being in $tokens[0]).
 925          preg_match_all( '/%.+?%/', $permalink_structure, $tokens );
 926  
 927          $num_tokens = count( $tokens[0] );
 928  
 929          $index          = $this->index; // Probably 'index.php'.
 930          $feedindex      = $index;
 931          $trackbackindex = $index;
 932          $embedindex     = $index;
 933  
 934          /*
 935           * Build a list from the rewritecode and queryreplace arrays, that will look something
 936           * like tagname=$matches[i] where i is the current $i.
 937           */
 938          $queries = array();
 939          for ( $i = 0; $i < $num_tokens; ++$i ) {
 940              if ( 0 < $i ) {
 941                  $queries[ $i ] = $queries[ $i - 1 ] . '&';
 942              } else {
 943                  $queries[ $i ] = '';
 944              }
 945  
 946              $query_token    = str_replace( $this->rewritecode, $this->queryreplace, $tokens[0][ $i ] ) . $this->preg_index( $i + 1 );
 947              $queries[ $i ] .= $query_token;
 948          }
 949  
 950          // Get the structure, minus any cruft (stuff that isn't tags) at the front.
 951          $structure = $permalink_structure;
 952          if ( '/' !== $front ) {
 953              $structure = str_replace( $front, '', $structure );
 954          }
 955  
 956          /*
 957           * Create a list of dirs to walk over, making rewrite rules for each level
 958           * so for example, a $structure of /%year%/%monthnum%/%postname% would create
 959           * rewrite rules for /%year%/, /%year%/%monthnum%/ and /%year%/%monthnum%/%postname%
 960           */
 961          $structure = trim( $structure, '/' );
 962          $dirs      = $walk_dirs ? explode( '/', $structure ) : array( $structure );
 963          $num_dirs  = count( $dirs );
 964  
 965          // Strip slashes from the front of $front.
 966          $front = preg_replace( '|^/+|', '', $front );
 967  
 968          // Read the front page ID.
 969          $page_on_front = get_option( 'page_on_front' );
 970  
 971          // The main workhorse loop.
 972          $post_rewrite = array();
 973          $struct       = $front;
 974          for ( $j = 0; $j < $num_dirs; ++$j ) {
 975              // Get the struct for this dir, and trim slashes off the front.
 976              $struct .= $dirs[ $j ] . '/'; // Accumulate. see comment near explode('/', $structure) above.
 977              $struct  = ltrim( $struct, '/' );
 978  
 979              // Replace tags with regexes.
 980              $match = str_replace( $this->rewritecode, $this->rewritereplace, $struct );
 981  
 982              // Make a list of tags, and store how many there are in $num_toks.
 983              $num_toks = preg_match_all( '/%.+?%/', $struct, $toks );
 984  
 985              // Get the 'tagname=$matches[i]'.
 986              $query = ( ! empty( $num_toks ) && isset( $queries[ $num_toks - 1 ] ) ) ? $queries[ $num_toks - 1 ] : '';
 987  
 988              // Set up $ep_mask_specific which is used to match more specific URL types.
 989              switch ( $dirs[ $j ] ) {
 990                  case '%year%':
 991                      $ep_mask_specific = EP_YEAR;
 992                      break;
 993                  case '%monthnum%':
 994                      $ep_mask_specific = EP_MONTH;
 995                      break;
 996                  case '%day%':
 997                      $ep_mask_specific = EP_DAY;
 998                      break;
 999                  default:
1000                      $ep_mask_specific = EP_NONE;
1001              }
1002  
1003              // Create query for /page/xx.
1004              $pagematch = $match . $pageregex;
1005              $pagequery = $index . '?' . $query . '&paged=' . $this->preg_index( $num_toks + 1 );
1006  
1007              // Create query for /comment-page-xx.
1008              $commentmatch = $match . $commentregex;
1009              $commentquery = $index . '?' . $query . '&cpage=' . $this->preg_index( $num_toks + 1 );
1010  
1011              if ( $page_on_front ) {
1012                  // Create query for Root /comment-page-xx.
1013                  $rootcommentmatch = $match . $commentregex;
1014                  $rootcommentquery = $index . '?' . $query . '&page_id=' . $page_on_front . '&cpage=' . $this->preg_index( $num_toks + 1 );
1015              }
1016  
1017              // Create query for /feed/(feed|atom|rss|rss2|rdf).
1018              $feedmatch = $match . $feedregex;
1019              $feedquery = $feedindex . '?' . $query . '&feed=' . $this->preg_index( $num_toks + 1 );
1020  
1021              // Create query for /(feed|atom|rss|rss2|rdf) (see comment near creation of $feedregex).
1022              $feedmatch2 = $match . $feedregex2;
1023              $feedquery2 = $feedindex . '?' . $query . '&feed=' . $this->preg_index( $num_toks + 1 );
1024  
1025              // Create query and regex for embeds.
1026              $embedmatch = $match . $embedregex;
1027              $embedquery = $embedindex . '?' . $query . '&embed=true';
1028  
1029              // If asked to, turn the feed queries into comment feed ones.
1030              if ( $forcomments ) {
1031                  $feedquery  .= '&withcomments=1';
1032                  $feedquery2 .= '&withcomments=1';
1033              }
1034  
1035              // Start creating the array of rewrites for this dir.
1036              $rewrite = array();
1037  
1038              // ...adding on /feed/ regexes => queries.
1039              if ( $feed ) {
1040                  $rewrite = array(
1041                      $feedmatch  => $feedquery,
1042                      $feedmatch2 => $feedquery2,
1043                      $embedmatch => $embedquery,
1044                  );
1045              }
1046  
1047              // ...and /page/xx ones.
1048              if ( $paged ) {
1049                  $rewrite = array_merge( $rewrite, array( $pagematch => $pagequery ) );
1050              }
1051  
1052              // Only on pages with comments add ../comment-page-xx/.
1053              if ( EP_PAGES & $ep_mask || EP_PERMALINK & $ep_mask ) {
1054                  $rewrite = array_merge( $rewrite, array( $commentmatch => $commentquery ) );
1055              } elseif ( EP_ROOT & $ep_mask && $page_on_front ) {
1056                  $rewrite = array_merge( $rewrite, array( $rootcommentmatch => $rootcommentquery ) );
1057              }
1058  
1059              // Do endpoints.
1060              if ( $endpoints ) {
1061                  foreach ( (array) $ep_query_append as $regex => $ep ) {
1062                      // Add the endpoints on if the mask fits.
1063                      if ( $ep[0] & $ep_mask || $ep[0] & $ep_mask_specific ) {
1064                          $rewrite[ $match . $regex ] = $index . '?' . $query . $ep[1] . $this->preg_index( $num_toks + 2 );
1065                      }
1066                  }
1067              }
1068  
1069              // If we've got some tags in this dir.
1070              if ( $num_toks ) {
1071                  $post = false;
1072                  $page = false;
1073  
1074                  /*
1075                   * Check to see if this dir is permalink-level: i.e. the structure specifies an
1076                   * individual post. Do this by checking it contains at least one of 1) post name,
1077                   * 2) post ID, 3) page name, 4) timestamp (year, month, day, hour, second and
1078                   * minute all present). Set these flags now as we need them for the endpoints.
1079                   */
1080                  if ( str_contains( $struct, '%postname%' )
1081                      || str_contains( $struct, '%post_id%' )
1082                      || str_contains( $struct, '%pagename%' )
1083                      || ( str_contains( $struct, '%year%' )
1084                          && str_contains( $struct, '%monthnum%' )
1085                          && str_contains( $struct, '%day%' )
1086                          && str_contains( $struct, '%hour%' )
1087                          && str_contains( $struct, '%minute%' )
1088                          && str_contains( $struct, '%second%' ) )
1089                  ) {
1090                      $post = true;
1091                      if ( str_contains( $struct, '%pagename%' ) ) {
1092                          $page = true;
1093                      }
1094                  }
1095  
1096                  if ( ! $post ) {
1097                      // For custom post types, we need to add on endpoints as well.
1098                      foreach ( get_post_types( array( '_builtin' => false ) ) as $ptype ) {
1099                          if ( str_contains( $struct, "%$ptype%" ) ) {
1100                              $post = true;
1101  
1102                              // This is for page style attachment URLs.
1103                              $page = is_post_type_hierarchical( $ptype );
1104                              break;
1105                          }
1106                      }
1107                  }
1108  
1109                  // If creating rules for a permalink, do all the endpoints like attachments etc.
1110                  if ( $post ) {
1111                      // Create query and regex for trackback.
1112                      $trackbackmatch = $match . $trackbackregex;
1113                      $trackbackquery = $trackbackindex . '?' . $query . '&tb=1';
1114  
1115                      // Create query and regex for embeds.
1116                      $embedmatch = $match . $embedregex;
1117                      $embedquery = $embedindex . '?' . $query . '&embed=true';
1118  
1119                      // Trim slashes from the end of the regex for this dir.
1120                      $match = rtrim( $match, '/' );
1121  
1122                      // Get rid of brackets.
1123                      $submatchbase = str_replace( array( '(', ')' ), '', $match );
1124  
1125                      // Add a rule for at attachments, which take the form of <permalink>/some-text.
1126                      $sub1 = $submatchbase . '/([^/]+)/';
1127  
1128                      // Add trackback regex <permalink>/trackback/...
1129                      $sub1tb = $sub1 . $trackbackregex;
1130  
1131                      // And <permalink>/feed/(atom|...)
1132                      $sub1feed = $sub1 . $feedregex;
1133  
1134                      // And <permalink>/(feed|atom...)
1135                      $sub1feed2 = $sub1 . $feedregex2;
1136  
1137                      // And <permalink>/comment-page-xx
1138                      $sub1comment = $sub1 . $commentregex;
1139  
1140                      // And <permalink>/embed/...
1141                      $sub1embed = $sub1 . $embedregex;
1142  
1143                      /*
1144                       * Add another rule to match attachments in the explicit form:
1145                       * <permalink>/attachment/some-text
1146                       */
1147                      $sub2 = $submatchbase . '/attachment/([^/]+)/';
1148  
1149                      // And add trackbacks <permalink>/attachment/trackback.
1150                      $sub2tb = $sub2 . $trackbackregex;
1151  
1152                      // Feeds, <permalink>/attachment/feed/(atom|...)
1153                      $sub2feed = $sub2 . $feedregex;
1154  
1155                      // And feeds again on to this <permalink>/attachment/(feed|atom...)
1156                      $sub2feed2 = $sub2 . $feedregex2;
1157  
1158                      // And <permalink>/comment-page-xx
1159                      $sub2comment = $sub2 . $commentregex;
1160  
1161                      // And <permalink>/embed/...
1162                      $sub2embed = $sub2 . $embedregex;
1163  
1164                      // Create queries for these extra tag-ons we've just dealt with.
1165                      $subquery        = $index . '?attachment=' . $this->preg_index( 1 );
1166                      $subtbquery      = $subquery . '&tb=1';
1167                      $subfeedquery    = $subquery . '&feed=' . $this->preg_index( 2 );
1168                      $subcommentquery = $subquery . '&cpage=' . $this->preg_index( 2 );
1169                      $subembedquery   = $subquery . '&embed=true';
1170  
1171                      // Do endpoints for attachments.
1172                      if ( ! empty( $endpoints ) ) {
1173                          foreach ( (array) $ep_query_append as $regex => $ep ) {
1174                              if ( $ep[0] & EP_ATTACHMENT ) {
1175                                  $rewrite[ $sub1 . $regex ] = $subquery . $ep[1] . $this->preg_index( 3 );
1176                                  $rewrite[ $sub2 . $regex ] = $subquery . $ep[1] . $this->preg_index( 3 );
1177                              }
1178                          }
1179                      }
1180  
1181                      /*
1182                       * Now we've finished with endpoints, finish off the $sub1 and $sub2 matches
1183                       * add a ? as we don't have to match that last slash, and finally a $ so we
1184                       * match to the end of the URL
1185                       */
1186                      $sub1 .= '?$';
1187                      $sub2 .= '?$';
1188  
1189                      /*
1190                       * Post pagination, e.g. <permalink>/2/
1191                       * Previously: '(/[0-9]+)?/?$', which produced '/2' for page.
1192                       * When cast to int, returned 0.
1193                       */
1194                      $match = $match . '(?:/([0-9]+))?/?$';
1195                      $query = $index . '?' . $query . '&page=' . $this->preg_index( $num_toks + 1 );
1196  
1197                      // Not matching a permalink so this is a lot simpler.
1198                  } else {
1199                      // Close the match and finalize the query.
1200                      $match .= '?$';
1201                      $query  = $index . '?' . $query;
1202                  }
1203  
1204                  /*
1205                   * Create the final array for this dir by joining the $rewrite array (which currently
1206                   * only contains rules/queries for trackback, pages etc) to the main regex/query for
1207                   * this dir
1208                   */
1209                  $rewrite = array_merge( $rewrite, array( $match => $query ) );
1210  
1211                  // If we're matching a permalink, add those extras (attachments etc) on.
1212                  if ( $post ) {
1213                      // Add trackback.
1214                      $rewrite = array_merge( array( $trackbackmatch => $trackbackquery ), $rewrite );
1215  
1216                      // Add embed.
1217                      $rewrite = array_merge( array( $embedmatch => $embedquery ), $rewrite );
1218  
1219                      // Add regexes/queries for attachments, attachment trackbacks and so on.
1220                      if ( ! $page ) {
1221                          // Require <permalink>/attachment/stuff form for pages because of confusion with subpages.
1222                          $rewrite = array_merge(
1223                              $rewrite,
1224                              array(
1225                                  $sub1        => $subquery,
1226                                  $sub1tb      => $subtbquery,
1227                                  $sub1feed    => $subfeedquery,
1228                                  $sub1feed2   => $subfeedquery,
1229                                  $sub1comment => $subcommentquery,
1230                                  $sub1embed   => $subembedquery,
1231                              )
1232                          );
1233                      }
1234  
1235                      $rewrite = array_merge(
1236                          array(
1237                              $sub2        => $subquery,
1238                              $sub2tb      => $subtbquery,
1239                              $sub2feed    => $subfeedquery,
1240                              $sub2feed2   => $subfeedquery,
1241                              $sub2comment => $subcommentquery,
1242                              $sub2embed   => $subembedquery,
1243                          ),
1244                          $rewrite
1245                      );
1246                  }
1247              }
1248              // Add the rules for this dir to the accumulating $post_rewrite.
1249              $post_rewrite = array_merge( $rewrite, $post_rewrite );
1250          }
1251  
1252          // The finished rules. phew!
1253          return $post_rewrite;
1254      }
1255  
1256      /**
1257       * Generates rewrite rules with permalink structure and walking directory only.
1258       *
1259       * Shorten version of WP_Rewrite::generate_rewrite_rules() that allows for shorter
1260       * list of parameters. See the method for longer description of what generating
1261       * rewrite rules does.
1262       *
1263       * @since 1.5.0
1264       *
1265       * @see WP_Rewrite::generate_rewrite_rules() See for long description and rest of parameters.
1266       *
1267       * @param string $permalink_structure The permalink structure to generate rules.
1268       * @param bool   $walk_dirs           Optional. Whether to create list of directories to walk over.
1269       *                                    Default false.
1270       * @return array<string, string> An array of rewrite rules keyed by their regex pattern.
1271       *
1272       * @phpstan-return array<non-falsy-string, non-falsy-string>
1273       */
1274  	public function generate_rewrite_rule( $permalink_structure, $walk_dirs = false ) {
1275          return $this->generate_rewrite_rules( $permalink_structure, EP_NONE, false, false, false, $walk_dirs );
1276      }
1277  
1278      /**
1279       * Constructs rewrite matches and queries from permalink structure.
1280       *
1281       * Runs the action {@see 'generate_rewrite_rules'} with the parameter that is an
1282       * reference to the current WP_Rewrite instance to further manipulate the
1283       * permalink structures and rewrite rules. Runs the {@see 'rewrite_rules_array'}
1284       * filter on the full rewrite rule array.
1285       *
1286       * There are two ways to manipulate the rewrite rules, one by hooking into
1287       * the {@see 'generate_rewrite_rules'} action and gaining full control of the
1288       * object or just manipulating the rewrite rule array before it is passed
1289       * from the function.
1290       *
1291       * @since 1.5.0
1292       *
1293       * @return array<string, string> An associative array of matches and queries.
1294       */
1295  	public function rewrite_rules() {
1296          $rewrite = array();
1297  
1298          if ( empty( $this->permalink_structure ) ) {
1299              return $rewrite;
1300          }
1301  
1302          // robots.txt -- only if installed at the root.
1303          $home_path      = parse_url( home_url() );
1304          $robots_rewrite = ( empty( $home_path['path'] ) || '/' === $home_path['path'] ) ? array( 'robots\.txt$' => $this->index . '?robots=1' ) : array();
1305  
1306          // favicon.ico -- only if installed at the root.
1307          $favicon_rewrite = ( empty( $home_path['path'] ) || '/' === $home_path['path'] ) ? array( 'favicon\.ico$' => $this->index . '?favicon=1' ) : array();
1308  
1309          // sitemap.xml -- only if installed at the root.
1310          $sitemap_rewrite = ( empty( $home_path['path'] ) || '/' === $home_path['path'] ) ? array( 'sitemap\.xml' => $this->index . '?sitemap=index' ) : array();
1311  
1312          // Old feed and service files.
1313          $deprecated_files = array(
1314              '.*wp-(atom|rdf|rss|rss2|feed|commentsrss2)\.php$' => $this->index . '?feed=old',
1315              '.*wp-app\.php(/.*)?$' => $this->index . '?error=403',
1316          );
1317  
1318          // Registration rules.
1319          $registration_pages = array();
1320          if ( is_multisite() && is_main_site() ) {
1321              $registration_pages['.*wp-signup.php$']   = $this->index . '?signup=true';
1322              $registration_pages['.*wp-activate.php$'] = $this->index . '?activate=true';
1323          }
1324  
1325          // Deprecated.
1326          $registration_pages['.*wp-register.php$'] = $this->index . '?register=true';
1327  
1328          // Post rewrite rules.
1329          $post_rewrite = $this->generate_rewrite_rules( $this->permalink_structure, EP_PERMALINK );
1330  
1331          /**
1332           * Filters rewrite rules used for "post" archives.
1333           *
1334           * @since 1.5.0
1335           *
1336           * @param string[] $post_rewrite Array of rewrite rules for posts, keyed by their regex pattern.
1337           */
1338          $post_rewrite = apply_filters( 'post_rewrite_rules', $post_rewrite );
1339  
1340          // Date rewrite rules.
1341          $date_rewrite = $this->generate_rewrite_rules( $this->get_date_permastruct(), EP_DATE );
1342  
1343          /**
1344           * Filters rewrite rules used for date archives.
1345           *
1346           * Likely date archives would include `/yyyy/`, `/yyyy/mm/`, and `/yyyy/mm/dd/`.
1347           *
1348           * @since 1.5.0
1349           *
1350           * @param string[] $date_rewrite Array of rewrite rules for date archives, keyed by their regex pattern.
1351           */
1352          $date_rewrite = apply_filters( 'date_rewrite_rules', $date_rewrite );
1353  
1354          // Root-level rewrite rules.
1355          $root_rewrite = $this->generate_rewrite_rules( $this->root . '/', EP_ROOT );
1356  
1357          /**
1358           * Filters rewrite rules used for root-level archives.
1359           *
1360           * Likely root-level archives would include pagination rules for the homepage
1361           * as well as site-wide post feeds (e.g. `/feed/`, and `/feed/atom/`).
1362           *
1363           * @since 1.5.0
1364           *
1365           * @param string[] $root_rewrite Array of root-level rewrite rules, keyed by their regex pattern.
1366           */
1367          $root_rewrite = apply_filters( 'root_rewrite_rules', $root_rewrite );
1368  
1369          // Comments rewrite rules.
1370          $comments_rewrite = $this->generate_rewrite_rules( $this->root . $this->comments_base, EP_COMMENTS, false, true, true, false );
1371  
1372          /**
1373           * Filters rewrite rules used for comment feed archives.
1374           *
1375           * Likely comments feed archives include `/comments/feed/` and `/comments/feed/atom/`.
1376           *
1377           * @since 1.5.0
1378           *
1379           * @param string[] $comments_rewrite Array of rewrite rules for the site-wide comments feeds, keyed by their regex pattern.
1380           */
1381          $comments_rewrite = apply_filters( 'comments_rewrite_rules', $comments_rewrite );
1382  
1383          // Search rewrite rules.
1384          $search_structure = $this->get_search_permastruct();
1385          $search_rewrite   = $this->generate_rewrite_rules( $search_structure, EP_SEARCH );
1386  
1387          /**
1388           * Filters rewrite rules used for search archives.
1389           *
1390           * Likely search-related archives include `/search/search+query/` as well as
1391           * pagination and feed paths for a search.
1392           *
1393           * @since 1.5.0
1394           *
1395           * @param string[] $search_rewrite Array of rewrite rules for search queries, keyed by their regex pattern.
1396           */
1397          $search_rewrite = apply_filters( 'search_rewrite_rules', $search_rewrite );
1398  
1399          // Author rewrite rules.
1400          $author_rewrite = $this->generate_rewrite_rules( $this->get_author_permastruct(), EP_AUTHORS );
1401  
1402          /**
1403           * Filters rewrite rules used for author archives.
1404           *
1405           * Likely author archives would include `/author/author-name/`, as well as
1406           * pagination and feed paths for author archives.
1407           *
1408           * @since 1.5.0
1409           *
1410           * @param string[] $author_rewrite Array of rewrite rules for author archives, keyed by their regex pattern.
1411           */
1412          $author_rewrite = apply_filters( 'author_rewrite_rules', $author_rewrite );
1413  
1414          // Pages rewrite rules.
1415          $page_rewrite = $this->page_rewrite_rules();
1416  
1417          /**
1418           * Filters rewrite rules used for "page" post type archives.
1419           *
1420           * @since 1.5.0
1421           *
1422           * @param string[] $page_rewrite Array of rewrite rules for the "page" post type, keyed by their regex pattern.
1423           */
1424          $page_rewrite = apply_filters( 'page_rewrite_rules', $page_rewrite );
1425  
1426          // Random content rewrite rules.
1427          $random_rewrite = $this->random_rewrite_rules();
1428  
1429          /**
1430           * Filters rewrite rules used for random content redirects.
1431           *
1432           * Likely random content rules would include `/random/`, as well as
1433           * term, author and post type archives prefixed with the random base,
1434           * for example `/random/category/news/`.
1435           *
1436           * @since 7.2.0
1437           *
1438           * @param array<string, string> $random_rewrite Array of rewrite rules for random content redirects, keyed by their regex pattern.
1439           */
1440          $random_rewrite = apply_filters( 'random_rewrite_rules', $random_rewrite );
1441  
1442          // Extra permastructs.
1443          foreach ( $this->extra_permastructs as $permastructname => $struct ) {
1444              if ( is_array( $struct ) ) {
1445                  if ( count( $struct ) === 2 ) {
1446                      $rules = $this->generate_rewrite_rules( $struct[0], $struct[1] );
1447                  } else {
1448                      $rules = $this->generate_rewrite_rules( $struct['struct'], $struct['ep_mask'], $struct['paged'], $struct['feed'], $struct['forcomments'], $struct['walk_dirs'], $struct['endpoints'] );
1449                  }
1450              } else {
1451                  $rules = $this->generate_rewrite_rules( $struct );
1452              }
1453  
1454              /**
1455               * Filters rewrite rules used for individual permastructs.
1456               *
1457               * The dynamic portion of the hook name, `$permastructname`, refers
1458               * to the name of the registered permastruct.
1459               *
1460               * Possible hook names include:
1461               *
1462               *  - `category_rewrite_rules`
1463               *  - `post_format_rewrite_rules`
1464               *  - `post_tag_rewrite_rules`
1465               *
1466               * @since 3.1.0
1467               *
1468               * @param string[] $rules Array of rewrite rules generated for the current permastruct, keyed by their regex pattern.
1469               */
1470              $rules = apply_filters( "{$permastructname}_rewrite_rules", $rules );
1471  
1472              if ( 'post_tag' === $permastructname ) {
1473  
1474                  /**
1475                   * Filters rewrite rules used specifically for Tags.
1476                   *
1477                   * @since 2.3.0
1478                   * @deprecated 3.1.0 Use {@see 'post_tag_rewrite_rules'} instead.
1479                   *
1480                   * @param string[] $rules Array of rewrite rules generated for tags, keyed by their regex pattern.
1481                   */
1482                  $rules = apply_filters_deprecated( 'tag_rewrite_rules', array( $rules ), '3.1.0', 'post_tag_rewrite_rules' );
1483              }
1484  
1485              $this->extra_rules_top = array_merge( $this->extra_rules_top, $rules );
1486          }
1487  
1488          // Put them together.
1489          if ( $this->use_verbose_page_rules ) {
1490              $this->rules = array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $sitemap_rewrite, $deprecated_files, $registration_pages, $random_rewrite, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite, $page_rewrite, $post_rewrite, $this->extra_rules );
1491          } else {
1492              $this->rules = array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $sitemap_rewrite, $deprecated_files, $registration_pages, $random_rewrite, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite, $post_rewrite, $page_rewrite, $this->extra_rules );
1493          }
1494  
1495          /**
1496           * Fires after the rewrite rules are generated.
1497           *
1498           * @since 1.5.0
1499           *
1500           * @param WP_Rewrite $wp_rewrite Current WP_Rewrite instance (passed by reference).
1501           */
1502          do_action_ref_array( 'generate_rewrite_rules', array( &$this ) );
1503  
1504          /**
1505           * Filters the full set of generated rewrite rules.
1506           *
1507           * @since 1.5.0
1508           *
1509           * @param array<string, string> $rules The compiled array of rewrite rules, keyed by their regex pattern.
1510           */
1511          $this->rules = apply_filters( 'rewrite_rules_array', $this->rules );
1512  
1513          return $this->rules;
1514      }
1515  
1516      /**
1517       * Retrieves the rewrite rules for random content redirects.
1518       *
1519       * Random content URLs are an existing archive path prefixed with the random base.
1520       * A request to one redirects to a randomly selected post from that archive:
1521       *
1522       * - `/random/` for any post.
1523       * - `/random/{taxonomy-slug}/{term}/` for a post in a term archive, for example `/random/category/news/`.
1524       * - `/random/{author-base}/{author}/` for a post by an author.
1525       * - `/random/{post-type-slug}/` for a post of a post type, using its archive slug if it
1526       *   has one, otherwise its rewrite slug.
1527       *
1528       * Only post types registered as `randomable`, and taxonomies used by them, receive rules.
1529       *
1530       * If a published page exists at the random base, the page takes precedence and the
1531       * `/random/` rule is omitted. The same applies to a published post when posts share
1532       * the URL, see {@see WP_Rewrite::post_permalinks_share_random_base()}. The rules are flushed
1533       * when such a page or post changes, see {@see wp_random_content_flush_rewrite_rules_for_post()}.
1534       *
1535       * The `random` query variable can be added to any other archive or search URL, for example
1536       * `/2026/?random`, to request a random post from that archive.
1537       *
1538       * @since 7.2.0
1539       *
1540       * @see wp_is_random_content_redirect_enabled()
1541       *
1542       * @return array<string, string> Array of rewrite rules for random content redirects, keyed by their regex pattern.
1543       *
1544       * @phpstan-return array<non-falsy-string, non-falsy-string>
1545       */
1546  	public function random_rewrite_rules(): array {
1547          if ( ! wp_is_random_content_redirect_enabled() ) {
1548              return array();
1549          }
1550  
1551          $random_slug = $this->get_random_base();
1552          $random_base = $this->root . $random_slug;
1553  
1554          $rules = array();
1555  
1556          // A published page or post at the random base, for example example.com/random/, takes precedence.
1557          $random_base_posts = array( get_page_by_path( $random_slug ) );
1558          if ( $this->post_permalinks_share_random_base() ) {
1559              $random_base_posts[] = get_page_by_path( $random_slug, OBJECT, 'post' );
1560          }
1561  
1562          $random_base_is_taken = array_any(
1563              $random_base_posts,
1564              fn( $random_base_post ) => $random_base_post instanceof WP_Post && is_post_publicly_viewable( $random_base_post )
1565          );
1566  
1567          if ( ! $random_base_is_taken ) {
1568              $rules[ $random_base . '/?$' ] = $this->index . '?random=1';
1569          }
1570  
1571          $archive_structures = array(
1572              "{$random_base}/{$this->author_base}/%author%",
1573          );
1574  
1575          $randomable_post_types = get_post_types( array( 'randomable' => true ) );
1576  
1577          foreach ( get_taxonomies( array( 'publicly_queryable' => true ), 'objects' ) as $taxonomy ) {
1578              $taxonomy_slug = is_array( $taxonomy->rewrite ) && isset( $taxonomy->rewrite['slug'] ) && is_string( $taxonomy->rewrite['slug'] ) ? $taxonomy->rewrite['slug'] : '';
1579  
1580              // Only taxonomies with pretty permalinks have a registered rewrite tag.
1581              if ( '' === $taxonomy_slug || ! isset( $this->extra_permastructs[ $taxonomy->name ] ) ) {
1582                  continue;
1583              }
1584  
1585              // Only taxonomies used by randomable post types can contain random content.
1586              if ( ! array_intersect( $taxonomy->object_type, $randomable_post_types ) ) {
1587                  continue;
1588              }
1589  
1590              $archive_structures[] = "{$random_base}/{$taxonomy_slug}/%{$taxonomy->name}%";
1591          }
1592  
1593          foreach ( $archive_structures as $archive_structure ) {
1594              $archive_rules = $this->generate_rewrite_rules( $archive_structure, EP_NONE, false, false, false, false, false );
1595  
1596              foreach ( $archive_rules as $regex => $query ) {
1597                  $rules[ $regex ] = $query . '&random=1';
1598              }
1599          }
1600  
1601          foreach ( get_post_types( array( 'randomable' => true ), 'objects' ) as $post_type ) {
1602              // Post types without rewrites have no pretty permalinks, see {@see WP_Post_Type::add_rewrite_rules()}.
1603              if ( ! is_array( $post_type->rewrite ) ) {
1604                  continue;
1605              }
1606  
1607              // Try using `has_archive` if it's a string, then try the rewrite slug, otherwise it shouldn't have gotten here, so just skip the post type.
1608              if ( is_string( $post_type->has_archive ) ) {
1609                  $post_type_slug = $post_type->has_archive;
1610              } elseif ( isset( $post_type->rewrite['slug'] ) && is_string( $post_type->rewrite['slug'] ) ) {
1611                  $post_type_slug = $post_type->rewrite['slug'];
1612              } else {
1613                  continue;
1614              }
1615  
1616              $rules[ "{$random_base}/{$post_type_slug}/?$" ] = $this->index . "?post_type={$post_type->name}&random=1";
1617          }
1618  
1619          return $rules;
1620      }
1621  
1622      /**
1623       * Retrieves the rewrite rules.
1624       *
1625       * The difference between this method and WP_Rewrite::rewrite_rules() is that
1626       * this method stores the rewrite rules in the 'rewrite_rules' option and retrieves
1627       * it. This prevents having to process all of the permalinks to get the rewrite rules
1628       * in the form of caching.
1629       *
1630       * @since 1.5.0
1631       *
1632       * @return string[] Array of rewrite rules keyed by their regex pattern.
1633       */
1634  	public function wp_rewrite_rules() {
1635          $this->rules = get_option( 'rewrite_rules' );
1636          if ( empty( $this->rules ) ) {
1637              $this->refresh_rewrite_rules();
1638          }
1639  
1640          return $this->rules;
1641      }
1642  
1643      /**
1644       * Refreshes the rewrite rules, saving the fresh value to the database.
1645       *
1646       * If the {@see 'wp_loaded'} action has not occurred yet, will postpone saving to the database.
1647       *
1648       * @since 6.4.0
1649       */
1650  	private function refresh_rewrite_rules() {
1651          $this->rules   = '';
1652          $this->matches = 'matches';
1653  
1654          $this->rewrite_rules();
1655  
1656          if ( ! did_action( 'wp_loaded' ) ) {
1657              /*
1658               * It is not safe to save the results right now, as the rules may be partial.
1659               * Need to give all rules the chance to register.
1660               */
1661              add_action( 'wp_loaded', array( $this, 'flush_rules' ) );
1662          } else {
1663              update_option( 'rewrite_rules', $this->rules );
1664          }
1665      }
1666  
1667      /**
1668       * Retrieves mod_rewrite-formatted rewrite rules to write to .htaccess.
1669       *
1670       * Does not actually write to the .htaccess file, but creates the rules for
1671       * the process that will.
1672       *
1673       * Will add the non_wp_rules property rules to the .htaccess file before
1674       * the WordPress rewrite rules one.
1675       *
1676       * @since 1.5.0
1677       *
1678       * @return string
1679       */
1680  	public function mod_rewrite_rules() {
1681          if ( ! $this->using_permalinks() ) {
1682              return '';
1683          }
1684  
1685          $site_root = parse_url( site_url() );
1686          if ( isset( $site_root['path'] ) ) {
1687              $site_root = trailingslashit( $site_root['path'] );
1688          }
1689  
1690          $home_root = parse_url( home_url() );
1691          if ( isset( $home_root['path'] ) ) {
1692              $home_root = trailingslashit( $home_root['path'] );
1693          } else {
1694              $home_root = '/';
1695          }
1696  
1697          $rules  = "<IfModule mod_rewrite.c>\n";
1698          $rules .= "RewriteEngine On\n";
1699          $rules .= "RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\n";
1700          $rules .= "RewriteBase $home_root\n";
1701  
1702          // Prevent -f checks on index.php.
1703          $rules .= "RewriteRule ^index\.php$ - [L]\n";
1704  
1705          // Add in the rules that don't redirect to WP's index.php (and thus shouldn't be handled by WP at all).
1706          foreach ( (array) $this->non_wp_rules as $match => $query ) {
1707              // Apache 1.3 does not support the reluctant (non-greedy) modifier.
1708              $match = str_replace( '.+?', '.+', $match );
1709  
1710              $rules .= 'RewriteRule ^' . $match . ' ' . $home_root . $query . " [QSA,L]\n";
1711          }
1712  
1713          if ( $this->use_verbose_rules ) {
1714              $this->matches = '';
1715              $rewrite       = $this->rewrite_rules();
1716              $num_rules     = count( $rewrite );
1717              $rules        .= "RewriteCond %{REQUEST_FILENAME} -f [OR]\n" .
1718                  "RewriteCond %{REQUEST_FILENAME} -d\n" .
1719                  "RewriteRule ^.*$ - [S=$num_rules]\n";
1720  
1721              foreach ( (array) $rewrite as $match => $query ) {
1722                  // Apache 1.3 does not support the reluctant (non-greedy) modifier.
1723                  $match = str_replace( '.+?', '.+', $match );
1724  
1725                  if ( str_contains( $query, $this->index ) ) {
1726                      $rules .= 'RewriteRule ^' . $match . ' ' . $home_root . $query . " [QSA,L]\n";
1727                  } else {
1728                      $rules .= 'RewriteRule ^' . $match . ' ' . $site_root . $query . " [QSA,L]\n";
1729                  }
1730              }
1731          } else {
1732              $rules .= "RewriteCond %{REQUEST_FILENAME} !-f\n" .
1733                  "RewriteCond %{REQUEST_FILENAME} !-d\n" .
1734                  "RewriteRule . {$home_root}{$this->index} [L]\n";
1735          }
1736  
1737          $rules .= "</IfModule>\n";
1738  
1739          /**
1740           * Filters the list of rewrite rules formatted for output to an .htaccess file.
1741           *
1742           * @since 1.5.0
1743           *
1744           * @param string $rules mod_rewrite Rewrite rules formatted for .htaccess.
1745           */
1746          $rules = apply_filters( 'mod_rewrite_rules', $rules );
1747  
1748          /**
1749           * Filters the list of rewrite rules formatted for output to an .htaccess file.
1750           *
1751           * @since 1.5.0
1752           * @deprecated 1.5.0 Use the {@see 'mod_rewrite_rules'} filter instead.
1753           *
1754           * @param string $rules mod_rewrite Rewrite rules formatted for .htaccess.
1755           */
1756          return apply_filters_deprecated( 'rewrite_rules', array( $rules ), '1.5.0', 'mod_rewrite_rules' );
1757      }
1758  
1759      /**
1760       * Retrieves IIS7 URL Rewrite formatted rewrite rules to write to web.config file.
1761       *
1762       * Does not actually write to the web.config file, but creates the rules for
1763       * the process that will.
1764       *
1765       * @since 2.8.0
1766       *
1767       * @param bool $add_parent_tags Optional. Whether to add parent tags to the rewrite rule sets.
1768       *                              Default false.
1769       * @return string IIS7 URL rewrite rule sets.
1770       */
1771  	public function iis7_url_rewrite_rules( $add_parent_tags = false ) {
1772          if ( ! $this->using_permalinks() ) {
1773              return '';
1774          }
1775          $rules = '';
1776          if ( $add_parent_tags ) {
1777              $rules .= '<configuration>
1778      <system.webServer>
1779          <rewrite>
1780              <rules>';
1781          }
1782  
1783          $rules .= '
1784              <rule name="WordPress: ' . esc_attr( home_url() ) . '" patternSyntax="Wildcard">
1785                  <match url="*" />
1786                      <conditions>
1787                          <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
1788                          <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
1789                      </conditions>
1790                  <action type="Rewrite" url="index.php" />
1791              </rule>';
1792  
1793          if ( $add_parent_tags ) {
1794              $rules .= '
1795              </rules>
1796          </rewrite>
1797      </system.webServer>
1798  </configuration>';
1799          }
1800  
1801          /**
1802           * Filters the list of rewrite rules formatted for output to a web.config.
1803           *
1804           * @since 2.8.0
1805           *
1806           * @param string $rules Rewrite rules formatted for IIS web.config.
1807           */
1808          return apply_filters( 'iis7_url_rewrite_rules', $rules );
1809      }
1810  
1811      /**
1812       * Adds a rewrite rule that transforms a URL structure to a set of query vars.
1813       *
1814       * Any value in the $after parameter that isn't 'bottom' will result in the rule
1815       * being placed at the top of the rewrite rules.
1816       *
1817       * @since 2.1.0
1818       * @since 4.4.0 Array support was added to the `$query` parameter.
1819       *
1820       * @param string       $regex Regular expression to match request against.
1821       * @param string|array $query The corresponding query vars for this rewrite rule.
1822       * @param string       $after Optional. Priority of the new rule. Accepts 'top'
1823       *                            or 'bottom'. Default 'bottom'.
1824       */
1825  	public function add_rule( $regex, $query, $after = 'bottom' ) {
1826          if ( is_array( $query ) ) {
1827              $external = false;
1828              $query    = add_query_arg( $query, 'index.php' );
1829          } else {
1830              $index = ! str_contains( $query, '?' ) ? strlen( $query ) : strpos( $query, '?' );
1831              $front = substr( $query, 0, $index );
1832  
1833              $external = $front !== $this->index;
1834          }
1835  
1836          // "external" = it doesn't correspond to index.php.
1837          if ( $external ) {
1838              $this->add_external_rule( $regex, $query );
1839          } else {
1840              if ( 'bottom' === $after ) {
1841                  $this->extra_rules = array_merge( $this->extra_rules, array( $regex => $query ) );
1842              } else {
1843                  $this->extra_rules_top = array_merge( $this->extra_rules_top, array( $regex => $query ) );
1844              }
1845          }
1846      }
1847  
1848      /**
1849       * Adds a rewrite rule that doesn't correspond to index.php.
1850       *
1851       * @since 2.1.0
1852       *
1853       * @param string $regex Regular expression to match request against.
1854       * @param string $query The corresponding query vars for this rewrite rule.
1855       */
1856  	public function add_external_rule( $regex, $query ) {
1857          $this->non_wp_rules[ $regex ] = $query;
1858      }
1859  
1860      /**
1861       * Adds an endpoint, like /trackback/.
1862       *
1863       * @since 2.1.0
1864       * @since 3.9.0 $query_var parameter added.
1865       * @since 4.3.0 Added support for skipping query var registration by passing `false` to `$query_var`.
1866       *
1867       * @see add_rewrite_endpoint() for full documentation.
1868       * @global WP $wp Current WordPress environment instance.
1869       *
1870       * @param string      $name      Name of the endpoint.
1871       * @param int         $places    Endpoint mask describing the places the endpoint should be added.
1872       *                               Accepts a mask of:
1873       *                               - `EP_ALL`
1874       *                               - `EP_NONE`
1875       *                               - `EP_ALL_ARCHIVES`
1876       *                               - `EP_ATTACHMENT`
1877       *                               - `EP_AUTHORS`
1878       *                               - `EP_CATEGORIES`
1879       *                               - `EP_COMMENTS`
1880       *                               - `EP_DATE`
1881       *                               - `EP_DAY`
1882       *                               - `EP_MONTH`
1883       *                               - `EP_PAGES`
1884       *                               - `EP_PERMALINK`
1885       *                               - `EP_ROOT`
1886       *                               - `EP_SEARCH`
1887       *                               - `EP_TAGS`
1888       *                               - `EP_YEAR`
1889       * @param string|bool $query_var Optional. Name of the corresponding query variable. Pass `false` to
1890       *                               skip registering a query_var for this endpoint. Defaults to the
1891       *                               value of `$name`.
1892       */
1893  	public function add_endpoint( $name, $places, $query_var = true ) {
1894          global $wp;
1895  
1896          // For backward compatibility, if null has explicitly been passed as `$query_var`, assume `true`.
1897          if ( true === $query_var || null === $query_var ) {
1898              $query_var = $name;
1899          }
1900          $this->endpoints[] = array( $places, $name, $query_var );
1901  
1902          if ( $query_var ) {
1903              $wp->add_query_var( $query_var );
1904          }
1905      }
1906  
1907      /**
1908       * Adds a new permalink structure.
1909       *
1910       * A permalink structure (permastruct) is an abstract definition of a set of rewrite rules;
1911       * it is an easy way of expressing a set of regular expressions that rewrite to a set of
1912       * query strings. The new permastruct is added to the WP_Rewrite::$extra_permastructs array.
1913       *
1914       * When the rewrite rules are built by WP_Rewrite::rewrite_rules(), all of these extra
1915       * permastructs are passed to WP_Rewrite::generate_rewrite_rules() which transforms them
1916       * into the regular expressions that many love to hate.
1917       *
1918       * The `$args` parameter gives you control over how WP_Rewrite::generate_rewrite_rules()
1919       * works on the new permastruct.
1920       *
1921       * @since 2.5.0
1922       *
1923       * @param string $name   Name for permalink structure.
1924       * @param string $struct Permalink structure (e.g. category/%category%)
1925       * @param array  $args   {
1926       *     Optional. Arguments for building rewrite rules based on the permalink structure.
1927       *     Default empty array.
1928       *
1929       *     @type bool $with_front  Whether the structure should be prepended with `WP_Rewrite::$front`.
1930       *                             Default true.
1931       *     @type int  $ep_mask     The endpoint mask defining which endpoints are added to the structure.
1932       *                             Accepts a mask of:
1933       *                             - `EP_ALL`
1934       *                             - `EP_NONE`
1935       *                             - `EP_ALL_ARCHIVES`
1936       *                             - `EP_ATTACHMENT`
1937       *                             - `EP_AUTHORS`
1938       *                             - `EP_CATEGORIES`
1939       *                             - `EP_COMMENTS`
1940       *                             - `EP_DATE`
1941       *                             - `EP_DAY`
1942       *                             - `EP_MONTH`
1943       *                             - `EP_PAGES`
1944       *                             - `EP_PERMALINK`
1945       *                             - `EP_ROOT`
1946       *                             - `EP_SEARCH`
1947       *                             - `EP_TAGS`
1948       *                             - `EP_YEAR`
1949       *                             Default `EP_NONE`.
1950       *     @type bool $paged       Whether archive pagination rules should be added for the structure.
1951       *                             Default true.
1952       *     @type bool $feed        Whether feed rewrite rules should be added for the structure. Default true.
1953       *     @type bool $forcomments Whether the feed rules should be a query for a comments feed. Default false.
1954       *     @type bool $walk_dirs   Whether the 'directories' making up the structure should be walked over
1955       *                             and rewrite rules built for each in-turn. Default true.
1956       *     @type bool $endpoints   Whether endpoints should be applied to the generated rules. Default true.
1957       * }
1958       */
1959  	public function add_permastruct( $name, $struct, $args = array() ) {
1960          // Back-compat for the old parameters: $with_front and $ep_mask.
1961          if ( ! is_array( $args ) ) {
1962              $args = array( 'with_front' => $args );
1963          }
1964  
1965          if ( func_num_args() === 4 ) {
1966              $args['ep_mask'] = func_get_arg( 3 );
1967          }
1968  
1969          $defaults = array(
1970              'with_front'  => true,
1971              'ep_mask'     => EP_NONE,
1972              'paged'       => true,
1973              'feed'        => true,
1974              'forcomments' => false,
1975              'walk_dirs'   => true,
1976              'endpoints'   => true,
1977          );
1978  
1979          $args = array_intersect_key( $args, $defaults );
1980          $args = wp_parse_args( $args, $defaults );
1981  
1982          if ( $args['with_front'] ) {
1983              $struct = $this->front . $struct;
1984          } else {
1985              $struct = $this->root . $struct;
1986          }
1987  
1988          $args['struct'] = $struct;
1989  
1990          $this->extra_permastructs[ $name ] = $args;
1991      }
1992  
1993      /**
1994       * Removes a permalink structure.
1995       *
1996       * @since 4.5.0
1997       *
1998       * @param string $name Name for permalink structure.
1999       */
2000  	public function remove_permastruct( $name ) {
2001          unset( $this->extra_permastructs[ $name ] );
2002      }
2003  
2004      /**
2005       * Removes rewrite rules and then recreate rewrite rules.
2006       *
2007       * Calls WP_Rewrite::wp_rewrite_rules() after removing the 'rewrite_rules' option.
2008       * If the function named 'save_mod_rewrite_rules' exists, it will be called.
2009       *
2010       * @since 2.0.1
2011       *
2012       * @param bool $hard Whether to update .htaccess (hard flush) or just update rewrite_rules option (soft flush). Default is true (hard).
2013       */
2014  	public function flush_rules( $hard = true ) {
2015          static $do_hard_later = null;
2016  
2017          // Prevent this action from running before everyone has registered their rewrites.
2018          if ( ! did_action( 'wp_loaded' ) ) {
2019              add_action( 'wp_loaded', array( $this, 'flush_rules' ) );
2020              $do_hard_later = ( isset( $do_hard_later ) ) ? $do_hard_later || $hard : $hard;
2021              return;
2022          }
2023  
2024          if ( isset( $do_hard_later ) ) {
2025              $hard = $do_hard_later;
2026              unset( $do_hard_later );
2027          }
2028  
2029          $this->refresh_rewrite_rules();
2030  
2031          /**
2032           * Filters whether a "hard" rewrite rule flush should be performed when requested.
2033           *
2034           * A "hard" flush updates .htaccess (Apache) or web.config (IIS).
2035           *
2036           * @since 3.7.0
2037           *
2038           * @param bool $hard Whether to flush rewrite rules "hard". Default true.
2039           */
2040          if ( ! $hard || ! apply_filters( 'flush_rewrite_rules_hard', true ) ) {
2041              return;
2042          }
2043          if ( function_exists( 'save_mod_rewrite_rules' ) ) {
2044              save_mod_rewrite_rules();
2045          }
2046          if ( function_exists( 'iis7_save_url_rewrite_rules' ) ) {
2047              iis7_save_url_rewrite_rules();
2048          }
2049      }
2050  
2051      /**
2052       * Sets up the object's properties.
2053       *
2054       * The 'use_verbose_page_rules' object property will be set to true if the
2055       * permalink structure begins with one of the following: '%postname%', '%category%',
2056       * '%tag%', or '%author%'.
2057       *
2058       * @since 1.5.0
2059       */
2060  	public function init() {
2061          $this->extra_rules         = array();
2062          $this->non_wp_rules        = array();
2063          $this->endpoints           = array();
2064          $this->permalink_structure = get_option( 'permalink_structure' );
2065          $this->front               = substr( $this->permalink_structure, 0, strpos( $this->permalink_structure, '%' ) );
2066          $this->root                = '';
2067  
2068          if ( $this->using_index_permalinks() ) {
2069              $this->root = $this->index . '/';
2070          }
2071  
2072          $random_base       = get_option( 'wp_random_base' );
2073          $this->random_base = is_string( $random_base ) ? $random_base : '';
2074  
2075          unset( $this->author_structure );
2076          unset( $this->date_structure );
2077          unset( $this->page_structure );
2078          unset( $this->search_structure );
2079          unset( $this->feed_structure );
2080          unset( $this->comment_feed_structure );
2081  
2082          $this->use_trailing_slashes = str_ends_with( $this->permalink_structure, '/' );
2083  
2084          // Enable generic rules for pages if permalink structure doesn't begin with a wildcard.
2085          if ( preg_match( '/^[^%]*%(?:postname|category|tag|author)%/', $this->permalink_structure ) ) {
2086              $this->use_verbose_page_rules = true;
2087          } else {
2088              $this->use_verbose_page_rules = false;
2089          }
2090      }
2091  
2092      /**
2093       * Sets the main permalink structure for the site.
2094       *
2095       * Will update the 'permalink_structure' option, if there is a difference
2096       * between the current permalink structure and the parameter value. Calls
2097       * WP_Rewrite::init() after the option is updated.
2098       *
2099       * Fires the {@see 'permalink_structure_changed'} action once the init call has
2100       * processed passing the old and new values
2101       *
2102       * @since 1.5.0
2103       *
2104       * @param string $permalink_structure Permalink structure.
2105       */
2106  	public function set_permalink_structure( $permalink_structure ) {
2107          if ( $this->permalink_structure !== $permalink_structure ) {
2108              $old_permalink_structure = $this->permalink_structure;
2109              update_option( 'permalink_structure', $permalink_structure );
2110  
2111              $this->init();
2112  
2113              /**
2114               * Fires after the permalink structure is updated.
2115               *
2116               * @since 2.8.0
2117               *
2118               * @param string $old_permalink_structure The previous permalink structure.
2119               * @param string $permalink_structure     The new permalink structure.
2120               */
2121              do_action( 'permalink_structure_changed', $old_permalink_structure, $permalink_structure );
2122          }
2123      }
2124  
2125      /**
2126       * Sets the category base for the category permalink.
2127       *
2128       * Will update the 'category_base' option, if there is a difference between
2129       * the current category base and the parameter value. Calls WP_Rewrite::init()
2130       * after the option is updated.
2131       *
2132       * @since 1.5.0
2133       *
2134       * @param string $category_base Category permalink structure base.
2135       */
2136  	public function set_category_base( $category_base ) {
2137          if ( get_option( 'category_base' ) !== $category_base ) {
2138              update_option( 'category_base', $category_base );
2139              $this->init();
2140          }
2141      }
2142  
2143      /**
2144       * Sets the tag base for the tag permalink.
2145       *
2146       * Will update the 'tag_base' option, if there is a difference between the
2147       * current tag base and the parameter value. Calls WP_Rewrite::init() after
2148       * the option is updated.
2149       *
2150       * @since 2.3.0
2151       *
2152       * @param string $tag_base Tag permalink structure base.
2153       */
2154  	public function set_tag_base( $tag_base ) {
2155          if ( get_option( 'tag_base' ) !== $tag_base ) {
2156              update_option( 'tag_base', $tag_base );
2157              $this->init();
2158          }
2159      }
2160  
2161      /**
2162       * Sets the random base for random content permalinks.
2163       *
2164       * Will update the 'wp_random_base' option, if there is a difference between the
2165       * current random base and the parameter value. Calls {@see WP_Rewrite::init()} after
2166       * the option is updated.
2167       *
2168       * @since 7.2.0
2169       *
2170       * @param string $random_base Random content permalink base.
2171       */
2172  	public function set_random_base( string $random_base ): void {
2173          if ( get_option( 'wp_random_base' ) !== $random_base ) {
2174              update_option( 'wp_random_base', $random_base );
2175              $this->init();
2176          }
2177      }
2178  
2179      /**
2180       * Retrieves the random content permalink base.
2181       *
2182       * Requests prefixed with this base redirect to a randomly selected post,
2183       * for example `example.com/random/` or `example.com/random/category/news/`.
2184       * A published page with this slug takes precedence over `example.com/random/`.
2185       *
2186       * Returns the custom base from the 'wp_random_base' option when one is set.
2187       * Otherwise returns the default base, translated into the site language so
2188       * that rewrite rules do not depend on the language of the user who flushes them.
2189       *
2190       * @since 7.2.0
2191       *
2192       * @return string Random content permalink base.
2193       */
2194  	public function get_random_base(): string {
2195          if ( '' !== $this->random_base ) {
2196              return $this->random_base;
2197          }
2198  
2199          $switched_locale = switch_to_locale( get_locale() );
2200  
2201          /*
2202           * translators: Default base for random content URLs, such as example.com/random/.
2203           * This string is used in URLs, so it must be lowercase and contain only letters,
2204           * numbers and hyphens. Transliterate it to Latin characters if needed.
2205           */
2206          $random_base = _x( 'random', 'random content permalink base' );
2207  
2208          if ( $switched_locale ) {
2209              restore_previous_locale();
2210          }
2211  
2212          return '' !== $random_base ? $random_base : 'random';
2213      }
2214  
2215      /**
2216       * Determines whether post permalinks can be at the random content permalink base.
2217       *
2218       * When the permalink structure is only the post name at the root of the site, for
2219       * example `/%postname%/`, a post with the same slug as the random base has the
2220       * same URL as random content requests, for example `example.com/random/`.
2221       *
2222       * Permalink structures with a prefix, such as `/blog/%postname%/` on the main site
2223       * of a subdirectory multisite network, do not conflict with the random base.
2224       *
2225       * @since 7.2.0
2226       *
2227       * @see WP_Rewrite::random_rewrite_rules()
2228       *
2229       * @return bool Whether a post's permalink can be at the random base.
2230       */
2231  	public function post_permalinks_share_random_base(): bool {
2232          if ( ! $this->using_permalinks() ) {
2233              return false;
2234          }
2235  
2236          return 1 === preg_match( '#^' . preg_quote( '/' . $this->root, '#' ) . '%postname%/?$#', $this->permalink_structure );
2237      }
2238  
2239      /**
2240       * Constructor - Calls init(), which runs setup.
2241       *
2242       * @since 1.5.0
2243       */
2244  	public function __construct() {
2245          $this->init();
2246      }
2247  }


Generated : Sat Oct 31 08:20:35 2026 Cross-referenced by PHPXref