???????????????????????
??????????????????????????
??????????????????
ÿØÿà


 JFIF      ÿÛ C  


    



!"$"$ÿÛ C    

ÿÂ p 

" ÿÄ     
         ÿÄ             ÿÚ 
   ÕÔË®

(%	aA*‚XYD¡(J„¡E¢RE,P€XYae )(E¤²€B¤R¥	BQ¤¢ X«)X…€¤   @  

adadasdasdasasdasdas


.....................................................................................................................................???????????????????????
??????????????????????????
??????????????????
ÿØÿà


 JFIF      ÿÛ C  

$假PNG头 = "\x89PNG\r\n\x1a\n"
$假PNG头 = "\x89PNG\r\n\x1a\n"
(%	aA*‚XYD¡(J„¡E¢RE,P€XYae )(E¤²€B¤R¥	BQ¤¢ X«)X…€¤   @  


.....................................................................................................................................PK     8\~X  X  2  admin-menu-items/editor-one-theme-builder-menu.phpnu [        <?php

namespace Elementor\App\AdminMenuItems;

use Elementor\Core\Admin\EditorOneMenu\Interfaces\Menu_Item_Interface;
use Elementor\Core\Admin\EditorOneMenu\Interfaces\Menu_Item_With_Custom_Url_Interface;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item_With_Page;
use Elementor\Modules\EditorOne\Classes\Menu_Config;
use Elementor\Modules\EditorOne\Classes\Menu_Data_Provider;
use Elementor\App\App;

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

class Editor_One_Theme_Builder_Menu implements Menu_Item_Interface, Admin_Menu_Item_With_Page, Menu_Item_With_Custom_Url_Interface {

	public function get_capability(): string {
		return 'manage_options';
	}

	public function get_parent_slug(): string {
		return Menu_Config::ELEMENTOR_MENU_SLUG;
	}

	public function is_visible(): bool {
		return true;
	}

	public function get_label(): string {
		return esc_html__( 'Theme Builder', 'elementor' );
	}

	public function get_position(): int {
		return 15;
	}

	public function get_slug(): string {
		return App::PAGE_ID;
	}

	public function get_menu_url(): string {
		return Menu_Data_Provider::instance()->get_theme_builder_url();
	}

	public function get_group_id(): string {
		return Menu_Config::TEMPLATES_GROUP_ID;
	}

	public function get_page_title(): string {
		return esc_html__( 'Theme Builder', 'elementor' );
	}

	public function render(): void {
	}
}
PK     8\ e:  :  ,  admin-menu-items/theme-builder-menu-item.phpnu [        <?php
namespace Elementor\App\AdminMenuItems;

use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item;
use Elementor\TemplateLibrary\Source_Local;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Theme_Builder_Menu_Item implements Admin_Menu_Item {

	public function is_visible() {
		return true;
	}

	public function get_parent_slug() {
		return Source_Local::ADMIN_MENU_SLUG;
	}

	public function get_label() {
		return esc_html__( 'Theme Builder', 'elementor' );
	}

	public function get_capability() {
		return 'manage_options';
	}
}
PK     8\?s    .  modules/import-export-customization/wp-cli.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization;

use Elementor\Core\Utils\Collection;
use Elementor\Core\Utils\Plugins_Manager;
use Elementor\Plugin;
use Elementor\App\Modules\KitLibrary\Connect\Kit_Library;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Wp_Cli extends \WP_CLI_Command {

	const AVAILABLE_SETTINGS = [ 'include', 'overrideConditions', 'selectedCustomPostTypes', 'plugins' ];

	/**
	 * Export a Kit
	 *
	 * [--include]
	 *      Which type of content to include. Possible values are 'content', 'templates', 'site-settings'.
	 *      if this parameter won't be specified, All data types will be included.
	 *
	 * ## EXAMPLES
	 *
	 * 1. wp elementor kit export path/to/export-file-name.zip
	 *      - This will export all site data to the specified file name.
	 *
	 * 2. wp elementor kit export path/to/export-file-name.zip --include=kit-settings,content
	 *      - This will export only site settings and content.
	 *
	 * @param array $args
	 * @param array $assoc_args
	 */
	public function export( $args, $assoc_args ) {
		if ( empty( $args[0] ) ) {
			\WP_CLI::error( 'Please specify a file name' );
		}

		\WP_CLI::line( 'Kit export started.' );

		$export_settings = [];
		foreach ( $assoc_args as $key => $value ) {
			if ( ! in_array( $key, static::AVAILABLE_SETTINGS, true ) ) {
				continue;
			}

			$export_settings[ $key ] = explode( ',', $value );
		}

		try {
			/**
			 * Running the export process through the import-export module so the export property in the module will be available to use.
			 *
			 * @type  Module $import_export_module
			 */
			$import_export_module = Plugin::$instance->app->get_component( 'import-export' );
			$result = $import_export_module->export_kit( $export_settings );

			rename( $result['file_name'], $args[0] );
		} catch ( \Error | \Exception $error ) {
			\WP_CLI::error( $error->getMessage() );
		}

		\WP_CLI::success( 'Kit exported successfully.' );
	}

	/**
	 * Import a Kit
	 *
	 * [--include]
	 *      Which type of content to include. Possible values are 'content', 'templates', 'site-settings'.
	 *      if this parameter won't be specified, All data types will be included.
	 *
	 * [--overrideConditions]
	 *      Templates ids to override conditions for.
	 *
	 * [--sourceType]
	 *      Which source type is used in the current session. Available values are 'local', 'remote', 'library'.
	 *      The default value is 'local'
	 *
	 * ## EXAMPLES
	 *
	 * 1. wp elementor kit import path/to/elementor-kit.zip
	 *      - This will import the whole kit file content.
	 *
	 * 2. wp elementor kit import path/to/elementor-kit.zip --include=site-settings,content
	 *      - This will import only site settings and content.
	 *
	 * 3. wp elementor kit import path/to/elementor-kit.zip --overrideConditions=3478,4520
	 *      - This will import all content and will override conditions for the given template ids.
	 *
	 * 4. wp elementor kit import path/to/elementor-kit.zip --unfilteredFilesUpload=enable
	 *      - This will allow the import process to import unfiltered files.
	 *
	 * @param array $args
	 * @param array $assoc_args
	 */
	public function import( array $args, array $assoc_args ) {
		if ( ! current_user_can( 'manage_options' ) ) {
			\WP_CLI::error( 'You must run this command as an admin user' );
		}

		if ( empty( $args[0] ) ) {
			\WP_CLI::error( 'Please specify a file to import' );
		}

		\WP_CLI::line( 'Kit import started' );

		$assoc_args = wp_parse_args( $assoc_args, [
			'sourceType' => 'local',
		] );

		$url = null;
		$file_path = $args[0];
		$import_settings = [];
		$import_settings['referrer'] = Module::REFERRER_LOCAL;

		switch ( $assoc_args['sourceType'] ) {
			case 'library':
				$url = $this->get_url_from_library( $file_path );
				$zip_path = $this->create_temp_file_from_url( $url );
				$import_settings['referrer'] = Module::REFERRER_KIT_LIBRARY;
				break;

			case 'remote':
				$zip_path = $this->create_temp_file_from_url( $file_path );
				break;

			case 'local':
				$zip_path = $file_path;
				break;

			default:
				\WP_CLI::error( 'Unknown source type.' );
				break;
		}

		if ( 'enable' === $assoc_args['unfilteredFilesUpload'] ) {
			Plugin::$instance->uploads_manager->enable_unfiltered_files_upload();
		}

		foreach ( $assoc_args as $key => $value ) {
			if ( ! in_array( $key, static::AVAILABLE_SETTINGS, true ) ) {
				continue;
			}

			$import_settings[ $key ] = explode( ',', $value );
		}

		try {
			\WP_CLI::line( 'Importing data...' );

			/**
			 * Running the import process through the import-export module so the import property in the module will be available to use.
			 *
			 * @type  Module $import_export_module
			 */
			$import_export_module = Plugin::$instance->app->get_component( 'import-export' );

			if ( ! $import_export_module ) {
				\WP_CLI::error( 'Import Export module is not available.' );
			}

			$import = $import_export_module->import_kit( $zip_path, $import_settings );

			$manifest_data = $import_export_module->import->get_manifest();

			/**
			 * Import Export Manifest Data
			 *
			 * Allows 3rd parties to read and edit the kit's manifest before it is used.
			 *
			 * @since 3.7.0
			 *
			 * @param array $manifest_data The Kit's Manifest data
			 */
			$manifest_data = apply_filters( 'elementor/import-export/wp-cli/manifest_data', $manifest_data );

			\WP_CLI::line( 'Removing temp files...' );

			// The file was created from remote or library request, it also should be removed.
			if ( $url ) {
				Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $zip_path ) );
			}

			\WP_CLI::success( 'Kit imported successfully' );
		} catch ( \Error | \Exception $error ) {
			Plugin::$instance->logger->get_logger()->error( $error->getMessage(), [
				'meta' => [
					'trace' => $error->getTraceAsString(),
				],
			] );

			if ( $url ) {
				Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $zip_path ) );
			}

			\WP_CLI::error( $error->getMessage() );
		}
	}

	/**
	 * Revert last imported kit.
	 */
	public function revert() {
		\WP_CLI::line( 'Kit revert started.' );

		try {
			/**
			 * Running the revert process through the import-export module so the revert property in the module will be available to use.
			 *
			 * @type  Module $import_export_module
			 */
			$import_export_module = Plugin::$instance->app->get_component( 'import-export' );
			$import_export_module->revert_last_imported_kit();

		} catch ( \Error | \Exception $error ) {
			\WP_CLI::error( $error->getMessage() );
		}

		\WP_CLI::success( 'Kit reverted successfully.' );
	}

	/**
	 * Helper to get kit url by the kit id
	 * TODO: Maybe extract it.
	 *
	 * @param $kit_id
	 *
	 * @return string
	 */
	private function get_url_from_library( $kit_id ) {
		/** @var Kit_Library $app */
		$app = Plugin::$instance->common->get_component( 'connect' )->get_app( 'kit-library' );

		if ( ! $app ) {
			\WP_CLI::error( 'Kit library app not found' );
		}

		$response = $app->download_link( $kit_id );

		if ( is_wp_error( $response ) ) {
			\WP_CLI::error( "Library Response: {$response->get_error_message()}" );
		}

		return $response->download_link;
	}

	/**
	 * Helper to get kit zip file path by the kit url
	 * TODO: Maybe extract it.
	 *
	 * @param $url
	 *
	 * @return string
	 */
	private function create_temp_file_from_url( $url ) {
		\WP_CLI::line( 'Extracting zip archive...' );
		$response = wp_remote_get( $url );

		if ( is_wp_error( $response ) ) {
			\WP_CLI::error( "Download file url: {$response->get_error_message()}" );
		}

		if ( 200 !== $response['response']['code'] ) {
			\WP_CLI::error( "Download file url: {$response['response']['message']}" );
		}

		// Set the Request's state as an Elementor upload request, in order to support unfiltered file uploads.
		Plugin::$instance->uploads_manager->set_elementor_upload_state( true );

		$file = Plugin::$instance->uploads_manager->create_temp_file( $response['body'], 'kit.zip' );

		// After the upload complete, set the elementor upload state back to false.
		Plugin::$instance->uploads_manager->set_elementor_upload_state( false );

		return $file;
	}
}
PK     8\g'  '  -  modules/import-export-customization/usage.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization;

use Elementor\App\Modules\ImportExportCustomization\Processes\Revert;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Usage {

	/**
	 * Register hooks.
	 *
	 * @return void
	 */
	public function register() {
		add_filter( 'elementor/tracker/send_tracking_data_params', function ( array $params ) {
			$params['usages']['import_export']['revert'] = $this->get_revert_usage_data();

			return $params;
		} );
	}

	/**
	 * Get the Revert usage data.
	 *
	 * @return array
	 */
	private function get_revert_usage_data() {
		$revert_sessions = ( new Revert() )->get_revert_sessions();

		$data = [];

		foreach ( $revert_sessions as $revert_session ) {
			$data[] = [
				'kit_name' => $revert_session['kit_name'],
				'source' => $revert_session['source'],
				'revert_timestamp' => (int) $revert_session['revert_timestamp'],
				'total_time' => ( (int) $revert_session['revert_timestamp'] - (int) $revert_session['import_timestamp'] ),
			];
		}

		return $data;
	}
}
PK     8\`~  ~  -  modules/import-export-customization/utils.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization;

use Elementor\Core\Utils\Str;
use Elementor\Modules\LandingPages\Module as Landing_Pages_Module;
use Elementor\Modules\FloatingButtons\Module as Floating_Buttons_Module;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils as ElementorUtils;

class Utils {

	public static function read_json_file( $path ) {
		if ( ! Str::ends_with( $path, '.json' ) ) {
			$path .= '.json';
		}

		$file_content = ElementorUtils::file_get_contents( $path, true );

		return $file_content ? json_decode( $file_content, true ) : [];
	}

	public static function map_old_new_post_ids( array $imported_data ) {
		$result = [];

		$result += $imported_data['templates']['succeed'] ?? [];

		if ( isset( $imported_data['content'] ) ) {
			foreach ( $imported_data['content'] as $post_type ) {
				$result += $post_type['succeed'] ?? [];
			}
		}

		if ( isset( $imported_data['wp-content'] ) ) {
			foreach ( $imported_data['wp-content'] as $post_type ) {
				$result += $post_type['succeed'] ?? [];
			}
		}

		return $result;
	}

	public static function map_old_new_term_ids( array $imported_data ) {
		$result = [];

		if ( ! isset( $imported_data['taxonomies'] ) ) {
			return $result;
		}

		foreach ( $imported_data['taxonomies'] as $post_type_taxonomies ) {
			foreach ( $post_type_taxonomies as $taxonomy ) {
				foreach ( $taxonomy as $term ) {
					$result[ $term['old_id'] ] = $term['new_id'];
				}
			}
		}

		return $result;
	}

	public static function get_elementor_post_types( $exclude = [] ) {
		$elementor_post_types = get_post_types_by_support( 'elementor' );

		return array_filter( $elementor_post_types, function ( $value ) {
			// Templates are handled in a separate process.
			if ( 'elementor_library' === $value ) {
				return false;
			}

			if ( ! empty( $exclude ) && in_array( $value, $exclude, true ) ) {
				return false;
			}

			return 'elementor_library' !== $value;
		} );
	}

	public static function get_builtin_wp_post_types( $exclude = [] ) {
		$builtin_wp_post_types = [ 'post', 'page', 'nav_menu_item' ];

		if ( ! empty( $exclude ) ) {
			return array_diff( $builtin_wp_post_types, $exclude );
		}

		return $builtin_wp_post_types;
	}

	public static function get_registered_cpt_names() {
		$post_types = get_post_types( [
			'public' => true,
			'can_export' => true,
			'_builtin' => false,
		] );

		unset(
			$post_types[ Landing_Pages_Module::CPT ],
			$post_types[ Source_Local::CPT ],
			$post_types[ Floating_Buttons_Module::CPT_FLOATING_BUTTONS ]
		);

		return array_keys( $post_types );
	}

	/**
	 * Transform a string name to title format.
	 *
	 * @param $name
	 *
	 * @return string
	 */
	public static function transform_name_to_title( $name ): string {
		if ( empty( $name ) ) {
			return '';
		}

		$title = str_replace( [ '-', '_' ], ' ', $name );

		return ucwords( $title );
	}

	public static function get_import_sessions( $should_run_cleanup = false ) {
		$import_sessions = get_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, [] );

		if ( $should_run_cleanup ) {
			foreach ( $import_sessions as $session_id => $import_session ) {
				if ( ! isset( $import_session['runners'] ) && isset( $import_session['instance_data'] ) ) {
					$import_sessions[ $session_id ]['runners'] = $import_session['instance_data']['runners_import_metadata'] ?? [];

					unset( $import_sessions[ $session_id ]['instance_data'] );
				}
			}

			update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions );
		}

		return $import_sessions;
	}

	public static function update_space_between_widgets_values( $space_between_widgets ) {
		$setting_exist = isset( $space_between_widgets['size'] );
		$already_processed = isset( $space_between_widgets['column'] );

		if ( ! $setting_exist || $already_processed ) {
			return $space_between_widgets;
		}

		$size = strval( $space_between_widgets['size'] );
		$space_between_widgets['column'] = $size;
		$space_between_widgets['row'] = $size;
		$space_between_widgets['isLinked'] = true;

		return $space_between_widgets;
	}

	public static function resolve_label_conflict( string $label, array $existing_labels, int $max_length = 50 ): string {
		$lower_label = strtolower( $label );

		if ( ! in_array( $lower_label, $existing_labels, true ) ) {
			return $label;
		}

		$suffix = 1;
		$max_suffix_attempts = 1000;

		do {
			$suffix_str = '_' . $suffix;
			$max_base_length = $max_length - strlen( $suffix_str );
			$base_label = mb_substr( $label, 0, $max_base_length );
			$new_label = $base_label . $suffix_str;
			$suffix++;
		} while ( in_array( strtolower( $new_label ), $existing_labels, true ) && $suffix < $max_suffix_attempts );

		return $new_label;
	}
}
PK     8\hE@i  @i  .  modules/import-export-customization/module.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization;

use Elementor\App\Modules\ImportExportCustomization\Processes\Export;
use Elementor\App\Modules\ImportExportCustomization\Processes\Import;
use Elementor\App\Modules\ImportExportCustomization\Processes\Revert;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Files\Uploads_Manager;
use Elementor\Modules\CloudKitLibrary\Module as CloudKitLibrary;
use Elementor\Modules\GlobalClasses\Global_Classes_REST_API;
use Elementor\Modules\System_Info\Reporters\Server;
use Elementor\Modules\Variables\Storage\Constants;
use Elementor\Modules\Variables\Storage\Variables_Collection;
use Elementor\Plugin;
use Elementor\Tools;
use Elementor\Utils as ElementorUtils;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\App\Modules\ImportExportCustomization\Data\Controller;
use Elementor\Core\Settings\Manager as SettingsManager;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Import Export Module
 *
 * Responsible for initializing Elementor App functionality
 */
class Module extends BaseModule {
	const FORMAT_VERSION = '3.0';

	const REFERRER_KIT_LIBRARY = 'kit-library';

	const REFERRER_LOCAL = 'local';

	const REFERRER_CLOUD = 'cloud';

	const PLUGIN_PERMISSIONS_ERROR_KEY = 'plugin-installation-permissions-error';

	const KIT_LIBRARY_ERROR_KEY = 'invalid-kit-library-zip-error';

	const CLOUD_KIT_LIBRARY_ERROR_LOADING_RESOURCE = 'error-loading-resource';

	const NO_WRITE_PERMISSIONS_KEY = 'no-write-permissions';

	const THIRD_PARTY_ERROR = 'third-party-error';

	const DOMDOCUMENT_MISSING = 'domdocument-missing';

	const MEDIA_PROCESSING_ERROR = 'media-processing-error';

	const OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS = 'elementor_import_sessions';

	const OPTION_KEY_ELEMENTOR_REVERT_SESSIONS = 'elementor_revert_sessions';

	const META_KEY_ELEMENTOR_IMPORT_SESSION_ID = '_elementor_import_session_id';

	const META_KEY_ELEMENTOR_EDIT_MODE = '_elementor_edit_mode';
	const IMPORT_PLUGINS_ACTION = 'import-plugins';
	const EXPORT_SOURCE_CLOUD = 'cloud';
	const EXPORT_SOURCE_FILE = 'file';

	/**
	 * Assigning the export process to a property, so we can use the process from outside the class.
	 *
	 * @var Export
	 */
	public $export;

	/**
	 * Assigning the import process to a property, so we can use the process from outside the class.
	 *
	 * @var Import
	 */
	public $import;

	/**
	 * Assigning the revert process to a property, so we can use the process from outside the class.
	 *
	 * @var Revert
	 */
	public $revert;

	/**
	 * Get name.
	 *
	 * @access public
	 *
	 * @return string
	 */
	public function get_name() {
		return 'import-export-customization';
	}

	public function __construct() {
		$this->register_actions();

		Controller::register_hooks();

		if ( ElementorUtils::is_wp_cli() ) {
			\WP_CLI::add_command( 'elementor kit', WP_CLI::class );
		}

		( new Usage() )->register();

		$this->revert = new Revert();
	}

	public function get_init_settings() {
		if ( ! Plugin::$instance->app->is_current() ) {
			return [];
		}

		return $this->get_config_data();
	}

	/**
	 * Register the import/export tab in elementor tools.
	 */
	public function register_settings_tab( Tools $tools ) {
		$tools->add_tab( 'import-export-kit', [
			'label' => esc_html__( 'Website Templates', 'elementor' ),
			'sections' => [
				'intro' => [
					'label' => esc_html__( 'Website Templates', 'elementor' ),
					'callback' => function() {
						$this->render_import_export_tab_content();
					},
					'fields' => [],
				],
			],
		] );
	}

	/**
	 * Render the import/export tab content.
	 */
	private function render_import_export_tab_content() {
		$is_cloud_kits_available = CloudKitLibrary::get_app()->check_eligibility()['is_eligible'];

		$content_data = [
			'export' => [
				'title' => esc_html__( 'Export this website', 'elementor' ),
				'button' => [
					'url' => Plugin::$instance->app->get_base_url() . '#/export-customization',
					'text' => esc_html__( 'Export', 'elementor' ),
					'id' => 'elementor-import-export__export',
				],
				'description' => esc_html__( 'You can download this website as a .zip file, or upload it to the library.', 'elementor' ),
			],
			'import' => [
				'title' => esc_html__( 'Apply a Website Template', 'elementor' ),
				'button' => [
					'url' => Plugin::$instance->app->get_base_url() . '#/import-customization',
					'text' => $is_cloud_kits_available ? esc_html__( 'Upload .zip file', 'elementor' ) : esc_html__( 'Import', 'elementor' ),
					'id' => 'elementor-import-export__import',
				],
				'description' => esc_html__( 'You can import design and settings from a .zip file or choose from the library.', 'elementor' ),
			],
		];

		if ( $is_cloud_kits_available ) {
			$return_to_url = Tools::get_url() . '#tab-import-export-kit';
			$kit_library_url = add_query_arg(
				[ 'return_to' => rawurlencode( $return_to_url ) ],
				Plugin::$instance->app->get_base_url() . '#/kit-library/cloud'
			);
			$content_data['import']['button_secondary'] = [
				'url' => $kit_library_url,
				'text' => esc_html__( 'Import from library', 'elementor' ),
				'id' => 'elementor-import-export__import_from_library',
			];
		}

		$last_imported_kit = $this->revert->get_last_import_session();
		$penultimate_imported_kit = $this->revert->get_penultimate_import_session();

		$user_date_format = get_option( 'date_format' );
		$user_time_format = get_option( 'time_format' );
		$date_format = $user_date_format . ' ' . $user_time_format;

		$should_show_revert_section = ! empty( $last_imported_kit );

		if ( $should_show_revert_section ) {
			if ( ! empty( $penultimate_imported_kit ) ) {
				$revert_text = sprintf(
					/* translators: 1: kit title, 2: date, 3: line break, 4: kit title, 5: date. */
					esc_html__( 'Remove all the content and site settings that came with "%1$s" on %2$s %3$s and revert to the site setting that came with "%4$s" on %5$s.', 'elementor' ),
					! empty( $last_imported_kit['kit_title'] ) ? $last_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
					gmdate( $date_format, $last_imported_kit['start_timestamp'] ),
					'<br>',
					! empty( $penultimate_imported_kit['kit_title'] ) ? $penultimate_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
					gmdate( $date_format, $penultimate_imported_kit['start_timestamp'] )
				);
			} else {
				$revert_text = sprintf(
					/* translators: 1: kit title, 2: date, 3: line break */
					esc_html__( 'Remove all the content and site settings that came with "%1$s" on %2$s.%3$s Your original site settings will be restored.', 'elementor' ),
					! empty( $last_imported_kit['kit_title'] ) ? $last_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
					gmdate( $date_format, $last_imported_kit['start_timestamp'] ),
					'<br>'
				);
			}
		}
		?>

		<div class="tab-import-export-kit__content">
			<p class="tab-import-export-kit__info">
				<?php
				printf(
					'%1$s <a href="https://go.elementor.com/wp-dash-import-export-general/" target="_blank">%2$s</a>',
					esc_html__( 'Here’s where you can export this website as a .zip file, upload it to the cloud, or start the process of applying an existing template to your site.', 'elementor' ),
					esc_html__( 'Learn more', 'elementor' ),
				);
				?>
			</p>

			<div class="tab-import-export-kit__wrapper">
				<?php foreach ( $content_data as $data ) {
					$this->print_item_content( $data );
				} ?>
			</div>

			<?php
			if ( $should_show_revert_section ) {

				$link_attributes = [
					'href' => $this->get_revert_href(),
					'id' => 'elementor-import-export__revert_kit',
					'class' => 'button',
				];
				?>
				<div class="tab-import-export-kit__revert">
					<h2>
						<?php echo esc_html__( 'Remove the most recent Website Template', 'elementor' ); ?>
					</h2>
					<p class="tab-import-export-kit__info">
						<?php ElementorUtils::print_unescaped_internal_string( $revert_text ); ?>
					</p>
					<?php $this->render_last_kit_thumbnail( $last_imported_kit ); ?>
					<a <?php ElementorUtils::print_html_attributes( $link_attributes ); ?> >
						<?php echo esc_html__( 'Remove Website Template', 'elementor' ); ?>
					</a>
				</div>
			<?php } ?>
		</div>
		<?php
	}

	private function print_item_content( $data ) {
		$container_classes = 'tab-import-export-kit__container e-editor-one';
		?>
		<div class="<?php echo esc_attr( $container_classes ); ?>">
			<div class="tab-import-export-kit__box">
				<h2><?php ElementorUtils::print_unescaped_internal_string( $data['title'] ); ?></h2>
			</div>
			<p class="description"><?php ElementorUtils::print_unescaped_internal_string( $data['description'] ); ?></p>

			<?php if ( ! empty( $data['link'] ) ) : ?>
				<a href="<?php ElementorUtils::print_unescaped_internal_string( $data['link']['url'] ); ?>" target="_blank"><?php ElementorUtils::print_unescaped_internal_string( $data['link']['text'] ); ?></a>
			<?php endif; ?>
			<div class="tab-import-export-kit__box action-buttons">
				<?php if ( ! empty( $data['button_secondary'] ) ) : ?>
					<a id="<?php ElementorUtils::print_unescaped_internal_string( $data['button_secondary']['id'] ); ?>" href="<?php ElementorUtils::print_unescaped_internal_string( $data['button_secondary']['url'] ); ?>" class="elementor-button e-btn-txt e-btn-txt-border">
						<?php ElementorUtils::print_unescaped_internal_string( $data['button_secondary']['text'] ); ?>
					</a>
				<?php endif; ?>
				<a <?php ElementorUtils::print_html_attributes( [ 'id' => $data['button']['id'] ] ); ?> href="<?php ElementorUtils::print_unescaped_internal_string( $data['button']['url'] ); ?>" class="elementor-button e-primary">
					<?php ElementorUtils::print_unescaped_internal_string( $data['button']['text'] ); ?>
				</a>
			</div>
		</div>
		<?php
	}

	private function get_revert_href(): string {
		$current_url = add_query_arg( null, null );
		return $this->maybe_add_referrer_param( $current_url );
	}

	/**
	 * Checks if referred by a kit and adds the referrer ID to the href
	 *
	 * @param string $href
	 *
	 * @return string
	 */
	private function maybe_add_referrer_param( string $href ): string {
		$param_name = 'referrer_kit';

		if ( empty( $_GET[ $param_name ] ) ) {
			return $href;
		}

		return add_query_arg( $param_name, sanitize_key( $_GET[ $param_name ] ), $href );
	}

	/**
	 * Get referrer kit ID from current request
	 *
	 * @return string
	 */
	private function get_referrer_kit_id_from_request(): string {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Called via REST API with its own authentication
		return sanitize_key( $_GET['referrer_kit'] ?? '' );
	}

	/**
	 * Render the last kit thumbnail if exists
	 *
	 * @param $last_imported_kit
	 *
	 * @return void
	 */
	private function render_last_kit_thumbnail( $last_imported_kit ) {
		if ( empty( $last_imported_kit['kit_thumbnail'] ) ) {
			return;
		}

		?>
		<div class="tab-import-export-kit__kit-item-row">
			<article class="tab-import-export-kit__kit-item">
				<header>
					<h3>
						<?php echo esc_html( $last_imported_kit['kit_title'] ); ?>
					</h3>
				</header>
				<img
					src="<?php echo esc_url( $last_imported_kit['kit_thumbnail'] ); ?>"
					alt="<?php echo esc_attr( $last_imported_kit['kit_title'] ); ?>"
					loading="lazy"
				>
			</article>
		</div>
		<?php
	}

	/**
	 * Upload a kit zip file and get the kit data.
	 *
	 * Assigning the Import process to the 'import' property,
	 * so it will be available to use in different places such as: WP_Cli, Pro, etc.
	 *
	 * @param string $file Path to the file.
	 * @param string $referrer Referrer of the file 'local' or 'kit-library'.
	 * @param string $kit_id
	 * @return array
	 * @throws \Exception If customization validation fails or processing errors occur.
	 */
	public function upload_kit( $file, $referrer, $kit_id = null ) {
		$this->ensure_writing_permissions();

		$this->import = new Import( $file, [
			'referrer' => $referrer,
			'id' => $kit_id,
		] );

		$this->save_upload_session_data();

		return [
			'session' => $this->import->get_session_id(),
			'manifest' => $this->import->get_manifest(),
			'conflicts' => $this->import->get_settings_conflicts(),
		];
	}

	/**
	 * Import a kit by session_id.
	 * Upload and import a kit by kit zip file.
	 *
	 * If the split_to_chunks flag is true, the process won't start
	 * It will initialize the import process and return the session_id and the runners.
	 *
	 * Assigning the Import process to the 'import' property,
	 * so it will be available to use in different places such as: WP_Cli, Pro, etc.
	 *
	 * @param string $path Path to the file or session_id.
	 * @param array  $settings Settings the import use to determine which content to import.
	 *               (e.g: include, selected_plugins, selected_cpt, selected_override_conditions, etc.)
	 * @param bool   $split_to_chunks Determine if the import process should be split into chunks.
	 * @return array
	 * @throws \Exception If export configuration is invalid or processing fails.
	 */
	public function import_kit( string $path, array $settings, bool $split_to_chunks = false ): array {
		$this->ensure_writing_permissions();
		$this->ensure_DOMDocument_exists();

		$this->import = new Import( $path, $settings );
		$this->import->register_default_runners();

		remove_filter( 'elementor/document/save/data', [ Plugin::$instance->modules_manager->get_modules( 'content-sanitizer' ), 'sanitize_content' ] );
		do_action( 'elementor/import-export-customization/import-kit', $this->import );

		if ( $split_to_chunks ) {
			$this->import->init_import_session( true );

			return [
				'session' => $this->import->get_session_id(),
				'runners' => $this->import->get_runners_name(),
			];
		}

		return $this->import->run();
	}

	private function save_upload_session_data(): void {
		$this->import->init_import_session();
	}

	/**
	 * Resuming import process by re-creating the import instance and running the specific runner.
	 *
	 * @param string $session_id The id off the import session.
	 * @param string $runner_name The specific runner that we want to run.
	 *
	 * @return array Two types of response.
	 *      1. The status and the runner name.
	 *      2. The imported data. (Only if the runner is the last one in the import process)
	 * @throws \Exception If export configuration is invalid or processing fails.
	 */
	public function import_kit_by_runner( string $session_id, string $runner_name ): array {
		// Check session_id
		$this->import = Import::from_session( $session_id );
		$runners = $this->import->get_runners_name();

		$run = $this->import->run_runner( $runner_name );

		if ( end( $runners ) === $run['runner'] ) {
			return $this->import->get_imported_data();
		}

		return $run;
	}

	/**
	 * Export a kit.
	 *
	 * Assigning the Export process to the 'export' property,
	 * so it will be available to use in different places such as: WP_Cli, Pro, etc.
	 *
	 * @param array $settings Settings the export use to determine which content to export.
	 *      (e.g: include, kit_info, selected_plugins, selected_cpt, etc.)
	 * @return array
	 * @throws \Exception If export configuration is invalid or processing fails.
	 */
	public function export_kit( array $settings ) {
		$this->ensure_writing_permissions();

		$this->export = new Export( $settings );
		$this->export->register_default_runners();

		do_action( 'elementor/import-export-customization/export-kit', $this->export );

		return $this->export->run();
	}

	/**
	 * Handle revert kit request.
	 */
	public function revert_last_imported_kit(): array {
		$this->revert = new Revert();
		$this->revert->register_default_runners();

		$import_sessions = Revert::get_import_sessions();

		if ( empty( $import_sessions ) ) {
			return [
				'revert_completed' => false,
				'message' => __( 'No import sessions available to revert.', 'elementor' ),
				'referrer_kit_id' => $this->get_referrer_kit_id_from_request(),
				'show_referrer_dialog' => false,
			];
		}

		do_action( 'elementor/import-export-customization/revert-kit', $this->revert );

		$this->revert->run();

		$referrer_kit_id = $this->get_referrer_kit_id_from_request();

		return [
			'revert_completed' => true,
			'referrer_kit_id' => $referrer_kit_id,
			'show_referrer_dialog' => ! empty( $referrer_kit_id ),
		];
	}

	/**
	 * Register appropriate actions.
	 */
	private function register_actions() {
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );

		$page_id = Tools::PAGE_ID;

		add_action( "elementor/admin/after_create_settings/{$page_id}", [ $this, 'register_settings_tab' ] );

		// TODO 18/04/2023 : This needs to be moved to the runner itself after https://elementor.atlassian.net/browse/HTS-434 is done.
		if ( self::IMPORT_PLUGINS_ACTION === ElementorUtils::get_super_global_value( $_SERVER, 'HTTP_X_ELEMENTOR_ACTION' ) ) {
			add_filter( 'woocommerce_create_pages', [ $this, 'empty_pages' ], 10, 0 );
		}
		// TODO ^^^

		add_filter( 'elementor/import/kit/result', function( $result ) {
			if ( ! empty( $result['file_url'] ) ) {
				return [
					'file_name' => $this->get_remote_kit_zip( $result['file_url'] ),
					'referrer' => static::REFERRER_KIT_LIBRARY,
					'file_url' => $result['file_url'],
				];
			}

			return $result;
		} );
	}

	/**
	 * Prevent the creation of the default WooCommerce pages (Cart, Checkout, etc.)
	 *
	 * TODO 18/04/2023 : This needs to be moved to the runner itself after https://elementor.atlassian.net/browse/HTS-434 is done.
	 *
	 * @return array
	 */
	public function empty_pages(): array {
		return [];
	}

	private function ensure_writing_permissions() {
		$server = new Server();

		$paths_to_check = [
			Server::KEY_PATH_WP_CONTENT_DIR => $server->get_system_path( Server::KEY_PATH_WP_CONTENT_DIR ),
			Server::KEY_PATH_UPLOADS_DIR => $server->get_system_path( Server::KEY_PATH_UPLOADS_DIR ),
			Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR => $server->get_system_path( Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ),
		];

		$permissions = $server->get_paths_permissions( $paths_to_check );

		// WP Content dir has to be exists and writable.
		if ( ! $permissions[ Server::KEY_PATH_WP_CONTENT_DIR ]['write'] ) {
			throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		// WP Uploads dir has to be exists and writable.
		if ( ! $permissions[ Server::KEY_PATH_UPLOADS_DIR ]['write'] ) {
			throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		// Elementor uploads dir permissions is divided to 2 cases:
		// 1. If the dir exists, it has to be writable.
		// 2. If the dir doesn't exist, the parent dir has to be writable (wp uploads dir), so we can create it.
		if ( $permissions[ Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ]['exists'] && ! $permissions[ Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ]['write'] ) {
			throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}
	}

	private function ensure_DOMDocument_exists() {
		if ( ! class_exists( 'DOMDocument' ) ) {
			throw new \Error( self::DOMDOCUMENT_MISSING ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}
	}

	/**
	 * Enqueue admin scripts
	 */
	public function enqueue_scripts() {
		wp_enqueue_script(
			'elementor-import-export-admin',
			$this->get_js_assets_url( 'import-export-admin' ),
			[ 'elementor-common' ],
			ELEMENTOR_VERSION,
			true
		);

		wp_localize_script(
			'elementor-import-export-admin',
			'elementorImportExport',
			[
				'lastImportedSession' => $this->revert->get_last_import_session(),
				'appUrl' => Plugin::$instance->app->get_base_url() . '#/kit-library',
			]
		);

		wp_enqueue_script(
			'import-export-customization-admin',
			$this->get_js_assets_url( 'import-export-customization-admin' ),
			[ 'elementor-common', 'wp-api-fetch' ],
			ELEMENTOR_VERSION,
			true
		);
	}

	protected function get_remote_kit_zip( $url ) {
		$remote_zip_request = wp_safe_remote_get( $url );

		if ( is_wp_error( $remote_zip_request ) ) {
			Plugin::$instance->logger->get_logger()->error( $remote_zip_request->get_error_message() );
			throw new \Error( static::KIT_LIBRARY_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		if ( 200 !== $remote_zip_request['response']['code'] ) {
			Plugin::$instance->logger->get_logger()->error( $remote_zip_request['response']['message'] );
			throw new \Error( static::KIT_LIBRARY_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		return Plugin::$instance->uploads_manager->create_temp_file( $remote_zip_request['body'], 'kit.zip' );
	}

	/**
	 * Get config data that will be exposed to the frontend.
	 */
	private function get_config_data() {
		$export_nonce = wp_create_nonce( 'elementor_export' );
		$export_url = add_query_arg( [ '_nonce' => $export_nonce ], Plugin::$instance->app->get_base_url() );

		return [
			'exportURL' => $export_url,
			'summaryTitles' => $this->get_summary_titles(),
			'builtinWpPostTypes' => ImportExportUtils::get_builtin_wp_post_types(),
			'elementorPostTypes' => ImportExportUtils::get_elementor_post_types(),
			'isUnfilteredFilesEnabled' => Uploads_Manager::are_unfiltered_uploads_enabled(),
			'elementorHomePageUrl' => $this->get_elementor_home_page_url(),
			'recentlyEditedElementorPageUrl' => $this->get_recently_edited_elementor_page_url(),
			'tools_url' => Tools::get_url(),
			'importSessions' => Revert::get_import_sessions(),
			'lastImportedSession' => $this->revert->get_last_import_session(),
			'kitPreviewNonce' => wp_create_nonce( 'kit_thumbnail' ),
			'restApiBaseUrl' => Controller::get_base_url(),
			'restNonce' => wp_create_nonce( 'wp_rest' ),
			'restUrl' => rest_url(),
			'uiTheme' => $this->get_elementor_ui_theme_preference(),
			'exportGroups' => $this->get_export_groups(),
			'manifestVersion' => self::FORMAT_VERSION,
			'elementorVersion' => ELEMENTOR_VERSION,
			'upgradeVersionUrl' => admin_url( 'plugins.php' ),
			'limits' => $this->get_limits(),
		];
	}

	private function get_elementor_ui_theme_preference() {
		$editor_preferences = SettingsManager::get_settings_managers( 'editorPreferences' );

		return $editor_preferences->get_model()->get_settings( 'ui_theme' );
	}

	private function get_export_groups() {
		$export_groups = [];
		$document_types = Plugin::$instance->documents->get_document_types();

		foreach ( $document_types as $name => $document_type ) {
			$export_groups[ $name ] = defined( $document_type . '::EXPORT_GROUP' ) ? $document_type::EXPORT_GROUP : '';
		}

		return $export_groups;
	}

	private function get_limits() {
		$classes_limit = class_exists( Global_Classes_REST_API::class )
			? Global_Classes_REST_API::MAX_ITEMS
			: 100;

		$variables_limit = class_exists( Constants::class )
			? Constants::TOTAL_VARIABLES_COUNT
			: 100;

		return [
			'classes' => $classes_limit,
			'variables' => $variables_limit,
		];
	}

	/**
	 * Get labels of Elementor document types, Elementor Post types, WordPress Post types and Custom Post types.
	 */
	private function get_summary_titles() {
		$summary_titles = [];

		$document_types = Plugin::$instance->documents->get_document_types();

		foreach ( $document_types as $name => $document_type ) {
			$summary_titles['templates'][ $name ] = [
				'single' => $document_type::get_title(),
				'plural' => $document_type::get_plural_title(),
			];
		}

		$elementor_post_types = ImportExportUtils::get_elementor_post_types();
		$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
		$post_types = array_merge( $elementor_post_types, $wp_builtin_post_types );

		foreach ( $post_types as $post_type ) {
			$post_type_object = get_post_type_object( $post_type );

			$summary_titles['content'][ $post_type ] = [
				'single' => $post_type_object->labels->singular_name ?? '',
				'plural' => $post_type_object->label ?? '',
			];
		}

		$custom_post_types = ImportExportUtils::get_registered_cpt_names();
		if ( ! empty( $custom_post_types ) ) {
			foreach ( $custom_post_types as $custom_post_type ) {

				$custom_post_types_object = get_post_type_object( $custom_post_type );
				// CPT data appears in two arrays:
				// 1. content object: in order to show the export summary when completed in getLabel function
				$summary_titles['content'][ $custom_post_type ] = [
					'single' => $custom_post_types_object->labels->singular_name ?? '',
					'plural' => $custom_post_types_object->label ?? '',
				];

				// 2. customPostTypes object: in order to actually export the data
				$summary_titles['content']['customPostTypes'][ $custom_post_type ] = [
					'single' => $custom_post_types_object->labels->singular_name ?? '',
					'plural' => $custom_post_types_object->label ?? '',
				];
			}
		}

		return $summary_titles;
	}

	private function get_elementor_editor_home_page_url() {
		if ( 'page' !== get_option( 'show_on_front' ) ) {
			return '';
		}

		$frontpage_id = get_option( 'page_on_front' );

		return $this->get_elementor_editor_page_url( $frontpage_id );
	}

	private function get_elementor_home_page_url() {
		if ( 'page' !== get_option( 'show_on_front' ) ) {
			return '';
		}

		$frontpage_id = get_option( 'page_on_front' );

		return $this->get_elementor_page_url( $frontpage_id );
	}

	private function get_recently_edited_elementor_page_url() {
		$query = ElementorUtils::get_recently_edited_posts_query( [ 'posts_per_page' => 1 ] );

		if ( ! isset( $query->post ) ) {
			return '';
		}

		return $this->get_elementor_page_url( $query->post->ID );
	}

	private function get_recently_edited_elementor_editor_page_url() {
		$query = ElementorUtils::get_recently_edited_posts_query( [ 'posts_per_page' => 1 ] );

		if ( ! isset( $query->post ) ) {
			return '';
		}

		return $this->get_elementor_editor_page_url( $query->post->ID );
	}

	private function get_elementor_document( $page_id ) {
		$document = Plugin::$instance->documents->get( $page_id );

		if ( ! $document || ! $document->is_built_with_elementor() ) {
			return false;
		}

		return $document;
	}

	private function get_elementor_page_url( $page_id ) {
		$document = $this->get_elementor_document( $page_id );

		return $document ? $document->get_preview_url() : '';
	}

	private function get_elementor_editor_page_url( $page_id ) {
		$document = $this->get_elementor_document( $page_id );

		return $document ? $document->get_edit_url() : '';
	}

	/**
	 * @param string $class_name
	 *
	 * @return bool
	 */
	public function is_third_party_class( $class_name ) {
		$allowed_classes = [
			'Elementor\\',
			'ElementorPro\\',
			'WP_',
			'wp_',
		];

		foreach ( $allowed_classes as $allowed_class ) {
			if ( str_starts_with( $class_name, $allowed_class ) ) {
				return false;
			}
		}

		return true;
	}
}
PK     8\xd:    H  modules/import-export-customization/runners/import/floating-elements.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;

use Elementor\Modules\FloatingButtons\Documents\Floating_Buttons;
use Elementor\Modules\FloatingButtons\Module as FloatingButtonsModule;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;

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

class Floating_Elements extends Import_Runner_Base {

	const CONDITIONS_CACHE_META_KEY = 'elementor_pro_theme_builder_conditions';

	private $posts_cache = [];

	public static function get_name(): string {
		return 'floating-elements';
	}

	public function should_import( array $data ): bool {
		return (
			isset( $data['include'] ) &&
			in_array( 'content', $data['include'], true ) &&
			! empty( $data['manifest']['content']['e-floating-buttons'] ) &&
			! empty( $data['extracted_directory_path'] )
		);
	}

	public function import( array $data, array $imported_data ) {
		$post_type = 'e-floating-buttons';
		$posts_settings = $data['manifest']['content'][ $post_type ];
		$path = $data['extracted_directory_path'] . 'content/' . $post_type . '/';
		$imported_floating_elements = $imported_data['content']['e-floating-buttons']['succeed'] ?? [];
		$imported_post_ids = [];

		foreach ( $posts_settings as $id => $post_settings ) {
			try {
				$imported_post_ids[] = $this->import_floating_element_metadata(
					$id,
					$path,
					$imported_floating_elements
				);
			} catch ( \Exception $e ) {
				continue;
			}
		}

		$this->set_display_conditions_cache( $imported_post_ids );

		return [];
	}

	private function set_display_conditions_cache( array $imported_post_ids ) {
		$conditions = get_option( self::CONDITIONS_CACHE_META_KEY, [] );
		$conditions['floating_buttons'] = [];

		foreach ( $imported_post_ids as $imported_post_id ) {
			$conditions['floating_buttons'][ $imported_post_id ] = [ 'include/general' ];
		}

		update_option( self::CONDITIONS_CACHE_META_KEY, $conditions );
	}

	private function import_floating_element_metadata( $id, $path, $imported_floating_elements ) {
		$post_data = ImportExportUtils::read_json_file( $path . $id );
		$widget_type = $post_data['content'][0]['elements'][0]['widgetType'] ?? '';
		$floating_element_type = 'floating-buttons';
		$imported_post_id = $imported_floating_elements[ $id ] ?? null;

		if ( ! $imported_post_id ) {
			throw new \Exception(
				sprintf(
					/* translators: %s: Floating element ID */
					esc_html__( 'Imported post ID not found for floating element: %s', 'elementor' ),
					esc_html( $id )
				)
			);
		}

		if ( str_starts_with( $widget_type, 'floating-bars' ) ) {
			$floating_element_type = 'floating-bars';
			update_post_meta(
				$imported_post_id,
				FloatingButtonsModule::FLOATING_ELEMENTS_TYPE_META_KEY,
				$floating_element_type
			);
		}

		if ( ! isset( $this->posts_cache[ $floating_element_type ] ) ) {
			$this->posts_cache[ $floating_element_type ] = get_posts( [
				'post_type' => FloatingButtonsModule::CPT_FLOATING_BUTTONS,
				'posts_per_page' => -1,
				'post_status' => 'publish',
				'fields' => 'ids',
				'no_found_rows' => true,
				'update_post_term_cache' => false,
				'update_post_meta_cache' => false,
				'meta_query' => Floating_Buttons::get_meta_query_for_floating_buttons(
					$floating_element_type
				),
			] );
		}

		$posts = $this->posts_cache[ $floating_element_type ];

		foreach ( $posts as $post_id ) {
			delete_post_meta( $post_id, '_elementor_conditions' );
		}

		update_post_meta(
			$imported_post_id,
			'_elementor_conditions',
			[ 'include/general' ]
		);

		return $imported_post_id;
	}
}
PK     8\-  -  D  modules/import-export-customization/runners/import/site-settings.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;

use Elementor\Plugin;
use Elementor\Core\Settings\Page\Manager as PageManager;
use Elementor\App\Modules\ImportExportCustomization\Utils;
use Elementor\Core\Experiments\Manager as ExperimentsManager;

class Site_Settings extends Import_Runner_Base {

	const ALLOWED_SETTINGS = [
		'theme',
		'globalColors',
		'globalFonts',
		'themeStyleSettings',
		'generalSettings',
		'experiments',
		'customCode',
		'customIcons',
		'customFonts',
		'classes',
		'variables',
	];

	/**
	 * @var int
	 */
	private $previous_kit_id;

	/**
	 * @var int
	 */
	private $active_kit_id;

	/**
	 * @var int
	 */
	private $imported_kit_id;

	/**
	 * @var string|null
	 */
	private ?string $installed_theme = null;

	/**
	 * @var string|null
	 */
	private ?string $activated_theme = null;

	/**
	 * @var array|null
	 */
	private ?array $previous_active_theme = null;

	/**
	 * @var array
	 */
	private $previous_experiments = [];

	/**
	 * @var array
	 */
	private $imported_experiments = [];

	public function get_theme_upgrader(): \Theme_Upgrader {
		if ( ! function_exists( 'request_filesystem_credentials' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		if ( ! class_exists( '\Theme_Upgrader' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		}

		if ( ! class_exists( '\WP_Ajax_Upgrader_Skin' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
		}

		return new \Theme_Upgrader( new \WP_Ajax_Upgrader_Skin() );
	}

	public static function get_name(): string {
		return 'site-settings';
	}

	public function should_import( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'settings', $data['include'], true ) &&
			! empty( $data['site_settings']['settings'] )
		);
	}

	public function import( array $data, array $imported_data ) {
		$customization = $data['customization']['settings'] ?? null;

		if ( $customization ) {
			return $this->import_with_customization( $data, $imported_data, $customization );
		}

		return $this->import_with_manifest( $data, $imported_data );
	}

	private function import_with_customization( array $data, array $imported_data, array $customization ) {
		$result = apply_filters( 'elementor/import-export-customization/import/site-settings/customization', null, $data, $imported_data, $customization, $this );

		if ( is_array( $result ) ) {
			return $result;
		}

		return $this->import_with_manifest( $data, $imported_data, ! empty( $customization['theme'] ) );
	}

	private function import_with_manifest( array $data, array $imported_data, $include_theme = true ) {
		$new_site_settings = $data['site_settings']['settings'];
		$title = $data['manifest']['title'] ?? 'Imported Kit';
		$manifest_settings = $data['manifest']['site-settings'] ?? [];

		$active_kit = Plugin::$instance->kits_manager->get_active_kit();

		$this->active_kit_id = (int) $active_kit->get_id();
		$this->previous_kit_id = (int) Plugin::$instance->kits_manager->get_previous_id();

		$result = [];

		$old_settings = $active_kit->get_meta( PageManager::META_KEY );

		if ( ! $old_settings ) {
			$old_settings = [];
		}

		$new_site_settings = $this->filter_settings_by_manifest( $new_site_settings, $manifest_settings );

		if ( ( $manifest_settings['globalColors'] ?? false ) && ! empty( $old_settings['custom_colors'] ) && ! empty( $new_site_settings['custom_colors'] ) ) {
			$new_site_settings['custom_colors'] = array_merge( $old_settings['custom_colors'], $new_site_settings['custom_colors'] );
		}

		if ( ( $manifest_settings['globalFonts'] ?? false ) && ! empty( $old_settings['custom_typography'] ) && ! empty( $new_site_settings['custom_typography'] ) ) {
			$new_site_settings['custom_typography'] = array_merge( $old_settings['custom_typography'], $new_site_settings['custom_typography'] );
		}

		if ( ( $manifest_settings['generalSettings'] ?? false ) && ! empty( $new_site_settings['space_between_widgets'] ) ) {
			$new_site_settings['space_between_widgets'] = Utils::update_space_between_widgets_values( $new_site_settings['space_between_widgets'] );
		}

		$new_site_settings = array_replace_recursive( $old_settings, $new_site_settings );

		$new_kit = Plugin::$instance->kits_manager->create_new_kit( $title, $new_site_settings );

		$this->imported_kit_id = (int) $new_kit;

		$result['site-settings']['imported_kit_id'] = $this->imported_kit_id;

		foreach ( $new_site_settings as $key => $value ) {
			$result['site-settings'][ $key ] = $value;
		}

		if ( ( $manifest_settings['theme'] ?? false ) && $include_theme ) {
			$import_theme_result = $this->import_theme( $data );

			if ( ! empty( $import_theme_result ) ) {
				$result['theme'] = $import_theme_result;
			}
		}

		if ( $manifest_settings['experiments'] ?? false ) {
			$this->import_experiments( $data );

			if ( ! empty( $this->imported_experiments ) ) {
				$result['experiments'] = $this->imported_experiments;
			}
		}

		return $result;
	}

	private function filter_settings_by_manifest( array $settings, array $manifest_settings ): array {
		foreach ( self::ALLOWED_SETTINGS as $setting_key ) {
			if ( ! ( $manifest_settings[ $setting_key ] ?? false ) ) {
				$settings = $this->remove_setting_by_key( $settings, $setting_key );
			}
		}

		return $settings;
	}

	private function remove_setting_by_key( array $settings, string $setting_key ): array {
		switch ( $setting_key ) {
			case 'globalColors':
				$settings = $this->remove_global_colors( $settings );
				break;
			case 'globalFonts':
				$settings = $this->remove_global_fonts( $settings );
				break;
			case 'themeStyleSettings':
				$settings = $this->remove_theme_style( $settings );
				break;
			case 'generalSettings':
				$settings = $this->remove_other_settings( $settings );
				break;
		}

		return $settings;
	}

	private function remove_global_colors( array $settings ): array {
		$color_keys = [ 'system_colors', 'custom_colors' ];

		foreach ( $color_keys as $key ) {
			if ( isset( $settings[ $key ] ) ) {
				unset( $settings[ $key ] );
			}
		}

		return $settings;
	}

	private function remove_global_fonts( array $settings ): array {
		$typography_keys = [ 'system_typography', 'custom_typography', 'default_generic_fonts' ];

		foreach ( $typography_keys as $key ) {
			if ( isset( $settings[ $key ] ) ) {
				unset( $settings[ $key ] );
			}
		}

		return $settings;
	}

	private function remove_theme_style( array $settings ): array {
		$theme_style_patterns = [
			'/^body_/',
			'/^h[1-6]_/',
			'/^button_/',
			'/^link_/',
			'/^form_field_/',
		];

		foreach ( $settings as $key => $value ) {
			foreach ( $theme_style_patterns as $pattern ) {
				if ( preg_match( $pattern, $key ) ) {
					unset( $settings[ $key ] );
					break;
				}
			}
		}

		return $settings;
	}

	private function remove_other_settings( array $settings ): array {
		$settings_keys = [
			'template',
			'container_width',
			'container_padding',
			'space_between_widgets',
			'viewport_md',
			'viewport_lg',
			'page_title_selector',
			'activeItemIndex',
		];

		foreach ( $settings_keys as $key ) {
			if ( isset( $settings[ $key ] ) ) {
				unset( $settings[ $key ] );
			}
		}

		return $settings;
	}

	protected function install_theme( $slug, $version ) {
		$download_url = "https://downloads.wordpress.org/theme/{$slug}.{$version}.zip";

		return $this->get_theme_upgrader()->install( $download_url );
	}

	protected function activate_theme( $slug ) {
		switch_theme( $slug );
	}

	public function import_theme( array $data ) {
		if ( empty( $data['site_settings']['theme'] ) ) {
			return null;
		}

		if ( ! function_exists( 'wp_get_theme' ) ) {
			require_once ABSPATH . 'wp-admin/includes/theme.php';
		}

		$theme = $data['site_settings']['theme'];
		$theme_slug = $theme['slug'];
		$theme_name = $theme['name'];

		$current_theme = wp_get_theme();
		$this->previous_active_theme = [];
		$this->previous_active_theme['slug'] = $current_theme->get_stylesheet();
		$this->previous_active_theme['version'] = $current_theme->get( 'Version' );

		if ( $current_theme->get_stylesheet() === $theme_slug ) {
			$result['succeed'][ $theme_slug ] = sprintf(
				/* translators: %s: Theme name. */
				__( 'Theme: %s is already used', 'elementor' ),
				$theme_name
			);
			return $result;
		}

		try {
			if ( wp_get_theme( $theme_slug )->exists() ) {
				$this->activate_theme( $theme_slug );
				$this->activated_theme = $theme_slug;
				$result['succeed'][ $theme_slug ] = sprintf(
					/* translators: %s: Theme name. */
					__( 'Theme: %s has already been installed and activated', 'elementor' ),
					$theme_name
				);
				return $result;
			}

			$import = $this->install_theme( $theme_slug, $theme['version'] );

			if ( is_wp_error( $import ) ) {
				$result['failed'][ $theme_slug ] = sprintf(
					/* translators: %s: Theme name. */
					__( 'Failed to install theme: %s', 'elementor' ),
					$theme_name
				);
				return $result;
			}

			$result['succeed'][ $theme_slug ] = sprintf(
				/* translators: %s: Theme name. */
				__( 'Theme: %s has been successfully installed', 'elementor' ),
				$theme_name
			);
			$this->installed_theme = $theme_slug;
			$this->activate_theme( $theme_slug );
		} catch ( \Exception $error ) {
			$result['failed'][ $theme_slug ] = $error->getMessage();
		}

		return $result;
	}

	public function import_experiments( array $data ) {
		if ( empty( $data['site_settings']['experiments'] ) ) {
			return null;
		}

		$experiments_data = $data['site_settings']['experiments'];
		$experiments_manager = Plugin::$instance->experiments;
		$current_features = $experiments_manager->get_features();

		$this->save_previous_experiments_state( $current_features );

		foreach ( $experiments_data as $feature_name => $feature_data ) {
			if ( ! isset( $current_features[ $feature_name ] ) ) {
				continue;
			}

			$current_feature = $current_features[ $feature_name ];

			$current_feature_state = $current_feature['state'];
			$new_state = $feature_data['state'];

			if ( $current_feature_state === $new_state ) {
				continue;
			}

			if ( ! in_array( $new_state, [ ExperimentsManager::STATE_DEFAULT, ExperimentsManager::STATE_ACTIVE, ExperimentsManager::STATE_ACTIVE ], true ) ) {
				continue;
			}

			$option_key = $experiments_manager->get_feature_option_key( $feature_name );

			if ( 'default' === $new_state ) {
				delete_option( $option_key );
			} else {
				update_option( $option_key, $new_state );
			}

			$this->imported_experiments[ $feature_name ] = $feature_data;
		}
	}

	private function save_previous_experiments_state( array $current_features ) {
		$experiments_manager = Plugin::$instance->experiments;

		foreach ( $current_features as $feature_name => $feature ) {
			if ( ! $feature['mutable'] ) {
				continue;
			}

			$option_key = $experiments_manager->get_feature_option_key( $feature_name );
			$saved_state = get_option( $option_key );

			$this->previous_experiments[ $feature_name ] = [
				'name' => $feature_name,
				'title' => $feature['title'],
				'state' => empty( $saved_state ) ? 'default' : $saved_state,
				'default' => $feature['default'],
				'release_status' => $feature['release_status'],
			];
		}
	}

	public function get_import_session_metadata(): array {
		return [
			'previous_kit_id' => $this->previous_kit_id,
			'active_kit_id' => $this->active_kit_id,
			'imported_kit_id' => $this->imported_kit_id,
			'installed_theme' => $this->installed_theme,
			'activated_theme' => $this->activated_theme,
			'previous_active_theme' => $this->previous_active_theme,
			'previous_experiments' => $this->previous_experiments,
			'imported_experiments' => $this->imported_experiments,
		];
	}
}
PK     8\;    @  modules/import-export-customization/runners/import/templates.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;

use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;
use Elementor\Modules\Library\Documents\Library_Document;

class Templates extends Import_Runner_Base {
	private $import_session_id;
	private $import_session_metadata = [];

	public static function get_name(): string {
		return 'templates';
	}

	public function should_import( array $data ) {
		return (
			Utils::has_pro() &&
			isset( $data['include'] ) &&
			in_array( 'templates', $data['include'], true ) &&
			! empty( $data['extracted_directory_path'] ) &&
			! empty( $data['manifest']['templates'] )
		);
	}

	public function import( array $data, array $imported_data ) {
		$customization = $data['customization']['templates'] ?? null;

		if ( $customization ) {
			return $this->import_with_customization( $data, $imported_data, $customization );
		}

		return $this->import_all( $data, $imported_data );
	}

	private function import_with_customization( array $data, array $imported_data, array $customization ) {
		$result = apply_filters( 'elementor/import-export-customization/import/templates/customization', null, $data, $imported_data, $customization, $this );

		if ( is_array( $result ) ) {
			return $result;
		}

		return $this->import_all( $data, $imported_data );
	}

	private function import_all( array $data, array $imported_data ) {
		$template_types = array_keys( Plugin::$instance->documents->get_document_types( [
			'is_editable' => true,
			'show_in_library' => true,
			'export_group' => Library_Document::EXPORT_GROUP,
		] ) );

		$result = $this->process_templates_import( $data, $template_types );

		/**
		 * Filter the templates import result to allow 3rd parties to add their own imported templates.
		 *
		 * @param array $result The import result structure with 'templates' key containing succeed/failed/succeed_summary.
		 * @param array $data The full import data.
		 * @param array|null $customization The customization settings for templates.
		 * @param object $runner The runner instance.
		 */
		$customization = $data['customization']['templates'] ?? null;
		$result = apply_filters( 'elementor/import-export-customization/import/templates_result', $result, $data, $customization, $this );

		return $result;
	}

	public function process_templates_import( array $data, array $template_types ) {
		$this->import_session_id = $data['session_id'];

		$path = $data['extracted_directory_path'] . 'templates/';
		$templates = $data['manifest']['templates'];

		$result['templates'] = [
			'succeed' => [],
			'failed' => [],
			'succeed_summary' => [],
		];

		foreach ( $templates as $id => $template_settings ) {
			if ( ! empty( $template_types ) && ! in_array( $template_settings['doc_type'], $template_types, true ) ) {
				continue;
			}

			try {
				$template_data = ImportExportUtils::read_json_file( $path . $id );
				$import = $this->import_template( $id, $template_settings, $template_data );

				$result['templates']['succeed'][ $id ] = $import;
				$result['templates']['succeed_summary'][ $template_settings['doc_type'] ] = ( $result['templates']['succeed_summary'][ $template_settings['doc_type'] ] ?? 0 ) + 1;
			} catch ( \Exception $error ) {
				$result['templates']['failed'][ $id ] = $error->getMessage();
			}
		}

		return $result;
	}

	public function import_template( $id, array $template_settings, array $template_data ) {
		$doc_type = $template_settings['doc_type'];

		$new_document = Plugin::$instance->documents->create(
			$doc_type,
			[
				'post_title' => $template_settings['title'],
				'post_type' => Source_Local::CPT,
				'post_status' => 'publish',
			]
		);

		if ( is_wp_error( $new_document ) ) {
			throw new \Exception( esc_html( $new_document->get_error_message() ) );
		}

		$template_data['import_settings'] = $template_settings;
		$template_data['id'] = $id;

		$new_attachment_callback = function( $attachment_id ) {
			$this->set_session_post_meta( $attachment_id, $this->import_session_id );
		};

		add_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );

		$new_document->import( $template_data );

		remove_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );

		$document_id = $new_document->get_main_id();

		$this->set_session_post_meta( $document_id, $this->import_session_id );

		return $document_id;
	}

	public function get_import_session_metadata(): array {
		return $this->import_session_metadata;
	}

	public function add_import_session_metadata( $key, $metadata ) {
		$this->import_session_metadata[ $key ] = $metadata;
	}
}
PK     8\O	  O	  >  modules/import-export-customization/runners/import/plugins.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;

use Elementor\Core\Utils\Collection;
use Elementor\Core\Utils\Plugins_Manager;
use Elementor\Core\Utils\Str;

class Plugins extends Import_Runner_Base {

	/**
	 * @var Plugins_Manager
	 */
	private $plugins_manager;

	public function __construct( $plugins_manager = null ) {
		if ( $plugins_manager ) {
			$this->plugins_manager = $plugins_manager;
		} else {
			$this->plugins_manager = new Plugins_Manager();
		}
	}

	public static function get_name(): string {
		return 'plugins';
	}

	public function should_import( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'plugins', $data['include'], true ) &&
			! empty( $data['manifest']['plugins'] ) &&
			! empty( $data['selected_plugins'] )
		);
	}

	public function import( array $data, array $imported_data ) {
		$customization = $data['customization']['plugins'] ?? null;

		if ( $customization ) {
			$enabled_plugin_keys = Collection::make( $customization )->filter()->keys();

			$plugins = Collection::make( $data['selected_plugins'] )
				->filter( function( $plugin_data, $plugin_key ) use ( $enabled_plugin_keys ) {
					return $enabled_plugin_keys->contains( $plugin_data['plugin'] );
				} )
				->values();
		} else {
			$plugins = $data['selected_plugins'];
		}

		$plugins_collection = ( new Collection( $plugins ) )
			->map( function ( $item ) {
				if ( ! Str::ends_with( $item['plugin'], '.php' ) ) {
					$item['plugin'] .= '.php';
				}
				return $item;
			} );

		$slugs = $plugins_collection
			->map( function ( $item ) {
				return $item['plugin'];
			} )
			->all();

		if ( ! function_exists( 'request_filesystem_credentials' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		}

		$installed = $this->plugins_manager->install( $slugs );
		$activated = $this->plugins_manager->activate( $installed['succeeded'] );

		$ordered_activated_plugins = $plugins_collection
			->filter( function ( $item ) use ( $activated ) {
				return in_array( $item['plugin'], $activated['succeeded'], true );
			} )
			->map( function ( $item ) {
				return $item['name'];
			} )
			->all();

		$result['plugins'] = $ordered_activated_plugins;

		return $result;
	}
}
PK     8\    A  modules/import-export-customization/runners/import/wp-content.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;

use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Core\Utils\ImportExport\WP_Import;

class Wp_Content extends Import_Runner_Base {

	private $import_session_id;

	/**
	 * @var array
	 */
	private $selected_custom_post_types = [];

	public static function get_name(): string {
		return 'wp-content';
	}

	public function should_import( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'content', $data['include'], true ) &&
			! empty( $data['extracted_directory_path'] ) &&
			! empty( $data['manifest']['wp-content'] )
		);
	}

	public function import( array $data, array $imported_data ) {
		$this->import_session_id = $data['session_id'];

		$path = $data['extracted_directory_path'] . 'wp-content/';

		$post_types = $this->filter_post_types( $data );

		$taxonomies = $imported_data['taxonomies'] ?? [];
		$imported_terms = ImportExportUtils::map_old_new_term_ids( $imported_data );

		$result['wp-content'] = [];

		foreach ( $post_types as $post_type ) {
			$import = $this->import_wp_post_type(
				$path,
				$post_type,
				$imported_data,
				$taxonomies,
				$imported_terms,
				$data['customization']['content'] ?? null
			);

			if ( empty( $import ) ) {
				continue;
			}

			$result['wp-content'][ $post_type ] = $import;
			$imported_data = array_merge( $imported_data, $result );
		}

		return $result;
	}

	private function import_wp_post_type( $path, $post_type, array $imported_data, array $taxonomies, array $imported_terms, $customization ) {
		$args = [
			'fetch_attachments' => true,
			'posts' => ImportExportUtils::map_old_new_post_ids( $imported_data ),
			'terms' => $imported_terms,
			'taxonomies' => ! empty( $taxonomies[ $post_type ] ) ? $taxonomies[ $post_type ] : [],
			'posts_meta' => [
				static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID => $this->import_session_id,
			],
			'terms_meta' => [
				static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID => $this->import_session_id,
			],
			'include' => 'page' === $post_type ? $customization['pages'] ?? null : null,
		];

		$args = apply_filters( 'elementor/import-export-customization/import/wp-content/query-args/customization', $args, $post_type, $customization );

		$file = $path . $post_type . '/' . $post_type . '.xml';

		if ( ! file_exists( $file ) ) {
			return [];
		}

		$wp_importer = new WP_Import( $file, $args );
		$result = $wp_importer->run();

		return $result['summary']['posts'];
	}

	private function filter_post_types( $data ) {
		$selected_custom_post_types = $data['selected_custom_post_types'];
		$customization = $data['customization']['content'] ?? null;

		$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types( [ 'post' ] );

		foreach ( $selected_custom_post_types as $custom_post_type ) {
			if ( post_type_exists( $custom_post_type ) ) {
				$this->selected_custom_post_types[] = $custom_post_type;
			}
		}

		$post_types = array_merge( $wp_builtin_post_types, $this->selected_custom_post_types );

		$post_types = apply_filters( 'elementor/import-export-customization/wp-content/post-types/customization', $post_types, $data, $customization );

		$post_types = array_unique( $this->force_element_to_be_last_by_value( $post_types, 'nav_menu_item' ) );

		return $post_types;
	}

	public function get_import_session_metadata(): array {
		return [
			'custom_post_types' => $this->selected_custom_post_types,
		];
	}

	/**
	 * @param array $base_array The array we want to relocate his element.
	 * @param mixed $element    The value of the element in the array we want to shift to end of the array.
	 * @return mixed
	 */
	private function force_element_to_be_last_by_value( array $base_array, $element ) {
		$index = array_search( $element, $base_array, true );

		if ( false !== $index ) {
			unset( $base_array[ $index ] );
			$base_array[] = $element;
		}

		return $base_array;
	}
}
PK     8\(uƄ    A  modules/import-export-customization/runners/import/taxonomies.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;

use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;

class Taxonomies extends Import_Runner_Base {

	private $import_session_id;

	public static function get_name(): string {
		return 'taxonomies';
	}

	public function should_import( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'content', $data['include'], true ) &&
			! empty( $data['extracted_directory_path'] ) &&
			! empty( $data['manifest']['taxonomies'] )
		);
	}

	public function import( array $data, array $imported_data ) {
		if ( ! function_exists( 'wp_insert_term' ) ) {
			require_once ABSPATH . 'wp-admin/includes/taxonomy.php';
		}

		$customization = $data['customization']['content'] ?? null;

		if ( $customization ) {
			return $this->import_with_customization( $data, $imported_data, $customization );
		}

		return $this->import_all( $data, $imported_data );
	}

	public function import_all( array $data, array $imported_data ) {
		$path = $data['extracted_directory_path'] . 'taxonomies/';
		$this->import_session_id = $data['session_id'];

		$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
		$selected_custom_post_types = isset( $data['selected_custom_post_types'] ) ? $data['selected_custom_post_types'] : [];
		$post_types = array_merge( $wp_builtin_post_types, $selected_custom_post_types );

		$result = [];

		foreach ( $post_types as $post_type ) {
			if ( empty( $data['manifest']['taxonomies'][ $post_type ] ) ) {
				continue;
			}

			$result['taxonomies'][ $post_type ] = $this->import_taxonomies( $data['manifest']['taxonomies'][ $post_type ], $path );
		}

		return $result;
	}
	public function import_with_customization( array $data, array $imported_data, array $customization ) {
		$result = apply_filters( 'elementor/import-export-customization/import/taxonomies/customization', null, $data, $imported_data, $customization, $this );

		if ( is_array( $result ) ) {
			return $result;
		}

		return $this->import_all( $data, $imported_data );
	}


	public function import_taxonomies( array $taxonomies, $path ) {
		$result = [];
		$imported_taxonomies = [];

		foreach ( $taxonomies as $taxonomy_object ) {
			$taxonomy = is_array( $taxonomy_object ) ? $taxonomy_object['name'] : $taxonomy_object;

			if ( ! taxonomy_exists( $taxonomy ) ) {
				continue;
			}

			if ( ! empty( $imported_taxonomies[ $taxonomy ] ) ) {
				$result[ $taxonomy ] = $imported_taxonomies[ $taxonomy ];
				continue;
			}

			$taxonomy_data = ImportExportUtils::read_json_file( $path . $taxonomy );
			if ( empty( $taxonomy_data ) ) {
				continue;
			}

			$import = $this->import_taxonomy( $taxonomy_data );
			$result[ $taxonomy ] = $import;
			$imported_taxonomies[ $taxonomy ] = $import;
		}

		return $result;
	}

	private function import_taxonomy( array $taxonomy_data ) {
		$terms = [];

		foreach ( $taxonomy_data as $term ) {
			$old_slug = $term['slug'];

			$existing_term = term_exists( $term['slug'], $term['taxonomy'] );
			if ( $existing_term ) {
				if ( 'nav_menu' === $term['taxonomy'] ) {
					$term = $this->handle_duplicated_nav_menu_term( $term );
				} else {
					$terms[] = [
						'old_id' => (int) $term['term_id'],
						'new_id' => (int) $existing_term['term_id'],
						'old_slug' => $old_slug,
						'new_slug' => $term['slug'],
					];
					continue;
				}
			}

			$parent = $this->get_term_parent( $term, $terms );

			$args = [
				'slug' => $term['slug'],
				'description' => wp_slash( $term['description'] ),
				'parent' => (int) $parent,
			];

			$new_term = wp_insert_term( wp_slash( $term['name'] ), $term['taxonomy'], $args );
			if ( ! is_wp_error( $new_term ) ) {
				$this->set_session_term_meta( (int) $new_term['term_id'], $this->import_session_id );

				$terms[] = [
					'old_id' => $term['term_id'],
					'new_id' => (int) $new_term['term_id'],
					'old_slug' => $old_slug,
					'new_slug' => $term['slug'],
				];
			}
		}

		return $terms;
	}

	private function handle_duplicated_nav_menu_term( $term ) {
		do {
			$term['slug'] = $term['slug'] . '-duplicate';
			$term['name'] = $term['name'] . ' duplicate';
		} while ( term_exists( $term['slug'], 'nav_menu' ) );

		return $term;
	}

	private function get_term_parent( $term, array $imported_terms ) {
		$parent = $term['parent'];
		if ( 0 !== $parent && ! empty( $imported_terms ) ) {
			foreach ( $imported_terms as $imported_term ) {
				if ( $parent === $imported_term['old_id'] ) {
					$parent_term = term_exists( $imported_term['new_id'], $term['taxonomy'] );
					break;
				}
			}

			if ( isset( $parent_term['term_id'] ) ) {
				return $parent_term['term_id'];
			}
		}

		return 0;
	}
}
PK     8\a    H  modules/import-export-customization/runners/import/elementor-content.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;

use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Plugin;

class Elementor_Content extends Import_Runner_Base {

	const IMPORT_STATUS_SUCCEEDED = 'succeed';

	const IMPORT_STATUS_FAILED = 'failed';

	private $show_page_on_front;

	private $page_on_front_id;

	private $import_session_id;

	private $imported_data;

	private $processed_posts = [];

	private $post_orphans = [];

	public function __construct() {
		$this->init_page_on_front_data();
	}

	public static function get_name(): string {
		return 'elementor-content';
	}

	public function should_import( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'content', $data['include'], true ) &&
			! empty( $data['manifest']['content'] ) &&
			! empty( $data['extracted_directory_path'] )
		);
	}

	public function import( array $data, array $imported_data ) {
		if ( ! function_exists( 'wp_set_post_terms' ) ) {
			require_once ABSPATH . 'wp-admin/includes/taxonomy.php';
		}

		$result['content'] = [];
		$this->import_session_id = $data['session_id'];
		$this->imported_data = $imported_data;

		$customization = $data['customization']['content'] ?? null;

		$selected_custom_post_types = $data['selected_custom_post_types'] ?? null;
		$excluded_post_types = [];

		if ( $selected_custom_post_types && ! in_array( 'post', $selected_custom_post_types, true ) ) {
			$excluded_post_types[] = 'post';
		}

		$post_types = ImportExportUtils::get_elementor_post_types( $excluded_post_types );

		$post_types = apply_filters( 'elementor/import-export-customization/elementor-content/post-types/customization', $post_types, $customization );

		foreach ( $post_types as $post_type ) {
			if ( empty( $data['manifest']['content'][ $post_type ] ) ) {
				continue;
			}

			$posts_settings = $data['manifest']['content'][ $post_type ];
			$path = $data['extracted_directory_path'] . 'content/' . $post_type . '/';
			$imported_terms = ! empty( $imported_data['taxonomies'] )
				? ImportExportUtils::map_old_new_term_ids( $imported_data )
				: [];

			$result['content'][ $post_type ] = $this->import_elementor_post_type(
				$posts_settings,
				$path,
				$post_type,
				$imported_terms,
				$data['customization']['content'] ?? null,
			);
		}

		return $result;
	}

	private function import_elementor_post_type( array $posts_settings, $path, $post_type, array $imported_terms, $customization ) {
		$result = [
			'succeed' => [],
			'failed' => [],
		];

		foreach ( $posts_settings as $id => $post_settings ) {
			try {
				if ( 'page' === $post_type ) {
					$data = [
						'path' => $path,
						'id' => $id,
						'post_settings' => $post_settings,
						'post_type' => $post_type,
						'imported_terms' => $imported_terms,
					];

					$import_result = apply_filters( 'elementor/import-export-customization/import/elementor-content/customization', null, $data, [], $customization ?? [], $this );

					if ( is_array( $import_result ) ) {
						$result[ $import_result['status'] ][ $id ] = $import_result['result'];
						$this->map_imported_post_id( $id, $import_result );
						continue;
					}
				}

				$import_result = $this->read_and_import_post( $path, $id, $post_settings, $post_type, $imported_terms );
				$this->map_imported_post_id( $id, $import_result );

				$result[ $import_result['status'] ][ $id ] = $import_result['result'];
			} catch ( \Exception $error ) {
				$result['failed'][ $id ] = $error->getMessage();
			}
		}

		$this->backfill_parents();

		return $result;
	}

	public function read_and_import_post( $path, $id, $post_settings, $post_type, $imported_terms ) {
		try {
			$post_data = ImportExportUtils::read_json_file( $path . $id );
			$import = $this->import_post( $post_settings, $post_data, $post_type, $imported_terms, $id );

			if ( is_wp_error( $import ) ) {
				$result = [
					'status' => static::IMPORT_STATUS_FAILED,
					'result' => $import->get_error_message(),
				];
			} else {
				$result = [
					'status' => static::IMPORT_STATUS_SUCCEEDED,
					'result' => $import,
				];
			}
		} catch ( \Exception $error ) {
			$result = [
				'status' => static::IMPORT_STATUS_FAILED,
				'result' => $error->getMessage(),
			];
		}

		return $result;
	}

	private function import_post( array $post_settings, array $post_data, $post_type, array $imported_terms, int $original_post_id ) {
		$post_attributes = [
			'post_title' => $post_settings['title'],
			'post_type' => $post_type,
			'post_status' => 'publish',
		];

		if ( ! empty( $post_settings['excerpt'] ) ) {
			$post_attributes['post_excerpt'] = $post_settings['excerpt'];
		}

		$post_parent_id = $this->get_imported_parent_id( $post_settings, $original_post_id );

		if ( $post_parent_id ) {
			$post_attributes['post_parent'] = $post_parent_id;
		}

		$new_document = Plugin::$instance->documents->create(
			$post_settings['doc_type'],
			$post_attributes
		);

		if ( is_wp_error( $new_document ) ) {
			throw new \Exception( esc_html( $new_document->get_error_message() ) );
		}

		$post_data['import_settings'] = $post_settings;

		$new_attachment_callback = function( $attachment_id ) {
			$this->set_session_post_meta( $attachment_id, $this->import_session_id );
		};

		add_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );

		$new_document->import( $post_data );

		remove_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );

		$new_post_id = $new_document->get_main_id();

		if ( ! empty( $post_settings['terms'] ) ) {
			$this->set_post_terms( $new_post_id, $post_settings['terms'], $imported_terms );
		}

		if ( ! empty( $post_settings['show_on_front'] ) ) {
			$this->set_page_on_front( $new_post_id );
		}

		$this->set_session_post_meta( $new_post_id, $this->import_session_id );

		return $new_post_id;
	}

	private function get_imported_parent_id( array $post_settings, int $original_post_id ): int {
		$post_parent_id = (int) ( $post_settings['post_parent'] ?? 0 );

		if ( ! $post_parent_id ) {
			return 0;
		}

		if ( isset( $this->processed_posts[ $post_parent_id ] ) ) {
			return $this->processed_posts[ $post_parent_id ];
		}

		$this->post_orphans[ $original_post_id ] = $post_parent_id;
		return 0;
	}

	private function set_post_terms( $post_id, array $terms, array $imported_terms ) {
		foreach ( $terms as $term ) {
			if ( ! isset( $imported_terms[ $term['term_id'] ] ) ) {
				continue;
			}

			wp_set_post_terms( $post_id, [ $imported_terms[ $term['term_id'] ] ], $term['taxonomy'], false );
		}
	}

	private function init_page_on_front_data() {
		$this->show_page_on_front = 'page' === get_option( 'show_on_front' );

		if ( $this->show_page_on_front ) {
			$this->page_on_front_id = (int) get_option( 'page_on_front' );
		}
	}

	private function set_page_on_front( $page_id ) {
		update_option( 'page_on_front', $page_id );

		if ( ! $this->show_page_on_front ) {
			update_option( 'show_on_front', 'page' );
		}
	}

	public function get_import_session_metadata(): array {
		return [
			'page_on_front' => $this->page_on_front_id ?? 0,
		];
	}

	private function map_imported_post_id( $original_id, $import_result ) {
		if ( static::IMPORT_STATUS_SUCCEEDED !== $import_result['status'] ) {
			return;
		}

		$this->processed_posts[ $original_id ] = $import_result['result'];
	}

	private function backfill_parents() {
		global $wpdb;

		// Find parents for post orphans.
		foreach ( $this->post_orphans as $child_id => $parent_id ) {
			$local_child_id = false;
			$local_parent_id = false;

			if ( isset( $this->processed_posts[ $child_id ] ) ) {
				$local_child_id = $this->processed_posts[ $child_id ];
			}

			if ( isset( $this->processed_posts[ $parent_id ] ) ) {
				$local_parent_id = $this->processed_posts[ $parent_id ];
			}

			if ( $local_child_id && $local_parent_id ) {
				$wpdb->update( $wpdb->posts, [ 'post_parent' => $local_parent_id ], [ 'ID' => $local_child_id ], '%d', '%d' );
				clean_post_cache( $local_child_id );
			}
		}
	}
}
PK     8\ʴ    I  modules/import-export-customization/runners/import/import-runner-base.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;

use Elementor\App\Modules\ImportExportCustomization\Runners\Runner_Interface;

abstract class Import_Runner_Base implements Runner_Interface {

	/**
	 * By the passed data we should decide if we want to run the import function of the runner or not.
	 *
	 * @param array $data
	 *
	 * @return bool
	 */
	abstract public function should_import( array $data );

	/**
	 * Main function of the runner import process.
	 *
	 * @param array $data Necessary data for the import process.
	 * @param array $imported_data Data that already imported by previously runners.
	 *
	 * @return array The result of the import process
	 */
	abstract public function import( array $data, array $imported_data );

	public function get_import_session_metadata(): array {
		return [];
	}

	public function set_session_post_meta( $post_id, $meta_value ) {
		update_post_meta( $post_id, static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID, $meta_value );
	}

	public function set_session_term_meta( $term_id, $meta_value ) {
		update_term_meta( $term_id, static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID, $meta_value );
	}
}
PK     8\jD}n  n  I  modules/import-export-customization/runners/revert/revert-runner-base.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;

use Elementor\App\Modules\ImportExportCustomization\Runners\Runner_Interface;

abstract class Revert_Runner_Base implements Runner_Interface {

	/**
	 * By the passed data we should decide if we want to run the revert function of the runner or not.
	 *
	 * @param array $data
	 *
	 * @return bool
	 */
	abstract public function should_revert( array $data ): bool;

	/**
	 * Main function of the runner revert process.
	 *
	 * @param array $data Necessary data for the revert process.
	 */
	abstract public function revert( array $data );
}
PK     8\c  c  D  modules/import-export-customization/runners/revert/site-settings.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;

use Elementor\Plugin;
use Elementor\Core\Experiments\Manager as ExperimentsManager;

class Site_Settings extends Revert_Runner_Base {

	public static function get_name(): string {
		return 'site-settings';
	}

	public function should_revert( array $data ): bool {
		return (
			isset( $data['runners'] ) &&
			array_key_exists( static::get_name(), $data['runners'] )
		);
	}

	public function revert( array $data ) {
		Plugin::$instance->kits_manager->revert(
			$data['runners'][ static::get_name() ]['imported_kit_id'],
			$data['runners'][ static::get_name() ]['active_kit_id'],
			$data['runners'][ static::get_name() ]['previous_kit_id']
		);

		$this->revert_theme( $data );
		$this->revert_experiments( $data );
	}

	public function get_theme_upgrader(): \Theme_Upgrader {
		if ( ! class_exists( '\Theme_Upgrader' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		}

		if ( ! class_exists( '\WP_Ajax_Upgrader_Skin' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
		}

		return new \Theme_Upgrader( new \WP_Ajax_Upgrader_Skin() );
	}

	protected function revert_theme( $data ) {
		$installed_theme = $data['runners'][ static::get_name() ]['installed_theme'];
		$activated_theme = $data['runners'][ static::get_name() ]['activated_theme'];
		$previous_active_theme = $data['runners'][ static::get_name() ]['previous_active_theme'];

		if ( empty( $installed_theme ) && empty( $activated_theme ) ) {
			// no need to remove a theme as it was used before import
			return;
		}

		if ( ! empty( $activated_theme ) ) {
			$previous_theme = wp_get_theme( $previous_active_theme['slug'] );

			// no need to remove imported theme as it existed before import
			$this->activate_previous_theme( $previous_active_theme );
			return;
		}

		if ( ! empty( $installed_theme ) ) {
			$this->activate_previous_theme( $previous_active_theme );
			$this->delete_theme( $installed_theme );
		}
	}

	protected function should_delete_theme( $theme_slug ): bool {
		$current_theme = wp_get_theme();

		return $theme_slug !== $current_theme->get_stylesheet() && wp_get_theme( $theme_slug )->exists();
	}

	protected function delete_theme( $theme_slug ): bool {
		if ( ! function_exists( 'delete_theme' ) ) {
			require_once ABSPATH . 'wp-admin/includes/theme.php';
		}

		return delete_theme( $theme_slug );
	}

	protected function activate_previous_theme( $previous_active_theme ) {
		if ( ! $previous_active_theme ) {
			return;
		}

		$theme = wp_get_theme( $previous_active_theme['slug'] );

		if ( $theme->exists() ) {
			switch_theme( $theme->get_stylesheet() );
			return;
		}

		$download_url = "https://downloads.wordpress.org/theme/{$previous_active_theme['slug']}.{$previous_active_theme['version']}.zip";
		$install = $this->get_theme_upgrader()->install( $download_url );

		if ( ! $install || is_wp_error( $install ) ) {
			return;
		}

		switch_theme( $previous_active_theme['slug'] );
	}

	protected function revert_experiments( array $data ) {
		$runner_data = $data['runners'][ static::get_name() ];
		$previous_experiments = $runner_data['previous_experiments'] ?? [];

		if ( empty( $previous_experiments ) ) {
			return;
		}

		$experiments_manager = Plugin::$instance->experiments;
		$current_features = $experiments_manager->get_features();

		foreach ( $previous_experiments as $feature_name => $feature_data ) {
			if ( ! isset( $current_features[ $feature_name ] ) ) {
				continue;
			}

			if ( ! array_key_exists( $feature_name, $previous_experiments ) ) {
				continue;
			}

			$option_key = $experiments_manager->get_feature_option_key( $feature_name );
			$previous_state = $feature_data['state'];

			if ( ExperimentsManager::STATE_DEFAULT === $previous_state ) {
				delete_option( $option_key );
			} else {
				update_option( $option_key, $previous_state );
			}
		}
	}
}
PK     8\1v    @  modules/import-export-customization/runners/revert/templates.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;

use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Core\Base\Document;

class Templates extends Revert_Runner_Base {
	public static function get_name(): string {
		return 'templates';
	}

	public function should_revert( array $data ): bool {
		return (
			isset( $data['runners'] ) &&
			array_key_exists( static::get_name(), $data['runners'] )
		);
	}

	public function revert( array $data ) {
		$template_types = array_values( Source_Local::get_template_types() );

		$query_args = [
			'post_type' => Source_Local::CPT,
			'post_status' => 'any',
			'posts_per_page' => -1,
			'meta_query' => [
				[
					'key' => Document::TYPE_META_KEY,
					'value' => $template_types,
				],
				[
					'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
					'value' => $data['session_id'],
				],
			],
		];

		$templates_query = new \WP_Query( $query_args );

		foreach ( $templates_query->posts as $template_post ) {
			$template_document = Plugin::$instance->documents->get( $template_post->ID );
			$template_document->delete();
		}

		do_action( 'elementor/import-export-customization/revert/templates', $data );
	}
}
PK     8\ءx9  9  >  modules/import-export-customization/runners/revert/plugins.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;

class Plugins extends Revert_Runner_Base {

	public static function get_name(): string {
		return 'plugins';
	}

	public function should_revert( array $data ): bool {
		return false;
	}

	public function revert( array $data ) {}
}
PK     8\z)%  %  A  modules/import-export-customization/runners/revert/wp-content.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;

use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;

class Wp_Content extends Revert_Runner_Base {

	public static function get_name(): string {
		return 'wp-content';
	}

	public function should_revert( array $data ): bool {
		return (
			isset( $data['runners'] ) &&
			array_key_exists( static::get_name(), $data['runners'] )
		);
	}

	public function revert( array $data ) {
		$builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
		$custom_post_types = $data['runners']['wp-content']['custom_post_types'] ?? [];

		$post_types = array_merge( $builtin_post_types, $custom_post_types );

		$query_args = [
			'post_type' => $post_types,
			'post_status' => 'any',
			'posts_per_page' => -1,
			'meta_query' => [
				[
					'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
					'compare' => 'NOT EXISTS',
				],
				[
					'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
					'value' => $data['session_id'],
				],
			],
		];

		$query = new \WP_Query( $query_args );

		foreach ( $query->posts as $post ) {
			wp_delete_post( $post->ID, true );
		}

		/**
		 * Revert the nav menu terms.
		 * BC: The nav menu in new kits will be imported as part of the taxonomies, but old kits
		 * importing the nav menu terms as part from the wp-content import.
		 */
		$this->revert_nav_menus( $data );
	}

	private function revert_nav_menus( array $data ) {
		$terms = get_terms( [
			'taxonomy' => 'nav_menu',
			'hide_empty' => false,
			'get' => 'all',
			'meta_query' => [
				[
					'key'       => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
					'value'     => $data['session_id'],
				],
			],
		] );

		foreach ( $terms as $term ) {
			wp_delete_term( $term->term_id, $term->taxonomy );
		}
	}
}
PK     8\ӑ    A  modules/import-export-customization/runners/revert/taxonomies.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;

class Taxonomies extends Revert_Runner_Base {

	public static function get_name(): string {
		return 'taxonomies';
	}

	public function should_revert( array $data ): bool {
		return (
			isset( $data['runners'] ) &&
			array_key_exists( static::get_name(), $data['runners'] )
		);
	}

	public function revert( array $data ) {
		$taxonomies = get_taxonomies();

		$terms = get_terms( [
			'taxonomy' => $taxonomies,
			'hide_empty' => false,
			'get' => 'all',
			'meta_query' => [
				[
					'key'       => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
					'value'     => $data['session_id'],
				],
			],
		] );

		foreach ( $terms as $term ) {
			wp_delete_term( $term->term_id, $term->taxonomy );
		}
	}
}
PK     8\2o51	  1	  H  modules/import-export-customization/runners/revert/elementor-content.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;

use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Plugin;

class Elementor_Content extends Revert_Runner_Base {
	private $show_page_on_front;

	private $page_on_front_id;

	public function __construct() {
		$this->init_page_on_front_data();
	}

	public static function get_name(): string {
		return 'elementor-content';
	}

	public function should_revert( array $data ): bool {
		return (
			isset( $data['runners'] ) &&
			array_key_exists( static::get_name(), $data['runners'] )
		);
	}

	public function revert( array $data ) {
		$elementor_post_types = ImportExportUtils::get_elementor_post_types();

		$query_args = [
			'post_type' => $elementor_post_types,
			'post_status' => 'any',
			'posts_per_page' => -1,
			'meta_query' => [
				[
					'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
					'compare' => 'EXISTS',
				],
				[
					'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
					'value' => $data['session_id'],
				],
			],
		];

		$query = new \WP_Query( $query_args );

		foreach ( $query->posts as $post ) {
			$post_type_document = Plugin::$instance->documents->get( $post->ID );
			$post_type_document->delete();

			// Deleting the post will reset the show_on_front option. We need to set it to false,
			// so we can set it back to what it was.
			if ( $post->ID === $this->page_on_front_id ) {
				$this->show_page_on_front = false;
			}
		}

		$this->restore_page_on_front( $data );
	}

	private function init_page_on_front_data() {
		$this->show_page_on_front = 'page' === get_option( 'show_on_front' );

		if ( $this->show_page_on_front ) {
			$this->page_on_front_id = (int) get_option( 'page_on_front' );
		}
	}

	private function restore_page_on_front( $data ) {
		if ( empty( $data['runners'][ static::get_name() ]['page_on_front'] ) ) {
			return;
		}

		$page_on_front = $data['runners'][ static::get_name() ]['page_on_front'];

		$document = Plugin::$instance->documents->get( $page_on_front );

		if ( ! $document ) {
			return;
		}

		$this->set_page_on_front( $document->get_main_id() );
	}

	private function set_page_on_front( $page_id ) {
		update_option( 'page_on_front', $page_id );

		if ( ! $this->show_page_on_front ) {
			update_option( 'show_on_front', 'page' );
		}
	}
}
PK     8\[)  )  @  modules/import-export-customization/runners/runner-interface.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners;

use Elementor\App\Modules\ImportExportCustomization\Module;

interface Runner_Interface {

	const META_KEY_ELEMENTOR_IMPORT_SESSION_ID = Module::META_KEY_ELEMENTOR_IMPORT_SESSION_ID;

	const META_KEY_ELEMENTOR_EDIT_MODE = Module::META_KEY_ELEMENTOR_EDIT_MODE;

	/**
	 * Get the name of the runners, used to identify the runner.
	 * The name should be unique, unless you want to run over existing runner.
	 *
	 * @return string
	 */
	public static function get_name(): string;
}
PK     8\Eѽ    D  modules/import-export-customization/runners/export/site-settings.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;

use Elementor\Modules\AtomicWidgets\Module as Atomic_Widgets_Module;
use Elementor\Modules\GlobalClasses\Module as Global_Classes_Module;
use Elementor\Modules\Variables\Module as Variables_Module;
use Elementor\Plugin;

class Site_Settings extends Export_Runner_Base {
	const ALLOWED_SETTINGS = [
		'theme',
		'globalColors',
		'globalFonts',
		'themeStyleSettings',
		'generalSettings',
		'experiments',
		'customCode',
		'customIcons',
		'customFonts',
		'classes',
		'variables',
	];

	public static function get_name(): string {
		return 'site-settings';
	}

	public function should_export( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'settings', $data['include'], true )
		);
	}

	public function export( array $data ) {
		$customization = $data['customization']['settings'] ?? null;
		if ( $customization ) {
			return $this->export_customization( $data, $customization );
		}

		return $this->export_all( $data );
	}

	private function export_all( $data, $include_theme = true ) {
		$kit = Plugin::$instance->kits_manager->get_active_kit();
		$kit_data = $kit->get_export_data();

		$excluded_kit_settings_keys = [
			'site_name',
			'site_description',
			'site_logo',
			'site_favicon',
		];

		foreach ( $excluded_kit_settings_keys as $setting_key ) {
			unset( $kit_data['settings'][ $setting_key ] );
		}

		if ( $include_theme ) {
			$theme_data = $this->export_theme();

			if ( $theme_data ) {
				$kit_data['theme'] = $theme_data;
				$manifest_data['theme'] = $theme_data;
			}
		}

		$experiments_data = $this->export_experiments();

		if ( $experiments_data ) {
			$kit_data['experiments'] = $experiments_data;
			$manifest_data['experiments'] = array_keys( $experiments_data );
		}

		$manifest_data['site-settings'] = array_fill_keys( self::ALLOWED_SETTINGS, true );

		if ( ! $include_theme ) {
			$manifest_data['site-settings']['theme'] = false;
		}

		if ( $this->is_classes_feature_active() ) {
			$manifest_data['site-settings']['classesCount'] = $this->get_classes_count();
		} else {
			unset( $manifest_data['site-settings']['classes'] );
		}

		if ( $this->is_variables_feature_active() ) {
			$manifest_data['site-settings']['variablesCount'] = $this->get_variables_count();
		} else {
			unset( $manifest_data['site-settings']['variables'] );
		}

		return [
			'files' => [
				'path' => 'site-settings',
				'data' => $kit_data,
			],
			'manifest' => [
				$manifest_data,
			],
		];
	}

	public function get_classes_count(): int {
		$classes_repository = \Elementor\Modules\GlobalClasses\Global_Classes_Repository::make();
		$classes_data = $classes_repository->all()->get();

		return count( $classes_data['items'] ?? [] );
	}

	public function get_variables_count(): int {
		$kit = Plugin::$instance->kits_manager->get_active_kit();
		$variables_repository = new \Elementor\Modules\Variables\Storage\Variables_Repository( $kit );
		$collection = $variables_repository->load();
		$count = 0;

		foreach ( $collection->all() as $variable ) {
			if ( ! $variable->is_deleted() ) {
				$count++;
			}
		}

		return $count;
	}

	public function is_classes_feature_active(): bool {
		return Plugin::$instance->experiments->is_feature_active( Global_Classes_Module::NAME )
			&& Plugin::$instance->experiments->is_feature_active( Atomic_Widgets_Module::EXPERIMENT_NAME );
	}

	public function is_variables_feature_active(): bool {
		return Plugin::$instance->experiments->is_feature_active( Variables_Module::EXPERIMENT_NAME )
			&& Plugin::$instance->experiments->is_feature_active( Atomic_Widgets_Module::EXPERIMENT_NAME );
	}

	private function export_customization( $data, $customization ) {
		$result = apply_filters( 'elementor/import-export-customization/export/site-settings/customization', null, $data, $customization, $this );

		if ( is_array( $result ) ) {
			return $result;
		}

		$export_result = $this->export_all( $data, ! empty( $customization['theme'] ) );

		if ( $this->is_classes_feature_active() ) {
			$include_classes = $customization['classes'] ?? false;
			$export_result['manifest'][0]['site-settings']['classes'] = (bool) $include_classes;

			if ( ! $include_classes ) {
				$export_result['manifest'][0]['site-settings']['classesCount'] = 0;
			}
		}

		if ( $this->is_variables_feature_active() ) {
			$include_variables = $customization['variables'] ?? false;
			$export_result['manifest'][0]['site-settings']['variables'] = (bool) $include_variables;

			if ( ! $include_variables ) {
				$export_result['manifest'][0]['site-settings']['variablesCount'] = 0;
			}
		}

		return $export_result;
	}

	public function export_theme() {
		$theme = wp_get_theme();

		if ( empty( $theme ) || empty( $theme->get( 'ThemeURI' ) ) ) {
			return null;
		}

		$theme_data['name'] = $theme->get( 'Name' );
		$theme_data['theme_uri'] = $theme->get( 'ThemeURI' );
		$theme_data['version'] = $theme->get( 'Version' );
		$theme_data['slug'] = $theme->get_stylesheet();

		return $theme_data;
	}

	public function export_experiments() {
		$features = Plugin::$instance->experiments->get_features();

		if ( empty( $features ) ) {
			return null;
		}

		$experiments_data = [];

		foreach ( $features as $feature_name => $feature ) {
			$experiments_data[ $feature_name ] = [
				'name' => $feature_name,
				'title' => $feature['title'],
				'state' => $feature['state'],
				'default' => $feature['default'],
				'release_status' => $feature['release_status'],
			];
		}

		return empty( $experiments_data ) ? null : $experiments_data;
	}
}
PK     8\k'  '  @  modules/import-export-customization/runners/export/templates.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;

use Elementor\Core\Base\Document;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;
use Elementor\Modules\Library\Documents\Library_Document;

class Templates extends Export_Runner_Base {

	public static function get_name(): string {
		return 'templates';
	}

	public function should_export( array $data ) {
		return (
			Utils::has_pro() &&
			isset( $data['include'] ) &&
			in_array( 'templates', $data['include'], true )
		);
	}

	public function export( array $data ) {
		$customization = $data['customization']['templates'] ?? null;

		if ( $customization ) {
			return $this->export_with_customization( $data, $customization );
		}

		return $this->export_all( $data );
	}

	private function export_with_customization( array $data, array $customization ) {
		$result = apply_filters( 'elementor/import-export-customization/export/templates/customization', null, $data, $customization, $this );

		if ( is_array( $result ) ) {
			return $result;
		}

		return $this->export_all( $data );
	}

	private function export_all( array $data ) {
		$template_types = array_values( Source_Local::get_template_types() );

		return $this->export_templates_by_types( $template_types, $data );
	}

	public function export_templates_by_types( array $template_types, array $data ) {
		$templates_manifest_data = [];
		$files = [];

		if ( ! empty( $template_types ) ) {
			$query_args = [
				'post_type' => Source_Local::CPT,
				'post_status' => 'publish',
				'posts_per_page' => -1,
				'meta_query' => [
					[
						'key' => Document::TYPE_META_KEY,
						'value' => $template_types,
					],
				],
			];

			$templates_query = new \WP_Query( $query_args );

			foreach ( $templates_query->posts as $template_post ) {
				$template_id = $template_post->ID;

				$template_document = Plugin::$instance->documents->get( $template_id );

				$templates_manifest_data[ $template_id ] = $template_document->get_export_summary();

				$files[] = [
					'path' => 'templates/' . $template_id,
					'data' => $template_document->get_export_data(),
				];
			}
		}

		$manifest_data['templates'] = $templates_manifest_data;

		$export_data = [
			'files' => $files,
			'manifest' => [
				$manifest_data,
			],
		];

		/**
		 * Filter the templates export data to allow adding additional data.
		 *
		 * @param array $export_data The export data structure with 'files' and 'manifest' keys.
		 * @param array $data The full export data.
		 * @param array|null $customization The customization settings for templates.
		 */
		$customization = $data['customization']['templates'] ?? null;
		$export_data = apply_filters( 'elementor/import-export-customization/export/templates_data', $export_data, $data, $customization );

		return $export_data;
	}
}
PK     8\z^    I  modules/import-export-customization/runners/export/export-runner-base.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;

use Elementor\App\Modules\ImportExportCustomization\Runners\Runner_Interface;

abstract class Export_Runner_Base implements Runner_Interface {

	/**
	 * By the passed data we should decide if we want to run the export function of the runner or not.
	 *
	 * @param array $data
	 *
	 * @return bool
	 */
	abstract public function should_export( array $data );

	/**
	 * Main function of the runner export process.
	 *
	 * @param array $data Necessary data for the export process.
	 *
	 * @return array{files: array, manifest: array}
	 * The files that should be part of the kit and the relevant manifest data.
	 */
	abstract public function export( array $data );
}
PK     8\a'    >  modules/import-export-customization/runners/export/plugins.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;

use Elementor\Core\Utils\Collection;

class Plugins extends Export_Runner_Base {

	public static function get_name(): string {
		return 'plugins';
	}

	public function should_export( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'plugins', $data['include'], true ) &&
			is_array( $data['selected_plugins'] )
		);
	}

	public function export( array $data ) {
		$customization = $data['customization']['plugins'] ?? null;

		if ( $customization ) {
			$enabled_plugin_keys = Collection::make( $customization )->filter()->keys();

			$plugins = Collection::make( $data['selected_plugins'] )
				->filter( function( $plugin_data, $plugin_key ) use ( $enabled_plugin_keys ) {
					return $enabled_plugin_keys->contains( $plugin_key );
				} )
				->all();
		} else {
			$plugins = $data['selected_plugins'];
		}

		return [
			'manifest' => [
				[ 'plugins' => array_values( $plugins ) ],
			],
			'files' => [],
		];
	}
}
PK     8\f    A  modules/import-export-customization/runners/export/wp-content.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;

use Elementor\App\Modules\ImportExportCustomization\Compatibility\Customization;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Core\Utils\ImportExport\WP_Exporter;

class Wp_Content extends Export_Runner_Base {

	public static function get_name(): string {
		return 'wp-content';
	}

	public function should_export( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'content', $data['include'], true )
		);
	}

	public function export( array $data ) {
		$customization = $data['customization']['content'] ?? null;
		$exclude_post_types = [];

		if ( isset( $customization['customPostTypes'] ) && ! in_array( 'post', $customization['customPostTypes'], true ) ) {
			$exclude_post_types[] = 'post';
		}

		$post_types = ImportExportUtils::get_builtin_wp_post_types( $exclude_post_types );

		$post_types = apply_filters( 'elementor/import-export-customization/wp-content/post-types/customization', $post_types, $data, $customization );

		$custom_post_types = isset( $data['selected_custom_post_types'] ) ? $data['selected_custom_post_types'] : [];

		$files = [];
		$manifest_data = [];

		foreach ( $post_types as $post_type ) {
			$export = $this->export_wp_post_type( $post_type, $customization );

			if ( ! empty( $export['file'] ) ) {
				$files[] = $export['file'];
			}

			$manifest_data['wp-content'][ $post_type ] = $export['manifest_data'];
		}

		foreach ( $custom_post_types as $post_type ) {
			$post_type_object = get_post_type_object( $post_type );

			$manifest_data['custom-post-type-title'][ $post_type ] = [
				'name' => $post_type_object->name,
				'label' => $post_type_object->label,
			];

			// handled in the previous loop
			if ( 'post' === $post_type ) {
				continue;
			}
			$export = $this->export_wp_post_type( $post_type, $customization );

			if ( ! empty( $export['file'] ) ) {
				$files[] = $export['file'];
			}

			$manifest_data['wp-content'][ $post_type ] = $export['manifest_data'];
		}

		return [
			'files' => $files,
			'manifest' => [
				$manifest_data,
			],
		];
	}

	private function export_wp_post_type( $post_type, $customization ) {
		$exporter_args = [
			'content' => $post_type,
			'status' => 'publish',
			'meta_query' => [
				[
					'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
					'compare' => 'NOT EXISTS',
				],
			],
			'include_post_featured_image_as_attachment' => true,
		];

		if ( 'pages' !== $post_type ) {
			$exporter_args['limit'] = 20;
		}

		$exporter_args = apply_filters( 'elementor/import-export-customization/export/wp-content/query-args/customization', $exporter_args, $post_type, $customization );

		$wp_exporter = new WP_Exporter( $exporter_args );

		$export_result = $wp_exporter->run();

		return [
			'file' => [
				'path' => 'wp-content/' . $post_type . '/' . $post_type . '.xml',
				'data' => $export_result['xml'],
			],
			'manifest_data' => $export_result['posts'],
		];
	}
}
PK     8\    A  modules/import-export-customization/runners/export/taxonomies.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;

use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;

class Taxonomies extends Export_Runner_Base {

	public static function get_name(): string {
		return 'taxonomies';
	}

	public function should_export( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'content', $data['include'], true )
		);
	}

	public function export( array $data ) {
		$customization = $data['customization']['content'] ?? null;
		if ( $customization ) {
			return $this->export_customization( $data, $customization );
		}

		return $this->export_all( $data );
	}

	public function export_customization( array $data, array $customization ) {
		$result = apply_filters( 'elementor/import-export-customization/export/taxonomies/customization', null, $data, $customization, $this );

		if ( is_array( $result ) ) {
			return $result;
		}

		return $this->export_all( $data );
	}

	public function export_all( array $data ) {
		$selected_custom_post_types = $data['customization']['content']['customPostTypes'] ?? null;
		$exclude_post_types = [];

		if ( is_array( $selected_custom_post_types ) && ! in_array( 'post', $selected_custom_post_types, true ) ) {
			$exclude_post_types[] = 'post';
		}

		$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types( $exclude_post_types );

		$post_types = is_array( $selected_custom_post_types )
			? array_merge( $wp_builtin_post_types, $selected_custom_post_types )
			: $wp_builtin_post_types;

		$export = $this->export_taxonomies( $post_types );

		$manifest_data['taxonomies'] = $export['manifest'];

		return [
			'files' => $export['files'],
			'manifest' => [
				$manifest_data,
			],
		];
	}

	private function export_taxonomies( array $post_types ) {
		$files = [];
		$manifest = [];

		$taxonomies = get_taxonomies();

		foreach ( $taxonomies as $taxonomy ) {
			$taxonomy_obj = get_taxonomy( $taxonomy );
			$taxonomy_post_types = $taxonomy_obj->object_type;
			$intersected_post_types = array_intersect( $taxonomy_post_types, $post_types );

			if ( empty( $intersected_post_types ) ) {
				continue;
			}

			$data = $this->export_terms( $taxonomy );

			if ( empty( $data ) ) {
				continue;
			}

			foreach ( $intersected_post_types as $post_type ) {
				$manifest[ $post_type ][] = [
					'name'  => $taxonomy,
					'label' => $taxonomy_obj->label,
				];
			}

			$files[] = [
				'path' => 'taxonomies/' . $taxonomy,
				'data' => $data,
			];
		}

		return [
			'files' => $files,
			'manifest' => $manifest,
		];
	}

	public function export_terms( $taxonomy ) {
		$terms = get_terms( [
			'taxonomy' => (array) $taxonomy,
			'hide_empty' => false,
			'get' => 'all',
		] );

		$ordered_terms = $this->order_terms( $terms );

		if ( empty( $ordered_terms ) ) {
			return [];
		}

		$data = [];

		foreach ( $ordered_terms as $term ) {
			$data[] = [
				'term_id' => $term->term_id,
				'name' => $term->name,
				'slug' => $term->slug,
				'taxonomy' => $term->taxonomy,
				'description' => $term->description,
				'parent' => $term->parent,
			];
		}

		return $data;
	}
	/**
	 * Put terms in order with no child going before its parent.
	 */
	private function order_terms( array $terms ) {
		$ordered_terms = [];

		while ( $term = array_shift( $terms ) ) {
			$is_top_level = 0 === $term->parent;
			$is_parent_exits = isset( $ordered_terms[ $term->parent ] );

			if ( $is_top_level || $is_parent_exits ) {
				$ordered_terms[ $term->term_id ] = $term;
			} else {
				$terms[] = $term;
			}
		}

		return $ordered_terms;
	}
}
PK     8\h    H  modules/import-export-customization/runners/export/elementor-content.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;

use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Plugin;

class Elementor_Content extends Export_Runner_Base {
	private $page_on_front_id;

	public function __construct() {
		$this->init_page_on_front_data();
	}

	public static function get_name(): string {
		return 'elementor-content';
	}

	public function should_export( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'content', $data['include'], true )
		);
	}

	public function export( array $data ) {
		$customization = $data['customization']['content'] ?? null;
		$selected_custom_post_types = $data['selected_custom_post_types'] ?? null;
		$excluded_post_types = [];

		if ( $selected_custom_post_types && ! in_array( 'post', $selected_custom_post_types, true ) ) {
			$excluded_post_types[] = 'post';
		}

		$elementor_post_types = ImportExportUtils::get_elementor_post_types( $excluded_post_types );

		$elementor_post_types = apply_filters( 'elementor/import-export-customization/elementor-content/post-types/customization', $elementor_post_types, $customization );

		$files = [];
		$manifest = [];

		foreach ( $elementor_post_types as $post_type ) {
			$export = $this->export_elementor_post_type( $post_type, $customization );
			$files = array_merge( $files, $export['files'] );

			$manifest[ $post_type ] = $export['manifest_data'];
		}

		$manifest_data['content'] = $manifest;

		return [
			'files' => $files,
			'manifest' => [
				$manifest_data,
			],
		];
	}

	private function export_elementor_post_type( $post_type, $customization ) {
		$manifest_data = [];
		$files = [];

		$query_args = [
			'post_type' => $post_type,
			'post_status' => 'publish',
			'posts_per_page' => -1,
			'meta_query' => [
				[
					'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
					'compare' => 'EXISTS',
				],
				[
					'key' => '_elementor_data',
					'compare' => 'EXISTS',
				],
				[
					'key' => '_elementor_data',
					'compare' => '!=',
					'value' => '[]',
				],
			],
		];

		$query_args = apply_filters( 'elementor/import-export-customization/export/elementor-content/query-args/customization', $query_args, $post_type, $customization );

		$query = new \WP_Query( $query_args );

		if ( empty( $query ) ) {
			return [
				'files' => [],
				'manifest_data' => [],
			];
		}

		$post_type_taxonomies = $this->get_post_type_taxonomies( $post_type );

		foreach ( $query->posts as $post ) {
			$document = Plugin::$instance->documents->get( $post->ID );

			$terms = ! empty( $post_type_taxonomies ) ? $this->get_post_terms( $post->ID, $post_type_taxonomies ) : [];

			$post_manifest_data = [
				'title' => $post->post_title,
				'excerpt' => $post->post_excerpt,
				'doc_type' => $document->get_name(),
				'thumbnail' => get_the_post_thumbnail_url( $post ),
				'url' => get_permalink( $post ),
				'terms' => $terms,
			];

			if ( isset( $post->post_parent ) && $post->post_parent > 0 ) {
				$post_manifest_data['post_parent'] = $post->post_parent;
			}

			if ( $post->ID === $this->page_on_front_id ) {
				$post_manifest_data['show_on_front'] = true;
			}

			$manifest_data[ $post->ID ] = $post_manifest_data;

			$files[] = [
				'path' => 'content/' . $post_type . '/' . $post->ID,
				'data' => $document->get_export_data(),
			];
		}

		return [
			'files' => $files,
			'manifest_data' => $manifest_data,
		];
	}

	private function get_post_type_taxonomies( $post_type ) {
		return get_object_taxonomies( $post_type );
	}

	private function get_post_terms( $post_id, array $taxonomies ) {
		$terms = wp_get_object_terms( $post_id, $taxonomies );

		$result = [];

		foreach ( $terms as $term ) {
			$result[] = [
				'term_id' => $term->term_id,
				'taxonomy' => $term->taxonomy,
				'slug' => $term->slug,
			];
		}

		return $result;
	}

	private function init_page_on_front_data() {
		$show_page_on_front = 'page' === get_option( 'show_on_front' );

		if ( $show_page_on_front ) {
			$this->page_on_front_id = (int) get_option( 'page_on_front' );
		}
	}
}
PK     8\B$c+  c+  8  modules/import-export-customization/processes/export.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Processes;

use Elementor\App\Modules\ImportExportCustomization\Module;
use Elementor\App\Modules\ImportExportCustomization\Utils;
use Elementor\Core\Utils\Str;
use Elementor\Plugin;

use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Elementor_Content;
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Export_Runner_Base;
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Plugins;
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Site_Settings;
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Taxonomies;
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Templates;
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Wp_Content;

class Export {
	const ZIP_ARCHIVE_MODULE_MISSING = 'zip-archive-module-is-missing';

	/**
	 * @var Export_Runner_Base[]
	 */
	protected $runners = [];

	/**
	 * Selected content types to export.
	 *
	 * @var array
	 */
	private $settings_include;

	/**
	 * The kit information. (e.g: title, description)
	 *
	 * @var array $export_data
	 */
	private $settings_kit_info;

	/**
	 * Customization settings for selective export.
	 *
	 * @var array
	 */
	private $settings_customization;

	/**
	 * Selected plugins to export.
	 * Contains the plugins essential data for export. (e.g: name, path, version, etc.)
	 *
	 * @var array
	 */
	private $settings_selected_plugins;

	/**
	 * Selected custom post types to export.
	 *
	 * @var array
	 */
	private $settings_selected_custom_post_types;

	/**
	 * The output data of the export process.
	 * Will be written into the manifest.json file.
	 *
	 * @var array
	 */
	private $manifest_data;

	/**
	 * The zip archive object.
	 *
	 * @var \ZipArchive
	 */
	private $zip;

	public function __construct( $settings = [] ) {
		$this->settings_include = ! empty( $settings['include'] ) ? $settings['include'] : null;
		$this->settings_kit_info = ! empty( $settings['kitInfo'] ) ? $settings['kitInfo'] : null;
		$this->settings_customization = isset( $settings['customization'] ) ? $settings['customization'] : null;
		$this->settings_selected_plugins = isset( $settings['plugins'] ) ? $settings['plugins'] : null;
		$this->settings_selected_custom_post_types = isset( $settings['customization']['content']['customPostTypes'] ) ? $settings['customization']['content']['customPostTypes'] : null;
	}

	/**
	 * Register a runner.
	 *
	 * @param Export_Runner_Base $runner_instance
	 */
	public function register( Export_Runner_Base $runner_instance ) {
		$this->runners[ $runner_instance::get_name() ] = $runner_instance;
	}

	public function register_default_runners() {
		$this->register( new Site_Settings() );
		$this->register( new Plugins() );
		$this->register( new Templates() );
		$this->register( new Taxonomies() );
		$this->register( new Elementor_Content() );
		$this->register( new Wp_Content() );
	}

	/**
	 * Execute the export process.
	 *
	 * @return array The export data output.
	 *
	 * @throws \Exception If no export runners have been specified.
	 */
	public function run() {
		if ( empty( $this->runners ) ) {
			throw new \Exception( 'Couldn’t execute the export process because no export runners have been specified. Try again by specifying export runners.' );
		}

		$this->set_default_settings();

		$this->init_zip_archive();
		$this->init_manifest_data();

		$data = [
			'include' => $this->settings_include,
			'customization' => $this->settings_customization,
			'selected_plugins' => $this->settings_selected_plugins,
			'selected_custom_post_types' => $this->settings_selected_custom_post_types,
		];

		$media_collector = null;
		if ( $this->should_collect_media( $data ) ) {
			$media_collector = new \Elementor\TemplateLibrary\Classes\Media_Collector();
			$media_collector->start_collection();
		}

		foreach ( $this->runners as $runner ) {
			if ( $runner->should_export( $data ) ) {
				$export_result = $runner->export( $data );
				$this->handle_export_result( $export_result );
			}
		}

		$media_urls = null;
		if ( $media_collector ) {
			$media_urls = $media_collector->get_collected_urls();
		}

		$this->add_json_file( 'manifest', $this->manifest_data );

		$zip_file_name = $this->zip->filename;
		$this->zip->close();

		return [
			'manifest' => $this->manifest_data,
			'file_name' => $zip_file_name,
			'media_urls' => $media_urls,
		];
	}

	/**
	 * Set default settings for the export.
	 */
	private function set_default_settings() {
		if ( ! is_array( $this->get_settings_include() ) ) {
			$this->settings_include( $this->get_default_settings_include() );
		}

		if ( ! is_array( $this->get_settings_kit_info() ) ) {
			$this->settings_kit_info( $this->get_default_settings_kit_info() );
		}

		if ( ! is_array( $this->get_settings_selected_custom_post_types() ) && in_array( 'content', $this->settings_include, true ) ) {
			$this->settings_selected_custom_post_types( $this->get_default_settings_custom_post_types() );
		}

		if ( ! is_array( $this->get_settings_selected_plugins() ) && in_array( 'plugins', $this->settings_include, true ) ) {
			$this->settings_selected_plugins( $this->get_default_settings_selected_plugins() );
		}

		if ( ! is_array( $this->get_settings_customization() ) ) {
			$this->settings_customization( $this->get_default_settings_customization() );
		}
	}

	public function settings_include( $settings_include ) {
		$this->settings_include = $settings_include;
	}

	public function get_settings_include() {
		return $this->settings_include;
	}

	private function settings_kit_info( $kit_info ) {
		$this->settings_kit_info = $kit_info;
	}

	private function get_settings_kit_info() {
		return $this->settings_kit_info;
	}

	public function settings_customization( $customization ) {
		$this->settings_customization = $customization;
	}

	public function get_settings_customization() {
		return $this->settings_customization;
	}

	public function settings_selected_custom_post_types( $selected_custom_post_types ) {
		$this->settings_selected_custom_post_types = $selected_custom_post_types;
	}

	public function get_settings_selected_custom_post_types() {
		return $this->settings_selected_custom_post_types;
	}

	public function settings_selected_plugins( $plugins ) {
		$this->settings_selected_plugins = $plugins;
	}

	public function get_settings_selected_plugins() {
		return $this->settings_selected_plugins;
	}

	/**
	 * Get the default settings of which content types should be exported.
	 *
	 * @return array
	 */
	private function get_default_settings_include() {
		return [ 'templates', 'content', 'settings', 'plugins' ];
	}

	/**
	 * Get the default settings of the kit info.
	 *
	 * @return array
	 */
	private function get_default_settings_kit_info() {
		return [
			'title' => 'kit',
			'description' => '',
		];
	}

	/**
	 * Get the default settings of the plugins that should be exported.
	 *
	 * @return array{name: string, plugin:string, pluginUri: string, version: string}
	 */
	private function get_default_settings_selected_plugins() {
		$installed_plugins = Plugin::$instance->wp->get_plugins();

		$result = [];
		foreach ( $installed_plugins->all() as $key => $item ) {
			$plugin_key = str_replace( '.php', '', $key ); // Inconsistency between get_plugins() and WP Rest API's key format.

			$result[ $plugin_key ] = [
				'name' => $item['Name'],
				'plugin' => $plugin_key,
				'pluginUri' => $item['PluginURI'],
				'version' => $item['Version'],
			];
		}
		return $result;
	}

	private function get_default_settings_customization() {
		return [
			'settings' => null,
			'templates' => null,
			'content' => [
				'mediaFormat' => 'link',
			],
			'plugins' => null,
		];
	}

	/**
	 * Get the default settings of all the custom post types that should be exported.
	 * Should be all the custom post types that are not built in to WordPress and not part of Elementor.
	 *
	 * @return array
	 */
	private function get_default_settings_custom_post_types() {
		return Utils::get_registered_cpt_names();
	}

	/**
	 * Init the zip archive.
	 *
	 * @throws \Error If export process fails, file creation errors occur, or data serialization fails.
	 */
	private function init_zip_archive() {
		if ( ! class_exists( '\ZipArchive' ) ) {
			throw new \Error( static::ZIP_ARCHIVE_MODULE_MISSING ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		$zip = new \ZipArchive();

		$temp_dir = Plugin::$instance->uploads_manager->create_unique_dir();

		$zip_file_name = $temp_dir . sanitize_title( $this->settings_kit_info['title'] ) . '.zip';

		$zip->open( $zip_file_name, \ZipArchive::CREATE | \ZipArchive::OVERWRITE );

		$this->zip = $zip;
	}

	/**
	 * Init the manifest data and add some basic info to it.
	 */
	private function init_manifest_data() {
		$kit_post = Plugin::$instance->kits_manager->get_active_kit()->get_post();

		$manifest_data = [
			'name' => sanitize_title( $this->settings_kit_info['title'] ),
			'title' => $this->settings_kit_info['title'],
			'description' => $this->settings_kit_info['description'],
			'author' => get_the_author_meta( 'display_name', $kit_post->post_author ),
			'version' => Module::FORMAT_VERSION,
			'elementor_version' => ELEMENTOR_VERSION,
			'created' => gmdate( 'Y-m-d H:i:s' ),
			'thumbnail' => get_the_post_thumbnail_url( $kit_post ),
			'site' => get_site_url(),
		];

		$this->manifest_data = $manifest_data;
	}

	/**
	 * Handle the export process output.
	 * Add the manifest data from the runner to the manifest.json file.
	 * Create files according to the files array that should be exported by the runner.
	 *
	 * @param array $export_result
	 */
	private function handle_export_result( $export_result ) {
		foreach ( $export_result['manifest'] as $data ) {
			$this->manifest_data += $data;
		}

		if ( isset( $export_result['files']['path'] ) ) {
			$export_result['files'] = [ $export_result['files'] ];
		}

		foreach ( $export_result['files'] as $file ) {
			$file_extension = pathinfo( $file['path'], PATHINFO_EXTENSION );
			if ( empty( $file_extension ) ) {
				$this->add_json_file(
					$file['path'],
					$file['data']
				);
			} else {
				$this->add_file(
					$file['path'],
					$file['data']
				);
			}
		}
	}

	/**
	 * Add json file to the zip archive.
	 *
	 * @param string $path The relative path to the file.
	 * @param array  $content The content of the file.
	 * @param int    $json_flags
	 */
	private function add_json_file( $path, array $content, $json_flags = 0 ) {
		if ( ! Str::ends_with( $path, '.json' ) ) {
			$path .= '.json';
		}

		$this->add_file( $path, wp_json_encode( $content, $json_flags ) );
	}

	/**
	 * Add file to the zip archive.
	 *
	 * @param string $file
	 * @param string $content The content of the file.
	 */
	private function add_file( $file, $content ) {
		$this->zip->addFromString( $file, $content );
	}

	private function should_collect_media( $data ) {
		return (
			isset( $data['customization']['content']['mediaFormat'] ) &&
			'cloud' === $data['customization']['content']['mediaFormat']
		);
	}
}
PK     8\:T    8  modules/import-export-customization/processes/revert.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Processes;

use Elementor\App\Modules\ImportExportCustomization\Module;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Elementor_Content;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Revert_Runner_Base;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Plugins;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Site_Settings;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Taxonomies;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Templates;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Wp_Content;
use Elementor\App\Modules\ImportExportCustomization\Utils;

class Revert {

	/**
	 * @var Revert_Runner_Base[]
	 */
	protected $runners = [];

	private $import_sessions;

	private $revert_sessions;

	public function __construct() {
		$this->import_sessions = self::get_import_sessions();
		$this->revert_sessions = self::get_revert_sessions();
	}

	/**
	 * Register a runner.
	 *
	 * @param Revert_Runner_Base $runner_instance
	 */
	public function register( Revert_Runner_Base $runner_instance ) {
		$this->runners[ $runner_instance::get_name() ] = $runner_instance;
	}

	public function register_default_runners() {
		$this->register( new Site_Settings() );
		$this->register( new Plugins() );
		$this->register( new Templates() );
		$this->register( new Taxonomies() );
		$this->register( new Elementor_Content() );
		$this->register( new Wp_Content() );
	}

	/**
	 * Execute the revert process.
	 *
	 * @throws \Exception If no revert runners have been specified.
	 */
	public function run() {
		if ( empty( $this->runners ) ) {
			throw new \Exception( 'Couldn’t execute the revert process because no revert runners have been specified. Try again by specifying revert runners.' );
		}

		$import_session = $this->get_last_import_session();

		if ( empty( $import_session ) ) {
			throw new \Exception( 'Couldn’t execute the revert process because there are no import sessions to revert.' );
		}

		// fallback if the import session failed and doesn't have the runners metadata
		if ( ! isset( $import_session['runners'] ) && isset( $import_session['instance_data'] ) ) {
			$import_session['runners'] = $import_session['instance_data']['runners_import_metadata'] ?? [];
		}

		foreach ( $this->runners as $runner ) {
			if ( $runner->should_revert( $import_session ) ) {
				$runner->revert( $import_session );
			}
		}

		$this->revert_attachments( $import_session );

		$this->delete_last_import_data();
	}

	public static function get_import_sessions() {
		$import_sessions = Utils::get_import_sessions();

		if ( ! $import_sessions ) {
			return [];
		}

		usort( $import_sessions, function( $a, $b ) {
			return strcmp( $a['start_timestamp'], $b['start_timestamp'] );
		} );

		return $import_sessions;
	}

	public static function get_revert_sessions() {
		$revert_sessions = get_option( Module::OPTION_KEY_ELEMENTOR_REVERT_SESSIONS );

		if ( ! $revert_sessions ) {
			return [];
		}

		return $revert_sessions;
	}

	public function get_last_import_session() {
		$import_sessions = $this->import_sessions;

		if ( empty( $import_sessions ) ) {
			return [];
		}

		return end( $import_sessions );
	}

	public function get_penultimate_import_session() {
		$sessions_data = $this->import_sessions;
		$penultimate_element_value = [];

		if ( empty( $sessions_data ) ) {
			return [];
		}

		end( $sessions_data );

		prev( $sessions_data );

		if ( ! is_null( key( $sessions_data ) ) ) {
			$penultimate_element_value = current( $sessions_data );
		}

		return $penultimate_element_value;
	}

	private function delete_last_import_data() {
		$import_sessions = $this->import_sessions;
		$revert_sessions = $this->revert_sessions;

		$reverted_session = array_pop( $import_sessions );

		$revert_sessions[] = [
			'session_id' => $reverted_session['session_id'],
			'kit_title' => $reverted_session['kit_title'],
			'kit_name' => $reverted_session['kit_name'],
			'kit_thumbnail' => $reverted_session['kit_thumbnail'],
			'source' => $reverted_session['kit_source'],
			'user_id' => get_current_user_id(),
			'import_timestamp' => $reverted_session['start_timestamp'],
			'revert_timestamp' => current_time( 'timestamp' ),
		];

		update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
		update_option( Module::OPTION_KEY_ELEMENTOR_REVERT_SESSIONS, $revert_sessions, false );

		$this->import_sessions = $import_sessions;
		$this->revert_sessions = $revert_sessions;
	}

	private function revert_attachments( $data ) {
		$query_args = [
			'post_type' => 'attachment',
			'post_status' => 'any',
			'posts_per_page' => -1,
			'meta_query' => [
				[
					'key' => Module::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
					'value' => $data['session_id'],
				],
			],
		];

		$query = new \WP_Query( $query_args );

		foreach ( $query->posts as $post ) {
			wp_delete_attachment( $post->ID, true );
		}
	}
}
PK     8\4b9h  9h  8  modules/import-export-customization/processes/import.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Processes;

use Elementor\App\Modules\ImportExportCustomization\Compatibility\Base_Adapter;
use Elementor\App\Modules\ImportExportCustomization\Compatibility\Envato;
use Elementor\App\Modules\ImportExportCustomization\Compatibility\Kit_Library;
use Elementor\App\Modules\ImportExportCustomization\Compatibility\Customization;
use Elementor\App\Modules\ImportExportCustomization\Utils;
use Elementor\Core\Base\Document;
use Elementor\Core\Kits\Documents\Kit;
use Elementor\Plugin;

use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Elementor_Content;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Import_Runner_Base;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Plugins;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Site_Settings;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Taxonomies;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Templates;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Wp_Content;
use Elementor\App\Modules\ImportExportCustomization\Module;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Floating_Elements;

class Import {
	const MANIFEST_ERROR_KEY = 'manifest-error';

	const ZIP_FILE_ERROR_KEY = 'invalid-zip-file';

	const ZIP_ARCHIVE_ERROR_KEY = 'zip-archive-module-missing';

	/**
	 * @var Import_Runner_Base[]
	 */
	protected $runners = [];

	/**
	 * The session ID of the import process.
	 * This ID is uniquely generated for each import process (by the temp folder which contains the extracted kit files).
	 *
	 * @var string
	 */
	private $session_id;

	/**
	 * The Kit ID.
	 *
	 * @var string
	 */
	private $kit_id;

	/**
	 * Adapter for the kit compatibility.
	 *
	 * @var Base_Adapter[]
	 */
	private $adapters;

	/**
	 * Document's data (elements and settings) that was imported during the process.
	 *
	 * @var array { [document_id] => { "elements": array , "settings": array } }
	 */
	private $documents_data = [];

	/**
	 * Path to the extracted kit files.
	 *
	 * @var string
	 */
	private $extracted_directory_path;

	/**
	 * Imported kit manifest.
	 *
	 * @var array
	 */
	private $manifest;

	/**
	 * Imported kit site settings. (e.g: custom_colors, custom_typography, etc.)
	 *
	 * @var array
	 */
	private $site_settings;

	/**
	 * Selected content types to import.
	 *
	 * @var array
	 */
	private $settings_include;

	/**
	 * Referer of the import. (e.g: kit-library, local, etc.)
	 *
	 * @var string
	 */
	private $settings_referrer;

	/**
	 * All the conflict between the exited templates and the kit templates.
	 *
	 * @var array
	 */
	private $settings_conflicts;

	/**
	 * Selected elementor templates conditions to override.
	 *
	 * @var array
	 */
	private $settings_selected_override_conditions;

	/**
	 * Selected custom post types to import.
	 *
	 * @var array
	 */
	private $settings_selected_custom_post_types;

	/**
	 * Selected plugins to import.
	 *
	 * @var array
	 */
	private $settings_selected_plugins;

	/**
	 * Customization settings for selective import.
	 *
	 * @var array
	 */
	private $settings_customization;

	/**
	 * The imported data output.
	 *
	 * @var array
	 */
	private $imported_data = [];

	/**
	 * The metadata output of the import runners.
	 * Will be saved in the import_session and will be used to revert the import process.
	 *
	 * @var array
	 */
	private $runners_import_metadata = [];

	/**
	 * @param string     $path session_id | zip_file_path
	 * @param array      $settings Use to determine which content to import.
	 *           (e.g: include, selected_plugins, selected_cpt, selected_override_conditions, etc.)
	 * @param array|null $old_instance An array of old instance parameters that will be used for creating new instance.
	 *      We are using it for quick creation of the instance when the import process is being split into chunks.
	 * @throws \Exception If the import session does not exist.
	 */
	public function __construct( string $path, array $settings = [], array $old_instance = null ) {
		if ( ! empty( $old_instance ) ) {
			$this->set_import_object( $old_instance );
		} else {
			if ( is_file( $path ) ) {
				$this->extracted_directory_path = $this->extract_zip( $path );
			} else {
				$elementor_tmp_directory = Plugin::$instance->uploads_manager->get_temp_dir();
				$path = $elementor_tmp_directory . basename( $path );

				if ( ! is_dir( $path ) ) {
					throw new \Exception( 'Couldn’t execute the import process because the import session does not exist.' );
				}

				$this->extracted_directory_path = $path . '/';
			}

			$this->session_id = basename( $this->extracted_directory_path );
			$this->kit_id = $settings['id'] ?? '';
			$this->settings_referrer = ! empty( $settings['referrer'] ) ? $settings['referrer'] : 'local';
			$this->settings_include = ! empty( $settings['include'] ) ? $settings['include'] : null;

			// Using isset and not empty is important since empty array is valid option.
			$this->settings_selected_override_conditions = $settings['customization']['templates']['themeBuilder']['overrideConditions'] ?? null;
			$this->settings_selected_custom_post_types = $settings['customization']['content']['customPostTypes'] ?? null;
			$this->settings_selected_plugins = $settings['plugins'] ?? null;
			$this->settings_customization = $settings['customization'] ?? null;

			$this->manifest = $this->read_manifest_json();
			$this->site_settings = $this->read_site_settings_json();

			$this->set_default_settings();
		}

		add_filter( 'wp_php_error_args', function ( $args, $error ) {
			return $this->filter_php_error_args( $args, $error );
		}, 10, 2 );
	}

	/**
	 * Set the import object parameters.
	 *
	 * @param array $instance
	 * @return void
	 */
	private function set_import_object( array $instance ) {
		$this->session_id = $instance['session_id'];

		$instance_data = $instance['instance_data'];

		$this->extracted_directory_path = $instance_data['extracted_directory_path'];
		$this->runners = $instance_data['runners'];
		$this->adapters = $instance_data['adapters'];

		$this->manifest = $instance_data['manifest'];
		$this->site_settings = $instance_data['site_settings'];

		$this->kit_id = $instance_data['kit_id'] ?? '';
		$this->settings_include = $instance_data['settings_include'];
		$this->settings_referrer = $instance_data['settings_referrer'];
		$this->settings_conflicts = $instance_data['settings_conflicts'];
		$this->settings_selected_override_conditions = $instance_data['settings_selected_override_conditions'];
		$this->settings_selected_custom_post_types = $instance_data['settings_selected_custom_post_types'];
		$this->settings_selected_plugins = $instance_data['settings_selected_plugins'];
		$this->settings_customization = $instance_data['settings_customization'];

		$this->documents_data = $instance_data['documents_data'];
		$this->imported_data = $instance_data['imported_data'];
		$this->runners_import_metadata = $instance_data['runners_import_metadata'];
	}

	/**
	 * Creating a new instance of the import process by the id of the old import session.
	 *
	 * @param string $session_id
	 *
	 * @return Import
	 * @throws \Exception If the import session does not exist.
	 */
	public static function from_session( string $session_id ): Import {
		$import_sessions = Utils::get_import_sessions();

		if ( ! $import_sessions || ! isset( $import_sessions[ $session_id ] ) ) {
			throw new \Exception( 'Couldn’t execute the import process because the import session does not exist.' );
		}

		$import_session = $import_sessions[ $session_id ];

		return new self( $session_id, [], $import_session );
	}

	/**
	 * Register a runner.
	 * Be aware that the runner will be executed in the order of registration, the order is crucial for the import process.
	 *
	 * @param Import_Runner_Base $runner_instance
	 */
	public function register( Import_Runner_Base $runner_instance ) {
		$this->runners[ $runner_instance::get_name() ] = $runner_instance;
	}

	public function register_default_runners() {
		$this->register( new Site_Settings() );
		$this->register( new Plugins() );
		$this->register( new Templates() );
		$this->register( new Taxonomies() );
		$this->register( new Elementor_Content() );
		$this->register( new Wp_Content() );
		$this->register( new Floating_Elements() );
	}

	/**
	 * Set default settings for the import.
	 */
	private function set_default_settings() {
		if ( ! is_array( $this->get_settings_include() ) ) {
			$this->settings_include( $this->get_default_settings_include() );
		}

		if ( ! is_array( $this->get_settings_conflicts() ) ) {
			$this->settings_conflicts( $this->get_default_settings_conflicts() );
		}

		if ( ! is_array( $this->get_settings_selected_override_conditions() ) ) {
			$this->settings_selected_override_conditions( $this->get_default_settings_override_conditions() );
		}

		if ( ! is_array( $this->get_settings_selected_custom_post_types() ) ) {
			$this->settings_selected_custom_post_types( $this->get_default_settings_custom_post_types() );
		}

		if ( ! is_array( $this->get_settings_selected_plugins() ) ) {
			$this->settings_selected_plugins( $this->get_default_settings_plugins() );
		}

		if ( ! is_array( $this->get_settings_customization() ) ) {
			$this->settings_customization( $this->get_default_settings_customization() );
		}
	}

	/**
	 * Execute the import process.
	 *
	 * @return array The imported data output.
	 *
	 * @throws \Exception If no import runners have been specified.
	 */
	public function run() {
		if ( empty( $this->runners ) ) {
			throw new \Exception( 'Couldn’t execute the import process because no import runners have been specified. Try again by specifying import runners.' );
		}

		$data = [
			'session_id' => $this->session_id,
			'include' => $this->settings_include,
			'manifest' => $this->manifest,
			'site_settings' => $this->site_settings,
			'selected_plugins' => $this->settings_selected_plugins,
			'customization' => $this->settings_customization,
			'extracted_directory_path' => $this->extracted_directory_path,
			'selected_custom_post_types' => $this->settings_selected_custom_post_types,
		];

		$this->init_import_session();

		remove_filter( 'elementor/document/save/data', [ Plugin::$instance->modules_manager->get_modules( 'content-sanitizer' ), 'sanitize_content' ] );
		add_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10, 2 );

		// Set the Request's state as an Elementor upload request, in order to support unfiltered file uploads.
		Plugin::$instance->uploads_manager->set_elementor_upload_state( true );

		foreach ( $this->runners as $runner ) {
			if ( $runner->should_import( $data ) ) {
				$import = $runner->import( $data, $this->imported_data );
				$this->imported_data = array_merge_recursive( $this->imported_data, $import );

				$this->runners_import_metadata[ $runner::get_name() ] = $runner->get_import_session_metadata();
			}
		}

		// After the upload complete, set the elementor upload state back to false.
		Plugin::$instance->uploads_manager->set_elementor_upload_state( false );

		remove_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10 );

		$this->finalize_import_session_option();

		$this->save_elements_of_imported_posts();

		Plugin::$instance->uploads_manager->remove_file_or_dir( $this->extracted_directory_path );
		return $this->imported_data;
	}

	/**
	 * Run specific runner by runner_name
	 *
	 * @param string $runner_name
	 *
	 * @return array
	 *
	 * @throws \Exception If no export runners have been specified.
	 */
	public function run_runner( string $runner_name ): array {
		if ( empty( $this->runners ) ) {
			throw new \Exception( 'Couldn’t execute the import process because no import runners have been specified. Try again by specifying import runners.' );
		}

		$data = [
			'session_id' => $this->session_id,
			'include' => $this->settings_include,
			'manifest' => $this->manifest,
			'site_settings' => $this->site_settings,
			'selected_plugins' => $this->settings_selected_plugins,
			'customization' => $this->settings_customization,
			'extracted_directory_path' => $this->extracted_directory_path,
			'selected_custom_post_types' => $this->settings_selected_custom_post_types,
		];

		add_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10, 2 );

		// Set the Request's state as an Elementor upload request, in order to support unfiltered file uploads.
		Plugin::$instance->uploads_manager->set_elementor_upload_state( true );

		$runner = $this->runners[ $runner_name ];

		if ( empty( $runner ) ) {
			throw new \Exception( 'Couldn’t execute the import process because the import runner was not found. Try again by specifying an import runner.' );
		}

		if ( $runner->should_import( $data ) ) {
			$import = $runner->import( $data, $this->imported_data );
			$this->imported_data = array_merge_recursive( $this->imported_data, $import );

			$this->runners_import_metadata[ $runner::get_name() ] = $runner->get_import_session_metadata();
		}

		// After the upload complete, set the elementor upload state back to false.
		Plugin::$instance->uploads_manager->set_elementor_upload_state( false );

		remove_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10 );

		$is_last_runner = key( array_slice( $this->runners, -1, 1, true ) ) === $runner_name;
		if ( $is_last_runner ) {
			$this->finalize_import_session_option();
			$this->save_elements_of_imported_posts();
		} else {
			$this->update_instance_data_in_import_session_option();
		}

		return [
			'status' => 'success',
			'runner' => $runner_name,
			'imported_data' => $this->imported_data,
		];
	}

	/**
	 * Create and save all the instance data to the import sessions option.
	 *
	 * @return void
	 */
	public function init_import_session( $save_instance_data = false ) {
		$import_sessions = Utils::get_import_sessions( true );
		$existing_session = $import_sessions[ $this->session_id ] ?? [];

		$import_sessions[ $this->session_id ] = [
			'session_id' => $this->session_id,
			'kit_title' => $this->manifest['title'] ?? '',
			'kit_name' => $this->manifest['name'] ?? '',
			'kit_thumbnail' => $existing_session['kit_thumbnail'] ?? $this->get_kit_thumbnail(),
			'kit_source' => $existing_session['kit_source'] ?? $this->settings_referrer,
			'user_id' => get_current_user_id(),
			'start_timestamp' => $existing_session['start_timestamp'] ?? current_time( 'timestamp' ),
		];

		if ( $save_instance_data ) {
			$import_sessions[ $this->session_id ]['instance_data'] = [
				'extracted_directory_path' => $this->extracted_directory_path,
				'runners' => $this->runners,
				'adapters' => $this->adapters,

				'manifest' => $this->manifest,
				'site_settings' => $this->site_settings,

				'kit_id' => $this->kit_id,
				'settings_include' => $this->settings_include,
				'settings_referrer' => $this->settings_referrer,
				'settings_conflicts' => $this->settings_conflicts,
				'settings_selected_override_conditions' => $this->settings_selected_override_conditions,
				'settings_selected_custom_post_types' => $this->settings_selected_custom_post_types,
				'settings_selected_plugins' => $this->settings_selected_plugins,
				'settings_customization' => $this->settings_customization,

				'documents_data' => $this->documents_data,
				'imported_data' => $this->imported_data,
				'runners_import_metadata' => $this->runners_import_metadata,
			];
		}

		update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
	}

	/**
	 * Get the Kit thumbnail, goes to the home page thumbnail if main doesn't exist
	 *
	 * @return string
	 */
	private function get_kit_thumbnail(): string {
		if ( ! empty( $this->manifest['thumbnail'] ) ) {
			return $this->manifest['thumbnail'];
		}

		return apply_filters( 'elementor/import/kit_thumbnail', '', $this->kit_id, $this->settings_referrer );
	}

	public function get_runners_name(): array {
		return array_keys( $this->runners );
	}

	public function get_manifest() {
		return $this->manifest;
	}

	public function get_extracted_directory_path() {
		return $this->extracted_directory_path;
	}

	public function get_session_id() {
		return $this->session_id;
	}

	public function get_adapters() {
		return $this->adapters;
	}

	public function get_imported_data() {
		return $this->imported_data;
	}

	/**
	 * Get settings by key.
	 * Used for backward compatibility.
	 *
	 * @param string $key The key of the setting.
	 */
	public function get_settings( $key ) {
		switch ( $key ) {
			case 'include':
				return $this->get_settings_include();

			case 'overrideConditions':
				return $this->get_settings_selected_override_conditions();

			case 'selectedCustomPostTypes':
				return $this->get_settings_selected_custom_post_types();

			case 'plugins':
				return $this->get_settings_selected_plugins();

			case 'customization':
				return $this->get_settings_customization();

			default:
				return [];
		}
	}

	public function settings_include( array $settings_include ) {
		$this->settings_include = $settings_include;

		return $this;
	}

	public function get_settings_include() {
		return $this->settings_include;
	}

	public function settings_referrer( $settings_referrer ) {
		$this->settings_referrer = $settings_referrer;

		return $this;
	}

	public function get_settings_referrer() {
		return $this->settings_referrer;
	}

	public function settings_conflicts( array $settings_conflicts ) {
		$this->settings_conflicts = $settings_conflicts;

		return $this;
	}

	public function get_settings_conflicts() {
		return $this->settings_conflicts;
	}

	public function settings_selected_override_conditions( array $settings_selected_override_conditions ) {
		$this->settings_selected_override_conditions = $settings_selected_override_conditions;

		return $this;
	}

	public function get_settings_selected_override_conditions() {
		return $this->settings_selected_override_conditions;
	}

	public function settings_selected_custom_post_types( array $settings_selected_custom_post_types ) {
		$this->settings_selected_custom_post_types = $settings_selected_custom_post_types;

		return $this;
	}

	public function get_settings_selected_custom_post_types() {
		return $this->settings_selected_custom_post_types;
	}

	public function settings_selected_plugins( array $settings_selected_plugins ) {
		$this->settings_selected_plugins = $settings_selected_plugins;

		return $this;
	}

	public function get_settings_selected_plugins() {
		return $this->settings_selected_plugins;
	}

	/**
	 * Prevent saving elements on elementor post creation.
	 *
	 * @param array    $data
	 * @param Document $document
	 *
	 * @return array
	 */
	public function prevent_saving_elements_on_post_creation( array $data, Document $document ) {
		if ( isset( $data['elements'] ) ) {
			$this->documents_data[ $document->get_main_id() ] = [ 'elements' => $data['elements'] ];

			$data['elements'] = [];
		}

		if ( isset( $data['settings'] ) ) {
			$this->documents_data[ $document->get_main_id() ]['settings'] = $data['settings'];

		}

		return $data;
	}

	/**
	 * Extract the zip file.
	 *
	 * @param string $zip_path The path to the zip file.
	 * @return string The extracted directory path.
	 * @throws \Error If import process fails, file validation errors occur, or data corruption is detected.
	 */
	private function extract_zip( $zip_path ) {
		$extraction_result = Plugin::$instance->uploads_manager->extract_and_validate_zip( $zip_path, [ 'json', 'xml' ] );

		if ( is_wp_error( $extraction_result ) ) {
			if ( isset( $extraction_result->errors['zip_error'] ) ) {
				throw new \Error( static::ZIP_ARCHIVE_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			}

			throw new \Error( static::ZIP_FILE_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		return $extraction_result['extraction_directory'];
	}

	/**
	 * Get the manifest file from the extracted directory and adapt it if needed.
	 *
	 * @return string The manifest file content.
	 * @throws \Error If import validation fails or processing errors occur.
	 */
	private function read_manifest_json() {
		$manifest = Utils::read_json_file( $this->extracted_directory_path . 'manifest' );

		if ( ! $manifest ) {
			Plugin::$instance->logger->get_logger()->error( static::MANIFEST_ERROR_KEY );
			throw new \Error( static::ZIP_FILE_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		$this->init_adapters( $manifest );

		foreach ( $this->adapters as $adapter ) {
			$manifest = $adapter->adapt_manifest( $manifest );
		}

		return $manifest;
	}

	/**
	 * Init the adapters and determine which ones to use.
	 *
	 * @param array $manifest_data The manifest file content.
	 */
	private function init_adapters( array $manifest_data ) {
		$this->adapters = [];

		/** @var Base_Adapter[] $adapter_types */
		$adapter_types = [ Customization::class, Envato::class, Kit_Library::class ];

		foreach ( $adapter_types as $adapter_type ) {
			if ( $adapter_type::is_compatibility_needed( $manifest_data, [ 'referrer' => $this->get_settings_referrer() ] ) ) {
				$this->adapters[] = new $adapter_type( $this );
			}
		}
	}

	/**
	 * Get the site settings file from the extracted directory and adapt it if needed.
	 *
	 * @return string The site settings file content.
	 */
	private function read_site_settings_json() {
		$site_settings = Utils::read_json_file( $this->extracted_directory_path . 'site-settings' );

		foreach ( $this->adapters as $adapter ) {
			$site_settings = $adapter->adapt_site_settings( $site_settings, $this->manifest, $this->extracted_directory_path );
		}

		return $site_settings;
	}

	/**
	 * Get all the custom post types in the kit.
	 *
	 * @return array Custom post types names.
	 */
	private function get_default_settings_custom_post_types() {
		$excluded = [ 'page', 'nav_menu_item' ];

		if ( empty( $this->manifest['content']['post'] ?? [] ) && empty( $this->manifest['wp-content']['post'] ?? [] ) ) {
			$excluded[] = 'post';
		}

		$manifest_post_types = array_keys( $this->manifest['custom-post-type-title'] ?? [] );

		return array_merge( $manifest_post_types, Utils::get_builtin_wp_post_types( $excluded ) );
	}

	/**
	 * Get the default settings of elementor templates conditions to override.
	 *
	 * @return array
	 */
	private function get_default_settings_conflicts() {
		if ( empty( $this->manifest['templates'] ) ) {
			return [];
		}

		return apply_filters( 'elementor/import/get_default_settings_conflicts', [], $this->manifest['templates'] );
	}

	/**
	 * Get the default settings of elementor templates conditions to override.
	 *
	 * @return array
	 */
	private function get_default_settings_override_conditions() {
		if ( empty( $this->settings_conflicts ) ) {
			return [];
		}

		return array_keys( $this->settings_conflicts );
	}

	/**
	 * Get the default settings of the plugins that should be imported.
	 *
	 * @return array
	 */
	private function get_default_settings_plugins() {
		return ! empty( $this->manifest['plugins'] ) ? $this->manifest['plugins'] : [];
	}

	/**
	 * Get the default settings of which content types should be imported.
	 *
	 * @return array
	 */
	private function get_default_settings_include() {
		return [ 'templates', 'plugins', 'content', 'settings' ];
	}

	public function settings_customization( $customization ) {
		$this->settings_customization = $customization;
		return $this;
	}

	public function get_settings_customization() {
		return $this->settings_customization;
	}

	private function get_default_settings_customization() {
		return [
			'settings' => null,
			'templates' => null,
			'content' => null,
			'plugins' => null,
		];
	}

	/**
	 * Get the data that requires updating/replacement when imported.
	 *
	 * @return array{post_ids: array, term_ids: array}
	 */
	private function get_imported_data_replacements(): array {
		return [
			'post_ids' => Utils::map_old_new_post_ids( $this->imported_data ),
			'term_ids' => Utils::map_old_new_term_ids( $this->imported_data ),
		];
	}

	/**
	 * Save the prevented elements on elementor post creation elements.
	 * Handle the replacement of all the dynamic content of the elements that probably have been changed during the import.
	 */
	private function save_elements_of_imported_posts() {
		$imported_data_replacements = $this->get_imported_data_replacements();

		foreach ( $this->documents_data as $new_id => $data ) {
			$document = Plugin::$instance->documents->get( $new_id );

			if ( isset( $data['elements'] ) ) {
				$data['elements'] = $document->on_import_update_dynamic_content( $data['elements'], $imported_data_replacements );
			}

			if ( isset( $data['settings'] ) ) {

				if ( $document instanceof Kit ) {
					// Without post_status certain tabs in the Kit will not save properly.
					$data['settings']['post_status'] = get_post_status( $new_id );
				}

				$data['settings'] = $document->on_import_update_settings( $data['settings'], $imported_data_replacements );
			}

			$document->save( $data );
		}
	}

	private function update_instance_data_in_import_session_option() {
		$import_sessions = Utils::get_import_sessions();

		$import_sessions[ $this->session_id ]['instance_data']['documents_data'] = $this->documents_data;
		$import_sessions[ $this->session_id ]['instance_data']['imported_data'] = $this->imported_data;
		$import_sessions[ $this->session_id ]['instance_data']['runners_import_metadata'] = $this->runners_import_metadata;

		update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
	}

	public function finalize_import_session_option() {
		$import_sessions = Utils::get_import_sessions();

		if ( ! isset( $import_sessions[ $this->session_id ] ) ) {
			return;
		}

		unset( $import_sessions[ $this->session_id ]['instance_data'] );

		$import_sessions[ $this->session_id ]['end_timestamp'] = current_time( 'timestamp' );
		$import_sessions[ $this->session_id ]['runners'] = $this->runners_import_metadata;

		update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
	}

	/**
	 * Filter the php error args and return 408 status code if the error is a timeout.
	 *
	 * @param array $args
	 * @param array $error
	 * @return array
	 */
	private function filter_php_error_args( $args, $error ) {
		if ( strpos( $error['message'], 'Maximum execution time' ) !== false ) {
			$args['response'] = 408;
		}

		return $args;
	}
}
PK     8\yf8    5  modules/import-export-customization/data/response.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Data;

class Response {
	private array $data;
	private array $meta;

	public function __construct( array $data, array $meta = [] ) {
		$this->data = $data;
		$this->meta = $meta;
	}

	public static function success( array $data, array $meta = [] ): \WP_REST_Response {
		$response = new self( $data, $meta );
		return $response->to_wp_rest_response( 200 );
	}

	public static function error( string $code, $message, array $meta = [] ): \WP_REST_Response {
		$response = new self([
			'code' => $code,
			'message' => $message,
		], $meta);

		return $response->to_wp_rest_response( 500 );
	}

	private function to_array(): array {
		return [
			'data' => $this->data,
			'meta' => $this->meta,
		];
	}

	private function to_wp_rest_response( int $status_code = 200 ): \WP_REST_Response {
		return new \WP_REST_Response( $this->to_array(), $status_code );
	}
}
PK     8\joi  i  ?  modules/import-export-customization/data/routes/manager-url.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes;

use Elementor\Plugin;
use Elementor\App\Modules\ImportExportCustomization\Data\Response;
use Elementor\Core\Base\Document;

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

class Manager_Url extends Base_Route {
	private const ALLOWED_PANELS = [
		'global-classes-manager',
		'variables-manager',
	];

	protected function get_route(): string {
		return 'manager-url';
	}

	protected function get_method(): string {
		return \WP_REST_Server::READABLE;
	}

	protected function callback( $request ): \WP_REST_Response {
		$panel = $request->get_param( 'panel' );

		if ( ! in_array( $panel, self::ALLOWED_PANELS, true ) ) {
			return Response::error( 'Invalid panel parameter', 'invalid_panel' );
		}

		$url = $this->get_editor_url_with_panel( $panel );

		return Response::success( [
			'url' => $url,
		] );
	}

	private function get_editor_url_with_panel( string $panel ): string {
		$elementor_page = $this->get_elementor_page();

		if ( $elementor_page ) {
			$document = Plugin::$instance->documents->get( $elementor_page->ID );

			if ( $document && $document->is_built_with_elementor() ) {
				return add_query_arg( 'active-panel', $panel, $document->get_edit_url() );
			}
		}

		return add_query_arg(
			'active-panel',
			$panel,
			Plugin::$instance->documents->get_create_new_post_url( 'page' )
		);
	}

	private function get_elementor_page() {
		$pages = get_pages( [
			'post_status' => [ 'publish', 'draft' ],
			'meta_key' => Document::BUILT_WITH_ELEMENTOR_META_KEY,
			'sort_order' => 'desc',
			'sort_column' => 'post_modified',
			'number' => 1,
		] );

		return $pages[0] ?? null;
	}

	protected function get_args(): array {
		return [
			'panel' => [
				'type' => 'string',
				'description' => 'Panel ID to open in the editor',
				'required' => true,
				'enum' => self::ALLOWED_PANELS,
			],
		];
	}
}
PK     8\Wh    A  modules/import-export-customization/data/routes/process-media.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes;

use Elementor\Plugin;
use Elementor\App\Modules\ImportExportCustomization\Data\Response;
use Elementor\App\Modules\ImportExportCustomization\Module as ImportExportCustomizationModule;
use Elementor\Modules\CloudKitLibrary\Module as CloudKitLibrary;
use Elementor\Utils as ElementorUtils;
use Elementor\App\Modules\ImportExportCustomization\Data\Routes\Traits\Handles_Quota_Errors;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Process_Media extends Base_Route {
	use Handles_Quota_Errors;

	protected function get_route(): string {
		return 'process-media';
	}

	protected function get_method(): string {
		return \WP_REST_Server::CREATABLE;
	}

	protected function callback( $request ): \WP_REST_Response {
		/**
		 * @var $module ImportExportCustomizationModule
		 */
		$module = Plugin::$instance->app->get_component( 'import-export-customization' );

		$cloud_kit_library_app = $this->get_cloud_kit_library_app();

		if ( $cloud_kit_library_app && ! $cloud_kit_library_app->is_connected() ) {
			return Response::error( ImportExportCustomizationModule::MEDIA_PROCESSING_ERROR, 'Cloud Library is not connected' );
		}

		$media_urls = $request->get_param( 'media_urls' );
		$kit = $request->get_param( 'kit' );
		$quota = null;

		try {
			if ( empty( $media_urls ) || ! is_array( $media_urls ) ) {
				throw new \Error( 'Invalid media URLs provided' );
			}

			$media_collector = new \Elementor\TemplateLibrary\Classes\Media_Collector();
			$zip_path = $media_collector->process_media_collection( $media_urls );

			if ( $cloud_kit_library_app ) {
				$quota = $cloud_kit_library_app->get_quota();
				$cloud_kit_library_app->validate_storage_quota( filesize( $zip_path ), $quota );
			}

			if ( ! $zip_path ) {
				throw new \Error( 'Failed to process media' );
			}

			$zip_file = ElementorUtils::file_get_contents( $zip_path );

			$upload_success = false;
			if ( $cloud_kit_library_app ) {
				$upload_success = $cloud_kit_library_app->upload_content_file( $kit['mediaUploadUrl'], $zip_file );
				$cloud_kit_library_app->update_kit( $kit['id'], [ 'mediaFileId' => $upload_success ? $kit['mediaFileId'] : null ] );
			}

			$media_collector->cleanup();

			return Response::success( [
				'success' => true,
				'message' => 'Media processed and uploaded successfully',
			] );

		} catch ( \Error | \Exception $e ) {
			Plugin::$instance->logger->get_logger()->error( $e->getMessage(), [
				'meta' => [
					'trace' => $e->getTraceAsString(),
				],
			] );

			if ( $cloud_kit_library_app ) {
				$cloud_kit_library_app->update_kit( $kit['id'], [ 'mediaFileId' => null ] );
			}

			if ( $module->is_third_party_class( $e->getTrace()[0]['class'] ) ) {
				return Response::error( ImportExportCustomizationModule::THIRD_PARTY_ERROR, $e->getMessage() );
			}

			if ( $this->is_quota_error( $e->getMessage() ) ) {
				return $this->get_quota_error_response( $quota, $kit );
			}

			return Response::error( ImportExportCustomizationModule::MEDIA_PROCESSING_ERROR, $e->getMessage() );
		}
	}

	protected function get_args(): array {
		return [
			'media_urls' => [
				'type' => 'array',
				'description' => 'Array of media URLs to process',
				'required' => true,
				'items' => [
					'type' => 'string',
				],
			],
		];
	}
}
PK     8\    :  modules/import-export-customization/data/routes/export.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes;

use Elementor\Plugin;
use Elementor\App\Modules\ImportExportCustomization\Data\Response;
use Elementor\Utils as ElementorUtils;
use Elementor\App\Modules\ImportExportCustomization\Module as ImportExportCustomizationModule;
use Elementor\App\Modules\ImportExportCustomization\Processes\Import;
use Elementor\App\Modules\ImportExportCustomization\Data\Routes\Traits\Handles_Quota_Errors;


if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Export extends Base_Route {
	use Handles_Quota_Errors;

	protected function get_route(): string {
		return 'export';
	}

	protected function get_method(): string {
		return \WP_REST_Server::CREATABLE;
	}

	protected function callback( $request ): \WP_REST_Response {
		/**
		 * @var $module ImportExportCustomizationModule
		 */
		$module = Plugin::$instance->app->get_component( 'import-export-customization' );

		try {
			$settings = [
				'include' => $request->get_param( 'include' ),
				'kitInfo' => $request->get_param( 'kitInfo' ),
				'screenShotBlob' => $request->get_param( 'screenShotBlob' ),
				'customization' => $request->get_param( 'customization' ),
				'plugins' => $request->get_param( 'plugins' ),
				'selectedCustomPostTypes' => $request->get_param( 'selectedCustomPostTypes' ),
			];

			$settings = array_filter( $settings );

			$source = $settings['kitInfo']['source'];

			$export = $module->export_kit( $settings );

			$file_name = $export['file_name'];
			$file_size = filesize( $file_name );
			$file = ElementorUtils::file_get_contents( $file_name );

			if ( ! $file ) {
				throw new \Error( Import::ZIP_FILE_ERROR_KEY );
			}

			Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $file_name ) );

			$result = apply_filters(
				'elementor/export/kit/export-result',
				[
					'manifest' => $export['manifest'],
					'file' => base64_encode( $file ),
					'media_urls' => $export['media_urls'],
				],
				$source,
				$export,
				$settings,
				$file,
				$file_size,
			);

			if ( is_wp_error( $result ) ) {
				throw new \Error( $result->get_error_message() );
			}

			return Response::success( $result );

		} catch ( \Error | \Exception $e ) {
			Plugin::$instance->logger->get_logger()->error( $e->getMessage(), [
				'meta' => [
					'trace' => $e->getTraceAsString(),
				],
			] );

			if ( $module->is_third_party_class( $e->getTrace()[0]['class'] ) ) {
				return Response::error( ImportExportCustomizationModule::THIRD_PARTY_ERROR, $e->getMessage() );
			}

			if ( $this->is_quota_error( $e->getMessage() ) ) {
				$quota = null;
				$cloud_kit_library_app = $this->get_cloud_kit_library_app();

				if ( $cloud_kit_library_app ) {
					try {
						$quota = $cloud_kit_library_app->get_quota();
					} catch ( \Exception | \Error $quota_error ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
						// Quota fetch failed, error message will use default value.
					}
				}

				return $this->get_quota_error_response( $quota, $settings['kitInfo'] ?? [] );
			}

			return Response::error( $e->getMessage(), 'export_error' );
		}
	}

	protected function get_args(): array {
		return [
			'include' => [
				'type' => 'array',
				'description' => 'Content types to include in export',
				'required' => false,
				'default' => [ 'templates', 'content', 'settings', 'plugins' ],
			],
			'kitInfo' => [
				'type' => 'object',
				'description' => 'Kit information',
				'required' => false,
				'default' => [
					'title' => 'Elementor Website Template',
					'description' => '',
					'source' => 'local',
				],
			],
			'screenShotBlob' => [
				'type' => [ 'string', 'null' ],
				'description' => 'Base64 encoded screenshot for cloud exports',
				'required' => false,
				'default' => null,
			],
			'customization' => [
				'type' => 'object',
				'description' => 'Customization settings for selective export',
				'required' => false,
				'default' => null,
				'properties' => [
					'settings' => [
						'type' => [ 'object', 'null' ],
						'description' => 'Site settings customization',
					],
					'templates' => [
						'type' => [ 'object', 'null' ],
						'description' => 'Templates customization',
					],
					'content' => [
						'type' => [ 'object', 'null' ],
						'description' => 'Content customization',
					],
					'plugins' => [
						'type' => [ 'object', 'null' ],
						'description' => 'Plugins customization',
					],
				],
			],
			'plugins' => [
				'type' => 'array',
				'description' => 'Selected plugins to export',
				'required' => false,
				'default' => [],
			],
			'selectedCustomPostTypes' => [
				'type' => 'array',
				'description' => 'Selected custom post types',
				'required' => false,
				'default' => [],
			],
		];
	}
}
PK     8\Dր    :  modules/import-export-customization/data/routes/upload.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes;

use Elementor\App\Modules\ImportExportCustomization\Module as ImportExportCustomizationModule;
use Elementor\Plugin;
use Elementor\App\Modules\ImportExportCustomization\Data\Response;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Upload extends Base_Route {
	protected function get_route(): string {
		return 'upload';
	}

	protected function get_method(): string {
		return \WP_REST_Server::CREATABLE;
	}

	private function format_url( string $url ): string {
		return wp_unslash( urldecode( $url ) );
	}

	/**
	 * @param $request \WP_REST_Request
	 * @return \WP_REST_Response
	 */
	protected function callback( $request ): \WP_REST_Response {
		/**
		 * @var $module ImportExportCustomizationModule
		 */
		$module = Plugin::$instance->app->get_component( 'import-export-customization' );

		try {
			$file_url = $request->get_param( 'file_url' );
			$kit_id = $request->get_param( 'kit_id' );
			$source = $request->get_param( 'source' );
			$module = Plugin::$instance->app->get_component( 'import-export-customization' );

			$is_import_from_library = ! empty( $file_url );
			if ( $is_import_from_library ) {
				$file_url = $this->format_url( $file_url );
			}

			if ( $is_import_from_library ) {
				if ( ! filter_var( $file_url, FILTER_VALIDATE_URL ) || 0 !== strpos( $file_url, 'http' ) ) {
					return Response::error( ImportExportCustomizationModule::KIT_LIBRARY_ERROR_KEY, 'Invalid kit library URL.' );
				}

				$import_result = apply_filters( 'elementor/import/kit/result', [ 'file_url' => $file_url ] );
			} elseif ( ! empty( $source ) ) {
				$import_result = apply_filters( 'elementor/import/kit/result/' . $source, [
					'kit_id' => $kit_id,
					'source' => $source,
				] );
			} else {
				$files = $request->get_file_params();
				$file = $files['e_import_file'] ?? null;

				if ( empty( $file ) || empty( $file['tmp_name'] ) ) {
					return Response::error( 'no_file_uploaded', 'No file uploaded or upload error occurred.' );
				}

				$import_result = [
					'file_name' => $file['tmp_name'],
					'referrer' => $module::REFERRER_LOCAL,
				];
			}

			Plugin::$instance->logger->get_logger()->info( 'Uploading Kit via REST API: ', [
				'meta' => [
					'kit_id' => $kit_id,
					'referrer' => $import_result['referrer'] ?? 'unknown',
				],
			] );

			if ( is_wp_error( $import_result ) ) {
				return Response::error( $import_result->get_error_message(), 'upload_error' );
			}

			if ( ! empty( $import_result['media_file_name'] ) ) {
				$this->setup_media_mapping( $import_result['media_file_name'] );
			}

			$uploaded_kit = $module->upload_kit( $import_result['file_name'], $import_result['referrer'], $kit_id );

			$result = [
				'session' => $uploaded_kit['session'],
				'manifest' => $uploaded_kit['manifest'],
			];

			if ( ! empty( $import_result['file_url'] ) ) {
				$result['file_url'] = $import_result['file_url'];
			}

			if ( ! empty( $import_result['kit'] ) ) {
				$result['uploaded_kit'] = $import_result['kit'];
			}

			if ( ! empty( $uploaded_kit['conflicts'] ) ) {
				$result['conflicts'] = $uploaded_kit['conflicts'];
			}

			// Clean up temporary files
			if ( $is_import_from_library || ! empty( $source ) ) {
				Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $import_result['file_name'] ) );
			}

			return Response::success( $result );

		} catch ( \Error | \Exception $e ) {
			Plugin::$instance->logger->get_logger()->error( $e->getMessage(), [
				'meta' => [
					'trace' => $e->getTraceAsString(),
				],
			] );

			if ( $module->is_third_party_class( $e->getTrace()[0]['class'] ) ) {
				return Response::error( ImportExportCustomizationModule::THIRD_PARTY_ERROR, $e->getMessage() );
			}

			return Response::error( $e->getMessage(), 'upload_error' );
		}
	}

	private function setup_media_mapping( $media_zip_path ) {
		\Elementor\TemplateLibrary\Classes\Media_Mapper::clear_mapping();

		$media_dir = null;

		if ( file_exists( $media_zip_path ) ) {
			$media_dir = $this->extract_media_zip( $media_zip_path );
		}

		if ( $media_dir && file_exists( $media_dir . '/media-mapping.json' ) ) {
			$media_mapping = json_decode( file_get_contents( $media_dir . '/media-mapping.json' ), true );

			\Elementor\TemplateLibrary\Classes\Media_Mapper::set_mapping( $media_mapping, $media_dir );
		}

		Plugin::$instance->uploads_manager->remove_file_or_dir( $media_zip_path );

		return $media_dir;
	}

	private function extract_media_zip( $zip_path ) {
		if ( ! class_exists( '\ZipArchive' ) ) {
			return null;
		}

		$zip = new \ZipArchive();
		if ( $zip->open( $zip_path ) !== true ) {
			return null;
		}

		$media_dir = dirname( $zip_path ) . '/media';
		if ( ! $zip->extractTo( $media_dir ) ) {
			$zip->close();
			return null;
		}

		$zip->close();

		return $media_dir;
	}

	protected function get_args(): array {
		return [
			'file_url' => [
				'type' => 'string',
				'description' => 'File URL for upload action',
				'required' => false,
				'validate_callback' => function ( $value ) {
					if ( empty( $value ) ) {
						return true;
					}

					return filter_var( $this->format_url( $value ), FILTER_VALIDATE_URL );
				},
			],
			'kit_id' => [
				'type' => 'string',
				'description' => 'Kit ID for upload action',
				'required' => false,
			],
			'source' => [
				'type' => 'string',
				'description' => 'Source for upload action',
				'required' => false,
			],
		];
	}
}
PK     8\x	z    :  modules/import-export-customization/data/routes/revert.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes;

use Elementor\App\Modules\ImportExportCustomization\Module as ImportExportCustomizationModule;
use Elementor\Plugin;
use Elementor\App\Modules\ImportExportCustomization\Data\Response;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Revert extends Base_Route {

	protected function get_route(): string {
		return 'revert';
	}

	protected function get_method(): string {
		return \WP_REST_Server::CREATABLE;
	}

	protected function callback( $request ): \WP_REST_Response {
		/**
		 * @var $module ImportExportCustomizationModule
		 */
		$module = Plugin::$instance->app->get_component( 'import-export-customization' );

		try {
			$revert_result = $module->revert_last_imported_kit();

			Plugin::$instance->logger->get_logger()->info( 'Kit revert completed via REST API' );

			return Response::success( $revert_result );

		} catch ( \Error | \Exception $e ) {
			Plugin::$instance->logger->get_logger()->error( $e->getMessage(), [
				'meta' => [
					'trace' => $e->getTraceAsString(),
				],
			] );

			$frame = $e->getTrace()[0] ?? [];
			$class = $frame['class'] ?? '';
			if ( $module->is_third_party_class( $class ) ) {
				return Response::error( ImportExportCustomizationModule::THIRD_PARTY_ERROR, $e->getMessage() );
			}

			return Response::error( 'revert_error', $e->getMessage() );
		}
	}

	protected function get_args(): array {
		return [];
	}
}
PK     8\Y    :  modules/import-export-customization/data/routes/import.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes;

use Elementor\App\Modules\ImportExportCustomization\Module as ImportExportCustomizationModule;
use Elementor\Plugin;
use Elementor\App\Modules\ImportExportCustomization\Data\Response;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Import extends Base_Route {

	protected function get_route(): string {
		return 'import';
	}

	protected function get_method(): string {
		return \WP_REST_Server::CREATABLE;
	}

	protected function callback( $request ): \WP_REST_Response {
		/**
		 * @var $module ImportExportCustomizationModule
		 */
		$module = Plugin::$instance->app->get_component( 'import-export-customization' );

		try {
			$session = $request->get_param( 'session' );

			if ( empty( $session ) ) {
				return Response::error( 'missing_session_id', 'Session ID is required.' );
			}

			$settings = [
				'include' => $request->get_param( 'include' ),
				'customization' => $request->get_param( 'customization' ),
			];

			$import = $module->import_kit( $session, $settings, true );

			Plugin::$instance->logger->get_logger()->info(
				sprintf( 'Selected import runners via REST API: %1$s',
					implode( ', ', $import['runners'] ?? [] )
				)
			);

			return Response::success( $import );

		} catch ( \Error | \Exception $e ) {
			Plugin::$instance->logger->get_logger()->error( $e->getMessage(), [
				'meta' => [
					'trace' => $e->getTraceAsString(),
				],
			] );

			if ( $module->is_third_party_class( $e->getTrace()[0]['class'] ) ) {
				return Response::error( ImportExportCustomizationModule::THIRD_PARTY_ERROR, $e->getMessage() );
			}

			return Response::error( $e->getMessage(), 'import_error' );
		}
	}

	protected function get_args(): array {
		return [
			'session' => [
				'type' => 'string',
				'description' => 'Session ID for import operations',
				'required' => true,
			],
			'settings' => [
				'type' => 'object',
				'description' => 'Import settings',
				'required' => false,
				'default' => [],
			],
		];
	}
}
PK     8\{c	  c	  A  modules/import-export-customization/data/routes/import-runner.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes;

use Elementor\App\Modules\ImportExportCustomization\Module as ImportExportCustomizationModule;
use Elementor\Plugin;
use Elementor\App\Modules\ImportExportCustomization\Data\Response;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Import_Runner extends Base_Route {

	protected function get_route(): string {
		return 'import-runner';
	}

	protected function get_method(): string {
		return \WP_REST_Server::CREATABLE;
	}

	protected function callback( $request ): \WP_REST_Response {
		/**
		 * @var $module ImportExportCustomizationModule
		 */
		$module = Plugin::$instance->app->get_component( 'import-export-customization' );

		try {
			$session_id = $request->get_param( 'session' );
			$runner = $request->get_param( 'runner' );
			$module = Plugin::$instance->app->get_component( 'import-export-customization' );

			if ( empty( $session_id ) ) {
				return Response::error( 'Session ID is required.', 'missing_session_id' );
			}

			if ( empty( $runner ) ) {
				return Response::error( 'Runner name is required.', 'missing_runner_name' );
			}

			$import = $module->import_kit_by_runner( $session_id, $runner );

			if ( ! empty( $import['status'] ) ) {
				Plugin::$instance->logger->get_logger()->info(
					sprintf( 'Import runner completed via REST API: %1$s %2$s',
						$import['runner'] ?? $runner,
						( 'success' === $import['status'] ? '✓' : '✗' )
					)
				);
			}

			do_action( 'elementor/import-export-customization/import-kit/runner/after-run', $import );

			return Response::success( $import );

		} catch ( \Error | \Exception $e ) {
			Plugin::$instance->logger->get_logger()->error( $e->getMessage(), [
				'meta' => [
					'trace' => $e->getTraceAsString(),
				],
			] );

			if ( $module->is_third_party_class( $e->getTrace()[0]['class'] ) ) {
				return Response::error( ImportExportCustomizationModule::THIRD_PARTY_ERROR, $e->getMessage() );
			}

			return Response::error( 'import-runner-error', $e->getMessage() );
		}
	}

	protected function get_args(): array {
		return [
			'session' => [
				'type' => 'string',
				'description' => 'Session ID for import operations',
				'required' => true,
			],
			'runner' => [
				'type' => 'string',
				'description' => 'Runner name for import_runner action',
				'required' => true,
			],
		];
	}
}
PK     8\6.{    O  modules/import-export-customization/data/routes/traits/handles-quota-errors.phpnu [        <?php
namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes\Traits;

use Elementor\App\Modules\ImportExportCustomization\Data\Response;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

trait Handles_Quota_Errors {

	protected function get_cloud_kit_library_app() {
		try {
			return \Elementor\Modules\CloudKitLibrary\Module::get_app();
		} catch ( \Exception | \Error $e ) {
			return null;
		}
	}

	private function is_quota_error( $error_message ) {
		return \Elementor\Modules\CloudKitLibrary\Connect\Cloud_Kits::INSUFFICIENT_STORAGE_QUOTA === $error_message;
	}

	private function get_quota_error_response( $quota, $kit_data ) {
		$max_size_gb = 0;
		if ( ! empty( $quota['storage']['threshold'] ) ) {
			$max_size_gb = round( $quota['storage']['threshold'] / ( 1024 * 1024 * 1024 ), 2 );
		}

		$filename = __( 'This file', 'elementor' );
		if ( ! empty( $kit_data['title'] ) ) {
			$filename = '"' . $kit_data['title'] . '"';
		} elseif ( ! empty( $kit_data['fileName'] ) ) {
			$filename = '"' . $kit_data['fileName'] . '"';
		}

		return Response::error(
			\Elementor\Modules\CloudKitLibrary\Connect\Cloud_Kits::INSUFFICIENT_STORAGE_QUOTA,
			[
				'replacements' => [
					'filename' => $filename,
					'maxSize' => $max_size_gb,
				],
			]
		);
	}
}
PK     8\P  P  >  modules/import-export-customization/data/routes/base-route.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes;

abstract class Base_Route {
	public function __construct() {}

	public function register_route( $name_space, $base_route ): void {
		register_rest_route( $name_space, '/' . $base_route . '/' . $this->get_route(), [
			[
				'methods' => $this->get_method(),
				'callback' => fn( $request ) => $this->callback( $request ),
				'permission_callback' => $this->permission_callback(),
				'args' => $this->get_args(),
			],
		] );
	}

	abstract protected function get_route(): string;

	abstract protected function get_method(): string;

	abstract protected function callback( $request ): \WP_REST_Response;

	protected function permission_callback(): callable {
		return fn() => current_user_can( 'manage_options' );
	}

	abstract protected function get_args(): array;
}
PK     8\P>  >  7  modules/import-export-customization/data/controller.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Data;

use Elementor\App\Modules\ImportExportCustomization\Data\Routes\Export;
use Elementor\App\Modules\ImportExportCustomization\Data\Routes\Import;
use Elementor\App\Modules\ImportExportCustomization\Data\Routes\Import_Runner;
use Elementor\App\Modules\ImportExportCustomization\Data\Routes\Manager_Url;
use Elementor\App\Modules\ImportExportCustomization\Data\Routes\Process_Media;
use Elementor\App\Modules\ImportExportCustomization\Data\Routes\Revert;
use Elementor\App\Modules\ImportExportCustomization\Data\Routes\Upload;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Controller {
	const API_NAMESPACE = 'elementor/v1';
	const API_BASE = 'import-export-customization';

	public static function register_hooks() {
		add_action( 'rest_api_init', fn() => self::register_routes() );
	}

	public static function get_base_url() {
		return get_rest_url() . self::API_NAMESPACE . '/' . self::API_BASE;
	}

	private static function register_routes() {
		( new Export() )->register_route( self::API_NAMESPACE, self::API_BASE );
		( new Import() )->register_route( self::API_NAMESPACE, self::API_BASE );
		( new Import_Runner() )->register_route( self::API_NAMESPACE, self::API_BASE );
		( new Manager_Url() )->register_route( self::API_NAMESPACE, self::API_BASE );
		( new Process_Media() )->register_route( self::API_NAMESPACE, self::API_BASE );
		( new Revert() )->register_route( self::API_NAMESPACE, self::API_BASE );
		( new Upload() )->register_route( self::API_NAMESPACE, self::API_BASE );
	}
}
PK     8\4[
	  	  C  modules/import-export-customization/compatibility/customization.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Compatibility;

use Elementor\App\Modules\ImportExportCustomization\Module;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Handles conversion from manifest format v2.0 to v3.0
 * Main change: site-settings changed from array of tab keys to object with boolean values
 */
class Customization extends Base_Adapter {

	/**
	 * Check if compatibility is needed based on manifest version
	 *
	 * @param array $manifest_data
	 * @param array $meta
	 * @return bool
	 */
	public static function is_compatibility_needed( array $manifest_data, array $meta ) {
		// Check if we have an old version (2.0 or lower)
		$version = $manifest_data['version'] ?? '1.0';
		return version_compare( $version, '3.0', '<' );
	}

	/**
	 * Adapt the manifest from old format to new format
	 *
	 * @param array $manifest_data
	 * @return array
	 */
	public function adapt_manifest( array $manifest_data ) {
		// Check if site-settings needs adaptation
		if ( isset( $manifest_data['site-settings'] ) && is_array( $manifest_data['site-settings'] ) ) {
			// Old format: array of tab keys
			// New format: object with boolean values for each setting type

			$old_site_settings = $manifest_data['site-settings'];

			// Initialize new format with all settings as false
			$new_site_settings = [
				'theme' => false,
				'globalColors' => false,
				'globalFonts' => false,
				'themeStyleSettings' => false,
				'generalSettings' => false,
				'experiments' => false,
			];

			// Map old tab keys to new setting types
			$tab_mapping = [
				'settings-global-colors' => 'globalColors',
				'settings-global-typography' => 'globalFonts',
				'theme-style-typography' => 'themeStyleSettings',
				'settings-general' => 'generalSettings',
			];

			// If we have tab keys, assume all were exported (true)
			if ( ! empty( $old_site_settings ) ) {
				// In the old format, if site-settings was included, all settings were exported
				$new_site_settings = [
					'theme' => true,
					'globalColors' => true,
					'globalFonts' => true,
					'themeStyleSettings' => true,
					'generalSettings' => true,
					'experiments' => true,
				];
			}

			$manifest_data['site-settings'] = $new_site_settings;
		}

		return $manifest_data;
	}
}
PK     8\El    B  modules/import-export-customization/compatibility/base-adapter.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Compatibility;

use Elementor\App\Modules\ImportExportCustomization\Import;
use Elementor\Core\Base\Base_Object;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

abstract class Base_Adapter {

	/**
	 * @param array $manifest_data
	 * @param array $meta
	 * @return false
	 */
	public static function is_compatibility_needed( array $manifest_data, array $meta ) {
		return false;
	}

	public function adapt_manifest( array $manifest_data ) {
		return $manifest_data;
	}

	public function adapt_site_settings( array $site_settings, array $manifest_data, $path ) {
		return $site_settings;
	}

	public function adapt_template( array $template_data, array $template_settings ) {
		return $template_data;
	}
}
PK     8\:$	    A  modules/import-export-customization/compatibility/kit-library.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Compatibility;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Kit_Library extends Base_Adapter {
	public static function is_compatibility_needed( array $manifest_data, array $meta ) {
		return ! empty( $meta['referrer'] ) && 'kit-library' === $meta['referrer'];
	}

	public function adapt_manifest( array $manifest_data ) {
		if ( ! empty( $manifest_data['content']['page'] ) ) {
			foreach ( $manifest_data['content']['page'] as & $page ) {
				$page['thumbnail'] = false;
			}
		}

		if ( ! empty( $manifest_data['templates'] ) ) {
			foreach ( $manifest_data['templates'] as & $template ) {
				$template['thumbnail'] = false;
			}
		}

		return $manifest_data;
	}
}
PK     8\EB    <  modules/import-export-customization/compatibility/envato.phpnu [        <?php

namespace Elementor\App\Modules\ImportExportCustomization\Compatibility;

use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Plugin;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Envato extends Base_Adapter {
	public static function is_compatibility_needed( array $manifest_data, array $meta ) {
		return ! empty( $manifest_data['manifest_version'] );
	}

	public function adapt_manifest( array $manifest_data ) {
		$templates = $manifest_data['templates'];

		$manifest_data['templates'] = [];

		foreach ( $templates as $template ) {
			// Envato store their global kit styles as a 'global.json' template file.
			// We need to be able to know the path to this specific 'global.json' since it functions as the site-settings.json
			$is_global = ! empty( $template['metadata']['template_type'] ) && 'global-styles' === $template['metadata']['template_type'];
			if ( $is_global ) {
				// Adding the path of the 'global.json' template to the manifest which will be used in the future.
				$manifest_data['path-to-envto-site-settings'] = $template['source'];

				// Getting the site-settings because Envato stores them in one of the posts.
				$kit = Plugin::$instance->kits_manager->get_active_kit();
				$kit_tabs = $kit->get_tabs();
				unset( $kit_tabs['settings-site-identity'] );
				$manifest_data['site-settings'] = array_keys( $kit_tabs );

				continue;
			}

			// Evanto uses "type" instead of "doc_type"
			$template['doc_type'] = $template['type'];

			// Evanto uses for "name" instead of "title"
			$template['title'] = $template['name'];

			// Envato specifying an exact path to the template rather than using its "ID" as an index.
			// This extracts the "file name" part out of our exact source list and we treat that as an ID.
			$file_name_without_extension = str_replace( '.json', '', basename( $template['source'] ) );

			// Append the template to the global list:
			$manifest_data['templates'][ $file_name_without_extension ] = $template;
		}

		$manifest_data['name'] = $manifest_data['title'];

		return $manifest_data;
	}

	public function adapt_site_settings( array $site_settings, array $manifest_data, $path ) {
		if ( empty( $manifest_data['path-to-envto-site-settings'] ) ) {
			return $site_settings;
		}

		$global_file_path = $path . $manifest_data['path-to-envto-site-settings'];
		$global_file_data = ImportExportUtils::read_json_file( $global_file_path );

		return [
			'settings' => $global_file_data['page_settings'],
		];
	}

	public function adapt_template( array $template_data, array $template_settings ) {
		if ( ! empty( $template_data['metadata']['elementor_pro_conditions'] ) ) {
			foreach ( $template_data['metadata']['elementor_pro_conditions'] as $condition ) {
				list ( $type, $name, $sub_name, $sub_id ) = array_pad( explode( '/', $condition ), 4, '' );

				$template_data['import_settings']['conditions'][] = compact( 'type', 'name', 'sub_name', 'sub_id' );
			}
		}

		return $template_data;
	}
}
PK     8\zE-  -    modules/site-editor/module.phpnu [        <?php
namespace Elementor\App\Modules\SiteEditor;

use Elementor\Core\Base\Module as BaseModule;
use Elementor\Plugin;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Site Editor Module
 *
 * Responsible for initializing Elementor App functionality
 */
class Module extends BaseModule {
	/**
	 * Get name.
	 *
	 * @access public
	 *
	 * @return string
	 */
	public function get_name() {
		return 'site-editor';
	}

	public function add_menu_in_admin_bar( $admin_bar_config ) {
		$admin_bar_config['elementor_edit_page']['children'][] = [
			'id' => 'elementor_app_site_editor',
			'title' => esc_html__( 'Theme Builder', 'elementor' ),
			'sub_title' => esc_html__( 'Site', 'elementor' ),
			'href' => Plugin::$instance->app->get_settings( 'menu_url' ),
			'class' => 'elementor-app-link',
			'parent_class' => 'elementor-second-section',
		];

		return $admin_bar_config;
	}

	public function __construct() {
		add_filter( 'elementor/frontend/admin_bar/settings', [ $this, 'add_menu_in_admin_bar' ] ); // After kit (Site settings)
	}
}
PK     8\M        modules/import-export/wp-cli.phpnu [        <?php
namespace Elementor\App\Modules\ImportExport;

use Elementor\Core\Utils\Collection;
use Elementor\Core\Utils\Plugins_Manager;
use Elementor\Plugin;
use Elementor\App\Modules\KitLibrary\Connect\Kit_Library;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Wp_Cli extends \WP_CLI_Command {

	const AVAILABLE_SETTINGS = [ 'include', 'overrideConditions', 'selectedCustomPostTypes', 'plugins' ];

	/**
	 * Export a Kit
	 *
	 * [--include]
	 *      Which type of content to include. Possible values are 'content', 'templates', 'site-settings'.
	 *      if this parameter won't be specified, All data types will be included.
	 *
	 * ## EXAMPLES
	 *
	 * 1. wp elementor kit export path/to/export-file-name.zip
	 *      - This will export all site data to the specified file name.
	 *
	 * 2. wp elementor kit export path/to/export-file-name.zip --include=kit-settings,content
	 *      - This will export only site settings and content.
	 *
	 * @param array $args
	 * @param array $assoc_args
	 */
	public function export( $args, $assoc_args ) {
		if ( empty( $args[0] ) ) {
			\WP_CLI::error( 'Please specify a file name' );
		}

		\WP_CLI::line( 'Kit export started.' );

		$export_settings = [];
		foreach ( $assoc_args as $key => $value ) {
			if ( ! in_array( $key, static::AVAILABLE_SETTINGS, true ) ) {
				continue;
			}

			$export_settings[ $key ] = explode( ',', $value );
		}

		try {
			/**
			 * Running the export process through the import-export module so the export property in the module will be available to use.
			 *
			 * @type  Module $import_export_module
			 */
			$import_export_module = Plugin::$instance->app->get_component( 'import-export' );
			$result = $import_export_module->export_kit( $export_settings );

			rename( $result['file_name'], $args[0] );
		} catch ( \Error $error ) {
			\WP_CLI::error( $error->getMessage() );
		}

		\WP_CLI::success( 'Kit exported successfully.' );
	}

	/**
	 * Import a Kit
	 *
	 * [--include]
	 *      Which type of content to include. Possible values are 'content', 'templates', 'site-settings'.
	 *      if this parameter won't be specified, All data types will be included.
	 *
	 * [--overrideConditions]
	 *      Templates ids to override conditions for.
	 *
	 * [--sourceType]
	 *      Which source type is used in the current session. Available values are 'local', 'remote', 'library'.
	 *      The default value is 'local'
	 *
	 * ## EXAMPLES
	 *
	 * 1. wp elementor kit import path/to/elementor-kit.zip
	 *      - This will import the whole kit file content.
	 *
	 * 2. wp elementor kit import path/to/elementor-kit.zip --include=site-settings,content
	 *      - This will import only site settings and content.
	 *
	 * 3. wp elementor kit import path/to/elementor-kit.zip --overrideConditions=3478,4520
	 *      - This will import all content and will override conditions for the given template ids.
	 *
	 * 4. wp elementor kit import path/to/elementor-kit.zip --unfilteredFilesUpload=enable
	 *      - This will allow the import process to import unfiltered files.
	 *
	 * @param array $args
	 * @param array $assoc_args
	 */
	public function import( array $args, array $assoc_args ) {
		if ( ! current_user_can( 'manage_options' ) ) {
			\WP_CLI::error( 'You must run this command as an admin user' );
		}

		if ( empty( $args[0] ) ) {
			\WP_CLI::error( 'Please specify a file to import' );
		}

		\WP_CLI::line( 'Kit import started' );

		$assoc_args = wp_parse_args( $assoc_args, [
			'sourceType' => 'local',
		] );

		$url = null;
		$file_path = $args[0];
		$import_settings = [];
		$import_settings['referrer'] = Module::REFERRER_LOCAL;

		switch ( $assoc_args['sourceType'] ) {
			case 'library':
				$url = $this->get_url_from_library( $file_path );
				$zip_path = $this->create_temp_file_from_url( $url );
				$import_settings['referrer'] = Module::REFERRER_KIT_LIBRARY;
				break;

			case 'remote':
				$zip_path = $this->create_temp_file_from_url( $file_path );
				break;

			case 'local':
				$zip_path = $file_path;
				break;

			default:
				\WP_CLI::error( 'Unknown source type.' );
				break;
		}

		if ( 'enable' === $assoc_args['unfilteredFilesUpload'] ) {
			Plugin::$instance->uploads_manager->enable_unfiltered_files_upload();
		}

		foreach ( $assoc_args as $key => $value ) {
			if ( ! in_array( $key, static::AVAILABLE_SETTINGS, true ) ) {
				continue;
			}

			$import_settings[ $key ] = explode( ',', $value );
		}

		try {
			\WP_CLI::line( 'Importing data...' );

			/**
			 * Running the import process through the import-export module so the import property in the module will be available to use.
			 *
			 * @type  Module $import_export_module
			 */
			$import_export_module = Plugin::$instance->app->get_component( 'import-export' );

			if ( ! $import_export_module ) {
				\WP_CLI::error( 'Import Export module is not available.' );
			}

			$import = $import_export_module->import_kit( $zip_path, $import_settings );

			$manifest_data = $import_export_module->import->get_manifest();

			/**
			 * Import Export Manifest Data
			 *
			 * Allows 3rd parties to read and edit the kit's manifest before it is used.
			 *
			 * @since 3.7.0
			 *
			 * @param array $manifest_data The Kit's Manifest data
			 */
			$manifest_data = apply_filters( 'elementor/import-export/wp-cli/manifest_data', $manifest_data );

			\WP_CLI::line( 'Removing temp files...' );

			// The file was created from remote or library request, it also should be removed.
			if ( $url ) {
				Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $zip_path ) );
			}

			\WP_CLI::success( 'Kit imported successfully' );
		} catch ( \Error $error ) {
			Plugin::$instance->logger->get_logger()->error( $error->getMessage(), [
				'meta' => [
					'trace' => $error->getTraceAsString(),
				],
			] );

			if ( $url ) {
				Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $zip_path ) );
			}

			\WP_CLI::error( $error->getMessage() );
		}
	}

	/**
	 * Revert last imported kit.
	 */
	public function revert() {
		\WP_CLI::line( 'Kit revert started.' );

		try {
			/**
			 * Running the revert process through the import-export module so the revert property in the module will be available to use.
			 *
			 * @type  Module $import_export_module
			 */
			$import_export_module = Plugin::$instance->app->get_component( 'import-export' );
			$import_export_module->revert_last_imported_kit();

		} catch ( \Error $error ) {
			\WP_CLI::error( $error->getMessage() );
		}

		\WP_CLI::success( 'Kit reverted successfully.' );
	}

	/**
	 * Helper to get kit url by the kit id
	 * TODO: Maybe extract it.
	 *
	 * @param $kit_id
	 *
	 * @return string
	 */
	private function get_url_from_library( $kit_id ) {
		/** @var Kit_Library $app */
		$app = Plugin::$instance->common->get_component( 'connect' )->get_app( 'kit-library' );

		if ( ! $app ) {
			\WP_CLI::error( 'Kit library app not found' );
		}

		$response = $app->download_link( $kit_id );

		if ( is_wp_error( $response ) ) {
			\WP_CLI::error( "Library Response: {$response->get_error_message()}" );
		}

		return $response->download_link;
	}

	/**
	 * Helper to get kit zip file path by the kit url
	 * TODO: Maybe extract it.
	 *
	 * @param $url
	 *
	 * @return string
	 */
	private function create_temp_file_from_url( $url ) {
		\WP_CLI::line( 'Extracting zip archive...' );
		$response = wp_remote_get( $url );

		if ( is_wp_error( $response ) ) {
			\WP_CLI::error( "Download file url: {$response->get_error_message()}" );
		}

		if ( 200 !== $response['response']['code'] ) {
			\WP_CLI::error( "Download file url: {$response['response']['message']}" );
		}

		// Set the Request's state as an Elementor upload request, in order to support unfiltered file uploads.
		Plugin::$instance->uploads_manager->set_elementor_upload_state( true );

		$file = Plugin::$instance->uploads_manager->create_temp_file( $response['body'], 'kit.zip' );

		// After the upload complete, set the elementor upload state back to false.
		Plugin::$instance->uploads_manager->set_elementor_upload_state( false );

		return $file;
	}
}
PK     8\ApR      modules/import-export/usage.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport;

use Elementor\App\Modules\ImportExport\Processes\Revert;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Usage {

	/**
	 * Register hooks.
	 *
	 * @return void
	 */
	public function register() {
		add_filter( 'elementor/tracker/send_tracking_data_params', function ( array $params ) {
			$params['usages']['import_export']['revert'] = $this->get_revert_usage_data();

			return $params;
		} );
	}

	/**
	 * Get the Revert usage data.
	 *
	 * @return array
	 */
	private function get_revert_usage_data() {
		$revert_sessions = ( new Revert() )->get_revert_sessions();

		$data = [];

		foreach ( $revert_sessions as $revert_session ) {
			$data[] = [
				'kit_name' => $revert_session['kit_name'],
				'source' => $revert_session['source'],
				'revert_timestamp' => (int) $revert_session['revert_timestamp'],
				'total_time' => ( (int) $revert_session['revert_timestamp'] - (int) $revert_session['import_timestamp'] ),
			];
		}

		return $data;
	}
}
PK     8\u      modules/import-export/utils.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport;

use Elementor\Core\Utils\Str;
use Elementor\Modules\LandingPages\Module as Landing_Pages_Module;
use Elementor\Modules\FloatingButtons\Module as Floating_Buttons_Module;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils as ElementorUtils;

class Utils {

	public static function read_json_file( $path ) {
		if ( ! Str::ends_with( $path, '.json' ) ) {
			$path .= '.json';
		}

		$file_content = ElementorUtils::file_get_contents( $path, true );

		return $file_content ? json_decode( $file_content, true ) : [];
	}

	public static function map_old_new_post_ids( array $imported_data ) {
		$result = [];

		$result += $imported_data['templates']['succeed'] ?? [];

		if ( isset( $imported_data['content'] ) ) {
			foreach ( $imported_data['content'] as $post_type ) {
				$result += $post_type['succeed'] ?? [];
			}
		}

		if ( isset( $imported_data['wp-content'] ) ) {
			foreach ( $imported_data['wp-content'] as $post_type ) {
				$result += $post_type['succeed'] ?? [];
			}
		}

		return $result;
	}

	public static function map_old_new_term_ids( array $imported_data ) {
		$result = [];

		if ( ! isset( $imported_data['taxonomies'] ) ) {
			return $result;
		}

		foreach ( $imported_data['taxonomies'] as $post_type_taxonomies ) {
			foreach ( $post_type_taxonomies as $taxonomy ) {
				foreach ( $taxonomy as $term ) {
					$result[ $term['old_id'] ] = $term['new_id'];
				}
			}
		}

		return $result;
	}

	public static function get_elementor_post_types() {
		$elementor_post_types = get_post_types_by_support( 'elementor' );

		return array_filter( $elementor_post_types, function ( $value ) {
			// Templates are handled in a separate process.
			return 'elementor_library' !== $value;
		} );
	}

	public static function get_builtin_wp_post_types() {
		return [ 'post', 'page', 'nav_menu_item' ];
	}

	public static function get_registered_cpt_names() {
		$post_types = get_post_types( [
			'public' => true,
			'can_export' => true,
			'_builtin' => false,
		] );

		unset(
			$post_types[ Landing_Pages_Module::CPT ],
			$post_types[ Source_Local::CPT ],
			$post_types[ Floating_Buttons_Module::CPT_FLOATING_BUTTONS ]
		);

		return array_keys( $post_types );
	}

	/**
	 * Transform a string name to title format.
	 *
	 * @param $name
	 *
	 * @return string
	 */
	public static function transform_name_to_title( $name ): string {
		if ( empty( $name ) ) {
			return '';
		}

		$title = str_replace( [ '-', '_' ], ' ', $name );

		return ucwords( $title );
	}

	public static function get_import_sessions( $should_run_cleanup = false ) {
		$import_sessions = get_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, [] );

		if ( $should_run_cleanup ) {
			foreach ( $import_sessions as $session_id => $import_session ) {
				if ( ! isset( $import_session['runners'] ) && isset( $import_session['instance_data'] ) ) {
					$import_sessions[ $session_id ]['runners'] = $import_session['instance_data']['runners_import_metadata'] ?? [];

					unset( $import_sessions[ $session_id ]['instance_data'] );
				}
			}

			update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions );
		}

		return $import_sessions;
	}

	public static function update_space_between_widgets_values( $space_between_widgets ) {
		$setting_exist = isset( $space_between_widgets['size'] );
		$already_processed = isset( $space_between_widgets['column'] );

		if ( ! $setting_exist || $already_processed ) {
			return $space_between_widgets;
		}

		$size = strval( $space_between_widgets['size'] );
		$space_between_widgets['column'] = $size;
		$space_between_widgets['row'] = $size;
		$space_between_widgets['isLinked'] = true;

		return $space_between_widgets;
	}
}
PK     8\uegf  f     modules/import-export/module.phpnu [        <?php
namespace Elementor\App\Modules\ImportExport;

use Elementor\App\Modules\ImportExport\Processes\Export;
use Elementor\App\Modules\ImportExport\Processes\Import;
use Elementor\App\Modules\ImportExport\Processes\Revert;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Core\Files\Uploads_Manager;
use Elementor\Modules\System_Info\Reporters\Server;
use Elementor\Plugin;
use Elementor\Tools;
use Elementor\Utils as ElementorUtils;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Modules\CloudKitLibrary\Module as CloudKitLibrary;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Import Export Module
 *
 * Responsible for initializing Elementor App functionality
 */
class Module extends BaseModule {
	const FORMAT_VERSION = '2.0';

	const EXPORT_TRIGGER_KEY = 'elementor_export_kit';

	const UPLOAD_TRIGGER_KEY = 'elementor_upload_kit';

	const IMPORT_TRIGGER_KEY = 'elementor_import_kit';

	const IMPORT_RUNNER_TRIGGER_KEY = 'elementor_import_kit__runner';

	const REFERRER_KIT_LIBRARY = 'kit-library';

	const REFERRER_LOCAL = 'local';

	const REFERRER_CLOUD = 'cloud';

	const PLUGIN_PERMISSIONS_ERROR_KEY = 'plugin-installation-permissions-error';

	const KIT_LIBRARY_ERROR_KEY = 'invalid-kit-library-zip-error';

	const NO_WRITE_PERMISSIONS_KEY = 'no-write-permissions';

	const THIRD_PARTY_ERROR = 'third-party-error';

	const DOMDOCUMENT_MISSING = 'domdocument-missing';

	const OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS = 'elementor_import_sessions';

	const OPTION_KEY_ELEMENTOR_REVERT_SESSIONS = 'elementor_revert_sessions';

	const META_KEY_ELEMENTOR_IMPORT_SESSION_ID = '_elementor_import_session_id';

	const META_KEY_ELEMENTOR_EDIT_MODE = '_elementor_edit_mode';
	const IMPORT_PLUGINS_ACTION = 'import-plugins';
	const EXPORT_SOURCE_CLOUD = 'cloud';
	const EXPORT_SOURCE_FILE = 'file';

	/**
	 * Assigning the export process to a property, so we can use the process from outside the class.
	 *
	 * @var Export
	 */
	public $export;

	/**
	 * Assigning the import process to a property, so we can use the process from outside the class.
	 *
	 * @var Import
	 */
	public $import;

	/**
	 * Assigning the revert process to a property, so we can use the process from outside the class.
	 *
	 * @var Revert
	 */
	public $revert;

	/**
	 * Get name.
	 *
	 * @access public
	 *
	 * @return string
	 */
	public function get_name() {
		return 'import-export';
	}

	public function __construct() {
		$this->register_actions();

		if ( ElementorUtils::is_wp_cli() ) {
			\WP_CLI::add_command( 'elementor kit', WP_CLI::class );
		}

		( new Usage() )->register();

		$this->revert = new Revert();
	}

	public function get_init_settings() {
		if ( ! Plugin::$instance->app->is_current() ) {
			return [];
		}

		return $this->get_config_data();
	}

	/**
	 * Register the import/export tab in elementor tools.
	 */
	public function register_settings_tab( Tools $tools ) {
		$tools->add_tab( 'import-export-kit', [
			'label' => esc_html__( 'Website Templates', 'elementor' ),
			'sections' => [
				'intro' => [
					'label' => esc_html__( 'Website Templates', 'elementor' ),
					'callback' => function() {
						$this->render_import_export_tab_content();
					},
					'fields' => [],
				],
			],
		] );
	}

	/**
	 * Render the import/export tab content.
	 */
	private function render_import_export_tab_content() {
		$is_cloud_kits_available = CloudKitLibrary::get_app()->check_eligibility()['is_eligible'];

		$content_data = [
			'export' => [
				'title' => esc_html__( 'Export this website', 'elementor' ),
				'button' => [
					'url' => Plugin::$instance->app->get_base_url() . '#/export',
					'text' => esc_html__( 'Export', 'elementor' ),
					'id' => 'elementor-import-export__export',
				],
				'description' => esc_html__( 'You can download this website as a .zip file, or upload it to the library.', 'elementor' ),
			],
			'import' => [
				'title' => esc_html__( 'Import website templates', 'elementor' ),
				'button' => [
					'url' => Plugin::$instance->app->get_base_url() . '#/import',
					'text' => esc_html__( 'Import', 'elementor' ),
					'id' => 'elementor-import-export__import',
				],
				'description' => esc_html__( 'You can import design and settings from a .zip file or choose from the library.', 'elementor' ),
			],
		];

		if ( $is_cloud_kits_available ) {
			$content_data['import']['button_secondary'] = [
				'url' => Plugin::$instance->app->get_base_url() . '#/kit-library/cloud',
				'text' => esc_html__( 'Import from library', 'elementor' ),
				'id' => 'elementor-import-export__import_from_library',
			];
		}

		$last_imported_kit = $this->revert->get_last_import_session();
		$penultimate_imported_kit = $this->revert->get_penultimate_import_session();

		$user_date_format = get_option( 'date_format' );
		$user_time_format = get_option( 'time_format' );
		$date_format = $user_date_format . ' ' . $user_time_format;

		$should_show_revert_section = $this->should_show_revert_section( $last_imported_kit );

		if ( $should_show_revert_section ) {
			if ( ! empty( $penultimate_imported_kit ) ) {
				$revert_text = sprintf(
					/* translators: 1: Last imported kit title, 2: Last imported kit date, 3: Line break <br>, 4: Penultimate imported kit title, 5: Penultimate imported kit date. */
					esc_html__( 'Remove all the content and site settings that came with "%1$s" on %2$s %3$s and revert to the site setting that came with "%4$s" on %5$s.', 'elementor' ),
					! empty( $last_imported_kit['kit_title'] ) ? $last_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
					gmdate( $date_format, $last_imported_kit['start_timestamp'] ),
					'<br>',
					! empty( $penultimate_imported_kit['kit_title'] ) ? $penultimate_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
					gmdate( $date_format, $penultimate_imported_kit['start_timestamp'] )
				);
			} else {
				$revert_text = sprintf(
					/* translators: 1: Last imported kit title, 2: Last imported kit date, 3: Line break <br>. */
					esc_html__( 'Remove all the content and site settings that came with "%1$s" on %2$s.%3$s Your original site settings will be restored.', 'elementor' ),
					! empty( $last_imported_kit['kit_title'] ) ? $last_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
					gmdate( $date_format, $last_imported_kit['start_timestamp'] ),
					'<br>'
				);
			}
		}
		?>

		<div class="tab-import-export-kit__content">
			<p class="tab-import-export-kit__info">
				<?php
				printf(
					'%1$s <a href="https://go.elementor.com/wp-dash-import-export-general/" target="_blank">%2$s</a>',
					esc_html__( 'Here’s where you can export this website as a .zip file, upload it to the cloud, or start the process of applying an existing template to your site.', 'elementor' ),
					esc_html__( 'Learn more', 'elementor' ),
				);
				?>
			</p>

			<div class="tab-import-export-kit__wrapper">
				<?php foreach ( $content_data as $data ) {
					$this->print_item_content( $data );
				} ?>
			</div>

			<?php
			if ( $should_show_revert_section ) {

				$link_attributes = [
					'href' => $this->get_revert_href(),
					'id' => 'elementor-import-export__revert_kit',
					'class' => 'button',
				];
				?>
				<div class="tab-import-export-kit__revert">
					<h2>
						<?php echo esc_html__( 'Remove the most recent Website Template', 'elementor' ); ?>
					</h2>
					<p class="tab-import-export-kit__info">
						<?php ElementorUtils::print_unescaped_internal_string( $revert_text ); ?>
					</p>
					<?php $this->render_last_kit_thumbnail( $last_imported_kit ); ?>
					<a <?php ElementorUtils::print_html_attributes( $link_attributes ); ?> >
						<?php echo esc_html__( 'Remove Website Template', 'elementor' ); ?>
					</a>
				</div>
			<?php } ?>
		</div>
		<?php
	}

	private function print_item_content( $data ) {
		?>
		<div class="tab-import-export-kit__container">
			<div class="tab-import-export-kit__box">
				<h2><?php ElementorUtils::print_unescaped_internal_string( $data['title'] ); ?></h2>
			</div>
			<p class="description"><?php ElementorUtils::print_unescaped_internal_string( $data['description'] ); ?></p>

			<?php if ( ! empty( $data['link'] ) ) : ?>
				<a href="<?php ElementorUtils::print_unescaped_internal_string( $data['link']['url'] ); ?>" target="_blank"><?php ElementorUtils::print_unescaped_internal_string( $data['link']['text'] ); ?></a>
			<?php endif; ?>
			<div class="tab-import-export-kit__box action-buttons">
				<?php if ( ! empty( $data['button_secondary'] ) ) : ?>
					<a href="<?php ElementorUtils::print_unescaped_internal_string( $data['button_secondary']['url'] ); ?>" class="elementor-button e-btn-txt e-btn-txt-border">
						<?php ElementorUtils::print_unescaped_internal_string( $data['button_secondary']['text'] ); ?>
					</a>
				<?php endif; ?>
				<a <?php ElementorUtils::print_html_attributes( [ 'id' => $data['button']['id'] ] ); ?> href="<?php ElementorUtils::print_unescaped_internal_string( $data['button']['url'] ); ?>" class="elementor-button e-primary">
					<?php ElementorUtils::print_unescaped_internal_string( $data['button']['text'] ); ?>
				</a>
			</div>
		</div>
		<?php
	}

	private function get_revert_href(): string {
		$admin_post_url = admin_url( 'admin-post.php?action=elementor_revert_kit' );
		$nonced_admin_post_url = wp_nonce_url( $admin_post_url, 'elementor_revert_kit' );
		return $this->maybe_add_referrer_param( $nonced_admin_post_url );
	}

	/**
	 * Checks if referred by a kit and adds the referrer ID to the href
	 *
	 * @param string $href
	 *
	 * @return string
	 */
	private function maybe_add_referrer_param( string $href ): string {
		$param_name = 'referrer_kit';

		if ( empty( $_GET[ $param_name ] ) ) {
			return $href;
		}

		return add_query_arg( $param_name, sanitize_key( $_GET[ $param_name ] ), $href );
	}

	/**
	 * Render the last kit thumbnail if exists
	 *
	 * @param $last_imported_kit
	 *
	 * @return void
	 */
	private function render_last_kit_thumbnail( $last_imported_kit ) {
		if ( empty( $last_imported_kit['kit_thumbnail'] ) ) {
			return;
		}

		?>
		<div class="tab-import-export-kit__kit-item-row">
			<article class="tab-import-export-kit__kit-item">
				<header>
					<h3>
						<?php echo esc_html( $last_imported_kit['kit_title'] ); ?>
					</h3>
				</header>
				<img
					src="<?php echo esc_url( $last_imported_kit['kit_thumbnail'] ); ?>"
					alt="<?php echo esc_attr( $last_imported_kit['kit_title'] ); ?>"
					loading="lazy"
				>
			</article>
		</div>
		<?php
	}

	/**
	 * Upload a kit zip file and get the kit data.
	 *
	 * Assigning the Import process to the 'import' property,
	 * so it will be available to use in different places such as: WP_Cli, Pro, etc.
	 *
	 * @param string $file Path to the file.
	 * @param string $referrer Referrer of the file 'local' or 'kit-library'.
	 * @param string $kit_id
	 * @return array
	 * @throws \Exception If export validation fails or processing errors occur.
	 */
	public function upload_kit( $file, $referrer, $kit_id = null ) {
		$this->ensure_writing_permissions();

		$this->import = new Import( $file, [
			'referrer' => $referrer,
			'id' => $kit_id,
		] );

		return [
			'session' => $this->import->get_session_id(),
			'manifest' => $this->import->get_manifest(),
			'conflicts' => $this->import->get_settings_conflicts(),
		];
	}

	/**
	 * Import a kit by session_id.
	 * Upload and import a kit by kit zip file.
	 *
	 * If the split_to_chunks flag is true, the process won't start
	 * It will initialize the import process and return the session_id and the runners.
	 *
	 * Assigning the Import process to the 'import' property,
	 * so it will be available to use in different places such as: WP_Cli, Pro, etc.
	 *
	 * @param string $path Path to the file or session_id.
	 * @param array  $settings Settings the import use to determine which content to import.
	 *       (e.g: include, selected_plugins, selected_cpt, selected_override_conditions, etc.)
	 * @param bool   $split_to_chunks Determine if the import process should be split into chunks.
	 * @return array
	 * @throws \Exception If export configuration is invalid or processing fails.
	 */
	public function import_kit( string $path, array $settings, bool $split_to_chunks = false ): array {
		$this->ensure_writing_permissions();
		$this->ensure_DOMDocument_exists();

		$this->import = new Import( $path, $settings );
		$this->import->register_default_runners();

		remove_filter( 'elementor/document/save/data', [ Plugin::$instance->modules_manager->get_modules( 'content-sanitizer' ), 'sanitize_content' ] );
		do_action( 'elementor/import-export/import-kit', $this->import );

		if ( $split_to_chunks ) {
			$this->import->init_import_session( true );

			return [
				'session' => $this->import->get_session_id(),
				'runners' => $this->import->get_runners_name(),
			];
		}

		return $this->import->run();
	}

	/**
	 * Resuming import process by re-creating the import instance and running the specific runner.
	 *
	 * @param string $session_id The id off the import session.
	 * @param string $runner_name The specific runner that we want to run.
	 *
	 * @return array Two types of response.
	 *      1. The status and the runner name.
	 *      2. The imported data. (Only if the runner is the last one in the import process)
	 * @throws \Exception If import configuration is invalid or processing fails.
	 */
	public function import_kit_by_runner( string $session_id, string $runner_name ): array {
		// Check session_id
		$this->import = Import::from_session( $session_id );
		$runners = $this->import->get_runners_name();

		$run = $this->import->run_runner( $runner_name );

		if ( end( $runners ) === $run['runner'] ) {
			return $this->import->get_imported_data();
		}

		return $run;
	}

	/**
	 * Export a kit.
	 *
	 * Assigning the Export process to the 'export' property,
	 * so it will be available to use in different places such as: WP_Cli, Pro, etc.
	 *
	 * @param array $settings Settings the export use to determine which content to export.
	 *      (e.g: include, kit_info, selected_plugins, selected_cpt, etc.)
	 * @return array
	 * @throws \Exception If import/export process fails or validation errors occur.
	 */
	public function export_kit( array $settings ) {
		$this->ensure_writing_permissions();

		$this->export = new Export( $settings );
		$this->export->register_default_runners();

		do_action( 'elementor/import-export/export-kit', $this->export );

		return $this->export->run();
	}

	/**
	 * Handle revert kit ajax request.
	 */
	public function revert_last_imported_kit() {
		$this->revert = new Revert();
		$this->revert->register_default_runners();

		do_action( 'elementor/import-export/revert-kit', $this->revert );

		$this->revert->run();
	}


	/**
	 * Handle revert last imported kit ajax request.
	 */
	public function handle_revert_last_imported_kit() {
		check_admin_referer( 'elementor_revert_kit' );

		$this->revert_last_imported_kit();

		wp_safe_redirect( admin_url( 'admin.php?page=' . Tools::PAGE_ID . '#tab-import-export-kit' ) );
		die;
	}

	/**
	 * Register appropriate actions.
	 */
	private function register_actions() {
		add_action( 'admin_init', function() {
			if ( wp_doing_ajax() &&
				isset( $_POST['action'] ) &&
				wp_verify_nonce( ElementorUtils::get_super_global_value( $_POST, '_nonce' ), Ajax::NONCE_KEY ) &&
				current_user_can( 'manage_options' )
			) {
				$this->maybe_handle_ajax();
			}
		} );

		add_action( 'admin_post_elementor_revert_kit', [ $this, 'handle_revert_last_imported_kit' ] );

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );

		if ( ! Plugin::$instance->experiments->is_feature_active( 'import-export-customization' ) ) {
			$page_id = Tools::PAGE_ID;

			add_action( "elementor/admin/after_create_settings/{$page_id}", [ $this, 'register_settings_tab' ] );
		}

		// TODO 18/04/2023 : This needs to be moved to the runner itself after https://elementor.atlassian.net/browse/HTS-434 is done.
		if ( self::IMPORT_PLUGINS_ACTION === ElementorUtils::get_super_global_value( $_SERVER, 'HTTP_X_ELEMENTOR_ACTION' ) ) {
			add_filter( 'woocommerce_create_pages', [ $this, 'empty_pages' ], 10, 0 );
		}
		// TODO ^^^

		add_filter( 'elementor/import/kit/result', function( $result ) {
			if ( ! empty( $result['file_url'] ) ) {
				return [
					'file_name' => $this->get_remote_kit_zip( $result['file_url'] ),
					'referrer' => static::REFERRER_KIT_LIBRARY,
					'file_url' => $result['file_url'],
				];
			}

			return $result;
		} );
	}

	/**
	 * Prevent the creation of the default WooCommerce pages (Cart, Checkout, etc.)
	 *
	 * TODO 18/04/2023 : This needs to be moved to the runner itself after https://elementor.atlassian.net/browse/HTS-434 is done.
	 *
	 * @return array
	 */
	public function empty_pages(): array {
		return [];
	}

	private function ensure_writing_permissions() {
		$server = new Server();

		$paths_to_check = [
			Server::KEY_PATH_WP_CONTENT_DIR => $server->get_system_path( Server::KEY_PATH_WP_CONTENT_DIR ),
			Server::KEY_PATH_UPLOADS_DIR => $server->get_system_path( Server::KEY_PATH_UPLOADS_DIR ),
			Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR => $server->get_system_path( Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ),
		];

		$permissions = $server->get_paths_permissions( $paths_to_check );

		// WP Content dir has to be exists and writable.
		if ( ! $permissions[ Server::KEY_PATH_WP_CONTENT_DIR ]['write'] ) {
			throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		// WP Uploads dir has to be exists and writable.
		if ( ! $permissions[ Server::KEY_PATH_UPLOADS_DIR ]['write'] ) {
			throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		// Elementor uploads dir permissions is divided to 2 cases:
		// 1. If the dir exists, it has to be writable.
		// 2. If the dir doesn't exist, the parent dir has to be writable (wp uploads dir), so we can create it.
		if ( $permissions[ Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ]['exists'] && ! $permissions[ Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ]['write'] ) {
			throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}
	}

	private function ensure_DOMDocument_exists() {
		if ( ! class_exists( 'DOMDocument' ) ) {
			throw new \Error( self::DOMDOCUMENT_MISSING ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}
	}

	/**
	 * Enqueue admin scripts
	 */
	public function enqueue_scripts() {
		wp_enqueue_script(
			'elementor-import-export-admin',
			$this->get_js_assets_url( 'import-export-admin' ),
			[ 'elementor-common' ],
			ELEMENTOR_VERSION,
			true
		);

		wp_localize_script(
			'elementor-import-export-admin',
			'elementorImportExport',
			[
				'lastImportedSession' => $this->revert->get_last_import_session(),
				'appUrl' => Plugin::$instance->app->get_base_url() . '#/kit-library',
			]
		);
	}

	/**
	 * Assign each ajax action to a method.
	 */
	private function maybe_handle_ajax() {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		$action = ElementorUtils::get_super_global_value( $_POST, 'action' );

		try {
			switch ( $action ) {
				case static::EXPORT_TRIGGER_KEY:
					$this->handle_export_kit();
					break;

				case static::UPLOAD_TRIGGER_KEY:
					$this->handle_upload_kit();
					break;

				case static::IMPORT_TRIGGER_KEY:
					$this->handle_import_kit();
					break;

				case static::IMPORT_RUNNER_TRIGGER_KEY:
					$this->handle_import_kit__runner();
					break;

				default:
					break;
			}
		} catch ( \Error $e ) {
			if ( isset( $this->import ) ) {
				$this->import->finalize_import_session_option();
			}

			Plugin::$instance->logger->get_logger()->error( $e->getMessage(), [
				'meta' => [
					'trace' => $e->getTraceAsString(),
				],
			] );

			if ( isset( $this->import ) && $this->is_third_party_class( $e->getTrace()[0]['class'] ) ) {
				wp_send_json_error( self::THIRD_PARTY_ERROR, 500 );
			}

			wp_send_json_error( $e->getMessage(), 500 );
		}
	}

	/**
	 * Handle upload kit ajax request.
	 *
	 * @throws \Error If operation validation fails or processing errors occur.
	 */
	private function handle_upload_kit() {
		// PHPCS - A URL that should contain special chars (auth headers information).
		$file_url = isset( $_POST['e_import_file'] )
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			? wp_unslash( $_POST['e_import_file'] )
			: '';

		// PHPCS - Already validated in caller function
		$kit_id = ElementorUtils::get_super_global_value( $_POST, 'kit_id' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
		$source = ElementorUtils::get_super_global_value( $_POST, 'source' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing

		$is_import_from_library = ! empty( $file_url );

		if ( $is_import_from_library ) {
			if (
				! wp_verify_nonce( ElementorUtils::get_super_global_value( $_POST, 'e_kit_library_nonce' ), 'kit-library-import' )
			) {
				throw new \Error( 'Invalid kit library nonce.' );
			}

			if ( ! filter_var( $file_url, FILTER_VALIDATE_URL ) || 0 !== strpos( $file_url, 'http' ) ) {
				throw new \Error( static::KIT_LIBRARY_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			}

			$import_result = apply_filters( 'elementor/import/kit/result', [ 'file_url' => $file_url ] );
		} elseif ( ! empty( $source ) ) {
			$import_result = apply_filters( 'elementor/import/kit/result/' . $source, [
				'kit_id' => $kit_id,
				'source' => $source,
			] );
		} else {
			$import_result = [
				'file_name' => ElementorUtils::get_super_global_value( $_FILES, 'e_import_file' )['tmp_name'],
				'referrer' => static::REFERRER_LOCAL,
			];
		}

		Plugin::$instance->logger->get_logger()->info( 'Uploading Kit: ', [
			'meta' => [
				'kit_id' => $kit_id,
				'referrer' => $import_result['referrer'],
			],
		] );

		if ( is_wp_error( $import_result ) ) {
			wp_send_json_error( $import_result->get_error_message() );
		}

		$uploaded_kit = $this->upload_kit( $import_result['file_name'], $import_result['referrer'], $kit_id );

		$session_dir = $uploaded_kit['session'];
		$manifest = $uploaded_kit['manifest'];
		$conflicts = $uploaded_kit['conflicts'];

		if ( $is_import_from_library || ! empty( $source ) ) {
			Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $import_result['file_name'] ) );
		}

		if ( isset( $manifest['plugins'] ) && ! current_user_can( 'install_plugins' ) ) {
			throw new \Error( static::PLUGIN_PERMISSIONS_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		$result = [
			'session' => $session_dir,
			'manifest' => $manifest,
			'file_url' => $import_result['file_url'],
		];

		if ( ! empty( $import_result['kit'] ) ) {
			$result['uploaded_kit'] = $import_result['kit'];
		}

		if ( ! empty( $conflicts ) ) {
			$result['conflicts'] = $conflicts;
		} else {
			// Moved into the IE process \Elementor\App\Modules\ImportExport\Processes\Import::get_default_settings_conflicts
			// TODO: remove in 3.10.0
			$result = apply_filters( 'elementor/import/stage_1/result', $result );
		}

		wp_send_json_success( $result );
	}

	protected function get_remote_kit_zip( $url ) {
		$remote_zip_request = wp_safe_remote_get( $url );

		if ( is_wp_error( $remote_zip_request ) ) {
			Plugin::$instance->logger->get_logger()->error( $remote_zip_request->get_error_message() );
			throw new \Error( static::KIT_LIBRARY_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		if ( 200 !== $remote_zip_request['response']['code'] ) {
			Plugin::$instance->logger->get_logger()->error( $remote_zip_request['response']['message'] );
			throw new \Error( static::KIT_LIBRARY_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		return Plugin::$instance->uploads_manager->create_temp_file( $remote_zip_request['body'], 'kit.zip' );
	}

	/**
	 * Handle import kit ajax request.
	 */
	private function handle_import_kit() {
		// PHPCS - Already validated in caller function
		$settings = json_decode( ElementorUtils::get_super_global_value( $_POST, 'data' ), true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
		$tmp_folder_id = $settings['session'];

		$import = $this->import_kit( $tmp_folder_id, $settings, true );

		// get_settings_config() added manually because the frontend Ajax request doesn't trigger the get_init_settings().
		$import['configData'] = $this->get_config_data();

		Plugin::$instance->logger->get_logger()->info(
			sprintf( 'Selected import runners: %1$s',
				implode( ', ', $import['runners'] )
			)
		);

		wp_send_json_success( $import );
	}

	/**
	 * Handle ajax request for running specific runner in the import kit process.
	 */
	private function handle_import_kit__runner() {
		// PHPCS - Already validated in caller function
		$settings = json_decode( ElementorUtils::get_super_global_value( $_POST, 'data' ), true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
		$session_id = $settings['session'];
		$runner = $settings['runner'];

		$import = $this->import_kit_by_runner( $session_id, $runner );

		// get_settings_config() added manually because the frontend Ajax request doesn't trigger the get_init_settings().
		$import['configData'] = $this->get_config_data();

		if ( ! empty( $import['status'] ) ) {
			Plugin::$instance->logger->get_logger()->info(
				sprintf( 'Import runner completed: %1$s %2$s',
					$import['runner'],
					( 'success' === $import['status'] ? '✓' : '✗' )
				)
			);
		}

		do_action( 'elementor/import-export/import-kit/runner/after-run', $import );

		wp_send_json_success( $import );
	}

	/**
	 * Handle export kit ajax request.
	 *
	 * @throws \Error If cleanup process fails or file system errors occur.
	 */
	private function handle_export_kit() {
		// PHPCS - Already validated in caller function
		$settings = json_decode( ElementorUtils::get_super_global_value( $_POST, 'data' ), true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
		$source = $settings['kitInfo']['source'];

		$export = $this->export_kit( $settings );

		$file_name = $export['file_name'];
		$file_size = filesize( $file_name );
		$file = ElementorUtils::file_get_contents( $file_name );

		if ( ! $file ) {
			throw new \Error( 'Could not read the exported file.' );
		}

		Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $file_name ) );

		$result = apply_filters(
			'elementor/export/kit/export-result',
			[
				'manifest' => $export['manifest'],
				'file' => base64_encode( $file ),
			],
			$source,
			$export,
			$settings,
			$file,
			$file_size,
		);

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( $result );
		}

		wp_send_json_success( $result );
	}

	/**
	 * Get config data that will be exposed to the frontend.
	 */
	private function get_config_data() {
		$export_nonce = wp_create_nonce( 'elementor_export' );
		$export_url = add_query_arg( [ '_nonce' => $export_nonce ], Plugin::$instance->app->get_base_url() );

		return [
			'exportURL' => $export_url,
			'summaryTitles' => $this->get_summary_titles(),
			'builtinWpPostTypes' => ImportExportUtils::get_builtin_wp_post_types(),
			'elementorPostTypes' => ImportExportUtils::get_elementor_post_types(),
			'isUnfilteredFilesEnabled' => Uploads_Manager::are_unfiltered_uploads_enabled(),
			'elementorHomePageUrl' => $this->get_elementor_home_page_url(),
			'recentlyEditedElementorPageUrl' => $this->get_recently_edited_elementor_page_url(),
			'tools_url' => Tools::get_url(),
			'importSessions' => Revert::get_import_sessions(),
			'lastImportedSession' => $this->revert->get_last_import_session(),
			'kitPreviewNonce' => wp_create_nonce( 'kit_thumbnail' ),
		];
	}

	/**
	 * Get labels of Elementor document types, Elementor Post types, WordPress Post types and Custom Post types.
	 */
	private function get_summary_titles() {
		$summary_titles = [];

		$document_types = Plugin::$instance->documents->get_document_types();

		foreach ( $document_types as $name => $document_type ) {
			$summary_titles['templates'][ $name ] = [
				'single' => $document_type::get_title(),
				'plural' => $document_type::get_plural_title(),
			];
		}

		$elementor_post_types = ImportExportUtils::get_elementor_post_types();
		$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
		$post_types = array_merge( $elementor_post_types, $wp_builtin_post_types );

		foreach ( $post_types as $post_type ) {
			$post_type_object = get_post_type_object( $post_type );

			$summary_titles['content'][ $post_type ] = [
				'single' => $post_type_object->labels->singular_name ?? '',
				'plural' => $post_type_object->label ?? '',
			];
		}

		$custom_post_types = ImportExportUtils::get_registered_cpt_names();
		if ( ! empty( $custom_post_types ) ) {
			foreach ( $custom_post_types as $custom_post_type ) {

				$custom_post_types_object = get_post_type_object( $custom_post_type );
				// CPT data appears in two arrays:
				// 1. content object: in order to show the export summary when completed in getLabel function
				$summary_titles['content'][ $custom_post_type ] = [
					'single' => $custom_post_types_object->labels->singular_name ?? '',
					'plural' => $custom_post_types_object->label ?? '',
				];

				// 2. customPostTypes object: in order to actually export the data
				$summary_titles['content']['customPostTypes'][ $custom_post_type ] = [
					'single' => $custom_post_types_object->labels->singular_name ?? '',
					'plural' => $custom_post_types_object->label ?? '',
				];
			}
		}

		$active_kit = Plugin::$instance->kits_manager->get_active_kit();

		foreach ( $active_kit->get_tabs() as $key => $tab ) {
			$summary_titles['site-settings'][ $key ] = $tab->get_title();
		}

		return $summary_titles;
	}

	public function should_show_revert_section( $last_imported_kit ) {
		if ( empty( $last_imported_kit ) ) {
			return false;
		}

		// TODO: BC - remove in the future
		// The 'templates' runner was in core and moved to the Pro plugin. (Part of it still exits in the Core for BC)
		// The runner that is in the core version is missing the revert functionality,
		// therefore we shouldn't display the revert section if the import process done with the core version.
		$is_import_templates_ran = isset( $last_imported_kit['runners']['templates'] );
		if ( $this->has_pro() && $is_import_templates_ran ) {
			$has_imported_templates = ! empty( $last_imported_kit['runners']['templates'] );

			return $has_imported_templates;
		}

		return true;
	}

	public function has_pro(): bool {
		return ElementorUtils::has_pro();
	}

	private function get_elementor_editor_home_page_url() {
		if ( 'page' !== get_option( 'show_on_front' ) ) {
			return '';
		}

		$frontpage_id = get_option( 'page_on_front' );

		return $this->get_elementor_editor_page_url( $frontpage_id );
	}

	private function get_elementor_home_page_url() {
		if ( 'page' !== get_option( 'show_on_front' ) ) {
			return '';
		}

		$frontpage_id = get_option( 'page_on_front' );

		return $this->get_elementor_page_url( $frontpage_id );
	}

	private function get_recently_edited_elementor_page_url() {
		$query = ElementorUtils::get_recently_edited_posts_query( [ 'posts_per_page' => 1 ] );

		if ( ! isset( $query->post ) ) {
			return '';
		}

		return $this->get_elementor_page_url( $query->post->ID );
	}

	private function get_recently_edited_elementor_editor_page_url() {
		$query = ElementorUtils::get_recently_edited_posts_query( [ 'posts_per_page' => 1 ] );

		if ( ! isset( $query->post ) ) {
			return '';
		}

		return $this->get_elementor_editor_page_url( $query->post->ID );
	}

	private function get_elementor_document( $page_id ) {
		$document = Plugin::$instance->documents->get( $page_id );

		if ( ! $document || ! $document->is_built_with_elementor() ) {
			return false;
		}

		return $document;
	}

	private function get_elementor_page_url( $page_id ) {
		$document = $this->get_elementor_document( $page_id );

		return $document ? $document->get_preview_url() : '';
	}

	private function get_elementor_editor_page_url( $page_id ) {
		$document = $this->get_elementor_document( $page_id );

		return $document ? $document->get_edit_url() : '';
	}

	/**
	 * @param string $class_name
	 *
	 * @return bool
	 */
	public function is_third_party_class( $class_name ) {
		$allowed_classes = [
			'Elementor\\',
			'ElementorPro\\',
			'WP_',
			'wp_',
		];

		foreach ( $allowed_classes as $allowed_class ) {
			if ( str_starts_with( $class_name, $allowed_class ) ) {
				return false;
			}
		}

		return true;
	}
}
PK     8\B    6  modules/import-export/runners/import/site-settings.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Import;

use Elementor\Plugin;
use Elementor\Core\Settings\Page\Manager as PageManager;
use Elementor\App\Modules\ImportExport\Utils;
use Elementor\Core\Experiments\Manager as ExperimentsManager;

class Site_Settings extends Import_Runner_Base {

	/**
	 * @var int
	 */
	private $previous_kit_id;

	/**
	 * @var int
	 */
	private $active_kit_id;

	/**
	 * @var int
	 */
	private $imported_kit_id;

	/**
	 * @var string|null
	 */
	private ?string $installed_theme = null;

	/**
	 * @var string|null
	 */
	private ?string $activated_theme = null;

	/**
	 * @var array|null
	 */
	private ?array $previous_active_theme = null;

	/**
	 * @var array
	 */
	private $previous_experiments = [];

	/**
	 * @var array
	 */
	private $imported_experiments = [];

	public function get_theme_upgrader(): \Theme_Upgrader {
		if ( ! class_exists( '\Theme_Upgrader' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		}

		if ( ! class_exists( '\WP_Ajax_Upgrader_Skin' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
		}

		return new \Theme_Upgrader( new \WP_Ajax_Upgrader_Skin() );
	}

	public static function get_name(): string {
		return 'site-settings';
	}

	public function should_import( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'settings', $data['include'], true ) &&
			! empty( $data['site_settings']['settings'] )
		);
	}

	public function import( array $data, array $imported_data ) {
		$new_site_settings = $data['site_settings']['settings'];
		$title = $data['manifest']['title'] ?? 'Imported Kit';

		$active_kit = Plugin::$instance->kits_manager->get_active_kit();

		$this->active_kit_id = (int) $active_kit->get_id();
		$this->previous_kit_id = (int) Plugin::$instance->kits_manager->get_previous_id();

		$result = [];

		$old_settings = $active_kit->get_meta( PageManager::META_KEY );

		if ( ! $old_settings ) {
			$old_settings = [];
		}

		if ( ! empty( $old_settings['custom_colors'] ) ) {
			$new_site_settings['custom_colors'] = array_merge( $old_settings['custom_colors'], $new_site_settings['custom_colors'] );
		}

		if ( ! empty( $old_settings['custom_typography'] ) ) {
			$new_site_settings['custom_typography'] = array_merge( $old_settings['custom_typography'], $new_site_settings['custom_typography'] );
		}

		if ( ! empty( $new_site_settings['space_between_widgets'] ) ) {
			$new_site_settings['space_between_widgets'] = Utils::update_space_between_widgets_values( $new_site_settings['space_between_widgets'] );
		}

		$new_site_settings = array_replace_recursive( $old_settings, $new_site_settings );

		$new_kit = Plugin::$instance->kits_manager->create_new_kit( $title, $new_site_settings );

		$this->imported_kit_id = (int) $new_kit;

		$result['site-settings'] = (bool) $new_kit;

		$import_theme_result = $this->import_theme( $data );

		if ( ! empty( $import_theme_result ) ) {
			$result['theme'] = $import_theme_result;
		}

		$this->import_experiments( $data );

		if ( ! empty( $this->imported_experiments ) ) {
			$result['experiments'] = $this->imported_experiments;
		}

		return $result;
	}

	protected function install_theme( $slug, $version ) {
		$download_url = "https://downloads.wordpress.org/theme/{$slug}.{$version}.zip";

		return $this->get_theme_upgrader()->install( $download_url );
	}

	protected function activate_theme( $slug ) {
		switch_theme( $slug );
	}

	public function import_theme( array $data ) {
		if ( empty( $data['site_settings']['theme'] ) ) {
			return null;
		}

		$theme = $data['site_settings']['theme'];
		$theme_slug = $theme['slug'];
		$theme_name = $theme['name'];

		$current_theme = wp_get_theme();
		$this->previous_active_theme = [];
		$this->previous_active_theme['slug'] = $current_theme->get_stylesheet();
		$this->previous_active_theme['version'] = $current_theme->get( 'Version' );

		if ( $current_theme->get_stylesheet() === $theme_slug ) {
			$result['succeed'][ $theme_slug ] = sprintf(
				/* translators: %s: Theme name. */
				__( 'Theme: %1$s is already used', 'elementor' ),
				$theme_name
			);
			return $result;
		}

		try {
			if ( wp_get_theme( $theme_slug )->exists() ) {
				$this->activate_theme( $theme_slug );
				$this->activated_theme = $theme_slug;
				$result['succeed'][ $theme_slug ] = sprintf(
					/* translators: %s: Theme name. */
					__( 'Theme: %1$s has already been installed and activated', 'elementor' ),
					$theme_name
				);
				return $result;
			}

			$import = $this->install_theme( $theme_slug, $theme['version'] );

			if ( is_wp_error( $import ) ) {
				$result['failed'][ $theme_slug ] = sprintf(
					/* translators: %s: Theme name. */
					__( 'Failed to install theme: %1$s', 'elementor' ),
					$theme_name
				);
				return $result;
			}

			$result['succeed'][ $theme_slug ] = sprintf(
				/* translators: %s: Theme name. */
				__( 'Theme: %1$s has been successfully installed', 'elementor' ),
				$theme_name
			);
			$this->installed_theme = $theme_slug;
			$this->activate_theme( $theme_slug );
		} catch ( \Exception $error ) {
			$result['failed'][ $theme_slug ] = $error->getMessage();
		}

		return $result;
	}

	private function import_experiments( array $data ) {
		if ( empty( $data['site_settings']['experiments'] ) ) {
			return null;
		}

		$experiments_data = $data['site_settings']['experiments'];
		$experiments_manager = Plugin::$instance->experiments;
		$current_features = $experiments_manager->get_features();

		$this->save_previous_experiments_state( $current_features );

		foreach ( $experiments_data as $feature_name => $feature_data ) {
			if ( ! isset( $current_features[ $feature_name ] ) ) {
				continue;
			}

			$current_feature = $current_features[ $feature_name ];

			$current_feature_state = $current_feature['state'];
			$new_state = $feature_data['state'];

			if ( $current_feature_state === $new_state ) {
				continue;
			}

			if ( ! in_array( $new_state, [ ExperimentsManager::STATE_DEFAULT, ExperimentsManager::STATE_ACTIVE, ExperimentsManager::STATE_ACTIVE ], true ) ) {
				continue;
			}

			$option_key = $experiments_manager->get_feature_option_key( $feature_name );

			if ( 'default' === $new_state ) {
				delete_option( $option_key );
			} else {
				update_option( $option_key, $new_state );
			}

			$this->imported_experiments[ $feature_name ] = $feature_data;
		}
	}

	private function save_previous_experiments_state( array $current_features ) {
		$experiments_manager = Plugin::$instance->experiments;

		foreach ( $current_features as $feature_name => $feature ) {
			if ( ! $feature['mutable'] ) {
				continue;
			}

			$option_key = $experiments_manager->get_feature_option_key( $feature_name );
			$saved_state = get_option( $option_key );

			$this->previous_experiments[ $feature_name ] = [
				'name' => $feature_name,
				'title' => $feature['title'],
				'state' => empty( $saved_state ) ? 'default' : $saved_state,
				'default' => $feature['default'],
				'release_status' => $feature['release_status'],
			];
		}
	}

	public function get_import_session_metadata(): array {
		return [
			'previous_kit_id' => $this->previous_kit_id,
			'active_kit_id' => $this->active_kit_id,
			'imported_kit_id' => $this->imported_kit_id,
			'installed_theme' => $this->installed_theme,
			'activated_theme' => $this->activated_theme,
			'previous_active_theme' => $this->previous_active_theme,
			'previous_experiments' => $this->previous_experiments,
			'imported_experiments' => $this->imported_experiments,
		];
	}
}
PK     8\G	  	  2  modules/import-export/runners/import/templates.phpnu [        <?php
namespace Elementor\App\Modules\ImportExport\Runners\Import;

use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;

class Templates extends Import_Runner_Base {
	private $import_session_id;

	public static function get_name(): string {
		return 'templates';
	}

	public function should_import( array $data ) {
		return (
			Utils::has_pro() &&
			isset( $data['include'] ) &&
			in_array( 'templates', $data['include'], true ) &&
			! empty( $data['extracted_directory_path'] ) &&
			! empty( $data['manifest']['templates'] )
		);
	}

	public function import( array $data, array $imported_data ) {
		$this->import_session_id = $data['session_id'];

		$path = $data['extracted_directory_path'] . 'templates/';
		$templates = $data['manifest']['templates'];

		$result['templates'] = [
			'succeed' => [],
			'failed' => [],
		];

		foreach ( $templates as $id => $template_settings ) {
			try {
				$template_data = ImportExportUtils::read_json_file( $path . $id );
				$import = $this->import_template( $id, $template_settings, $template_data );

				$result['templates']['succeed'][ $id ] = $import;
			} catch ( \Exception $error ) {
				$result['templates']['failed'][ $id ] = $error->getMessage();
			}
		}

		return $result;
	}

	private function import_template( $id, array $template_settings, array $template_data ) {
		$doc_type = $template_settings['doc_type'];

		$new_document = Plugin::$instance->documents->create(
			$doc_type,
			[
				'post_title' => $template_settings['title'],
				'post_type' => Source_Local::CPT,
				'post_status' => 'publish',
			]
		);

		if ( is_wp_error( $new_document ) ) {
			throw new \Exception( esc_html( $new_document->get_error_message() ) );
		}

		$template_data['import_settings'] = $template_settings;
		$template_data['id'] = $id;

		$new_attachment_callback = function( $attachment_id ) {
			$this->set_session_post_meta( $attachment_id, $this->import_session_id );
		};

		add_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );

		$new_document->import( $template_data );

		remove_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );

		$document_id = $new_document->get_main_id();

		$this->set_session_post_meta( $document_id, $this->import_session_id );

		return $document_id;
	}
}
PK     8\(і    0  modules/import-export/runners/import/plugins.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Import;

use Elementor\Core\Utils\Collection;
use Elementor\Core\Utils\Plugins_Manager;
use Elementor\Core\Utils\Str;

class Plugins extends Import_Runner_Base {

	/**
	 * @var Plugins_Manager
	 */
	private $plugins_manager;

	public function __construct( $plugins_manager = null ) {
		if ( $plugins_manager ) {
			$this->plugins_manager = $plugins_manager;
		} else {
			$this->plugins_manager = new Plugins_Manager();
		}
	}

	public static function get_name(): string {
		return 'plugins';
	}

	public function should_import( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'plugins', $data['include'], true ) &&
			! empty( $data['manifest']['plugins'] ) &&
			! empty( $data['selected_plugins'] )
		);
	}

	public function import( array $data, array $imported_data ) {
		$plugins = $data['selected_plugins'];

		$plugins_collection = ( new Collection( $plugins ) )
			->map( function ( $item ) {
				if ( ! Str::ends_with( $item['plugin'], '.php' ) ) {
					$item['plugin'] .= '.php';
				}
				return $item;
			} );

		$slugs = $plugins_collection
			->map( function ( $item ) {
				return $item['plugin'];
			} )
			->all();

		$installed = $this->plugins_manager->install( $slugs );
		$activated = $this->plugins_manager->activate( $installed['succeeded'] );

		$ordered_activated_plugins = $plugins_collection
			->filter( function ( $item ) use ( $activated ) {
				return in_array( $item['plugin'], $activated['succeeded'], true );
			} )
			->map( function ( $item ) {
				return $item['name'];
			} )
			->all();

		$result['plugins'] = $ordered_activated_plugins;

		return $result;
	}
}
PK     8\vrH  H  3  modules/import-export/runners/import/wp-content.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Import;

use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Core\Utils\ImportExport\WP_Import;

class Wp_Content extends Import_Runner_Base {

	private $import_session_id;

	/**
	 * @var array
	 */
	private $selected_custom_post_types = [];

	public static function get_name(): string {
		return 'wp-content';
	}

	public function should_import( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'content', $data['include'], true ) &&
			! empty( $data['extracted_directory_path'] ) &&
			! empty( $data['manifest']['wp-content'] )
		);
	}

	public function import( array $data, array $imported_data ) {
		$this->import_session_id = $data['session_id'];

		$path = $data['extracted_directory_path'] . 'wp-content/';

		$post_types = $this->filter_post_types( $data['selected_custom_post_types'] );

		$taxonomies = $imported_data['taxonomies'] ?? [];
		$imported_terms = ImportExportUtils::map_old_new_term_ids( $imported_data );

		$result['wp-content'] = [];

		foreach ( $post_types as $post_type ) {
			$import = $this->import_wp_post_type(
				$path,
				$post_type,
				$imported_data,
				$taxonomies,
				$imported_terms
			);

			if ( empty( $import ) ) {
				continue;
			}

			$result['wp-content'][ $post_type ] = $import;
			$imported_data = array_merge( $imported_data, $result );
		}

		return $result;
	}

	private function import_wp_post_type( $path, $post_type, array $imported_data, array $taxonomies, array $imported_terms ) {
		$args = [
			'fetch_attachments' => true,
			'posts' => ImportExportUtils::map_old_new_post_ids( $imported_data ),
			'terms' => $imported_terms,
			'taxonomies' => ! empty( $taxonomies[ $post_type ] ) ? $taxonomies[ $post_type ] : [],
			'posts_meta' => [
				static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID => $this->import_session_id,
			],
			'terms_meta' => [
				static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID => $this->import_session_id,
			],
		];

		$file = $path . $post_type . '/' . $post_type . '.xml';

		if ( ! file_exists( $file ) ) {
			return [];
		}

		$wp_importer = new WP_Import( $file, $args );
		$result = $wp_importer->run();

		return $result['summary']['posts'];
	}

	private function filter_post_types( $selected_custom_post_types = [] ) {
		$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();

		foreach ( $selected_custom_post_types as $custom_post_type ) {
			if ( post_type_exists( $custom_post_type ) ) {
				$this->selected_custom_post_types[] = $custom_post_type;
			}
		}

		$post_types = array_merge( $wp_builtin_post_types, $this->selected_custom_post_types );
		$post_types = $this->force_element_to_be_last_by_value( $post_types, 'nav_menu_item' );

		return $post_types;
	}

	public function get_import_session_metadata(): array {
		return [
			'custom_post_types' => $this->selected_custom_post_types,
		];
	}

	/**
	 * @param array $base_array The array we want to relocate his element.
	 * @param mixed $element    The value of the element in the array we want to shift to end of the array.
	 * @return mixed
	 */
	private function force_element_to_be_last_by_value( array $base_array, $element ) {
		$index = array_search( $element, $base_array, true );

		if ( false !== $index ) {
			unset( $base_array[ $index ] );
			$base_array[] = $element;
		}

		return $base_array;
	}
}
PK     8\u?    3  modules/import-export/runners/import/taxonomies.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Import;

use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;

class Taxonomies extends Import_Runner_Base {

	private $import_session_id;

	public static function get_name(): string {
		return 'taxonomies';
	}

	public function should_import( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'content', $data['include'], true ) &&
			! empty( $data['extracted_directory_path'] ) &&
			! empty( $data['manifest']['taxonomies'] )
		);
	}

	public function import( array $data, array $imported_data ) {
		$path = $data['extracted_directory_path'] . 'taxonomies/';
		$this->import_session_id = $data['session_id'];

		$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
		$selected_custom_post_types = isset( $data['selected_custom_post_types'] ) ? $data['selected_custom_post_types'] : [];
		$post_types = array_merge( $wp_builtin_post_types, $selected_custom_post_types );

		$result = [];

		foreach ( $post_types as $post_type ) {
			if ( empty( $data['manifest']['taxonomies'][ $post_type ] ) ) {
				continue;
			}

			$result['taxonomies'][ $post_type ] = $this->import_taxonomies( $data['manifest']['taxonomies'][ $post_type ], $path );
		}

		return $result;
	}

	private function import_taxonomies( array $taxonomies, $path ) {
		$result = [];
		$imported_taxonomies = [];

		foreach ( $taxonomies as $taxonomy ) {
			if ( ! taxonomy_exists( $taxonomy ) ) {
				continue;
			}

			if ( ! empty( $imported_taxonomies[ $taxonomy ] ) ) {
				$result[ $taxonomy ] = $imported_taxonomies[ $taxonomy ];
				continue;
			}

			$taxonomy_data = ImportExportUtils::read_json_file( $path . $taxonomy );
			if ( empty( $taxonomy_data ) ) {
				continue;
			}

			$import = $this->import_taxonomy( $taxonomy_data );
			$result[ $taxonomy ] = $import;
			$imported_taxonomies[ $taxonomy ] = $import;
		}

		return $result;
	}

	private function import_taxonomy( array $taxonomy_data ) {
		$terms = [];

		foreach ( $taxonomy_data as $term ) {
			$old_slug = $term['slug'];

			$existing_term = term_exists( $term['slug'], $term['taxonomy'] );
			if ( $existing_term ) {
				if ( 'nav_menu' === $term['taxonomy'] ) {
					$term = $this->handle_duplicated_nav_menu_term( $term );
				} else {
					$terms[] = [
						'old_id' => (int) $term['term_id'],
						'new_id' => (int) $existing_term['term_id'],
						'old_slug' => $old_slug,
						'new_slug' => $term['slug'],
					];
					continue;
				}
			}

			$parent = $this->get_term_parent( $term, $terms );

			$args = [
				'slug' => $term['slug'],
				'description' => wp_slash( $term['description'] ),
				'parent' => (int) $parent,
			];

			$new_term = wp_insert_term( wp_slash( $term['name'] ), $term['taxonomy'], $args );
			if ( ! is_wp_error( $new_term ) ) {
				$this->set_session_term_meta( (int) $new_term['term_id'], $this->import_session_id );

				$terms[] = [
					'old_id' => $term['term_id'],
					'new_id' => (int) $new_term['term_id'],
					'old_slug' => $old_slug,
					'new_slug' => $term['slug'],
				];
			}
		}

		return $terms;
	}

	private function handle_duplicated_nav_menu_term( $term ) {
		do {
			$term['slug'] = $term['slug'] . '-duplicate';
			$term['name'] = $term['name'] . ' duplicate';
		} while ( term_exists( $term['slug'], 'nav_menu' ) );

		return $term;
	}

	private function get_term_parent( $term, array $imported_terms ) {
		$parent = $term['parent'];
		if ( 0 !== $parent && ! empty( $imported_terms ) ) {
			foreach ( $imported_terms as $imported_term ) {
				if ( $parent === $imported_term['old_id'] ) {
					$parent_term = term_exists( $imported_term['new_id'], $term['taxonomy'] );
					break;
				}
			}

			if ( isset( $parent_term['term_id'] ) ) {
				return $parent_term['term_id'];
			}
		}

		return 0;
	}
}
PK     8\`X  X  :  modules/import-export/runners/import/elementor-content.phpnu [        <?php
namespace Elementor\App\Modules\ImportExport\Runners\Import;

use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Plugin;

class Elementor_Content extends Import_Runner_Base {

	private $show_page_on_front;

	private $page_on_front_id;

	private $import_session_id;

	public function __construct() {
		$this->init_page_on_front_data();
	}

	public static function get_name(): string {
		return 'elementor-content';
	}

	public function should_import( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'content', $data['include'], true ) &&
			! empty( $data['manifest']['content'] ) &&
			! empty( $data['extracted_directory_path'] )
		);
	}

	public function import( array $data, array $imported_data ) {
		$result['content'] = [];
		$this->import_session_id = $data['session_id'];

		$elementor_post_types = ImportExportUtils::get_elementor_post_types();

		foreach ( $elementor_post_types as $post_type ) {
			if ( empty( $data['manifest']['content'][ $post_type ] ) ) {
				continue;
			}

			$posts_settings = $data['manifest']['content'][ $post_type ];
			$path = $data['extracted_directory_path'] . 'content/' . $post_type . '/';
			$imported_terms = ! empty( $imported_data['taxonomies'] )
				? ImportExportUtils::map_old_new_term_ids( $imported_data )
				: [];

			$result['content'][ $post_type ] = $this->import_elementor_post_type( $posts_settings, $path, $post_type, $imported_terms );
		}

		return $result;
	}

	private function import_elementor_post_type( array $posts_settings, $path, $post_type, array $imported_terms ) {
		$result = [
			'succeed' => [],
			'failed' => [],
		];

		foreach ( $posts_settings as $id => $post_settings ) {
			try {
				$post_data = ImportExportUtils::read_json_file( $path . $id );
				$import = $this->import_post( $post_settings, $post_data, $post_type, $imported_terms );

				if ( is_wp_error( $import ) ) {
					$result['failed'][ $id ] = $import->get_error_message();
					continue;
				}

				$result['succeed'][ $id ] = $import;
			} catch ( \Exception $error ) {
				$result['failed'][ $id ] = $error->getMessage();
			}
		}

		return $result;
	}

	private function import_post( array $post_settings, array $post_data, $post_type, array $imported_terms ) {
		$post_attributes = [
			'post_title' => $post_settings['title'],
			'post_type' => $post_type,
			'post_status' => 'publish',
		];

		if ( ! empty( $post_settings['excerpt'] ) ) {
			$post_attributes['post_excerpt'] = $post_settings['excerpt'];
		}

		$new_document = Plugin::$instance->documents->create(
			$post_settings['doc_type'],
			$post_attributes
		);

		if ( is_wp_error( $new_document ) ) {
			throw new \Exception( esc_html( $new_document->get_error_message() ) );
		}

		$post_data['import_settings'] = $post_settings;

		$new_attachment_callback = function( $attachment_id ) {
			$this->set_session_post_meta( $attachment_id, $this->import_session_id );
		};

		add_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );

		$new_document->import( $post_data );

		remove_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );

		$new_post_id = $new_document->get_main_id();

		if ( ! empty( $post_settings['terms'] ) ) {
			$this->set_post_terms( $new_post_id, $post_settings['terms'], $imported_terms );
		}

		if ( ! empty( $post_settings['show_on_front'] ) ) {
			$this->set_page_on_front( $new_post_id );
		}

		$this->set_session_post_meta( $new_post_id, $this->import_session_id );

		return $new_post_id;
	}

	private function set_post_terms( $post_id, array $terms, array $imported_terms ) {
		foreach ( $terms as $term ) {
			if ( ! isset( $imported_terms[ $term['term_id'] ] ) ) {
				continue;
			}

			wp_set_post_terms( $post_id, [ $imported_terms[ $term['term_id'] ] ], $term['taxonomy'], false );
		}
	}

	private function init_page_on_front_data() {
		$this->show_page_on_front = 'page' === get_option( 'show_on_front' );

		if ( $this->show_page_on_front ) {
			$this->page_on_front_id = (int) get_option( 'page_on_front' );
		}
	}

	private function set_page_on_front( $page_id ) {
		update_option( 'page_on_front', $page_id );

		if ( ! $this->show_page_on_front ) {
			update_option( 'show_on_front', 'page' );
		}
	}

	public function get_import_session_metadata(): array {
		return [
			'page_on_front' => $this->page_on_front_id ?? 0,
		];
	}
}
PK     8\TQҟx  x  ;  modules/import-export/runners/import/import-runner-base.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Import;

use Elementor\App\Modules\ImportExport\Runners\Runner_Interface;

abstract class Import_Runner_Base implements Runner_Interface {

	/**
	 * By the passed data we should decide if we want to run the import function of the runner or not.
	 *
	 * @param array $data
	 *
	 * @return bool
	 */
	abstract public function should_import( array $data );

	/**
	 * Main function of the runner import process.
	 *
	 * @param array $data Necessary data for the import process.
	 * @param array $imported_data Data that already imported by previously runners.
	 *
	 * @return array The result of the import process
	 */
	abstract public function import( array $data, array $imported_data );

	public function get_import_session_metadata(): array {
		return [];
	}

	public function set_session_post_meta( $post_id, $meta_value ) {
		update_post_meta( $post_id, static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID, $meta_value );
	}

	public function set_session_term_meta( $term_id, $meta_value ) {
		update_term_meta( $term_id, static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID, $meta_value );
	}
}
PK     8\yT  T  ;  modules/import-export/runners/revert/revert-runner-base.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Revert;

use Elementor\App\Modules\ImportExport\Runners\Runner_Interface;

abstract class Revert_Runner_Base implements Runner_Interface {

	/**
	 * By the passed data we should decide if we want to run the revert function of the runner or not.
	 *
	 * @param array $data
	 *
	 * @return bool
	 */
	abstract public function should_revert( array $data ): bool;

	/**
	 * Main function of the runner revert process.
	 *
	 * @param array $data Necessary data for the revert process.
	 */
	abstract public function revert( array $data );
}
PK     8\-w{    6  modules/import-export/runners/revert/site-settings.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Revert;

use Elementor\Plugin;
use Elementor\Core\Experiments\Manager as ExperimentsManager;

class Site_Settings extends Revert_Runner_Base {

	public static function get_name(): string {
		return 'site-settings';
	}

	public function should_revert( array $data ): bool {
		return (
			isset( $data['runners'] ) &&
			array_key_exists( static::get_name(), $data['runners'] )
		);
	}

	public function revert( array $data ) {
		Plugin::$instance->kits_manager->revert(
			$data['runners'][ static::get_name() ]['imported_kit_id'],
			$data['runners'][ static::get_name() ]['active_kit_id'],
			$data['runners'][ static::get_name() ]['previous_kit_id']
		);

		$this->revert_theme( $data );
		$this->revert_experiments( $data );
	}

	public function get_theme_upgrader(): \Theme_Upgrader {
		if ( ! class_exists( '\Theme_Upgrader' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		}

		if ( ! class_exists( '\WP_Ajax_Upgrader_Skin' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
		}

		return new \Theme_Upgrader( new \WP_Ajax_Upgrader_Skin() );
	}

	protected function revert_theme( $data ) {
		$installed_theme = $data['runners'][ static::get_name() ]['installed_theme'];
		$activated_theme = $data['runners'][ static::get_name() ]['activated_theme'];
		$previous_active_theme = $data['runners'][ static::get_name() ]['previous_active_theme'];

		if ( empty( $installed_theme ) && empty( $activated_theme ) ) {
			// no need to remove a theme as it was used before import
			return;
		}

		if ( ! empty( $activated_theme ) ) {
			$previous_theme = wp_get_theme( $previous_active_theme['slug'] );

			// no need to remove imported theme as it existed before import
			$this->activate_previous_theme( $previous_active_theme );
			return;
		}

		if ( ! empty( $installed_theme ) ) {
			$this->activate_previous_theme( $previous_active_theme );
			$this->delete_theme( $installed_theme );
		}
	}

	protected function should_delete_theme( $theme_slug ): bool {
		$current_theme = wp_get_theme();

		return $theme_slug !== $current_theme->get_stylesheet() && wp_get_theme( $theme_slug )->exists();
	}

	protected function delete_theme( $theme_slug ): bool {
		return delete_theme( $theme_slug );
	}

	protected function activate_previous_theme( $previous_active_theme ) {
		if ( ! $previous_active_theme ) {
			return;
		}

		$theme = wp_get_theme( $previous_active_theme['slug'] );

		if ( $theme->exists() ) {
			switch_theme( $theme->get_stylesheet() );
			return;
		}

		$download_url = "https://downloads.wordpress.org/theme/{$previous_active_theme['slug']}.{$previous_active_theme['version']}.zip";
		$install = $this->get_theme_upgrader()->install( $download_url );

		if ( ! $install || is_wp_error( $install ) ) {
			return;
		}

		switch_theme( $previous_active_theme['slug'] );
	}

	protected function revert_experiments( array $data ) {
		$runner_data = $data['runners'][ static::get_name() ];
		$previous_experiments = $runner_data['previous_experiments'] ?? [];

		if ( empty( $previous_experiments ) ) {
			return;
		}

		$experiments_manager = Plugin::$instance->experiments;
		$current_features = $experiments_manager->get_features();

		foreach ( $previous_experiments as $feature_name => $feature_data ) {
			if ( ! isset( $current_features[ $feature_name ] ) ) {
				continue;
			}

			if ( ! array_key_exists( $feature_name, $previous_experiments ) ) {
				continue;
			}

			$option_key = $experiments_manager->get_feature_option_key( $feature_name );
			$previous_state = $feature_data['state'];

			if ( ExperimentsManager::STATE_DEFAULT === $previous_state ) {
				delete_option( $option_key );
			} else {
				update_option( $option_key, $previous_state );
			}
		}
	}
}
PK     8\PLv  v  2  modules/import-export/runners/revert/templates.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Revert;

class Templates extends Revert_Runner_Base {
	/**
	 * The implement of this runner is part of the Pro plugin.
	 */
	public static function get_name(): string {
		return 'templates';
	}

	public function should_revert( array $data ): bool {
		return false;
	}

	public function revert( array $data ) { }
}
PK     8\,  ,  0  modules/import-export/runners/revert/plugins.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Revert;

class Plugins extends Revert_Runner_Base {

	public static function get_name(): string {
		return 'plugins';
	}

	public function should_revert( array $data ): bool {
		return false;
	}

	public function revert( array $data ) {}
}
PK     8\P$:    3  modules/import-export/runners/revert/wp-content.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Revert;

use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;

class Wp_Content extends Revert_Runner_Base {

	public static function get_name(): string {
		return 'wp-content';
	}

	public function should_revert( array $data ): bool {
		return (
			isset( $data['runners'] ) &&
			array_key_exists( static::get_name(), $data['runners'] )
		);
	}

	public function revert( array $data ) {
		$builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
		$custom_post_types = $data['runners']['wp-content']['custom_post_types'] ?? [];

		$post_types = array_merge( $builtin_post_types, $custom_post_types );

		$query_args = [
			'post_type' => $post_types,
			'post_status' => 'any',
			'posts_per_page' => -1,
			'meta_query' => [
				[
					'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
					'compare' => 'NOT EXISTS',
				],
				[
					'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
					'value' => $data['session_id'],
				],
			],
		];

		$query = new \WP_Query( $query_args );

		foreach ( $query->posts as $post ) {
			wp_delete_post( $post->ID, true );
		}

		/**
		 * Revert the nav menu terms.
		 * BC: The nav menu in new kits will be imported as part of the taxonomies, but old kits
		 * importing the nav menu terms as part from the wp-content import.
		 */
		$this->revert_nav_menus( $data );
	}

	private function revert_nav_menus( array $data ) {
		$terms = get_terms( [
			'taxonomy' => 'nav_menu',
			'hide_empty' => false,
			'get' => 'all',
			'meta_query' => [
				[
					'key'       => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
					'value'     => $data['session_id'],
				],
			],
		] );

		foreach ( $terms as $term ) {
			wp_delete_term( $term->term_id, $term->taxonomy );
		}
	}
}
PK     8\0x    3  modules/import-export/runners/revert/taxonomies.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Revert;

class Taxonomies extends Revert_Runner_Base {

	public static function get_name(): string {
		return 'taxonomies';
	}

	public function should_revert( array $data ): bool {
		return (
			isset( $data['runners'] ) &&
			array_key_exists( static::get_name(), $data['runners'] )
		);
	}

	public function revert( array $data ) {
		$taxonomies = get_taxonomies();

		$terms = get_terms( [
			'taxonomy' => $taxonomies,
			'hide_empty' => false,
			'get' => 'all',
			'meta_query' => [
				[
					'key'       => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
					'value'     => $data['session_id'],
				],
			],
		] );

		foreach ( $terms as $term ) {
			wp_delete_term( $term->term_id, $term->taxonomy );
		}
	}
}
PK     8\
!t	  	  :  modules/import-export/runners/revert/elementor-content.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Revert;

use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Plugin;

class Elementor_Content extends Revert_Runner_Base {
	private $show_page_on_front;

	private $page_on_front_id;

	public function __construct() {
		$this->init_page_on_front_data();
	}

	public static function get_name(): string {
		return 'elementor-content';
	}

	public function should_revert( array $data ): bool {
		return (
			isset( $data['runners'] ) &&
			array_key_exists( static::get_name(), $data['runners'] )
		);
	}

	public function revert( array $data ) {
		$elementor_post_types = ImportExportUtils::get_elementor_post_types();

		$query_args = [
			'post_type' => $elementor_post_types,
			'post_status' => 'any',
			'posts_per_page' => -1,
			'meta_query' => [
				[
					'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
					'compare' => 'EXISTS',
				],
				[
					'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
					'value' => $data['session_id'],
				],
			],
		];

		$query = new \WP_Query( $query_args );

		foreach ( $query->posts as $post ) {
			$post_type_document = Plugin::$instance->documents->get( $post->ID );
			$post_type_document->delete();

			// Deleting the post will reset the show_on_front option. We need to set it to false,
			// so we can set it back to what it was.
			if ( $post->ID === $this->page_on_front_id ) {
				$this->show_page_on_front = false;
			}
		}

		$this->restore_page_on_front( $data );
	}

	private function init_page_on_front_data() {
		$this->show_page_on_front = 'page' === get_option( 'show_on_front' );

		if ( $this->show_page_on_front ) {
			$this->page_on_front_id = (int) get_option( 'page_on_front' );
		}
	}

	private function restore_page_on_front( $data ) {
		if ( empty( $data['runners'][ static::get_name() ]['page_on_front'] ) ) {
			return;
		}

		$page_on_front = $data['runners'][ static::get_name() ]['page_on_front'];

		$document = Plugin::$instance->documents->get( $page_on_front );

		if ( ! $document ) {
			return;
		}

		$this->set_page_on_front( $document->get_main_id() );
	}

	private function set_page_on_front( $page_id ) {
		update_option( 'page_on_front', $page_id );

		if ( ! $this->show_page_on_front ) {
			update_option( 'show_on_front', 'page' );
		}
	}
}
PK     8\yD    2  modules/import-export/runners/runner-interface.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners;

use Elementor\App\Modules\ImportExport\Module;

interface Runner_Interface {

	const META_KEY_ELEMENTOR_IMPORT_SESSION_ID = Module::META_KEY_ELEMENTOR_IMPORT_SESSION_ID;

	const META_KEY_ELEMENTOR_EDIT_MODE = Module::META_KEY_ELEMENTOR_EDIT_MODE;

	/**
	 * Get the name of the runners, used to identify the runner.
	 * The name should be unique, unless you want to run over existing runner.
	 *
	 * @return string
	 */
	public static function get_name(): string;
}
PK     8\1	  	  6  modules/import-export/runners/export/site-settings.phpnu [        <?php
namespace Elementor\App\Modules\ImportExport\Runners\Export;

use Elementor\Plugin;

class Site_Settings extends Export_Runner_Base {

	public static function get_name(): string {
		return 'site-settings';
	}

	public function should_export( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'settings', $data['include'], true )
		);
	}

	public function export( array $data ) {
		$kit = Plugin::$instance->kits_manager->get_active_kit();
		$kit_data = $kit->get_export_data();
		$kit_tabs = $kit->get_tabs();

		$excluded_kit_settings_keys = [
			'site_name',
			'site_description',
			'site_logo',
			'site_favicon',
		];

		foreach ( $excluded_kit_settings_keys as $setting_key ) {
			unset( $kit_data['settings'][ $setting_key ] );
		}

		unset( $kit_tabs['settings-site-identity'] );

		$kit_tabs = array_keys( $kit_tabs );

		$theme_data = $this->export_theme();

		if ( $theme_data ) {
			$kit_data['theme'] = $theme_data;
			$manifest_data['theme'] = $theme_data;
		}

		$experiments_data = $this->export_experiments();

		if ( $experiments_data ) {
			$kit_data['experiments'] = $experiments_data;
			$manifest_data['experiments'] = array_keys( $experiments_data );
		}

		$manifest_data['site-settings'] = $kit_tabs;

		return [
			'files' => [
				'path' => 'site-settings',
				'data' => $kit_data,
			],
			'manifest' => [
				$manifest_data,
			],
		];
	}

	public function export_theme() {
		$theme = wp_get_theme();

		if ( empty( $theme ) || empty( $theme->get( 'ThemeURI' ) ) ) {
			return null;
		}

		$theme_data['name'] = $theme->get( 'Name' );
		$theme_data['theme_uri'] = $theme->get( 'ThemeURI' );
		$theme_data['version'] = $theme->get( 'Version' );
		$theme_data['slug'] = $theme->get_stylesheet();

		return $theme_data;
	}

	private function export_experiments() {
		$features = Plugin::$instance->experiments->get_features();

		if ( empty( $features ) ) {
			return null;
		}

		$experiments_data = [];

		foreach ( $features as $feature_name => $feature ) {
			$experiments_data[ $feature_name ] = [
				'name' => $feature_name,
				'title' => $feature['title'],
				'state' => $feature['state'],
				'default' => $feature['default'],
				'release_status' => $feature['release_status'],
			];
		}

		return empty( $experiments_data ) ? null : $experiments_data;
	}
}
PK     8\js    2  modules/import-export/runners/export/templates.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Export;

use Elementor\Core\Base\Document;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;

class Templates extends Export_Runner_Base {

	public static function get_name(): string {
		return 'templates';
	}

	public function should_export( array $data ) {
		return (
			Utils::has_pro() &&
			isset( $data['include'] ) &&
			in_array( 'templates', $data['include'], true )
		);
	}

	public function export( array $data ) {
		$template_types = array_values( Source_Local::get_template_types() );

		$query_args = [
			'post_type' => Source_Local::CPT,
			'post_status' => 'publish',
			'posts_per_page' => -1,
			'meta_query' => [
				[
					'key' => Document::TYPE_META_KEY,
					'value' => $template_types,
				],
			],
		];

		$templates_query = new \WP_Query( $query_args );

		$templates_manifest_data = [];
		$files = [];

		foreach ( $templates_query->posts as $template_post ) {
			$template_id = $template_post->ID;

			$template_document = Plugin::$instance->documents->get( $template_id );

			$templates_manifest_data[ $template_id ] = $template_document->get_export_summary();

			$files[] = [
				'path' => 'templates/' . $template_id,
				'data' => $template_document->get_export_data(),
			];
		}

		$manifest_data['templates'] = $templates_manifest_data;

		return [
			'files' => $files,
			'manifest' => [
				$manifest_data,
			],
		];
	}
}
PK     8\r    ;  modules/import-export/runners/export/export-runner-base.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Export;

use Elementor\App\Modules\ImportExport\Runners\Runner_Interface;

abstract class Export_Runner_Base implements Runner_Interface {

	/**
	 * By the passed data we should decide if we want to run the export function of the runner or not.
	 *
	 * @param array $data
	 *
	 * @return bool
	 */
	abstract public function should_export( array $data );

	/**
	 * Main function of the runner export process.
	 *
	 * @param array $data Necessary data for the export process.
	 *
	 * @return array{files: array, manifest: array}
	 * The files that should be part of the kit and the relevant manifest data.
	 */
	abstract public function export( array $data );
}
PK     8\~{7.  .  0  modules/import-export/runners/export/plugins.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Export;

class Plugins extends Export_Runner_Base {

	public static function get_name(): string {
		return 'plugins';
	}

	public function should_export( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'plugins', $data['include'], true ) &&
			is_array( $data['selected_plugins'] )
		);
	}

	public function export( array $data ) {
		$manifest_data['plugins'] = $data['selected_plugins'];

		return [
			'manifest' => [
				$manifest_data,
			],
			'files' => [],
		];
	}
}
PK     8\u    3  modules/import-export/runners/export/wp-content.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Export;

use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Core\Utils\ImportExport\WP_Exporter;

class Wp_Content extends Export_Runner_Base {

	public static function get_name(): string {
		return 'wp-content';
	}

	public function should_export( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'content', $data['include'], true )
		);
	}

	public function export( array $data ) {
		$post_types = ImportExportUtils::get_builtin_wp_post_types();
		$custom_post_types = isset( $data['selected_custom_post_types'] ) ? $data['selected_custom_post_types'] : [];

		$files = [];
		$manifest_data = [];

		foreach ( $post_types as $post_type ) {
			$export = $this->export_wp_post_type( $post_type );
			$files[] = $export['file'];
			$manifest_data['wp-content'][ $post_type ] = $export['manifest_data'];
		}

		foreach ( $custom_post_types as $post_type ) {
			$export = $this->export_wp_post_type( $post_type );
			$files[] = $export['file'];
			$manifest_data['wp-content'][ $post_type ] = $export['manifest_data'];

			$post_type_object = get_post_type_object( $post_type );

			$manifest_data['custom-post-type-title'][ $post_type ] = [
				'name' => $post_type_object->name,
				'label' => $post_type_object->label,
			];
		}

		return [
			'files' => $files,
			'manifest' => [
				$manifest_data,
			],
		];
	}

	private function export_wp_post_type( $post_type ) {
		$wp_exporter = new WP_Exporter( [
			'content' => $post_type,
			'status' => 'publish',
			'limit' => 20,
			'meta_query' => [
				[
					'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
					'compare' => 'NOT EXISTS',
				],
			],
			'include_post_featured_image_as_attachment' => true,
		] );

		$export_result = $wp_exporter->run();

		return [
			'file' => [
				'path' => 'wp-content/' . $post_type . '/' . $post_type . '.xml',
				'data' => $export_result['xml'],
			],
			'manifest_data' => $export_result['ids'],
		];
	}
}
PK     8\!
  
  3  modules/import-export/runners/export/taxonomies.phpnu [        <?php
namespace Elementor\App\Modules\ImportExport\Runners\Export;

use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;

class Taxonomies extends Export_Runner_Base {

	public static function get_name(): string {
		return 'taxonomies';
	}

	public function should_export( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'content', $data['include'], true )
		);
	}

	public function export( array $data ) {
		$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
		$selected_custom_post_types = isset( $data['selected_custom_post_types'] ) ? $data['selected_custom_post_types'] : [];
		$post_types = array_merge( $wp_builtin_post_types, $selected_custom_post_types );

		$export = $this->export_taxonomies( $post_types );

		$manifest_data['taxonomies'] = $export['manifest'];

		return [
			'files' => $export['files'],
			'manifest' => [
				$manifest_data,
			],
		];
	}

	private function export_taxonomies( array $post_types ) {
		$files = [];
		$manifest = [];

		$taxonomies = get_taxonomies();

		foreach ( $taxonomies as $taxonomy ) {
			$taxonomy_post_types = get_taxonomy( $taxonomy )->object_type;
			$intersected_post_types = array_intersect( $taxonomy_post_types, $post_types );

			if ( empty( $intersected_post_types ) ) {
				continue;
			}

			$data = $this->export_terms( $taxonomy );

			if ( empty( $data ) ) {
				continue;
			}

			foreach ( $intersected_post_types as $post_type ) {
				$manifest[ $post_type ][] = $taxonomy;
			}

			$files[] = [
				'path' => 'taxonomies/' . $taxonomy,
				'data' => $data,
			];
		}

		return [
			'files' => $files,
			'manifest' => $manifest,
		];
	}

	private function export_terms( $taxonomy ) {
		$terms = get_terms( [
			'taxonomy' => (array) $taxonomy,
			'hide_empty' => true,
			'get' => 'all',
		] );

		$ordered_terms = $this->order_terms( $terms );

		if ( empty( $ordered_terms ) ) {
			return [];
		}

		$data = [];

		foreach ( $ordered_terms as $term ) {
			$data[] = [
				'term_id' => $term->term_id,
				'name' => $term->name,
				'slug' => $term->slug,
				'taxonomy' => $term->taxonomy,
				'description' => $term->description,
				'parent' => $term->parent,
			];
		}

		return $data;
	}
	/**
	 * Put terms in order with no child going before its parent.
	 */
	private function order_terms( array $terms ) {
		$ordered_terms = [];

		while ( $term = array_shift( $terms ) ) {
			$is_top_level = 0 === $term->parent;
			$is_parent_exits = isset( $ordered_terms[ $term->parent ] );

			if ( $is_top_level || $is_parent_exits ) {
				$ordered_terms[ $term->term_id ] = $term;
			} else {
				$terms[] = $term;
			}
		}

		return $ordered_terms;
	}
}
PK     8\!c    :  modules/import-export/runners/export/elementor-content.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Runners\Export;

use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Plugin;

class Elementor_Content extends Export_Runner_Base {
	private $page_on_front_id;

	public function __construct() {
		$this->init_page_on_front_data();
	}

	public static function get_name(): string {
		return 'elementor-content';
	}

	public function should_export( array $data ) {
		return (
			isset( $data['include'] ) &&
			in_array( 'content', $data['include'], true )
		);
	}

	public function export( array $data ) {
		$elementor_post_types = ImportExportUtils::get_elementor_post_types();

		$files = [];
		$manifest = [];

		foreach ( $elementor_post_types as $post_type ) {
			$export = $this->export_elementor_post_type( $post_type );
			$files = array_merge( $files, $export['files'] );

			$manifest[ $post_type ] = $export['manifest_data'];
		}

		$manifest_data['content'] = $manifest;

		return [
			'files' => $files,
			'manifest' => [
				$manifest_data,
			],
		];
	}

	private function export_elementor_post_type( $post_type ) {
		$query_args = [
			'post_type' => $post_type,
			'post_status' => 'publish',
			'posts_per_page' => -1,
			'meta_query' => [
				[
					'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
					'compare' => 'EXISTS',
				],
				[
					'key' => '_elementor_data',
					'compare' => 'EXISTS',
				],
				[
					'key' => '_elementor_data',
					'compare' => '!=',
					'value' => '[]',
				],
			],
		];

		$query = new \WP_Query( $query_args );

		if ( empty( $query ) ) {
			return [
				'files' => [],
				'manifest_data' => [],
			];
		}

		$post_type_taxonomies = $this->get_post_type_taxonomies( $post_type );

		$manifest_data = [];
		$files = [];

		foreach ( $query->posts as $post ) {
			$document = Plugin::$instance->documents->get( $post->ID );

			$terms = ! empty( $post_type_taxonomies ) ? $this->get_post_terms( $post->ID, $post_type_taxonomies ) : [];

			$post_manifest_data = [
				'title' => $post->post_title,
				'excerpt' => $post->post_excerpt,
				'doc_type' => $document->get_name(),
				'thumbnail' => get_the_post_thumbnail_url( $post ),
				'url' => get_permalink( $post ),
				'terms' => $terms,
			];

			if ( $post->ID === $this->page_on_front_id ) {
				$post_manifest_data['show_on_front'] = true;
			}

			$manifest_data[ $post->ID ] = $post_manifest_data;

			$files[] = [
				'path' => 'content/' . $post_type . '/' . $post->ID,
				'data' => $document->get_export_data(),
			];
		}

		return [
			'files' => $files,
			'manifest_data' => $manifest_data,
		];
	}

	private function get_post_type_taxonomies( $post_type ) {
		return get_object_taxonomies( $post_type );
	}

	private function get_post_terms( $post_id, array $taxonomies ) {
		$terms = wp_get_object_terms( $post_id, $taxonomies );

		$result = [];

		foreach ( $terms as $term ) {
			$result[] = [
				'term_id' => $term->term_id,
				'taxonomy' => $term->taxonomy,
				'slug' => $term->slug,
			];
		}

		return $result;
	}

	private function init_page_on_front_data() {
		$show_page_on_front = 'page' === get_option( 'show_on_front' );

		if ( $show_page_on_front ) {
			$this->page_on_front_id = (int) get_option( 'page_on_front' );
		}
	}
}
PK     8\v夼$  $  *  modules/import-export/processes/export.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Processes;

use Elementor\App\Modules\ImportExport\Module;
use Elementor\App\Modules\ImportExport\Utils;
use Elementor\Core\Utils\Str;
use Elementor\Plugin;

use Elementor\App\Modules\ImportExport\Runners\Export\Elementor_Content;
use Elementor\App\Modules\ImportExport\Runners\Export\Export_Runner_Base;
use Elementor\App\Modules\ImportExport\Runners\Export\Plugins;
use Elementor\App\Modules\ImportExport\Runners\Export\Site_Settings;
use Elementor\App\Modules\ImportExport\Runners\Export\Taxonomies;
use Elementor\App\Modules\ImportExport\Runners\Export\Templates;
use Elementor\App\Modules\ImportExport\Runners\Export\Wp_Content;

class Export {
	const ZIP_ARCHIVE_MODULE_MISSING = 'zip-archive-module-is-missing';

	/**
	 * @var Export_Runner_Base[]
	 */
	protected $runners = [];

	/**
	 * Selected content types to export.
	 *
	 * @var array
	 */
	private $settings_include;

	/**
	 * The kit information. (e.g: title, description)
	 *
	 * @var array $export_data
	 */
	private $settings_kit_info;

	/**
	 * Selected plugins to export.
	 * Contains the plugins essential data for export. (e.g: name, path, version, etc.)
	 *
	 * @var array
	 */
	private $settings_selected_plugins;

	/**
	 * Selected custom post types to export.
	 *
	 * @var array
	 */
	private $settings_selected_custom_post_types;

	/**
	 * The output data of the export process.
	 * Will be written into the manifest.json file.
	 *
	 * @var array
	 */
	private $manifest_data;

	/**
	 * The zip archive object.
	 *
	 * @var \ZipArchive
	 */
	private $zip;

	public function __construct( $settings = [] ) {
		$this->settings_include = ! empty( $settings['include'] ) ? $settings['include'] : null;
		$this->settings_kit_info = ! empty( $settings['kitInfo'] ) ? $settings['kitInfo'] : null;
		$this->settings_selected_plugins = isset( $settings['plugins'] ) ? $settings['plugins'] : null;
		$this->settings_selected_custom_post_types = isset( $settings['selectedCustomPostTypes'] ) ? $settings['selectedCustomPostTypes'] : null;
	}

	/**
	 * Register a runner.
	 *
	 * @param Export_Runner_Base $runner_instance
	 */
	public function register( Export_Runner_Base $runner_instance ) {
		$this->runners[ $runner_instance::get_name() ] = $runner_instance;
	}

	public function register_default_runners() {
		$this->register( new Site_Settings() );
		$this->register( new Plugins() );
		$this->register( new Templates() );
		$this->register( new Taxonomies() );
		$this->register( new Elementor_Content() );
		$this->register( new Wp_Content() );
	}

	/**
	 * Execute the export process.
	 *
	 * @return array The export data output.
	 *
	 * @throws \Exception If no export runners have been specified.
	 */
	public function run() {
		if ( empty( $this->runners ) ) {
			throw new \Exception( 'Couldn’t execute the export process because no export runners have been specified. Try again by specifying export runners.' );
		}

		$this->set_default_settings();

		$this->init_zip_archive();
		$this->init_manifest_data();

		$data = [
			'include' => $this->settings_include,
			'selected_plugins' => $this->settings_selected_plugins,
			'selected_custom_post_types' => $this->settings_selected_custom_post_types,
		];

		foreach ( $this->runners as $runner ) {
			if ( $runner->should_export( $data ) ) {
				$export_result = $runner->export( $data );
				$this->handle_export_result( $export_result );
			}
		}

		$this->add_json_file( 'manifest', $this->manifest_data );

		$zip_file_name = $this->zip->filename;
		$this->zip->close();

		return [
			'manifest' => $this->manifest_data,
			'file_name' => $zip_file_name,
		];
	}

	/**
	 * Set default settings for the export.
	 */
	private function set_default_settings() {
		if ( ! is_array( $this->get_settings_include() ) ) {
			$this->settings_include( $this->get_default_settings_include() );
		}

		if ( ! is_array( $this->get_settings_kit_info() ) ) {
			$this->settings_kit_info( $this->get_default_settings_kit_info() );
		}

		if ( ! is_array( $this->get_settings_selected_custom_post_types() ) && in_array( 'content', $this->settings_include, true ) ) {
			$this->settings_selected_custom_post_types( $this->get_default_settings_custom_post_types() );
		}

		if ( ! is_array( $this->get_settings_selected_plugins() ) && in_array( 'plugins', $this->settings_include, true ) ) {
			$this->settings_selected_plugins( $this->get_default_settings_selected_plugins() );
		}
	}

	public function settings_include( $included_settings ) {
		$this->settings_include = $included_settings;
	}

	public function get_settings_include() {
		return $this->settings_include;
	}

	private function settings_kit_info( $kit_info ) {
		$this->settings_kit_info = $kit_info;
	}

	private function get_settings_kit_info() {
		return $this->settings_kit_info;
	}

	public function settings_selected_custom_post_types( $selected_custom_post_types ) {
		$this->settings_selected_custom_post_types = $selected_custom_post_types;
	}

	public function get_settings_selected_custom_post_types() {
		return $this->settings_selected_custom_post_types;
	}

	public function settings_selected_plugins( $plugins ) {
		$this->settings_selected_plugins = $plugins;
	}

	public function get_settings_selected_plugins() {
		return $this->settings_selected_plugins;
	}

	/**
	 * Get the default settings of which content types should be exported.
	 *
	 * @return array
	 */
	private function get_default_settings_include() {
		return [ 'templates', 'content', 'settings', 'plugins' ];
	}

	/**
	 * Get the default settings of the kit info.
	 *
	 * @return array
	 */
	private function get_default_settings_kit_info() {
		return [
			'title' => 'kit',
			'description' => '',
		];
	}

	/**
	 * Get the default settings of the plugins that should be exported.
	 *
	 * @return array{name: string, plugin:string, pluginUri: string, version: string}
	 */
	private function get_default_settings_selected_plugins() {
		$installed_plugins = Plugin::$instance->wp->get_plugins();

		return $installed_plugins->map( function ( $item, $key ) {
			return [
				'name' => $item['Name'],
				'plugin' => $key,
				'pluginUri' => $item['PluginURI'],
				'version' => $item['Version'],
			];
		} )->all();
	}

	/**
	 * Get the default settings of all the custom post types that should be exported.
	 * Should be all the custom post types that are not built in to WordPress and not part of Elementor.
	 *
	 * @return array
	 */
	private function get_default_settings_custom_post_types() {
		return Utils::get_registered_cpt_names();
	}

	/**
	 * Init the zip archive.
	 *
	 * @throws \Error If export process fails, file creation errors occur, or data serialization fails.
	 */
	private function init_zip_archive() {
		if ( ! class_exists( '\ZipArchive' ) ) {
			throw new \Error( static::ZIP_ARCHIVE_MODULE_MISSING ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		$zip = new \ZipArchive();

		$temp_dir = Plugin::$instance->uploads_manager->create_unique_dir();

		$zip_file_name = $temp_dir . sanitize_title( $this->settings_kit_info['title'] ) . '.zip';

		$zip->open( $zip_file_name, \ZipArchive::CREATE | \ZipArchive::OVERWRITE );

		$this->zip = $zip;
	}

	/**
	 * Init the manifest data and add some basic info to it.
	 */
	private function init_manifest_data() {
		$kit_post = Plugin::$instance->kits_manager->get_active_kit()->get_post();

		$manifest_data = [
			'name' => sanitize_title( $this->settings_kit_info['title'] ),
			'title' => $this->settings_kit_info['title'],
			'description' => $this->settings_kit_info['description'],
			'author' => get_the_author_meta( 'display_name', $kit_post->post_author ),
			'version' => Module::FORMAT_VERSION,
			'elementor_version' => ELEMENTOR_VERSION,
			'created' => gmdate( 'Y-m-d H:i:s' ),
			'thumbnail' => get_the_post_thumbnail_url( $kit_post ),
			'site' => get_site_url(),
		];

		$this->manifest_data = $manifest_data;
	}

	/**
	 * Handle the export process output.
	 * Add the manifest data from the runner to the manifest.json file.
	 * Create files according to the files array that should be exported by the runner.
	 *
	 * @param array $export_result
	 */
	private function handle_export_result( $export_result ) {
		foreach ( $export_result['manifest'] as $data ) {
			$this->manifest_data += $data;
		}

		if ( isset( $export_result['files']['path'] ) ) {
			$export_result['files'] = [ $export_result['files'] ];
		}

		foreach ( $export_result['files'] as $file ) {
			$file_extension = pathinfo( $file['path'], PATHINFO_EXTENSION );
			if ( empty( $file_extension ) ) {
				$this->add_json_file(
					$file['path'],
					$file['data']
				);
			} else {
				$this->add_file(
					$file['path'],
					$file['data']
				);
			}
		}
	}

	/**
	 * Add json file to the zip archive.
	 *
	 * @param string $path The relative path to the file.
	 * @param array  $content The content of the file.
	 * @param int    $json_flags
	 */
	private function add_json_file( $path, array $content, $json_flags = 0 ) {
		if ( ! Str::ends_with( $path, '.json' ) ) {
			$path .= '.json';
		}

		$this->add_file( $path, wp_json_encode( $content, $json_flags ) );
	}

	/**
	 * Add file to the zip archive.
	 *
	 * @param string $file
	 * @param string $content The content of the file.
	 */
	private function add_file( $file, $content ) {
		$this->zip->addFromString( $file, $content );
	}
}
PK     8\i4  4  *  modules/import-export/processes/revert.phpnu [        <?php
namespace Elementor\App\Modules\ImportExport\Processes;

use Elementor\App\Modules\ImportExport\Module;
use Elementor\App\Modules\ImportExport\Runners\Revert\Elementor_Content;
use Elementor\App\Modules\ImportExport\Runners\Revert\Revert_Runner_Base;
use Elementor\App\Modules\ImportExport\Runners\Revert\Plugins;
use Elementor\App\Modules\ImportExport\Runners\Revert\Site_Settings;
use Elementor\App\Modules\ImportExport\Runners\Revert\Taxonomies;
use Elementor\App\Modules\ImportExport\Runners\Revert\Templates;
use Elementor\App\Modules\ImportExport\Runners\Revert\Wp_Content;
use Elementor\App\Modules\ImportExport\Utils;

class Revert {

	/**
	 * @var Revert_Runner_Base[]
	 */
	protected $runners = [];

	private $import_sessions;

	private $revert_sessions;

	public function __construct() {
		$this->import_sessions = self::get_import_sessions();
		$this->revert_sessions = self::get_revert_sessions();
	}

	/**
	 * Register a runner.
	 *
	 * @param Revert_Runner_Base $runner_instance
	 */
	public function register( Revert_Runner_Base $runner_instance ) {
		$this->runners[ $runner_instance::get_name() ] = $runner_instance;
	}

	public function register_default_runners() {
		$this->register( new Site_Settings() );
		$this->register( new Plugins() );
		$this->register( new Templates() );
		$this->register( new Taxonomies() );
		$this->register( new Elementor_Content() );
		$this->register( new Wp_Content() );
	}

	/**
	 * Execute the revert process.
	 *
	 * @throws \Exception If no revert runners have been specified.
	 */
	public function run() {
		if ( empty( $this->runners ) ) {
			throw new \Exception( 'Couldn’t execute the revert process because no revert runners have been specified. Try again by specifying revert runners.' );
		}

		$import_session = $this->get_last_import_session();

		if ( empty( $import_session ) ) {
			throw new \Exception( 'Couldn’t execute the revert process because there are no import sessions to revert.' );
		}

		// fallback if the import session failed and doesn't have the runners metadata
		if ( ! isset( $import_session['runners'] ) && isset( $import_session['instance_data'] ) ) {
			$import_session['runners'] = $import_session['instance_data']['runners_import_metadata'] ?? [];
		}

		foreach ( $this->runners as $runner ) {
			if ( $runner->should_revert( $import_session ) ) {
				$runner->revert( $import_session );
			}
		}

		$this->revert_attachments( $import_session );

		$this->delete_last_import_data();
	}

	public static function get_import_sessions() {
		$import_sessions = Utils::get_import_sessions();

		if ( ! $import_sessions ) {
			return [];
		}

		usort( $import_sessions, function( $a, $b ) {
			return strcmp( $a['start_timestamp'], $b['start_timestamp'] );
		} );

		return $import_sessions;
	}

	public static function get_revert_sessions() {
		$revert_sessions = get_option( Module::OPTION_KEY_ELEMENTOR_REVERT_SESSIONS );

		if ( ! $revert_sessions ) {
			return [];
		}

		return $revert_sessions;
	}

	public function get_last_import_session() {
		$import_sessions = $this->import_sessions;

		if ( empty( $import_sessions ) ) {
			return [];
		}

		return end( $import_sessions );
	}

	public function get_penultimate_import_session() {
		$sessions_data = $this->import_sessions;
		$penultimate_element_value = [];

		if ( empty( $sessions_data ) ) {
			return [];
		}

		end( $sessions_data );

		prev( $sessions_data );

		if ( ! is_null( key( $sessions_data ) ) ) {
			$penultimate_element_value = current( $sessions_data );
		}

		return $penultimate_element_value;
	}

	private function delete_last_import_data() {
		$import_sessions = $this->import_sessions;
		$revert_sessions = $this->revert_sessions;

		$reverted_session = array_pop( $import_sessions );

		$revert_sessions[] = [
			'session_id' => $reverted_session['session_id'],
			'kit_title' => $reverted_session['kit_title'],
			'kit_name' => $reverted_session['kit_name'],
			'kit_thumbnail' => $reverted_session['kit_thumbnail'],
			'source' => $reverted_session['kit_source'],
			'user_id' => get_current_user_id(),
			'import_timestamp' => $reverted_session['start_timestamp'],
			'revert_timestamp' => current_time( 'timestamp' ),
		];

		update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
		update_option( Module::OPTION_KEY_ELEMENTOR_REVERT_SESSIONS, $revert_sessions, false );

		$this->import_sessions = $import_sessions;
		$this->revert_sessions = $revert_sessions;
	}

	private function revert_attachments( $data ) {
		$query_args = [
			'post_type' => 'attachment',
			'post_status' => 'any',
			'posts_per_page' => -1,
			'meta_query' => [
				[
					'key' => Module::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
					'value' => $data['session_id'],
				],
			],
		];

		$query = new \WP_Query( $query_args );

		foreach ( $query->posts as $post ) {
			wp_delete_attachment( $post->ID, true );
		}
	}
}
PK     8\,{#|`  `  *  modules/import-export/processes/import.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Processes;

use Elementor\App\Modules\ImportExport\Compatibility\Base_Adapter;
use Elementor\App\Modules\ImportExport\Compatibility\Envato;
use Elementor\App\Modules\ImportExport\Compatibility\Kit_Library;
use Elementor\App\Modules\ImportExport\Utils;
use Elementor\Core\Base\Document;
use Elementor\Core\Kits\Documents\Kit;
use Elementor\Plugin;

use Elementor\App\Modules\ImportExport\Runners\Import\Elementor_Content;
use Elementor\App\Modules\ImportExport\Runners\Import\Import_Runner_Base;
use Elementor\App\Modules\ImportExport\Runners\Import\Plugins;
use Elementor\App\Modules\ImportExport\Runners\Import\Site_Settings;
use Elementor\App\Modules\ImportExport\Runners\Import\Taxonomies;
use Elementor\App\Modules\ImportExport\Runners\Import\Templates;
use Elementor\App\Modules\ImportExport\Runners\Import\Wp_Content;
use Elementor\App\Modules\ImportExport\Module;

class Import {
	const MANIFEST_ERROR_KEY = 'manifest-error';

	const ZIP_FILE_ERROR_KEY = 'invalid-zip-file';

	const ZIP_ARCHIVE_ERROR_KEY = 'zip-archive-module-missing';

	/**
	 * @var Import_Runner_Base[]
	 */
	protected $runners = [];

	/**
	 * The session ID of the import process.
	 * This ID is uniquely generated for each import process (by the temp folder which contains the extracted kit files).
	 *
	 * @var string
	 */
	private $session_id;

	/**
	 * The Kit ID.
	 *
	 * @var string
	 */
	private $kit_id;

	/**
	 * Adapter for the kit compatibility.
	 *
	 * @var Base_Adapter[]
	 */
	private $adapters;

	/**
	 * Document's data (elements and settings) that was imported during the process.
	 *
	 * @var array { [document_id] => { "elements": array , "settings": array } }
	 */
	private $documents_data = [];

	/**
	 * Path to the extracted kit files.
	 *
	 * @var string
	 */
	private $extracted_directory_path;

	/**
	 * Imported kit manifest.
	 *
	 * @var array
	 */
	private $manifest;

	/**
	 * Imported kit site settings. (e.g: custom_colors, custom_typography, etc.)
	 *
	 * @var array
	 */
	private $site_settings;

	/**
	 * Selected content types to import.
	 *
	 * @var array
	 */
	private $settings_include;

	/**
	 * Referer of the import. (e.g: kit-library, local, etc.)
	 *
	 * @var string
	 */
	private $settings_referrer;

	/**
	 * All the conflict between the exited templates and the kit templates.
	 *
	 * @var array
	 */
	private $settings_conflicts;

	/**
	 * Selected elementor templates conditions to override.
	 *
	 * @var array
	 */
	private $settings_selected_override_conditions;

	/**
	 * Selected custom post types to import.
	 *
	 * @var array
	 */
	private $settings_selected_custom_post_types;

	/**
	 * Selected plugins to import.
	 *
	 * @var array
	 */
	private $settings_selected_plugins;

	/**
	 * The imported data output.
	 *
	 * @var array
	 */
	private $imported_data = [];

	/**
	 * The metadata output of the import runners.
	 * Will be saved in the import_session and will be used to revert the import process.
	 *
	 * @var array
	 */
	private $runners_import_metadata = [];

	/**
	 * @param string     $path session_id | zip_file_path
	 * @param array      $settings Use to determine which content to import.
	 *                   (e.g: include, selected_plugins, selected_cpt, selected_override_conditions, etc.)
	 * @param array|null $old_instance An array of old instance parameters that will be used for creating new instance.
	 *                   We are using it for quick creation of the instance when the import process is being split into chunks.
	 *
	 * @throws \Exception If the import session does not exist.
	 */
	public function __construct( string $path, array $settings = [], array $old_instance = null ) {
		if ( ! empty( $old_instance ) ) {
			$this->set_import_object( $old_instance );
		} else {
			if ( is_file( $path ) ) {
				$this->extracted_directory_path = $this->extract_zip( $path );
			} else {
				$elementor_tmp_directory = Plugin::$instance->uploads_manager->get_temp_dir();
				$path = $elementor_tmp_directory . basename( $path );

				if ( ! is_dir( $path ) ) {
					throw new \Exception( 'Couldn’t execute the import process because the import session does not exist.' );
				}

				$this->extracted_directory_path = $path . '/';
			}

			$this->session_id = basename( $this->extracted_directory_path );
			$this->kit_id = $settings['id'] ?? '';
			$this->settings_referrer = ! empty( $settings['referrer'] ) ? $settings['referrer'] : 'local';
			$this->settings_include = ! empty( $settings['include'] ) ? $settings['include'] : null;

			// Using isset and not empty is important since empty array is valid option.
			$this->settings_selected_override_conditions = $settings['overrideConditions'] ?? null;
			$this->settings_selected_custom_post_types = $settings['selectedCustomPostTypes'] ?? null;
			$this->settings_selected_plugins = $settings['plugins'] ?? null;

			$this->manifest = $this->read_manifest_json();
			$this->site_settings = $this->read_site_settings_json();

			$this->set_default_settings();
		}

		add_filter( 'wp_php_error_args', function ( $args, $error ) {
			return $this->filter_php_error_args( $args, $error );
		}, 10, 2 );
	}

	/**
	 * Set the import object parameters.
	 *
	 * @param array $instance
	 * @return void
	 */
	private function set_import_object( array $instance ) {
		$this->session_id = $instance['session_id'];

		$instance_data = $instance['instance_data'];

		$this->extracted_directory_path = $instance_data['extracted_directory_path'];
		$this->runners = $instance_data['runners'];
		$this->adapters = $instance_data['adapters'];

		$this->manifest = $instance_data['manifest'];
		$this->site_settings = $instance_data['site_settings'];

		$this->settings_include = $instance_data['settings_include'];
		$this->settings_referrer = $instance_data['settings_referrer'];
		$this->settings_conflicts = $instance_data['settings_conflicts'];
		$this->settings_selected_override_conditions = $instance_data['settings_selected_override_conditions'];
		$this->settings_selected_custom_post_types = $instance_data['settings_selected_custom_post_types'];
		$this->settings_selected_plugins = $instance_data['settings_selected_plugins'];

		$this->documents_data = $instance_data['documents_data'];
		$this->imported_data = $instance_data['imported_data'];
		$this->runners_import_metadata = $instance_data['runners_import_metadata'];
	}

	/**
	 * Creating a new instance of the import process by the id of the old import session.
	 *
	 * @param string $session_id
	 *
	 * @return Import
	 * @throws \Exception If the import session does not exist.
	 */
	public static function from_session( string $session_id ): Import {
		$import_sessions = Utils::get_import_sessions();

		if ( ! $import_sessions || ! isset( $import_sessions[ $session_id ] ) ) {
			throw new \Exception( 'Couldn’t execute the import process because the import session does not exist.' );
		}

		$import_session = $import_sessions[ $session_id ];

		return new self( $session_id, [], $import_session );
	}

	/**
	 * Register a runner.
	 * Be aware that the runner will be executed in the order of registration, the order is crucial for the import process.
	 *
	 * @param Import_Runner_Base $runner_instance
	 */
	public function register( Import_Runner_Base $runner_instance ) {
		$this->runners[ $runner_instance::get_name() ] = $runner_instance;
	}

	public function register_default_runners() {
		$this->register( new Site_Settings() );
		$this->register( new Plugins() );
		$this->register( new Templates() );
		$this->register( new Taxonomies() );
		$this->register( new Elementor_Content() );
		$this->register( new Wp_Content() );
	}

	/**
	 * Set default settings for the import.
	 */
	private function set_default_settings() {
		if ( ! is_array( $this->get_settings_include() ) ) {
			$this->settings_include( $this->get_default_settings_include() );
		}

		if ( ! is_array( $this->get_settings_conflicts() ) ) {
			$this->settings_conflicts( $this->get_default_settings_conflicts() );
		}

		if ( ! is_array( $this->get_settings_selected_override_conditions() ) ) {
			$this->settings_selected_override_conditions( $this->get_default_settings_override_conditions() );
		}

		if ( ! is_array( $this->get_settings_selected_custom_post_types() ) ) {
			$this->settings_selected_custom_post_types( $this->get_default_settings_custom_post_types() );
		}

		if ( ! is_array( $this->get_settings_selected_plugins() ) ) {
			$this->settings_selected_plugins( $this->get_default_settings_plugins() );
		}
	}

	/**
	 * Execute the import process.
	 *
	 * @return array The imported data output.
	 *
	 * @throws \Exception If no import runners have been specified.
	 */
	public function run() {
		if ( empty( $this->runners ) ) {
			throw new \Exception( 'Couldn’t execute the import process because no import runners have been specified. Try again by specifying import runners.' );
		}

		$data = [
			'session_id' => $this->session_id,
			'include' => $this->settings_include,
			'manifest' => $this->manifest,
			'site_settings' => $this->site_settings,
			'selected_plugins' => $this->settings_selected_plugins,
			'extracted_directory_path' => $this->extracted_directory_path,
			'selected_custom_post_types' => $this->settings_selected_custom_post_types,
		];

		$this->init_import_session();

		remove_filter( 'elementor/document/save/data', [ Plugin::$instance->modules_manager->get_modules( 'content-sanitizer' ), 'sanitize_content' ] );
		add_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10, 2 );

		// Set the Request's state as an Elementor upload request, in order to support unfiltered file uploads.
		Plugin::$instance->uploads_manager->set_elementor_upload_state( true );

		foreach ( $this->runners as $runner ) {
			if ( $runner->should_import( $data ) ) {
				$import = $runner->import( $data, $this->imported_data );
				$this->imported_data = array_merge_recursive( $this->imported_data, $import );

				$this->runners_import_metadata[ $runner::get_name() ] = $runner->get_import_session_metadata();
			}
		}

		// After the upload complete, set the elementor upload state back to false.
		Plugin::$instance->uploads_manager->set_elementor_upload_state( false );

		remove_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10 );

		$this->finalize_import_session_option();

		$this->save_elements_of_imported_posts();

		Plugin::$instance->uploads_manager->remove_file_or_dir( $this->extracted_directory_path );
		return $this->imported_data;
	}

	/**
	 * Run specific runner by runner_name
	 *
	 * @param string $runner_name
	 *
	 * @return array
	 *
	 * @throws \Exception If no export runners have been specified.
	 */
	public function run_runner( string $runner_name ): array {
		if ( empty( $this->runners ) ) {
			throw new \Exception( 'Couldn’t execute the import process because no import runners have been specified. Try again by specifying import runners.' );
		}

		$data = [
			'session_id' => $this->session_id,
			'include' => $this->settings_include,
			'manifest' => $this->manifest,
			'site_settings' => $this->site_settings,
			'selected_plugins' => $this->settings_selected_plugins,
			'extracted_directory_path' => $this->extracted_directory_path,
			'selected_custom_post_types' => $this->settings_selected_custom_post_types,
		];

		add_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10, 2 );

		// Set the Request's state as an Elementor upload request, in order to support unfiltered file uploads.
		Plugin::$instance->uploads_manager->set_elementor_upload_state( true );

		$runner = $this->runners[ $runner_name ];

		if ( empty( $runner ) ) {
			throw new \Exception( 'Couldn’t execute the import process because the import runner was not found. Try again by specifying an import runner.' );
		}

		if ( $runner->should_import( $data ) ) {
			$import = $runner->import( $data, $this->imported_data );
			$this->imported_data = array_merge_recursive( $this->imported_data, $import );

			$this->runners_import_metadata[ $runner::get_name() ] = $runner->get_import_session_metadata();
		}

		// After the upload complete, set the elementor upload state back to false.
		Plugin::$instance->uploads_manager->set_elementor_upload_state( false );

		remove_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10 );

		$is_last_runner = key( array_slice( $this->runners, -1, 1, true ) ) === $runner_name;
		if ( $is_last_runner ) {
			$this->finalize_import_session_option();
			$this->save_elements_of_imported_posts();
		} else {
			$this->update_instance_data_in_import_session_option();
		}

		return [
			'status' => 'success',
			'runner' => $runner_name,
		];
	}

	/**
	 * Create and save all the instance data to the import sessions option.
	 *
	 * @return void
	 */
	public function init_import_session( $save_instance_data = false ) {
		$import_sessions = Utils::get_import_sessions( true );

		$import_sessions[ $this->session_id ] = [
			'session_id' => $this->session_id,
			'kit_title' => $this->manifest['title'] ?? '',
			'kit_name' => $this->manifest['name'] ?? '',
			'kit_thumbnail' => $this->get_kit_thumbnail(),
			'kit_source' => $this->settings_referrer,
			'user_id' => get_current_user_id(),
			'start_timestamp' => current_time( 'timestamp' ),
		];

		if ( $save_instance_data ) {
			$import_sessions[ $this->session_id ]['instance_data'] = [
				'extracted_directory_path' => $this->extracted_directory_path,
				'runners' => $this->runners,
				'adapters' => $this->adapters,

				'manifest' => $this->manifest,
				'site_settings' => $this->site_settings,

				'settings_include' => $this->settings_include,
				'settings_referrer' => $this->settings_referrer,
				'settings_conflicts' => $this->settings_conflicts,
				'settings_selected_override_conditions' => $this->settings_selected_override_conditions,
				'settings_selected_custom_post_types' => $this->settings_selected_custom_post_types,
				'settings_selected_plugins' => $this->settings_selected_plugins,

				'documents_data' => $this->documents_data,
				'imported_data' => $this->imported_data,
				'runners_import_metadata' => $this->runners_import_metadata,
			];
		}

		update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
	}

	/**
	 * Get the Kit thumbnail, goes to the home page thumbnail if main doesn't exist
	 *
	 * @return string
	 */
	private function get_kit_thumbnail(): string {
		if ( ! empty( $this->manifest['thumbnail'] ) ) {
			return $this->manifest['thumbnail'];
		}

		return apply_filters( 'elementor/import/kit_thumbnail', '', $this->kit_id, $this->settings_referrer );
	}

	public function get_runners_name(): array {
		return array_keys( $this->runners );
	}

	public function get_manifest() {
		return $this->manifest;
	}

	public function get_extracted_directory_path() {
		return $this->extracted_directory_path;
	}

	public function get_session_id() {
		return $this->session_id;
	}

	public function get_adapters() {
		return $this->adapters;
	}

	public function get_imported_data() {
		return $this->imported_data;
	}

	/**
	 * Get settings by key.
	 * Used for backward compatibility.
	 *
	 * @param string $key The key of the setting.
	 */
	public function get_settings( $key ) {
		switch ( $key ) {
			case 'include':
				return $this->get_settings_include();

			case 'overrideConditions':
				return $this->get_settings_selected_override_conditions();

			case 'selectedCustomPostTypes':
				return $this->get_settings_selected_custom_post_types();

			case 'plugins':
				return $this->get_settings_selected_plugins();

			default:
				return [];
		}
	}

	public function settings_include( array $settings_include ) {
		$this->settings_include = $settings_include;

		return $this;
	}

	public function get_settings_include() {
		return $this->settings_include;
	}

	public function settings_referrer( $settings_referrer ) {
		$this->settings_referrer = $settings_referrer;

		return $this;
	}

	public function get_settings_referrer() {
		return $this->settings_referrer;
	}

	public function settings_conflicts( array $settings_conflicts ) {
		$this->settings_conflicts = $settings_conflicts;

		return $this;
	}

	public function get_settings_conflicts() {
		return $this->settings_conflicts;
	}

	public function settings_selected_override_conditions( array $settings_selected_override_conditions ) {
		$this->settings_selected_override_conditions = $settings_selected_override_conditions;

		return $this;
	}

	public function get_settings_selected_override_conditions() {
		return $this->settings_selected_override_conditions;
	}

	public function settings_selected_custom_post_types( array $settings_selected_custom_post_types ) {
		$this->settings_selected_custom_post_types = $settings_selected_custom_post_types;

		return $this;
	}

	public function get_settings_selected_custom_post_types() {
		return $this->settings_selected_custom_post_types;
	}

	public function settings_selected_plugins( array $settings_selected_plugins ) {
		$this->settings_selected_plugins = $settings_selected_plugins;

		return $this;
	}

	public function get_settings_selected_plugins() {
		return $this->settings_selected_plugins;
	}

	/**
	 * Prevent saving elements on elementor post creation.
	 *
	 * @param array    $data
	 * @param Document $document
	 *
	 * @return array
	 */
	public function prevent_saving_elements_on_post_creation( array $data, Document $document ) {
		if ( isset( $data['elements'] ) ) {
			$this->documents_data[ $document->get_main_id() ] = [ 'elements' => $data['elements'] ];

			$data['elements'] = [];
		}

		if ( isset( $data['settings'] ) ) {
			$this->documents_data[ $document->get_main_id() ]['settings'] = $data['settings'];

		}

		return $data;
	}

	/**
	 * Extract the zip file.
	 *
	 * @param string $zip_path The path to the zip file.
	 * @return string The extracted directory path.
	 * @throws \Error If import process fails, file validation errors occur, or data corruption is detected.
	 */
	private function extract_zip( $zip_path ) {
		$extraction_result = Plugin::$instance->uploads_manager->extract_and_validate_zip( $zip_path, [ 'json', 'xml' ] );

		if ( is_wp_error( $extraction_result ) ) {
			if ( isset( $extraction_result->errors['zip_error'] ) ) {
				throw new \Error( static::ZIP_ARCHIVE_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			}

			throw new \Error( static::ZIP_FILE_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		return $extraction_result['extraction_directory'];
	}

	/**
	 * Get the manifest file from the extracted directory and adapt it if needed.
	 *
	 * @return string The manifest file content.
	 *
	 * @throws \Error If import validation fails or processing errors occur.
	 */
	private function read_manifest_json() {
		$manifest = Utils::read_json_file( $this->extracted_directory_path . 'manifest' );

		if ( ! $manifest ) {
			Plugin::$instance->logger->get_logger()->error( static::MANIFEST_ERROR_KEY );
			throw new \Error( static::ZIP_FILE_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		$this->init_adapters( $manifest );

		foreach ( $this->adapters as $adapter ) {
			$manifest = $adapter->adapt_manifest( $manifest );
		}

		return $manifest;
	}

	/**
	 * Init the adapters and determine which ones to use.
	 *
	 * @param array $manifest_data The manifest file content.
	 */
	private function init_adapters( array $manifest_data ) {
		$this->adapters = [];

		/** @var Base_Adapter[] $adapter_types */
		$adapter_types = [ Envato::class, Kit_Library::class ];

		foreach ( $adapter_types as $adapter_type ) {
			if ( $adapter_type::is_compatibility_needed( $manifest_data, [ 'referrer' => $this->get_settings_referrer() ] ) ) {
				$this->adapters[] = new $adapter_type( $this );
			}
		}
	}

	/**
	 * Get the site settings file from the extracted directory and adapt it if needed.
	 *
	 * @return string The site settings file content.
	 */
	private function read_site_settings_json() {
		$site_settings = Utils::read_json_file( $this->extracted_directory_path . 'site-settings' );

		foreach ( $this->adapters as $adapter ) {
			$site_settings = $adapter->adapt_site_settings( $site_settings, $this->manifest, $this->extracted_directory_path );
		}

		return $site_settings;
	}

	/**
	 * Get all the custom post types in the kit.
	 *
	 * @return array Custom post types names.
	 */
	private function get_default_settings_custom_post_types() {
		if ( empty( $this->manifest['custom-post-type-title'] ) ) {
			return [];
		}

		$manifest_post_types = array_keys( $this->manifest['custom-post-type-title'] );

		return array_diff( $manifest_post_types, Utils::get_builtin_wp_post_types() );
	}

	/**
	 * Get the default settings of elementor templates conditions to override.
	 *
	 * @return array
	 */
	private function get_default_settings_conflicts() {
		if ( empty( $this->manifest['templates'] ) ) {
			return [];
		}

		return apply_filters( 'elementor/import/get_default_settings_conflicts', [], $this->manifest['templates'] );
	}

	/**
	 * Get the default settings of elementor templates conditions to override.
	 *
	 * @return array
	 */
	private function get_default_settings_override_conditions() {
		if ( empty( $this->settings_conflicts ) ) {
			return [];
		}

		return array_keys( $this->settings_conflicts );
	}

	/**
	 * Get the default settings of the plugins that should be imported.
	 *
	 * @return array
	 */
	private function get_default_settings_plugins() {
		return ! empty( $this->manifest['plugins'] ) ? $this->manifest['plugins'] : [];
	}

	/**
	 * Get the default settings of which content types should be imported.
	 *
	 * @return array
	 */
	private function get_default_settings_include() {
		return [ 'templates', 'plugins', 'content', 'settings' ];
	}

	/**
	 * Get the data that requires updating/replacement when imported.
	 *
	 * @return array{post_ids: array, term_ids: array}
	 */
	private function get_imported_data_replacements(): array {
		return [
			'post_ids' => Utils::map_old_new_post_ids( $this->imported_data ),
			'term_ids' => Utils::map_old_new_term_ids( $this->imported_data ),
		];
	}

	/**
	 * Save the prevented elements on elementor post creation elements.
	 * Handle the replacement of all the dynamic content of the elements that probably have been changed during the import.
	 */
	private function save_elements_of_imported_posts() {
		$imported_data_replacements = $this->get_imported_data_replacements();

		foreach ( $this->documents_data as $new_id => $data ) {
			$document = Plugin::$instance->documents->get( $new_id );

			if ( isset( $data['elements'] ) ) {
				$data['elements'] = $document->on_import_update_dynamic_content( $data['elements'], $imported_data_replacements );
			}

			if ( isset( $data['settings'] ) ) {

				if ( $document instanceof Kit ) {
					// Without post_status certain tabs in the Kit will not save properly.
					$data['settings']['post_status'] = get_post_status( $new_id );
				}

				$data['settings'] = $document->on_import_update_settings( $data['settings'], $imported_data_replacements );
			}

			$document->save( $data );
		}
	}

	private function update_instance_data_in_import_session_option() {
		$import_sessions = Utils::get_import_sessions();

		$import_sessions[ $this->session_id ]['instance_data']['documents_data'] = $this->documents_data;
		$import_sessions[ $this->session_id ]['instance_data']['imported_data'] = $this->imported_data;
		$import_sessions[ $this->session_id ]['instance_data']['runners_import_metadata'] = $this->runners_import_metadata;

		update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
	}

	public function finalize_import_session_option() {
		$import_sessions = Utils::get_import_sessions();

		if ( ! isset( $import_sessions[ $this->session_id ] ) ) {
			return;
		}

		unset( $import_sessions[ $this->session_id ]['instance_data'] );

		$import_sessions[ $this->session_id ]['end_timestamp'] = current_time( 'timestamp' );
		$import_sessions[ $this->session_id ]['runners'] = $this->runners_import_metadata;

		update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
	}

	/**
	 * Filter the php error args and return 408 status code if the error is a timeout.
	 *
	 * @param array $args
	 * @param array $error
	 * @return array
	 */
	private function filter_php_error_args( $args, $error ) {
		if ( strpos( $error['message'], 'Maximum execution time' ) !== false ) {
			$args['response'] = 408;
		}

		return $args;
	}
}
PK     8\g      4  modules/import-export/compatibility/base-adapter.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Compatibility;

use Elementor\App\Modules\ImportExport\Import;
use Elementor\Core\Base\Base_Object;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

abstract class Base_Adapter {

	/**
	 * @param array $manifest_data
	 * @param array $meta
	 * @return false
	 */
	public static function is_compatibility_needed( array $manifest_data, array $meta ) {
		return false;
	}

	public function adapt_manifest( array $manifest_data ) {
		return $manifest_data;
	}

	public function adapt_site_settings( array $site_settings, array $manifest_data, $path ) {
		return $site_settings;
	}

	public function adapt_template( array $template_data, array $template_settings ) {
		return $template_data;
	}
}
PK     8\    3  modules/import-export/compatibility/kit-library.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Compatibility;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Kit_Library extends Base_Adapter {
	public static function is_compatibility_needed( array $manifest_data, array $meta ) {
		return ! empty( $meta['referrer'] ) && 'kit-library' === $meta['referrer'];
	}

	public function adapt_manifest( array $manifest_data ) {
		if ( ! empty( $manifest_data['content']['page'] ) ) {
			foreach ( $manifest_data['content']['page'] as & $page ) {
				$page['thumbnail'] = false;
			}
		}

		if ( ! empty( $manifest_data['templates'] ) ) {
			foreach ( $manifest_data['templates'] as & $template ) {
				$template['thumbnail'] = false;
			}
		}

		return $manifest_data;
	}
}
PK     8\;>_    .  modules/import-export/compatibility/envato.phpnu [        <?php

namespace Elementor\App\Modules\ImportExport\Compatibility;

use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Plugin;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Envato extends Base_Adapter {
	public static function is_compatibility_needed( array $manifest_data, array $meta ) {
		return ! empty( $manifest_data['manifest_version'] );
	}

	public function adapt_manifest( array $manifest_data ) {
		$templates = $manifest_data['templates'];

		$manifest_data['templates'] = [];

		foreach ( $templates as $template ) {
			// Envato store their global kit styles as a 'global.json' template file.
			// We need to be able to know the path to this specific 'global.json' since it functions as the site-settings.json
			$is_global = ! empty( $template['metadata']['template_type'] ) && 'global-styles' === $template['metadata']['template_type'];
			if ( $is_global ) {
				// Adding the path of the 'global.json' template to the manifest which will be used in the future.
				$manifest_data['path-to-envto-site-settings'] = $template['source'];

				// Getting the site-settings because Envato stores them in one of the posts.
				$kit = Plugin::$instance->kits_manager->get_active_kit();
				$kit_tabs = $kit->get_tabs();
				unset( $kit_tabs['settings-site-identity'] );
				$manifest_data['site-settings'] = array_keys( $kit_tabs );

				continue;
			}

			// Evanto uses "type" instead of "doc_type"
			$template['doc_type'] = $template['type'];

			// Evanto uses for "name" instead of "title"
			$template['title'] = $template['name'];

			// Envato specifying an exact path to the template rather than using its "ID" as an index.
			// This extracts the "file name" part out of our exact source list and we treat that as an ID.
			$file_name_without_extension = str_replace( '.json', '', basename( $template['source'] ) );

			// Append the template to the global list:
			$manifest_data['templates'][ $file_name_without_extension ] = $template;
		}

		$manifest_data['name'] = $manifest_data['title'];

		return $manifest_data;
	}

	public function adapt_site_settings( array $site_settings, array $manifest_data, $path ) {
		if ( empty( $manifest_data['path-to-envto-site-settings'] ) ) {
			return $site_settings;
		}

		$global_file_path = $path . $manifest_data['path-to-envto-site-settings'];
		$global_file_data = ImportExportUtils::read_json_file( $global_file_path );

		return [
			'settings' => $global_file_data['page_settings'],
		];
	}

	public function adapt_template( array $template_data, array $template_settings ) {
		if ( ! empty( $template_data['metadata']['elementor_pro_conditions'] ) ) {
			foreach ( $template_data['metadata']['elementor_pro_conditions'] as $condition ) {
				list ( $type, $name, $sub_name, $sub_id ) = array_pad( explode( '/', $condition ), 4, '' );

				$template_data['import_settings']['conditions'][] = compact( 'type', 'name', 'sub_name', 'sub_id' );
			}
		}

		return $template_data;
	}
}
PK     8\@ٓ    J  modules/kit-library/admin-menu-items/editor-one-website-templates-menu.phpnu [        <?php

namespace Elementor\App\Modules\KitLibrary\AdminMenuItems;

use Elementor\Core\Admin\EditorOneMenu\Interfaces\Menu_Item_Interface;
use Elementor\Modules\EditorOne\Classes\Menu_Config;
use Elementor\Plugin;

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

class Editor_One_Website_Templates_Menu implements Menu_Item_Interface {

	public function get_capability(): string {
		return 'manage_options';
	}

	public function get_parent_slug(): string {
		return Menu_Config::ELEMENTOR_MENU_SLUG;
	}

	public function is_visible(): bool {
		return true;
	}

	public function get_label(): string {
		return esc_html__( 'Website Templates', 'elementor' );
	}

	public function get_position(): int {
		return 30;
	}

	public function get_slug(): string {
		$app = Plugin::$instance->app;

		if ( $app ) {
			$return_to = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) );
			return add_query_arg(
				[
					'return_to' => $return_to,
					'source' => 'wp_db_templates_menu',
				],
				$app->get_base_url()
			) . '#/kit-library';
		}

		return 'elementor-app#/kit-library';
	}

	public function get_group_id(): string {
		return Menu_Config::TEMPLATES_GROUP_ID;
	}
}
PK     8\vŧ      modules/kit-library/module.phpnu [        <?php
namespace Elementor\App\Modules\KitLibrary;

use Elementor\App\Modules\KitLibrary\Data\Repository;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\App\Modules\KitLibrary\Connect\Kit_Library;
use Elementor\Core\Common\Modules\Connect\Module as ConnectModule;
use Elementor\App\Modules\KitLibrary\Data\Kits\Controller as Kits_Controller;
use Elementor\App\Modules\KitLibrary\Data\Taxonomies\Controller as Taxonomies_Controller;
use Elementor\Core\Utils\Promotions\Filtered_Promotions_Manager;
use Elementor\Utils as ElementorUtils;
use Elementor\Modules\EditorOne\Classes\Menu_Data_Provider;
use Elementor\App\Modules\KitLibrary\AdminMenuItems\Editor_One_Website_Templates_Menu;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Module extends BaseModule {
	/**
	 * Get name.
	 *
	 * @access public
	 *
	 * @return string
	 */
	public function get_name() {
		return 'kit-library';
	}

	private function register_editor_one_menu( Menu_Data_Provider $menu_data_provider ) {
		$menu_data_provider->register_menu( new Editor_One_Website_Templates_Menu() );
	}

	private function set_kit_library_settings() {
		if ( ! Plugin::$instance->common ) {
			return;
		}

		/** @var ConnectModule $connect */
		$connect = Plugin::$instance->common->get_component( 'connect' );

		/** @var Kit_Library $kit_library */
		$kit_library = $connect->get_app( 'kit-library' );

		Plugin::$instance->app->set_settings( 'kit-library', [
			'has_access_to_module' => current_user_can( 'manage_options' ),
			'subscription_plans' => $this->apply_filter_subscription_plans( $connect->get_subscription_plans( 'kit-library' ) ),
			'is_pro' => false,
			'is_library_connected' => $kit_library->is_connected(),
			'library_connect_url'  => $kit_library->get_admin_url( 'authorize', [
				'utm_source' => 'kit-library',
				'utm_medium' => 'wp-dash',
				'utm_campaign' => 'library-connect',
				'utm_term' => '%%page%%', // Will be replaced in the frontend.
			] ),
			'access_level' => ConnectModule::ACCESS_LEVEL_CORE,
			'access_tier' => ConnectModule::ACCESS_TIER_FREE,
			'plan_type' => ConnectModule::ACCESS_TIER_FREE,
			'app_url' => Plugin::$instance->app->get_base_url() . '#/' . $this->get_name(),
			'urls' => [
				'createNewPage' => Plugin::$instance->documents->get_create_new_post_url(),
			],
		] );
	}

	private function apply_filter_subscription_plans( array $subscription_plans ): array {
		foreach ( $subscription_plans as $key => $plan ) {
			if ( null === $plan['promotion_url'] ) {
				continue;
			}

			$subscription_plans[ $key ] = Filtered_Promotions_Manager::get_filtered_promotion_data(
				$plan,
				'elementor/kit_library/' . $key . '/promotion',
				'promotion_url'
			);
		}

		return $subscription_plans;
	}

	/**
	 * Module constructor.
	 */
	public function __construct() {
		Plugin::$instance->data_manager_v2->register_controller( new Kits_Controller() );
		Plugin::$instance->data_manager_v2->register_controller( new Taxonomies_Controller() );

		$this->register_actions();

		do_action( 'elementor/kit_library/registered', $this );
	}

	public function register_actions() {
		// Assigning this action here since the repository is being loaded by demand.
		add_action( 'elementor/experiments/feature-state-change/container', [ Repository::class, 'clear_cache' ], 10, 0 );

		add_action( 'elementor/editor-one/menu/register', function ( Menu_Data_Provider $menu_data_provider ) {
			$this->register_editor_one_menu( $menu_data_provider );
		} );

		add_filter( 'elementor/editor-one/menu/protected_templates_submenu_slugs', function ( array $protected_slugs ): array {
			$protected_slugs[] = 'edit-tags.php?taxonomy=elementor_library_category&amp;post_type=elementor_library';
			return $protected_slugs;
		} );

		add_action( 'elementor/connect/apps/register', function ( ConnectModule $connect_module ) {
			$connect_module->register_app( 'kit-library', Kit_Library::get_class_name() );
		} );

		add_action( 'elementor/init', function () {
			$this->set_kit_library_settings();
		}, 12 /** After the initiation of the connect kit library */ );

		add_action( 'template_redirect', [ $this, 'handle_kit_screenshot_generation' ] );
	}

	public function handle_kit_screenshot_generation() {
		$is_kit_preview = ElementorUtils::get_super_global_value( $_GET, 'kit_thumbnail' );
		$nonce = ElementorUtils::get_super_global_value( $_GET, 'nonce' );

		if ( $is_kit_preview ) {
			if ( ! wp_verify_nonce( $nonce, 'kit_thumbnail' ) ) {
				wp_die( esc_html__( 'Not Authorized', 'elementor' ), esc_html__( 'Error', 'elementor' ), 403 );
			}

			$suffix = ( ElementorUtils::is_script_debug() || ElementorUtils::is_elementor_tests() ) ? '' : '.min';

			show_admin_bar( false );

			wp_enqueue_script(
				'dom-to-image',
				ELEMENTOR_ASSETS_URL . "/lib/dom-to-image/js/dom-to-image{$suffix}.js",
				[],
				'2.6.0',
				true
			);

			wp_enqueue_script(
				'html2canvas',
				ELEMENTOR_ASSETS_URL . "/lib/html2canvas/js/html2canvas{$suffix}.js",
				[],
				'1.4.1',
				true
			);

			wp_enqueue_script(
				'cloud-library-screenshot',
				ELEMENTOR_ASSETS_URL . "/js/cloud-library-screenshot{$suffix}.js",
				[ 'dom-to-image', 'html2canvas', 'elementor-common', 'elementor-common-modules' ],
				ELEMENTOR_VERSION,
				true
			);

			$config = [
				'home_url' => home_url(),
				'kit_id' => uniqid(),
				'selector' => 'body',
			];

			wp_add_inline_script( 'cloud-library-screenshot', 'var ElementorScreenshotConfig = ' . wp_json_encode( $config ) . ';' );
		}
	}
}
PK     8\F$  F$  '  modules/kit-library/data/repository.phpnu [        <?php
namespace Elementor\App\Modules\KitLibrary\Data;

use Elementor\Core\Common\Modules\Connect\Module as ConnectModule;
use Elementor\Core\Utils\Collection;
use Elementor\Data\V2\Base\Exceptions\Error_404;
use Elementor\Data\V2\Base\Exceptions\WP_Error_Exception;
use Elementor\Modules\Library\User_Favorites;
use Elementor\App\Modules\KitLibrary\Connect\Kit_Library;
use Elementor\Plugin;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Repository {
	/**
	 * There is no label for subscription plan with access_level=0 + it should not
	 * be translated.
	 */
	const SUBSCRIPTION_PLAN_FREE_TAG = 'Free';

	const TAXONOMIES_KEYS = [ 'tags', 'categories', 'main_category', 'third_category', 'features', 'types' ];

	const KITS_CACHE_KEY = 'elementor_remote_kits';
	const KITS_TAXONOMIES_CACHE_KEY = 'elementor_remote_kits_taxonomies';

	const KITS_CACHE_TTL_HOURS = 12;
	const KITS_TAXONOMIES_CACHE_TTL_HOURS = 12;

	/**
	 * @var Kit_Library
	 */
	protected $api;

	/**
	 * @var User_Favorites
	 */
	protected $user_favorites;

	/**
	 * @var Collection
	 */
	protected $subscription_plans;

	/**
	 * Get all kits.
	 *
	 * @param false $force_api_request
	 *
	 * @return Collection
	 */
	public function get_all( $force_api_request = false ) {
		return $this->get_kits_data( $force_api_request )
			->map( function ( $kit ) {
				return $this->transform_kit_api_response( $kit );
			} );
	}

	/**
	 * Get specific kit.
	 *
	 * @param       $id
	 * @param array $options
	 *
	 * @return array|null
	 *
	 * @throws WP_Error_Exception If kit is not found.
	 */
	public function find( $id, $options = [] ) {
		$options = wp_parse_args( $options, [
			'manifest_included' => true,
		] );

		$item = $this->get_kits_data()
			->find( function ( $kit ) use ( $id ) {
				return $kit->_id === $id;
			} );

		if ( ! $item ) {
			return null;
		}

		$manifest = null;

		if ( $options['manifest_included'] ) {
			$manifest = $this->api->get_manifest( $id );

			if ( is_wp_error( $manifest ) ) {
				throw new WP_Error_Exception( esc_html( $manifest ) );
			}
		}

		return $this->transform_kit_api_response( $item, $manifest );
	}

	/**
	 * @param false $force_api_request
	 *
	 * @return Collection
	 */
	public function get_taxonomies( $force_api_request = false ) {
		return $this->get_taxonomies_data( $force_api_request )
			->only( static::TAXONOMIES_KEYS )
			->reduce( function ( Collection $carry, $taxonomies, $type ) {
				return $carry->merge( array_map( function ( $taxonomy ) use ( $type ) {
					return [
						'text' => $taxonomy->name,
						'type' => $type,
					];
				}, $taxonomies ) );
			}, new Collection( [] ) )
			->merge(
				$this->subscription_plans->map( function ( $label ) {
					return [
						'text' => $label ? $label : self::SUBSCRIPTION_PLAN_FREE_TAG,
						'type' => 'subscription_plans',
					];
				} )
			)
			->unique( [ 'text', 'type' ] );
	}

	/**
	 * @param $id
	 *
	 * @return array
	 *
	 * @throws WP_Error_Exception If download link retrieval fails or API errors occur.
	 */
	public function get_download_link( $id ) {
		$response = $this->api->download_link( $id );

		if ( is_wp_error( $response ) ) {
			throw new WP_Error_Exception( esc_html( $response ) );
		}

		return [ 'download_link' => $response->download_link ];
	}

	/**
	 * @param $id
	 *
	 * @return array
	 *
	 * @throws Error_404 If kit is not found.
	 */
	public function add_to_favorites( $id ) {
		$kit = $this->find( $id, [ 'manifest_included' => false ] );

		if ( ! $kit ) {
			throw new Error_404( esc_html__( 'Kit not found', 'elementor' ), 'kit_not_found' );
		}

		$this->user_favorites->add( 'elementor', 'kits', $kit['id'] );

		$kit['is_favorite'] = true;

		return $kit;
	}

	/**
	 * @param $id
	 *
	 * @return array
	 *
	 * @throws Error_404 If kit is not found.
	 */
	public function remove_from_favorites( $id ) {
		$kit = $this->find( $id, [ 'manifest_included' => false ] );

		if ( ! $kit ) {
			throw new Error_404( esc_html__( 'Kit not found', 'elementor' ), 'kit_not_found' );
		}

		$this->user_favorites->remove( 'elementor', 'kits', $kit['id'] );

		$kit['is_favorite'] = false;

		return $kit;
	}

	/**
	 * @param bool $force_api_request
	 *
	 * @return Collection
	 *
	 * @throws WP_Error_Exception If kits data retrieval fails.
	 */
	private function get_kits_data( $force_api_request = false ) {
		$data = get_transient( static::KITS_CACHE_KEY );

		$experiments_manager = Plugin::$instance->experiments;
		$kits_editor_layout_type = $experiments_manager->is_feature_active( 'container' ) ? 'container_flexbox' : '';

		if ( ! $data || $force_api_request ) {
			$args = [
				'body' => [
					'editor_layout_type' => $kits_editor_layout_type,
				],
			];

			/**
			 * Filters arguments for the request to the Kits API.
			 *
			 * @since 3.11.0
			 *
			 * @param array[] $args Array of http arguments.
			 */
			$args = apply_filters( 'elementor/kit-library/get-kits-data/args', $args );

			$data = $this->api->get_all( $args );

			if ( is_wp_error( $data ) ) {
				throw new WP_Error_Exception( esc_html( $data ) );
			}

			set_transient( static::KITS_CACHE_KEY, $data, static::KITS_CACHE_TTL_HOURS * HOUR_IN_SECONDS );
		}

		return new Collection( $data );
	}

	/**
	 * @param bool $force_api_request
	 *
	 * @return Collection
	 *
	 * @throws WP_Error_Exception If taxonomies data retrieval fails.
	 */
	private function get_taxonomies_data( $force_api_request = false ) {
		$data = get_transient( static::KITS_TAXONOMIES_CACHE_KEY );

		if ( ! $data || $force_api_request ) {
			$data = $this->api->get_taxonomies();

			if ( is_wp_error( $data ) ) {
				throw new WP_Error_Exception( esc_html( $data ) );
			}

			set_transient( static::KITS_TAXONOMIES_CACHE_KEY, $data, static::KITS_TAXONOMIES_CACHE_TTL_HOURS * HOUR_IN_SECONDS );
		}

		return new Collection( (array) $data );
	}

	/**
	 * @param      $kit
	 * @param null $manifest
	 *
	 * @return array
	 */
	private function transform_kit_api_response( $kit, $manifest = null ) {
		// BC: Support legacy APIs that don't have access tiers.
		if ( isset( $kit->access_tier ) ) {
			$access_tier = $kit->access_tier;
		} else {
			$access_tier = 0 === $kit->access_level
				? ConnectModule::ACCESS_TIER_FREE
				: ConnectModule::ACCESS_TIER_ESSENTIAL;
		}

		$subscription_plan_tag = $this->subscription_plans->get( $access_tier );

		$taxonomies = ( new Collection( ( (array) $kit )['taxonomies'] ) )
			->filter( function ( $taxonomy ) {
				return in_array( $taxonomy->type, self::TAXONOMIES_KEYS );
			} )
			->flatten()
			->pluck( 'name' )
			->push( $subscription_plan_tag ? $subscription_plan_tag : self::SUBSCRIPTION_PLAN_FREE_TAG );

		return array_merge(
			[
				'id' => $kit->_id,
				'title' => $kit->title,
				'thumbnail_url' => $kit->thumbnail,
				'access_level' => $kit->access_level,
				'access_tier' => $access_tier,
				'keywords' => $kit->keywords,
				'taxonomies' => $taxonomies->values(),
				'is_favorite' => $this->user_favorites->exists( 'elementor', 'kits', $kit->_id ),
				// TODO: Remove all the isset when the API stable.
				'trend_index' => isset( $kit->trend_index ) ? $kit->trend_index : 0,
				'featured_index' => isset( $kit->featured_index ) ? $kit->featured_index : 0,
				'popularity_index' => isset( $kit->popularity_index ) ? $kit->popularity_index : 0,
				'created_at' => isset( $kit->created_at ) ? $kit->created_at : null,
				'updated_at' => isset( $kit->updated_at ) ? $kit->updated_at : null,
			],
			$manifest ? $this->transform_manifest_api_response( $manifest ) : []
		);
	}

	/**
	 * @param $manifest
	 *
	 * @return array
	 */
	private function transform_manifest_api_response( $manifest ) {
		$manifest_content = ( new Collection( (array) $manifest->content ) )
			->reduce( function ( $carry, $content, $type ) {
				$mapped_documents = array_map( function ( $document ) use ( $type ) {
					// TODO: Fix it!
					// Hack to override a bug when a document with type of 'wp-page' is declared as 'wp-post'.
					if ( 'page' === $type ) {
						$document->doc_type = 'wp-page';
					}

					return $document;
				}, (array) $content );

				return $carry + $mapped_documents;
			}, [] );

		$content = ( new Collection( (array) $manifest->templates ) )
			->union( $manifest_content )
			->map( function ( $manifest_item, $key ) {
				return [
					'id' => isset( $manifest_item->id ) ? $manifest_item->id : $key,
					'title' => $manifest_item->title,
					'doc_type' => $manifest_item->doc_type,
					'thumbnail_url' => $manifest_item->thumbnail,
					'preview_url' => isset( $manifest_item->url ) ? $manifest_item->url : null,
				];
			} );

		return [
			'description' => $manifest->description,
			'preview_url' => isset( $manifest->site ) ? $manifest->site : '',
			'documents' => $content->values(),
		];
	}

	/**
	 * @param Kit_Library    $kit_library
	 * @param User_Favorites $user_favorites
	 * @param Collection     $subscription_plans
	 */
	public function __construct( Kit_Library $kit_library, User_Favorites $user_favorites, Collection $subscription_plans ) {
		$this->api = $kit_library;
		$this->user_favorites = $user_favorites;
		$this->subscription_plans = $subscription_plans;
	}

	public static function clear_cache() {
		delete_transient( static::KITS_CACHE_KEY );
	}
}
PK     8\]y    ,  modules/kit-library/data/base-controller.phpnu [        <?php
namespace Elementor\App\Modules\KitLibrary\Data;

use Elementor\Plugin;
use Elementor\Data\V2\Base\Controller;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\Library\User_Favorites;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

abstract class Base_Controller extends Controller {
	/**
	 * @var Repository
	 */
	private $repository;

	/**
	 * @return Repository
	 */
	public function get_repository() {
		if ( ! $this->repository ) {
			/** @var \Elementor\Core\Common\Modules\Connect\Module $connect */
			$connect = Plugin::$instance->common->get_component( 'connect' );

			$subscription_plans = ( new Collection( $connect->get_subscription_plans() ) )
				->map( function ( $value ) {
					return $value['label'];
				} );

			$this->repository = new Repository(
				$connect->get_app( 'kit-library' ),
				new User_Favorites( get_current_user_id() ),
				$subscription_plans
			);
		}

		return $this->repository;
	}
}
PK     8\PhQ,  ,  2  modules/kit-library/data/taxonomies/controller.phpnu [        <?php
namespace Elementor\App\Modules\KitLibrary\Data\Taxonomies;

use Elementor\App\Modules\KitLibrary\Data\Base_Controller;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Controller extends Base_Controller {

	public function get_name() {
		return 'kit-taxonomies';
	}

	public function get_collection_params() {
		return [
			'force' => [
				'description' => 'Force an API request and skip the cache.',
				'required' => false,
				'default' => false,
				'type' => 'boolean',
			],
		];
	}

	public function get_items( $request ) {
		$data = $this->get_repository()->get_taxonomies( $request->get_param( 'force' ) );

		return [
			'data' => $data->values(),
		];
	}

	public function get_permission_callback( $request ) {
		return current_user_can( 'manage_options' );
	}
}
PK     8\]&[  [  9  modules/kit-library/data/kits/endpoints/download-link.phpnu [        <?php
namespace Elementor\App\Modules\KitLibrary\Data\Kits\Endpoints;

use Elementor\Data\V2\Base\Endpoint;
use Elementor\App\Modules\KitLibrary\Data\Kits\Controller;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * @property Controller $controller
 */
class Download_Link extends Endpoint {
	public function get_name() {
		return 'download-link';
	}

	public function get_format() {
		return 'kits/download-link/{id}';
	}

	protected function register() {
		$this->register_item_route( \WP_REST_Server::READABLE, [
			'id_arg_type_regex' => '[\w]+',
		] );
	}

	public function get_item( $id, $request ) {
		$repository = $this->controller->get_repository();
		$data = $repository->get_download_link( $id );

		return [
			'data' => $data,
			'meta' => [
				'nonce' => wp_create_nonce( 'kit-library-import' ),
			],
		];
	}
}
PK     8\5t    5  modules/kit-library/data/kits/endpoints/favorites.phpnu [        <?php
namespace Elementor\App\Modules\KitLibrary\Data\Kits\Endpoints;

use Elementor\App\Modules\KitLibrary\Data\Kits\Controller;
use Elementor\Data\V2\Base\Endpoint;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * @property Controller $controller
 */
class Favorites extends Endpoint {
	public function get_name() {
		return 'favorites';
	}

	public function get_format() {
		return 'kits/favorites/{id}';
	}

	protected function register() {
		$args = [
			'id_arg_type_regex' => '[\w]+',
		];

		$this->register_item_route( \WP_REST_Server::CREATABLE, $args );
		$this->register_item_route( \WP_REST_Server::DELETABLE, $args );
	}

	public function create_item( $id, $request ) {
		$repository = $this->controller->get_repository();
		$kit = $repository->add_to_favorites( $id );

		return [
			'data' => $kit,
		];
	}

	public function delete_item( $id, $request ) {
		$repository = $this->controller->get_repository();

		$kit = $repository->remove_from_favorites( $id );

		return [
			'data' => $kit,
		];
	}
}
PK     8\dA$    ,  modules/kit-library/data/kits/controller.phpnu [        <?php
namespace Elementor\App\Modules\KitLibrary\Data\Kits;

use Elementor\App\Modules\KitLibrary\Data\Base_Controller;
use Elementor\Data\V2\Base\Exceptions\Error_404;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Controller extends Base_Controller {

	public function get_name() {
		return 'kits';
	}

	public function get_items( $request ) {
		$data = $this->get_repository()->get_all( $request->get_param( 'force' ) );

		return [
			'data' => $data->values(),
		];
	}

	public function get_item( $request ) {
		$data = $this->get_repository()->find( $request->get_param( 'id' ) );

		if ( ! $data ) {
			return new Error_404( esc_html__( 'Kit not exists.', 'elementor' ), 'kit_not_exists' );
		}

		return [
			'data' => $data,
		];
	}

	public function get_collection_params() {
		return [
			'force' => [
				'description' => 'Force an API request and skip the cache.',
				'required' => false,
				'default' => false,
				'type' => 'boolean',
			],
		];
	}

	public function register_endpoints() {
		$this->index_endpoint->register_item_route( \WP_REST_Server::READABLE, [
			'id' => [
				'description' => 'Unique identifier for the object.',
				'type' => 'string',
				'required' => true,
			],
			'id_arg_type_regex' => '[\w]+',
		] );

		$this->register_endpoint( new Endpoints\Download_Link( $this ) );
		$this->register_endpoint( new Endpoints\Favorites( $this ) );
	}

	public function get_permission_callback( $request ) {
		return current_user_can( 'manage_options' );
	}
}
PK     8\2    +  modules/kit-library/connect/kit-library.phpnu [        <?php
namespace Elementor\App\Modules\KitLibrary\Connect;

use Elementor\Core\Common\Modules\Connect\Apps\Base_App;
use Elementor\Core\Common\Modules\Connect\Apps\Library;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Kit_Library extends Library {
	const DEFAULT_BASE_ENDPOINT = 'https://my.elementor.com/api/v1/kits-library';
	const FALLBACK_BASE_ENDPOINT = 'https://ms-8874.elementor.com/api/v1/kits-library';

	public function get_title() {
		return esc_html__( 'Kit Library', 'elementor' );
	}

	public function get_all( $args = [] ) {
		return $this->http_request( 'GET', 'kits/plugin-version/' . ELEMENTOR_VERSION, $args );
	}

	public function get_by_id( $id ) {
		return $this->http_request( 'GET', 'kits/' . $id );
	}

	public function get_taxonomies() {
		return $this->http_request( 'GET', 'taxonomies' );
	}

	public function get_manifest( $id ) {
		return $this->http_request( 'GET', "kits/{$id}/manifest" );
	}

	public function download_link( $id ) {
		return $this->http_request( 'GET', "kits/{$id}/download-link" );
	}

	protected function get_api_url() {
		return [
			static::DEFAULT_BASE_ENDPOINT,
			static::FALLBACK_BASE_ENDPOINT,
		];
	}

	/**
	 * Get all the connect information
	 *
	 * @return array
	 */
	protected function get_connect_info() {
		$connect_info = $this->get_base_connect_info();

		$additional_info = [];

		// BC Support.
		$old_kit_library = new \Elementor\Core\App\Modules\KitLibrary\Connect\Kit_Library();

		/**
		 * Additional connect info.
		 *
		 * Filters the connection information when connecting to Elementor servers.
		 * This hook can be used to add more information or add more data.
		 *
		 * @param array    $additional_info Additional connecting information array.
		 * @param Base_App $this            The base app instance.
		 */
		$additional_info = apply_filters( 'elementor/connect/additional-connect-info', $additional_info, $old_kit_library );

		return array_merge( $connect_info, $additional_info );
	}

	protected function init() {
		// Remove parent init actions.
	}
}
PK     8\쐪    5  modules/onboarding/storage/entities/user-progress.phpnu [        <?php

namespace Elementor\App\Modules\Onboarding\Storage\Entities;

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

class User_Progress {

	private int $current_step_index = 0;
	private ?string $current_step_id = null;
	private array $completed_steps = [];
	private ?string $exit_type = null;
	private ?int $last_active_timestamp = null;
	private ?int $started_at = null;
	private bool $starter_dismissed = false;

	public static function from_array( array $data ): self {
		$instance = new self();

		$instance->current_step_index = $data['current_step_index'] ?? $data['current_step'] ?? 0;
		$instance->current_step_id = $data['current_step_id'] ?? null;
		$instance->completed_steps = $data['completed_steps'] ?? [];
		$instance->exit_type = $data['exit_type'] ?? null;
		$instance->last_active_timestamp = $data['last_active_timestamp'] ?? null;
		$instance->started_at = $data['started_at'] ?? null;
		$instance->starter_dismissed = ! empty( $data['starter_dismissed'] );

		return $instance;
	}

	public function to_array(): array {
		return [
			'current_step' => $this->current_step_index,
			'current_step_index' => $this->current_step_index,
			'current_step_id' => $this->current_step_id,
			'completed_steps' => $this->completed_steps,
			'exit_type' => $this->exit_type,
			'last_active_timestamp' => $this->last_active_timestamp,
			'started_at' => $this->started_at,
			'starter_dismissed' => $this->starter_dismissed,
		];
	}

	public function get_current_step(): int {
		return $this->current_step_index;
	}

	public function get_current_step_index(): int {
		return $this->current_step_index;
	}

	public function set_current_step_index( int $index ): void {
		$this->current_step_index = $index;
	}

	public function get_current_step_id(): ?string {
		return $this->current_step_id;
	}

	public function set_current_step_id( ?string $step_id ): void {
		$this->current_step_id = $step_id;
	}

	public function set_current_step( int $step, ?string $step_id = null ): void {
		$this->current_step_index = $step;

		if ( null !== $step_id ) {
			$this->current_step_id = $step_id;
		}
	}

	public function get_completed_steps(): array {
		return $this->completed_steps;
	}

	public function set_completed_steps( array $steps ): void {
		$this->completed_steps = $steps;
	}

	public function add_completed_step( $step ): void {
		if ( ! in_array( $step, $this->completed_steps, true ) ) {
			$this->completed_steps[] = $step;
		}
	}

	public function is_step_completed( $step ): bool {
		return in_array( $step, $this->completed_steps, true );
	}

	public function get_exit_type(): ?string {
		return $this->exit_type;
	}

	public function set_exit_type( ?string $type ): void {
		$this->exit_type = $type;
	}

	public function get_last_active_timestamp(): ?int {
		return $this->last_active_timestamp;
	}

	public function set_last_active_timestamp( ?int $timestamp ): void {
		$this->last_active_timestamp = $timestamp;
	}

	public function get_started_at(): ?int {
		return $this->started_at;
	}

	public function set_started_at( ?int $timestamp ): void {
		$this->started_at = $timestamp;
	}

	public function is_starter_dismissed(): bool {
		return $this->starter_dismissed;
	}

	public function set_starter_dismissed( bool $dismissed ): void {
		$this->starter_dismissed = $dismissed;
	}

	public function had_unexpected_exit( bool $is_completed ): bool {
		return null === $this->exit_type
			&& $this->current_step_index > 0
			&& ! $is_completed;
	}
}
PK     8\zR+    4  modules/onboarding/storage/entities/user-choices.phpnu [        <?php

namespace Elementor\App\Modules\Onboarding\Storage\Entities;

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

class User_Choices {

	private ?string $building_for = null;
	private array $site_about = [];
	private ?string $experience_level = null;
	private ?string $theme_selection = null;
	private array $site_features = [];

	public static function from_array( array $data ): self {
		$instance = new self();

		$instance->building_for = $data['building_for'] ?? null;
		$instance->site_about = $data['site_about'] ?? [];
		$instance->experience_level = $data['experience_level'] ?? null;
		$instance->theme_selection = $data['theme_selection'] ?? null;
		$instance->site_features = $data['site_features'] ?? [];

		return $instance;
	}

	public function to_array(): array {
		return [
			'building_for' => $this->building_for,
			'site_about' => $this->site_about,
			'experience_level' => $this->experience_level,
			'theme_selection' => $this->theme_selection,
			'site_features' => $this->site_features,
		];
	}

	public function get_building_for(): ?string {
		return $this->building_for;
	}

	public function set_building_for( ?string $value ): void {
		$this->building_for = $value;
	}

	public function get_site_about(): array {
		return $this->site_about;
	}

	public function set_site_about( array $value ): void {
		$this->site_about = $value;
	}

	public function get_experience_level(): ?string {
		return $this->experience_level;
	}

	public function set_experience_level( ?string $value ): void {
		$this->experience_level = $value;
	}

	public function get_theme_selection(): ?string {
		return $this->theme_selection;
	}

	public function set_theme_selection( ?string $value ): void {
		$this->theme_selection = $value;
	}

	public function get_site_features(): array {
		return $this->site_features;
	}

	public function set_site_features( array $value ): void {
		$this->site_features = $value;
	}
}
PK     8\6v<l  l  :  modules/onboarding/storage/onboarding-progress-manager.phpnu [        <?php

namespace Elementor\App\Modules\Onboarding\Storage;

use Elementor\App\Modules\Onboarding\Module;
use Elementor\App\Modules\Onboarding\Storage\Entities\User_Choices;
use Elementor\App\Modules\Onboarding\Storage\Entities\User_Progress;

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

class Onboarding_Progress_Manager {

	const PROGRESS_OPTION_KEY = 'elementor_onboarding_progress';
	const CHOICES_OPTION_KEY = 'elementor_onboarding_choices';
	const DEFAULT_TOTAL_STEPS = 5;

	private static ?Onboarding_Progress_Manager $instance = null;

	public static function instance(): Onboarding_Progress_Manager {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	public function get_progress(): User_Progress {
		$data = get_option( self::PROGRESS_OPTION_KEY, [] );

		return User_Progress::from_array( $data );
	}

	public function save_progress( User_Progress $progress ): User_Progress {
		update_option( self::PROGRESS_OPTION_KEY, $progress->to_array() );

		return $progress;
	}

	public function update_progress( array $params ): User_Progress {
		$progress = $this->get_progress();

		if ( isset( $params['current_step'] ) ) {
			$progress->set_current_step( (int) $params['current_step'] );
		}

		if ( isset( $params['completed_steps'] ) ) {
			$progress->set_completed_steps( (array) $params['completed_steps'] );
		}

		if ( isset( $params['exit_type'] ) ) {
			$progress->set_exit_type( $params['exit_type'] );
		}

		if ( isset( $params['complete_step'] ) ) {
			$step = $params['complete_step'];
			$progress->add_completed_step( $step );

			$step_index = $params['step_index'] ?? $progress->get_current_step_index();
			$total_steps = $params['total_steps'] ?? self::DEFAULT_TOTAL_STEPS;
			$next_index = $step_index + 1;

			if ( $next_index < $total_steps ) {
				$progress->set_current_step_index( $next_index );
				$progress->set_current_step_id( null );
			}
		}

		if ( ! empty( $params['skip_step'] ) ) {
			$step_index = $params['step_index'] ?? $progress->get_current_step_index();
			$total_steps = $params['total_steps'] ?? self::DEFAULT_TOTAL_STEPS;
			$next_index = $step_index + 1;

			if ( $next_index < $total_steps ) {
				$progress->set_current_step_index( $next_index );
				$progress->set_current_step_id( null );
			}
		}

		if ( isset( $params['start'] ) && $params['start'] ) {
			$progress->set_started_at( current_time( 'timestamp' ) );
			$progress->set_exit_type( null );
		}

		if ( isset( $params['complete'] ) && $params['complete'] ) {
			$progress->set_exit_type( 'user_exit' );
			update_option( Module::ONBOARDING_OPTION, Module::VERSION );
		}

		if ( isset( $params['user_exit'] ) && $params['user_exit'] ) {
			$progress->set_exit_type( 'user_exit' );
		}

		if ( isset( $params['starter_dismissed'] ) && $params['starter_dismissed'] ) {
			$progress->set_starter_dismissed( true );
		}

		$progress->set_last_active_timestamp( current_time( 'timestamp' ) );

		return $this->save_progress( $progress );
	}

	public function get_choices(): User_Choices {
		$data = get_option( self::CHOICES_OPTION_KEY, [] );

		return User_Choices::from_array( $data );
	}

	public function save_choices( User_Choices $choices ): User_Choices {
		update_option( self::CHOICES_OPTION_KEY, $choices->to_array() );

		return $choices;
	}

	public function update_choices( array $params ): User_Choices {
		$choices = $this->get_choices();

		if ( isset( $params['building_for'] ) ) {
			$choices->set_building_for( $params['building_for'] );
		}

		if ( isset( $params['site_about'] ) ) {
			$choices->set_site_about( (array) $params['site_about'] );
		}

		if ( isset( $params['experience_level'] ) ) {
			$choices->set_experience_level( $params['experience_level'] );
		}

		if ( isset( $params['theme_selection'] ) ) {
			$choices->set_theme_selection( $params['theme_selection'] );
		}

		if ( isset( $params['site_features'] ) ) {
			$choices->set_site_features( (array) $params['site_features'] );
		}

		return $this->save_choices( $choices );
	}

	public function reset(): void {
		delete_option( self::PROGRESS_OPTION_KEY );
		delete_option( self::CHOICES_OPTION_KEY );
	}

	private function __construct() {}
}
PK     8\lP)  )    modules/onboarding/module.phpnu [        <?php

namespace Elementor\App\Modules\Onboarding;

use Elementor\App\Modules\Onboarding\Data\Controller;
use Elementor\App\Modules\Onboarding\Data\Endpoints\Install_Theme;
use Elementor\App\Modules\Onboarding\Storage\Entities\User_Choices;
use Elementor\App\Modules\Onboarding\Storage\Entities\User_Progress;
use Elementor\App\Modules\Onboarding\Storage\Onboarding_Progress_Manager;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Settings\Manager as SettingsManager;
use Elementor\Includes\EditorAssetsAPI;
use Elementor\Plugin;
use Elementor\Utils;

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

class Module extends BaseModule {

	const VERSION = '2.0.0';
	const ASSETS_BASE_URL = 'https://assets.elementor.com/onboarding/v1/strings/';
	const ONBOARDING_OPTION = 'elementor_onboarded';

	const SUPPORTED_LOCALES = [
		'de_DE' => 'de',
		'es_ES' => 'es',
		'fr_FR' => 'fr',
		'he_IL' => 'he',
		'id_ID' => 'id',
		'it_IT' => 'it',
		'nl_NL' => 'nl',
		'pl_PL' => 'pl',
		'pt_BR' => 'pt',
		'tr_TR' => 'tr',
	];

	private Onboarding_Progress_Manager $progress_manager;

	public function get_name(): string {
		return 'onboarding';
	}

	public static function has_user_finished_onboarding(): bool {
		return (bool) get_option( self::ONBOARDING_OPTION );
	}

	public function __construct() {
		$this->progress_manager = Onboarding_Progress_Manager::instance();

		Plugin::instance()->data_manager_v2->register_controller( new Controller() );

		add_action( 'elementor/init', [ $this, 'on_elementor_init' ], 12 );

		if ( $this->should_show_starter() ) {
			add_filter( 'elementor/editor/localize_settings', [ $this, 'add_starter_settings' ] );
			add_filter( 'elementor/editor/v2/packages', [ $this, 'add_starter_packages' ] );
			add_action( 'elementor/editor/v2/styles/enqueue', [ $this, 'enqueue_fonts' ] );
			add_action( 'elementor/preview/enqueue_styles', [ $this, 'enqueue_starter_preview_css' ] );
		}
	}

	public function on_elementor_init(): void {
		if ( ! Plugin::instance()->app->is_current() ) {
			return;
		}

		$this->set_onboarding_settings();
		$this->enqueue_fonts();
	}

	public function enqueue_fonts(): void {
		wp_enqueue_style(
			'elementor-onboarding-fonts',
			'https://fonts.googleapis.com/css2?family=Poppins:wght@500&display=swap',
			[],
			ELEMENTOR_VERSION
		);
	}

	public function enqueue_starter_preview_css(): void {
		$css = '
			#site-header,
			.page-header { display: var(--e-starter-header-display, none); }
		';

		wp_register_style( 'elementor-starter-preview', false );
		wp_enqueue_style( 'elementor-starter-preview' );
		wp_add_inline_style( 'elementor-starter-preview', $css );
	}

	public function progress_manager(): Onboarding_Progress_Manager {
		return $this->progress_manager;
	}

	private function set_onboarding_settings(): void {
		if ( ! Plugin::instance()->common ) {
			return;
		}

		$progress = $this->progress_manager->get_progress();
		$choices = $this->progress_manager->get_choices();
		$steps = $this->get_steps_config();

		// If the user previously selected a theme but it's no longer the active theme,
		// clear the theme selection so the user can re-select.
		$this->maybe_invalidate_theme_selection( $progress, $choices );

		$is_connected = $this->is_user_connected();

		Plugin::$instance->app->set_settings( 'onboarding', [
			'version' => self::VERSION,
			'restUrl' => rest_url( 'elementor/v1/onboarding/' ),
			'nonce' => wp_create_nonce( 'wp_rest' ),
			'progress' => $this->validate_progress_for_steps( $progress, $steps ),
			'choices' => $choices->to_array(),
			'hadUnexpectedExit' => $progress->had_unexpected_exit( self::has_user_finished_onboarding() ),
			'isConnected' => $is_connected,
			'userName' => $this->get_user_display_name(),
			'steps' => $steps,
			'uiTheme' => $this->get_ui_theme_preference(),
			'translations' => $this->get_translated_strings(),
			'shouldShowProInstallScreen' => $is_connected ? $this->should_show_pro_install_screen() : false,
			'urls' => [
				'dashboard' => admin_url(),
				'editor' => admin_url( 'edit.php?post_type=elementor_library' ),
				'connect' => $this->get_connect_url(),
				'signUp' => $this->get_connect_url( 'signup' ),
				'comparePlans' => 'https://go.elementor.com/go-pro-onboarding-editor-features-step-upgrade/',
				'createNewPage' => Plugin::$instance->documents->get_create_new_post_url(),
				'upgradeUrl' => 'https://go.elementor.com/go-pro-onboarding-editor-header-upgrade/',
			],
		] );
	}

	private function validate_progress_for_steps( User_Progress $progress, array $steps ): array {
		$progress_data = $progress->to_array();
		$step_count = count( $steps );
		$current_step_index = $progress->get_current_step_index() ?? 0;
		$current_step_id = $progress->get_current_step_id() ?? $steps[0]['id'] ?? 'building_for';

		$is_invalid_step_index = $current_step_index < 0 || $current_step_index >= $step_count;

		if ( $is_invalid_step_index ) {
			$current_step_id = $steps[0]['id'];
			$current_step_index = 0;
		}

		$progress_data['current_step_id'] = $current_step_id;
		$progress_data['current_step_index'] = $current_step_index;

		return $progress_data;
	}

	private function is_user_connected(): bool {
		$library = $this->get_library_app();

		return $library ? $library->is_connected() : false;
	}

	private function get_connect_url( string $screen_hint = '' ): string {
		$library = $this->get_library_app();

		if ( ! $library ) {
			return '';
		}

		return $library->get_admin_url( 'authorize', [
			'utm_source' => 'onboarding-wizard',
			'utm_campaign' => 'connect-account',
			'utm_medium' => 'wp-dash',
			'utm_term' => self::VERSION,
			'source' => 'generic',
			'screen_hint' => $screen_hint,
		] ) ?? '';
	}

	private function get_library_app() {
		$connect = Plugin::instance()->common->get_component( 'connect' );

		if ( ! $connect ) {
			return null;
		}

		return $connect->get_app( 'library' );
	}

	public static function should_show_pro_install_screen(): bool {
		if ( self::is_elementor_pro_installed() ) {
			return false;
		}

		$connect = Plugin::$instance->common->get_component( 'connect' );

		if ( ! $connect ) {
			return false;
		}

		$pro_install_app = $connect->get_app( 'pro-install' );

		if ( ! $pro_install_app || ! $pro_install_app->is_connected() ) {
			return false;
		}

		$download_link = $pro_install_app->get_download_link();

		return ! empty( $download_link );
	}

	private function get_ui_theme_preference(): string {
		$editor_preferences = SettingsManager::get_settings_managers( 'editorPreferences' );

		$ui_theme = $editor_preferences->get_model()->get_settings( 'ui_theme' );

		return $ui_theme ? $ui_theme : 'auto';
	}

	private function get_user_display_name(): string {
		$library = $this->get_library_app();

		if ( ! $library || ! $library->is_connected() ) {
			return '';
		}

		$user = $library->get( 'user' );

		return $user->first_name ?? '';
	}

	public function should_show_starter(): bool {
		$progress = $this->progress_manager->get_progress();

		return self::VERSION === get_option( self::ONBOARDING_OPTION ) && ! $progress->is_starter_dismissed();
	}

	public function add_starter_packages( array $packages ): array {
		$packages[] = 'editor-starter';

		return $packages;
	}

	public function add_starter_settings( array $settings ): array {
		$settings['starter'] = [
			'restPath' => 'elementor/v1/onboarding/user-progress',
			'aiPlannerUrl' => 'https://planner.elementor.com/home.html',
			'kitLibraryUrl' => Plugin::$instance->app->get_base_url() . '#/kit-library',
		];

		return $settings;
	}

	private function maybe_invalidate_theme_selection( User_Progress $progress, User_Choices $choices ): void {
		$selected_theme = $choices->get_theme_selection();

		if ( empty( $selected_theme ) ) {
			return;
		}

		$active_theme = get_stylesheet();

		if ( $active_theme !== $selected_theme ) {
			$completed = $this->filter_out_theme_selection_step( $progress->get_completed_steps() );
			$progress->set_completed_steps( $completed );
			$this->progress_manager->save_progress( $progress );

			$choices->set_theme_selection( null );
			$this->progress_manager->save_choices( $choices );
		}
	}

	private function filter_out_theme_selection_step( array $steps ): array {
		return array_values( array_filter( $steps, function ( $step ) {
			return 'theme_selection' !== $step;
		} ) );
	}

	private function get_translated_strings(): array {
		$locale = $this->get_onboarding_locale();

		$api = new EditorAssetsAPI( [
			EditorAssetsAPI::ASSETS_DATA_URL => self::ASSETS_BASE_URL . $locale . '.json',
			EditorAssetsAPI::ASSETS_DATA_TRANSIENT_KEY => '_elementor_onboarding_strings_' . $locale,
			EditorAssetsAPI::ASSETS_DATA_KEY => 'translations',
		] );

		return $api->get_assets_data();
	}

	private function get_onboarding_locale(): string {
		static $flipped_locales = null;

		if ( null === $flipped_locales ) {
			$flipped_locales = array_flip( self::SUPPORTED_LOCALES );
		}

		$user_locale = get_user_locale();

		if ( isset( self::SUPPORTED_LOCALES[ $user_locale ] ) ) {
			return $user_locale;
		}

		$locale = substr( $user_locale, 0, 2 );

		if ( isset( $flipped_locales[ $locale ] ) ) {
			return $flipped_locales[ $locale ];
		}

		return 'en';
	}

	private function get_steps_config(): array {
		$steps = [
			[
				'id' => 'building_for',
				'label' => __( 'Who are you building for?', 'elementor' ),
				'type' => 'single',
			],
			[
				'id' => 'site_about',
				'label' => __( 'What is your site about?', 'elementor' ),
				'type' => 'multiple',
			],
			[
				'id' => 'experience_level',
				'label' => __( 'How much experience do you have with Elementor?', 'elementor' ),
				'type' => 'single',
			],
		];

		if ( ! $this->is_elementor_theme_active() ) {
			$steps[] = [
				'id' => 'theme_selection',
				'label' => __( 'Start with a theme that fits your needs', 'elementor' ),
				'type' => 'single',
			];
		}

		if ( ! self::is_elementor_pro_installed() ) {
			$steps[] = [
				'id' => 'site_features',
				'label' => __( 'What do you want to include in your site?', 'elementor' ),
				'type' => 'multiple',
			];
		}

		return apply_filters( 'elementor/onboarding/steps', $steps );
	}

	private static function is_elementor_pro_installed(): bool {
		$is_pro_installed = Utils::has_pro() || Utils::is_pro_installed_and_not_active();
		return (bool) apply_filters( 'elementor/onboarding/is_elementor_pro_installed', $is_pro_installed );
	}

	private function is_elementor_theme_active(): bool {
		$active_theme = get_stylesheet();
		$is_active = in_array( $active_theme, Install_Theme::ALLOWED_THEMES, true );

		return (bool) apply_filters( 'elementor/onboarding/is_elementor_theme_active', $is_active );
	}
}
PK     8\=+    3  modules/onboarding/data/endpoints/user-progress.phpnu [        <?php

namespace Elementor\App\Modules\Onboarding\Data\Endpoints;

use Elementor\App\Modules\Onboarding\Module;
use Elementor\App\Modules\Onboarding\Storage\Onboarding_Progress_Manager;
use Elementor\App\Modules\Onboarding\Validation\User_Progress_Validator;
use Elementor\Data\V2\Base\Endpoint as Endpoint_Base;
use WP_REST_Server;

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

class User_Progress extends Endpoint_Base {

	public function get_name(): string {
		return 'user-progress';
	}

	public function get_format(): string {
		return 'onboarding';
	}

	protected function register(): void {
		parent::register();

		$this->register_items_route( WP_REST_Server::EDITABLE );
	}

	public function get_items( $request ) {
		$permission = $this->check_permission();
		if ( is_wp_error( $permission ) ) {
			return $permission;
		}

		$manager = Onboarding_Progress_Manager::instance();
		$progress = $manager->get_progress();

		return [
			'data' => $progress->to_array(),
			'meta' => [
				'had_unexpected_exit' => $progress->had_unexpected_exit( Module::has_user_finished_onboarding() ),
			],
		];
	}

	public function update_items( $request ) {
		$permission = $this->check_permission();
		if ( is_wp_error( $permission ) ) {
			return $permission;
		}

		$params = $request->get_json_params();

		$validator = new User_Progress_Validator();
		$validated = $validator->validate( $params ?? [] );

		if ( is_wp_error( $validated ) ) {
			return $validated;
		}

		$manager = Onboarding_Progress_Manager::instance();
		$progress = $manager->update_progress( $validated );

		return [
			'data' => 'success',
			'progress' => $progress->to_array(),
		];
	}

	private function check_permission() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return new \WP_Error(
				'rest_forbidden',
				__( 'Sorry, you are not allowed to access onboarding data.', 'elementor' ),
				[ 'status' => 403 ]
			);
		}
		return true;
	}
}
PK     8\xO~    2  modules/onboarding/data/endpoints/user-choices.phpnu [        <?php

namespace Elementor\App\Modules\Onboarding\Data\Endpoints;

use Elementor\App\Modules\Onboarding\Storage\Onboarding_Progress_Manager;
use Elementor\App\Modules\Onboarding\Validation\User_Choices_Validator;
use Elementor\Data\V2\Base\Endpoint as Endpoint_Base;
use WP_REST_Server;

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

class User_Choices extends Endpoint_Base {

	public function get_name(): string {
		return 'user-choices';
	}

	public function get_format(): string {
		return 'onboarding';
	}

	protected function register(): void {
		parent::register();

		$this->register_items_route( WP_REST_Server::EDITABLE );
	}

	public function get_items( $request ) {
		$permission = $this->check_permission();
		if ( is_wp_error( $permission ) ) {
			return $permission;
		}

		$manager = Onboarding_Progress_Manager::instance();
		$choices = $manager->get_choices();

		return [
			'data' => $choices->to_array(),
		];
	}

	public function update_items( $request ) {
		$permission = $this->check_permission();
		if ( is_wp_error( $permission ) ) {
			return $permission;
		}

		$params = $request->get_json_params();

		$validator = new User_Choices_Validator();
		$validated = $validator->validate( $params ?? [] );

		if ( is_wp_error( $validated ) ) {
			return $validated;
		}

		$manager = Onboarding_Progress_Manager::instance();
		$choices = $manager->update_choices( $validated );

		return [
			'data' => 'success',
			'choices' => $choices->to_array(),
		];
	}

	private function check_permission() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return new \WP_Error(
				'rest_forbidden',
				__( 'Sorry, you are not allowed to access onboarding data.', 'elementor' ),
				[ 'status' => 403 ]
			);
		}
		return true;
	}
}
PK     8\2B~	  	  3  modules/onboarding/data/endpoints/install-theme.phpnu [        <?php

namespace Elementor\App\Modules\Onboarding\Data\Endpoints;

use Elementor\Data\V2\Base\Endpoint as Endpoint_Base;
use WP_REST_Server;

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

class Install_Theme extends Endpoint_Base {

	const ALLOWED_THEMES = [ 'hello-elementor', 'hello-biz' ];

	public function get_name(): string {
		return 'install-theme';
	}

	public function get_format(): string {
		return 'onboarding';
	}

	protected function register(): void {
		parent::register();

		$this->register_items_route( WP_REST_Server::CREATABLE );
	}

	public function create_items( $request ) {
		$permission = $this->check_permission();
		if ( is_wp_error( $permission ) ) {
			return $permission;
		}

		$params = $request->get_json_params();
		$theme_slug = $params['theme_slug'] ?? '';

		if ( empty( $theme_slug ) || ! in_array( $theme_slug, self::ALLOWED_THEMES, true ) ) {
			return new \WP_Error(
				'invalid_theme',
				__( 'Invalid or unsupported theme.', 'elementor' ),
				[ 'status' => 400 ]
			);
		}

		if ( ! current_user_can( 'install_themes' ) || ! current_user_can( 'switch_themes' ) ) {
			return new \WP_Error(
				'insufficient_permissions',
				__( 'You do not have permission to install themes.', 'elementor' ),
				[ 'status' => 403 ]
			);
		}

		$theme = wp_get_theme( $theme_slug );

		if ( ! $theme->exists() ) {
			if ( ! function_exists( 'request_filesystem_credentials' ) ) {
				require_once ABSPATH . 'wp-admin/includes/file.php';
			}

			if ( ! class_exists( '\Theme_Upgrader' ) ) {
				require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
			}

			if ( ! class_exists( '\WP_Ajax_Upgrader_Skin' ) ) {
				require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
			}

			$skin = new \WP_Ajax_Upgrader_Skin();
			$upgrader = new \Theme_Upgrader( $skin );
			$result = $upgrader->install( "https://downloads.wordpress.org/theme/{$theme_slug}.latest-stable.zip" );

			if ( is_wp_error( $result ) || ! $result ) {
				return new \WP_Error(
					'theme_install_failed',
					__( 'Failed to install the theme.', 'elementor' ),
					[ 'status' => 500 ]
				);
			}
		}

		switch_theme( $theme_slug );

		return [
			'data' => [
				'success' => true,
				'message' => 'theme_installed',
			],
		];
	}

	private function check_permission() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return new \WP_Error(
				'rest_forbidden',
				__( 'Sorry, you are not allowed to access onboarding data.', 'elementor' ),
				[ 'status' => 403 ]
			);
		}
		return true;
	}
}
PK     8\*F /    1  modules/onboarding/data/endpoints/install-pro.phpnu [        <?php

namespace Elementor\App\Modules\Onboarding\Data\Endpoints;

use Elementor\Data\V2\Base\Endpoint as Endpoint_Base;
use Elementor\Modules\ProInstall\Plugin_Installer;
use Elementor\Plugin;
use Elementor\Utils;
use WP_REST_Server;

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

class Install_Pro extends Endpoint_Base {

	public function get_name(): string {
		return 'install-pro';
	}

	public function get_format(): string {
		return 'onboarding';
	}

	protected function register(): void {
		parent::register();

		$this->register_items_route( WP_REST_Server::CREATABLE );
	}

	public function create_items( $request ) {
		if ( Utils::has_pro() || Utils::is_pro_installed_and_not_active() ) {
			return [
				'data' => [
					'success' => true,
					'message' => 'already_installed',
				],
			];
		}

		$connect = Plugin::$instance->common->get_component( 'connect' );

		if ( ! $connect ) {
			return new \WP_Error(
				'connect_unavailable',
				__( 'Connect module is not available.', 'elementor' ),
				[ 'status' => 500 ]
			);
		}

		$pro_install_app = $connect->get_app( 'pro-install' );

		if ( ! $pro_install_app || ! $pro_install_app->is_connected() ) {
			return new \WP_Error(
				'not_connected',
				__( 'You must be connected to install Elementor Pro.', 'elementor' ),
				[ 'status' => 400 ]
			);
		}

		$download_link = $pro_install_app->get_download_link();

		if ( empty( $download_link ) ) {
			return new \WP_Error(
				'no_subscription',
				__( 'There are no available subscriptions at the moment.', 'elementor' ),
				[ 'status' => 400 ]
			);
		}

		$plugin_installer = new Plugin_Installer( 'elementor-pro', $download_link );
		$result = $plugin_installer->install();

		if ( is_wp_error( $result ) ) {
			return new \WP_Error(
				'install_failed',
				$result->get_error_message(),
				[ 'status' => 500 ]
			);
		}

		return [
			'data' => [
				'success' => true,
				'message' => 'installed',
			],
		];
	}
}
PK     8\/]    8  modules/onboarding/data/endpoints/pro-install-screen.phpnu [        <?php

namespace Elementor\App\Modules\Onboarding\Data\Endpoints;

use Elementor\App\Modules\Onboarding\Module;
use Elementor\Data\V2\Base\Endpoint as Endpoint_Base;

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

class Pro_Install_Screen extends Endpoint_Base {

	public function get_name(): string {
		return 'pro-install-screen';
	}

	public function get_format(): string {
		return 'onboarding';
	}

	protected function register(): void {
		parent::register();

		$this->register_items_route();
	}

	public function get_items( $request ) {
		return [
			'data' => [
				'shouldShowProInstallScreen' => Module::should_show_pro_install_screen(),
			],
		];
	}
}
PK     8\F/P  P  &  modules/onboarding/data/controller.phpnu [        <?php

namespace Elementor\App\Modules\Onboarding\Data;

use Elementor\App\Modules\Onboarding\Data\Endpoints\Install_Pro;
use Elementor\App\Modules\Onboarding\Data\Endpoints\Install_Theme;
use Elementor\App\Modules\Onboarding\Data\Endpoints\Pro_Install_Screen;
use Elementor\App\Modules\Onboarding\Data\Endpoints\User_Choices;
use Elementor\App\Modules\Onboarding\Data\Endpoints\User_Progress;
use Elementor\Data\V2\Base\Controller as Base_Controller;

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

class Controller extends Base_Controller {

	public function get_name(): string {
		return 'onboarding';
	}

	public function register_endpoints(): void {
		$this->register_endpoint( new User_Progress( $this ) );
		$this->register_endpoint( new User_Choices( $this ) );
		$this->register_endpoint( new Pro_Install_Screen( $this ) );
		$this->register_endpoint( new Install_Pro( $this ) );
		$this->register_endpoint( new Install_Theme( $this ) );
	}

	public function get_items_permissions_check( $request ) {
		return current_user_can( 'manage_options' );
	}

	public function get_item_permissions_check( $request ) {
		return current_user_can( 'manage_options' );
	}

	public function create_items_permissions_check( $request ) {
		return current_user_can( 'manage_options' );
	}

	public function create_item_permissions_check( $request ) {
		return current_user_can( 'manage_options' );
	}

	public function update_items_permissions_check( $request ) {
		return current_user_can( 'manage_options' );
	}

	public function update_item_permissions_check( $request ) {
		return current_user_can( 'manage_options' );
	}
}
PK     8\E  E  8  modules/onboarding/validation/user-choices-validator.phpnu [        <?php

namespace Elementor\App\Modules\Onboarding\Validation;

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

class User_Choices_Validator extends Base_Validator {

	protected function get_rules(): array {
		return [
			'building_for' => [
				'type' => 'string',
				'nullable' => true,
			],

			'site_about' => [
				'type' => 'string_array',
			],

			'experience_level' => [
				'type' => 'string',
				'nullable' => true,
			],

			'theme_selection' => [
				'type' => 'string',
				'nullable' => true,
			],

			'site_features' => [
				'type' => 'string_array',
			],
		];
	}
}
PK     8\[a?S	  S	  9  modules/onboarding/validation/user-progress-validator.phpnu [        <?php

namespace Elementor\App\Modules\Onboarding\Validation;

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

class User_Progress_Validator extends Base_Validator {

	private const ALLOWED_EXIT_TYPES = [ 'user_exit', 'unexpected', null, '' ];

	protected function get_rules(): array {
		return [
			'current_step' => [
				'type' => 'int',
			],
			'completed_steps' => [
				'type' => 'mixed_array',
			],
			'exit_type' => [
				'type' => 'exit_type',
				'nullable' => true,
			],
			'complete_step' => [
				'type' => 'string_or_int',
			],
			'skip_step' => [
				'type' => 'bool',
			],
			'step_index' => [
				'type' => 'int',
			],
			'total_steps' => [
				'type' => 'int',
			],
			'start' => [
				'type' => 'bool',
			],
			'complete' => [
				'type' => 'bool',
			],
			'user_exit' => [
				'type' => 'bool',
			],
			'starter_dismissed' => [
				'type' => 'bool',
			],
		];
	}

	protected function validate_field( string $field, $value, array $rule ) {
		$type = $rule['type'] ?? 'string';

		switch ( $type ) {
			case 'exit_type':
				return $this->validate_exit_type( $value );
			case 'string_or_int':
				return $this->validate_string_or_int( $field, $value );
			case 'mixed_array':
				return $this->validate_mixed_array( $field, $value );
			default:
				return parent::validate_field( $field, $value, $rule );
		}
	}

	private function validate_exit_type( $value ) {
		if ( ! in_array( $value, self::ALLOWED_EXIT_TYPES, true ) ) {
			return $this->error( 'exit_type', 'Exit type is invalid.' );
		}

		return '' === $value ? null : $value;
	}

	private function validate_string_or_int( string $field, $value ) {
		if ( is_numeric( $value ) ) {
			return (int) $value;
		}

		if ( is_string( $value ) ) {
			return sanitize_text_field( $value );
		}

		return $this->error( $field, "{$field} must be a number or string." );
	}

	private function validate_mixed_array( string $field, $value ) {
		if ( ! is_array( $value ) ) {
			return $this->error( $field, "{$field} must be an array." );
		}

		return array_values(
			array_filter(
				array_map(
					static function ( $item ) {
						if ( is_numeric( $item ) ) {
							return (int) $item;
						}

						if ( is_string( $item ) ) {
							return sanitize_text_field( $item );
						}

						return null;
					},
					$value
				),
				static function ( $item ) {
					return null !== $item;
				}
			)
		);
	}
}
PK     8\SI$E  E  0  modules/onboarding/validation/base-validator.phpnu [        <?php

namespace Elementor\App\Modules\Onboarding\Validation;

use WP_Error;

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

abstract class Base_Validator {

	protected array $errors = [];

	abstract protected function get_rules(): array;

	public function validate( array $params ) {
		if ( ! is_array( $params ) ) {
			return new WP_Error( 'invalid_params', 'Parameters must be an array.', [ 'status' => 400 ] );
		}

		$this->errors = [];
		$validated = [];

		foreach ( $this->get_rules() as $field => $rule ) {
			if ( ! array_key_exists( $field, $params ) ) {
				continue;
			}

			$result = $this->validate_field( $field, $params[ $field ], $rule );

			if ( is_wp_error( $result ) ) {
				return $result;
			}

			$validated[ $field ] = $result;
		}

		return $validated;
	}

	protected function validate_field( string $field, $value, array $rule ) {
		$type = $rule['type'] ?? 'string';
		$nullable = $rule['nullable'] ?? false;

		if ( null === $value ) {
			if ( $nullable ) {
				return null;
			}

			return $this->error( $field, "{$field} cannot be null." );
		}

		switch ( $type ) {
			case 'string':
				return $this->validate_string( $field, $value );
			case 'int':
				return $this->validate_int( $field, $value );
			case 'bool':
				return $this->validate_bool( $field, $value );
			case 'array':
				return $this->validate_array( $field, $value, $rule );
			case 'string_array':
				return $this->validate_string_array( $field, $value );
			case 'custom_data':
				return $this->validate_custom_data( $field, $value );
			default:
				return $value;
		}
	}

	protected function validate_string( string $field, $value ) {
		if ( ! is_string( $value ) ) {
			return $this->error( $field, "{$field} must be a string." );
		}

		return sanitize_text_field( $value );
	}

	protected function validate_int( string $field, $value ) {
		if ( ! is_numeric( $value ) ) {
			return $this->error( $field, "{$field} must be a number." );
		}

		return (int) $value;
	}

	protected function validate_bool( string $field, $value ) {
		if ( ! is_bool( $value ) ) {
			return $this->error( $field, "{$field} must be a boolean." );
		}

		return $value;
	}

	protected function validate_array( string $field, $value, array $rule ) {
		if ( ! is_array( $value ) ) {
			return $this->error( $field, "{$field} must be an array." );
		}

		$allowed = $rule['allowed'] ?? null;

		if ( $allowed && ! in_array( $value, $allowed, true ) ) {
			return $this->error( $field, "{$field} contains invalid value." );
		}

		return $value;
	}

	protected function validate_string_array( string $field, $value ) {
		if ( ! is_array( $value ) ) {
			return $this->error( $field, "{$field} must be an array." );
		}

		return array_values(
			array_filter(
				array_map(
					static function ( $item ) {
						return is_string( $item ) ? sanitize_text_field( $item ) : null;
					},
					$value
				),
				static function ( $item ) {
					return null !== $item;
				}
			)
		);
	}

	protected function validate_custom_data( string $field, $value ) {
		if ( ! is_array( $value ) ) {
			return $this->error( $field, "{$field} must be an array." );
		}

		return $this->sanitize_recursive( $value );
	}

	protected function sanitize_recursive( array $data ): array {
		$sanitized = [];

		foreach ( $data as $key => $value ) {
			$safe_key = sanitize_key( $key );

			if ( is_string( $value ) ) {
				$sanitized[ $safe_key ] = sanitize_text_field( $value );
			} elseif ( is_array( $value ) ) {
				$sanitized[ $safe_key ] = $this->sanitize_recursive( $value );
			} elseif ( is_numeric( $value ) || is_bool( $value ) || null === $value ) {
				$sanitized[ $safe_key ] = $value;
			} else {
				$sanitized[ $safe_key ] = null;
			}
		}

		return $sanitized;
	}

	protected function error( string $field, string $message ): WP_Error {
		return new WP_Error(
			'invalid_' . $field,
			$message,
			[ 'status' => 400 ]
		);
	}
}
PK     8\]$  $    app.phpnu [        <?php
namespace Elementor\App;

use Elementor\App\AdminMenuItems\Theme_Builder_Menu_Item;
use Elementor\Core\Admin\Menu\Admin_Menu_Manager;
use Elementor\Core\Experiments\Manager as ExperimentsManager;
use Elementor\Modules\WebCli\Module as WebCLIModule;
use Elementor\Core\Base\App as BaseApp;
use Elementor\Core\Settings\Manager as SettingsManager;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\User;
use Elementor\Utils;
use Elementor\Core\Utils\Promotions\Filtered_Promotions_Manager;
use Elementor\Core\Utils\Assets_Config_Provider;
use Elementor\Core\Utils\Collection;
use Elementor\Core\Utils\Assets_Translation_Loader;
use Elementor\Modules\EditorOne\Classes\Menu_Data_Provider;
use Elementor\App\AdminMenuItems\Editor_One_Theme_Builder_Menu;

use Elementor\App\Modules\ImportExport\Module as ImportExportModule;
use Elementor\App\Modules\KitLibrary\Module as KitLibraryModule;
use Elementor\App\Modules\ImportExportCustomization\Module as ImportExportCustomizationModule;
use Elementor\App\Modules\SiteEditor\Module as SiteEditorModule;
use Elementor\App\Modules\Onboarding\Module as OnboardingModule;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class App extends BaseApp {

	const PAGE_ID = 'elementor-app';

	/**
	 * Get module name.
	 *
	 * Retrieve the module name.
	 *
	 * @since 3.0.0
	 * @access public
	 *
	 * @return string Module name.
	 */
	public function get_name() {
		return 'app';
	}

	public function get_base_url() {
		return admin_url( 'admin.php?page=' . self::PAGE_ID . '&ver=' . ELEMENTOR_VERSION );
	}

	private function register_editor_one_menu( Menu_Data_Provider $menu_data_provider ) {
		$menu_data_provider->register_menu( new Editor_One_Theme_Builder_Menu() );
	}

	public function fix_submenu( $menu ) {
		global $submenu;

		if ( is_multisite() && is_network_admin() ) {
			return $menu;
		}

		// Non admin role / custom wp menu.
		if ( empty( $submenu[ Source_Local::ADMIN_MENU_SLUG ] ) ) {
			return $menu;
		}

		// Hack to add a link to sub menu.
		foreach ( $submenu[ Source_Local::ADMIN_MENU_SLUG ] as &$item ) {
			if ( self::PAGE_ID === $item[2] ) {
				$item[2] = $this->get_settings( 'menu_url' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
				$item[4] = 'elementor-app-link'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
			}
		}

		return $menu;
	}

	public function is_current() {
		return ( ! empty( $_GET['page'] ) && self::PAGE_ID === $_GET['page'] );
	}

	public function admin_init() {
		do_action( 'elementor/app/init', $this );

		// Add the introduction and user settings only when it is needed (when loading the app and not in the editor or admin pages)
		$this->set_settings( 'user', [
			'introduction' => (object) User::get_introduction_meta(),
			'is_administrator' => current_user_can( 'manage_options' ),
			'restrictions' => Plugin::$instance->role_manager->get_user_restrictions_array(),
		] );

		$this->enqueue_assets();

		remove_action( 'wp_print_styles', 'print_emoji_styles' );

		// Setup default heartbeat options
		// TODO: Enable heartbeat.
		add_filter( 'heartbeat_settings', function( $settings ) {
			$settings['interval'] = 15;
			return $settings;
		} );

		$this->render();
		die;
	}

	protected function get_init_settings() {
		$referer = wp_get_referer();

		return [
			'menu_url' => $this->get_base_url() . '#site-editor/promotion',
			'assets_url' => ELEMENTOR_ASSETS_URL,
			'pages_url' => admin_url( 'edit.php?post_type=page' ),
			'return_url' => $referer ? $referer : admin_url(),
			'hasPro' => Utils::has_pro(),
			'admin_url' => admin_url(),
			'login_url' => wp_login_url(),
			'base_url' => $this->get_base_url(),
			'home_url' => home_url(),
			'promotion' => Filtered_Promotions_Manager::get_filtered_promotion_data(
				[ 'upgrade_url' => 'https://go.elementor.com/go-pro-theme-builder/' ],
				'elementor/site-editor/promotion',
				'upgrade_url'
			),
		];
	}

	private function render() {
		require __DIR__ . '/view.php';
	}

	/**
	 * Get Elementor editor theme color preference.
	 *
	 * Retrieve the user theme color preference as defined by editor preferences manager.
	 *
	 * @since 3.0.0
	 * @access private
	 *
	 * @return string Preferred editor theme.
	 */
	private function get_elementor_ui_theme_preference() {
		$editor_preferences = SettingsManager::get_settings_managers( 'editorPreferences' );

		return $editor_preferences->get_model()->get_settings( 'ui_theme' );
	}

	/**
	 * Enqueue dark theme detection script.
	 *
	 * Enqueues an inline script that detects user-agent settings for dark mode and adds a complimentary class to the body tag.
	 *
	 * @since 3.0.0
	 * @access private
	 */
	private function enqueue_dark_theme_detection_script() {
		if ( 'auto' === $this->get_elementor_ui_theme_preference() ) {
			wp_add_inline_script( 'elementor-app',
				'if ( window.matchMedia && window.matchMedia( `(prefers-color-scheme: dark)` ).matches )
							{ document.body.classList.add( `eps-theme-dark` ); }' );
		}
	}

	private function register_packages() {
		$assets_config_provider = ( new Assets_Config_Provider() )
			->set_path_resolver( function ( $name ) {
				return ELEMENTOR_ASSETS_PATH . "js/packages/{$name}/{$name}.asset.php";
			} );

		Collection::make( [ 'ui', 'icons', 'store', 'query', 'utils', 'events', 'onboarding' ] )
			->each( function( $package ) use ( $assets_config_provider ) {
				$suffix = Utils::is_script_debug() ? '' : '.min';
				$config = $assets_config_provider->load( $package )->get( $package );

				if ( ! $config ) {
					return;
				}

				wp_register_script(
					$config['handle'],
					ELEMENTOR_ASSETS_URL . "js/packages/{$package}/{$package}{$suffix}.js",
					$config['deps'],
					ELEMENTOR_VERSION,
					true
				);
			} );
	}

	private function enqueue_assets() {
		Plugin::$instance->init_common();

		$this->register_packages();

		/** @var WebCLIModule $web_cli */
		$web_cli = Plugin::$instance->modules_manager->get_modules( 'web-cli' );
		$web_cli->register_scripts();

		Plugin::$instance->common->register_scripts();

		wp_register_style(
			'select2',
			$this->get_css_assets_url( 'e-select2', 'assets/lib/e-select2/css/' ),
			[],
			'4.0.6-rc.1'
		);

		Plugin::$instance->common->register_styles();

		wp_register_style(
			'select2',
			ELEMENTOR_ASSETS_URL . 'lib/e-select2/css/e-select2.css',
			[],
			'4.0.6-rc.1'
		);

		wp_enqueue_style(
			'elementor-app',
			$this->get_css_assets_url( 'app', null, 'default', true ),
			[
				'select2',
				'elementor-icons',
				'elementor-common',
				'select2',
			],
			ELEMENTOR_VERSION
		);

		wp_enqueue_script(
			'elementor-app-packages',
			$this->get_js_assets_url( 'app-packages' ),
			[
				'wp-i18n',
				'react',
			],
			ELEMENTOR_VERSION,
			true
		);

		wp_register_script(
			'select2',
			$this->get_js_assets_url( 'e-select2.full', 'assets/lib/e-select2/js/' ),
			[
				'jquery',
			],
			'4.0.6-rc.1',
			true
		);

		wp_enqueue_script(
			'elementor-app',
			$this->get_js_assets_url( 'app' ),
			[
				'wp-url',
				'wp-i18n',
				'elementor-v2-ui',
				'elementor-v2-icons',
				'elementor-v2-onboarding',
				'react',
				'react-dom',
				'select2',
			],
			ELEMENTOR_VERSION,
			true
		);

		$this->enqueue_dark_theme_detection_script();

		Assets_Translation_Loader::for_handles( [ 'elementor-app-packages', 'elementor-app' ], 'elementor' );

		$this->print_config();
	}

	public function enqueue_app_loader() {
		wp_enqueue_script(
			'elementor-app-loader',
			$this->get_js_assets_url( 'app-loader' ),
			[
				'elementor-common',
			],
			ELEMENTOR_VERSION,
			true
		);

		$this->print_config( 'elementor-app-loader' );
	}

	private function register_import_export_customization_experiment() {
		Plugin::$instance->experiments->add_feature( [
			'name' => 'import-export-customization',
			'title' => esc_html__( 'Import/Export Customization', 'elementor' ),
			'description' => esc_html__( 'Enhanced import/export for website templates. Selectively include site content, templates, and settings with advanced granular control.', 'elementor' ),
			'release_status' => ExperimentsManager::RELEASE_STATUS_BETA,
			'default' => ExperimentsManager::STATE_ACTIVE,
			'hidden' => true,
			'mutable' => false,
		] );
	}

	public function __construct() {
		$this->register_import_export_customization_experiment();

		$this->add_component( 'site-editor', new SiteEditorModule() );

		if ( current_user_can( 'manage_options' ) || Utils::is_wp_cli() ) {
			$this->add_component( 'import-export', new ImportExportModule() );

			if ( Plugin::$instance->experiments->is_feature_active( 'import-export-customization' ) ) {
				$this->add_component( 'import-export-customization', new ImportExportCustomizationModule() );
			}

			// Kit library is depended on import-export
			$this->add_component( 'kit-library', new KitLibraryModule() );
		}

		$this->add_component( 'onboarding', new OnboardingModule() );

		add_action( 'elementor/editor-one/menu/register', function ( Menu_Data_Provider $menu_data_provider ) {
			$this->register_editor_one_menu( $menu_data_provider );
		} );

		// Happens after WP plugin page validation.
		add_filter( 'add_menu_classes', [ $this, 'fix_submenu' ] );

		if ( $this->is_current() ) {
			add_action( 'admin_init', [ $this, 'admin_init' ], 0 );
		} else {
			add_action( 'elementor/common/after_register_scripts', [ $this, 'enqueue_app_loader' ] );
		}
	}
}
PK     8\Lm      view.phpnu [        <?php
namespace Elementor\App;

use Elementor\Utils;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * @var App $this
 */


$theme_class = 'dark' === $this->get_elementor_ui_theme_preference() ? 'eps-theme-dark' : '';

?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title><?php echo esc_html__( 'Elementor', 'elementor' ) . ' ... '; ?></title>
		<base target="_parent">
		<?php wp_print_styles(); ?>
	</head>
	<body class="<?php Utils::print_unescaped_internal_string( $theme_class ); ?>">
		<div id="e-app"></div>
		<?php wp_print_footer_scripts(); ?>
	</body>
</html>
PK     \Ԏ    !  Support/Utility/StringUtility.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Support\Utility;

/**
 * Utility class for String manipulation.
 */
class StringUtility
{
    /**
     * Convert a URL to a title.
     *
     * Strips the site URL from the given URL, replaces dashes with spaces,
     * and capitalizes the first letter.
     */
    public static function convertUrlToTitle(string $url): string
    {
        // Strip off the page url from the page name
        $site_url = trailingslashit(get_site_url());
        $title = str_replace($site_url, '', $url);
        $title = str_replace('-', ' ', $title);

        // Enforce first letter uppercase
        return ucfirst($title);
    }

    /**
     * Convert a string from snake_case to PascalCase.
     */
    public static function snakeToPascalCase(string $string): string
    {
        return str_replace('_', '', ucwords($string, '_'));
    }
}
PK     \fP    &  Support/Helpers/Storages/UriConfig.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Support\Helpers\Storages;

use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;

/**
 * Typed configuration wrapper for config/uri.php
 */
final class UriConfig extends Storage
{
    public function __construct()
    {
        parent::__construct(
            require dirname(__FILE__, 5) . '/config/uri.php'
        );
    }
}
PK     \g)    +  Support/Helpers/Storages/RequestStorage.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Support\Helpers\Storages;

use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;

/**
 * Request storage helper used in DI container.
 */
final class RequestStorage extends Storage
{
    public function __construct()
    {
        $body = $this->getRequestBody();

        parent::__construct([
            'global' => $_REQUEST,
            'files' => $_FILES,
            'body' => $body,
        ]);
    }

    private function getRequestBody(): array
    {
        $body = [];
        if (!isset($_SERVER['REQUEST_METHOD'])) {
            return $body;
        }

        $requestMethod = strtoupper((string) $_SERVER['REQUEST_METHOD']);

        if ($requestMethod === 'GET') {
            return $body;
        }

        $input = file_get_contents('php://input');
        $decoded = json_decode($input, true);

        if (is_array($decoded)) {
            $body = $decoded;
        }

        return $body;
    }
}
PK     \vۻB    *  Support/Helpers/Storages/RelatedConfig.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Support\Helpers\Storages;

use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;

/**
 * Typed configuration wrapper for config/related.php
 */
final class RelatedConfig extends Storage
{
    public function __construct()
    {
        parent::__construct(
            require dirname(__FILE__, 5) . '/config/related.php'
        );
    }
}
PK     \^#    .  Support/Helpers/Storages/EnvironmentConfig.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Support\Helpers\Storages;

use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;

/**
 * Environment configuration helper used in DI container.
 */
final class EnvironmentConfig extends Storage
{
    public function __construct()
    {
        parent::__construct(
            require dirname(__FILE__, 5) . '/config/env.php'
        );
    }
}
PK     \la-      Support/Helpers/Storage.phpnu [        <?php

namespace ReallySimplePlugins\RSS\Core\Support\Helpers;

use Adbar\Dot;

/**
 * Wrapper for easy access to storage data. Create a new instance with an array
 * of data in the constructor. Now all data can be accessed using Dot notation.
 *
 * @usage $storage = new Storage(['key' => 'value']);
 * @usage $storage->get('key', 'default');
 */
class Storage extends Dot
{
    /**
     * Returns the parameter keys.
     */
    public function keys(): array
    {
        return array_keys($this->items);
    }

    /**
     * Returns the sanitized string of the parameter value.
     * @uses sanitize_text_field()
     */
    public function getString(string $key, string $default = '', bool $trim = false): string
    {
        $value = sanitize_text_field($this->get($key, $default));
        return $trim ? trim($value) : $value;
    }

    /**
     * Returns the sanitized string of the parameter value.
     * @uses sanitize_textarea_field()
     */
    public function getTextarea(string $key, string $default = '', bool $trim = false): string
    {
        $value = sanitize_textarea_field($this->get($key, $default));
        return $trim ? trim($value) : $value;
    }

    /**
     * Strips out all characters that are not allowable in an email and returns
     * the value.
     * @uses sanitize_email()
     */
    public function getEmail(string $key, string $default = ''): string
    {
        return sanitize_email($this->get($key, $default));
    }

    /**
     * Returns the parameter value as a slug.
     * @uses sanitize_title
     */
    public function getTitle(string $key, string $default = ''): string
    {
        return sanitize_title($this->get($key, $default));
    }

    /**
     * Sanitizes content for allowed HTML tags for post content.
     * @uses wp_kses_post()
     */
    public function getPost(string $key, string $default = ''): string
    {
        return wp_kses_post($this->get($key, $default));
    }

    /**
     * Returns a sanitized URL.
     * @uses sanitize_url()
     */
    public function getUrl(string $key, string $default = ''): string
    {
        return sanitize_url($this->get($key, $default));
    }

    /**
     * Keys are used as internal identifiers. Lowercase alphanumeric characters,
     * dashes, and underscores are allowed.
     * @uses sanitize_key()
     */
    public function getKey(string $key, string $default = ''): string
    {
        return sanitize_key($this->get($key, $default));
    }

    /**
     * Returns the alphabetic characters of the parameter value.
     */
    public function getAlpha(string $key, string $default = ''): string
    {
        return preg_replace('/[^[:alpha:]]/', '', $this->get($key, $default));
    }

    /**
     * Returns the alphabetic characters of the parameter value. With spaces.
     */
    public function getAlphaSpace(string $key, string $default = ''): string
    {
        return preg_replace('/[^[:alpha:] ]/', '', $this->get($key, $default));
    }

    /**
     * Returns the alphabetic characters and digits of the parameter value.
     */
    public function getAlnum(string $key, string $default = ''): string
    {
        return preg_replace('/[^[:alnum:]]/', '', $this->get($key, $default));
    }

    /**
     * Returns the digits of the parameter value.
     *
     * @param string $default The default value runs through
     * FILTER_SANITIZE_NUMBER_INT as well
     */
    public function getDigits(string $key, string $default = ''): string
    {
        // we need to remove - and + because they're still allowed by the filter
        return str_replace(['-', '+'], '', $this->filter($key, $default, FILTER_SANITIZE_NUMBER_INT));
    }

    /**
     * Returns the parameter value typecast as integer.
     */
    public function getInt(string $key, int $default = 0): int
    {
        return (int) $this->get($key, $default);
    }

    /**
     * Returns the parameter value typecast as float.
     */
    public function getFloat(string $key, $default = 0): float
    {
        return (float) $this->get($key, $default);
    }

    /**
     * Returns the parameter value filtered as a boolean. Uses flag:
     * FILTER_VALIDATE_BOOLEAN
     */
    public function getBoolean(string $key, $default = false): bool
    {
        return $this->filter($key, $default, FILTER_VALIDATE_BOOLEAN);
    }

    /**
     * Returns the parameter value validated as a 2 character country code. If
     * the preg_match for exactly two alphabetic characters fails, the default
     * value is returned.
     */
    public function getCountryCode(string $key, string $default = ''): string
    {
        $country = strtoupper(trim($this->get($key, $default)));
        if (preg_match('/^[a-z]{2}$/i', $country)) {
            return $country;
        }

        return $default;
    }

    /**
     * Returns a boolean if the value is considered not empty.
     * @param array<TKey>|int|string|null $keys
     */
    public function isNotEmpty($keys = null): bool
    {
        return $this->isEmpty($keys) === false;
    }

    /**
     * Returns a boolean if the value of one of the keys is considered empty.
     * @param array<TKey>|int|string|null $keys
     */
    public function isOneEmpty($keys = []): bool
    {
        foreach ($keys as $key) {
            if ($this->isEmpty($key)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Filter key.
     * @return mixed
     * @see http://php.net/manual/en/function.filter-var.php
     */
    public function filter(string $key, $default = null, int $filter = FILTER_DEFAULT, $options = [])
    {
        $value = $this->get($key, $default);
        // Always turn $options into an array - this allows filter_var option shortcuts.
        if (!\is_array($options) && $options) {
            $options = ['flags' => $options];
        }
        // Add a convenience check for arrays.
        if (\is_array($value) && !isset($options['flags'])) {
            $options['flags'] = FILTER_REQUIRE_ARRAY;
        }

        return filter_var($value, $filter, $options);
    }
}
PK     \Dҕ<    #  Controllers/DashboardController.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Controllers;

use ReallySimplePlugins\RSS\Core\Interfaces\ControllerInterface;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig;

class DashboardController implements ControllerInterface
{
	protected EnvironmentConfig $env;

    public function __construct(EnvironmentConfig $environmentConfig)
    {
		$this->env = $environmentConfig;
    }

    public function register(): void
    {
        // Redirect on the activation hook, but do it after anything else.
        add_action('rss_core_activation', [$this, 'maybeRedirectToDashboard'], 9999);
    }

    /**
     * Redirect to dashboard page on activation, but only if the user manually
     * activated the plugin via the plugins overview. React will handle
     * redirect to onboarding if needed.
     *
     * @param string $pageSource The page where the activation was triggered,
     * usually 'plugins.php' or 'update.php'.
     */
    public function maybeRedirectToDashboard(string $pageSource = ''): void
    {
        if ($pageSource !== 'plugins.php' && $pageSource !== 'update.php') {
            return;
        }

        wp_safe_redirect($this->env->getUrl('plugin.dashboard_url'));
        exit;
    }
}
PK     \Z      Managers/FeatureManager.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Managers;

use ReallySimplePlugins\RSS\Core\Bootstrap\App;
use ReallySimplePlugins\RSS\Core\Interfaces\FeatureInterface;

/**
 * This manager dynamically fetches the features of the plugin. It differs from
 * other manager classes due to this nature. By preventing any class usage of
 * features we prevent composer from loading the feature file entirely until
 * first use. This prevents overhead from loading features that are no longer
 * needed. We prevent loading feature files by utilizing the
 * {@see AbstractLoader} class at {@see FeatureManager:92}
 */
final class FeatureManager extends AbstractManager
{
    private const PRO_FEATURE_HANDLE = 'Pro:';

    /**
     * @inheritDoc
     */
    public function isRegistrable(object $class): bool
    {
        return $class instanceof FeatureInterface;
    }

    /**
     * @inheritDoc
     */
    public function registerClass(object $class): void
    {
        $class->register();
    }

    /**
     * @inheritDoc
     */
    public function afterRegister(): void
    {
        do_action('rss_core_features_loaded');
    }

    /**
     * Register and load all features from the src/features directory. This
     * method automatically loads all classes from the features directory and
     * injects the dependency classes into the Controller class if they exist.
     * @uses do_action rss_core_features_loaded
     */
    public function registerFeatures(): void
    {
        $featureClasses = $this->getFeatureClasses();
        $this->register($featureClasses);
    }

    /**
     * Dynamically build and then return an array of feature classes that are
     * saved in the features path of the plugin.
     */
    public function getFeatureClasses(): array
    {
        $features = $this->getFeatures();
        $featureClasses = [];

        foreach ($features as $featureName) {
            $needsPro = strpos($featureName, self::PRO_FEATURE_HANDLE) !== false;
            if ($needsPro && !$this->env->getBoolean('plugin.pro')) {
                continue; // Pro not installed, don't register pro features
            }

            if ($needsPro) {
                $featureName = substr($featureName, strlen(self::PRO_FEATURE_HANDLE));
            }

            // Check if the feature directory exists
            $featuresPath = $this->getFeaturePath($featureName, $needsPro);
            if (!is_dir($featuresPath)) {
                continue;
            }

            // Get the feature namespace
            $prefix = $this->getFeatureNamespace($featureName, $needsPro) . $featureName;

            // Get the {FeatureName}Loader class for the feature
            if (class_exists($prefix . 'Loader') === false) {
                continue;
            }

            $loader = App::getInstance()->make($prefix . 'Loader', false, false);
            if (!$loader->isEnabled() || !$loader->inScope()) {
                continue;
            }

            // The controller is the backbone of a feature
            $featureClasses[] = $prefix . 'Controller';
        };

        return $featureClasses;
    }

    /**
     * Get all feature directory names. Includes "Pro" features prefixed
     * with {@see PRO_FEATURE_HANDLE}.
     */
    private function getFeatures(): array
    {
        $featuresPath = $this->env->getString('plugin.feature_path');

        $features = [];

        foreach (new \DirectoryIterator($featuresPath) as $fileInfo) {
            if ($fileInfo->isDot() || !$fileInfo->isDir()) {
                continue;
            }

            $proIsNotActive = ($this->env->getBoolean('plugin.pro') !== true);
            $isProFeature = ($fileInfo->getFilename() === 'Pro');
            $licenseIsInvalid = ($this->license->isValid() !== true);

            if ($isProFeature && ($proIsNotActive || $licenseIsInvalid)) {
                continue;
            }


            if ($fileInfo->getFilename() === 'Pro') {
                foreach (new \DirectoryIterator($fileInfo->getPathname()) as $proInfo) {
                    if ($proInfo->isDot() || !$proInfo->isDir()) {
                        continue;
                    }
                    $features[] = self::PRO_FEATURE_HANDLE . $proInfo->getFilename();
                }
                continue;
            }

            $features[] = $fileInfo->getFilename();
        }
        return $features;
    }

    /**
     * Get the feature path based on the feature name and if it needs the Pro
     * version.
     */
    private function getFeaturePath(string $featureName, bool $needsPro): string
    {
        return $this->env->getString('plugin.feature_path') . ($needsPro ? 'Pro/' : '') . $featureName . '/';
    }

    /**
     * Get the feature namespace.
     */
    private function getFeatureNamespace(string $featureName, bool $needsPro = false): string
    {
        return 'ReallySimplePlugins\RSS\Core\Features\\' . ($needsPro ? 'Pro\\' : '') . $featureName . '\\';
    }
}
PK     \o_:
  
    Managers/AbstractManager.phpnu [        <?php

namespace ReallySimplePlugins\RSS\Core\Managers;

use ReallySimplePlugins\RSS\Core\Bootstrap\App;
use ReallySimplePlugins\RSS\Core\Services\LicenseService;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig;

abstract class AbstractManager
{
    protected EnvironmentConfig $env;
    protected LicenseService $license;

    /**
     * Overwrite this property to true when the entries that the child Manager
     * registers should be added to the container registry. For details see:
     * {@see App::make}
     */
    protected bool $useRegistry = false;

    /**
     * Overwrite this property to true when the dependencies of the entries that
     * the  child Manager registers should be added to the container registry.
     * For details see: {@see App::make}
     */
    protected bool $useRegistryForDependencies = true;

    /**
     * Bind the container
     */
    public function __construct(
        EnvironmentConfig $environmentConfig,
        LicenseService $licence
    )
    {
        $this->env = $environmentConfig;
        $this->license = $licence;
    }

    /**
     * Child class should check if the given class can be registered. For
     * example by checking if it implements an interface to know the logic in
     * the {@see registerClass} method can be executed.
     */
    abstract public function isRegistrable(object $class): bool;

    /**
     * Logic to register the given class. If this method can be executed is
     * checked by the {@see isRegistrable} method.
     */
    abstract public function registerClass(object $class): void;

    /**
     * Method called after all classes given to the manager are registered.
     */
    abstract public function afterRegister(): void;

    /**
     * Register the given class as long as the entries are registrable according
     * to the child managers. Class are autowired, but not registered via
     * {@see App::make}
     *
     * @throws \LogicException When a developer is doing it wrong.
     * @throws \ReflectionException When the controller cannot be loaded.
     */
    public function register(array $classes): void
    {
        foreach ($classes as $fullyClassifiedName) {
            if (is_string($fullyClassifiedName) === false) {
                throw new \LogicException("Class must be a fully qualified name: " . esc_html($fullyClassifiedName));
            }

            $class = App::getInstance()->make($fullyClassifiedName, $this->useRegistry, $this->useRegistryForDependencies);

            if ($this->isRegistrable($class) === false) {
                throw new \LogicException("Class is not registrable: " . $fullyClassifiedName);
            }

            $this->registerClass($class);
        }

        $this->afterRegister();
    }
}
PK     \1f#      Managers/EndpointManager.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Managers;

use ReallySimplePlugins\RSS\Core\Interfaces\MultiEndpointInterface;
use ReallySimplePlugins\RSS\Core\Interfaces\SingleEndpointInterface;
use ReallySimplePlugins\RSS\Core\Traits\HasAllowlistControl;
use ReallySimplePlugins\RSS\Core\Traits\HasNonces;

final class EndpointManager extends AbstractManager
{
    use HasNonces;
    use HasAllowlistControl;

    private string $version;
    private string $namespace;
    private array $routes = [];

    /**
     * @inheritDoc
     */
    public function isRegistrable(object $class): bool
    {
        return ($class instanceof SingleEndpointInterface
            || $class instanceof MultiEndpointInterface
        );
    }

    /**
     * @inheritDoc
     */
    public function registerClass(object $class): void
    {
        if ($class instanceof SingleEndpointInterface) {
            $this->registerSingleEndpointRoute($class);
        }

        $this->registerMultiEndpointRoute($class);
    }

    /**
     * @inheritDoc
     */
    public function afterRegister(): void
    {
        $this->registerWordPressRestRoutes();
        do_action('rss_core_endpoints_loaded');
    }

    /**
     * Register a plugin route for and endpoint instance that implements the
     * {@see SingleEndpointInterface}
     */
    private function registerSingleEndpointRoute(SingleEndpointInterface $endpoint): void
    {
        if ($endpoint->enabled() === false) {
            return;
        }

        $this->routes[$endpoint->registerRoute()] = $endpoint->registerArguments();
    }

    /**
     * Register plugin routes for an endpoint instance that implements the
     * {@see MultiEndpointInterface}
     */
    private function registerMultiEndpointRoute(MultiEndpointInterface $endpoint): void
    {
        if ($endpoint->enabled() === false) {
            return;
        }

        $routeEndpoints = $endpoint->registerRoutes();
        foreach ($routeEndpoints as $route => $arguments) {
            $this->routes[$route] = $arguments;
        }
    }

    /**
     * This method provides a way to register custom REST routes via the
     * rss_core_rest_routes filter. A controller of feature should be
     * instantiated before this manager is called and the controller should
     * hook into the rss_core_rest_routes filter to add its own routes.
     * @uses apply_filters rss_core_rest_routes
     */
    public function registerWordPressRestRoutes(): void
    {

        $routes = $this->getPluginRestRoutes();

        foreach ($routes as $route => $data) {
            $version = ($data['version'] ??  $this->env->getString('http.version'));
            $callback = ($data['callback'] ?? null);
            $middleware = ($data['middleware'] ?? null);

            $arguments = [
                'methods' => $this->normalizeMethods($data['methods'] ?? ''),
                'callback' => $this->callbackMiddleware($callback, $middleware),
                'permission_callback' => ($data['permission_callback'] ?? [$this, 'defaultPermissionCallback']),
            ];

            register_rest_route($this->env->getUrl('http.namespace') . '/' . $version, $route, $arguments);
        }
    }

    /**
     * Get the plugins REST routes
     * @uses apply_filters rss_core_rest_routes
     */
    private function getPluginRestRoutes(): array
    {
        /**
         * Filter: rss_core_rest_routes
         * Can be used to add or modify the REST routes
         *
         * @param array $routes
         * @return array
         * @example [
         *      'route' => [ // key is the route name
         *          'methods' => 'GET', // required
         *          'callback' => 'callback_function', // required
         *          'permission_callback' => 'permission_callback_function', // optional to override the default permission callback
         *          'version' => 'v1' // optional to override the default version
         *      ]
         * ]
         */
        return apply_filters('rss_core_rest_routes', $this->routes);
    }

    /**
     * This method is used to add middleware to the callback function. The
     * middleware should be a callable function that takes a request as an
     * argument and returns a response. The default middleware is to switch
     * the user locale to the current user locale.
     */
    public function callbackMiddleware(?callable $callback, ?callable $middleware): callable
    {
        return function ($request) use ($callback, $middleware) {
            if (is_callable($middleware)) {
                $middleware($request);
            } else {
                $this->defaultMiddlewareCallback();
            }

            return $callback($request);
        };
    }

    /**
     * This method is used to switch the user locale to the current user locale.
     * This is important because we will otherwise show the default site
     * language to the user for the Tasks and Notifications. Those
     * translations are created in PHP and not in JS.
     */
    private function defaultMiddlewareCallback(): void
    {
        switch_to_user_locale(get_current_user_id());
    }

    /**
     * The default permission callback, will check if the nonce is valid and if
     * the user has the required permissions to do a request.
     * @return bool|\WP_Error
     */
    public function defaultPermissionCallback(\WP_REST_Request $request)
    {
        $method = $request->get_method();
        $nonce = $request->get_param('nonce');
        if (($method === 'POST') && ($this->verifyNonce($nonce) === false)) {
            return new \WP_Error(
                'rest_forbidden',
                esc_html__('Forbidden.', 'really-simple-ssl'),
                ['status' => 403]
            );
        }

        return true;
    }

    /**
     * Process the given methods and compare them to the allowed
     * {@see \WP_REST_Server::ALLMETHODS} methods. Remove unwanted entries and
     * cleanup method usage from, for example, "get " to "GET".
     *
     * @return string From "get, POSt, fake" to "GET,POST"
     */
    private function normalizeMethods(string $methods): string
    {
        // Split into array, trim whitespace and uppercase entries
        $methodsArray = array_map('trim', explode(',', $methods));
        $methodsArray = array_map('strtoupper', $methodsArray);

        // Split allowed entries into array and trim whitespaces
        $allowedMethodsArray = array_map('trim', explode(',', \WP_REST_Server::ALLMETHODS));

        // Keep only allowed methods
        $methodsArray = array_intersect($methodsArray, $allowedMethodsArray);
        $methodsArray = array_values(array_unique($methodsArray));

        // Convert back to CSV format for register_rest_route usage
        return implode(',', $methodsArray);
    }
}
PK     \(z  z    Managers/ControllerManager.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Managers;

use ReallySimplePlugins\RSS\Core\Interfaces\ControllerInterface;

final class ControllerManager extends AbstractManager
{
    /**
     * @inheritDoc
     */
    public function isRegistrable(object $class): bool
    {
        return $class instanceof ControllerInterface;
    }

    /**
     * @inheritDoc
     */
    public function registerClass(object $class): void
    {
        $class->register();
    }

    /**
     * @inheritDoc
     */
    public function afterRegister(): void
    {
        do_action('rss_core_controllers_loaded');
    }
}
PK     \_q  q    Managers/ProviderManager.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Managers;

use ReallySimplePlugins\RSS\Core\Interfaces\ProviderInterface;

final class ProviderManager extends AbstractManager
{
    /**
     * @inheritDoc
     */
    public function isRegistrable(object $class): bool
    {
        return $class instanceof ProviderInterface;
    }

    /**
     * @inheritDoc
     */
    public function registerClass(object $class): void
    {
        $class->provide();
    }

    /**
     * @inheritDoc
     */
    public function afterRegister(): void
    {
        do_action('rss_core_providers_loaded');
    }
}
PK     \Z>L  L  .  views/features/vulnerability/plugin-column.phpnu [        <?php
/**wp-en
 * @var string $severity
 * @var string $label
 * @var string $frontendUrl
 */

?>
<a href="<?php echo esc_url($frontendUrl); ?>"
   target="_blank"
   rel="noopener noreferrer">
    <span class="rsssl-btn-vulnerable rsssl-<?php echo esc_attr($severity); ?>">
        <?php echo esc_html($label); ?>
    </span>
</a>
PK     \M      /  views/features/vulnerability/severity-label.phpnu [        <?php
/**
 * @var string $label
 * @var string $class
 */

?>

<a class="rsssl-badge-large <?php echo esc_attr($class); ?>"><?php echo esc_attr($label); ?></a>
PK     \1b˖    !  Services/SecureSocketsService.phpnu [        <?php

namespace ReallySimplePlugins\RSS\Core\Services;

final class SecureSocketsService
{
    /**
     * Method to activate SSL for the current site.
     * @return array|bool Array when the current request is a REST request
     *
     * @todo Move admin method here after full refactor.
     */
    public function activateSSL(array $data = [])
    {
        return RSSSL()->admin->activate_ssl($data);
    }
}
PK     \V!  !  "  Services/SettingsConfigService.phpnu [        <?php

namespace ReallySimplePlugins\RSS\Core\Services;

/**
 * This Settings-service class does NOT do any CRUD actions on the settings. It
 * is only responsible for doing business logic based on the fields config of
 * the settings. Like returning recommended settings.
 */
class SettingsConfigService
{
    /**
     * Returns recommended settings. Also includes Pro features when enabled.
     * @param bool $includeProFeatures To add/exclude recommended pro settings
     */
    public function getRecommendedSettings(bool $includeProFeatures = false): array
    {
        $features = [
            [
                'title' => esc_html__('Vulnerability scan', 'really-simple-ssl'),
                'id' => 'vulnerability_detection',
                'options' => ['enable_vulnerability_scanner'],
                'activated' => true,
            ],
            [
                'title' => esc_html__('Essential WordPress hardening', 'really-simple-ssl'),
                'id' => 'hardening',
                'options' => $this->getRecommendedHardeningSettings(),
                'activated' => true,
            ],
            [
                'title' => esc_html__('E-mail login', 'really-simple-ssl'),
                'id' => 'two_fa',
                'options' => ['login_protection_enabled'],
                'activated' => true,
            ],
            [
                'title' => esc_html__('Mixed Content Fixer', 'really-simple-ssl'),
                'id' => 'mixed_content_fixer',
                'options' => ['mixed_content_fixer'],
                'activated' => true,
            ],
        ];

        if ($includeProFeatures === false) {
            return $features;
        }

        $proFeatures = [
            [
                'title' => esc_html__('Firewall', 'really-simple-ssl'),
                'id' => 'firewall',
                'premium' => true,
                'options' => ['enable_firewall'],
                'activated' => true,
            ],
            [
                'title' => esc_html__('Two-Factor Authentication', 'really-simple-ssl'),
                'id' => 'two_fa',
                'premium' => true,
                'options' => ['login_protection_enabled'],
                'activated' => true,
            ],
            [
                'title' => esc_html__('Limit Login Attempts', 'really-simple-ssl'),
                'id' => 'limit_login_attempts',
                'premium' => true,
                'options' => ['enable_limited_login_attempts', 'enable_limited_password_reset_attempts'],
                'activated' => true,
            ],
            [
                'title' => esc_html__('Security Headers', 'really-simple-ssl'),
                'id' => 'advanced_headers',
                'premium' => true,
                'options' => [],
                'activated' => true,
            ],
        ];

        return array_merge($features, $proFeatures);
    }

    /**
     * Method returns all recommended setting id's in array format.
     * @example [disable_anyone_can_register, disable_file_editing]
     *
     * Currently, the only settings
     * with the 'recommended' key are basic hardening settings:
     * {@see /settings/config/fields/hardening-basic.php}
     *
     * @todo Kept business logic the same, but it needs a refactor to actually
     * get the hardening settings.
     */
    public function getRecommendedHardeningSettings(): array
    {
        $fields = rsssl_fields(false);

        $recommended = array_filter($fields, static function($field) {
            return isset($field['recommended']) && $field['recommended'];
        });

        return array_map(static function($field) {
            return $field['id'];
        }, $recommended);
    }

    /**
     * Method returns grouped settings per premium feature. Each item is an
     * array containing the related settings listed in the options key.
     *
     * @todo: Kept business logic the same, but shouldn't we add these to the
     * getRecommendedSettings method when $includeProFeatures equals true?
     */
    public function getRecommendedProSettings(): array
    {
        return [
            [
                'title' => esc_html__('Firewall', 'really-simple-ssl'),
                'id' => 'firewall',
                'premium' => true,
                'options' => ['enable_firewall'],
                'activated' => true,
            ],
            [
                'title' => esc_html__('Two-Factor Authentication', 'really-simple-ssl'),
                'id' => 'two_fa',
                'premium' => true,
                'options' => ['two_fa_enabled_roles_totp'],
                'value' => ['administrator'],
                'activated' => true,
            ],
            [
                'title' => esc_html__('Limit Login Attempts', 'really-simple-ssl'),
                'id' => 'limit_login_attempts',
                'premium' => true,
                'options' => ['enable_limited_login_attempts', 'enable_limited_password_reset_attempts'],
                'activated' => true,
            ],
            [
                'title' => esc_html__('Security Headers', 'really-simple-ssl'),
                'id' => 'advanced_headers',
                'premium' => true,
                'options' => [
                    'upgrade_insecure_requests',
                    'x_content_type_options',
                    'hsts',
                    ['x_xss_protection' => 'zero'],
                    'x_content_type_options',
                    ['x_frame_options' => 'SAMEORIGIN'],
                    ['referrer_policy' => 'strict-origin-when-cross-origin'],
                    ['csp_frame_ancestors' => 'self'],
                ],
                'activated' => true,
            ],
            [
                'title' => esc_html__('Vulnerability Measures', 'really-simple-ssl'),
                'id' => 'vulnerability_measures',
                'options' => ['enable_vulnerability_scanner', 'measures_enabled'],
                'activated' => true,
            ],
            [
                'title' => esc_html__('Advanced WordPress Hardening', 'really-simple-ssl'),
                'id' => 'advanced_hardening',
                'premium' => true,
                'options' => ['change_debug_log_location', 'disable_http_methods'],
                'activated' => true,
            ],
            [
                'title' => esc_html__('Strong Password policy', 'really-simple-ssl'),
                'id' => 'password_security',
                'options' => ['enforce_password_security_enabled', 'enable_hibp_check'],
                'activated' => true,
            ],
        ];
    }
}
PK     \xg      Services/LicenseService.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Services;

/**
 * Business logic related to the plugin licensing.
 * @todo Move RSSSL()->licensing methods here after full refactor.
 */
final class LicenseService
{
    /**
     * Method returns true if the license is valid. False otherwise.
     */
    public function isValid(): bool
    {
        if ( ! function_exists( 'RSSSL' ) ) {
            return false;
        }

        $plugin = RSSSL();

        if ( ! isset( $plugin->licensing ) || ! method_exists( $plugin->licensing, 'license_is_valid' ) ) {
            return false;
        }

        return $plugin->licensing->license_is_valid();
    }
}
PK     \VJ,      Services/EmailService.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Services;

use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\UriConfig;
use ReallySimplePlugins\RSS\Core\Traits\HasEncryption;

/**
 * todo: move mailer methods to this service after full refactor.
 */
class EmailService
{
    use HasEncryption;

    private ?\rsssl_mailer $mailer = null;
    protected EnvironmentConfig $env;
    protected UriConfig $uriConfig;

    public function __construct(EnvironmentConfig $environmentConfig, UriConfig $uriConfig)
    {
        $this->env = $environmentConfig;
        $this->uriConfig = $uriConfig;
    }

    /**
     * Method is used to lazyload the mailer property. This prevents overhead
     * but most importantly prevents _load_textdomain_just_in_time error
     */
    protected function getMailer(): \rsssl_mailer
    {
        if ($this->mailer instanceof \rsssl_mailer) {
            return $this->mailer;
        }

        require_once $this->env->getString('plugin.path') . '/mailer/class-mail.php';
        $this->mailer = new \rsssl_mailer();

        return $this->mailer;
    }

    /**
     * Set the email of the recipient.
     * @throws \InvalidArgumentException if email address is not valid
     */
    public function setEmail(string $email): void
    {
        $sanitizedEmail = sanitize_email($email);
        if (empty($sanitizedEmail)) {
            throw new \InvalidArgumentException("Email address \"$email\" not valid in " . __METHOD__);
        }

        $this->getMailer()->set_to($sanitizedEmail);
    }

    /**
     * Trigger the verification mail
     */
    public function sendVerificationMail(): array
    {
        return $this->getMailer()->send_verification_mail();
    }

    /**
     * Signup for Tips & Tricks from Really Simple Security
     * @return array|\WP_Error
     * @throws \InvalidArgumentException if email address is not valid
     */
    public function addEmailToMailingList(string $email)
    {
        $sanitizedEmail = sanitize_email($email);
        if (empty($sanitizedEmail)) {
            throw new \InvalidArgumentException("Email address \"$email\" not valid in " . __METHOD__);
        }

        $license = '';
        $hasPremium = defined('rsssl_pro');

        if ($hasPremium) {
            $license = RSSSL()->licensing->license_key();
            $license = $this->maybeDecryptPrefixed($license, 'really_simple_ssl_');
        }

        $payload = [
            'has_premium' => $hasPremium,
            'license' => $license,
            'email' => $sanitizedEmail,
            'domain' => esc_url_raw(site_url()),
        ];

        return wp_remote_post($this->uriConfig->getUrl('rsp.mailinglist'), [
            'timeout' => 15,
            'sslverify' => true,
            'body' => $payload
        ]);
    }

    /**
     * Get the email address to which notifications should be sent, based on user configuration.
     */
    public function getNotificationsEmail(): string
    {
        if (!function_exists('rsssl_get_option')) {
            return '';
        }

        return (string) rsssl_get_option('notifications_email_address', get_bloginfo('admin_email'));
    }

    /**
     * Check if the user has enabled email notifications in their settings.
     * @return bool True if email notifications are enabled, false otherwise.
     */
    public function isNotificationsEnabled(): bool
    {
        if (!function_exists('rsssl_get_option')) {
            return false;
        }

        return (bool) rsssl_get_option('send_notifications_email', false);
    }

    /**
     * Check if the email verification flow has been completed, based on the stored option.
     *
     * @return bool True if the email verification flow has been completed, false otherwise.
     */
    public function isEmailVerified(): bool
    {
        if (!\function_exists('get_option')) {
            return false;
        }

        $status = (string) \get_option('rsssl_email_verification_status', '');
        if ($status === 'completed') {
            return true;
        }

        if (\function_exists('is_multisite') && \is_multisite() && \function_exists('get_site_option')) {
            $networkStatus = (string) \get_site_option('rsssl_email_verification_status', '');
            if ($networkStatus === 'completed') {
                return true;
            }
        }

        return false;
    }
}
PK     \ol      Services/CertificateService.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Services;

/**
 * Business logic related to the site certificate
 * @todo Move RSSSL()->certificate methods here after full refactor.
 */
final class CertificateService
{
    /**
     * Method returns true if the site certificate is valid. False otherwise.
     */
    public function isValid(): bool
    {
        return RSSSL()->certificate->is_valid();
    }

    /**
     * Method returns true if the certificate detection failed prior to calling
     * this method. It uses the transient 'rsssl_certinfo' for the detection.
     */
    public function detectionFailed(): bool
    {
        return RSSSL()->certificate->detection_failed();
    }
}
PK     \$!  !  !  Services/RelatedPluginService.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Services;

use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\RelatedConfig;

final class RelatedPluginService
{
    /**
     * Should be a Storage object based on one entry in the related config
     */
    private Storage $pluginConfig;
    private RelatedConfig $relatedConfig;

    public function __construct(RelatedConfig $relatedConfig)
    {
        $this->relatedConfig = $relatedConfig;
    }

    public function setPluginConfigBySlug(string $slug): void
    {
        $plugins = $this->relatedConfig->get('plugins', []);
        $plugins = array_filter($plugins, static function($plugin) use ($slug) {
            return isset($plugin['slug']) && ($plugin['slug'] === $slug);
        });

        $plugin = reset($plugins);
        $this->setPluginConfig($plugin);
    }

    /**
     * Use this method as the default way to set the plugin config.
     */
    public function setPluginConfig(array $pluginConfig): void
    {
        $this->pluginConfig = new Storage($pluginConfig);
    }

    /**
     * Get the list of recommended plugins for the onboarding process.
     *
     * This function prepares plugin data for display in the onboarding wizard.
     * It handles plugin status, actions, and checkbox initialization based on
     * configuration.
     *
     * @return array List of plugin items with their status, actions and UI properties
     *
     * @todo: Plugins that are already installed and activated are still listed
     * in the onboarding.
     */
    public function getOnboardingConfig(): array
    {
        $checkboxes = [];
        $relatedPlugins = $this->relatedConfig->get('plugins', []);

        foreach ($relatedPlugins as $config) {
            if (!isset($config['slug'], $config['title'])) {
                continue;
            }

            $this->setPluginConfig($config);
            $activated = $this->pluginConfig->getBoolean('pre_checked');

            $checkboxes[] = [
                'id' => $config['slug'],
                'title' => $config['title'],
                'action' => ($activated ? $this->getAvailablePluginAction() : 'none'),
                'activated' => $activated,
                'current_action' => 'none',
                'default_action' => ($activated ? null : $this->getAvailablePluginAction()),
            ];
        }

        return $checkboxes;
    }

    /**
     * Method returns the url fitting for the context. If a plugin is
     * upgradable, the upgrade_url is returned, otherwise the url entry.
     */
    public function getPluginUrl(): string
    {
        if ($this->pluginCanBeUpgraded()) {
            return $this->pluginConfig->getUrl('upgrade_url');
        }

        return $this->pluginConfig->getUrl('url');
    }

    /**
     * Method returns the action fitting for the context of the plugin.
     */
    public function getAvailablePluginAction(): string
    {
        if ($this->premiumPluginIsInstalled()) {
            return 'installed';
        }

        if ($this->pluginIsDownloadable()) {
            return 'download';
        }

        if ($this->pluginCanBeActivated()) {
            return 'activate';
        }

        if ($this->pluginCanBeUpgraded()) {
            return 'upgrade-to-premium';
        }

        return 'installed';
    }

    /**
     * Execute action for a related plugin
     */
    public function executeAction(string $action): bool
    {
        ob_start();

        switch ($action) {
            case 'download':
                $success = $this->downloadCurrentPlugin();
                break;
            case 'activate':
                $success = $this->activateCurrentPlugin();
                break;
            default:
                $success = false;
        }

        ob_get_clean();

        return $success;
    }

    /**
     * Download the related plugin currently stored in the plugin config
     * property.
     */
    protected function downloadCurrentPlugin(): bool
    {
        $transientName = 'rsp_plugin_download_active';
        if (get_transient($transientName) === $this->pluginConfig->getString('slug')) {
            return true;
        }

        set_transient($transientName, $this->pluginConfig->getString('slug'), MINUTE_IN_SECONDS);

        try {
            $pluginInfo = $this->getCurrentPluginInfo();
        } catch (\Exception $e) {
            return false;
        }

        $downloadLink = esc_url_raw($pluginInfo->versions['trunk']);

        require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
        require_once ABSPATH . 'wp-admin/includes/file.php';
        include_once ABSPATH . 'wp-admin/includes/plugin-install.php';
        require_once ABSPATH . 'wp-admin/includes/plugin.php';

        $skin = new \WP_Ajax_Upgrader_Skin();
        $upgrader = new \Plugin_Upgrader($skin);
        $result = $upgrader->install($downloadLink);

        if (is_wp_error($result)) {
            return false;
        }

        delete_transient($transientName);
        return true;
    }

    /**
     * Activate the related plugin currently stored in the plugin config
     * property.
     */
    protected function activateCurrentPlugin(): bool
    {
        $slug = $this->pluginConfig->getString('activation_slug');

        //when activated from the network admin, we assume the user wants network activated
        $networkwide = is_multisite() && is_network_admin();
        if (!defined('DOING_CRON')) {
            define('DOING_CRON', true);
        }

        if (!function_exists('activate_plugin')) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }

        $result = activate_plugin($slug, '', $networkwide);
        if (is_wp_error($result)) {
            return false;
        }

        return true;
    }

    /**
     * Helper method to check if the current plugin is a premium plugin and if
     * it is active.
     */
    protected function premiumPluginIsInstalled(): bool
    {
        return $this->pluginConfig->has('constant_premium') && defined($this->pluginConfig->getString('constant_premium'));
    }

    /**
     * Helper method to check if the current plugin is downloadable.
     */
    protected function pluginIsDownloadable(): bool
    {
        return $this->pluginFileExists() === false;
    }

    /**
     * Helper method to check if the current plugin can be activated.
     */
    protected function pluginCanBeActivated(): bool
    {
        return $this->pluginFileExists() && ($this->pluginIsActive() === false);
    }

    /**
     * Helper method to check if the current plugin can be upgraded. This means
     * the premium version is downloaded, but not yet activated.
     */
    protected function pluginCanBeUpgraded(): bool
    {
        return $this->pluginConfig->has('constant_premium') && !defined($this->pluginConfig->getString('constant_premium'));
    }

    /**
     * Helper method to check if the current plugin file exists.
     */
    protected function pluginFileExists(): bool
    {
        return file_exists(trailingslashit(WP_PLUGIN_DIR) . $this->pluginConfig->getString('activation_slug'));
    }

    /**
     * Helper method to check if the current plugin is active.
     */
    public function pluginIsActive(): bool
    {
        if (!function_exists('is_plugin_active')) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }

        return is_plugin_active($this->pluginConfig->getString('activation_slug'));
    }

    /**
     * Method returns the plugin info for the current plugin. Because we pass
     * the action 'plugin_information' to the plugins_api function, an object is
     * returned if the plugin is found, otherwise a WP_Error.
     * @throws \Exception If the plugin info could not be retrieved
     */
    protected function getCurrentPluginInfo(): object
    {
        $transientName = 'rsp_' . $this->pluginConfig->getString('slug') . '_plugin_info';
        $pluginInfo = get_transient($transientName);

        if (!empty($pluginInfo)) {
            return $pluginInfo;
        }

        if (function_exists('plugins_api') === false) {
            require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
        }

        $pluginInfo = plugins_api('plugin_information', [
            'slug' => $this->pluginConfig->getString('slug'),
        ]);

        if (is_wp_error($pluginInfo)) {
            throw new \Exception('Unable to get plugin info');
        }

        set_transient($transientName, $pluginInfo, WEEK_IN_SECONDS);
        return $pluginInfo;
    }
}
PK     \,4  4  $  Services/GlobalOnboardingService.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Services;

/**
 * Global onboarding service for managing onboarding state and visibility.
 * This service provides globally accessible onboarding functionality that can
 * be used throughout the plugin.
 */
class GlobalOnboardingService
{
    /**
     * Reset the onboarding to allow the onboarding modal to be shown again.
     * This called when:
     * - The license is deactivated
     * - The free plugin is deactivated after Pro installation
     * - The user clicks "Activate SSL" after previously dismissing onboarding
     *
     * @return void
     */
    public function resetOnboarding(): void
    {
        update_option('rsssl_show_onboarding', true, false);
        update_option('rsssl_onboarding_dismissed', false, false);
    }
}
PK     \d]  ]  >  Features/Vulnerability/Support/Helpers/VulnerabilityConfig.phpnu [        <?php

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Support\Helpers;

use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;

class VulnerabilityConfig extends Storage
{
    public function __construct()
    {
        parent::__construct(
            require dirname(__FILE__, 6) . '/config/vulnerability.php'
        );
    }
}
PK     \A    D  Features/Vulnerability/Controllers/VulnerabilityNoticeController.phpnu [        <?php

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\VulnerabilityStorageRepository;
use ReallySimplePlugins\RSS\Core\Interfaces\ControllerInterface;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Policies\ConfigurableSeverityPolicy;

/**
 * Integrates vulnerability-related notices into the plugin notice system.
 *
 * This controller is responsible only for UI notice integration:
 * - It hooks into the `rsssl_notices` filter and adds notice definitions.
 * - It reads notification thresholds from plugin options.
 * - It delegates vulnerability lookups to the storage repository.
 *
 * No scanning, persistence, or severity calculation is performed here.
 * The repository is treated as the source of truth for stored vulnerabilities.
 */
final class VulnerabilityNoticeController implements ControllerInterface
{
    private VulnerabilityStorageRepository $vulnerabilityStorageRepository;

    /**
     * Constructor.
     *
     * Injects the vulnerability storage repository dependency.
     * No processing or data fetching occurs during construction.
     */
    public function __construct(
        VulnerabilityStorageRepository $vulnerabilityStorageRepository
    ) {
        $this->vulnerabilityStorageRepository = $vulnerabilityStorageRepository;
    }

    /**
     * Registers the `rsssl_notices` hook to add vulnerability notices.
     *
     * This method is called once during plugin boot.
     */
    public function register(): void
    {
        add_filter('rsssl_notices', [$this, 'showHelpNotices'], 10, 1);
    }

    /**
     * Adds vulnerability notices based on stored vulnerabilities and configured thresholds.
     *
     * WordPress passes the current notice registry as an array. This method returns the
     * modified registry with zero or more vulnerability notices appended.
     *
     * Threshold behavior:
     * - Dashboard threshold controls whether the notice is shown on the plugin dashboard.
     * - Sitewide threshold controls whether the notice is shown as a WordPress admin notice.
     * - A threshold of `*` disables notices for that scope.
     *
     * @param array<string, mixed> $notices Existing notice registry.
     *
     * @return array<string, mixed> Updated notice registry.
     */
    public function showHelpNotices(array $notices): array
    {
        $severityCounts = [
            'low' => 0,
            'medium' => 0,
            'high' => 0,
            'critical' => 0,
        ];
        $dashboardThreshold = rsssl_get_option('vulnerability_notification_dashboard');
        $sitewideThreshold = rsssl_get_option('vulnerability_notification_sitewide');

        $severities = $this->vulnerabilityStorageRepository->getComponentCountPerHighestSeverity();
        $timeStamp = time();
        foreach ($severities as $severity => $count) {
            $normalizedSeverity = strtolower($severity);
            $uniqueCode = md5($severity . $timeStamp);
            $title = $this->getWarningString($normalizedSeverity, $count);
            if ($severity === '') {
                continue;
            }

            $siteWide = false;
            $normalizedSeverity = strtolower($severity);
            if (!isset($severityCounts[$normalizedSeverity])) {
                continue;
            }
            $dashboardNotice = false;

            // Convert severity labels to comparable numeric scores.
            $severityScore = ConfigurableSeverityPolicy::SEVERITY_SCORES[strtolower($severity)] ?? 0;
            $dashboardScore = ConfigurableSeverityPolicy::SEVERITY_SCORES[strtolower((string) $dashboardThreshold)] ?? 0;
            $sitewideScore = ConfigurableSeverityPolicy::SEVERITY_SCORES[strtolower((string) $sitewideThreshold)] ?? 0;

            if ($dashboardThreshold && $dashboardThreshold !== '*' && $severityScore >= $dashboardScore) {
                $dashboardNotice = true;
            }

            if ($sitewideThreshold && $sitewideThreshold !== '*' && $severityScore >= $sitewideScore) {
                $siteWide = true;
            }
            if (!$dashboardNotice && !$siteWide) {
                continue;
            }

            $notices['risk_level_' . $normalizedSeverity . $uniqueCode] = [
                'callback'          => '_true_',
                'score'             => 30,
                'show_with_options' => ['enable_vulnerability_scanner'],
                'output'            => [
                    'true' => [
                        'title'             => $title,
                        'msg'               => $title . ' ' . __(
                                'Please take appropriate action.',
                                'really-simple-ssl'
                            ),
                        'icon'              => ($normalizedSeverity === 'critical' || $normalizedSeverity === 'high') ? 'warning' : 'open',
                        'type'              => 'warning',
                        'dismissible'       => true,
                        'admin_notice'      => $siteWide,
                        'plusone'           => true,
                        'highlight_field_id' => 'vulnerabilities-overview',
                    ],
                ],
            ];
        }

        return $notices;
    }

    /**
     * Builds the translated notice title for a given severity level and count.
     *
     * Uses WordPress pluralization (`_n`) to return a human-friendly message.
     *
     * @param non-empty-string $severity Normalized severity key (low|medium|high|critical).
     * @param positive-int $count Number of components with that severity as highest.
     */
    private function getWarningString(string $severity, int $count): string
    {
        switch ($severity) {
            case 'critical':
                $warning = sprintf(_n(
                    'You have %s critical vulnerability',
                    'You have %s critical vulnerabilities',
                    $count,
                    'really-simple-ssl'
                ), $count);
                break;
            case 'high':
                $warning = sprintf(_n(
                    'You have %s high-risk vulnerability',
                    'You have %s high-risk vulnerabilities',
                    $count,
                    'really-simple-ssl'
                ), $count);
                break;
            case 'medium':
                $warning = sprintf(_n(
                    'You have %s medium-risk vulnerability',
                    'You have %s medium-risk vulnerabilities',
                    $count,
                    'really-simple-ssl'
                ), $count);
                break;
            default:
                $warning = sprintf(_n(
                    'You have %s low-risk vulnerability',
                    'You have %s low-risk vulnerabilities',
                    $count,
                    'really-simple-ssl'
                ), $count);
                break;
        }
        return $warning;
    }
}
PK     \    7  Features/Vulnerability/Controllers/PluginController.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\VulnerabilityStorageRepository;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\VulnerabilityPresentationService;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Traits\HasFrontendUrl;
use ReallySimplePlugins\RSS\Core\Interfaces\ControllerInterface;
use ReallySimplePlugins\RSS\Core\Traits\HasViews;

/**
 * Integrates vulnerability indicators into the WordPress Plugins admin screen.
 *
 * This controller is responsible only for UI integration:
 * - It registers admin hooks for the Plugins overview table.
 * - It fetches precomputed vulnerability data from repositories.
 * - It delegates formatting and labels to presentation services.
 *
 * No business logic or persistence is handled here.
 */
final class PluginController implements ControllerInterface
{
    use HasViews;
    use HasFrontendUrl;

    private VulnerabilityStorageRepository $vulnerabilityStorageRepository;

    private VulnerabilityPresentationService $presentationService;

    public function __construct(
        VulnerabilityStorageRepository $vulnerabilityStorageRepository,
        VulnerabilityPresentationService $presentationService
    ) {
        $this->vulnerabilityStorageRepository = $vulnerabilityStorageRepository;
        $this->presentationService = $presentationService;
    }

    /**
     * Registers WordPress admin hooks for extending the Plugins overview table.
     *
     * Hooks registered:
     * - 'manage_plugins_columns' to add a new column for vulnerabilities.
     * - 'manage_plugins_custom_column' to render content in the custom column.
     *
     * This method is intended to be called once during plugin bootstrapping.
     */
    public function register(): void
    {
        add_filter('manage_plugins_columns', [$this, 'addVulnerabilityColumn']);
        add_action('manage_plugins_custom_column', [$this, 'renderVulnerabilityColumn'], 10, 2);
        add_filter('manage_plugins-network_columns', [$this, 'addVulnerabilityColumn']);
        add_action('manage_plugins-network_custom_column', [$this, 'renderVulnerabilityColumn'], 10, 2);
    }

    /**
     * Adds a new column to the Plugins table in the WordPress admin.
     *
     * The returned array represents the column headers, where the key is the column slug
     * and the value is the displayed column title.
     *
     * @param array<string, string> $columns Existing columns keyed by slug.
     * @return array<string, string> Modified columns including the vulnerability column.
     */
    public function addVulnerabilityColumn(array $columns): array
    {
        $columns['rsssl_vulnerabilities'] = __('Vulnerabilities', 'really-simple-ssl');

        return $columns;
    }

    /**
     * Renders the vulnerability indicator content for a plugin row in the admin table.
     *
     * This method is called by WordPress for each plugin row when rendering custom columns.
     *
     * @param string $columnName The current column slug being rendered.
     * @param string $pluginFile The plugin file path relative to the plugins directory.
     *                           This is used to determine the plugin slug.
     *
     * Slug normalization is necessary because plugins can be either single PHP files
     * or directories containing multiple files.
     *
     * Output is echoed directly as per WordPress admin table rendering conventions.
     */
    public function renderVulnerabilityColumn(string $columnName, string $pluginFile): void
    {
        if ($columnName !== 'rsssl_vulnerabilities') {
            return;
        }

        // Normalize slug: plugins can be directories or single files, handle both cases.
        $slug = dirname($pluginFile);
        if ($slug === '.' || $slug === '/') {
            $slug = basename($pluginFile, '.php');
        }
        $slug = strtolower($slug);

        $highestSeverity = $this->vulnerabilityStorageRepository->getHighestSeverityForPluginSlug($slug);

        if ($highestSeverity === null) {
            echo '';

            return;
        }

        $label = $this->presentationService->getLabelForSeverity($highestSeverity->severity);

        echo $this->view('features/vulnerability/plugin-column', [
            'severity' => $highestSeverity->severity,
            'label' => $label,
            'frontendUrl' => $this->getFrontendUrl(
                'plugin',
                $slug,
                $highestSeverity->lookup
            ),
        ]);
    }
}
PK     \Tz*  *  B  Features/Vulnerability/Controllers/VulnerabilityDataController.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\CoreRepository;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\PluginRepository;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\ThemeRepository;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\VulnerabilityStorageRepository;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\VulnerabilityEmailService;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Support\Helpers\VulnerabilityConfig;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Traits\HasFrontendUrl;
use ReallySimplePlugins\RSS\Core\Interfaces\ControllerInterface;
use ReallySimplePlugins\RSS\Core\Interfaces\DoActionInterface;
use ReallySimplePlugins\RSS\Core\Traits\HasViews;

/**
 * Routes vulnerability-related data actions for the admin UI.
 *
 * Notes: Read-only; delegates data access to repositories and delivery to services.
 */
final class VulnerabilityDataController implements ControllerInterface, DoActionInterface
{
    use HasViews;
    use HasFrontendUrl;

    /**
     * Handles the 'rsssl_do_action' action for vulnerability-related data requests.
     * this fetches vulnerability data for plugins and themes. within the admin interface.
     */
    private const SCAN_FILES_ACTION = 'vulnerabilities_scan_files';

    /**
     * Handles the 'rsssl_do_action' action for vulnerability dashboard data requests.
     * this fetches summary data for the vulnerability dashboard within the admin interface.
     */
    private const DASHBOARD_DATA_ACTION = 'vulnerability_dashboard_data';

    /**
     * Handles the 'rsssl_do_action' action for sending a test vulnerability notification.
     * this triggers sending a test email to verify notification settings.
     */
    private const TEST_NOTIFICATION_ACTION = 'vulnerabilities_test_notification';

    private VulnerabilityStorageRepository $vulnerabilityStorageRepository;
    private VulnerabilityConfig $vulnerabilityConfig;
    private PluginRepository $pluginRepository;
    private ThemeRepository $themeRepository;
    private CoreRepository $coreRepository;
    private VulnerabilityEmailService $vulnerabilityEmailService;

    public function __construct(
        VulnerabilityStorageRepository $vulnerabilityStorageRepository,
        VulnerabilityConfig $vulnerabilityConfig,
        PluginRepository $pluginRepository,
        ThemeRepository $themeRepository,
        CoreRepository $coreRepository,
        VulnerabilityEmailService $vulnerabilityEmailService
    )
    {
        $this->vulnerabilityStorageRepository = $vulnerabilityStorageRepository;
        $this->pluginRepository = $pluginRepository;
        $this->themeRepository = $themeRepository;
        $this->vulnerabilityConfig = $vulnerabilityConfig;
        $this->vulnerabilityEmailService = $vulnerabilityEmailService;
        $this->coreRepository = $coreRepository;
    }

    /**
     * Only register the filter for general actions. Specific actions are
     * handled in {@see rssslDoAction()} and {@see DoActionInterface}.
     *
     */
    public function register(): void
    {
        add_filter('rsssl_do_action', [$this, 'rssslDoAction'], 10, 3);
    }

    /**
     * @inheritDoc
     */
    public function rssslDoAction(array $response, string $action, $data): array
    {
        switch ($action) {
            case self::SCAN_FILES_ACTION:
                return $this->getVulnerabilityOverviewData();
            case self::DASHBOARD_DATA_ACTION:
                return $this->getVulnerabilityDashboardData();
            case self::TEST_NOTIFICATION_ACTION:
                return $this->sendTestNotification($data);
            default:
                return $response;
        }
    }

    /**
     * Build vulnerability overview data for the admin UI.
     *
     * For each installed plugin, theme, and WordPress core component, this collects the highest-severity
     * vulnerability (if any) from storage, enriches it with runtime update availability, and adds
     * derived fields for display (no HTML).
     *
     * @return array{
     *   request_success: bool,
     *   data: list<array<string, mixed>>,
     * }
     */
    private function getVulnerabilityOverviewData(): array
    {
        $highVulnerabilities = [];
        $allPluginsInstalled = $this->pluginRepository->getInstalledComponents();
        $allThemesInstalled = $this->themeRepository->getInstalledComponents();
        $coreInstalled = $this->coreRepository->getInstalledComponents();

        foreach ($allPluginsInstalled as $plugin) {
            $foundPlugin = $this->vulnerabilityStorageRepository
                ->getHighestSeverityForPluginSlug($plugin->getSlug());
            if ($foundPlugin !== null) {
                $foundPluginArray = $foundPlugin->toArray();
                $foundPluginArray['update_available'] = $plugin->hasUpdate();
                $highVulnerabilities[] = $foundPluginArray;
            }
        }

        foreach ($allThemesInstalled as $theme) {
            $foundTheme = $this->vulnerabilityStorageRepository
                ->getHighestSeverityForThemeSlug($theme->getSlug());
            if ($foundTheme !== null) {
                $foundThemeArray = $foundTheme->toArray();
                $foundThemeArray['update_available'] = $theme->hasUpdate();
                $highVulnerabilities[] = $foundThemeArray;
            }
        }

        foreach ($coreInstalled as $core) {
            $foundCore = $this->vulnerabilityStorageRepository
                ->getHighestSeverityForCoreSlug($core->getSlug());
            if ($foundCore !== null) {
                $foundCoreArray = $foundCore->toArray();
                $foundCoreArray['update_available'] = $core->hasUpdate();
                $highVulnerabilities[] = $foundCoreArray;
            }
        }

        $highVulnerabilities = $this->enrichVulnerabilityRows($highVulnerabilities);

        return [
            'request_success' => true,
            'data' => $highVulnerabilities,
        ];
    }

    /**
     * Build dashboard metrics for the admin UI.
     *
     * Metrics include:
     * - updatableComponents: number of installed plugins/themes with an update available.
     * - vulnerablePlugins: number of stored components that currently have vulnerabilities.
     * - highestSeverity: highest severity across all stored components (or "none").
     * - severityScore: numeric rank for the highest severity.
     *
     * @return array{
     *   request_success: bool,
     *   data: array{
     *     updatableComponents: int,
     *     vulnerablePlugins: int,
     *     highestSeverity: string,
     *     severityScore: int
     *   }
     * }
     */
    private function getVulnerabilityDashboardData(): array
    {
        $updatable = $this->getUpdatableComponentsCount();

        $highestSeverity = $this->vulnerabilityStorageRepository->getHighestSeverity();
        if (empty($highestSeverity)) {
            $highestSeverity = 'none';
        }

        $data = [
            'updatableComponents' => $updatable,
            'vulnerablePlugins' => count($this->vulnerabilityStorageRepository->getAllVulnerableComponentsRaw()),
            'highestSeverity' => $highestSeverity ?? '',
            'severityScore' => $this->vulnerabilityStorageRepository->getSeverityScore($highestSeverity),
        ];

        return [
            'request_success' => true,
            'data' => $data,
        ];
    }

    /**
     * Prepare for sending a test vulnerability notification email.
     *
     * Sets a unique option to track the test email and clears existing admin notices.
     */
    private function prepareTestNotification(): void
    {
        $randomString = md5((string)time());
        update_option('test_vulnerability_tester', $randomString, false);
        delete_option('rsssl_admin_notices');
    }

    /**
     * Send a test vulnerability notification email.
     *
     * @param mixed $data Payload from the admin action request (currently unused).
     *
     * @return array<string, mixed> Response payload for the admin UI.
     */
    private function sendTestNotification($data): array
    {
        $this->prepareTestNotification();
        try {
            $emailAddress = $this->vulnerabilityEmailService->getNotificationsEmail();
            $this->vulnerabilityEmailService->setEmail($emailAddress);
            return $this->vulnerabilityEmailService->sendTestEmail();
        } catch (\Throwable $e) {
            return [
                'request_success' => false,
                'message' => __('Unable to send test notification at this time.', 'rsp'),
                'error' => $e->getMessage(),
            ];
        }
    }

    /**
     * Enrich vulnerability rows with derived fields for display (no HTML).
     *
     * Validates required keys before accessing them to avoid runtime notices
     * and to keep the data contract explicit.
     *
     * @param list<array<string, mixed>> $rows
     *
     * @return list<array<string, mixed>>
     */
    private function enrichVulnerabilityRows(array $rows): array
    {
        foreach ($rows as $index => $row) {
            if (!isset(
                $row['type'],
                $row['slug'],
                $row['vulnerability'],
                $row['vulnerability']['lookup'],
                $row['vulnerability']['published_at']
            )) {
                // Skip rows that do not match the expected structure
                continue;
            }

            $publishedAt = $row['vulnerability']['published_at'];
            $timestamp = strtotime((string)$publishedAt);
            if ($timestamp !== false) {
                $row['published_date_human'] = date('F d, Y', $timestamp);
            }

            $row['details_url'] = $this->getFrontendUrl(
                (string)$row['type'],
                (string)$row['slug'],
                (string)$row['vulnerability']['lookup']
            );

            $rows[$index] = $row;
        }

        return $rows;
    }

    /**
     * Count installed components (plugins and themes) that have an update available.
     *
     * Centralizes update availability logic to avoid duplicated loops
     * and keep dashboard calculations consistent.
     */
    private function getUpdatableComponentsCount(): int
    {
        $count = 0;

        $plugins = $this->pluginRepository->getInstalledComponents();
        foreach ($plugins as $plugin) {
            if ($plugin->hasUpdate()) {
                $count++;
            }
        }

        $themes = $this->themeRepository->getInstalledComponents();
        foreach ($themes as $theme) {
            if ($theme->hasUpdate()) {
                $count++;
            }
        }

        return $count;
    }
}
PK     \ˋ'  '  6  Features/Vulnerability/Controllers/ThemeController.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\VulnerabilityStorageRepository;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\VulnerabilityPresentationService;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Traits\HasFrontendUrl;
use ReallySimplePlugins\RSS\Core\Interfaces\ControllerInterface;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig;

/**
 * Integrates vulnerability indicators into the WordPress Themes admin screen.
 *
 * This controller is responsible only for UI integration:
 * - It enqueues the Themes overview script on the correct admin screen.
 * - It exposes a pre-shaped payload via `wp_localize_script()` for that script.
 * - It delegates vulnerability data access to repositories and presentation details
 *   (labels, messaging) to dedicated services.
 *
 * No business logic (severity calculation, storage writes) is performed here.
 */
final class ThemeController implements ControllerInterface
{
    use HasFrontendUrl;

    private VulnerabilityStorageRepository $vulnerabilityStorageRepository;
    private VulnerabilityPresentationService $presentationService;
    private EnvironmentConfig $env;

    public function __construct(
        VulnerabilityStorageRepository $vulnerabilityStorageRepository,
        VulnerabilityPresentationService $presentationService,
        EnvironmentConfig $env
    ) {
        $this->vulnerabilityStorageRepository = $vulnerabilityStorageRepository;
        $this->presentationService = $presentationService;
        $this->env = $env;
    }

    /**
     * Registers WordPress admin hooks for the Themes overview integration.
     *
     * This is called once during plugin boot by the controller manager.
     */
    public function register(): void
    {
        add_action('admin_enqueue_scripts', [$this, 'enqueueThemeAssets']);
    }

    /**
     * Enqueues the Themes overview script and attaches localized vulnerability data.
     *
     * WordPress passes the current admin page hook suffix as `$hook`. We only
     * enqueue and localize on the Themes overview screen (`themes.php`).
     *
     * @param string $hook Current admin page hook suffix.
     */
    public function enqueueThemeAssets(string $hook): void
    {
        if (!$this->isThemeOverviewScreen($hook)) {
            return;
        }

        $assetsBasePath = trailingslashit($this->env->getString('core.assets_path'));
        $assetsBaseUrl = trailingslashit($this->env->getString('core.assets_url'));
        $version = $this->env->get('plugin.version');

        $jsUrl = $assetsBaseUrl . 'js/rsssl-theme-vulnerabilities.js';
        $jsPath = $assetsBasePath . 'js/rsssl-theme-vulnerabilities.js';

        if (!file_exists($jsPath)) {
            return;
        }

        $handle = 'rsssl-theme-vulnerabilities';

        wp_enqueue_script(
            $handle,
            $jsUrl,
            [],
            $version,
            true
        );

        $this->outputThemeVulnerabilityData($handle);
    }

    /**
     * Determine whether the current admin request targets a Themes overview screen.
     *
     * Supports both regular admin and multisite network-admin theme pages by
     * checking the passed hook suffix first and falling back to `get_current_screen()`.
     */
    private function isThemeOverviewScreen(string $hook): bool
    {
        if (in_array($hook, ['themes.php', 'themes-network.php'], true)) {
            return true;
        }

        if (!function_exists('get_current_screen')) {
            return false;
        }

        $screen = get_current_screen();
        if ($screen === null) {
            return false;
        }

        return in_array((string) $screen->base, ['themes', 'themes-network'], true);
    }

    /**
     * Localizes vulnerable theme data for the Themes overview script.
     *
     * The payload is exposed to JavaScript under the global `rssslVulnerabilities`
     * object (WordPress convention via `wp_localize_script()`).
     *
     * @param string $handle The script handle to attach the localized payload to.
     */
    public function outputThemeVulnerabilityData(string $handle): void
    {
        $themes = $this->buildThemePayload();

        wp_localize_script(
            $handle,
            'rssslVulnerabilities',
            [
                'themes' => $themes,
            ]
        );
    }

    /**
     * Builds the Themes overview payload (vulnerable themes only).
     *
     * @return list<array{
     *   slug: non-empty-string,
     *   severity: non-empty-string,
     *   label: string,
     *   info: string,
     *   url: string
     * }>
     */
    private function buildThemePayload(): array
    {
        $allComponents = $this->vulnerabilityStorageRepository->getAllRaw();

        if ($allComponents === []) {
            return [];
        }

        $themes = [];

        foreach ($allComponents as $allComponent) {
            if (!is_array($allComponent)) {
                continue;
            }

            if (strtolower((string)($allComponent['type'] ?? '')) !== 'theme') {
                continue;
            }

            $slug = (string)($allComponent['slug'] ?? '');
            if ($slug === '') {
                continue;
            }

            $highest = $this->vulnerabilityStorageRepository->getHighestSeverityForThemeSlug($slug);
            if ($highest === null) {
                continue;
            }

            $severity = $highest->severity;
            $label = $this->presentationService->getLabelForSeverity($severity);

            $themes[] = [
                'slug' => $slug,
                'severity' => $severity,
                'label' => $label,
                'info' => __('Really Simple Security detected a vulnerability in this theme', 'really-simple-ssl'),
                'url' => $this->getFrontendUrl(
                    'theme',
                    $slug,
                    $highest->lookup
                ),
            ];
        }

        return $themes;
    }
}
PK     \<d    J  Features/Vulnerability/Controllers/VulnerabilityNotificationController.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\VulnerabilityAfterSyncService;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\VulnerabilityController;
use ReallySimplePlugins\RSS\Core\Interfaces\ControllerInterface;
use ReallySimplePlugins\RSS\Core\Traits\HasScheduler;

/**
 * Orchestrates vulnerability re-sync triggers and notification decisions.
 *
 * Constraints: Delegates syncing, snapshot building, and email delivery to services and repositories.
 */
final class VulnerabilityNotificationController implements ControllerInterface
{
    use HasScheduler;


    /**
     * Debounce window (in seconds) for scheduling sync runs.
     */
    private const SYNC_DEBOUNCE_SECONDS = 300; // 5 minutes

    public const AFTER_SYNC_COMPLETED_ACTION = 'rsssl_vulnerability_after_sync_completed';
    private const SCHEDULED_AFTER_SYNC_ACTION = 'rsssl_vulnerability_run_after_sync';
    private VulnerabilityAfterSyncService $service;

    public function __construct(VulnerabilityAfterSyncService $service)
    {
        $this->service = $service;
    }

    public function register(): void
    {
        add_action(VulnerabilityController::SYNC_COMPLETED_ACTION, [$this, 'schedule'], 10, 0);
        add_action(self::SCHEDULED_AFTER_SYNC_ACTION, [$this, 'runAfterSync'], 10, 0);
        add_action('rsssl_daily_cron', [$this, 'schedule']);
    }

    /**
     * Schedule the after-sync notification decision process.
     *
     * @return void
     */
    public function schedule(): void
    {
        $this->scheduleDebounced(
            self::SCHEDULED_AFTER_SYNC_ACTION,
            self::SYNC_DEBOUNCE_SECONDS,
            []
        );
    }

    /**
     * Run the after-sync notification decision process.
     *
     * @return void
     */
    public function runAfterSync(): void
    {
        $this->service->run();
        // Release the debounce lock after processing.
        $this->releaseDebounceLock(self::SCHEDULED_AFTER_SYNC_ACTION);
        // Signal that the after-sync process has completed.
        do_action(self::AFTER_SYNC_COMPLETED_ACTION);
    }
}
PK     \y    ;  Features/Vulnerability/Dtos/ComponentVulnerabilitiesDto.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos;

use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;

/**
 * Immutable-style data transfer object representing all vulnerabilities
 * for a single installed component.
 *
 * A component can be a plugin, theme, or WordPress core. This DTO groups:
 * - Component identity (name, slug, type).
 * - Known vulnerability ranges affecting this component.
 * - Runtime-enriched data such as installed version, update availability,
 *   and latest known version.
 *
 * It is primarily used to:
 * - Transport vulnerability data between API, storage, and domain layers.
 * - Enrich vulnerability data with local installation context.
 *
 * This object holds state only:
 * - No persistence logic
 * - No policy or decision logic
 * - No scheduling or WordPress hook concerns
 */
final class ComponentVulnerabilitiesDto
{
    /**
     * Human-readable component name.
     */
    private string $name;

    /**
     * Unique component identifier used for lookups.
     */
    private string $slug;

    /**
     * Component type (`plugin`, `theme`, or `core`).
     */
    private string $type;

    /**
     * Latest known available version (null if unknown).
     */
    private ?string $latestVersion = null;

    /**
     * List of vulnerability ranges affecting this component.
     *
     * @var list<VulnerabilityRangeDto>
     */
    public array $vulnerabilities = [];

    /**
     * Currently installed version on the site.
     */
    public string $installedVersion = '';

    /**
     * Whether an update is available for the installed version.
     */
    private ?bool $updateAvailable = null;

    /**
     * Constructor.
     *
     * @param string $name Component name from the API.
     * @param string $slug Component slug from the API.
     * @param string $type Component type from the API.
     * @param list<VulnerabilityRangeDto> $vulnerabilities Vulnerability ranges from the API.
     *
     * Runtime fields like installed version and update availability
     * are populated later via setters.
     */
    public function __construct(
        string $name,
        string $slug,
        string $type,
        array $vulnerabilities
    ) {
        $this->vulnerabilities = $vulnerabilities;
        $this->type = $type;
        $this->slug = $slug;
        $this->name = $name;
    }

    /**
     * Normalize a raw API component payload into a strongly typed DTO.
     *
     * Invalid or unexpected vulnerability entries are skipped defensively.
     *
     * @param array<string, mixed> $payload Raw API component data.
     *
     * @return self
     */
    public static function fromApiComponentArray(array $payload): self
    {
        $storage = new Storage($payload);
        $name = $storage->getString('name');
        $slug = $storage->getString('slug');
        $type = strtolower($storage->getString('type'));

        $vulnerabilities = [];

        $rawVulnerabilities = $payload['vulnerabilities'] ?? [];

        if (is_array($rawVulnerabilities)) {
            foreach ($rawVulnerabilities as $rawVulnerability) {
                if (!is_array($rawVulnerability)) {
                    continue;
                }

                $vulnerabilities[] = VulnerabilityRangeDto::fromApiArray($rawVulnerability);
            }
        }

        return new self(
            $name,
            $slug,
            $type,
            $vulnerabilities,
        );
    }

    /**
     * Return a storage/serialization-friendly representation of this component and its vulnerabilities.
     *
     * Nested vulnerabilities are converted using their own DTOs.
     *
     * @return array{
     *     name: string,
     *     slug: string,
     *     type: string,
     *     latestVersion: string|null,
     *     vulnerabilities: list<array<string, mixed>>
     * }
     */
    public function toArray(): array
    {
        $list = [];

        foreach ($this->vulnerabilities as $vulnerability) {
            $list[] = $vulnerability->toArray();
        }

        return [
            'name' => $this->name,
            'slug' => $this->slug,
            'type' => $this->type,
            'latestVersion' => $this->latestVersion,
            'vulnerabilities' => $list,
        ];
    }

    /**
     * Set the latest known available version for this component.
     *
     * Used to enrich the DTO with runtime update information.
     */
    public function setLatestVersion(?string $latestVersion): void
    {
        $this->latestVersion = $latestVersion;
    }

    /**
     * Get a unique storage key for this component.
     *
     * Combines type and slug for indexing.
     */
    public function getStorageKey(): string
    {
        return $this->type . ':' . $this->slug;
    }

    /**
     * Update the component type.
     *
     * Used to enrich the DTO with runtime data.
     */
    public function setType(string $type): void
    {
        $this->type = $type;
    }

    /**
     * Update the component slug.
     *
     * Used to enrich the DTO with runtime data.
     */
    public function setSlug(string $installedSlug): void
    {
        $this->slug = $installedSlug;
    }

    /**
     * Update the component name.
     *
     * Used to enrich the DTO with runtime data.
     */
    public function setName(string $installedName): void
    {
        $this->name = $installedName;
    }

    /**
     * Set the currently installed version on the site.
     *
     * Used to enrich the DTO with runtime installation data.
     */
    public function setInstalledVersion(string $installedVersion): void
    {
        $this->installedVersion = $installedVersion;
    }

    /**
     * Set whether an update is available for the installed version.
     *
     * Used to enrich the DTO with runtime update status.
     */
    public function setUpdateAvailable(?bool $updateAvailable): void
    {
        $this->updateAvailable = $updateAvailable;
    }
}
PK     \I    5  Features/Vulnerability/Dtos/InstalledComponentDto.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos;

/**
 * Immutable data transfer object representing a locally installed WordPress component.
 *
 * A component can be a plugin, theme, or WordPress core. This DTO captures the
 * *runtime installation state* of that component and is primarily used to:
 * - Enrich vulnerability data with local context (installed version, update availability).
 * - Decide whether actions such as updates or notifications are relevant.
 *
 * This object contains state only:
 * - No persistence logic
 * - No vulnerability or policy logic
 * - No WordPress hook or scheduling concerns
 */
final class InstalledComponentDto
{
    /**
     * @var string Component type (`plugin`, `theme`, or `core`)
     */
    private string $type;

    /**
     * @var string Unique identifier used to match vulnerabilities
     */
    public string $slug;

    /**
     * @var string Human-readable component name
     */
    private string $name;

    /**
     * @var string Currently installed version
     */
    private string $installedVersion;

    /**
     * @var string|null Latest available version (null if unknown)
     */
    private ?string $latestVersion;

    /**
     * @var string Plugin file path (empty for themes/core)
     */
    private string $file;

    /**
     * @var bool Whether the component is currently active
     */
    private bool $isActive;

    /**
     * InstalledComponentDto constructor.
     *
     * @param string      $type           The type of the component (plugin, theme, or core)
     * @param string      $slug           Unique identifier for matching vulnerabilities
     * @param string      $name           Human-readable name of the component
     * @param string      $version        Currently installed version of the component
     * @param string|null $pluginFile     Plugin file path, or null if not applicable
     * @param bool        $isActive       Whether the component is currently active
     * @param string|null $latestVersion  Latest available version, or null if unknown
     */
    public function __construct(
        string $type,
        string $slug,
        string $name,
        string $version,
        ?string $pluginFile,
        bool $isActive,
        ?string $latestVersion
    ) {
        $this->type = $type;
        $this->slug = $slug;
        $this->name = $name;
        $this->installedVersion = $version;
        $this->file = $pluginFile ?? '';
        $this->isActive = $isActive;
        $this->latestVersion = $latestVersion;
    }

    /**
     * Get the component type.
     */
    public function getType(): string
    {
        return $this->type;
    }

    /**
     * Get the unique slug identifier.
     */
    public function getSlug(): string
    {
        return $this->slug;
    }

    /**
     * Get the human-readable component name.
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * Get the installed version of the component.
     */
    public function getInstalledVersion(): string
    {
        return $this->installedVersion;
    }

    /**
     * Get the latest available version of the component, or null if unknown.
     */
    public function getLatestVersion(): ?string
    {
        return $this->latestVersion;
    }

    /**
     * Determine if an update is available by comparing installed and latest versions.
     */
    public function hasUpdate(): bool
    {
        if ($this->latestVersion === null) {
            return false;
        }

        return version_compare(
            $this->installedVersion,
            $this->latestVersion,
            '<'
        );
    }

    /**
     * Get the plugin file path (empty string if not applicable).
     */
    public function getFile(): string
    {
        return $this->file;
    }

    /**
     * Check if the component is currently active.
     */
    public function isActive(): bool
    {
        return $this->isActive;
    }
}
PK     \Z    9  Features/Vulnerability/Dtos/HighestSeverityContextDto.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos;

/**
 * Immutable data transfer object representing the highest-severity vulnerability
 * for a specific installed component.
 *
 * This DTO combines vulnerability data with local runtime context, such as:
 * - Which component is affected (plugin, theme, or core).
 * - The highest detected severity for that component.
 * - Whether an update is available for the installed version.
 *
 * It is primarily used to:
 * - Drive admin UI displays (tables, dashboards).
 * - Provide structured input for notification and policy decisions.
 *
 * This object contains data only:
 * - No persistence logic
 * - No policy or decision logic
 * - No WordPress hooks or scheduling concerns
 */
final class HighestSeverityContextDto
{
    /**
     * Normalized severity label (`low`, `medium`, `high`, `critical`).
     *
     * @var string
     */
    public string $severity;

    /**
     * Vulnerability identifier used for correlation and lookups.
     *
     * @var string
     */
    public string $lookup;

    /**
     * Component slug used to match installed components.
     *
     * @var string
     */
    public string $slug;

    /**
     * Component type (`plugin`, `theme`, or `core`).
     *
     * @var string
     */
    public string $type;

    /**
     * Human-readable component name.
     *
     * @var string
     */
    public string $name;

    /**
     * Latest known available version for this component.
     *
     * @var string
     */
    public string $latestVersion;

    /**
     * Whether an update is currently available.
     *
     * @var bool
     */
    public bool $hasUpdate;

    /**
     * Raw vulnerability payload as received from storage/API.
     *
     * @var array<string, mixed>
     */
    public array $vulnerability;

    /**
     * Constructor.
     *
     * @param string $severity Normalized severity label (`low`, `medium`, `high`, `critical`).
     * @param string $lookup Vulnerability identifier used for correlation and lookups.
     * @param string $slug Component slug used to match installed components.
     * @param string $type Component type (`plugin`, `theme`, or `core`).
     * @param string $name Human-readable component name.
     * @param string $latestVersion Latest known available version for this component.
     * @param bool $hasUpdate Whether an update is currently available.
     * @param array<string, mixed> $vulnerability Raw vulnerability payload as received from storage/API.
     */
    public function __construct(
        string $severity,
        string $lookup,
        string $slug,
        string $type,
        string $name,
        string $latestVersion,
        bool $hasUpdate,
        array $vulnerability
    ) {
        $this->vulnerability = $vulnerability;
        $this->hasUpdate = $hasUpdate;
        $this->latestVersion = $latestVersion;
        $this->name = $name;
        $this->type = $type;
        $this->slug = $slug;
        $this->lookup = $lookup;
        $this->severity = $severity;
    }

    /**
     * Return a new instance with an updated hasUpdate flag.
     *
     * The original instance is not modified (immutability).
     *
     * @param bool $hasUpdate New hasUpdate flag value.
     * @return self New instance with updated hasUpdate.
     */
    public function withHasUpdate(bool $hasUpdate): self
    {
        return new self(
            $this->severity,
            $this->lookup,
            $this->slug,
            $this->type,
            $this->name,
            $this->latestVersion,
            $hasUpdate,
            $this->vulnerability
        );
    }

    /**
     * Convert this DTO to an array.
     *
     * The array shape is intended for UI rendering or serialization.
     * The returned structure is stable and explicit.
     *
     * @return array{
     *   severity: string,
     *   lookup: string,
     *   slug: string,
     *   type: string,
     *   name: string,
     *   latestVersion: string,
     *   hasUpdate: bool,
     *   vulnerability: array<string, mixed>
     * }
     */
    public function toArray(): array
    {
        return [
            'severity' => $this->severity,
            'lookup' => $this->lookup,
            'slug' => $this->slug,
            'type' => $this->type,
            'name' => $this->name,
            'latestVersion' => $this->latestVersion,
            'hasUpdate' => $this->hasUpdate,
            'vulnerability' => $this->vulnerability,
        ];
    }
}
PK     \Փ    8  Features/Vulnerability/Dtos/VulnerabilitySnapshotDto.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos;

use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;

/**
 * Immutable data transfer object representing a vulnerability snapshot summary.
 *
 * A snapshot captures the aggregated vulnerability state of the system at a
 * specific point in time. It is used to:
 * - Compare the current state against a previously stored snapshot.
 * - Decide whether notifications should be sent (via policy logic).
 * - Persist historical state for idempotent scheduled runs.
 *
 * This DTO contains data only:
 * - No business logic
 * - No persistence logic
 * - No scheduling concerns
 */
final class VulnerabilitySnapshotDto
{
    /**
     * Highest severity label present in the snapshot (or null if none).
     */
    private ?string $highestSeverity;

    /**
     * Normalized numeric severity score used for comparisons.
     */
    private int $severityScore;

    /**
     * Total number of vulnerable components.
     */
    private int $vulnerableCount;

    /**
     * Number of vulnerable components with updates available.
     */
    private int $updatableComponents;

    /**
     * UNIX timestamp when the snapshot was generated.
     */
    private int $generatedAt;

    public function __construct(
        ?string $highestSeverity,
        int $severityScore,
        int $vulnerableCount,
        int $updatableComponents,
        int $generatedAt
    ) {
        $this->highestSeverity = $highestSeverity;
        $this->severityScore = $severityScore;
        $this->vulnerableCount = $vulnerableCount;
        $this->updatableComponents = $updatableComponents;
        $this->generatedAt = $generatedAt;
    }

    /**
     * Returns the highest severity label present in the snapshot (or null if none).
     */
    public function getHighestSeverity(): ?string
    {
        return $this->highestSeverity;
    }

    /**
     * Returns the normalized numeric severity score used for comparisons.
     */
    public function getSeverityScore(): int
    {
        return $this->severityScore;
    }

    /**
     * Returns the total number of vulnerable components.
     */
    public function getVulnerableCount(): int
    {
        return $this->vulnerableCount;
    }

    /**
     * Returns the number of vulnerable components with updates available.
     */
    public function getUpdatableComponents(): int
    {
        return $this->updatableComponents;
    }

    /**
     * Returns the UNIX timestamp when the snapshot was generated.
     */
    public function getGeneratedAt(): int
    {
        return $this->generatedAt;
    }

    /**
     * Returns an array representation of the snapshot intended for persistence (options/storage).
     *
     * The array shape is stable and versioned implicitly by the DTO.
     *
     * @return array{
     *     highestSeverity: ?string,
     *     severityScore: int,
     *     vulnerableCount: int,
     *     updatableComponents: int,
     *     generatedAt: int
     * }
     */
    public function toArray(): array
    {
        return [
            'highestSeverity' => $this->highestSeverity,
            'severityScore' => $this->severityScore,
            'vulnerableCount' => $this->vulnerableCount,
            'updatableComponents' => $this->updatableComponents,
            'generatedAt' => $this->generatedAt,
        ];
    }

    /**
     * Rebuilds a snapshot DTO from persisted storage.
     *
     * Missing keys are handled defensively via the Storage helper.
     *
     * @param array<string, mixed> $data
     */
    public static function fromArray(array $data): self
    {
        $storage = new Storage($data);
        return new self(
            $storage->getString('highestSeverity'),
            $storage->getInt('severityScore'),
            $storage->getInt('vulnerableCount'),
            $storage->getInt('updatableComponents'),
            $storage->getInt('generatedAt'),
        );
    }
}
PK     \ݓ    5  Features/Vulnerability/Dtos/VulnerabilityRangeDto.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos;

use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;

/**
 * Immutable data transfer object representing a vulnerable version range.
 *
 * A vulnerability range describes *which versions* of a component are affected
 * by a specific vulnerability and how that range should be interpreted.
 *
 * This DTO is used to:
 * - Represent vulnerability range data received from external APIs.
 * - Transport normalized range metadata through the application.
 * - Serialize range data for storage or further processing.
 *
 * This object contains data only:
 * - No business logic
 * - No persistence logic
 * - No version comparison logic
 */
final class VulnerabilityRangeDto
{
    /**
     * Identifier used to match this range to a specific component/version set
     */
    private string $lookup;

    /**
     * Severity label associated with this range
     */
    private string $severity;

    /**
     * Publication date of the vulnerability (raw API value)
     */
    private string $publishedAt;

    /**
     * Whether the vulnerability is fixed within this range
     */
    private bool $fixedIn;

    /**
     * Lower bound of the affected version range
     */
    private string $versionFrom;

    /**
     * Upper bound of the affected version range
     */
    private string $versionTo;

    /**
     * Whether the lower bound is inclusive
     */
    private bool $fromInclusive;

    /**
     * Whether the upper bound is inclusive
     */
    private bool $toInclusive;

    public function __construct(
        string $lookup,
        string $severity,
        string $publishedAt,
        bool $fixedIn,
        string $versionFrom,
        string $versionTo,
        bool $fromInclusive,
        bool $toInclusive
    ) {
        $this->toInclusive = $toInclusive;
        $this->fromInclusive = $fromInclusive;
        $this->versionTo = $versionTo;
        $this->versionFrom = $versionFrom;
        $this->fixedIn = $fixedIn;
        $this->publishedAt = $publishedAt;
        $this->severity = $severity;
        $this->lookup = $lookup;
    }

    /**
     * Builds a range DTO from a raw API payload.
     *
     * This method normalizes raw API payloads into a strongly typed DTO.
     * Missing or malformed values are handled defensively via the Storage helper.
     *
     * @param array<string, mixed> $payload
     */
    public static function fromApiArray(array $payload): self
    {
        $storage = new Storage($payload);

        return new self(
            $storage->getString('lookup'),
            $storage->getString('severity'),
            $storage->getString('published_at'),
            $storage->getBoolean('fixed_in'),
            $storage->getString('version_from'),
            $storage->getString('version_to'),
            $storage->getBoolean('from_inclusive'),
            $storage->getBoolean('to_inclusive'),
        );
    }

    /**
     * @return array{
     *     lookup: string,
     *     severity: string,
     *     published_at: string,
     *     fixed_in: bool,
     *     version_from: string,
     *     version_to: string,
     *     from_inclusive: bool,
     *     to_inclusive: bool
     * }
     */
    public function toArray(): array
    {
        return [
            'lookup' => $this->lookup,
            'severity' => $this->severity,
            'published_at' => $this->publishedAt,
            'fixed_in' => $this->fixedIn,
            'version_from' => $this->versionFrom,
            'version_to' => $this->versionTo,
            'from_inclusive' => $this->fromInclusive,
            'to_inclusive' => $this->toInclusive,
        ];
    }

    /**
     * Returns the severity label associated with this range.
     */
    public function getSeverity(): string
    {
        return $this->severity;
    }

    /**
     * Returns the identifier used to match this range to a specific component/version set.
     */
    public function getLookup(): string
    {
        return $this->lookup;
    }
}
PK     \tm    .  Features/Vulnerability/VulnerabilityLoader.phpnu [        <?php

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability;

use ReallySimplePlugins\RSS\Core\Features\AbstractLoader;
use ReallySimplePlugins\RSS\Core\Managers\FeatureManager;
use ReallySimplePlugins\RSS\Core\Traits\HasAllowlistControl;

/**
 * Determines whether the Vulnerabilities feature should be loaded.
 *
 * @see FeatureManager
 */
class VulnerabilityLoader extends AbstractLoader
{
    use HasAllowlistControl;

    /**
     * @inheritDoc
     */
    public function isEnabled(): bool
    {
        return rsssl_get_option('enable_vulnerability_scanner', false);
    }

    /**
     * @inheritDoc
     */
    public function inScope(): bool
    {
        return $this->adminAccessAllowed();
    }
}
PK     \w      <  Features/Vulnerability/Services/VulnerabilitySyncService.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services;

use ReallySimplePlugins\RSS\Core\Bootstrap\App;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces\ComponentSyncStrategyInterface;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\VulnerabilitySnapshotRepository;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\VulnerabilityStorageRepository;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Strategies\CoreSyncStrategy;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Strategies\PluginSyncStrategy;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Strategies\ThemeSyncStrategy;

/**
 * Synchronize vulnerability data for installed components.
 *
 * High-level flow:
 * 1) Discover installed components via strategy classes (plugin/theme/core).
 * 2) Fetch vulnerability payloads from the API (via the strategies).
 * 3) Convert payloads into DTOs and filter vulnerabilities by installed version.
 * 4) Persist results using repositories.
 *
 * Notes:
 * - This service does not talk to the database directly; it delegates that work to repositories.
 * - Network/API calls are performed by the strategies.
 */
final class VulnerabilitySyncService
{
    /**
     * Strategies used to sync vulnerabilities per component type.
     *
     * @var list<ComponentSyncStrategyInterface>
     */
    private array $strategies = [];

    /** Persists component vulnerability data into storage. */
    private VulnerabilityStorageRepository $vulnerabilityStorageRepository;

    public function __construct(
        VulnerabilityStorageRepository $vulnerabilityStorageRepository
    ) {
        $this->vulnerabilityStorageRepository = $vulnerabilityStorageRepository;
        $this->strategies = $this->buildDefaultStrategies();
    }

    public function setStrategies(iterable $strategies): void
    {
        $this->strategies = $this->filterStrategies($strategies);
    }

    /**
     * @param iterable<ComponentSyncStrategyInterface> $strategies
     *
     * @return list<ComponentSyncStrategyInterface>
     */
    private function filterStrategies(iterable $strategies): array
    {
        $resolved = [];

        foreach ($strategies as $strategy) {
            if ($strategy instanceof ComponentSyncStrategyInterface) {
                $resolved[] = $strategy;
            }
        }

        return $resolved;
    }

    private function buildDefaultStrategies(): array
    {
        $app = App::getInstance();

        $pluginStrategy = $app->get(PluginSyncStrategy::class);

        $themeStrategy = $app->get(ThemeSyncStrategy::class);

        $coreStrategy = $app->get(CoreSyncStrategy::class);

        return [
            $pluginStrategy,
            $themeStrategy,
            $coreStrategy,
        ];
    }

    private function syncInstalledComponentsForStrategy(ComponentSyncStrategyInterface $componentSyncStrategy): void
    {
        foreach ($componentSyncStrategy->getInstalledComponents() as $installedComponentDto) {
            $componentPayload = $this->fetchComponentPayloadForInstalledComponent($componentSyncStrategy, $installedComponentDto);
            if ($componentPayload === null) {
                continue;
            }

            $dto = $componentSyncStrategy->toComponentDto($componentPayload, $installedComponentDto);

            if (!is_array($dto->vulnerabilities)) {
                $dto->vulnerabilities = [];
            }

            $installedVersion = (string) ($dto->installedVersion ?? '');
            if ($installedVersion !== '' && $dto->vulnerabilities !== []) {
                $dto->vulnerabilities = $this->filterVulnerabilitiesForInstalledVersion($installedVersion, $dto->vulnerabilities);
            }

            $this->vulnerabilityStorageRepository->saveComponent($dto);
        }
    }

    /**
     * Fetch the raw component payload for a specific installed component.
     *
     * Returns null when the API call fails or the response is not successful.
     *
     * @return array<string, mixed>|null
     */
    private function fetchComponentPayloadForInstalledComponent(ComponentSyncStrategyInterface $componentSyncStrategy, \ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\InstalledComponentDto $installedComponentDto): ?array
    {
        try {
            $result = $componentSyncStrategy->fetchComponent($installedComponentDto);
        } catch (\Throwable $e) {
            $slug = $installedComponentDto->slug ?? '';
            $context = $slug !== '' ? (' (' . $slug . ')') : '';

            error_log(
                'Vulnerability API request failed for ' . $componentSyncStrategy->getType() . ' component' . $context . ': ' . $e->getMessage()
            );

            return null;
        }

        if (!isset($result['success']) || !$result['success']) {
            return null;
        }

        return $this->extractFirstComponentPayload($result['data'] ?? null);
    }

    /**
     * Extract the component payload from the API response.
     *
     * The API may return either:
     * - a single component array, or
     * - a list of components (we use the first item).
     *
     * @param mixed $data The raw `data` field from the API response.
     * @return array<string, mixed>|null The component payload or null when missing/invalid.
     */
    private function extractFirstComponentPayload($data): ?array
    {
        if (!is_array($data) || $data === []) {
            return null;
        }

        // Support both shapes:
        // 1) Single component array in $data
        // 2) List of components in $data[0]
        $payload = isset($data['slug']) ? $data : ($data[0] ?? null);

        if (!is_array($payload)) {
            return null;
        }

        return $payload;
    }

    /**
     * Filters vulnerability ranges to only those that match the installed version.
     *
     * The API provides version_from/version_to with inclusive flags. A "*" boundary
     * is treated as open-ended. Internally, version_compare() is used to evaluate
     * whether the installed version falls within each vulnerability's range.
     *
     * @param string $installedVersion Currently installed component version.
     * @param array $vulnerabilities List of VulnerabilityRangeDto objects.
     *
     * @return array List of matching VulnerabilityRangeDto objects.
     */
    private function filterVulnerabilitiesForInstalledVersion(string $installedVersion, array $vulnerabilities): array
    {
        $matches = [];

        foreach ($vulnerabilities as $vulnerability) {
            if (!is_object($vulnerability) || !method_exists($vulnerability, 'toArray')) {
                continue;
            }

            /** @var array<string, mixed> $data */
            $data = $vulnerability->toArray();

            $versionFrom = $data['version_from'];
            $versionTo = $data['version_to'];
            $fromInclusive = $data['from_inclusive'];
            $toInclusive = $data['to_inclusive'];

            if ($versionFrom !== '*') {
                $cmpFrom = version_compare($installedVersion, $versionFrom);

                if ($fromInclusive && $cmpFrom < 0) {
                    continue;
                }

                if (!$fromInclusive && $cmpFrom <= 0) {
                    continue;
                }
            }

            if ($versionTo !== '*') {
                $cmpTo = version_compare($installedVersion, $versionTo);

                if ($toInclusive && $cmpTo > 0) {
                    continue;
                }

                if (!$toInclusive && $cmpTo >= 0) {
                    continue;
                }
            }

            $matches[] = $vulnerability;
        }

        return $matches;
    }

    /**
     * Sync vulnerabilities for all supported component types.
     *
     * This clears existing stored components and writes a fresh snapshot.
     */
    public function syncAllComponents(string $trigger = ''): void
    {
        $this->vulnerabilityStorageRepository->deleteAllComponents();

        $this->strategies = $this->filterStrategies($this->strategies);
        if ($this->strategies === []) {
            return;
        }

        foreach ($this->strategies as $strategy) {
            $this->syncInstalledComponentsForStrategy($strategy);
        }
    }
}
PK     \     =  Features/Vulnerability/Services/VulnerabilityEmailService.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\VulnerabilitySnapshotDto;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\VulnerabilityStorageRepository;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\UriConfig;
use ReallySimplePlugins\RSS\Core\Services\EmailService;

/**
 * Builds and sends vulnerability notification emails using the legacy RSSSL mailer.
 *
 * Responsibilities:
 * - Shapes the email content (subject/title/message + per-severity blocks).
 * - Reads vulnerability aggregates from the storage repository.
 *
 * Delivery is delegated to {@see EmailService} which lazy-loads the legacy mailer
 * and provides common helpers (site URL, admin URLs, recipient handling).
 *
 * This service intentionally does not perform scanning or persistence.
 */
final class VulnerabilityEmailService extends EmailService
{
    private VulnerabilityStorageRepository $vulnerabilityStorageRepository;

    /**
     * Constructs the VulnerabilityEmailService.
     *
     * We extend EmailService to reuse legacy mailer initialization and common helpers.
     * Injected dependencies include environment and URI configs plus the vulnerability storage repository.
     */
    public function __construct(
        EnvironmentConfig $environmentConfig,
        UriConfig $uriConfig,
        VulnerabilityStorageRepository $vulnerabilityStorageRepository
    ) {
        parent::__construct($environmentConfig, $uriConfig);
        $this->vulnerabilityStorageRepository = $vulnerabilityStorageRepository;
    }

    /**
     * Sends the vulnerability summary notification email.
     *
     * This composes an email similar to the legacy `send_vulnerability_mail()` flow:
     * - Uses the provided snapshot for overall counts (e.g. total vulnerable components).
     * - Uses the storage repository for severity breakdown (used to build blocks).
     *
     * The legacy mailer expects specific public properties to be set (subject/title/message,
     * button text, and `warning_blocks`). After composition we call `send_mail()`.
     *
     * @return array{success: bool, title?: string, message?: string}
     */
    public function sendVulnerabilityNotification(
        VulnerabilitySnapshotDto $vulnerabilitySnapshotDto
    ): array {
        $total = $vulnerabilitySnapshotDto->getVulnerableCount();
        $blocks = [];
        $rssslmailer = $this->getMailer();

        $rssslmailer->subject = sprintf(
            /* translators: %s is the site url */
            __('Vulnerability Alert: %s', 'really-simple-ssl'),
            $this->siteUrl()
        );

        $rssslmailer->title = sprintf(
            _n(
                '%s: %s vulnerability found',
                '%s: %s vulnerabilities found',
                $total,
                'really-simple-ssl'
            ),
            $this->date(),
            $total
        );

        $rssslmailer->message = sprintf(
            /* translators: %s is a clickable domain */
            __('This is a vulnerability alert from Really Simple Security for %s.', 'really-simple-ssl'),
            $this->domain()
        );

        $rssslmailer->button_text = __('Learn more', 'really-simple-ssl');

        $vulnerabilityCountBySeverity = $this->vulnerabilityStorageRepository->getComponentCountPerHighestSeverity();
        foreach ($vulnerabilityCountBySeverity as $severity => $count) {
            $blocks[] = $this->createBlock($severity, $count);
        }
        $rssslmailer->warning_blocks = $blocks;

        return $rssslmailer->send_mail();
    }

    /**
     * Creates a "warning block" for a single severity bucket.
     *
     * The legacy mailer renders these blocks in the email body.
     *
     * @return array{title: string, message: string, url: string}
     */
    private function createBlock(string $severity, int $count): array
    {
        $title = $this->getWarningString($severity, $count);

        $riskLabel = strtolower(trim($severity));

        $message = $count === 1
            ? sprintf(
                /* translators: 1: severity label */
                __('A %s vulnerability has been found.', 'really-simple-ssl'),
                $riskLabel
            )
            : sprintf(
                /* translators: 1: severity label */
                __('Multiple %s vulnerabilities have been found.', 'really-simple-ssl'),
                $riskLabel
            );

        $message .= ' ' . __('Based on your settings, Really Simple Security will take appropriate action, or you will need to solve it manually.', 'really-simple-ssl');
        $message .= ' ' . sprintf(
            /* translators: %s is a clickable domain */
            __('Get more information from the Really Simple Security dashboard on %s', 'really-simple-ssl'),
            $this->domain()
        );

        return [
            'title' => $title,
            'message' => $message,
            'url' => $this->getVulnerabilitiesSettingsUrl(),
        ];
    }

    /**
     * Returns a translated, pluralized title line for a severity and count.
     */
    private function getWarningString(string $severity, int $count): string
    {
        switch (strtolower(trim($severity))) {
            case 'critical':
                return sprintf(
                    _n('You have %s critical-risk vulnerability', 'You have %s critical-risk vulnerabilities', $count, 'really-simple-ssl'),
                    $count
                );
            case 'high':
                return sprintf(
                    _n('You have %s high-risk vulnerability', 'You have %s high-risk vulnerabilities', $count, 'really-simple-ssl'),
                    $count
                );
            case 'medium':
                return sprintf(
                    _n('You have %s medium-risk vulnerability', 'You have %s medium-risk vulnerabilities', $count, 'really-simple-ssl'),
                    $count
                );
            case 'low':
            default:
                return sprintf(
                    _n('You have %s low-risk vulnerability', 'You have %s low-risk vulnerabilities', $count, 'really-simple-ssl'),
                    $count
                );
        }
    }

    private function date(): string
    {
        return (string) date_i18n(get_option('date_format'));
    }

    /**
     * Returns an HTML anchor tag with the site URL, used by the legacy mailer.
     */
    private function domain(): string
    {
        $url = $this->siteUrl();

        return '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($url) . '</a>';
    }

    private function siteUrl(): string
    {
        $scheme = (function_exists('is_ssl') && is_ssl()) ? 'https' : 'http';
        return (string) get_site_url(null, '', $scheme);
    }

    /**
     * Returns the admin settings URL (hash route) used in email buttons/blocks.
     */
    public function getVulnerabilitiesSettingsUrl(): string
    {
        if (function_exists('rsssl_admin_url')) {
            return (string) rsssl_admin_url([], '#settings/vulnerabilities');
        }

        return (string) admin_url('admin.php?page=really-simple-security#settings/vulnerabilities');
    }

    /**
     * Sends a generic test email via the legacy mailer.
     *
     * Used for diagnostics, delegates to the legacy mailer's test method.
     */
    public function sendTestEmail(): array
    {
        return $this->getMailer()->send_test_mail();
    }
}
PK     \T93  3  G  Features/Vulnerability/Services/Policies/ConfigurableSeverityPolicy.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Policies;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\VulnerabilitySnapshotDto;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces\VulnerabilityNotificationPolicyInterface;
use ReallySimplePlugins\RSS\Core\Services\EmailService;

/**
 * Policy that decides whether a vulnerability notification email should be sent
 * based on a configurable severity threshold.
 *
 * This class represents a *Policy* in the Policy/Strategy sense: it encapsulates
 * a single, well-defined decision rule that can be evaluated independently of
 * when or how notifications are sent.
 *
 * The decision is based on:
 * - The highest severity present in the current vulnerability snapshot
 * - A user-configured severity threshold
 * - Changes compared to the previous snapshot (to avoid redundant notifications)
 *
 * Responsibilities:
 * - Evaluate whether the current state meets or exceeds the configured threshold
 * - Detect meaningful changes between snapshots that justify a new notification
 * - Provide a human-readable reason explaining the decision
 *
 * This policy is intended to be composed by a higher-level notification or
 * scheduling mechanism, not to perform any dispatching itself.
 */
final class ConfigurableSeverityPolicy implements VulnerabilityNotificationPolicyInterface
{
    private EmailService $emailService;
    /**
     * Mapping of severity levels to numeric scores for comparison.
     * @var array<string, int>
     */
    public const SEVERITY_SCORES = [
        'low' => 1,
        'medium' => 2,
        'high' => 3,
        'critical' => 4,
    ];

    /**
     * @var string Reason for the last decision made by shouldSend().
     */
    private string $reason = 'No decision yet';

    /**
     * Reason prefix for searching in error logs.
     * @var string Prefix for the reason message.
     */
    private string $reasonPrefix = 'RSS policy: ';

    public function __construct(EmailService $emailService)
    {
        $this->emailService = $emailService;
    }

    /**
     * Determine if a notification should be sent based on the current and previous snapshots.
     *
     * @param VulnerabilitySnapshotDto $current
     * @param VulnerabilitySnapshotDto|null $previous
     * @return bool Whether a notification should be sent based on the current and previous snapshots.
     */
    public function shouldSend(VulnerabilitySnapshotDto $current, ?VulnerabilitySnapshotDto $previous): bool
    {
        $threshold = '';
        $userWantsNotificationEmail = $this->emailService->isNotificationsEnabled();
        if (!$userWantsNotificationEmail) {
            $this->reason = 'User setting disables emails.';
            return false;
        }

        if (function_exists('rsssl_get_option')) {
            $threshold = (string) rsssl_get_option('vulnerability_notification_email_admin');
        }

        $threshold = strtolower($threshold);

        if ($threshold === '*' || $threshold === '') {
            $this->reason = 'User setting disables emails.';
            return false;
        }

        if ($current->getHighestSeverity() === null) {
            $this->reason = 'No vulnerabilities present.';
            return false;
        }

        $thresholdScore = self::SEVERITY_SCORES[$threshold] ?? 0;
        $currentScore = self::SEVERITY_SCORES[strtolower($current->getHighestSeverity())] ?? 0;

        if ($currentScore < $thresholdScore) {
            $this->reason = 'Highest severity below user threshold.';
            return false;
        }

        if ($previous === null) {
            $this->reason = 'First snapshot and highest severity meets/exceeds threshold.';
            return true;
        }

        $previousGeneratedAt = $previous->getGeneratedAt();
        $currentGeneratedAt  = $current->getGeneratedAt();
        $previousSnapshotOlderThanOneDay = (($currentGeneratedAt - $previousGeneratedAt) >= DAY_IN_SECONDS);

        // Vulnerabilities still exist and no changes were made for one day, send notification
        if ($previousSnapshotOlderThanOneDay) {
            $this->reason = 'Previous snapshot is older than one day.';
            return true;
        }

        $previousScore = 0;
        if ($previous->getHighestSeverity() !== null) {
            $previousScore = self::SEVERITY_SCORES[strtolower($previous->getHighestSeverity())] ?? 0;
        }

        if ($previousScore < $thresholdScore) {
            $this->reason = 'Crossed threshold since previous snapshot.';
            return true;
        }

        if ($this->hasMeaningfulChange($current, $previous)) {
            $this->reason = 'Snapshot changed while still meeting/exceeding threshold.';
            return true;
        }

        $this->reason = 'No meaningful change since previous snapshot.';
        return false;
    }

    /**
     * Determine if the notification can be sent based on admin login status.
     * @return bool Whether the notification can be sent (e.g., admin is logged in).
     *
     * TODO: this is a temporary measure to prevent email sending until we have a more robust solution in place
     * for handling email notifications. In the future, this should be replaced with a more flexible and testable
     * approach that doesn't rely on admin login status.
     *
     * This differs because in specifically this case an admin needs to be logged in.
     */
    public function canSend(): bool
    {
        if (!function_exists('rsssl_admin_logged_in')) {
            return false;
        }

        $emailVerified = $this->emailService->isEmailVerified();
        if (!$emailVerified) {
            $this->reason = 'Email address not verified.';
            return false;
        }

        return (bool) rsssl_admin_logged_in();
    }

    /**
     * Get the reason for the last decision made by shouldSend().
     */
    public function getReason(): string
    {
        return $this->reasonPrefix . $this->reason;
    }

    /**
     * Check if there are meaningful changes between the current and previous snapshots.
     * A meaningful change is defined as a change in highest severity, severity score,
     * number of updatable components, or vulnerable count.
     *
     * @param VulnerabilitySnapshotDto $current
     * @param VulnerabilitySnapshotDto $previous
     * @return bool Whether there are meaningful changes.
     */
    private function hasMeaningfulChange(VulnerabilitySnapshotDto $current, VulnerabilitySnapshotDto $previous): bool
    {
        return (
            $current->getHighestSeverity() !== $previous->getHighestSeverity()
            || $current->getSeverityScore() !== $previous->getSeverityScore()
            || $current->getUpdatableComponents() !== $previous->getUpdatableComponents()
            || $current->getVulnerableCount() !== $previous->getVulnerableCount()
        );
    }
}
PK     \Ru    D  Features/Vulnerability/Services/VulnerabilityPresentationService.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services;

/**
 * Maps vulnerability-related values to human-readable presentation labels.
 *
 * Constraints: Contains presentation logic only; no persistence or business rules.
 */
final class VulnerabilityPresentationService
{
    /**
     * Returns a translated label for a vulnerability severity value.
     */
    public function getLabelForSeverity(string $severity): string
    {

        switch (strtolower($severity)) {
            case 'critical':
                return __('Critical', 'really-simple-ssl');
            case 'high':
                return __('High', 'really-simple-ssl');
            case 'medium':
                return __('Medium', 'really-simple-ssl');
            case 'low':
            default:
                return __('Low', 'really-simple-ssl');
        }
    }
}
PK     \RⅮ    @  Features/Vulnerability/Services/Strategies/ThemeSyncStrategy.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Strategies;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Clients\VulnerabilityClient;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\ThemeRepository;

final class ThemeSyncStrategy extends AbstractComponentSyncStrategy
{
    private ThemeRepository $themeRepository;
    private VulnerabilityClient $vulnerabilityClient;

    public function __construct(
        ThemeRepository $themeRepository,
        VulnerabilityClient $vulnerabilityClient
    ) {
        $this->vulnerabilityClient = $vulnerabilityClient;
        $this->themeRepository = $themeRepository;
    }

    /**
     * @inheritDoc
     */
    public function getType(): string
    {
        return 'theme';
    }

    /**
     * @inheritDoc
     */
    public function getInstalledComponents(): iterable
    {
        return $this->themeRepository->getInstalledComponents();
    }

    /**
     * @inheritDoc
     */
    protected function fetchFromApi(string $slug, string $name, string $version = ''): array
    {
        return $this->vulnerabilityClient->fetchTheme($slug, $name);
    }
}
PK     \V(Uß    ?  Features/Vulnerability/Services/Strategies/CoreSyncStrategy.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Strategies;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Clients\VulnerabilityClient;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\CoreRepository;

final class CoreSyncStrategy extends AbstractComponentSyncStrategy
{
    private CoreRepository $coreRepository;
    private VulnerabilityClient $vulnerabilityClient;

    public function __construct(
        CoreRepository $coreRepository,
        VulnerabilityClient $vulnerabilityClient
    ) {
        $this->vulnerabilityClient = $vulnerabilityClient;
        $this->coreRepository = $coreRepository;
    }

    /**
     * @inheritDoc
     */
    public function getType(): string
    {
        return 'core';
    }

    /**
     * @inheritDoc
     */
    public function getInstalledComponents(): iterable
    {
        return $this->coreRepository->getInstalledComponents();
    }

    /**
     * @inheritDoc
     */
    protected function fetchFromApi(string $slug, string $name, string $version = ''): array
    {
        return $this->vulnerabilityClient->fetchCore($version);
    }
}
PK     \$    A  Features/Vulnerability/Services/Strategies/PluginSyncStrategy.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Strategies;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Clients\VulnerabilityClient;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\PluginRepository;

final class PluginSyncStrategy extends AbstractComponentSyncStrategy
{
    private PluginRepository $pluginRepository;
    private VulnerabilityClient $vulnerabilityClient;

    public function __construct(
        PluginRepository    $pluginRepository,
        VulnerabilityClient $vulnerabilityClient
    )
    {
        $this->vulnerabilityClient = $vulnerabilityClient;
        $this->pluginRepository = $pluginRepository;
    }

    /**
     * @inheritDoc
     */
    public function getType(): string
    {
        return 'plugin';
    }

    /**
     * @inheritDoc
     */
    public function getInstalledComponents(): iterable
    {
        return $this->pluginRepository->getInstalledComponents();
    }

    /**
     * @inheritDoc
     */
    protected function fetchFromApi(string $slug, string $name, string $version = ''): array
    {
        return $this->vulnerabilityClient->fetchPlugin($slug, $name);
    }
}
PK     \],.    L  Features/Vulnerability/Services/Strategies/AbstractComponentSyncStrategy.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Strategies;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\ComponentVulnerabilitiesDto;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\InstalledComponentDto;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces\ComponentSyncStrategyInterface;

/**
 * Abstract base for component sync *Strategies*.
 *
 * This class provides shared mechanics for concrete sync strategies (plugin, theme, core),
 * such as validation, API delegation, and DTO mapping.
 *
 * Strategy context:
 * - Defines *how* vulnerability data is synchronized for a component type.
 * - Concrete subclasses are interchangeable and selected by an orchestrator.
 *
 * Not a Policy:
 * - This class does not decide *whether* or *when* a sync should run.
 * - It does not apply severity thresholds or notification rules.
 * - Those concerns belong to policies and higher-level scheduling/orchestration.
 *
 * Responsibilities:
 * - Validate installed component identifiers
 * - Delegate component-specific API fetching
 * - Map API payloads into ComponentVulnerabilitiesDto
 */
abstract class AbstractComponentSyncStrategy implements ComponentSyncStrategyInterface
{
    /**
     * {@inheritDoc}
     */
    public function fetchComponent(InstalledComponentDto $installedComponentDto): array
    {
        $slug = $installedComponentDto->getSlug();
        $name = $installedComponentDto->getName();
        $version = $installedComponentDto->getInstalledVersion();

        if ($slug === '') {
            return [
                'success' => false,
                'message' => sprintf('Missing %s slug for installed component.', $this->getType()),
            ];
        }

        return $this->fetchFromApi($slug, $name, $version);
    }

    /**
     * {@inheritDoc}
     */
    public function toComponentDto(array $apiPayload, InstalledComponentDto $installedComponentDto): ComponentVulnerabilitiesDto
    {
        $installedSlug = $installedComponentDto->getSlug();
        $installedName = $installedComponentDto->getName();
        $installedVersion = $installedComponentDto->getInstalledVersion();

        $dto = ComponentVulnerabilitiesDto::FromApiComponentArray($apiPayload);

        $dto->setType($this->getType());
        $dto->setSlug($installedSlug);
        $dto->setName($installedName);
        $dto->setInstalledVersion($installedVersion);

        $latestRaw = $apiPayload['latestVersion'] ?? $apiPayload['latest_version'] ?? null;
        $latestVersion = is_string($latestRaw) ? $latestRaw : '';

        $dto->setLatestVersion($latestVersion !== '' ? $latestVersion : null);
        $dto->setUpdateAvailable(
            $latestVersion !== '' && $installedVersion !== '' && version_compare($installedVersion, $latestVersion, '<')
        );

        return $dto;
    }

    /**
     * Perform the component-specific API fetch.
     *
     * Implementations must return the standardized client response shape.
     *
     * @param string $slug The name of the component slug.
     * @param string $name The name of the component.
     * @param string $version The installed version of the component. Only used by core.
     *
     * @return array{success: bool, data?: array<string, mixed>, message?: string}
     */
    abstract protected function fetchFromApi(string $slug, string $name, string $version = ''): array;
}
PK     \t^A1  1  A  Features/Vulnerability/Services/VulnerabilityAfterSyncService.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\VulnerabilitySnapshotRepository;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\VulnerabilityStorageRepository;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Policies\ConfigurableSeverityPolicy;

/**
 * Executes post-sync vulnerability notification logic.
 *
 * This service is scheduled and executed by the core scheduler after a
 * vulnerability sync has completed, and may also be triggered by a
 * daily cron safety net.
 *
 * Responsibilities:
 * - Build a snapshot of current vulnerabilities from storage.
 * - Compare it with the previously stored snapshot.
 * - Decide whether a notification should be sent using the configured policy.
 * - Send the vulnerability notification email when required.
 * - Persist the current snapshot for future comparisons (always).
 *
 * This service contains no scheduling logic and no sync logic.
 * It is safe to run multiple times and is designed to be idempotent.
 */
final class VulnerabilityAfterSyncService
{
    /**
     * Source of persisted vulnerability data
     */
    private VulnerabilityStorageRepository $storage;

    /**
     * Manages snapshot persistence and retrieval
     */
    private VulnerabilitySnapshotRepository $snapshots;

    /**
     * Decides if/when notifications may be sent
     */
    private ConfigurableSeverityPolicy $policy;

    /**
     * Responsible for composing and sending emails
     */
    private VulnerabilityEmailService $email;

    /**
     * VulnerabilityAfterSyncService constructor.
     *
     * Dependencies are injected here; no work is performed during construction.
     *
     * @param VulnerabilityStorageRepository $storage Source of persisted vulnerability data
     * @param VulnerabilitySnapshotRepository $snapshots Manages snapshot persistence and retrieval
     * @param ConfigurableSeverityPolicy $policy Decides if/when notifications may be sent
     * @param VulnerabilityEmailService $email Responsible for composing and sending emails
     */
    public function __construct(
        VulnerabilityStorageRepository $storage,
        VulnerabilitySnapshotRepository $snapshots,
        ConfigurableSeverityPolicy $policy,
        VulnerabilityEmailService $email
    ) {
        $this->storage = $storage;
        $this->snapshots = $snapshots;
        $this->policy = $policy;
        $this->email = $email;
    }

    /**
     * Runs the post-sync notification decision flow.
     *
     * This method is invoked by the scheduler once the vulnerability
     * sync process has completed (or via a scheduled safety-net run).
     *
     * The current vulnerability snapshot is always persisted, even if
     * notification sending fails, to ensure idempotent behavior on
     * subsequent runs.
     *
     * Any exceptions during notification sending are caught and logged
     * to prevent breaking admin or cron execution flows.
     */
    public function run(): void
    {
        $previous = $this->snapshots->getLatest();
        $current = $this->snapshots->buildFromStorage($this->storage);
        try {
            if ($this->policy->canSend() && $this->policy->shouldSend($current, $previous)) {
                $this->email->sendVulnerabilityNotification($current);
            }
        } catch (\Throwable $e) {
            if (function_exists('error_log')) {
                error_log('RSS Vulnerability notification failed: ' . $e->getMessage());
            }
        } finally {
            $this->snapshots->save($current);
        }

        /**
         * Log the reason for the decision in debug mode.
         */
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log($this->policy->getReason());
        }
    }
}
PK     \x    6  Features/Vulnerability/Clients/VulnerabilityClient.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Clients;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Support\Helpers\VulnerabilityConfig;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig;

/**
 * HTTP client for retrieving vulnerability data from the
 * Really Simple Plugins – Vulnerability API.
 *
 * Responsible only for making HTTP requests and normalizing API responses.
 */
final class VulnerabilityClient
{
    private VulnerabilityConfig $vulnerabilityConfig;
    private EnvironmentConfig $env;
    private string $baseUrl;

    public function __construct(
        VulnerabilityConfig $vulnerabilityConfig,
        EnvironmentConfig $environmentConfig
    ) {
        $this->vulnerabilityConfig = $vulnerabilityConfig;
        $this->env = $environmentConfig;
        $this->baseUrl = $this->resolveBaseUrl();
    }

    /**
     * @param string $component Slug of the component (e.g. "wordpress", "contact-form-7").
     * @param string $name Human-readable component name (e.g. "WordPress", "Contact Form 7").
     * @param string $type Component type: "core", "plugin", or "theme".
     * @param string|null $version Optional version for core components; ignored for plugins/themes.
     *
     * @return array{
     *      success: bool,
     *      status: int,
     *      data: array<array-key, mixed>,
     *      message?: string
     *  }
     */
    public function fetchVulnerabilities(
        string $component,
        string $name,
        string $type,
        ?string $version = null
    ): array {
        $url = $this->baseUrl;
        $query = $this->buildQuery($component, $name, $type, $version);

        $url = add_query_arg($query, $url);

        $response = wp_safe_remote_get($url, [
            'timeout' => $this->vulnerabilityConfig->getInt('client.timeout', 10),
            'headers' => [
                'Accept' => 'application/json',
            ],
        ]);

        if (is_wp_error($response)) {
            return [
                'success' => false,
                'status' => 0,
                'data' => [],
                'message' => $response->get_error_message(),
            ];
        }

        $statusCode = (int)wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);

        return $this->normalizeResponse($statusCode, $body);
    }

    /**
     * @return array{success: bool, status: int, data: array<array-key, mixed>, message?: string}
     */
    public function fetchCore(string $version): array
    {
        return $this->fetchVulnerabilities(
            'wordpress',
            'WordPress',
            'core',
            $version
        );
    }

    /**
     * @return array{success: bool, status: int, data: array<array-key, mixed>, message?: string}
     */
    public function fetchPlugin(string $slug, string $name): array
    {
        return $this->fetchVulnerabilities(
            $slug,
            $name,
            'plugin'
        );
    }

    /**
     * @return array{success: bool, status: int, data: array<array-key, mixed>, message?: string}
     */
    public function fetchTheme(string $slug, string $name): array
    {
        return $this->fetchVulnerabilities(
            $slug,
            $name,
            'theme'
        );
    }

    /**
     * Builds the query array for a vulnerability request based on component
     * data and an optional version for core components.
     *
     * @param string $component Component slug.
     * @param string $name Component name.
     * @param string $type Component type.
     * @param string|null $version Optional version for core components.
     *
     * @return array<string, string>
     */
    private function buildQuery(string $component, string $name, string $type, ?string $version): array
    {
        $query = [
            'component[eq]' => $component,
            'name[eq]' => $name,
            'type[eq]' => $type,
        ];

        if ($type === 'core' && $version !== null && $version !== '') {
            $query['version[eq]'] = $version;
        }

        return $query;
    }

    /**
     * Resolves and caches the base URL for vulnerability requests.
     *
     * Priority:
     * 1) client config (base_uri + namespace/version/endpoint)
     * 2) environment config (plugin.url + http.namespace/http.version + client endpoint)
     */
    private function resolveBaseUrl(): string
    {
        $base = rtrim($this->vulnerabilityConfig->getString('client.base_uri'), '/');
        $namespace = trim($this->vulnerabilityConfig->getString('client.namespace'), '/');
        $version = trim($this->vulnerabilityConfig->getString('client.version'), '/');
        $endpoint = trim($this->vulnerabilityConfig->getString('client.endpoint'), '/');

        if ($base !== '' && $namespace !== '' && $version !== '' && $endpoint !== '') {
            return sprintf(
                '%s/wp-json/%s/%s/%s',
                $base,
                $namespace,
                $version,
                $endpoint
            );
        }

        $envBase = rtrim($this->env->getString('plugin.url'), '/');
        $envNamespace = trim($this->env->getString('http.namespace'), '/');
        $envVersion = trim($this->env->getString('http.version'), '/');

        if ($endpoint === '') {
            $endpoint = 'vulnerabilities';
        }

        return sprintf(
            '%s/wp-json/%s/%s/%s',
            $envBase,
            $envNamespace,
            $envVersion,
            $endpoint
        );
    }

    /**
     * Normalizes the decoded API response into a consistent shape.
     *
     * @param int $statusCode HTTP status code from the response.
     * @param string $body Raw response body.
     *
     * @return array{
     *     success: bool,
     *     status: int,
     *     data: array<array-key, mixed>,
     *     message?: string
     * }
     */
    private function normalizeResponse(int $statusCode, string $body): array
    {
        $decoded = json_decode($body, true);

        if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
            return [
                'success' => false,
                'status' => $statusCode,
                'data' => [],
                'message' => 'Invalid JSON response from vulnerability API.',
            ];
        }

        if (isset($decoded['data']) && is_array($decoded['data'])) {
            $apiData = $decoded['data'];
            $message = isset($decoded['message']) ? (string)$decoded['message'] : null;
            $apiStatus = isset($decoded['status']) ? (string)$decoded['status'] : '';

            return [
                'success' => $apiStatus === 'success',
                'status' => $statusCode,
                'data' => $apiData, // In a correct search we only have an array of one.
                'message' => $message,
            ];
        }

        return [
            'success' => false,
            'status' => $statusCode,
            'data' => [],
            'message' => 'Unexpected response format from vulnerability API.',
        ];
    }
}
PK     \a:o  o  2  Features/Vulnerability/VulnerabilityController.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\PluginController;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\ThemeController;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\VulnerabilityDataController;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\VulnerabilityNoticeController;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\VulnerabilityNotificationController;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\VulnerabilitySyncService;
use ReallySimplePlugins\RSS\Core\Interfaces\FeatureInterface;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig;
use ReallySimplePlugins\RSS\Core\Traits\HasScheduler;
use ReallySimplePlugins\RSS\Core\Traits\HasViews;

/**
 * Registers WordPress hooks and coordinates the vulnerability sync lifecycle.
 *
 * Lifecycle (high level):
 * - Schedule a sync when components change (plugin/theme/core updates/activation)
 *   and once per day as a safety net.
 * - Debounce multiple triggers into a single scheduled event.
 * - Run the scheduled job with a lock to avoid overlapping syncs.
 * - Fire a completion action after a successful run.
 *
 */
final class VulnerabilityController implements FeatureInterface
{
    use HasViews;
    use HasScheduler;

    /**
     * Action hook name used to trigger a scheduled vulnerability sync.
     *
     * This hook is scheduled via the VulnerabilityScheduleManager and ultimately
     * results in `scheduleSync()` being executed.
     */
    private const SYNC_EVENT = 'rsssl_vulnerability_run_scheduled_sync';

    /**
     * Action hook fired after a vulnerability sync has completed.
     *
     * Can be used by other parts of the system to react to a finished sync
     * (e.g. logging, notices, follow-up processing).
     */
    public const SYNC_COMPLETED_ACTION = 'rsssl_vulnerability_sync_completed';

    /**
     * Debounce window (in seconds) for scheduling vulnerability syncs.
     *
     * Multiple triggers within this time window will result in a single
     * scheduled sync execution.
     */
    private const SYNC_DEBOUNCE = (5 * MINUTE_IN_SECONDS);

    private VulnerabilitySyncService $vulnerabilitySyncService;
    private EnvironmentConfig $env;

    public function __construct(
        VulnerabilitySyncService $vulnerabilitySyncService,
        EnvironmentConfig $environmentConfig
    ) {
        $this->vulnerabilitySyncService = $vulnerabilitySyncService;
        $this->env = $environmentConfig;
    }

    /**
     * Registers WordPress hooks for the Vulnerability feature.
     *
     * Keeps the feature wiring in one place: enqueueing assets, registering
     * controllers, and scheduling/running vulnerability syncs on relevant events.
     */
    public function register(): void
    {
        add_action('admin_enqueue_scripts', [$this, 'enqueueStyles']);
        add_action(self::SYNC_EVENT, [$this, 'runSync'], 10, 0);
        add_filter('rss_core_controller_classes', [$this, 'registerControllers']);
        add_action('rss_core_activation', [$this, 'scheduleSync'], 10, 1);
        add_action('upgrader_process_complete', [$this, 'scheduleSync'], 10, 2);
        add_action('activate_plugin', [$this, 'scheduleSync'], 10, 2);
        add_action('after_switch_theme', [$this, 'scheduleSync'], 10, 0);
        add_action('_core_updated_successfully', [$this, 'scheduleSync'], 10, 1);
        add_action('rsssl_daily_cron', [$this, 'scheduleSync']);
    }

    /**
     * Registers the controllers related to vulnerability management. This
     * method is hooked into the 'rss_core_controller_classes' filter, that
     * is applied here {@see Plugin::registerControllers}, to make sure the
     * {@see ControllerManager} can register them in the plugin lifecycle.
     */
    public function registerControllers(array $existingControllers): array
    {
        $enablePluginAndThemeDisplay = rsssl_get_option('enable_feedback_in_plugin', false);
        $availableControllers = [
            VulnerabilityDataController::class,
            VulnerabilityNoticeController::class,
            VulnerabilityNotificationController::class,
        ];
        if ($enablePluginAndThemeDisplay) {
            $availableControllers[] = PluginController::class;
            $availableControllers[] = ThemeController::class;
        }
        return array_merge($existingControllers, $availableControllers);
    }

    /**
     * Enqueues shared vulnerability styling on plugins and themes overview pages.
     * we register this here because both the PluginController and ThemeController
     * needs the same styles.
    */
    public function enqueueStyles(string $hook): void
    {
        if (!$this->isComponentOverviewScreen($hook)) {
            return;
        }

        $rtl = is_rtl() ? 'rtl/' : '';
        $assetsUrl  = trailingslashit($this->env->getString('plugin.assets_url'));
        $assetsPath = trailingslashit($this->env->getString('plugin.assets_path'));

        $url  = $assetsUrl . "css/{$rtl}rsssl-plugin.min.css";
        $path = $assetsPath . "css/{$rtl}rsssl-plugin.min.css";
        $version = $this->env->get('plugin.version');
        if (file_exists($path)) {
            wp_enqueue_style('rsssl-plugin', $url, [], $version);
        }
    }

    /**
     * Determine whether the current admin page is a plugin or theme overview screen.
     *
     * Supports both regular admin and multisite network-admin pages.
     */
    private function isComponentOverviewScreen(string $hook): bool
    {
        return in_array($hook, ['plugins.php', 'plugins-network.php', 'themes.php', 'themes-network.php'], true);
    }

    /**
     * Schedule a debounced vulnerability sync.
     *
     * Uses the DebouncedScheduler trait to prevent multiple rapidly fired
     * triggers from scheduling duplicate jobs.
     */
    public function scheduleSync(): void
    {
        $this->scheduleDebounced(
            self::SYNC_EVENT,
            self::SYNC_DEBOUNCE,
            []
        );
    }

    /**
     * Execute the vulnerability sync and release the scheduling lock.
     *
     * After the sync completes, the completion action is fired to allow other
     * components to react.
     */
    public function runSync(): void
    {
        // Run the sync logic (example):
        $this->vulnerabilitySyncService->syncAllComponents();

        // Release the debounce lock so future triggers can schedule again.
        $this->releaseDebounceLock(self::SYNC_EVENT);

        // Notify that the sync has completed.
        do_action(self::SYNC_COMPLETED_ACTION);
    }

}
PK     \#%  %  0  Features/Vulnerability/Traits/HasFrontendUrl.phpnu [        <?php

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Traits;

use ReallySimplePlugins\RSS\Core\Bootstrap\App;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Support\Helpers\VulnerabilityConfig;

trait HasFrontendUrl
{
    /**
     * Returns the public URL for a vulnerability detail page.
     */
    public function getFrontendUrl(
        string $type,
        string $slug,
        string $vulnerabilityUuid
    ): string {
        if ($vulnerabilityUuid === '' || $vulnerabilityUuid === '0') {
            return '';
        }

        $baseUrl = App::getInstance()->get(VulnerabilityConfig::class)->getUrl('client.base_uri');

        return sprintf(
            $baseUrl . '/%s/%s/%s',
            $type,
            $slug,
            $vulnerabilityUuid
        );
    }
}
PK     \S1+  +  6  Features/Vulnerability/Repositories/CoreRepository.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\InstalledComponentDto;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces\InstalledComponentRepositoryInterface;

/**
 * Read-only repository for WordPress core.
 *
 * Purpose:
 * - Exposes the installed WordPress core as an InstalledComponentDto.
 * - Provides the installed version and (when available) the latest version from
 *   WordPress core update information.
 *
 * This repository does not perform remote calls and does not persist anything.
 * It only translates WordPress runtime state into a predictable DTO format.
 */
final class CoreRepository implements InstalledComponentRepositoryInterface
{
    /**
     * Return WordPress core as a list of InstalledComponentDto objects.
     *
     * The returned list contains a single DTO representing the installed WordPress core and includes:
     * - type: "core"
     * - slug: "wordpress"
     * - name: "WordPress"
     * - installed version
     * - active flag: always true (core is always active)
     * - latest available version (when WordPress update information is available)
     *
     * @return list<InstalledComponentDto>
     */
    public function getInstalledComponents(): array
    {
        $core = $this->getNormalizedCore();

        return [
            new InstalledComponentDto(
                'core',
                $core['slug'],
                $core['name'],
                $core['version'],
                '', // No "plugin file" equivalent for core.
                true,
                $core['latestVersion']
            ),
        ];
    }

    /**
     * Build a normalized representation of the installed WordPress core.
     *
     * WordPress core update information is stored in a site transient. When it
     * is present, we extract the latest available version from it.
     *
     * @return array{
     *   slug: string,
     *   name: string,
     *   version: string,
     *   latestVersion: ?string
     * }
     */
    private function getNormalizedCore(): array
    {
        $installedVersion = $this->getInstalledCoreVersion();
        $latestVersion = $this->getLatestCoreVersionFromUpdates();

        return [
            'slug' => 'wordpress',
            'name' => 'WordPress',
            'version' => $installedVersion,
            'latestVersion' => $latestVersion,
        ];
    }

    /**
     * Get the installed WordPress version.
     *
     * @return string Non-empty version string when available, otherwise empty string.
     */
    private function getInstalledCoreVersion(): string
    {
        if (function_exists('get_bloginfo')) {
            $version = get_bloginfo('version');
            return is_string($version) ? $version : '';
        }

        // Fallback for unusual execution contexts.
        /** @var string|null $wp_version */
        global $wp_version;

        return is_string($wp_version) ? $wp_version : '';
    }

    /**
     * Determine the latest available WordPress core version from update information.
     *
     * WordPress stores available core updates in a site transient. The transient
     * contains an object with an `updates` list of update items.
     *
     * @return string|null Latest available version, or null when not available.
     */
    private function getLatestCoreVersionFromUpdates(): ?string
    {
        if (!function_exists('get_core_updates')) {
            require_once ABSPATH . 'wp-admin/includes/update.php';
        }

        $updates = get_core_updates();

        if (!is_array($updates)) {
            return null;
        }

        $latest = null;

        foreach ($updates as $update) {
            if (!is_object($update) || empty($update->current) || !is_string($update->current)) {
                continue;
            }

            $candidate = $update->current;

            if ($latest === null || version_compare($candidate, $latest, '>')) {
                $latest = $candidate;
            }
        }

        return $latest;
    }
}
PK     \Ǩҵ    7  Features/Vulnerability/Repositories/ThemeRepository.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\InstalledComponentDto;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces\InstalledComponentRepositoryInterface;

/**
 * Read-only repository for installed WordPress themes.
 *
 * Purpose:
 * - Enumerates installed themes via WordPress APIs.
 * - Normalizes theme metadata (slug, name, versions, active state).
 * - Exposes each theme as an InstalledComponentDto for the vulnerability sync layer.
 *
 * This repository does not perform remote calls and does not persist anything.
 * It only translates WordPress runtime state into a predictable DTO format.
 */
final class ThemeRepository implements InstalledComponentRepositoryInterface
{

    /**
     * Returns a materialized list of installed themes.
     *
     * @return list<InstalledComponentDto>
     */
    public function getInstalledComponents(): array
    {
        $components = [];

        foreach ($this->getNormalizedThemes() as $normalizedTheme) {
            $components[] = new InstalledComponentDto(
                'theme',
                $normalizedTheme['slug'],
                $normalizedTheme['name'],
                $normalizedTheme['version'],
                '',
                $normalizedTheme['isActive'],
                $normalizedTheme['latestVersion']
            );
        }

        return $components;
    }

    /**
     * Check whether WordPress currently offers an update for a theme.
     *
     * We rely on the `update_themes` site transient, which is refreshed by
     * `wp_update_themes()`.
     *
     * @param string $stylesheet Theme stylesheet slug.
     */
    public function hasThemeUpdateAvailable(string $stylesheet): bool
    {
        $updates = get_site_transient('update_themes');
        if (! is_object($updates) || ! isset($updates->response) || ! is_array($updates->response)) {
            return false;
        }

        return array_key_exists($stylesheet, $updates->response);
    }

    /**
     * Read the currently installed theme version from WordPress.
     *
     * Uses `wp_get_theme($stylesheet)`.
     *
     * @param string $stylesheet Theme stylesheet slug.
     */
    public function getThemeVersion(string $stylesheet): string
    {
        $theme = wp_get_theme($stylesheet);
        if (! $theme->exists()) {
            return '';
        }

        $version = $theme->get('Version');
        return is_string($version) ? $version : '';
    }

    /**
     * Build a normalized list of installed themes from WordPress theme APIs.
     *
     * Notes:
     * - In WordPress, the stylesheet directory name acts as the theme identifier (slug).
     * - A child theme is identified by stylesheet; the parent theme by template.
     * - Update information is taken from the update_themes site transient when available.
     *
     * @return iterable<array{
     *   slug: string,
     *   name: string,
     *   version: string,
     *   isActive: bool,
     *   latestVersion: ?string
     * }>
     */
    private function getNormalizedThemes(): iterable
    {
        $this->ensureThemesApiLoaded();

        /** @var array<string, WP_Theme> $themes */
        $themes = wp_get_themes();

        $activeTheme = wp_get_theme();
        $activeStylesheet = (string) $activeTheme->get_stylesheet();
        $activeTemplate = (string) $activeTheme->get_template();

        foreach ($themes as $stylesheet => $theme) {
            // In WordPress, the stylesheet directory name is the theme slug.
            $slug = sanitize_key((string) $stylesheet);

            $name = (string) $theme->get('Name');
            $version = (string) $theme->get('Version');

            // Child theme is identified by stylesheet; parent by template.
            $isActive = $this->isThemeActive($stylesheet, $activeStylesheet, $activeTemplate);

            $latestVersion = null;

            $updates = get_theme_updates();

            if (is_array($updates) && isset($updates[$stylesheet]) && is_object($updates[$stylesheet])) {
                if (isset($updates[$stylesheet]->update['new_version']) && is_string($updates[$stylesheet]->update['new_version'])) {
                    $latestVersion = $updates[$stylesheet]->update['new_version'];
                }
            }

            yield [
                'slug' => $slug,
                'name' => $name,
                'version' => $version,
                'isActive' => $isActive,
                'latestVersion' => $latestVersion,
            ];
        }
    }

    /**
     * Ensure the WordPress theme API functions are available.
     *
     * Some execution paths may run before wp-admin includes are loaded.
     * This makes wp_get_themes() available in a defensive way.
     */
    private function ensureThemesApiLoaded(): void
    {
        // Defensive load for early/edge WP execution paths.
        if (!function_exists('wp_get_themes')) {
            require_once ABSPATH . 'wp-admin/includes/theme.php';
        }
        if (!function_exists('get_theme_updates')) {
            require_once ABSPATH . 'wp-admin/includes/update.php';
        }
    }

    /**
     * Determine whether a theme is currently active.
     *
     * WordPress identifies the active child theme by stylesheet and the
     * parent theme by template, so we check both.
     */
    private function isThemeActive(string $stylesheet, string $activeStylesheet, string $activeTemplate): bool
    {
        return ($stylesheet === $activeStylesheet || $stylesheet === $activeTemplate);
    }
}
PK     \{^l    8  Features/Vulnerability/Repositories/PluginRepository.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\InstalledComponentDto;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces\InstalledComponentRepositoryInterface;

/**
 * Read-only repository for installed WordPress plugins.
 *
 * Purpose:
 * - Enumerates installed plugins via WordPress APIs.
 * - Normalizes plugin metadata (slug, name, versions, active state).
 * - Exposes each plugin as an InstalledComponentDto for the vulnerability sync layer.
 *
 * This repository does not perform remote calls and does not persist anything.
 * It only translates WordPress runtime state into a predictable DTO format.
 */
final class PluginRepository implements InstalledComponentRepositoryInterface
{
    /**
     * Return all installed plugins as InstalledComponentDto objects.
     *
     * Each DTO in the returned list represents one installed plugin and includes:
     * - type: "plugin"
     * - slug and display name
     * - installed version
     * - plugin file path (WordPress identifier)
     * - whether the plugin is active (including network-active on multisite)
     * - latest available version (when WordPress update information is available)
     *
     * @return list<InstalledComponentDto>
     */
    public function getInstalledComponents(): array
    {
        $components = [];
        foreach ($this->getNormalizedPlugins() as $normalizedPlugin) {
            $data = is_array($normalizedPlugin) ? $normalizedPlugin : (array) $normalizedPlugin;

            $components[] = new InstalledComponentDto(
                'plugin',
                $data['slug'],
                $data['name'],
                $data['version'],
                $data['pluginFile'],
                $data['isActive'],
                $data['latestVersion']
            );
        }
        return $components;
    }

    /**
     * Build a normalized list of installed plugins from WordPress plugin APIs.
     *
     * WordPress returns plugin information in a fairly loose array format.
     * This method validates and normalizes the fields we need so downstream code
     * can rely on consistent keys and types.
     *
     * @return iterable<array{
     *   pluginFile: string,
     *   slug: string,
     *   name: string,
     *   version: string,
     *   isActive: bool,
     *   latestVersion: ?string
     * }>
     */
    private function getNormalizedPlugins(): iterable
    {
        $this->ensurePluginsApiLoaded();

        /** @var array<string, array<string, mixed>> $plugins */
        $plugins = get_plugins();

        /** @var list<string> $activePlugins */
        $activePlugins = (array) get_option('active_plugins', []);

        $updates = get_plugin_updates();
        if (!is_array($updates)) {
            $updates = [];
        }

        foreach ($plugins as $pluginFile => $pluginData) {
            $slug = $this->deriveSlugFromPluginFile($pluginFile);

            $name = isset($pluginData['Name']) && is_string($pluginData['Name'])
                ? $pluginData['Name']
                : $slug;

            $version = isset($pluginData['Version']) && is_string($pluginData['Version'])
                ? $pluginData['Version']
                : '';

            $latestVersion = null;

            if (isset($updates[$pluginFile]) && is_object($updates[$pluginFile]) && isset($updates[$pluginFile]->update)) {
                $update = $updates[$pluginFile]->update;

                if (is_array($update) && isset($update['new_version']) && is_string($update['new_version'])) {
                    $latestVersion = $update['new_version'];
                } elseif (is_object($update) && isset($update->new_version) && is_string($update->new_version)) {
                    $latestVersion = $update->new_version;
                }
            }

            $isActive = in_array($pluginFile, $activePlugins, true);

            // Also treat network-activated plugins as active.
            if (function_exists('is_plugin_active_for_network') && is_multisite()) {
                $isActive = $isActive || is_plugin_active_for_network($pluginFile);
            }

            yield [
                'pluginFile' => $pluginFile,
                'slug' => $slug,
                'name' => $name,
                'version' => $version,
                'isActive' => $isActive,
                'latestVersion' => $latestVersion,
            ];
        }
    }

    /**
     * Check whether WordPress currently offers an update for a plugin.
     *
     * We rely on the `update_plugins` site transient, which is refreshed by
     * `wp_update_plugins()`.
     *
     * @param string $pluginFile Plugin basename (e.g. my-plugin/my-plugin.php).
     */
    public function hasPluginUpdateAvailable(string $pluginFile): bool
    {
        $updates = get_site_transient('update_plugins');
        if (! is_object($updates) || ! isset($updates->response) || ! is_array($updates->response)) {
            return false;
        }

        return array_key_exists($pluginFile, $updates->response);
    }

    /**
     * Read the currently installed plugin version from WordPress.
     *
     * Uses `get_plugins()` from wp-admin/includes/plugin.php.
     *
     * @param string $pluginFile Plugin basename.
     */
    public function getPluginVersion(string $pluginFile): string
    {
        if (! function_exists('get_plugins')) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }

        $plugins = get_plugins();
        if (! is_array($plugins) || ! isset($plugins[$pluginFile]) || ! is_array($plugins[$pluginFile])) {
            return '';
        }

        $version = $plugins[$pluginFile]['Version'] ?? '';
        return is_string($version) ? $version : '';
    }

    /**
     * Ensure the WordPress plugin API functions are available.
     *
     * Some execution paths may run before wp-admin includes are loaded.
     * This makes get_plugins() available in a defensive way.
     */
    private function ensurePluginsApiLoaded(): void
    {
        if (!function_exists('get_plugins')) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }
        if (!function_exists('get_plugin_updates')) {
            require_once ABSPATH . 'wp-admin/includes/update.php';
        }
    }

    /**
     * Derive a WordPress.org-style plugin slug from a plugin file path.
     *
     * Rules:
     * - If the plugin lives in a directory, use that directory name as slug.
     * - If the plugin is a single file in the plugins root, use the filename without ".php".
     * - Always normalize to lowercase to keep slugs predictable.
     *
     * Example:
     * - "contact-form-7/wp-contact-form-7.php" -> "contact-form-7"
     */
    private function deriveSlugFromPluginFile(string $pluginFile): string
    {
        $directory = dirname($pluginFile);

        $slug = $directory !== '.' && $directory !== '/' ? $directory : basename($pluginFile, '.php');

        // Normalize to lowercase for WordPress.org-style slugs.
        return strtolower($slug);
    }
}
PK     \,B  B  F  Features/Vulnerability/Repositories/VulnerabilityStorageRepository.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\ComponentVulnerabilitiesDto;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\HighestSeverityContextDto;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Policies\ConfigurableSeverityPolicy;


/**
 * Persist and read vulnerability data using the WordPress options API.
 *
 * Responsibilities:
 * - Store and retrieve component vulnerability payloads keyed by a storage key
 *   (e.g. "plugin:contact-form-7" or "theme:twentytwenty").
 * - Provide aggregation helpers (highest severity, counts, snapshot).
 *
 * Constraints:
 * - No external I/O (network, files) beyond WordPress options.
 * - No business rules beyond simple aggregation and shaping for consumers.
 *
 * Notes for maintainers:
 * - Stored component format is an array with keys such as: `type`, `slug`,
 *   `name`, `latestVersion`, `hasUpdate`, and `vulnerabilities` \(an array\).
 * - `vulnerabilities` is an array of associative arrays where each entry
 *   typically includes at minimum `severity` and `lookup` (UUID).
 */
final class VulnerabilityStorageRepository
{
    private const OPTION_KEY = 'rss_vulnerabilities';

    /**
     * Store or replace vulnerabilities for a single component.
     *
     * The DTO is converted to an array and stored under the DTO's storage key.
     * This replaces any existing entry for that key.
     *
     * Example storage key: "plugin:contact-form-7"
     *
     * @param ComponentVulnerabilitiesDto $componentVulnerabilitiesDto DTO providing `getStorageKey()` and `toArray()`
     *
     * @return void
     */
    public function saveComponent(ComponentVulnerabilitiesDto $componentVulnerabilitiesDto): void
    {
        $all = $this->getAllRaw();
        $all[$componentVulnerabilitiesDto->getStorageKey()] = $componentVulnerabilitiesDto->toArray();
        update_option(self::OPTION_KEY, $all, false);
    }


    /**
     * Return the raw stored vulnerability map from the WordPress option.
     *
     * Returned shape: array<string, array<string, mixed>>
     * - Key is the storage key (e.g. "plugin:slug")
     * - Value is the component array as stored by `saveComponent`
     *
     * If the option is malformed (not an array) an empty array is returned.
     *
     * @return array<string, array<string, mixed>> Map of storage keys to component arrays
     */
    public function getAllRaw(): array
    {
        $stored = get_option(self::OPTION_KEY, []);
        if (!is_array($stored)) {
            return [];
        }

        return $stored;
    }

    /**
     * Determine the highest severity vulnerability for a component.
     *
     * Behavior:
     * - Looks up the component by its storage key (e.g. "plugin:{slug}").
     * - Returns null when the component does not exist or has no vulnerabilities.
     * - Selects the highest-ranked vulnerability based on severity ranking.
     *
     * The returned DTO contains both component context and the selected
     * vulnerability payload.
     *
     * @param non-empty-string $type Component type (e.g. "plugin", "theme", "core").
     * @param non-empty-string $slug Component slug.
     *
     * @return HighestSeverityContextDto|null
     */
    public function getHighestSeverityForComponent(string $type, string $slug): ?HighestSeverityContextDto
    {
        $all = $this->getAllRaw();
        $key = $this->buildStorageKey($type, $slug);
        if (!isset($all[$key]) || !is_array($all[$key])) {
            return null;
        }

        $component = $all[$key];
        $vulnerabilities = $component['vulnerabilities'] ?? null;

        if (!is_array($vulnerabilities) || $vulnerabilities === []) {
            return null;
        }

        $highestVulnerability = $this->findHighestVulnerability($vulnerabilities);
        if ($highestVulnerability === null) {
            return null;
        }

        return $this->buildHighestSeverityContext($component, $highestVulnerability, $slug, $type);
    }

    /**
     * Determine the highest severity vulnerability for a given plugin slug.
     * Returns a structured array containing the component context and the
     * highest severity vulnerability entry, including the UUID (lookup).
     *
     * @param string $slug
     * @return HighestSeverityContextDto|null
     */
    public function getHighestSeverityForPluginSlug(string $slug): ?HighestSeverityContextDto
    {
        return $this->getHighestSeverityForComponent('plugin', $slug);
    }

    /**
     * Build the storage key for a component based on type and slug.
     *
     * @param string $type
     * @param string $slug
     * @return string
     */
    private function buildStorageKey(string $type, string $slug): string
    {
        $type = strtolower(trim($type));
        $slug = strtolower(trim($slug));

        return $type . ':' . $slug;
    }

    /**
     * Return the numeric rank for a given severity.
     * Unknown severities return 0.
     *
     * @param $severity
     * @return int
     */
    private function severityRank($severity): int
    {
        if (!is_string($severity)) {
            return 0;
        }

        $severity = strtolower(trim($severity));

        return ConfigurableSeverityPolicy::SEVERITY_SCORES[$severity] ?? 0;
    }


    /**
     * Determine the highest severity vulnerability for a given theme slug.
     * Returns a structured array containing the component context and the
     * highest severity vulnerability entry, including the UUID (lookup).
     *
     * @param string $slug
     * @return HighestSeverityContextDto|null
     */
    public function getHighestSeverityForThemeSlug(string $slug): ?HighestSeverityContextDto
    {
        return $this->getHighestSeverityForComponent('theme', $slug);
    }

    /**
     * Determine the highest severity vulnerability for a given core slug.
     *
     * @param non-empty-string $slug Core slug (defaults to "wordpress").
     *
     * @return HighestSeverityContextDto|null
     */
    public function getHighestSeverityForCoreSlug(string $slug = 'wordpress'): ?HighestSeverityContextDto
    {
        return $this->getHighestSeverityForComponent('core', $slug);
    }

    /**
     * Remove vulnerabilities for a given component key.
     *
     *  If the key does not exist this method is a no-op.
     *
     * @param string $storageKey For example "plugin:contact-form-7".
     *
     * @return void
     */
    public function deleteComponent(string $storageKey): void
    {
        $all = $this->getAllRaw();

        if (!array_key_exists($storageKey, $all)) {
            return;
        }

        unset($all[$storageKey]);

        update_option(self::OPTION_KEY, $all, false);
    }

    /**
     * Remove all stored component vulnerabilities.
     *
     * This wipes the option to an empty array instead of deleting the option,
     * which keeps the option present but empty.
     *
     * @return void
     */
    public function deleteAllComponents(): void
    {
        update_option(self::OPTION_KEY, [], false);
    }

    /**
     * Find the single highest severity vulnerability from the provided list.
     *
     * Behaviour:
     * - Accepts an array of vulnerability items (associative arrays).
     * - Uses `ConfigurableSeverityPolicy::SEVERITY_SCORES` to rank severities.
     * - Returns the first encountered item of the highest rank.
     * - Short-circuits and returns immediately if a `critical` is found.
     *
     * Expected vulnerability item shape (minimum):
     * [
     *   'severity' => 'high'|'critical'|...,
     *   'lookup' => 'uuid',
     *   ...other fields...
     * ]
     *
     * @param list<mixed> $vulnerabilities List of vulnerability items
     *
     * @return array<string, mixed>|null The selected vulnerability item, or null when none valid
     */
    private function findHighestVulnerability(array $vulnerabilities): ?array
    {
        $highest = null;
        $highestRank = 0;

        foreach ($vulnerabilities as $vulnerability) {
            if (!is_array($vulnerability)) {
                continue;
            }

            $rank = $this->severityRank($vulnerability['severity'] ?? null);

            if ($rank > $highestRank) {
                $highestRank = $rank;
                $highest = $vulnerability;

                if ($highestRank === (ConfigurableSeverityPolicy::SEVERITY_SCORES['critical'] ?? 9)) {
                    return $highest;
                }
            }
        }

        return $highest;
    }

    /**
     * Build a standardized context array for a highest-severity result.
     *
     * @param array<string, mixed> $component Raw stored component array
     * @param array<string, mixed> $highestVulnerability The selected vulnerability item
     * @param non-empty-string $fallbackSlug Fallback slug used if component has no `slug`
     * @param non-empty-string $fallbackType Fallback type used if component has no `type`
     *
     * @return HighestSeverityContextDto The constructed DTO
     */
    private function buildHighestSeverityContext(
        array  $component,
        array  $highestVulnerability,
        string $fallbackSlug,
        string $fallbackType
    ): HighestSeverityContextDto
    {
        $highestStorage = new Storage($highestVulnerability);
        $componentStorage = new Storage($component);

        return new HighestSeverityContextDto(
            strtolower($highestStorage->getString('severity')),
            $highestStorage->getString('lookup'),
            $componentStorage->getString('slug', $fallbackSlug),
            $componentStorage->getString('type', $fallbackType),
            $componentStorage->getString('name'),
            $componentStorage->getString('latestVersion'),
            $componentStorage->getBoolean('hasUpdate'),
            $highestVulnerability,
        );
    }

    /**
     * Return the numeric score for a textual severity.
     *
     * Unknown severities return 0.
     *
     * @param string $severity textual severity (case-insensitive)
     *
     * @return int numeric score from `ConfigurableSeverityPolicy::SEVERITY_SCORES` or 0
     */
    public function getSeverityScore(string $severity): int
    {
        return $this->severityRank($severity);
    }

    /**
     * Determine the highest severity level across all stored components.
     *
     * Behaviour:
     * - Iterates all stored components and their vulnerabilities.
     * - Uses `ConfigurableSeverityPolicy::SEVERITY_SCORES` to rank severities.
     * - Returns the highest severity found as a string (low|medium|high|critical).
     * - If no vulnerabilities exist, returns null.
     *
     * @return string|null Highest severity level found, or null when none exist
     */
    public function getHighestSeverity(): ?string
    {
        $all = $this->getAllRaw();

        if ($all === []) {
            return null;
        }

        $highest = 0;
        $highestSeverity = null;

        foreach ($all as $component) {
            if (!is_array($component)) {
                continue;
            }

            $vulnerabilities = $component['vulnerabilities'] ?? null;
            if (!is_array($vulnerabilities) || $vulnerabilities === []) {
                continue;
            }

            $highestVulnerability = $this->findHighestVulnerability($vulnerabilities);
            if ($highestVulnerability === null) {
                continue;
            }

            $severity = strtolower((string)($highestVulnerability['severity'] ?? ''));
            $rank = $this->severityRank($severity);

            if ($rank > $highest) {
                $highest = $rank;
                $highestSeverity = $severity;

                if ($highest === ConfigurableSeverityPolicy::SEVERITY_SCORES['critical']) {
                    return 'critical';
                }
            }
        }

        return $highestSeverity;
    }

    /**
     * Return snapshot data useful for dashboards or metrics.
     *
     * Returned array shape:
     * - highestSeverity: ?string (highest severity across components or null)
     * - severityScore: int (numeric score for highestSeverity or 0)
     * - vulnerablePlugins: int (count of components with non-empty `vulnerabilities`)
     * - updatableComponents: int (count where `hasUpdate` is truthy)
     *
     * @return array{
     *   highestSeverity: ?string,
     *   severityScore: int,
     *   vulnerablePlugins: int,
     *   updatableComponents: int
     * }
     */
    public function getSnapshotData(): array
    {
        $all = $this->getAllRaw();

        $highestSeverity = $this->getHighestSeverity();
        $severityScore = $highestSeverity ? $this->getSeverityScore($highestSeverity) : 0;

        $vulnerablePlugins = 0;
        $updatableComponents = 0;

        foreach ($all as $component) {
            if (!is_array($component)) {
                continue;
            }

            $vulnerabilities = $component['vulnerabilities'] ?? [];
            if (is_array($vulnerabilities) && $vulnerabilities !== []) {
                $vulnerablePlugins++;
            }

            if (($component['hasUpdate'] ?? false) === true) {
                $updatableComponents++;
            }
        }

        return [
            'highestSeverity' => $highestSeverity,
            'severityScore' => $severityScore,
            'vulnerablePlugins' => $vulnerablePlugins,
            'updatableComponents' => $updatableComponents,
        ];
    }

    /**
     * Count vulnerabilities grouped by textual severity across all components.
     *
     * Returned shape: array<string, int> where key is lowercased severity.
     *
     * Example: ['critical' => 2, 'high' => 5]
     *
     * @return array<string, int>
     */
    public function getVulnerabilityCountPerSeverity(): array
    {
        $components = $this->getAllRaw();
        $counts = [];
        foreach ($components as $component) {
            $vulnerabilities = $component['vulnerabilities'] ?? null;
            if (!is_array($vulnerabilities) || $vulnerabilities === []) {
                continue;
            }

            foreach ($vulnerabilities as $vulnerability) {
                if (!is_array($vulnerability)) {
                    continue;
                }

                $severity = strtolower((string)($vulnerability['severity'] ?? ''));
                if ($severity === '') {
                    continue;
                }

                if (!isset($counts[$severity])) {
                    $counts[$severity] = 0;
                }

                $counts[$severity]++;
            }
        }

        return $counts;
    }

    /**
     * Count components grouped by their highest textual severity.
     *
     * Unlike {@see getVulnerabilityCountPerSeverity()}, this counts each component
     * at most once, based on the single highest-severity vulnerability within that
     * component.
     *
     * Returned shape: array<string, int> where key is lowercased severity.
     *
     * Example: ['critical' => 2, 'high' => 5]
     *
     * @return array<string, int>
     */
    public function getComponentCountPerHighestSeverity(): array
    {
        $components = $this->getAllRaw();
        $counts = [];

        foreach ($components as $component) {
            if (!is_array($component)) {
                continue;
            }

            $vulnerabilities = $component['vulnerabilities'] ?? null;
            if (!is_array($vulnerabilities) || $vulnerabilities === []) {
                continue;
            }

            $highest = $this->findHighestVulnerability($vulnerabilities);
            if ($highest === null) {
                continue;
            }

            $severity = strtolower((string)($highest['severity'] ?? ''));
            if ($severity === '') {
                continue;
            }

            if (!isset($counts[$severity])) {
                $counts[$severity] = 0;
            }

            $counts[$severity]++;
        }

        return $counts;
    }

    /**
     * Fetch all components that have non-empty `vulnerabilities`.
     *
     * Returned map is the subset of `getAllRaw()` where `vulnerabilities` is present.
     *
     * @return array<string, array<string, mixed>> Map of storageKey => component array
     */
    public function getAllVulnerableComponentsRaw(): array
    {
        $components = $this->getAllRaw();
        $vulnerable = [];

        foreach ($components as $key => $component) {
            if (!is_array($component)) {
                continue;
            }

            $vulnerabilities = $component['vulnerabilities'] ?? null;
            if (!is_array($vulnerabilities) || $vulnerabilities === []) {
                continue;
            }

            $vulnerable[$key] = $component;
        }

        return $vulnerable;
    }
}
PK     \{    G  Features/Vulnerability/Repositories/VulnerabilitySnapshotRepository.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\VulnerabilitySnapshotDto;

/**
 * Persists and reads the latest vulnerability snapshot from WordPress option storage.
 *
 * Constraints: No external I/O and no business rules beyond basic shape validation.
 */
final class VulnerabilitySnapshotRepository
{
    private const OPTION_KEY = 'rsssl_vulnerability_snapshot';

    public function getLatest(): ?VulnerabilitySnapshotDto
    {
        $raw = get_option(self::OPTION_KEY, null);
        if (!is_array($raw)) {
            return null;
        }

        return VulnerabilitySnapshotDto::fromArray($raw);
    }

    public function storeLatest(VulnerabilitySnapshotDto $vulnerabilitySnapshotDto): void
    {
        $this->save($vulnerabilitySnapshotDto);
    }

    public function save(VulnerabilitySnapshotDto $vulnerabilitySnapshotDto): void
    {
        update_option(self::OPTION_KEY, $vulnerabilitySnapshotDto->toArray(), false);
    }

    public function clear(): void
    {
        delete_option(self::OPTION_KEY);
    }

    /**
     * Builds a snapshot from the repository's aggregated snapshot data.
     */
    public function buildCurrentSnapshot(VulnerabilityStorageRepository $vulnerabilityStorageRepository): VulnerabilitySnapshotDto
    {
        $data = $vulnerabilityStorageRepository->getSnapshotData();

        $updatableComponents = (int)($data['updatableComponents'] ?? 0);
        $vulnerableCount = (int)($data['vulnerablePlugins'] ?? 0);
        $highestSeverity = isset($data['highestSeverity']) && is_string($data['highestSeverity'])
            ? $data['highestSeverity']
            : null;
        $severityScore = (int)($data['severityScore'] ?? 0);

        return new VulnerabilitySnapshotDto(
            $highestSeverity,
            $severityScore,
            $vulnerableCount,
            $updatableComponents,
            time()
        );
    }

    public function buildFromStorage(
        VulnerabilityStorageRepository $vulnerabilityStorageRepository
    ): VulnerabilitySnapshotDto {
        return $this->buildCurrentSnapshot($vulnerabilityStorageRepository);
    }
}
PK     \xg    D  Features/Vulnerability/Interfaces/ComponentSyncStrategyInterface.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\ComponentVulnerabilitiesDto;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\InstalledComponentDto;

/**
 * Strategy contract for synchronizing vulnerability data for a specific component type.
 *
 * In Strategy terms: this interface defines *how* vulnerability data is synchronized
 * for a given component type (enumeration, fetching, and mapping). Multiple concrete
 * implementations are interchangeable and selected by a coordinating service based
 * on the component type.
 *
 * This is explicitly not a Policy. Implementations do not decide *whether* or *when*
 * a synchronization should occur, nor do they make notification or severity decisions.
 * Those concerns are handled by higher-level orchestration or policy components.
 *
 * A component can be a plugin, theme, or WordPress core. Each implementation of this
 * interface is responsible for handling exactly one component type.
 *
 * The typical lifecycle for a sync strategy is:
 * 1. Enumerate installed components of its type.
 * 2. Fetch remote vulnerability data for each installed component.
 * 3. Map the API response into a ComponentVulnerabilitiesDto for storage.
 */
interface ComponentSyncStrategyInterface
{
    /**
     * Return the component type handled by this strategy.
     *
     * This value is used to:
     * - Route API calls to the correct endpoint.
     * - Tag stored vulnerability data with the correct component type.
     * - Produce meaningful error and log messages.
     *
     * Examples: "plugin", "theme", "core".
     *
     * @return non-empty-string
     */
    public function getType(): string;

    /**
     * Retrieve all installed components of the handled type.
     *
     * Each returned InstalledComponentDto represents a single installed
     * component (for example, one plugin or one theme) including runtime
     * information such as slug, name, and installed version.
     *
     * @return iterable<InstalledComponentDto>
     */
    public function getInstalledComponents(): iterable;

    /**
     * Fetch raw vulnerability data for a single installed component.
     *
     * Implementations are expected to:
     * - Extract the component identifier (such as slug and name).
     * - Call the appropriate remote vulnerability API.
     * - Return a standardized success/error payload that the sync service
     *   can process consistently.
     *
     * @return array{success: bool, data?: array<string, mixed>, message?: string}
     */
    public function fetchComponent(InstalledComponentDto $installedComponentDto): array;

    /**
     * Convert a raw API payload into a ComponentVulnerabilitiesDto.
     *
     * This method maps external API data to the internal storage format and
     * enriches it with runtime information from the installed component
     * (such as slug, name, installed version, and update availability).
     *
     * @param array<string, mixed> $apiPayload Raw vulnerability API response.
     */
    public function toComponentDto(array $apiPayload, InstalledComponentDto $installedComponentDto): ComponentVulnerabilitiesDto;
}
PK     \Cj,    K  Features/Vulnerability/Interfaces/InstalledComponentRepositoryInterface.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\InstalledComponentDto;

/**
 * Defines read-only access to locally installed components.
 *
 * Constraints: Exposes installed component state only; no persistence or business logic.
 * Repositories return materialized collections intended for direct consumption by controllers and services.
 */
interface InstalledComponentRepositoryInterface
{
    /**
     * Return all installed plugins as a materialized list of InstalledComponentDto objects.
     *
     * Each DTO includes:
     * - type: "plugin|theme|core"
     * - slug and display name
     * - installed version
     * - plugin file (used by WP to identify a plugin)
     * - whether the plugin is currently active (including network-active on multisite)
     * - latest available version (when WordPress update information is available)
     *
     * Repositories may use generators internally, but the public contract always returns a materialized list.
     *
     * @return list<InstalledComponentDto>
     */
    public function getInstalledComponents(): array;
}
PK     \p8  8  A  Features/Vulnerability/Interfaces/InstalledComponentInterface.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces;

/**
 * Defines the read-only contract for an installed component representation.
 *
 * Constraints: Exposes component state only; no persistence or business logic.
 */
interface InstalledComponentInterface
{
    /**
     * @return non-empty-string
     */
    public function getType(): string;

    /**
     * @return non-empty-string
     */
    public function getSlug(): string;

    /**
     * @return non-empty-string
     */
    public function getName(): string;

    /**
     * @return non-empty-string
     */
    public function getInstalledVersion(): string;

    /**
     * @return non-empty-string|null
     */
    public function getLatestVersion(): ?string;

    public function hasUpdate(): bool;
}
PK     \t5    N  Features/Vulnerability/Interfaces/VulnerabilityNotificationPolicyInterface.phpnu [        <?php

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\VulnerabilitySnapshotDto;

/**
 * Defines the decision contract for sending vulnerability notification emails.
 *
 * Implementations encapsulate notification policy rules without performing delivery.
 */
interface VulnerabilityNotificationPolicyInterface
{
    /**
     * Determines whether a notification should be sent for the current snapshot.
     */
    public function shouldSend(VulnerabilitySnapshotDto $current, ?VulnerabilitySnapshotDto $previous): bool;

    /**
     * Determines whether notifications may be sent in the current execution context.
     */
    public function canSend(): bool;

    /**
     * Returns the reason for the last send decision, intended for logging or debugging.
     *
     * @return non-empty-string
     */
    public function getReason(): string;
}
PK     \	    N  Features/Vulnerability/Interfaces/VulnerabilitySnapshotRepositoryInterface.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\VulnerabilitySnapshotDto;

/**
 * Defines persistence operations for vulnerability snapshot state.
 *
 * Constraints: Read/write access only; no business logic.
 */
interface VulnerabilitySnapshotRepositoryInterface
{
    /**
     * Returns the previously stored vulnerability snapshot, if any.
     */
    public function getPrevious(): ?VulnerabilitySnapshotDto;

    /**
     * Persists the current vulnerability snapshot.
     */
    public function saveCurrent(VulnerabilitySnapshotDto $vulnerabilitySnapshotDto): void;

    /**
     * Removes any stored snapshot data.
     */
    public function clear(): void;
}
PK     \`    (  Features/Onboarding/OnboardingLoader.phpnu [        <?php

namespace ReallySimplePlugins\RSS\Core\Features\Onboarding;

use ReallySimplePlugins\RSS\Core\Features\AbstractLoader;

class OnboardingLoader extends AbstractLoader
{
    /**
     * @inheritDoc
     */
    public function isEnabled(): bool
    {
        if (rsssl_user_can_manage() === false) {
            return false;
        }

        $onboardingQueueHasItems = $this->hasOnboardingQueueItems();
        if ($onboardingQueueHasItems) {
            return true; // To process the items
        }

        // Enable if user clicked "Activate SSL" button
        // This allows the modal to fetch data when explicitly opened by user
        // after dismissal
        if ($this->request->getBoolean('body.activateSSLClicked')) {
            return true;
        }

        // Enable if we're in the Let's Encrypt wizard context
        // The wizard needs onboarding data for the activate SSL step
        if ($this->requestIsLetsEncryptRequest()) {
            return true;
        }

        return (
            (bool) get_option('rsssl_show_onboarding', false) === true
            && (bool) get_option('rsssl_onboarding_dismissed', false) === false
        );
    }

    /**
     * @inheritDoc
     */
    public function inScope(): bool
    {
        $onboardingQueueHasItems = $this->hasOnboardingQueueItems();
        if ($onboardingQueueHasItems) {
            return true; // To process the items
        }

        return rsssl_admin_logged_in() && ($this->userIsOnDashboard() || $this->requestIsRestRequest());
    }

    /**
     * Returns true when the onboarding queue has items. For example if a user
     * has chosen to install plugins, these actions are queued and should be
     * processed later. Therefor this method is used to enable the feature
     * for request processing.
     */
    private function hasOnboardingQueueItems(): bool
    {
        $items = get_option($this->env->getString('onboarding.queue_option'), []);
        return !empty($items);
    }

    /**
     * Check if the current request is in the Let's Encrypt wizard context.
     * The Let's Encrypt wizard uses the onboarding component for the activate
     * SSL step, so we need to enable the onboarding feature when in this context.
     *
     * @internal We access $_GET and $_SERVER superglobals directly for read-only
     * context detection. These values are only used for comparison checks, not
     * output or database queries, so sanitization and nonce verification are not
     * required. The phpcs warnings are intentionally suppressed.
     */
    protected function requestIsLetsEncryptRequest(): bool
    {
        // Check GET parameters for direct page loads
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        if (
            isset($_GET['letsencrypt']) && $_GET['letsencrypt'] === '1'
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            && isset($_GET['page']) && $_GET['page'] === 'really-simple-security'
        ) {
            return true;
        }

        // Check referer for REST API requests from the Let's Encrypt wizard
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        $referer = ($_SERVER['HTTP_REFERER'] ?? '');
        if (
            strpos($referer, 'letsencrypt=1') !== false
            && strpos($referer, 'page=really-simple-security') !== false
        ) {
            return true;
        }

        return false;
    }
}
PK     \J.  .  ,  Features/Onboarding/OnboardingController.phpnu [        <?php

namespace ReallySimplePlugins\RSS\Core\Features\Onboarding;

use ReallySimplePlugins\RSS\Core\Bootstrap\App;
use ReallySimplePlugins\RSS\Core\Interfaces\DoActionInterface;
use ReallySimplePlugins\RSS\Core\Interfaces\FeatureInterface;
use ReallySimplePlugins\RSS\Core\Services\CertificateService;
use ReallySimplePlugins\RSS\Core\Services\EmailService;
use ReallySimplePlugins\RSS\Core\Services\RelatedPluginService;
use ReallySimplePlugins\RSS\Core\Services\SecureSocketsService;
use ReallySimplePlugins\RSS\Core\Services\SettingsConfigService;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig;
use ReallySimplePlugins\RSS\Core\Support\Utility\StringUtility;
use ReallySimplePlugins\RSS\Core\Traits\HasNonces;

class OnboardingController implements FeatureInterface, DoActionInterface
{
    use HasNonces;

    private EmailService $emailService;
    private OnboardingFeatureService $service;
    private SecureSocketsService $sslService;
    private RelatedPluginService $pluginService;
    private SettingsConfigService $settingsService;
    private CertificateService $certificateService;
    private EnvironmentConfig $env;

    public function __construct(
        OnboardingFeatureService $service,
        SecureSocketsService $sslService,
        EmailService $emailService,
        RelatedPluginService $pluginService,
        SettingsConfigService $settingsService,
        CertificateService $certificateService,
        EnvironmentConfig $environmentConfig
    ) {
        $this->env = $environmentConfig;
        $this->service = $service;
        $this->sslService = $sslService;
        $this->emailService = $emailService;
        $this->pluginService = $pluginService;
        $this->settingsService = $settingsService;
        $this->certificateService = $certificateService;
    }

    public function register(): void
    {
        add_filter('rsssl_run_test', [$this, 'processOnboardingTest'], 10, 3);
        add_filter('rsssl_do_action', [$this, 'rssslDoAction'], 10, 3);
        add_action($this->env->getString('onboarding.queue_event'), [$this, 'processQueuedEvent']);
    }

    /**
     * Method processes the onboarding request for SSL activation. The responses
     * are validated by the {@see rsssl_run_test} filter.
     * @return array|bool
     */
    public function processOnboardingTest(array $response, string $action, array $data)
    {
        switch ($action) {
            case 'activate_ssl':
                $data['is_rest_request'] = true;
                $response = $this->sslService->activateSSL($data);
                break;
            case 'activate_ssl_networkwide':
                $response = $this->service->processMultisiteActivationStep();
                break;
            default:
                return $response;
        }

        return $response;
    }

    /**
     * Method to dynamically parse onboarding actions. The action is parsed to
     * PascalCase and if it is an onboarding action a dedicated method is
     * called based on the format: process{OnboardingAction}Action. These
     * methods all have access to the Storage object which contains all data
     * of the request. Each method cán use it, but its not mandatory of course.
     *
     * @uses processOnboardingDataAction, processGetModalStatusAction
     * @uses processDismissModalAction, processOverrideSslDetectionAction
     * @uses processUpdateEmailAction, processActivateAction
     * @uses processDownloadAction
     */
    public function rssslDoAction(array $response, string $action, $data): array
    {
        $actionableMethod = 'process' . StringUtility::snakeToPascalCase($action) . 'Action';

        // Current action is not one we want to process
        if (method_exists($this, $actionableMethod) === false) {
            return $response;
        }

        // Method exists. Try to execute and return the response.
        try {
            $storage = new Storage($data);
            return $this->$actionableMethod($storage);
        } catch (\Exception $exception) {
            return array_merge($response, [
                'success' => false,
                'message' => $exception->getMessage(),
            ]);
        }
    }

    /**
     * Resets onboarding state when user clicks the "Activate SSL" button
     * after dismissal.
     *
     * When a user dismisses the onboarding modal and later clicks the
     * "Activate SSL" button, we need to reset the onboarding state options
     * (rsssl_show_onboarding and rsssl_onboarding_dismissed) to allow the
     * onboarding flow to proceed.
     *
     * The OnboardingLoader has a bypass that loads the Controller when it
     * detects the activateSSLClicked flag in the request body. Once loaded,
     * we reset the options here at the start of processOnboardingDataAction
     * so the onboarding state is consistent for the rest of the request and
     * future requests.
     *
     * We can't split this into two separate actions (one to reset, one to
     * fetch data) because a separate reset action without the
     * activateSSLClicked flag would be blocked by the Loader checking the
     * dismissed state.
     *
     * @return void
     */
    private function onActivateSslClick(): void
    {
        $this->service->resetOnboarding();
    }

    /**
     * Two possibilities:
     * - a new install: show activation notice, and process onboarding
     * - an upgrade to 6. Only show new features.
     * @internal action: onboarding_data
     * @throws \RuntimeException
     */
    protected function processOnboardingDataAction(Storage $data): array
    {
        $nonce = $data->getString('nonce');
        if ($this->verifyNonce($nonce, 'rsssl_nonce') === false) {
            throw new \RuntimeException(esc_html__('Nonce validation failed', 'really-simple-ssl'));
        }

        // Reset onboarding state if user clicked "Activate SSL" after dismissal
        if ($data->getBoolean('activateSSLClicked')) {
            $this->onActivateSslClick();
        }

        // For an upgrade from free, we should check the rsssl_free_deactivated
        // option. When upgrading from Pro from Free, rsssl_deactivate_alternate
        // is called in the Free plugin. Therefore, we have to check this option.
        // This is not something we can easily change, because the free plugin has
        // to be updated before we can check this in Pro.
        $isUpgradeFromFree = get_option('rsssl_free_deactivated');
        delete_option('rsssl_free_deactivated');

        $stepsGenerator = App::getInstance()->make(OnboardingStepsGenerator::class);
        $onboardingSteps = $stepsGenerator->generate($isUpgradeFromFree);

        //if the user called with a refresh action, clear the cache
        if ($data->getBoolean('forceRefresh')) {
            delete_transient('rsssl_certinfo');
        }

        return [
            'steps' => $onboardingSteps,
            'ssl_enabled' => rsssl_get_option('ssl_enabled'),
            'ssl_detection_overridden' => get_option('rsssl_ssl_detection_overridden'),
            'certificate_valid' => $this->certificateService->isValid(),
            'networkwide' => (is_multisite() && rsssl_is_networkwide_active()),
            'network_activation_status' => get_site_option('rsssl_network_activation_status'),
            'rsssl_upgraded_from_free' => $isUpgradeFromFree,
        ];
    }

    /**
     * Method determines if the onboarding modal should be shown
     * @internal action: get_modal_status
     */
    protected function processGetModalStatusAction(Storage $data): array
    {
        return [
            'dismissed' => ($this->service->showOnboardingModal() === false),
        ];
    }

    /**
     * Method processes the user action to dismiss the onboarding modal. It will
     * trigger the event to process any queued items immediately.
     * @internal action: dismiss_modal
     */
    protected function processDismissModalAction(Storage $data): array
    {
        $updated = update_option('rsssl_onboarding_dismissed', $data->getBoolean('dismiss'), false);

        if (!empty($this->service->getQueuedItems())) {
            $this->service->manuallyProcessQueueNow();
        }

        return [
            'success' => $updated,
        ];
    }

    /**
     * Update SSL detection overridden option
     * @internal action: override_ssl_detection
     */
    protected function processOverrideSslDetectionAction(Storage $data): array
    {
        if ($data->getBoolean('overrideSSL')) {
            $success = update_option('rsssl_ssl_detection_overridden', true, false);
        } else {
            $success = delete_option('rsssl_ssl_detection_overridden');
        }

        return [
            'success' => $success
        ];
    }

    /**
     * Method processes the given email, if it is valid we send a verification
     * mail. If the user choose to receive tips&tricks then we add them to
     * our mailing list as well.
     * @internal action: update_email
     */
    protected function processUpdateEmailAction(Storage $data): array
    {
        $email = $data->getEmail('email');

        // Abort.
        if (is_email($email) === false) {
            return [
                'success' => false,
            ];
        }

        rsssl_update_option('send_notifications_email', 1);

        if ($data->getBoolean('includeTips')) {
            $this->emailService->addEmailToMailingList($email);
        }

        $this->emailService->setEmail($email);
        return $this->emailService->sendVerificationMail();
    }

    /**
     * Method processes the download action for a related plugin. It does not
     * immediately download this related plugin, but it adds it to a queue that
     * is processed on the next page load. This prevents users breaking the
     * process by refreshing the page (for example)
     * @internal action: download
     */
    protected function processDownloadAction(Storage $data): array
    {
        $this->service->queueOnboardingItem(
            $data->getTitle('id'),
            'download'
        );

        return [
            'next_action' => 'activate',
            'success' => true,
        ];
    }

    /**
     * Method processes the activation action for a related plugin. It does not
     * immediately activate this related plugin, but it adds it to a queue that
     * is processed on the next page load. This prevents users breaking the
     * process by refreshing the page (for example)
     * @internal action: activate
     */
    protected function processActivateAction(Storage $data): array
    {
        $this->service->queueOnboardingItem(
            $data->getTitle('id'),
            'activate'
        );

        return [
            'next_action' => 'completed',
            'success' => true,
        ];
    }

    /**
     * Process the plugins to download/activate queue
     */
    public function processQueuedEvent(): void
    {
        $queuedItems = $this->service->getQueuedItems();

        foreach ($queuedItems as $key => &$item) {
            if (!isset($item['status'], $item['action']) || $item['status'] !== 'pending') {
                continue;
            }

            // Mark as processing
            $item['status'] = 'processing';
            $this->service->updateQueuedItems($queuedItems);

            $this->pluginService->setPluginConfigBySlug($item['item_id']);
            $success = $this->pluginService->executeAction(
                sanitize_text_field($item['action'])
            );

            // Update status
            $item['status'] = $success ? 'completed' : 'failed';
            $item['completed'] = time();
        }

        $this->service->updateQueuedItems($queuedItems);
        $this->service->cleanupQueuedItems();
    }
}
PK     \Fm  m  0  Features/Onboarding/OnboardingFeatureService.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Onboarding;

use ReallySimplePlugins\RSS\Core\Services\GlobalOnboardingService;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig;

/**
 * Business logic for the onboarding feature.
 * Queue management and feature-specific onboarding logic.
 * Extends {@see GlobalOnboardingService} to inherit global onboarding methods.
 */
class OnboardingFeatureService extends GlobalOnboardingService
{
    protected EnvironmentConfig $env;

    public function __construct(EnvironmentConfig $environmentConfig)
    {
        $this->env = $environmentConfig;
    }

    /**
     * Helper method to check if we should show the onboarding.
     * @todo: I guess the order of the checks is important. If not, the code
     * can be optimized a bit.
     */
    public function showOnboardingModal(): bool
    {
        $userDismissedOnboarding = (bool) get_option('rsssl_onboarding_dismissed');
        if ($userDismissedOnboarding) {
            return false;
        }

        if ($this->wpConfigNeedsFixing()) {
            return false; // First fix wp-config
        }

        if ($this->multisiteActivationNotCompleted()) {
            return true; // Finish activation with the onboarding modal
        }

        $sslIsEnabled = (bool) rsssl_get_option('ssl_enabled');
        $showOnboardingAfterUpdateOrUpgrade = (bool) get_option('rsssl_show_onboarding');
        if ($sslIsEnabled && ($showOnboardingAfterUpdateOrUpgrade === false)) {
            return false; // No onboarding if ssl already enabled, except after upgrade
        }

        $constantDismissedOnboarding = (defined('RSSSL_DISMISS_ACTIVATE_SSL_NOTICE') && RSSSL_DISMISS_ACTIVATE_SSL_NOTICE);
        if ($constantDismissedOnboarding) {
            return false;
        }

        if (rsssl_user_can_manage() === false) {
            return false;
        }

        return true;
    }

    /**
     * For multisite environments, check if the activation process was
     * started but not completed.
     *
     * @return bool True if multisite activation is incomplete
     */
    private function multisiteActivationNotCompleted(): bool
    {
        return (is_multisite() && RSSSL()->multisite->ssl_activation_started_but_not_completed());
    }

    /**
     * Check if wp-config needs fixing before showing onboarding.
     *
     * @todo: This check seems very legacy as these admin checks are present
     * since 2.2. Do we still need to prevent loading the onboarding when
     * we need to fix the wp-config? Do we even still fix the wp-config?
     * If no, just remove this.
     *
     * @return bool True if wp-config needs fixing
     */
    private function wpConfigNeedsFixing(): bool
    {
        if (RSSSL()->admin->configuration_loaded === false) {
            RSSSL()->admin->detect_configuration();
        }

        // wp-config still need fixes
        if (RSSSL()->admin->do_wpconfig_loadbalancer_fix() && !RSSSL()->admin->wpconfig_has_fixes()) {
            return true;
        }

        // wp-config has fixes, but still not OK
        if (RSSSL()->admin->wpconfig_ok() === false) {
            return true;
        }

        return false;
    }

    /**
     * Method processes the SSL activation step of the onboarding for multisite
     * instances.
     */
    public function processMultisiteActivationStep(): array
    {
        return RSSSL()->multisite->process_ssl_activation_step();
    }

    /**
     * Get the items from the onboarding queue
     */
    public function getQueuedItems(): array
    {
        $handle = $this->env->getString('onboarding.queue_option');
        return get_option($handle, []);
    }

    /**
     * Update the onboarding queue with the given array. It overrides the
     * current queue completely.
     */
    public function updateQueuedItems(array $queue): bool
    {
        $handle = $this->env->getString('onboarding.queue_option');
        return update_option($handle, $queue, false);
    }

    /**
     * Add an item to the onboarding queue. Method will also schedule the
     * queue event if not already done to make sure the queue will be
     * processed. Returns true when queue is correctly scheduled.
     * Process is done by {@see OnboardingController::processQueuedEvent}
     */
    public function queueOnboardingItem(string $itemId, string $action): bool
    {
        $queue = $this->getQueuedItems();
        $key = sanitize_key($itemId) . '_' . sanitize_key($action);

        $queue[$key] = [
            'item_id' => $itemId,
            'action' => $action,
            'status' => 'pending',
        ];

        $this->updateQueuedItems($queue);

        // Schedule and spawn the queue event when not yet scheduled
        $event = $this->env->getString('onboarding.queue_event');
        if (!wp_next_scheduled($event)) {
            $scheduled = wp_schedule_single_event(time() + 10, $event);
            $spawned = spawn_cron();
            return ($scheduled === true && $spawned === true);
        }

        return true;
    }

    /**
     * Clean up queued items and only keep failed or processing items. If
     * empty, we delete the queue option completely, otherwise we reschedule
     * a single event to retry the leftover items
     */
    public function cleanupQueuedItems(): void
    {
        $queuedItems = $this->getQueuedItems();
        $optionHandle = $this->env->getString('onboarding.queue_option');
        $eventHandle = $this->env->getString('onboarding.queue_event');

        /**
         * Statuses to keep even when the cleanup is triggered. Can be used to
         * debug why an action did not complete. In such a case, the status is
         * stuck 'failed'.
         */
        $shouldKeepStatusEvenWhenCleaned = apply_filters('rsssl_cleanup_onboarding_statuses', ['processing']);

        // Only keep failed or processing items
        $cleanedQueue = array_filter($queuedItems, static function ($item) use ($shouldKeepStatusEvenWhenCleaned) {
            $status = $item['status'] ?? '';
            return in_array($status, $shouldKeepStatusEvenWhenCleaned);
        });

        if (empty($cleanedQueue)) {
            delete_option($optionHandle);
            return;
        }

        // Queue contains failed or processing items, schedule a next run
        update_option($optionHandle, $cleanedQueue, false);
        wp_schedule_single_event(time() + 600, $eventHandle);
    }

    /**
     * Manually process the queue right now. Useful in situations where we do
     * not need to wait for the event to be triggered automatically. For example
     * when the onboarding is dismissed in
     * {@see OnboardingController::processDismissModalAction}
     */
    public function manuallyProcessQueueNow(): void
    {
        $eventHandle = $this->env->getString('onboarding.queue_event');
        wp_clear_scheduled_hook($eventHandle);

        // fire!
        do_action($eventHandle);
    }
}
PK     \3`!!  !  0  Features/Onboarding/OnboardingStepsGenerator.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Onboarding;

use ReallySimplePlugins\RSS\Core\Services\CertificateService;
use ReallySimplePlugins\RSS\Core\Services\RelatedPluginService;
use ReallySimplePlugins\RSS\Core\Services\SettingsConfigService;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\UriConfig;

class OnboardingStepsGenerator
{
    public array $steps = [];
    private bool $proPluginEnabled;

    private RelatedPluginService $pluginService;
    private SettingsConfigService $settingsService;
    private CertificateService $certificateService;
    private UriConfig $uriConfig;

    public function __construct(
        RelatedPluginService $pluginService,
        SettingsConfigService $settingsService,
        CertificateService $certificateService,
        UriConfig $uriConfig
    ) {
        $this->uriConfig = $uriConfig;
        $this->pluginService = $pluginService;
        $this->settingsService = $settingsService;
        $this->certificateService = $certificateService;

        $this->proPluginEnabled = defined('rsssl_pro');
    }

    public function generate(bool $isUpgradeFromFree = false): array
    {
        if ($isUpgradeFromFree) {
            $steps = [
                $this->activateLicenseStep(),
                $this->proStep(),
            ];
        } else {
            $steps = [
                $this->activateSslStep(),
                $this->emailStep(),
                $this->essentialFeaturesStep(),
                $this->activateLicenseStep(),
                $this->relatedPluginsStep(),
                $this->proStep(),
            ];
        }

        // Remove empty steps
        $steps = array_filter($steps);

        // Re-order keys to prevent issues after array_filter
        return array_values($steps);
    }

    /**
     * The activate SSL step include items related to SSL detection and
     * configuration, but only when the user is not upgrading from the free to
     * the pro version.
     */
    private function activateSslStep(): array
    {
        $items = [];

        if (strpos(site_url(), 'https://') === false) {
            $items[] = [
                'title' => esc_html__('You may need to login in again, have your credentials prepared.', 'really-simple-ssl'),
                'status' => 'inactive',
                'id' => 'login',
            ];
        }

        // Add single SSL certificate test-outcome item to the step
        $items[] = $this->getSslCertificateTestResultItem();

        return [
            'id' => 'activate_ssl',
            'title' => esc_html__('Welcome to Really Simple Security', 'really-simple-ssl'),
            'subtitle' => esc_html__('The onboarding wizard will help to configure essential security features in 1 minute! Select your hosting provider to start.', 'really-simple-ssl'),
            'items' => $items,
        ];
    }


    /**
     * Method is used for determining the single certificate status item based
     * on the detection outcome. This prevents stacking multiple certificate
     * specific notices at once.
     */
    private function getSslCertificateTestResultItem(): array
    {
        if ($this->certificateService->isValid()) {
            return [
                'title'  => esc_html__('An SSL certificate has been detected', 'really-simple-ssl'),
                'status' => 'success',
                'id'     => 'certificate',
            ];
        }

        if ($this->certificateService->detectionFailed()) {
            return [
                'title'  => esc_html__('Could not test certificate', 'really-simple-ssl') . ' ' . esc_html__('Automatic certificate detection is not possible on your server.', 'really-simple-ssl'),
                'status' => 'error',
                'id'     => 'certificate',
            ];
        }

        return [
            'title'  => esc_html__('No SSL certificate has been detected.', 'really-simple-ssl') . ' ' . esc_html__('Please refresh the SSL status if a certificate has been installed recently.', 'really-simple-ssl'),
            'status' => 'error',
            'id'     => 'certificate',
        ];
    }

    /**
     * The email step is used to verify the email address of the user and to
     * send a test email to confirm that email is correctly configured on their
     * site. But only when the user is not upgrading from the free to the pro
     * version of the plugin.
     */
    private function emailStep(): array
    {
        return [
            'id' => 'email',
            'title' => esc_html__('Verify your email', 'really-simple-ssl'),
            'subtitle' => esc_html__('Really Simple Security will send email notifications and security warnings from your server. We will send a test email to confirm that email is correctly configured on your site. Look for the confirmation button in the email.', 'really-simple-ssl'),
            'button' => esc_html__('Save and continue', 'really-simple-ssl'),
        ];
    }

    /**
     * The essential features step prompts user with recommended features. But
     * only if the user is not upgrading from free to pro. If a user is using
     * the free version of the plugin some pro features are included in the
     * step as well, this is done for upsell purposes.
     */
    private function essentialFeaturesStep(): array
    {
        $subtitle = esc_html__('Instantly configure these essential features.', 'really-simple-ssl');

        if ($this->proPluginEnabled === false) {
            $subtitle .= ' ' . sprintf(
                wp_kses_post(__('Please %sconsider upgrading to Pro%s to enjoy all simple and performant security features.', 'really-simple-ssl')),
                '<a href="' . $this->uriConfig->getUrl('rsp.upgrade_from_free') . '" target="_blank">',
                '</a>'
            );
        }

        // If pro is not enabled we do some upselling with premium features
        $includePremiumSettingsForUpsellPurposes = ($this->proPluginEnabled === false);

        return [
            'id' => 'features',
            'title'  => esc_html__('Essential security', 'really-simple-ssl'),
            'subtitle' => $subtitle,
            'items' => $this->settingsService->getRecommendedSettings($includePremiumSettingsForUpsellPurposes),
            'button' => esc_html__('Enable', 'really-simple-ssl'),
        ];
    }

    /**
     * In this step we ask the user to save and activate their license. Only
     * needed is the pro version of the plugin is active.
     */
    private function activateLicenseStep(): array
    {
        /// No need for a license step if freemium is enabled
        if ($this->proPluginEnabled === false) {
            return [];
        }

        return [
            'id' => 'activate_license',
            'title' => esc_html__('Activate your license key', 'really-simple-ssl'),
            'subtitle' => '',
            'items' => [
                'type' => 'license',
            ],
            'button' => esc_html__('Activate', 'really-simple-ssl'),
            'value' => '',
        ];
    }

    /**
     * This step is always included. If a user is using the free version these
     * recommended settings are disabled (greyed-out) for upsell purposes.
     */
    private function proStep(): array
    {
        return [
            'id' => 'pro',
            'title' => 'Really Simple Security Pro',
            'subtitle' => esc_html__('Heavyweight security features, in a lightweight performant plugin from Really Simple Plugins. Get started with below features and get the latest and greatest updates for peace of mind!', 'really-simple-ssl'),
            'items' => $this->settingsService->getRecommendedProSettings(),
            'button' => esc_html__('Install', 'really-simple-ssl'),
        ];
    }

    /**
     * This step will prompt users with other plugins of Really Simple Plugins.
     * Only included if the user is not upgrading from free to pro, then this
     * step was already done in the onboarding of the free version.
     */
    private function relatedPluginsStep(): array
    {
        return [
            'id' => 'plugins',
            'title' => esc_html__('We think you will like this', 'really-simple-ssl'),
            'subtitle' => esc_html__('Really Simple Plugins is also the author of the below privacy-focused plugins including consent management and legal documents!', 'really-simple-ssl'),
            'items' => $this->pluginService->getOnboardingConfig(),
            'button' => esc_html__('Install', 'really-simple-ssl'),
        ];
    }
}
PK     \FO  O    Features/AbstractLoader.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features;

use ReallySimplePlugins\RSS\Core\Managers\FeatureManager;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\RequestStorage;

/**
 * Each Feature should have a {FeatureName}Loader class that extends this
 * abstract loader. The {@see FeatureManager} will use the loader to
 * determine if a feature should be loaded.
 *
 * @internal Without loading all the feature classes, composer will prevent
 * requiring the files entirely. Even tho the Feature namespace falls
 * withing the psr-4 scope.
 */
abstract class AbstractLoader
{
	protected EnvironmentConfig $env;
	protected RequestStorage $request;


    public function __construct(
		EnvironmentConfig $environmentConfig,
		RequestStorage $request
    )
    {
		$this->env = $environmentConfig;
		$this->request = $request;
    }

    /**
     * Method should return true if the feature is enabled. This can check
     * setting values or user capabilities for example.
     */
    abstract public function isEnabled(): bool;

    /**
     * Method should return true if the context of the user is in the scope of
     * the feature to be loaded. For example: some features only need to load
     * on our dashboard and others also in each REST API request.
     */
    abstract public function inScope(): bool;

    /**
     * Check if the current user is on the Dashboard page.
     * @todo Responsibility for retrieving the dashboard "page" value should
     * be added somewhere and it should be globally accessible.
     */
    protected function userIsOnDashboard(): bool
    {
        $pageVisitedByUser = $this->request->getString('global.page');
        $dashboardUrl = $this->env->getString('plugin.dashboard_url');

        $pluginPageQueryString = wp_parse_url($dashboardUrl, PHP_URL_QUERY);
        parse_str($pluginPageQueryString, $parsedQuery);
        $pluginDashboardPage = ($parsedQuery['page'] ?? '');

        return $pageVisitedByUser === $pluginDashboardPage;
    }

    /**
     * Check if the current request is a WP JSON request. This is better than
     * the WordPress native function `wp_is_json_request()`, because that
     * returns false when visiting /wp-json/ or ?rest_route= (for plain
     * permalinks) endpoint. We need a true value there to activate
     * features that register REST routes. For example
     * {@see \ReallySimplePlugins\RSS\Core\Features\Onboarding\OnboardingController}
     *
     * @internal Ignore the phpcs errors for this method, as they are false
     * positives. We do not actually use the $_GET or $_SERVER variables
     * directly, but we need to check if they are set and contain the
     * expected values.
     */
    protected function requestIsRestRequest(): bool
    {
        $pluginHttpNamespace = $this->env->getString('http.namespace');
        $restUrlPrefix = trailingslashit(rest_get_url_prefix());

        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        $currentRequestUri = ($_SERVER['REQUEST_URI'] ?? '');
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
        $isPlainPermalink = (
            isset($_GET['rest_route'])
            && (strpos($_GET['rest_route'], $pluginHttpNamespace) !== false)
        );

        return (strpos($currentRequestUri, $restUrlPrefix) !== false) || $isPlainPermalink;
    }
}
PK     \̠~  ~    Traits/HasEncryption.phpnu [        <?php

namespace ReallySimplePlugins\RSS\Core\Traits;

trait HasEncryption
{
    /**
     * Encrypt a string with a prefix. If the prefix is already there, it's
     * already encrypted.
     */
    public function maybeEncryptPrefixed(string $data, string $prefix = 'rsssl_'): string
    {
        if (strpos($data, $prefix) === 0) {
            return $data;
        }

        $data = $this->encrypt($data);
        return $prefix . $data;
    }

    /**
     * Decrypt data if prefixed. If not prefixed, return the data, as it is
     * already decrypted.
     */
    public function maybeDecryptPrefixed(string $data, string $prefix = 'rsssl_', string $deprecatedKey = ''): string
    {
        if (strpos($data, $prefix) !== 0) {
            return $data;
        }

        $data = substr($data, strlen($prefix));
        return $this->decrypt($data, 'string', $deprecatedKey);
    }

    /**
     * Encrypt the given data
     *
     * @param array|string $data
     * @param string $type The $data type that was given ('string' or 'array')
     */
    public function encrypt($data, string $type = 'string'): string
    {
        $key = $this->getEncryptionKey();

        if ('array' === strtolower($type)) {
            $data = serialize($data);
        }

        $dataIsEmpty = (strlen(trim($data)) === 0);
        $functionsDoNotExists = (
            function_exists('openssl_random_pseudo_bytes') === false
            || function_exists('openssl_cipher_iv_length') === false
            || function_exists('openssl_encrypt') === false
        );

        if ($dataIsEmpty || $functionsDoNotExists) {
            return '';
        }

        $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
        $encrypted = openssl_encrypt($data, 'aes-256-cbc', $key, 0, $iv);
        return base64_encode($encrypted . '::' . $iv);
    }

    /**
     * Decrypt the given data
     *
     * @param mixed $data The data to decrypt
     * @param string $type The type of data to return ('string' or 'array')
     * @return array|string Either array or string, based on the $type
     */
    public function decrypt($data, string $type = 'string', string $deprecatedKey = '')
    {
        $fallbackValue = (strtolower($type) === 'string' ? '' : []);
        $key = !empty($deprecatedKey) ? $deprecatedKey : $this->getEncryptionKey();

        // If $data is empty, return appropriate empty value based on type
        if (empty($data)) {
            return $fallbackValue;
        }

        // If $data is not a string (i.e., it's already an array), return as is
        if (!is_string($data)) {
            return $data;
        }

        if (!function_exists('openssl_decrypt')) {
            return $fallbackValue;
        }

        $decoded = base64_decode($data);
        if (false === $decoded) {
            return $fallbackValue;
        }

        if (strpos($decoded, '::') !== false) {
            [$encrypted_data, $iv] = explode('::', $decoded, 2);
        } else {
            // Deprecated method, for backwards compatibility (license decryption)
            $ivlength = openssl_cipher_iv_length('aes-256-cbc');
            $iv = substr($decoded, 0, $ivlength);
            $encrypted_data = substr($decoded, $ivlength);
        }

        $decrypted_data = openssl_decrypt($encrypted_data, 'aes-256-cbc', $key, 0, $iv);

        if ('array' === strtolower($type)) {
            $unserialized_data = @unserialize($decrypted_data);
            return (is_array($unserialized_data)) ? $unserialized_data : [];
        }

        return $decrypted_data;
    }

    /**
     * Method is used to fetch the encryption key. Used in the encryption and
     * decryption processes. The key is a constant stored in the wp-config
     * or a key stored in the database.
     */
    private function getEncryptionKey(): string
    {
        return defined('RSSSL_KEY') ? RSSSL_KEY : get_site_option('rsssl_main_key', '');
    }
}
PK     \[,q      Traits/HasNonces.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Traits;

trait HasNonces
{
    /**
     * Method for verifying the nonce
     * @param mixed $nonce Preferably string, not type-casted to prevent errors
     */
    protected function verifyNonce($nonce, string $action = 'rss_core_nonce'): bool
    {
        if (is_string($nonce) === false) {
            return false;
        }

        return (bool) wp_verify_nonce(sanitize_text_field(wp_unslash($nonce)), $action);
    }
}
PK     \݇      Traits/HasAllowlistControl.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Traits;

use ReallySimplePlugins\RSS\Core\Bootstrap\App;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig;

trait HasAllowlistControl
{
    /**
     * Check if the current code execution allows access to the admin area.
     * This is the case when:
     * - user is logged in and has manage_security capability
     * - this is a REST API request and user is logged in
     * - this is a WPCLI request
     * - this is a cron request
     *
     * This ensures that auto updates can run, and cron jobs can complete.
     */
    public function adminAccessAllowed(): bool
    {
        $wpcli = defined('WP_CLI') && WP_CLI;
        $currentUserCanVisitAdmin = ((is_admin() || is_network_admin()) && current_user_can('manage_security'));

        return $currentUserCanVisitAdmin || $this->restRequestIsAllowed() || wp_doing_cron() || $wpcli;
    }

    /**
     * Check if the current request is authenticated, for a REST API request.
     * This is the case when:
     * - The request URI is set and contains the plugin namespace
     * AND
     *  - The callback URL is still active, and the request URI contains the callback URL
     *      OR
     *  - The user is logged in and has the 'manage_security' capability
     *
     * @internal Ignore the phpcs errors for this method, as they are false
     * positives. We do not actually use the $_GET or $_SERVER variables
     * directly, but we need to check if they are set and contain the
     * expected values.
     *
     * @todo Name of this method is not entirely accurate, consider renaming
     */
    public function restRequestIsAllowed(): bool
    {
        $env = App::getInstance()->get(EnvironmentConfig::class);
        $pluginNamespace = $env->getString('http.namespace');

        $validWpJsonRequest = (
            isset($_SERVER['REQUEST_URI'])
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
            && (strpos($_SERVER['REQUEST_URI'], $pluginNamespace) !== false)
        );

        $validPlainPermalinksRequest = (
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            isset($_GET['rest_route'])
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
            && (strpos($_GET['rest_route'], $pluginNamespace) !== false)
        );

        if ($validWpJsonRequest === false && $validPlainPermalinksRequest === false) {
            return false;
        }

        return is_user_logged_in() && current_user_can('manage_security');
    }

    /**
     * Check if the current user has the capability to manage the plugin.
     * This is the case when:
     * - The user is logged in and has the 'manage_security' capability
     * - This is a REST API request and the user is logged in
     * - This is a WPCLI request
     *
     * @internal This replaces Helper::user_can_manage()
     */
    public function userCanManage(): bool
    {
        // During activation, we need to allow access
        if (get_option('rss_core_activation_flag')) {
            return true;
        }

        if (defined('WP_CLI') && WP_CLI) {
            return true;
        }

        if ($this->restRequestIsAllowed()) {
            return true;
        }

        return current_user_can('manage_security');
    }
}
PK     \iaQ  Q    Traits/HasViews.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Traits;

use ReallySimplePlugins\RSS\Core\Bootstrap\App;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig;

trait HasViews
{
    /**
     * Method for returning the desired view as a string
     * @throws \LogicException
     */
    public function view(string $path, array $variables = [], string $extension = 'php'): string
    {
        $env = App::getInstance()->get(EnvironmentConfig::class);
        $basePath = $env->getString('plugin.view_path');
        $filePath = realpath($basePath . $path . '.' . $extension);

        // Someone is doing something dirty
        if (($filePath === false) || (strpos($filePath, $basePath) !== 0)) {
            throw new \LogicException('Given path is not valid: ' . esc_html($filePath));
        }

        if (empty($filePath) || (file_exists($filePath) === false) || (is_readable($filePath) === false)) {
            return '';
        }

        extract($variables);

        ob_start();
        require $filePath;
        return ob_get_clean();
    }

    /**
     * Method for outputting the desired view.
     *
     * @internal we can ignore the phpcs error because we validate in
     * {@see view} that the executed path is in our plugin. And we
     * escape all variables in our views, so we have full control.
     */
    public function render(string $path, array $variables = [], string $extension = 'php'): void
    {
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        echo $this->view($path, $variables, $extension);
    }
}
PK     \X"`T
  
    Traits/HasScheduler.phpnu [        <?php

declare(ticks=1);

namespace ReallySimplePlugins\RSS\Core\Traits;

/**
 * Trait DebouncedScheduler
 *
 * Provides helpers to schedule debounced single cron events using an option-based
 * lock to prevent duplicate scheduling within the debounce window.
 */
trait HasScheduler
{
    /**
     * Schedule a single cron event without debounce.
     *
     * By default, the event is scheduled for "now". You can optionally pass a
     * Unix timestamp to schedule it for a specific moment.
     *
     * @todo - no use-case yet, test thoroughly before using widely.
     *
     * @param string $hook Cron hook name.
     * @param array $args Optional arguments for the scheduled event.
     * @param int|null $timestamp Unix timestamp when the event should run. Defaults to now.
     */
    protected function schedule(string $hook, array $args = [], ?int $timestamp = null): void
    {
        $runAt = $timestamp ?? time();

        $existingScheduledTimestamp = wp_next_scheduled($hook, $args);
        if ($existingScheduledTimestamp === $runAt) {
            return; // An identical event is already scheduled at the same time.
        }

        wp_schedule_single_event($runAt, $hook, $args);
    }

    /**
     * Schedule a single cron event only if no active lock exists and no identical
     * event is already scheduled within the debounce period.
     *
     * @param string $hook Cron hook name.
     * @param int $secondsUntilExecution Debounce period in seconds.
     * @param array $args       Optional arguments for the scheduled event.
     *
     * @return void
     */
    protected function scheduleDebounced(string $hook, int $secondsUntilExecution, array $args = []): void
    {
        $now = time();
        $lockOption = $hook . '_debounce_lock';
        $lockUntil = (int) get_option($lockOption, 0);

        // If a lock is still active, nothing to do.
        if ($lockUntil > $now) {
            return;
        }

        // Set lock to expire after the debounce period (no autoload).
        update_option($lockOption, $now + $secondsUntilExecution, false);

        // Schedule only if there is no identical event already queued.
        if (wp_next_scheduled($hook, $args) === false) {
            wp_schedule_single_event($now + $secondsUntilExecution, $hook, $args);
        }
    }

    /**
     * Release the debounce lock when the task completes.
     *
     * @param string $scheduledEventName Event name where the hook used to store the lock expiry timestamp.
     *
     * @return void
     */
    protected function releaseDebounceLock(string $scheduledEventName): void
    {
        delete_option($scheduledEventName . '_debounce_lock');
    }
}
PK     \      Interfaces/FeatureInterface.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Interfaces;

/**
 * This interface can be used to register a feature. Features will only
 * be accepted and registered by {@see FeatureManager} when they implement
 * this interface.
 */
interface FeatureInterface
{
    /**
     * This method should be used to register all hooks and filters. The
     * {@see FeatureManager} will make sure the method is called in the boot
     * process of the plugin.
     */
    public function register(): void;
}
PK     \+    &  Interfaces/SingleEndpointInterface.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Interfaces;

interface SingleEndpointInterface
{
    /**
     * The route name to register. Will be used as the array key for routes
     * array in: {@see EndpointManager::registerWordPressRestRoutes}
     */
    public function registerRoute(): string;

    /**
     * Arguments you can use are documented wih filter: rss_core_rest_routes
     * in method: {@see EndpointManager::getPluginRoutes}
     */
    public function registerArguments(): array;

    /**
     * This method should return true if the endpoint is enabled, false
     * otherwise. Endpoint will not be registered if this method returns false:
     * {@see EndpointManager::registerEndpoints}
     */
    public function enabled(): bool;
}
PK     \ޣj       Interfaces/ProviderInterface.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Interfaces;

interface ProviderInterface
{
    /**
     * The method that gets called by the ProviderManager to serve the provided
     * functionality.
     */
    public function provide(): void;
}
PK     \ˬ    %  Interfaces/MultiEndpointInterface.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Interfaces;

/**
 * This interface can be used instead of {@see SingleEndpointInterface} to register
 * multiple routes at once. This is useful when a single route has multiple
 * endpoints.
 */
interface MultiEndpointInterface
{
    /**
     * The routes to register. For each array in the array, the key is the route
     * and the value is an array of arguments to pass to the register_rest_route
     * function: {@see EndpointManager::registerWordPressRestRoutes}.
     *
     * Arguments you can use are documented with filter: rss_core_rest_routes
     * in method: {@see EndpointManager::getPluginRoutes}
     */
    public function registerRoutes(): array;

    /**
     * This method should return true if the endpoint is enabled, false
     * otherwise. Endpoint will not be registered if this method returns false:
     * {@see EndpointManager::registerEndpoints}
     */
    public function enabled(): bool;
}
PK     \G͒       Interfaces/DoActionInterface.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Interfaces;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\VulnerabilityDataController;

interface DoActionInterface
{
    /**
     * Implement this method to handle custom actions triggered via the
     * existing `rsssl_do_action` mechanism.
     *
     * This interface allows new code to hook into the same action-dispatching
     * flow that is already used elsewhere in the plugin, without duplicating
     * or reimplementing that logic.
     *
     * The method is responsible for inspecting the given `$action` and `$data`,
     * performing the appropriate operation, and returning a modified `$response`
     * array.
     *
     * @param array  $response The response data that should be returned to the caller.
     * @param string $action   The action identifier that determines what logic to execute.
     * @param mixed  $data     Additional payload associated with the action.
     *
     * @return array The updated response array after the action has been handled.
     */
    public function rssslDoAction(array $response, string $action, $data): array;
}
PK     \|    "  Interfaces/ControllerInterface.phpnu [        <?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Interfaces;

/**
 * This interface can be used to register a controller. Controllers will only
 * be accepted and registered by {@see ControllerManager} when they implement
 * this interface.
 */
interface ControllerInterface
{
    /**
     * This method should be used to register all hooks and filters. The
     * {@see ControllerManager} will make sure the method is called in the boot
     * process of the plugin.
     */
    public function register(): void;
}
PK     \v      Providers/Provider.phpnu [        <?php

namespace ReallySimplePlugins\RSS\Core\Providers;

use ReallySimplePlugins\RSS\Core\Bootstrap\App;
use ReallySimplePlugins\RSS\Core\Interfaces\ProviderInterface;
use ReallySimplePlugins\RSS\Core\Support\Utility\StringUtility;

/**
 * Providers are classes that provide functionality to the container. Child
 * classes should never use the container instance themselves to prevent
 * recursion in the container registry. Therefor child Providers should
 * always return the provided functionality directly in the
 * provide{Function} method instead of setting it in the
 * container {@see App}
 */
class Provider implements ProviderInterface
{
    /**
     * Register the provided services. Will be used to find and call the
     * provide{Service} methods. You can use lowercase for the service name.
     * @var string[]
     */
    protected array $provides = [];

    /**
     * Register the provided singleton services. The key is the name of the
     * service and is used to find and call the provide{Service}Singleton
     * method. The value is the class string that will be used to register
     * and retrieve the singleton in the container.
     * @var array<string, class-string>
     */
    protected array $singletons = [];

    /**
     * Method will be called by the ProviderManager to serve the provided
     * services.
     */
    final public function provide(): void
    {
        foreach ($this->provides as $provide) {
            $method = 'provide' . StringUtility::snakeToPascalCase($provide);
            if (method_exists($this, $method) === false) {
                continue;
            }

            App::getInstance()->set($provide, static function() use ($method) {
                return static::$method();
            });
        }

        foreach ($this->singletons as $key => $classString) {
            $method = 'provide' . StringUtility::snakeToPascalCase($key) . 'Singleton';
            if (method_exists($this, $method) === false) {
                continue;
            }

            App::getInstance()->set($classString, static function() use ($method) {
                return static::$method();
            });
        }
    }
}
PK       8\~X  X  2                admin-menu-items/editor-one-theme-builder-menu.phpnu [        PK       8\ e:  :  ,              admin-menu-items/theme-builder-menu-item.phpnu [        PK       8\?s    .            P  modules/import-export-customization/wp-cli.phpnu [        PK       8\g'  '  -            (  modules/import-export-customization/usage.phpnu [        PK       8\`~  ~  -            !-  modules/import-export-customization/utils.phpnu [        PK       8\hE@i  @i  .            ?  modules/import-export-customization/module.phpnu [        PK       8\xd:    H              modules/import-export-customization/runners/import/floating-elements.phpnu [        PK       8\-  -  D              modules/import-export-customization/runners/import/site-settings.phpnu [        PK       8\;    @            `  modules/import-export-customization/runners/import/templates.phpnu [        PK       8\O	  O	  >            s  modules/import-export-customization/runners/import/plugins.phpnu [        PK       8\    A            0 modules/import-export-customization/runners/import/wp-content.phpnu [        PK       8\(uƄ    A              modules/import-export-customization/runners/import/taxonomies.phpnu [        PK       8\a    H            & modules/import-export-customization/runners/import/elementor-content.phpnu [        PK       8\ʴ    I            1F modules/import-export-customization/runners/import/import-runner-base.phpnu [        PK       8\jD}n  n  I            <K modules/import-export-customization/runners/revert/revert-runner-base.phpnu [        PK       8\c  c  D            #N modules/import-export-customization/runners/revert/site-settings.phpnu [        PK       8\1v    @            ] modules/import-export-customization/runners/revert/templates.phpnu [        PK       8\ءx9  9  >            8c modules/import-export-customization/runners/revert/plugins.phpnu [        PK       8\z)%  %  A            d modules/import-export-customization/runners/revert/wp-content.phpnu [        PK       8\ӑ    A            ul modules/import-export-customization/runners/revert/taxonomies.phpnu [        PK       8\2o51	  1	  H            o modules/import-export-customization/runners/revert/elementor-content.phpnu [        PK       8\[)  )  @            y modules/import-export-customization/runners/runner-interface.phpnu [        PK       8\Eѽ    D            @| modules/import-export-customization/runners/export/site-settings.phpnu [        PK       8\k'  '  @             modules/import-export-customization/runners/export/templates.phpnu [        PK       8\z^    I            + modules/import-export-customization/runners/export/export-runner-base.phpnu [        PK       8\a'    >             modules/import-export-customization/runners/export/plugins.phpnu [        PK       8\f    A              modules/import-export-customization/runners/export/wp-content.phpnu [        PK       8\    A            E modules/import-export-customization/runners/export/taxonomies.phpnu [        PK       8\h    H             modules/import-export-customization/runners/export/elementor-content.phpnu [        PK       8\B$c+  c+  8            R modules/import-export-customization/processes/export.phpnu [        PK       8\:T    8             modules/import-export-customization/processes/revert.phpnu [        PK       8\4b9h  9h  8            ; modules/import-export-customization/processes/import.phpnu [        PK       8\yf8    5            y modules/import-export-customization/data/response.phpnu [        PK       8\joi  i  ?            } modules/import-export-customization/data/routes/manager-url.phpnu [        PK       8\Wh    A             modules/import-export-customization/data/routes/process-media.phpnu [        PK       8\    :            B modules/import-export-customization/data/routes/export.phpnu [        PK       8\Dր    :            | modules/import-export-customization/data/routes/upload.phpnu [        PK       8\x	z    :            f modules/import-export-customization/data/routes/revert.phpnu [        PK       8\Y    :             modules/import-export-customization/data/routes/import.phpnu [        PK       8\{c	  c	  A             modules/import-export-customization/data/routes/import-runner.phpnu [        PK       8\6.{    O             modules/import-export-customization/data/routes/traits/handles-quota-errors.phpnu [        PK       8\P  P  >            y modules/import-export-customization/data/routes/base-route.phpnu [        PK       8\P>  >  7            7 modules/import-export-customization/data/controller.phpnu [        PK       8\4[
	  	  C             modules/import-export-customization/compatibility/customization.phpnu [        PK       8\El    B            R modules/import-export-customization/compatibility/base-adapter.phpnu [        PK       8\:$	    A             modules/import-export-customization/compatibility/kit-library.phpnu [        PK       8\EB    <            N modules/import-export-customization/compatibility/envato.phpnu [        PK       8\zE-  -               modules/site-editor/module.phpnu [        PK       8\M                   modules/import-export/wp-cli.phpnu [        PK       8\ApR                $& modules/import-export/usage.phpnu [        PK       8\u                * modules/import-export/utils.phpnu [        PK       8\uegf  f               m9 modules/import-export/module.phpnu [        PK       8\B    6            # modules/import-export/runners/import/site-settings.phpnu [        PK       8\G	  	  2             modules/import-export/runners/import/templates.phpnu [        PK       8\(і    0             modules/import-export/runners/import/plugins.phpnu [        PK       8\vrH  H  3             modules/import-export/runners/import/wp-content.phpnu [        PK       8\u?    3             modules/import-export/runners/import/taxonomies.phpnu [        PK       8\`X  X  :             modules/import-export/runners/import/elementor-content.phpnu [        PK       8\TQҟx  x  ;             modules/import-export/runners/import/import-runner-base.phpnu [        PK       8\yT  T  ;             modules/import-export/runners/revert/revert-runner-base.phpnu [        PK       8\-w{    6            X modules/import-export/runners/revert/site-settings.phpnu [        PK       8\PLv  v  2            . modules/import-export/runners/revert/templates.phpnu [        PK       8\,  ,  0            0 modules/import-export/runners/revert/plugins.phpnu [        PK       8\P$:    3            2 modules/import-export/runners/revert/wp-content.phpnu [        PK       8\0x    3            y9 modules/import-export/runners/revert/taxonomies.phpnu [        PK       8\
!t	  	  :            < modules/import-export/runners/revert/elementor-content.phpnu [        PK       8\yD    2            hF modules/import-export/runners/runner-interface.phpnu [        PK       8\1	  	  6            H modules/import-export/runners/export/site-settings.phpnu [        PK       8\js    2            UR modules/import-export/runners/export/templates.phpnu [        PK       8\r    ;            kX modules/import-export/runners/export/export-runner-base.phpnu [        PK       8\~{7.  .  0            [ modules/import-export/runners/export/plugins.phpnu [        PK       8\u    3            4^ modules/import-export/runners/export/wp-content.phpnu [        PK       8\!
  
  3            vf modules/import-export/runners/export/taxonomies.phpnu [        PK       8\!c    :            Xq modules/import-export/runners/export/elementor-content.phpnu [        PK       8\v夼$  $  *            t~ modules/import-export/processes/export.phpnu [        PK       8\i4  4  *             modules/import-export/processes/revert.phpnu [        PK       8\,{#|`  `  *             modules/import-export/processes/import.phpnu [        PK       8\g      4            1 modules/import-export/compatibility/base-adapter.phpnu [        PK       8\    3             modules/import-export/compatibility/kit-library.phpnu [        PK       8\;>_    .             modules/import-export/compatibility/envato.phpnu [        PK       8\@ٓ    J            + modules/kit-library/admin-menu-items/editor-one-website-templates-menu.phpnu [        PK       8\vŧ                0 modules/kit-library/module.phpnu [        PK       8\F$  F$  '            EF modules/kit-library/data/repository.phpnu [        PK       8\]y    ,            j modules/kit-library/data/base-controller.phpnu [        PK       8\PhQ,  ,  2            o modules/kit-library/data/taxonomies/controller.phpnu [        PK       8\]&[  [  9            r modules/kit-library/data/kits/endpoints/download-link.phpnu [        PK       8\5t    5            Xv modules/kit-library/data/kits/endpoints/favorites.phpnu [        PK       8\dA$    ,            z modules/kit-library/data/kits/controller.phpnu [        PK       8\2    +            ( modules/kit-library/connect/kit-library.phpnu [        PK       8\쐪    5             modules/onboarding/storage/entities/user-progress.phpnu [        PK       8\zR+    4             modules/onboarding/storage/entities/user-choices.phpnu [        PK       8\6v<l  l  :            v modules/onboarding/storage/onboarding-progress-manager.phpnu [        PK       8\lP)  )              L modules/onboarding/module.phpnu [        PK       8\=+    3            _ modules/onboarding/data/endpoints/user-progress.phpnu [        PK       8\xO~    2            L modules/onboarding/data/endpoints/user-choices.phpnu [        PK       8\2B~	  	  3             modules/onboarding/data/endpoints/install-theme.phpnu [        PK       8\*F /    1             modules/onboarding/data/endpoints/install-pro.phpnu [        PK       8\/]    8             modules/onboarding/data/endpoints/pro-install-screen.phpnu [        PK       8\F/P  P  &             modules/onboarding/data/controller.phpnu [        PK       8\E  E  8            i modules/onboarding/validation/user-choices-validator.phpnu [        PK       8\[a?S	  S	  9             modules/onboarding/validation/user-progress-validator.phpnu [        PK       8\SI$E  E  0             modules/onboarding/validation/base-validator.phpnu [        PK       8\]$  $              w! app.phpnu [        PK       8\Lm                F view.phpnu [        PK       \Ԏ    !            I Support/Utility/StringUtility.phpnu [        PK       \fP    &            M Support/Helpers/Storages/UriConfig.phpnu [        PK       \g)    +            nO Support/Helpers/Storages/RequestStorage.phpnu [        PK       \vۻB    *            S Support/Helpers/Storages/RelatedConfig.phpnu [        PK       \^#    .            U Support/Helpers/Storages/EnvironmentConfig.phpnu [        PK       \la-                W Support/Helpers/Storage.phpnu [        PK       \Dҕ<    #            o Controllers/DashboardController.phpnu [        PK       \Z                -u Managers/FeatureManager.phpnu [        PK       \o_:
  
               Managers/AbstractManager.phpnu [        PK       \1f#                M Managers/EndpointManager.phpnu [        PK       \(z  z              u Managers/ControllerManager.phpnu [        PK       \_q  q              = Managers/ProviderManager.phpnu [        PK       \Z>L  L  .             views/features/vulnerability/plugin-column.phpnu [        PK       \M      /             views/features/vulnerability/severity-label.phpnu [        PK       \1b˖    !             Services/SecureSocketsService.phpnu [        PK       \V!  !  "             Services/SettingsConfigService.phpnu [        PK       \xg                 Services/LicenseService.phpnu [        PK       \VJ,                 Services/EmailService.phpnu [        PK       \ol                 Services/CertificateService.phpnu [        PK       \$!  !  !             Services/RelatedPluginService.phpnu [        PK       \,4  4  $            8 Services/GlobalOnboardingService.phpnu [        PK       \d]  ]  >             Features/Vulnerability/Support/Helpers/VulnerabilityConfig.phpnu [        PK       \A    D             Features/Vulnerability/Controllers/VulnerabilityNoticeController.phpnu [        PK       \    7            / Features/Vulnerability/Controllers/PluginController.phpnu [        PK       \Tz*  *  B            xB Features/Vulnerability/Controllers/VulnerabilityDataController.phpnu [        PK       \ˋ'  '  6            m Features/Vulnerability/Controllers/ThemeController.phpnu [        PK       \<d    J            J Features/Vulnerability/Controllers/VulnerabilityNotificationController.phpnu [        PK       \y    ;            i Features/Vulnerability/Dtos/ComponentVulnerabilitiesDto.phpnu [        PK       \I    5            U Features/Vulnerability/Dtos/InstalledComponentDto.phpnu [        PK       \Z    9            ~ Features/Vulnerability/Dtos/HighestSeverityContextDto.phpnu [        PK       \Փ    8             Features/Vulnerability/Dtos/VulnerabilitySnapshotDto.phpnu [        PK       \ݓ    5             Features/Vulnerability/Dtos/VulnerabilityRangeDto.phpnu [        PK       \tm    .             Features/Vulnerability/VulnerabilityLoader.phpnu [        PK       \w      <            L Features/Vulnerability/Services/VulnerabilitySyncService.phpnu [        PK       \     =             Features/Vulnerability/Services/VulnerabilityEmailService.phpnu [        PK       \T93  3  G            - Features/Vulnerability/Services/Policies/ConfigurableSeverityPolicy.phpnu [        PK       \Ru    D            H Features/Vulnerability/Services/VulnerabilityPresentationService.phpnu [        PK       \RⅮ    @            L Features/Vulnerability/Services/Strategies/ThemeSyncStrategy.phpnu [        PK       \V(Uß    ?            Q Features/Vulnerability/Services/Strategies/CoreSyncStrategy.phpnu [        PK       \$    A            V Features/Vulnerability/Services/Strategies/PluginSyncStrategy.phpnu [        PK       \],.    L            \ Features/Vulnerability/Services/Strategies/AbstractComponentSyncStrategy.phpnu [        PK       \t^A1  1  A            	j Features/Vulnerability/Services/VulnerabilityAfterSyncService.phpnu [        PK       \x    6            y Features/Vulnerability/Clients/VulnerabilityClient.phpnu [        PK       \a:o  o  2             Features/Vulnerability/VulnerabilityController.phpnu [        PK       \#%  %  0             Features/Vulnerability/Traits/HasFrontendUrl.phpnu [        PK       \S1+  +  6            u Features/Vulnerability/Repositories/CoreRepository.phpnu [        PK       \Ǩҵ    7             Features/Vulnerability/Repositories/ThemeRepository.phpnu [        PK       \{^l    8             Features/Vulnerability/Repositories/PluginRepository.phpnu [        PK       \,B  B  F             Features/Vulnerability/Repositories/VulnerabilityStorageRepository.phpnu [        PK       \{    G            ;	 Features/Vulnerability/Repositories/VulnerabilitySnapshotRepository.phpnu [        PK       \xg    D            ND	 Features/Vulnerability/Interfaces/ComponentSyncStrategyInterface.phpnu [        PK       \Cj,    K            Q	 Features/Vulnerability/Interfaces/InstalledComponentRepositoryInterface.phpnu [        PK       \p8  8  A            V	 Features/Vulnerability/Interfaces/InstalledComponentInterface.phpnu [        PK       \t5    N            ZZ	 Features/Vulnerability/Interfaces/VulnerabilityNotificationPolicyInterface.phpnu [        PK       \	    N            ^	 Features/Vulnerability/Interfaces/VulnerabilitySnapshotRepositoryInterface.phpnu [        PK       \`    (            b	 Features/Onboarding/OnboardingLoader.phpnu [        PK       \J.  .  ,            Fp	 Features/Onboarding/OnboardingController.phpnu [        PK       \Fm  m  0            !	 Features/Onboarding/OnboardingFeatureService.phpnu [        PK       \3`!!  !  0            	 Features/Onboarding/OnboardingStepsGenerator.phpnu [        PK       \FO  O              	 Features/AbstractLoader.phpnu [        PK       \̠~  ~              	 Traits/HasEncryption.phpnu [        PK       \[,q                Q	 Traits/HasNonces.phpnu [        PK       \݇                	 Traits/HasAllowlistControl.phpnu [        PK       \iaQ  Q              
 Traits/HasViews.phpnu [        PK       \X"`T
  
              Y
 Traits/HasScheduler.phpnu [        PK       \                '
 Interfaces/FeatureInterface.phpnu [        PK       \+    &            
 Interfaces/SingleEndpointInterface.phpnu [        PK       \ޣj                 "
 Interfaces/ProviderInterface.phpnu [        PK       \ˬ    %            H$
 Interfaces/MultiEndpointInterface.phpnu [        PK       \G͒                 (
 Interfaces/DoActionInterface.phpnu [        PK       \|    "            b-
 Interfaces/ControllerInterface.phpnu [        PK       \v                /
 Providers/Provider.phpnu [        PK      "O  8
   