<?php

use WP_CLI\Utils;

/**
 * Lists or flushes the site's rewrite rules, updates the permalink structure.
 *
 * See the WordPress [Rewrite API](https://codex.wordpress.org/Rewrite_API) and
 * [WP Rewrite](https://codex.wordpress.org/Class_Reference/WP_Rewrite) class reference.
 *
 * ## EXAMPLES
 *
 *     # Flush rewrite rules
 *     $ wp rewrite flush
 *     Success: Rewrite rules flushed.
 *
 *     # Update permalink structure
 *     $ wp rewrite structure '/%year%/%monthnum%/%postname%'
 *     Success: Rewrite structure set.
 *
 *     # List rewrite rules
 *     $ wp rewrite list --format=csv
 *     match,query,source
 *     ^wp-json/?$,index.php?rest_route=/,other
 *     ^wp-json/(.*)?,index.php?rest_route=/$matches[1],other
 *     category/(.+?)/feed/(feed|rdf|rss|rss2|atom)/?$,index.php?category_name=$matches[1]&feed=$matches[2],category
 *     category/(.+?)/(feed|rdf|rss|rss2|atom)/?$,index.php?category_name=$matches[1]&feed=$matches[2],category
 *     category/(.+?)/embed/?$,index.php?category_name=$matches[1]&embed=true,category
 *
 * @package wp-cli
 */

class Rewrite_Command extends WP_CLI_Command {

	/**
	 * Flushes rewrite rules.
	 *
	 * Resets WordPress' rewrite rules based on registered post types, etc.
	 *
	 * To regenerate a .htaccess file with WP-CLI, you'll need to add the mod_rewrite module
	 * to your wp-cli.yml or config.yml. For example:
	 *
	 * ```
	 * apache_modules:
	 *   - mod_rewrite
	 * ```
	 *
	 * ## OPTIONS
	 *
	 * [--hard]
	 * : Perform a hard flush - update `.htaccess` rules as well as rewrite rules in database. Works only on single site installs.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp rewrite flush
	 *     Success: Rewrite rules flushed.
	 */
	public function flush( $args, $assoc_args ) {
		// make sure we detect mod_rewrite if configured in apache_modules in config
		self::apache_modules();

		if ( Utils\get_flag_value( $assoc_args, 'hard' ) && ! in_array( 'mod_rewrite', (array) WP_CLI::get_config( 'apache_modules' ), true ) ) {
			WP_CLI::warning( 'Regenerating a .htaccess file requires special configuration. See usage docs.' );
		}

		if ( Utils\get_flag_value( $assoc_args, 'hard' ) && is_multisite() ) {
			WP_CLI::warning( "WordPress can't generate .htaccess file for a multisite install." );
		}

		self::check_skip_plugins_themes();

		flush_rewrite_rules( Utils\get_flag_value( $assoc_args, 'hard' ) );

		if ( ! get_option( 'rewrite_rules' ) ) {
			WP_CLI::warning( "Rewrite rules are empty, possibly because of a missing permalink_structure option. Use 'wp rewrite list' to verify, or 'wp rewrite structure' to update permalink_structure." );
		} else {
			WP_CLI::success( 'Rewrite rules flushed.' );
		}
	}

