HEX
Server: Apache/2.4.65 (Debian)
System: Linux 88f31f35b0b8 6.1.0-38-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.147-1 (2025-08-02) x86_64
User: www-data (33)
PHP: 8.2.29
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/breeze/inc/class-breeze-protected-urls-index.php
<?php
/**
 * Keep an indexed list of password-protected URL paths.
 *
 * This index is written to a small PHP file in `wp-content/breeze-config/`
 * and is consumed by early cache execution logic to avoid expensive post scans.
 *
 * @package Breeze
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Manage password-protected URL path index files.
 */
class Breeze_Protected_Urls_Index {

	/**
	 * Register content lifecycle hooks.
	 */
	public function __construct() {
		add_action( 'init', array( $this, 'ensure_index_file_exists' ), 20 );
		add_action( 'save_post', array( $this, 'handle_save_post' ), 10, 3 );
		add_action( 'deleted_post', array( $this, 'handle_deleted_post' ) );
		add_action( 'trashed_post', array( $this, 'handle_deleted_post' ) );
		add_action( 'untrashed_post', array( $this, 'handle_save_post_by_id' ) );
	}

	/**
	 * Build index once if the file is missing.
	 *
	 * @return void
	 */
	public function ensure_index_file_exists() {
		if ( is_file( self::get_index_file_path() ) ) {
			return;
		}

		self::rebuild_current_blog_index();
	}

	/**
	 * Save/update handler for posts.
	 *
	 * @param int     $post_id Post ID.
	 * @param WP_Post $post    Post object.
	 * @param bool    $update  Whether this is an existing post being updated.
	 * @return void
	 */
	public function handle_save_post( $post_id, $post, $update ) {
		unset( $update );

		if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
			return;
		}

		if ( ! ( $post instanceof WP_Post ) ) {
			$post = get_post( $post_id );
			if ( ! ( $post instanceof WP_Post ) ) {
				return;
			}
		}

		if ( ! $this->should_track_post_type( $post->post_type ) ) {
			$this->remove_post_from_index( $post_id );
			return;
		}

		$is_published          = ( 'publish' === $post->post_status );
		$is_password_protected = ! empty( $post->post_password );

		if ( $is_published && $is_password_protected ) {
			$path = $this->get_post_path( $post_id );
			if ( '' !== $path ) {
				$this->upsert_post_path( $post_id, $path );
				return;
			}
		}

