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