}
/**
* Fires after the rewrite rules are generated.
*
* @since 1.5.0
*
* @param WP_Rewrite $wp_rewrite Current WP_Rewrite instance (passed by reference).
*/
do_action_ref_array( 'generate_rewrite_rules', array( &$this ) );
/**
* Filters the full set of generated rewrite rules.
*
* @since 1.5.0
*
* @param string[] $rules The compiled array of rewrite rules, keyed by their regex pattern.
*/
$this->rules = apply_filters( 'rewrite_rules_array', $this->rules );
return $this->rules;
}
/**
* Retrieves the rewrite rules.
*
* The difference between this method and WP_Rewrite::rewrite_rules() is that
* this method stores the rewrite rules in the 'rewrite_rules' option and retrieves
* it. This prevents having to process all of the permalinks to get the rewrite rules
* in the form of caching.
*
* @since 1.5.0
*
* @return string[] Array of rewrite rules keyed by their regex pattern.
*/
public function wp_rewrite_rules() {
$this->rules = get_option( 'rewrite_rules' );
if ( empty( $this->rules ) ) {
$this->refresh_rewrite_rules();
}
return $this->rules;
}
/**
* Refreshes the rewrite rules, saving the fresh value to the database.
*
* If the {@see 'wp_loaded'} action has not occurred yet, will postpone saving to the database.
*
* @since 6.4.0
*/
private function refresh_rewrite_rules() {
$this->rules = '';
$this->matches = 'matches';
$this->rewrite_rules();
if ( ! did_action( 'wp_loaded' ) ) {
/*
* It is not safe to save the results right now, as the rules may be partial.
* Need to give all rules the chance to register.
*/
add_action( 'wp_loaded', array( $this, 'flush_rules' ) );
} else {
update_option( 'rewrite_rules', $this->rules );
}
}
/**
* Retrieves mod_rewrite-formatted rewrite rules to write to .htaccess.
*
* Does not actually write to the .htaccess file, but creates the rules for
* the process that will.
*
* Will add the non_wp_rules property rules to the .htaccess file before
* the WordPress rewrite rules one.
*
* @since 1.5.0
*
* @return string
*/
public function mod_rewrite_rules() {
if ( ! $this->using_permalinks() ) {
return '';
}
$site_root = parse_url( site_url() );
if ( isset( $site_root['path'] ) ) {
$site_root = trailingslashit( $site_root['path'] );
}
$home_root = parse_url( home_url() );
if ( isset( $home_root['path'] ) ) {
$home_root = trailingslashit( $home_root['path'] );
} else {
$home_root = '/';
}
$rules = "\n";
$rules .= "RewriteEngine On\n";
$rules .= "RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\n";
$rules .= "RewriteBase $home_root\n";
// Prevent -f checks on index.php.
$rules .= "RewriteRule ^index\.php$ - [L]\n";
// Add in the rules that don't redirect to WP's index.php (and thus shouldn't be handled by WP at all).
foreach ( (array) $this->non_wp_rules as $match => $query ) {
// Apache 1.3 does not support the reluctant (non-greedy) modifier.
$match = str_replace( '.+?', '.+', $match );
$rules .= 'RewriteRule ^' . $match . ' ' . $home_root . $query . " [QSA,L]\n";
}
if ( $this->use_verbose_rules ) {
$this->matches = '';
$rewrite = $this->rewrite_rules();
$num_rules = count( $rewrite );
$rules .= "RewriteCond %{REQUEST_FILENAME} -f [OR]\n" .
"RewriteCond %{REQUEST_FILENAME} -d\n" .
"RewriteRule ^.*$ - [S=$num_rules]\n";
foreach ( (array) $rewrite as $match => $query ) {
// Apache 1.3 does not support the reluctant (non-greedy) modifier.
$match = str_replace( '.+?', '.+', $match );
if ( str_contains( $query, $this->index ) ) {
$rules .= 'RewriteRule ^' . $match . ' ' . $home_root . $query . " [QSA,L]\n";
} else {
$rules .= 'RewriteRule ^' . $match . ' ' . $site_root . $query . " [QSA,L]\n";
}
}
} else {
$rules .= "RewriteCond %{REQUEST_FILENAME} !-f\n" .
"RewriteCond %{REQUEST_FILENAME} !-d\n" .
"RewriteRule . {$home_root}{$this->index} [L]\n";
}
$rules .= "\n";
/**
* Filters the list of rewrite rules formatted for output to an .htaccess file.
*
* @since 1.5.0
*
* @param string $rules mod_rewrite Rewrite rules formatted for .htaccess.
*/
$rules = apply_filters( 'mod_rewrite_rules', $rules );
/**
* Filters the list of rewrite rules formatted for output to an .htaccess file.
*
* @since 1.5.0
* @deprecated 1.5.0 Use the {@see 'mod_rewrite_rules'} filter instead.
*
* @param string $rules mod_rewrite Rewrite rules formatted for .htaccess.
*/
return apply_filters_deprecated( 'rewrite_rules', array( $rules ), '1.5.0', 'mod_rewrite_rules' );
}
/**
* Retrieves IIS7 URL Rewrite formatted rewrite rules to write to web.config file.
*
* Does not actually write to the web.config file, but creates the rules for
* the process that will.
*
* @since 2.8.0
*
* @param bool $add_parent_tags Optional. Whether to add parent tags to the rewrite rule sets.
* Default false.
* @return string IIS7 URL rewrite rule sets.
*/
public function iis7_url_rewrite_rules( $add_parent_tags = false ) {
if ( ! $this->using_permalinks() ) {
return '';
}
$rules = '';
if ( $add_parent_tags ) {
$rules .= '
';
}
$rules .= '
';
if ( $add_parent_tags ) {
$rules .= '
';
}
/**
* Filters the list of rewrite rules formatted for output to a web.config.
*
* @since 2.8.0
*
* @param string $rules Rewrite rules formatted for IIS web.config.
*/
return apply_filters( 'iis7_url_rewrite_rules', $rules );
}
/**
* Adds a rewrite rule that transforms a URL structure to a set of query vars.
*
* Any value in the $after parameter that isn't 'bottom' will result in the rule
* being placed at the top of the rewrite rules.
*
* @since 2.1.0
* @since 4.4.0 Array support was added to the `$query` parameter.
*
* @param string $regex Regular expression to match request against.
* @param string|array $query The corresponding query vars for this rewrite rule.
* @param string $after Optional. Priority of the new rule. Accepts 'top'
* or 'bottom'. Default 'bottom'.
*/
public function add_rule( $regex, $query, $after = 'bottom' ) {
if ( is_array( $query ) ) {
$external = false;
$query = add_query_arg( $query, 'index.php' );
} else {
$index = ! str_contains( $query, '?' ) ? strlen( $query ) : strpos( $query, '?' );
$front = substr( $query, 0, $index );
$external = $front !== $this->index;
}
// "external" = it doesn't correspond to index.php.
if ( $external ) {
$this->add_external_rule( $regex, $query );
} else {
if ( 'bottom' === $after ) {
$this->extra_rules = array_merge( $this->extra_rules, array( $regex => $query ) );
} else {
$this->extra_rules_top = array_merge( $this->extra_rules_top, array( $regex => $query ) );
}
}
}
/**
* Adds a rewrite rule that doesn't correspond to index.php.
*
* @since 2.1.0
*
* @param string $regex Regular expression to match request against.
* @param string $query The corresponding query vars for this rewrite rule.
*/
public function add_external_rule( $regex, $query ) {
$this->non_wp_rules[ $regex ] = $query;
}
/**
* Adds an endpoint, like /trackback/.
*
* @since 2.1.0
* @since 3.9.0 $query_var parameter added.
* @since 4.3.0 Added support for skipping query var registration by passing `false` to `$query_var`.
*
* @see add_rewrite_endpoint() for full documentation.
* @global WP $wp Current WordPress environment instance.
*
* @param string $name Name of the endpoint.
* @param int $places Endpoint mask describing the places the endpoint should be added.
* Accepts a mask of:
* - `EP_ALL`
* - `EP_NONE`
* - `EP_ALL_ARCHIVES`
* - `EP_ATTACHMENT`
* - `EP_AUTHORS`
* - `EP_CATEGORIES`
* - `EP_COMMENTS`
* - `EP_DATE`
* - `EP_DAY`
* - `EP_MONTH`
* - `EP_PAGES`
* - `EP_PERMALINK`
* - `EP_ROOT`
* - `EP_SEARCH`
* - `EP_TAGS`
* - `EP_YEAR`
* @param string|bool $query_var Optional. Name of the corresponding query variable. Pass `false` to
* skip registering a query_var for this endpoint. Defaults to the
* value of `$name`.
*/
public function add_endpoint( $name, $places, $query_var = true ) {
global $wp;
// For backward compatibility, if null has explicitly been passed as `$query_var`, assume `true`.
if ( true === $query_var || null === $query_var ) {
$query_var = $name;
}
$this->endpoints[] = array( $places, $name, $query_var );
if ( $query_var ) {
$wp->add_query_var( $query_var );
}
}
/**
* Adds a new permalink structure.
*
* A permalink structure (permastruct) is an abstract definition of a set of rewrite rules;
* it is an easy way of expressing a set of regular expressions that rewrite to a set of
* query strings. The new permastruct is added to the WP_Rewrite::$extra_permastructs array.
*
* When the rewrite rules are built by WP_Rewrite::rewrite_rules(), all of these extra
* permastructs are passed to WP_Rewrite::generate_rewrite_rules() which transforms them
* into the regular expressions that many love to hate.
*
* The `$args` parameter gives you control over how WP_Rewrite::generate_rewrite_rules()
* works on the new permastruct.
*
* @since 2.5.0
*
* @param string $name Name for permalink structure.
* @param string $struct Permalink structure (e.g. category/%category%)
* @param array $args {
* Optional. Arguments for building rewrite rules based on the permalink structure.
* Default empty array.
*
* @type bool $with_front Whether the structure should be prepended with `WP_Rewrite::$front`.
* Default true.
* @type int $ep_mask The endpoint mask defining which endpoints are added to the structure.
* Accepts a mask of:
* - `EP_ALL`
* - `EP_NONE`
* - `EP_ALL_ARCHIVES`
* - `EP_ATTACHMENT`
* - `EP_AUTHORS`
* - `EP_CATEGORIES`
* - `EP_COMMENTS`
* - `EP_DATE`
* - `EP_DAY`
* - `EP_MONTH`
* - `EP_PAGES`
* - `EP_PERMALINK`
* - `EP_ROOT`
* - `EP_SEARCH`
* - `EP_TAGS`
* - `EP_YEAR`
* Default `EP_NONE`.
* @type bool $paged Whether archive pagination rules should be added for the structure.
* Default true.
* @type bool $feed Whether feed rewrite rules should be added for the structure. Default true.
* @type bool $forcomments Whether the feed rules should be a query for a comments feed. Default false.
* @type bool $walk_dirs Whether the 'directories' making up the structure should be walked over
* and rewrite rules built for each in-turn. Default true.
* @type bool $endpoints Whether endpoints should be applied to the generated rules. Default true.
* }
*/
public function add_permastruct( $name, $struct, $args = array() ) {
// Back-compat for the old parameters: $with_front and $ep_mask.
if ( ! is_array( $args ) ) {
$args = array( 'with_front' => $args );
}
if ( func_num_args() === 4 ) {
$args['ep_mask'] = func_get_arg( 3 );
}
$defaults = array(
'with_front' => true,
'ep_mask' => EP_NONE,
'paged' => true,
'feed' => true,
'forcomments' => false,
'walk_dirs' => true,
'endpoints' => true,
);
$args = array_intersect_key( $args, $defaults );
$args = wp_parse_args( $args, $defaults );
if ( $args['with_front'] ) {
$struct = $this->front . $struct;
} else {
$struct = $this->root . $struct;
}
$args['struct'] = $struct;
$this->extra_permastructs[ $name ] = $args;
}
/**
* Removes a permalink structure.
*
* @since 4.5.0
*
* @param string $name Name for permalink structure.
*/
public function remove_permastruct( $name ) {
unset( $this->extra_permastructs[ $name ] );
}
/**
* Removes rewrite rules and then recreate rewrite rules.
*
* Calls WP_Rewrite::wp_rewrite_rules() after removing the 'rewrite_rules' option.
* If the function named 'save_mod_rewrite_rules' exists, it will be called.
*
* @since 2.0.1
*
* @param bool $hard Whether to update .htaccess (hard flush) or just update rewrite_rules option (soft flush). Default is true (hard).
*/
public function flush_rules( $hard = true ) {
static $do_hard_later = null;
// Prevent this action from running before everyone has registered their rewrites.
if ( ! did_action( 'wp_loaded' ) ) {
add_action( 'wp_loaded', array( $this, 'flush_rules' ) );
$do_hard_later = ( isset( $do_hard_later ) ) ? $do_hard_later || $hard : $hard;
return;
}
if ( isset( $do_hard_later ) ) {
$hard = $do_hard_later;
unset( $do_hard_later );
}
$this->refresh_rewrite_rules();
/**
* Filters whether a "hard" rewrite rule flush should be performed when requested.
*
* A "hard" flush updates .htaccess (Apache) or web.config (IIS).
*
* @since 3.7.0
*
* @param bool $hard Whether to flush rewrite rules "hard". Default true.
*/
if ( ! $hard || ! apply_filters( 'flush_rewrite_rules_hard', true ) ) {
return;
}
if ( function_exists( 'save_mod_rewrite_rules' ) ) {
save_mod_rewrite_rules();
}
if ( function_exists( 'iis7_save_url_rewrite_rules' ) ) {
iis7_save_url_rewrite_rules();
}
}
/**
* Sets up the object's properties.
*
* The 'use_verbose_page_rules' object property will be set to true if the
* permalink structure begins with one of the following: '%postname%', '%category%',
* '%tag%', or '%author%'.
*
* @since 1.5.0
*/
public function init() {
$this->extra_rules = array();
$this->non_wp_rules = array();
$this->endpoints = array();
$this->permalink_structure = get_option( 'permalink_structure' );
$this->front = substr( $this->permalink_structure, 0, strpos( $this->permalink_structure, '%' ) );
$this->root = '';
if ( $this->using_index_permalinks() ) {
$this->root = $this->index . '/';
}
unset( $this->author_structure );
unset( $this->date_structure );
unset( $this->page_structure );
unset( $this->search_structure );
unset( $this->feed_structure );
unset( $this->comment_feed_structure );
$this->use_trailing_slashes = str_ends_with( $this->permalink_structure, '/' );
// Enable generic rules for pages if permalink structure doesn't begin with a wildcard.
if ( preg_match( '/^[^%]*%(?:postname|category|tag|author)%/', $this->permalink_structure ) ) {
$this->use_verbose_page_rules = true;
} else {
$this->use_verbose_page_rules = false;
}
}
/**
* Sets the main permalink structure for the site.
*
* Will update the 'permalink_structure' option, if there is a difference
* between the current permalink structure and the parameter value. Calls
* WP_Rewrite::init() after the option is updated.
*
* Fires the {@see 'permalink_structure_changed'} action once the init call has
* processed passing the old and new values
*
* @since 1.5.0
*
* @param string $permalink_structure Permalink structure.
*/
public function set_permalink_structure( $permalink_structure ) {
if ( $this->permalink_structure !== $permalink_structure ) {
$old_permalink_structure = $this->permalink_structure;
update_option( 'permalink_structure', $permalink_structure );
$this->init();
/**
* Fires after the permalink structure is updated.
*
* @since 2.8.0
*
* @param string $old_permalink_structure The previous permalink structure.
* @param string $permalink_structure The new permalink structure.
*/
do_action( 'permalink_structure_changed', $old_permalink_structure, $permalink_structure );
}
}
/**
* Sets the category base for the category permalink.
*
* Will update the 'category_base' option, if there is a difference between
* the current category base and the parameter value. Calls WP_Rewrite::init()
* after the option is updated.
*
* @since 1.5.0
*
* @param string $category_base Category permalink structure base.
*/
public function set_category_base( $category_base ) {
if ( get_option( 'category_base' ) !== $category_base ) {
update_option( 'category_base', $category_base );
$this->init();
}
}
/**
* Sets the tag base for the tag permalink.
*
* Will update the 'tag_base' option, if there is a difference between the
* current tag base and the parameter value. Calls WP_Rewrite::init() after
* the option is updated.
*
* @since 2.3.0
*
* @param string $tag_base Tag permalink structure base.
*/
public function set_tag_base( $tag_base ) {
if ( get_option( 'tag_base' ) !== $tag_base ) {
update_option( 'tag_base', $tag_base );
$this->init();
}
}
/**
* Constructor - Calls init(), which runs setup.
*
* @since 1.5.0
*/
public function __construct() {
$this->init();
}
}