<?php
/**
 * Private storage for PHP snippet include files.
 *
 * Design (fail closed):
 * - Preferred: writable directory outside DOCUMENT_ROOT / ABSPATH / wp-content.
 * - Never: publicly downloadable source under wp-content (Nginx ignores .htaccess).
 * - If no private directory is available, return empty — snippets keep running from the DB.
 *
 * @package CodeSnippet
 */

namespace CodeSnippet;

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

final class SnippetPrivateStorage {

	public const LEAF = 'wp-powersuite-code-snippets';

	/** @deprecated Legacy web-mapped path — never used for new writes; purged when present. */
	public const FALLBACK_LEAF = 'wpps/wpps-snippets-exec';

	public const FILE_EXT = 'wpps';

	/** Absolute directory for PHP includes, or empty when unavailable. */
	public static function getDir(): string {
		static $dir = null;
		if ( is_string( $dir ) ) {
			return $dir;
		}

		self::purgeLegacyWebFallbackDir();

		$resolved = self::resolvePreferredDir();
		if ( $resolved === '' ) {
			$dir = '';
			return $dir;
		}

		self::hardenDirectory( $resolved );
		$dir = $resolved;
		return $dir;
	}

	/** Whether a private, non-web-mapped directory is available for PHP includes. */
	public static function isAvailable(): bool {
		return self::getDir() !== '';
	}

	/**
	 * @deprecated Use ! isAvailable(). Always false — web-mapped fallback was removed.
	 */
	public static function isUsingWebAccessibleFallback(): bool {
		return false;
	}

	public static function configurationHelpMessage(): string {
		return __(
			'To enable file-based PHP/HTML loading, define a writable private directory outside the public site in wp-config.php: define( \'WP_POWERSUITE_SNIPPET_PATH\', \'/path/to/wp-powersuite-code-snippets\' );',
			'wp-powersuite'
		);
	}

	/**
	 * Whether $path is inside the managed private snippet directory.
	 */
	public static function isPathAllowed( string $path ): bool {
		$base = self::getDir();
		if ( $base === '' ) {
			return false;
		}
		$resolved_file = realpath( $path );
		$resolved_dir  = realpath( $base );
		if ( ! $resolved_file || ! $resolved_dir ) {
			return false;
		}
		$file = wp_normalize_path( $resolved_file );
		$dir  = trailingslashit( wp_normalize_path( $resolved_dir ) );
		return strcasecmp( substr( $file, 0, strlen( $dir ) ), $dir ) === 0;
	}

	public static function legacyPublicPhpDir(): string {
		return wp_normalize_path( WP_CONTENT_DIR . '/wpps/wpps-snippets' );
	}

	/** Absolute path to the retired web-mapped exec dir (for cleanup only). */
	public static function legacyWebFallbackDir(): string {
		return wp_normalize_path( WP_CONTENT_DIR . '/' . self::FALLBACK_LEAF );
	}

	/** @return string Absolute path, or empty when no preferred candidate works. */
	private static function resolvePreferredDir(): string {
		$candidates = [];

		if ( defined( 'WP_POWERSUITE_SNIPPET_PATH' ) && is_string( WP_POWERSUITE_SNIPPET_PATH ) && WP_POWERSUITE_SNIPPET_PATH !== '' ) {
			$candidates[] = wp_normalize_path( WP_POWERSUITE_SNIPPET_PATH );
		}

		/**
		 * Absolute path override for private PHP snippet storage (empty = ignore).
		 *
		 * @param string $path Absolute path or empty.
		 */
		$filtered = apply_filters( 'wpps_code_snippet_private_storage_path', '' );
		if ( is_string( $filtered ) && $filtered !== '' ) {
			$candidates[] = wp_normalize_path( $filtered );
		}

		$parent = dirname( ABSPATH );
		if ( is_string( $parent ) && $parent !== '' && $parent !== '.' ) {
			$candidates[] = wp_normalize_path( trailingslashit( $parent ) . self::LEAF );
			$grand = dirname( $parent );
			if ( is_string( $grand ) && $grand !== '' && $grand !== $parent && $grand !== '.' ) {
				$candidates[] = wp_normalize_path( trailingslashit( $grand ) . self::LEAF );
			}
		}

		$tmp = wp_normalize_path( trailingslashit( get_temp_dir() ) . self::LEAF );
		$candidates[] = $tmp;

		foreach ( array_unique( $candidates ) as $candidate ) {
			if ( ! self::isAbsolutePath( $candidate ) ) {
				continue;
			}
			if ( self::isInsideWebRoot( $candidate ) ) {
				continue;
			}
			if ( self::ensureWritableDir( $candidate ) ) {
				return $candidate;
			}
		}

		return '';
	}