	/**
	 * Updates the permalink structure.
	 *
	 * Sets the post permalink structure to the specified pattern.
	 *
	 * To regenerate a .htaccess file with WP-CLI, you'll need to add
	 * the mod_rewrite module to your [WP-CLI config](https://make.wordpress.org/cli/handbook/config/#config-files).
	 * For example:
	 *
	 * ```
	 * apache_modules:
	 *   - mod_rewrite
	 * ```
	 *
	 * ## OPTIONS
	 *
	 * <permastruct>
	 * : The new permalink structure to apply.
	 *
	 * [--category-base=<base>]
	 * : Set the base for category permalinks, i.e. '/category/'.
	 *
	 * [--tag-base=<base>]
	 * : Set the base for tag permalinks, i.e. '/tag/'.
	 *
	 * [--hard]
	 * : Perform a hard flush - update `.htaccess` rules as well as rewrite rules in database.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp rewrite structure '/%year%/%monthnum%/%postname%/'
	 *     Success: Rewrite structure set.
	 */
	public function structure( $args, $assoc_args ) {
		global $wp_rewrite;

		// copypasta from /wp-admin/options-permalink.php

		$blog_prefix = '';
		$prefix      = $blog_prefix;
		if ( is_multisite() && ! is_subdomain_install() && is_main_site() ) {
			$blog_prefix = '/blog';
		}

		$permalink_structure = ( 'default' === $args[0] ) ? '' : $args[0];

		if ( ! empty( $permalink_structure ) ) {
			$permalink_structure = preg_replace( '#/+#', '/', '/' . str_replace( '#', '', $permalink_structure ) );
			if ( $prefix && $blog_prefix ) {
				$permalink_structure = $prefix . preg_replace( '#^/?index\.php#', '', $permalink_structure );
			} else {
				$permalink_structure = $blog_prefix . $permalink_structure;
			}
		}
		$wp_rewrite->set_permalink_structure( $permalink_structure );

		// Update category or tag bases
		if ( isset( $assoc_args['category-base'] ) ) {

			$category_base = $assoc_args['category-base'];
			if ( ! empty( $category_base ) ) {
				$category_base = $blog_prefix . preg_replace( '#/+#', '/', '/' . str_replace( '#', '', $category_base ) );
			}
			$wp_rewrite->set_category_base( $category_base );
		}

		if ( isset( $assoc_args['tag-base'] ) ) {

			$tag_base = $assoc_args['tag-base'];
			if ( ! empty( $tag_base ) ) {
				$tag_base = $blog_prefix . preg_replace( '#/+#', '/', '/' . str_replace( '#', '', $tag_base ) );
			}
			$wp_rewrite->set_tag_base( $tag_base );
		}

		// make sure we detect mod_rewrite if configured in apache_modules in config
		self::apache_modules();

		WP_CLI::success( 'Rewrite structure set.' );

		// Launch a new process to flush rewrites because core expects flush
		// to happen after rewrites are set
		$new_assoc_args = [];
		$cmd            = 'rewrite flush';
		if ( Utils\get_flag_value( $assoc_args, 'hard' ) ) {
			$cmd                   .= ' --hard';
			$new_assoc_args['hard'] = true;
			if ( ! in_array( 'mod_rewrite', (array) WP_CLI::get_config( 'apache_modules' ), true ) ) {
				WP_CLI::warning( 'Regenerating a .htaccess file requires special configuration. See usage docs.' );
			}
		}

		$process_run = WP_CLI::runcommand( $cmd );
		if ( ! empty( $process_run->stderr ) ) {
			// Strip "Warning: "
			WP_CLI::warning( substr( $process_run->stderr, 9 ) );
		}
	}

	/**
	 * Gets a list of the current rewrite rules.
	 *
	 * ## OPTIONS
	 *
	 * [--match=<url>]
	 * : Show rewrite rules matching a particular URL.
	 *
	 * [--source=<source>]
	 * : Show rewrite rules from a particular source.
	 *
	 * [--fields=<fields>]
	 * : Limit the output to specific fields. Defaults to match,query,source.
	 *
	 * [--format=<format>]
	 * : Render output in a particular format.
	 * ---
	 * default: table
	 * options:
	 *   - table
	 *   - csv
	 *   - json
	 *   - count
	 *   - yaml
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp rewrite list --format=csv
	 *     match,query,source
	 *     ^wp-json/?$,index.php?rest_route=/,other
	 *     ^wp-json/(.*)?,index.php?rest_route=/$matches[1],other
	 *     category/(.+?)/feed/(feed|rdf|rss|rss2|atom)/?$,index.php?category_name=$matches[1]&feed=$matches[2],category
	 *     category/(.+?)/(feed|rdf|rss|rss2|atom)/?$,index.php?category_name=$matches[1]&feed=$matches[2],category
	 *     category/(.+?)/embed/?$,index.php?category_name=$matches[1]&embed=true,category
	 *
	 * @subcommand list
	 */
	public function list_( $args, $assoc_args ) {
		global $wp_rewrite;

		$rules = get_option( 'rewrite_rules' );
		if ( ! $rules ) {
			$rules = [];
			WP_CLI::warning( 'No rewrite rules.' );
		}

		self::check_skip_plugins_themes();

		$defaults   = [
			'source' => '',
			'match'  => '',
			'format' => 'table',
			'fields' => 'match,query,source',
		];
		$assoc_args = array_merge( $defaults, $assoc_args );

		if ( ! empty( $assoc_args['match'] ) ) {
			if ( 0 === stripos( $assoc_args['match'], 'http://' )
				|| 0 === stripos( $assoc_args['match'], 'https://' ) ) {
				$bits                = WP_CLI\Utils\parse_url( $assoc_args['match'] );
				$assoc_args['match'] = ( isset( $bits['path'] ) ? $bits['path'] : '' )
					. ( isset( $bits['query'] ) ? '?' . $bits['query'] : '' );
			}
		}

		$rewrite_rules_by_source             = [];
		$rewrite_rules_by_source['post']     = $wp_rewrite->generate_rewrite_rules( $wp_rewrite->permalink_structure, EP_PERMALINK );
		$rewrite_rules_by_source['date']     = $wp_rewrite->generate_rewrite_rules( $wp_rewrite->get_date_permastruct(), EP_DATE );
		$rewrite_rules_by_source['root']     = $wp_rewrite->generate_rewrite_rules( $wp_rewrite->root . '/', EP_ROOT );
		$rewrite_rules_by_source['comments'] = $wp_rewrite->generate_rewrite_rules( $wp_rewrite->root . $wp_rewrite->comments_base, EP_COMMENTS, true, true, true, false );
		$rewrite_rules_by_source['search']   = $wp_rewrite->generate_rewrite_rules( $wp_rewrite->get_search_permastruct(), EP_SEARCH );
		$rewrite_rules_by_source['author']   = $wp_rewrite->generate_rewrite_rules( $wp_rewrite->get_author_permastruct(), EP_AUTHORS );
		$rewrite_rules_by_source['page']     = $wp_rewrite->page_rewrite_rules();

		// Extra permastructs including tags, categories, etc.
		foreach ( $wp_rewrite->extra_permastructs as $permastructname => $permastruct ) {
			if ( is_array( $permastruct ) ) {
				$rewrite_rules_by_source[ $permastructname ] = $wp_rewrite->generate_rewrite_rules( $permastruct['struct'], $permastruct['ep_mask'], $permastruct['paged'], $permastruct['feed'], $permastruct['forcomments'], $permastruct['walk_dirs'], $permastruct['endpoints'] );
			} else {
				$rewrite_rules_by_source[ $permastructname ] = $wp_rewrite->generate_rewrite_rules( $permastruct, EP_NONE );
			}
		}

		// Apply the filters used in core just in case
		foreach ( $rewrite_rules_by_source as $source => $source_rules ) {
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Can't prefix dynamic hooks here, calling hooks for custom permastructs.
			$rewrite_rules_by_source[ $source ] = apply_filters( $source . '_rewrite_rules', $source_rules );
			if ( 'post_tag' === $source ) {
				if ( Utils\wp_version_compare( '3.1.0', '>=' ) ) {
					// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook.
					$rewrite_rules_by_source[ $source ] = apply_filters( 'post_tag_rewrite_rules', $source_rules );
				} else {
					// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook.
					$rewrite_rules_by_source[ $source ] = apply_filters( 'tag_rewrite_rules', $source_rules );
				}
			}
		}

		$rule_list = [];
		foreach ( $rules as $match => $query ) {

			if ( ! empty( $assoc_args['match'] ) && ! preg_match( "!^$match!", trim( $assoc_args['match'], '/' ) ) ) {
				continue;
			}

			$source = 'other';
			foreach ( $rewrite_rules_by_source as $rules_source => $source_rules ) {
				if ( array_key_exists( $match, $source_rules ) ) {
					$source = $rules_source;
				}
			}

			if ( ! empty( $assoc_args['source'] ) && $source !== $assoc_args['source'] ) {
				continue;
			}

			$rule_list[] = compact( 'match', 'query', 'source' );
		}

		Utils\format_items( $assoc_args['format'], $rule_list, explode( ',', $assoc_args['fields'] ) );
	}