		$this->remove_post_from_index( $post_id );
	}

	/**
	 * Update handler when only post ID is available.
	 *
	 * @param int $post_id Post ID.
	 * @return void
	 */
	public function handle_save_post_by_id( $post_id ) {
		$post = get_post( $post_id );
		if ( ! ( $post instanceof WP_Post ) ) {
			return;
		}

		$this->handle_save_post( $post_id, $post, true );
	}

	/**
	 * Remove post from index on deletion-like events.
	 *
	 * @param int $post_id Post ID.
	 * @return void
	 */
	public function handle_deleted_post( $post_id ) {
		$this->remove_post_from_index( $post_id );
	}

	/**
	 * Rebuild index for the current blog from scratch.
	 *
	 * @return void
	 */
	public static function rebuild_current_blog_index() {
		$post_ids = get_posts(
			array(
				'post_type'      => get_post_types( array( 'public' => true ), 'names' ),
				'post_status'    => 'publish',
				'posts_per_page' => -1,
				'has_password'   => true,
				'fields'         => 'ids',
				'no_found_rows'  => true,
			)
		);

		$paths_by_post_id = array();
		foreach ( $post_ids as $post_id ) {
			$path = self::normalize_path( self::get_post_path_static( (int) $post_id ) );
			if ( '' !== $path ) {
				$paths_by_post_id[ (int) $post_id ] = $path;
			}
		}

		self::write_index( $paths_by_post_id );
	}

	/**
	 * Delete index files on uninstall.
	 *
	 * @return void
	 */
	public static function delete_index_files_on_uninstall() {
		$index_files = glob( trailingslashit( WP_CONTENT_DIR ) . 'breeze-config/breeze-protected-urls*.php' );
		if ( empty( $index_files ) ) {
			return;
		}

		foreach ( $index_files as $index_file ) {
			if ( is_file( $index_file ) ) {
				@unlink( $index_file );
			}
		}
	}

	/**
	 * Check if a post type should be indexed.
	 *
	 * @param string $post_type Post type.
	 * @return bool
	 */
	private function should_track_post_type( $post_type ) {
		$post_type_object = get_post_type_object( $post_type );
		if ( empty( $post_type_object ) ) {
			return false;
		}

		if ( empty( $post_type_object->public ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Insert or update a post path in index.
	 *
	 * @param int    $post_id Post ID.
	 * @param string $path    Normalized path.
	 * @return void
	 */
	private function upsert_post_path( $post_id, $path ) {
		$paths_by_post_id = self::read_index();
		$post_id          = (int) $post_id;
		$normalized_path  = self::normalize_path( $path );

		if ( isset( $paths_by_post_id[ $post_id ] ) && $paths_by_post_id[ $post_id ] === $normalized_path ) {
			return;
		}

		$paths_by_post_id[ $post_id ] = $normalized_path;
		self::write_index( $paths_by_post_id );
	}

	/**
	 * Remove post from index map.
	 *
	 * @param int $post_id Post ID.
	 * @return void
	 */
	private function remove_post_from_index( $post_id ) {
		$paths_by_post_id = self::read_index();
		$post_id          = (int) $post_id;

		if ( ! isset( $paths_by_post_id[ $post_id ] ) ) {
			return;
		}

		unset( $paths_by_post_id[ $post_id ] );
		self::write_index( $paths_by_post_id );
	}

	/**
	 * Read index map from file.
	 *
	 * @return array<int,string>
	 */
	private static function read_index() {
		$index_file = self::get_index_file_path();
		if ( ! is_file( $index_file ) ) {
			return array();
		}

		$index_payload = include $index_file;
		if ( ! is_array( $index_payload ) ) {
			return array();
		}

		if ( isset( $index_payload['paths_by_post_id'] ) && is_array( $index_payload['paths_by_post_id'] ) ) {
			return $index_payload['paths_by_post_id'];
		}

		return array();
	}

	/**
	 * Persist index map atomically.
	 *
	 * @param array<int,string> $paths_by_post_id Map of post ID to normalized path.
	 * @return void
	 */
	private static function write_index( array $paths_by_post_id ) {
		$index_file = self::get_index_file_path();
		$index_dir  = dirname( $index_file );

		if ( ! wp_mkdir_p( $index_dir ) ) {
			return;
		}

		$payload = array(
			'paths_by_post_id' => $paths_by_post_id,
		);

		$file_contents = "<?php\nreturn " . var_export( $payload, true ) . ";\n";
		$temp_file     = $index_file . '.' . getmypid() . '.tmp';

		if ( false === file_put_contents( $temp_file, $file_contents, LOCK_EX ) ) {
			return;
		}

		@rename( $temp_file, $index_file );
	}

	/**
	 * Resolve normalized path for a post.
	 *
	 * @param int $post_id Post ID.
	 * @return string
	 */
	private function get_post_path( $post_id ) {
		return self::get_post_path_static( $post_id );
	}

	/**
	 * Resolve normalized path for a post (static helper).
	 *
	 * @param int $post_id Post ID.
	 * @return string
	 */
	private static function get_post_path_static( $post_id ) {
		$permalink = get_permalink( $post_id );
		if ( empty( $permalink ) ) {
			return '';
		}

		$path = wp_parse_url( $permalink, PHP_URL_PATH );
		if ( ! is_string( $path ) ) {
			return '';
		}

		return self::normalize_path( $path );
	}

	/**
	 * Normalize URL path for stable matching.
	 *
	 * @param string $path Path.
	 * @return string
	 */
	private static function normalize_path( $path ) {
		$decoded_path = rawurldecode( (string) $path );
		$trimmed_path = rtrim( $decoded_path, '/' );
		if ( '' === $trimmed_path ) {
			$trimmed_path = '/';
		}

		if ( function_exists( 'mb_strtolower' ) ) {
			return mb_strtolower( $trimmed_path );
		}

		return strtolower( $trimmed_path );
	}

	/**
	 * Resolve index file path for current blog.
	 *
	 * @return string
	 */
	private static function get_index_file_path() {
		$config_dir = trailingslashit( WP_CONTENT_DIR ) . 'breeze-config';
		$filename   = 'breeze-protected-urls.php';

		if ( is_multisite() ) {
			$blog_id = function_exists( 'get_current_blog_id' ) ? (int) get_current_blog_id() : 0;
			if ( ! empty( $blog_id ) ) {
				$filename = 'breeze-protected-urls-' . $blog_id . '.php';
			}
		}

		return $config_dir . '/' . $filename;
	}
}