/wp-includes/rewrite.php

  1. <?php 
  2. /** 
  3. * WordPress Rewrite API 
  4. * 
  5. * @package WordPress 
  6. * @subpackage Rewrite 
  7. */ 
  8.  
  9. /** 
  10. * Endpoint Mask for default, which is nothing. 
  11. * 
  12. * @since 2.1.0 
  13. */ 
  14. define('EP_NONE', 0); 
  15.  
  16. /** 
  17. * Endpoint Mask for Permalink. 
  18. * 
  19. * @since 2.1.0 
  20. */ 
  21. define('EP_PERMALINK', 1); 
  22.  
  23. /** 
  24. * Endpoint Mask for Attachment. 
  25. * 
  26. * @since 2.1.0 
  27. */ 
  28. define('EP_ATTACHMENT', 2); 
  29.  
  30. /** 
  31. * Endpoint Mask for date. 
  32. * 
  33. * @since 2.1.0 
  34. */ 
  35. define('EP_DATE', 4); 
  36.  
  37. /** 
  38. * Endpoint Mask for year 
  39. * 
  40. * @since 2.1.0 
  41. */ 
  42. define('EP_YEAR', 8); 
  43.  
  44. /** 
  45. * Endpoint Mask for month. 
  46. * 
  47. * @since 2.1.0 
  48. */ 
  49. define('EP_MONTH', 16); 
  50.  
  51. /** 
  52. * Endpoint Mask for day. 
  53. * 
  54. * @since 2.1.0 
  55. */ 
  56. define('EP_DAY', 32); 
  57.  
  58. /** 
  59. * Endpoint Mask for root. 
  60. * 
  61. * @since 2.1.0 
  62. */ 
  63. define('EP_ROOT', 64); 
  64.  
  65. /** 
  66. * Endpoint Mask for comments. 
  67. * 
  68. * @since 2.1.0 
  69. */ 
  70. define('EP_COMMENTS', 128); 
  71.  
  72. /** 
  73. * Endpoint Mask for searches. 
  74. * 
  75. * @since 2.1.0 
  76. */ 
  77. define('EP_SEARCH', 256); 
  78.  
  79. /** 
  80. * Endpoint Mask for categories. 
  81. * 
  82. * @since 2.1.0 
  83. */ 
  84. define('EP_CATEGORIES', 512); 
  85.  
  86. /** 
  87. * Endpoint Mask for tags. 
  88. * 
  89. * @since 2.3.0 
  90. */ 
  91. define('EP_TAGS', 1024); 
  92.  
  93. /** 
  94. * Endpoint Mask for authors. 
  95. * 
  96. * @since 2.1.0 
  97. */ 
  98. define('EP_AUTHORS', 2048); 
  99.  
  100. /** 
  101. * Endpoint Mask for pages. 
  102. * 
  103. * @since 2.1.0 
  104. */ 
  105. define('EP_PAGES', 4096); 
  106.  
  107. /** 
  108. * Endpoint Mask for all archive views. 
  109. * 
  110. * @since 3.7.0 
  111. */ 
  112. define( 'EP_ALL_ARCHIVES', EP_DATE | EP_YEAR | EP_MONTH | EP_DAY | EP_CATEGORIES | EP_TAGS | EP_AUTHORS ); 
  113.  
  114. /** 
  115. * Endpoint Mask for everything. 
  116. * 
  117. * @since 2.1.0 
  118. */ 
  119. define( 'EP_ALL', EP_PERMALINK | EP_ATTACHMENT | EP_ROOT | EP_COMMENTS | EP_SEARCH | EP_PAGES | EP_ALL_ARCHIVES ); 
  120.  
  121. /** 
  122. * Adds a rewrite rule that transforms a URL structure to a set of query vars. 
  123. * 
  124. * Any value in the $after parameter that isn't 'bottom' will result in the rule 
  125. * being placed at the top of the rewrite rules. 
  126. * 
  127. * @since 2.1.0 
  128. * @since 4.4.0 Array support was added to the `$query` parameter. 
  129. * 
  130. * @global WP_Rewrite $wp_rewrite WordPress Rewrite Component. 
  131. * 
  132. * @param string $regex Regular expression to match request against. 
  133. * @param string|array $query The corresponding query vars for this rewrite rule. 
  134. * @param string $after Optional. Priority of the new rule. Accepts 'top' 
  135. * or 'bottom'. Default 'bottom'. 
  136. */ 
  137. function add_rewrite_rule( $regex, $query, $after = 'bottom' ) { 
  138. global $wp_rewrite; 
  139.  
  140. $wp_rewrite->add_rule( $regex, $query, $after ); 
  141.  
  142. /** 
  143. * Add a new rewrite tag (like %postname%). 
  144. * 
  145. * The $query parameter is optional. If it is omitted you must ensure that 
  146. * you call this on, or before, the {@see 'init'} hook. This is because $query defaults 
  147. * to "$tag=", and for this to work a new query var has to be added. 
  148. * 
  149. * @since 2.1.0 
  150. * 
  151. * @global WP_Rewrite $wp_rewrite 
  152. * @global WP $wp 
  153. * 
  154. * @param string $tag Name of the new rewrite tag. 
  155. * @param string $regex Regular expression to substitute the tag for in rewrite rules. 
  156. * @param string $query Optional. String to append to the rewritten query. Must end in '='. Default empty. 
  157. */ 
  158. function add_rewrite_tag( $tag, $regex, $query = '' ) { 
  159. // validate the tag's name 
  160. if ( strlen( $tag ) < 3 || $tag[0] != '%' || $tag[ strlen($tag) - 1 ] != '%' ) 
  161. return; 
  162.  
  163. global $wp_rewrite, $wp; 
  164.  
  165. if ( empty( $query ) ) { 
  166. $qv = trim( $tag, '%' ); 
  167. $wp->add_query_var( $qv ); 
  168. $query = $qv . '='; 
  169.  
  170. $wp_rewrite->add_rewrite_tag( $tag, $regex, $query ); 
  171.  
  172. /** 
  173. * Removes an existing rewrite tag (like %postname%). 
  174. * 
  175. * @since 4.5.0 
  176. * 
  177. * @global WP_Rewrite $wp_rewrite WordPress rewrite component. 
  178. * 
  179. * @param string $tag Name of the rewrite tag. 
  180. */ 
  181. function remove_rewrite_tag( $tag ) { 
  182. global $wp_rewrite; 
  183. $wp_rewrite->remove_rewrite_tag( $tag ); 
  184.  
  185. /** 
  186. * Add permalink structure. 
  187. * 
  188. * @since 3.0.0 
  189. * 
  190. * @see WP_Rewrite::add_permastruct() 
  191. * @global WP_Rewrite $wp_rewrite WordPress rewrite component. 
  192. * 
  193. * @param string $name Name for permalink structure. 
  194. * @param string $struct Permalink structure. 
  195. * @param array $args Optional. Arguments for building the rules from the permalink structure,  
  196. * see WP_Rewrite::add_permastruct() for full details. Default empty array. 
  197. */ 
  198. function add_permastruct( $name, $struct, $args = array() ) { 
  199. global $wp_rewrite; 
  200.  
  201. // Back-compat for the old parameters: $with_front and $ep_mask. 
  202. if ( ! is_array( $args ) ) 
  203. $args = array( 'with_front' => $args ); 
  204. if ( func_num_args() == 4 ) 
  205. $args['ep_mask'] = func_get_arg( 3 ); 
  206.  
  207. $wp_rewrite->add_permastruct( $name, $struct, $args ); 
  208.  
  209. /** 
  210. * Removes a permalink structure. 
  211. * 
  212. * Can only be used to remove permastructs that were added using add_permastruct(). 
  213. * Built-in permastructs cannot be removed. 
  214. * 
  215. * @since 4.5.0 
  216. * 
  217. * @see WP_Rewrite::remove_permastruct() 
  218. * @global WP_Rewrite $wp_rewrite WordPress rewrite component. 
  219. * 
  220. * @param string $name Name for permalink structure. 
  221. */ 
  222. function remove_permastruct( $name ) { 
  223. global $wp_rewrite; 
  224.  
  225. $wp_rewrite->remove_permastruct( $name ); 
  226.  
  227. /** 
  228. * Add a new feed type like /atom1/. 
  229. * 
  230. * @since 2.1.0 
  231. * 
  232. * @global WP_Rewrite $wp_rewrite 
  233. * 
  234. * @param string $feedname Feed name. 
  235. * @param callable $function Callback to run on feed display. 
  236. * @return string Feed action name. 
  237. */ 
  238. function add_feed( $feedname, $function ) { 
  239. global $wp_rewrite; 
  240.  
  241. if ( ! in_array( $feedname, $wp_rewrite->feeds ) ) { 
  242. $wp_rewrite->feeds[] = $feedname; 
  243.  
  244. $hook = 'do_feed_' . $feedname; 
  245.  
  246. // Remove default function hook 
  247. remove_action( $hook, $hook ); 
  248.  
  249. add_action( $hook, $function, 10, 2 ); 
  250.  
  251. return $hook; 
  252.  
  253. /** 
  254. * Remove rewrite rules and then recreate rewrite rules. 
  255. * 
  256. * @since 3.0.0 
  257. * 
  258. * @global WP_Rewrite $wp_rewrite 
  259. * 
  260. * @param bool $hard Whether to update .htaccess (hard flush) or just update 
  261. * rewrite_rules transient (soft flush). Default is true (hard). 
  262. */ 
  263. function flush_rewrite_rules( $hard = true ) { 
  264. global $wp_rewrite; 
  265. $wp_rewrite->flush_rules( $hard ); 
  266.  
  267. /** 
  268. * Add an endpoint, like /trackback/. 
  269. * 
  270. * Adding an endpoint creates extra rewrite rules for each of the matching 
  271. * places specified by the provided bitmask. For example: 
  272. * 
  273. * add_rewrite_endpoint( 'json', EP_PERMALINK | EP_PAGES ); 
  274. * 
  275. * will add a new rewrite rule ending with "json(/(.*))?/?$" for every permastruct 
  276. * that describes a permalink (post) or page. This is rewritten to "json=$match" 
  277. * where $match is the part of the URL matched by the endpoint regex (e.g. "foo" in 
  278. * "[permalink]/json/foo/"). 
  279. * 
  280. * A new query var with the same name as the endpoint will also be created. 
  281. * 
  282. * When specifying $places ensure that you are using the EP_* constants (or a 
  283. * combination of them using the bitwise OR operator) as their values are not 
  284. * guaranteed to remain static (especially `EP_ALL`). 
  285. * 
  286. * Be sure to flush the rewrite rules - see flush_rewrite_rules() - when your plugin gets 
  287. * activated and deactivated. 
  288. * 
  289. * @since 2.1.0 
  290. * @since 4.3.0 Added support for skipping query var registration by passing `false` to `$query_var`. 
  291. * 
  292. * @global WP_Rewrite $wp_rewrite 
  293. * 
  294. * @param string $name Name of the endpoint. 
  295. * @param int $places Endpoint mask describing the places the endpoint should be added. 
  296. * @param string|bool $query_var Name of the corresponding query variable. Pass `false` to skip registering a query_var 
  297. * for this endpoint. Defaults to the value of `$name`. 
  298. */ 
  299. function add_rewrite_endpoint( $name, $places, $query_var = true ) { 
  300. global $wp_rewrite; 
  301. $wp_rewrite->add_endpoint( $name, $places, $query_var ); 
  302.  
  303. /** 
  304. * Filters the URL base for taxonomies. 
  305. * 
  306. * To remove any manually prepended /index.php/. 
  307. * 
  308. * @access private 
  309. * @since 2.6.0 
  310. * 
  311. * @param string $base The taxonomy base that we're going to filter 
  312. * @return string 
  313. */ 
  314. function _wp_filter_taxonomy_base( $base ) { 
  315. if ( !empty( $base ) ) { 
  316. $base = preg_replace( '|^/index\.php/|', '', $base ); 
  317. $base = trim( $base, '/' ); 
  318. return $base; 
  319.  
  320.  
  321. /** 
  322. * Resolve numeric slugs that collide with date permalinks. 
  323. * 
  324. * Permalinks of posts with numeric slugs can sometimes look to WP_Query::parse_query() 
  325. * like a date archive, as when your permalink structure is `/%year%/%postname%/` and 
  326. * a post with post_name '05' has the URL `/2015/05/`. 
  327. * 
  328. * This function detects conflicts of this type and resolves them in favor of the 
  329. * post permalink. 
  330. * 
  331. * Note that, since 4.3.0, wp_unique_post_slug() prevents the creation of post slugs 
  332. * that would result in a date archive conflict. The resolution performed in this 
  333. * function is primarily for legacy content, as well as cases when the admin has changed 
  334. * the site's permalink structure in a way that introduces URL conflicts. 
  335. * 
  336. * @since 4.3.0 
  337. * 
  338. * @param array $query_vars Optional. Query variables for setting up the loop, as determined in 
  339. * WP::parse_request(). Default empty array. 
  340. * @return array Returns the original array of query vars, with date/post conflicts resolved. 
  341. */ 
  342. function wp_resolve_numeric_slug_conflicts( $query_vars = array() ) { 
  343. if ( ! isset( $query_vars['year'] ) && ! isset( $query_vars['monthnum'] ) && ! isset( $query_vars['day'] ) ) { 
  344. return $query_vars; 
  345.  
  346. // Identify the 'postname' position in the permastruct array. 
  347. $permastructs = array_values( array_filter( explode( '/', get_option( 'permalink_structure' ) ) ) ); 
  348. $postname_index = array_search( '%postname%', $permastructs ); 
  349.  
  350. if ( false === $postname_index ) { 
  351. return $query_vars; 
  352.  
  353. /** 
  354. * A numeric slug could be confused with a year, month, or day, depending on position. To account for 
  355. * the possibility of post pagination (eg 2015/2 for the second page of a post called '2015'), our 
  356. * `is_*` checks are generous: check for year-slug clashes when `is_year` *or* `is_month`, and check 
  357. * for month-slug clashes when `is_month` *or* `is_day`. 
  358. */ 
  359. $compare = ''; 
  360. if ( 0 === $postname_index && ( isset( $query_vars['year'] ) || isset( $query_vars['monthnum'] ) ) ) { 
  361. $compare = 'year'; 
  362. } elseif ( '%year%' === $permastructs[ $postname_index - 1 ] && ( isset( $query_vars['monthnum'] ) || isset( $query_vars['day'] ) ) ) { 
  363. $compare = 'monthnum'; 
  364. } elseif ( '%monthnum%' === $permastructs[ $postname_index - 1 ] && isset( $query_vars['day'] ) ) { 
  365. $compare = 'day'; 
  366.  
  367. if ( ! $compare ) { 
  368. return $query_vars; 
  369.  
  370. // This is the potentially clashing slug. 
  371. $value = $query_vars[ $compare ]; 
  372.  
  373. $post = get_page_by_path( $value, OBJECT, 'post' ); 
  374. if ( ! ( $post instanceof WP_Post ) ) { 
  375. return $query_vars; 
  376.  
  377. // If the date of the post doesn't match the date specified in the URL, resolve to the date archive. 
  378. if ( preg_match( '/^([0-9]{4})\-([0-9]{2})/', $post->post_date, $matches ) && isset( $query_vars['year'] ) && ( 'monthnum' === $compare || 'day' === $compare ) ) { 
  379. // $matches[1] is the year the post was published. 
  380. if ( intval( $query_vars['year'] ) !== intval( $matches[1] ) ) { 
  381. return $query_vars; 
  382.  
  383. // $matches[2] is the month the post was published. 
  384. if ( 'day' === $compare && isset( $query_vars['monthnum'] ) && intval( $query_vars['monthnum'] ) !== intval( $matches[2] ) ) { 
  385. return $query_vars; 
  386.  
  387. /** 
  388. * If the located post contains nextpage pagination, then the URL chunk following postname may be 
  389. * intended as the page number. Verify that it's a valid page before resolving to it. 
  390. */ 
  391. $maybe_page = ''; 
  392. if ( 'year' === $compare && isset( $query_vars['monthnum'] ) ) { 
  393. $maybe_page = $query_vars['monthnum']; 
  394. } elseif ( 'monthnum' === $compare && isset( $query_vars['day'] ) ) { 
  395. $maybe_page = $query_vars['day']; 
  396. // Bug found in #11694 - 'page' was returning '/4' 
  397. $maybe_page = (int) trim( $maybe_page, '/' ); 
  398.  
  399. $post_page_count = substr_count( $post->post_content, '<!--nextpage-->' ) + 1; 
  400.  
  401. // If the post doesn't have multiple pages, but a 'page' candidate is found, resolve to the date archive. 
  402. if ( 1 === $post_page_count && $maybe_page ) { 
  403. return $query_vars; 
  404.  
  405. // If the post has multiple pages and the 'page' number isn't valid, resolve to the date archive. 
  406. if ( $post_page_count > 1 && $maybe_page > $post_page_count ) { 
  407. return $query_vars; 
  408.  
  409. // If we've gotten to this point, we have a slug/date clash. First, adjust for nextpage. 
  410. if ( '' !== $maybe_page ) { 
  411. $query_vars['page'] = intval( $maybe_page ); 
  412.  
  413. // Next, unset autodetected date-related query vars. 
  414. unset( $query_vars['year'] ); 
  415. unset( $query_vars['monthnum'] ); 
  416. unset( $query_vars['day'] ); 
  417.  
  418. // Then, set the identified post. 
  419. $query_vars['name'] = $post->post_name; 
  420.  
  421. // Finally, return the modified query vars. 
  422. return $query_vars; 
  423.  
  424. /** 
  425. * Examine a URL and try to determine the post ID it represents. 
  426. * 
  427. * Checks are supposedly from the hosted site blog. 
  428. * 
  429. * @since 1.0.0 
  430. * 
  431. * @global WP_Rewrite $wp_rewrite 
  432. * @global WP $wp 
  433. * 
  434. * @param string $url Permalink to check. 
  435. * @return int Post ID, or 0 on failure. 
  436. */ 
  437. function url_to_postid( $url ) { 
  438. global $wp_rewrite; 
  439.  
  440. /** 
  441. * Filters the URL to derive the post ID from. 
  442. * 
  443. * @since 2.2.0 
  444. * 
  445. * @param string $url The URL to derive the post ID from. 
  446. */ 
  447. $url = apply_filters( 'url_to_postid', $url ); 
  448.  
  449. // First, check to see if there is a 'p=N' or 'page_id=N' to match against 
  450. if ( preg_match('#[?&](p|page_id|attachment_id)=(\d+)#', $url, $values) ) { 
  451. $id = absint($values[2]); 
  452. if ( $id ) 
  453. return $id; 
  454.  
  455. // Get rid of the #anchor 
  456. $url_split = explode('#', $url); 
  457. $url = $url_split[0]; 
  458.  
  459. // Get rid of URL ?query=string 
  460. $url_split = explode('?', $url); 
  461. $url = $url_split[0]; 
  462.  
  463. // Set the correct URL scheme. 
  464. $scheme = parse_url( home_url(), PHP_URL_SCHEME ); 
  465. $url = set_url_scheme( $url, $scheme ); 
  466.  
  467. // Add 'www.' if it is absent and should be there 
  468. if ( false !== strpos(home_url(), '://www.') && false === strpos($url, '://www.') ) 
  469. $url = str_replace('://', '://www.', $url); 
  470.  
  471. // Strip 'www.' if it is present and shouldn't be 
  472. if ( false === strpos(home_url(), '://www.') ) 
  473. $url = str_replace('://www.', '://', $url); 
  474.  
  475. if ( trim( $url, '/' ) === home_url() && 'page' == get_option( 'show_on_front' ) ) { 
  476. $page_on_front = get_option( 'page_on_front' ); 
  477.  
  478. if ( $page_on_front && get_post( $page_on_front ) instanceof WP_Post ) { 
  479. return (int) $page_on_front; 
  480.  
  481. // Check to see if we are using rewrite rules 
  482. $rewrite = $wp_rewrite->wp_rewrite_rules(); 
  483.  
  484. // Not using rewrite rules, and 'p=N' and 'page_id=N' methods failed, so we're out of options 
  485. if ( empty($rewrite) ) 
  486. return 0; 
  487.  
  488. // Strip 'index.php/' if we're not using path info permalinks 
  489. if ( !$wp_rewrite->using_index_permalinks() ) 
  490. $url = str_replace( $wp_rewrite->index . '/', '', $url ); 
  491.  
  492. if ( false !== strpos( trailingslashit( $url ), home_url( '/' ) ) ) { 
  493. // Chop off http://domain.com/[path] 
  494. $url = str_replace(home_url(), '', $url); 
  495. } else { 
  496. // Chop off /path/to/blog 
  497. $home_path = parse_url( home_url( '/' ) ); 
  498. $home_path = isset( $home_path['path'] ) ? $home_path['path'] : '' ; 
  499. $url = preg_replace( sprintf( '#^%s#', preg_quote( $home_path ) ), '', trailingslashit( $url ) ); 
  500.  
  501. // Trim leading and lagging slashes 
  502. $url = trim($url, '/'); 
  503.  
  504. $request = $url; 
  505. $post_type_query_vars = array(); 
  506.  
  507. foreach ( get_post_types( array() , 'objects' ) as $post_type => $t ) { 
  508. if ( ! empty( $t->query_var ) ) 
  509. $post_type_query_vars[ $t->query_var ] = $post_type; 
  510.  
  511. // Look for matches. 
  512. $request_match = $request; 
  513. foreach ( (array)$rewrite as $match => $query) { 
  514.  
  515. // If the requesting file is the anchor of the match, prepend it 
  516. // to the path info. 
  517. if ( !empty($url) && ($url != $request) && (strpos($match, $url) === 0) ) 
  518. $request_match = $url . '/' . $request; 
  519.  
  520. if ( preg_match("#^$match#", $request_match, $matches) ) { 
  521.  
  522. if ( $wp_rewrite->use_verbose_page_rules && preg_match( '/pagename=\$matches\[([0-9]+)\]/', $query, $varmatch ) ) { 
  523. // This is a verbose page match, let's check to be sure about it. 
  524. $page = get_page_by_path( $matches[ $varmatch[1] ] ); 
  525. if ( ! $page ) { 
  526. continue; 
  527.  
  528. $post_status_obj = get_post_status_object( $page->post_status ); 
  529. if ( ! $post_status_obj->public && ! $post_status_obj->protected 
  530. && ! $post_status_obj->private && $post_status_obj->exclude_from_search ) { 
  531. continue; 
  532.  
  533. // Got a match. 
  534. // Trim the query of everything up to the '?'. 
  535. $query = preg_replace("!^.+\?!", '', $query); 
  536.  
  537. // Substitute the substring matches into the query. 
  538. $query = addslashes(WP_MatchesMapRegex::apply($query, $matches)); 
  539.  
  540. // Filter out non-public query vars 
  541. global $wp; 
  542. parse_str( $query, $query_vars ); 
  543. $query = array(); 
  544. foreach ( (array) $query_vars as $key => $value ) { 
  545. if ( in_array( $key, $wp->public_query_vars ) ) { 
  546. $query[$key] = $value; 
  547. if ( isset( $post_type_query_vars[$key] ) ) { 
  548. $query['post_type'] = $post_type_query_vars[$key]; 
  549. $query['name'] = $value; 
  550.  
  551. // Resolve conflicts between posts with numeric slugs and date archive queries. 
  552. $query = wp_resolve_numeric_slug_conflicts( $query ); 
  553.  
  554. // Do the query 
  555. $query = new WP_Query( $query ); 
  556. if ( ! empty( $query->posts ) && $query->is_singular ) 
  557. return $query->post->ID; 
  558. else 
  559. return 0; 
  560. return 0; 
.