	/**
	 * Exposes apache modules if present in config
	 *
	 * Implementation Notes: This function exposes a global function
	 * apache_get_modules and also sets the $is_apache global variable.
	 *
	 * This is so that flush_rewrite_rules will actually write out the
	 * .htaccess file for apache WordPress installations. There is a check
	 * to see:
	 *
	 * 1. if the $is_apache variable is set.
	 * 2. if the mod_rewrite module is returned from the apache_get_modules
	 *    function.
	 *
	 * To get this to work with wp-cli you'll need to add the mod_rewrite module
	 * to your config.yml. For example
	 *
	 * ```
	 * apache_modules:
	 *   - mod_rewrite
	 * ```
	 *
	 * If this isn't done then the .htaccess rewrite rules won't be flushed out
	 * to disk.
	 */
	private static function apache_modules() {
		$mods = WP_CLI::get_config( 'apache_modules' );
		if ( ! empty( $mods ) && ! function_exists( 'apache_get_modules' ) ) {
			global $is_apache;
			// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
			$is_apache = true;

			// needed for get_home_path() and .htaccess location
			$_SERVER['SCRIPT_FILENAME'] = ABSPATH;

			function apache_get_modules() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals
				return WP_CLI::get_config( 'apache_modules' );
			}
		}
	}

	/**
	 * Displays a warning if --skip-plugins or --skip-themes are in use.
	 *
	 * Skipping the loading of plugins or themes can mean some rewrite rules
	 * are unregistered, which may cause erroneous behavior.
	 */
	private static function check_skip_plugins_themes() {
		$skipped = [];
		if ( WP_CLI::get_config( 'skip-plugins' ) ) {
			$skipped[] = 'plugins';
		}
		if ( WP_CLI::get_config( 'skip-themes' ) ) {
			$skipped[] = 'themes';
		}
		if ( empty( $skipped ) ) {
			return;
		}
		$skipped = implode( ' and ', $skipped );
		WP_CLI::warning( sprintf( "Some rewrite rules may be missing because %s weren't loaded by WP-CLI.", $skipped ) );
	}
}

WP_CLI::add_command( 'rewrite', 'Rewrite_Command' );