	/**
	 * Remove any previously written sources from the retired web-mapped fallback.
	 * Idempotent — safe to run on every getDir() so leftovers cannot linger for HTTP download.
	 */
	public static function purgeLegacyWebFallbackDir(): void {
		$dir = self::legacyWebFallbackDir();
		if ( ! is_dir( $dir ) ) {
			return;
		}

		foreach ( glob( trailingslashit( $dir ) . '*.' . self::FILE_EXT ) ?: [] as $file ) {
			if ( is_string( $file ) && is_file( $file ) ) {
				wp_delete_file( $file );
			}
		}
		foreach ( glob( trailingslashit( $dir ) . '*.php' ) ?: [] as $file ) {
			if ( ! is_string( $file ) || ! is_file( $file ) ) {
				continue;
			}
			if ( basename( $file ) === 'index.php' ) {
				continue;
			}
			wp_delete_file( $file );
		}
	}

	private static function ensureWritableDir( string $dir ): bool {
		if ( ! is_dir( $dir ) ) {
			if ( ! wp_mkdir_p( $dir ) ) {
				return false;
			}
		}
		return is_dir( $dir ) && wp_is_writable( $dir );
	}

	private static function hardenDirectory( string $dir ): void {
		static $hardened = [];
		$key = wp_normalize_path( $dir );
		if ( isset( $hardened[ $key ] ) ) {
			return;
		}
		$hardened[ $key ] = true;

		if ( ! is_dir( $dir ) ) {
			wp_mkdir_p( $dir );
		}

		$index = trailingslashit( $dir ) . 'index.php';
		if ( ! file_exists( $index ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
			@file_put_contents( $index, '<?php // Silence is golden.' );
		}

		$htaccess = trailingslashit( $dir ) . '.htaccess';
		$rules    = "# WP PowerSuite Code Snippet: deny all HTTP access\n"
			. "Require all denied\n"
			. "<IfModule !mod_authz_core.c>\n"
			. "\tOrder deny,allow\n"
			. "\tDeny from all\n"
			. "</IfModule>\n";
		if ( ! file_exists( $htaccess ) || @file_get_contents( $htaccess ) !== $rules ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
			@file_put_contents( $htaccess, $rules );
		}

		$webconfig = trailingslashit( $dir ) . 'web.config';
		$iis       = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
			. "<configuration>\n"
			. "\t<system.webServer>\n"
			. "\t\t<authorization>\n"
			. "\t\t\t<deny users=\"*\" />\n"
			. "\t\t</authorization>\n"
			. "\t\t<handlers>\n"
			. "\t\t\t<clear />\n"
			. "\t\t</handlers>\n"
			. "\t</system.webServer>\n"
			. "</configuration>\n";
		if ( ! file_exists( $webconfig ) || @file_get_contents( $webconfig ) !== $iis ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
			@file_put_contents( $webconfig, $iis );
		}

		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- owner-only perms on private snippet storage.
		@chmod( $dir, 0700 );
	}

	private static function isAbsolutePath( string $path ): bool {
		if ( $path === '' ) {
			return false;
		}
		if ( $path[0] === '/' || $path[0] === '\\' ) {
			return true;
		}
		return (bool) preg_match( '#^[A-Za-z]:[/\\\\]#', $path );
	}

	private static function isInsideWebRoot( string $path ): bool {
		$path  = trailingslashit( wp_normalize_path( $path ) );
		$roots = [];
		foreach ( [ ABSPATH, WP_CONTENT_DIR ] as $root ) {
			if ( is_string( $root ) && $root !== '' ) {
				$roots[] = trailingslashit( wp_normalize_path( $root ) );
			}
		}
		if ( ! empty( $_SERVER['DOCUMENT_ROOT'] ) && is_string( $_SERVER['DOCUMENT_ROOT'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$doc = wp_normalize_path( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) );
			if ( $doc !== '' ) {
				$roots[] = trailingslashit( $doc );
			}
		}
		$uploads = wp_upload_dir( null, false );
		if ( is_array( $uploads ) && ! empty( $uploads['basedir'] ) ) {
			$roots[] = trailingslashit( wp_normalize_path( (string) $uploads['basedir'] ) );
		}

		/**
		 * Extra web-mapped roots that private snippet storage must not use.
		 *
		 * @param list<string> $roots Absolute directory roots.
		 */
		$extra = apply_filters( 'wpps_code_snippet_web_accessible_roots', [] );
		if ( is_array( $extra ) ) {
			foreach ( $extra as $root ) {
				if ( is_string( $root ) && $root !== '' ) {
					$roots[] = trailingslashit( wp_normalize_path( $root ) );
				}
			}
		}

		foreach ( array_unique( $roots ) as $root ) {
			if ( $root === '/' || $root === '' ) {
				continue;
			}
			if ( strcasecmp( substr( $path, 0, strlen( $root ) ), $root ) === 0 ) {
				return true;
			}
		}
		return false;
	}
}
