File manager - Edit - /home/dubair8/saluempire.com/core.tar
Back
app/modules/onboarding/module.php 0000755 00000003605 15111667160 0013127 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\Onboarding; use Elementor\Core\App\Modules\Onboarding\Module as Core_Onboarding_Module; use ElementorPro\Plugin; use Elementor\Core\Base\Module as BaseModule; use ElementorPro\Core\Connect\Apps\Activate; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Module extends BaseModule { /** * Get name * * @since 3.6.0 * @access public * * @return string */ public function get_name() { return 'onboarding'; } /** * Set Onboarding Settings * * Overrides the Onboarding App's Core settings with updated settings to accommodate for Elementor Pro. * * @since 3.6.0 * @access private */ private function set_onboarding_settings() { $common = Plugin::elementor()->common; $app = Plugin::elementor()->app; $onboarding_settings = $app->get_settings( 'onboarding' ); // If the installed Elementor Core version does not include the Onboarding module, exit here. if ( ! $onboarding_settings ) { return; } /** @var Activate $activate */ $activate = $common->get_component( 'connect' )->get_app( 'activate' ); $onboarding_settings['urls']['connect'] = $activate->get_admin_url( 'authorize', [ 'utm_source' => 'editor-app', 'utm_campaign' => 'connect-account', 'utm_medium' => 'wp-dash', 'utm_term' => Core_Onboarding_Module::VERSION, 'source' => 'generic', ] ); $onboarding_settings['urls']['signUp'] = $activate->get_admin_url( 'authorize', [ 'utm_source' => 'editor-app', 'utm_campaign' => 'connect-account', 'utm_medium' => 'wp-dash', 'utm_term' => Core_Onboarding_Module::VERSION, 'source' => 'generic', 'screen_hint' => 'signup', ] ); $app->set_settings( 'onboarding', $onboarding_settings ); } public function __construct() { add_action( 'elementor/init', function () { $this->set_onboarding_settings(); }, 13 /** after elementor core */ ); } } app/modules/import-export/runners/revert/templates.php 0000755 00000003403 15111667160 0017346 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\ImportExport\Runners\Revert; use Elementor\App\Modules\ImportExport\Runners\Revert\Revert_Runner_Base; use Elementor\Core\Base\Document; use Elementor\Plugin; use ElementorPro\Modules\ThemeBuilder\Module as ThemeBuilderModule; use ElementorPro\Plugin as ProPlugin; use Elementor\TemplateLibrary\Source_Local; 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(); } /** @var ThemeBuilderModule $theme_builder_module */ $theme_builder_module = ProPlugin::instance()->modules_manager->get_modules( 'theme-builder' ); $theme_builder_module->get_conditions_manager()->clear_cache(); $old_conditions = $data['runners']['templates']['template_conditions'] ?? []; foreach ( $old_conditions as $template_id => $conditions ) { $theme_builder_module->get_conditions_manager()->save_conditions( $template_id, $conditions ); } } } app/modules/import-export/runners/import/templates.php 0000755 00000007445 15111667160 0017363 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\ImportExport\Runners\Import; use Elementor\App\Modules\ImportExport\Runners\Import\Import_Runner_Base; use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils; use Elementor\Core\Base\Document; use Elementor\Plugin; use ElementorPro\Modules\ThemeBuilder\Classes\Conditions_Manager; use ElementorPro\Modules\ThemeBuilder\Module as ThemeBuilderModule; use ElementorPro\Plugin as ProPlugin; use Elementor\TemplateLibrary\Source_Local; class Templates extends Import_Runner_Base { private $import_session_id; private $templates_conditions = []; public static function get_name() : string { return 'templates'; } public function should_import( array $data ) { return ( 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( $new_document->get_error_message() ); } $template_data['import_settings'] = $template_settings; $template_data['id'] = $id; $this->set_templates_conditions( $template_data ); $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 [ 'template_conditions' => $this->templates_conditions, ]; } private function set_templates_conditions( $template_data ) { $conditions = $template_data['import_settings']['conditions']; if ( empty( $conditions ) ) { return; } $condition = $conditions[0]; $condition = rtrim( implode( '/', $condition ), '/' ); /** @var ThemeBuilderModule $theme_builder_module */ $theme_builder_module = ProPlugin::instance()->modules_manager->get_modules( 'theme-builder' ); $conditions_manager = $theme_builder_module->get_conditions_manager(); $conflicts = $conditions_manager->get_conditions_conflicts_by_location( $condition, $template_data['import_settings']['location'] ); foreach ( $conflicts as $template ) { $template_document = Plugin::$instance->documents->get( $template['template_id'] ); $template_conditions = $theme_builder_module->get_conditions_manager()->get_document_conditions( $template_document ); $this->templates_conditions[ $template['template_id'] ] = $template_conditions; } } } app/modules/import-export/runners/export/templates.php 0000755 00000002733 15111667160 0017365 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\ImportExport\Runners\Export; use Elementor\App\Modules\ImportExport\Runners\Export\Export_Runner_Base; use Elementor\Core\Base\Document; use Elementor\Plugin; use Elementor\TemplateLibrary\Source_Local; class Templates extends Export_Runner_Base { public static function get_name() : string { return 'templates'; } public function should_export( array $data ) { return ( 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, ], ]; } } app/modules/import-export/module.php 0000755 00000004350 15111667160 0013634 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\ImportExport; use Elementor\Core\Base\Module as BaseModule; use ElementorPro\Plugin; use ElementorPro\Modules\ThemeBuilder\Module as ThemeBuilderModule; use Elementor\App\Modules\ImportExport\Processes\Export; use Elementor\App\Modules\ImportExport\Processes\Import; use Elementor\App\Modules\ImportExport\Processes\Revert; use ElementorPro\Core\App\Modules\ImportExport\Runners\Import\Templates as ImportTemplates; use ElementorPro\Core\App\Modules\ImportExport\Runners\Export\Templates as ExportTemplates; use ElementorPro\Core\App\Modules\ImportExport\Runners\Revert\Templates as RevertTemplates; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Module extends BaseModule { public function get_name() { return 'import-export'; } public function __construct() { parent::__construct(); $this->add_actions(); } private function add_actions() { add_filter( 'elementor/import/get_default_settings_conflicts', function( array $conflicts, array $templates ) { return $this->apply_conditions_conflicts( $conflicts, $templates ); }, 10, 2 ); add_action( 'elementor/import-export/import-kit', function( Import $import ) { $this->register_import_kit_runners( $import ); } ); add_action( 'elementor/import-export/export-kit', function( Export $export ) { $this->register_export_kit_runners( $export ); } ); add_action( 'elementor/import-export/revert-kit', function( Revert $revert ) { $this->register_revert_kit_runners( $revert ); } ); } private function apply_conditions_conflicts( $conflicts, $templates ) { /** @var ThemeBuilderModule $theme_builder_module */ $theme_builder_module = Plugin::instance()->modules_manager->get_modules( 'theme-builder' ); if ( ! $theme_builder_module ) { return $conflicts; } return $conflicts + $theme_builder_module->get_conditions_conflicts( $templates ); } private function register_import_kit_runners( Import $import ) { $import->register( new ImportTemplates() ); } private function register_export_kit_runners( Export $export ) { $export->register( new ExportTemplates() ); } private function register_revert_kit_runners( Revert $revert ) { $revert->register( new RevertTemplates() ); } } app/modules/site-editor/render-mode-template-preview.php 0000755 00000002077 15111667160 0017443 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\SiteEditor; use Elementor\Core\Frontend\RenderModes\Render_Mode_Base; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Render_Mode_Template_Preview extends Render_Mode_Base { /** * @return string */ public static function get_name() { return 'template-preview'; } public function filter_template() { return ELEMENTOR_PATH . 'modules/page-templates/templates/canvas.php'; } public function prepare_render() { parent::prepare_render(); show_admin_bar( false ); remove_filter( 'the_content', [ \ElementorPro\Modules\ThemeBuilder\Module::instance()->get_locations_manager(), 'builder_wrapper' ], 9999999 ); add_filter( 'template_include', [ $this, 'filter_template' ] ); add_action( 'wp_head', [ $this, 'render_pointer_event_style' ] ); } /** * disable all the interactions in the preview render mode. */ public function render_pointer_event_style() { echo '<style> html { pointer-events: none; } </style>'; } public function is_static() { return true; } } app/modules/site-editor/data/endpoints/template-types.php 0000755 00000001105 15111667160 0017632 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Endpoints; use ElementorPro\Plugin; use ElementorPro\Core\App\Modules\SiteEditor\Module; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Template_Types extends Base_Endpoint { /** * @return string */ public function get_name() { return 'template-types'; } public function get_items( $request ) { /** @var Module $site_editor_module */ $site_editor_module = Plugin::instance()->app->get_component( 'site-editor' ); return $site_editor_module->get_template_types(); } } app/modules/site-editor/data/endpoints/conditions-config.php 0000755 00000001034 15111667160 0020272 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Endpoints; use ElementorPro\Modules\ThemeBuilder\Module as ThemeBuilderModule; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Conditions_Config extends Base_Endpoint { /** * @return string */ public function get_name() { return 'conditions-config'; } public function get_items( $request ) { $conditions_manager = ThemeBuilderModule::instance()->get_conditions_manager(); return $conditions_manager->get_conditions_config(); } } app/modules/site-editor/data/endpoints/templates-conditions.php 0000755 00000004207 15111667160 0021030 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Endpoints; use ElementorPro\Plugin; use Elementor\Core\Utils\Exceptions; use ElementorPro\Modules\ThemeBuilder\Module; use ElementorPro\Core\App\Modules\SiteEditor\Data\Responses\Lock_Error_Response; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Templates_Conditions extends Base_Endpoint { /** * @return string */ public function get_name() { return 'templates-conditions'; } protected function register() { $this->register_item_route(); $this->register_item_route( \WP_REST_Server::EDITABLE ); } public function get_item( $template_id, $request ) { return $this->get_conditions( $template_id ); } public function update_item( $template_id, $request ) { $lock_by_user_id = $this->is_post_lock( $template_id ); if ( $lock_by_user_id ) { return new Lock_Error_Response( $lock_by_user_id ); } $data = $request->get_body_params(); if ( ! isset( $data['conditions'] ) ) { $data['conditions'] = []; } $is_saved = $this->save_conditions( $template_id, $data['conditions'] ); if ( ! $is_saved ) { return new \WP_Error( 'conditions', __( 'Error while saving conditions.', 'elementor-pro' ), [ 'status' => Exceptions::INTERNAL_SERVER_ERROR ] ); } return true; } protected function get_conditions( $post_id ) { $document = \Elementor\Plugin::$instance->documents->get( $post_id ); /** @var Module $theme_builder */ $theme_builder = Plugin::instance()->modules_manager->get_modules( 'theme-builder' ); return $theme_builder ->get_conditions_manager() ->get_document_conditions( $document ); } protected function save_conditions( $post_id, $conditions ) { /** @var Module $theme_builder */ $theme_builder = Plugin::instance()->modules_manager->get_modules( 'theme-builder' ); $is_saved = $theme_builder ->get_conditions_manager() ->save_conditions( $post_id, $conditions ); if ( ! $is_saved ) { return new \WP_Error( 'conditions_save', __( 'Cannot save those conditions.', 'elementor-pro' ), [ 'status' => Exceptions::INTERNAL_SERVER_ERROR ] ); } return true; } } app/modules/site-editor/data/endpoints/templates-conditions-conflicts.php 0000755 00000001271 15111667160 0023010 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Endpoints; use ElementorPro\Plugin; use ElementorPro\Modules\ThemeBuilder\Module; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Templates_Conditions_Conflicts extends Base_Endpoint { /** * @return string */ public function get_name() { return 'templates-conditions-conflicts'; } public function get_items( $request ) { /** @var Module $theme_builder */ $theme_builder = Plugin::instance()->modules_manager->get_modules( 'theme-builder' ); return $theme_builder ->get_conditions_manager() ->get_conditions_conflicts( intval( $request['post_id'] ), $request['condition'] ); } } app/modules/site-editor/data/endpoints/templates.php 0000755 00000015223 15111667160 0016661 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Endpoints; use ElementorPro\Plugin; use Elementor\Core\Utils\Exceptions; use Elementor\TemplateLibrary\Manager as TemplateManager; use ElementorPro\Modules\ThemeBuilder\Documents\Theme_Document; use ElementorPro\Modules\ThemeBuilder\Classes\Conditions_Manager; use ElementorPro\Modules\ThemeBuilder\Module as ThemeBuilderModule; use ElementorPro\Modules\ThemeBuilder\Classes\Templates_Types_Manager; use ElementorPro\Core\App\Modules\SiteEditor\Render_Mode_Template_Preview; use ElementorPro\Core\App\Modules\SiteEditor\Data\Responses\Lock_Error_Response; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Templates extends Base_Endpoint { /** * @var TemplateManager */ private $templates_manager; /** * @var array */ private $document_types; public function __construct( $controller ) { parent::__construct( $controller ); $this->templates_manager = Plugin::elementor()->templates_manager; } /** * @return string */ public function get_name() { return 'templates'; } protected function register() { parent::register(); $this->register_item_route( \WP_REST_Server::DELETABLE ); $this->register_item_route( \WP_REST_Server::EDITABLE ); $this->register_items_route( \WP_REST_Server::CREATABLE ); } public function get_items( $request ) { $templates = $this->templates_manager->get_source( 'local' )->get_items( [ 'type' => array_keys( $this->get_documents_types() ), 'post_status' => 'any', 'orderby' => 'post_date', 'order' => 'DESC', ] ); return $this->normalize_templates_json( $templates ); } public function create_items( $request ) { $response = $this->templates_manager->import_template( $request->get_body_params() ); if ( is_wp_error( $response ) ) { return new \WP_Error( 'file', $response->get_error_message(), [ 'status' => Exceptions::BAD_REQUEST ] ); } return $this->normalize_templates_json( $response ); } public function update_item( $id, $request ) { $lock_by_user_id = $this->is_post_lock( $id ); if ( $lock_by_user_id ) { return new Lock_Error_Response( $lock_by_user_id ); } wp_update_post( array_merge( [ 'ID' => $id, ], $request->get_body_params() ) ); return $this->normalize_template_json_item( $this->templates_manager->get_source( 'local' )->get_item( $id ) ); } public function delete_item( $id, $request ) { $lock_by_user_id = $this->is_post_lock( $id ); if ( $lock_by_user_id ) { return new Lock_Error_Response( $lock_by_user_id ); } return ! ! wp_trash_post( $id ); } /** * @return array */ private function get_documents_types() { if ( ! $this->document_types ) { /** @var Templates_Types_Manager $types_manager */ $types_manager = ThemeBuilderModule::instance()->get_types_manager(); $this->document_types = $types_manager->get_types_config( [ 'support_site_editor' => true, ] ); } return $this->document_types; } /** * @param $templates * * @return array */ private function normalize_templates_json( $templates ) { return array_map( [ $this, 'normalize_template_json_item' ], $templates ); } /** * @param $template * * @return array */ private function normalize_template_json_item( $template ) { /** @var Conditions_Manager $conditions_manager */ $conditions_manager = Plugin::instance()->modules_manager->get_modules( 'theme-builder' )->get_conditions_manager(); /** @var Theme_Document $document */ $document = Plugin::elementor()->documents->get( $template['template_id'] ); $supports_site_editor = $document::get_property( 'support_site_editor' ); // Supports also a non site editor parts. if ( ! $supports_site_editor ) { return [ 'id' => $template['template_id'], 'url' => $template['url'], 'editURL' => $document->get_edit_url(), 'supportsSiteEditor' => false, ]; } $types = $this->get_documents_types(); $template['instances'] = $conditions_manager->get_document_instances( $template['template_id'] ); $template['defaultCondition'] = $types[ $template['type'] ]['condition_type']; $has_instances = ! empty( $template['instances'] ); $is_active = false; if ( ! $has_instances ) { $template['instances'] = [ 'no_instances' => esc_html__( 'No instances', 'elementor-pro' ) ]; } else { $is_active = 'publish' === $template['status']; } if ( ! $template['thumbnail'] ) { $template['thumbnail'] = ''; } $site_editor_config = $document->get_site_editor_config(); $data = array_merge( $template, [ 'id' => $template['template_id'], 'exportLink' => $template['export_link'], 'modifiedDate' => $template['human_modified_date'], 'editURL' => $document->get_edit_url(), 'conditions' => array_map( function ( $condition ) { return array_merge( $condition, [ 'sub' => $condition['sub_name'], 'subId' => $condition['sub_id'], ] ); }, $conditions_manager->get_document_conditions( $document ) ), 'isActive' => $is_active, 'type' => $this->calculate_template_type( $template['type'], $template['instances'] ), 'previewUrl' => $this->get_preview_url( $template['template_id'] ), 'placeholderUrl' => $site_editor_config['urls']['thumbnail'], 'pageLayout' => $site_editor_config['page_layout'], 'supportsSiteEditor' => true, 'showInstances' => $site_editor_config['show_instances'], ] ); /** * Template data. * * Filters the data returned by Elementor API as JSON. * * By default Elementor API returns data in a JSON format that enables the * builder to work properly. This hook allows developers to alter the data * returned by the API to add new elements. * * @param array $data Template data. */ $data = apply_filters( 'elementor-pro/site-editor/data/template', $data ); return $data; } /** * @param $type * @param $instances * * @return string */ private function calculate_template_type( $type, $instances ) { $condition_to_type_map = [ 'front_page' => 'single-page', 'child_of' => 'single-page', 'page' => 'single-page', 'not_found404' => 'error-404', 'search' => 'search-results', ]; // "single" type was split into "single-page", "single-post" and "404". // this section supports all the old templates that was created as "single". if ( 'single' === $type ) { // By default show it under single-post. $type = 'single-post'; foreach ( $instances as $condition_name => $condition_label ) { if ( isset( $condition_to_type_map[ $condition_name ] ) ) { $type = $condition_to_type_map[ $condition_name ]; break; } } } return $type; } private function get_preview_url( $post_id ) { return Render_Mode_Template_Preview::get_url( $post_id ); } } app/modules/site-editor/data/endpoints/base-endpoint.php 0000755 00000000726 15111667160 0017415 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Endpoints; use Elementor\Data\Base\Endpoint; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } abstract class Base_Endpoint extends Endpoint { /** * Check if post is lock. * * @param $post_id * * @return bool|false|int */ protected function is_post_lock( $post_id ) { require_once ABSPATH . 'wp-admin/includes/post.php'; return wp_check_post_lock( $post_id ); } } app/modules/site-editor/data/controller.php 0000755 00000001423 15111667160 0015040 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\SiteEditor\Data; use ElementorPro\Plugin; use Elementor\Data\Base\Controller as Controller_Base; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Controller extends Controller_Base { public function get_name() { return 'site-editor'; } public function register_endpoints() { $this->register_endpoint( Endpoints\Templates::class ); $this->register_endpoint( Endpoints\Conditions_Config::class ); $this->register_endpoint( Endpoints\Templates_Conditions::class ); $this->register_endpoint( Endpoints\Templates_Conditions_Conflicts::class ); } public function get_permission_callback( $request ) { return Plugin::elementor()->kits_manager->get_active_kit()->is_editable_by_current_user(); } } app/modules/site-editor/data/responses/lock-error-response.php 0000755 00000001170 15111667160 0020610 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Responses; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Lock_Error_Response extends \WP_Error { public function __construct( $user_id ) { $user = get_user_by( 'ID', $user_id ); parent::__construct( 'post_lock', sprintf( /* translators: %s: User display name. */ esc_html__( '%s is currently editing this template, please try again later', 'elementor-pro' ), $user->display_name ), [ 'status' => 403, 'locked_by_user_id' => $user_id, 'locked_by_user_name' => $user->display_name, ] ); } } app/modules/site-editor/assets/js/site-editor.scss 0000755 00000000401 15111667160 0016271 0 ustar 00 .e-site-editor__content_container { flex-direction: column; min-height: 100%; flex-wrap: nowrap; } .e-site-editor__content_container_main { flex: 1; } .e-site-editor__content_container_secondary { margin: 0 auto; padding-block-start: spacing(44); } app/modules/site-editor/assets/js/molecules/site-template-body.js 0000755 00000001427 15111667160 0021213 0 ustar 00 import { CardBody } from '@elementor/app-ui'; import SiteTemplateThumbnail from './site-template-thumbnail'; import PreviewIFrame from '../atoms/preview-iframe'; export const SiteTemplateBody = ( props ) => { return ( <CardBody> { props.extended ? <PreviewIFrame src={ props.previewUrl } templateType={ props.type } /> : <SiteTemplateThumbnail id={ props.id } title={ props.title } type={ props.type } thumbnail={ props.thumbnail } placeholder={ props.placeholderUrl } /> } </CardBody> ); }; SiteTemplateBody.propTypes = { extended: PropTypes.bool, id: PropTypes.number, title: PropTypes.string, thumbnail: PropTypes.string, placeholderUrl: PropTypes.string, type: PropTypes.string, previewUrl: PropTypes.string, }; app/modules/site-editor/assets/js/molecules/back-button.js 0000755 00000000711 15111667160 0017707 0 ustar 00 import { Button } from '@elementor/app-ui'; import './back-button.scss'; export default function BackButton( props ) { return ( <div className="back-button-wrapper"> <Button className="eps-back-button" text={ __( 'Back', 'elementor-pro' ) } icon="eicon-chevron-left" onClick={ props.onClick } /> </div> ); } BackButton.propTypes = { onClick: PropTypes.func, }; BackButton.defaultProps = { onClick: () => history.back(), }; app/modules/site-editor/assets/js/molecules/site-template-header.js 0000755 00000003046 15111667160 0021505 0 ustar 00 import { Button, CardHeader, Heading, Icon, Text } from '@elementor/app-ui'; import PartActionsButtons from '../part-actions/dialogs-and-buttons'; import { Indicator } from '../atoms/indicator-bullet'; export const SiteTemplateHeader = ( props ) => { const status = props.status && 'publish' !== props.status ? ` (${ props.status })` : '', title = props.title + status, ActionButtons = () => ( <> <Button text={ __( 'Edit', 'elementor-pro' ) } icon="eicon-edit" className="e-site-template__edit-btn" size="sm" url={ props.editURL } /> <PartActionsButtons { ... props } /> </> ), MetaDataIcon = ( innerProps ) => ( <Text tag="span" className="e-site-template__meta-data"> <Icon className={ innerProps.icon } /> { innerProps.content } </Text> ), MetaData = () => ( <> <MetaDataIcon icon="eicon-user-circle-o" content={ props.author } /> <MetaDataIcon icon="eicon-clock-o" content={ props.modifiedDate } /> </> ), IndicatorDot = props.showInstances ? <Indicator active={ props.isActive } /> : ''; return ( <CardHeader> { IndicatorDot } <Heading tag="h1" title={ title } variant="text-sm" className="eps-card__headline">{ title }</Heading> { props.extended && <MetaData /> } { props.extended && <ActionButtons /> } </CardHeader> ); }; SiteTemplateHeader.propTypes = { isActive: PropTypes.bool, author: PropTypes.string, editURL: PropTypes.string, extended: PropTypes.bool, modifiedDate: PropTypes.string, status: PropTypes.string, title: PropTypes.string, showInstances: PropTypes.bool, }; app/modules/site-editor/assets/js/molecules/back-button.scss 0000755 00000000254 15111667160 0020250 0 ustar 00 .#{$eps-prefix}back-button { font-size: 14px; margin-block-end: spacing(24); .#{$eps-prefix}icon { transform: getValueByDirection(rotate(0deg), rotate(180deg)); } } app/modules/site-editor/assets/js/molecules/site-template.js 0000755 00000002723 15111667160 0020260 0 ustar 00 import { Card } from '@elementor/app-ui'; import { SiteTemplateHeader } from './site-template-header'; import { SiteTemplateBody } from './site-template-body'; import { SiteTemplateFooter } from './site-template-footer'; import './site-template.scss'; export default function SiteTemplate( props ) { const baseClassName = 'e-site-template', classes = [ baseClassName ], ref = React.useRef( null ); React.useEffect( () => { if ( ! props.isSelected ) { return; } ref.current.scrollIntoView( { behavior: 'smooth', block: 'start', } ); }, [ props.isSelected ] ); if ( props.extended ) { classes.push( `${ baseClassName }--extended` ); } if ( props.aspectRatio ) { classes.push( `${ baseClassName }--${ props.aspectRatio }` ); } const CardFooter = props.extended && props.showInstances ? <SiteTemplateFooter { ...props } /> : ''; return ( <Card className={ classes.join( ' ' ) } ref={ ref }> <SiteTemplateHeader { ... props } /> <SiteTemplateBody { ... props } /> { CardFooter } </Card> ); } SiteTemplate.propTypes = { aspectRatio: PropTypes.string, className: PropTypes.string, extended: PropTypes.bool, id: PropTypes.number.isRequired, isActive: PropTypes.bool.isRequired, status: PropTypes.string, thumbnail: PropTypes.string.isRequired, title: PropTypes.string.isRequired, isSelected: PropTypes.bool, type: PropTypes.string.isRequired, showInstances: PropTypes.bool, }; SiteTemplate.defaultProps = { isSelected: false, }; app/modules/site-editor/assets/js/molecules/site-template-thumbnail.js 0000755 00000001436 15111667160 0022241 0 ustar 00 import { Button, CardImage, CardOverlay } from '@elementor/app-ui'; export default function SiteTemplateThumbnail( props ) { return ( <CardImage alt={ props.title } src={ props.thumbnail || props.placeholder } className={ ! props.thumbnail ? 'e-site-template__placeholder' : '' } > <CardOverlay className="e-site-template__overlay-preview"> <Button className="e-site-template__overlay-preview-button" text={ __( 'Preview', 'elementor-pro' ) } icon="eicon-preview-medium" url={ `/site-editor/templates/${ props.type }/${ props.id }` } /> </CardOverlay> </CardImage> ); } SiteTemplateThumbnail.propTypes = { id: PropTypes.number, title: PropTypes.string, type: PropTypes.string, thumbnail: PropTypes.string, placeholder: PropTypes.string, }; app/modules/site-editor/assets/js/molecules/site-template-footer.js 0000755 00000001416 15111667160 0021552 0 ustar 00 import { Button, CardFooter, Icon, Text } from '@elementor/app-ui'; export const SiteTemplateFooter = ( props ) => { const instances = Object.values( props.instances ).join( ', ' ); return ( <CardFooter> <div className="e-site-template__instances"> <Icon className="eicon-flow" /> <Text tag="span" variant="sm"><b>{ __( 'Instances', 'elementor-pro' ) }:</b></Text> <Text className="e-site-template__instances-list" tag="span" variant="xxs"> { instances }</Text> <Button text={ __( 'Edit Conditions', 'elementor-pro' ) } className="e-site-template__edit-conditions" url={ `/site-editor/conditions/${ props.id }` } /> </div> </CardFooter> ); }; SiteTemplateFooter.propTypes = { id: PropTypes.number.isRequired, instances: PropTypes.any, }; app/modules/site-editor/assets/js/molecules/site-template.scss 0000755 00000004361 15111667160 0020617 0 ustar 00 $eps-meta-icon-color: tints(400); $eps-meta-icon-dark-color: dark-tints(200); $preview-button-height: spacing(30); $image-aspect-ratio: var(--card-image-aspect-ratio, #{$ratio-portrait}); $overlay-preview-padding-block-start: calc(#{$image-aspect-ratio} - #{$preview-button-height}); $aspect-ratio-wide: calc(100% * 0.1235); :root { --eps-meta-icon-color: #{$eps-meta-icon-color}; } .eps-theme-dark { --eps-meta-icon-color: #{$eps-meta-icon-dark-color}; } .e-site-template { &__meta-data { margin-inline-start: spacing(10); @include text-truncate(); font-size: type(text, xxs); &:last-of-type { margin-inline-end: auto; } &:first-of-type { margin-inline-start: spacing(16); } .#{$eps-prefix}icon { margin-inline-end: spacing(5); color: var(--eps-meta-icon-color); font-size: type(text, sm); } } &__placeholder { .#{$eps-prefix}card__image { filter: var(--placeholder-filter, none); } } &__overlay-preview { padding-block-start: $overlay-preview-padding-block-start; position: relative; &-button { font-weight: bold; display: flex; flex-wrap: wrap; height: $preview-button-height; width: 100%; background-color: var(--card-background-color-hover); justify-content: center; align-items: center; padding-block-start: spacing(10); line-height: spacing(20); --button-background-color: var(--card-headline-color); &::before { content: ''; position: absolute; display: block; width: 100%; top: 0; left: 0; padding-block-start: $overlay-preview-padding-block-start; } & > :not(:first-child) { margin-inline-start: spacing(5); } } } &__edit-btn { margin-inline-end: spacing(20); .#{$eps-prefix}icon { margin-inline-end: spacing(5); } } &__instances { .#{$eps-prefix}icon { margin-inline-end: spacing(5); color: var(--eps-meta-icon-color); font-size: type(text, sm) } &-list { @include text-truncate(); } } &__edit-conditions { margin-inline-start: spacing(16); text-decoration: underline; font-style: italic; } &--extended { .#{$eps-prefix}card { &__figure { overflow: auto; } &__headline { flex-grow: 0; } } } &--wide { --card-image-aspect-ratio: #{$aspect-ratio-wide}; } } app/modules/site-editor/assets/js/atoms/indicator-bullet.js 0000755 00000000431 15111667160 0020071 0 ustar 00 import './indicator-bullet.scss'; export const Indicator = ( props ) => { let className = 'eps-indicator-bullet'; if ( props.active ) { className += ` ${ className }--active`; } return <i className={ className } />; }; Indicator.propTypes = { active: PropTypes.bool, }; app/modules/site-editor/assets/js/atoms/preview-iframe.js 0000755 00000002211 15111667160 0017550 0 ustar 00 import './preview-iframe.scss'; export default function PreviewIFrame( props ) { const ref = React.useRef( null ), previewBreakpoint = 1200, [ scale, setScale ] = React.useState( 1 ), [ height, setHeight ] = React.useState( 0 ); // In order to make sure that the iframe itself show the content in specific viewport, // and it should fit to the size of the card, there is a use of css props `scale` and `height`, // and another element that wraps the iframe to be the guidelines of the iframe sizes. React.useEffect( () => { const currentScale = ref.current.clientWidth / previewBreakpoint; setScale( currentScale ); setHeight( ref.current.clientHeight / currentScale ); }, [] ); return ( <div ref={ ref } className={ `site-editor__preview-iframe site-editor__preview-iframe--${ props.templateType }` } > <iframe title="preview" src={ props.src } className={ `site-editor__preview-iframe__iframe` } style={ { transform: `scale(${ scale })`, height, width: previewBreakpoint } } /> </div> ); } PreviewIFrame.propTypes = { src: PropTypes.string.isRequired, templateType: PropTypes.string.isRequired, }; app/modules/site-editor/assets/js/atoms/indicator-bullet.scss 0000755 00000002110 15111667160 0020424 0 ustar 00 $eps-indicator-bullet-border-color: theme-colors(light); $eps-indicator-bullet-dark-border-color: dark-tints(500); :root { --indicator-bullet-border-color: #{$eps-indicator-bullet-border-color}; } .eps-theme-dark { --indicator-bullet-border-color: #{$eps-indicator-bullet-dark-border-color}; } $eps-indicator-bullet-size: spacing(16) * 0.75; $eps-indicator-bullet-color: tints(300); $eps-indicator-bullet-color-active: theme-colors(success); $eps-indicator-bullet-box-shadow: $eps-box-shadow-1; $eps-indicator-bullet-radius: 100%; $eps-indicator-bullet-border: 2px solid var(--indicator-bullet-border-color); $eps-indicator-bullet-spacing: spacing(10); .eps-indicator-bullet { display: block; flex-shrink: 0; width: $eps-indicator-bullet-size; height: $eps-indicator-bullet-size; box-shadow: $eps-indicator-bullet-box-shadow; background-color: $eps-indicator-bullet-color; border: $eps-indicator-bullet-border; border-radius: $eps-indicator-bullet-radius; margin-inline-end: $eps-indicator-bullet-spacing; &--active { background-color: $eps-indicator-bullet-color-active; } } app/modules/site-editor/assets/js/atoms/preview-iframe.scss 0000755 00000000345 15111667160 0020115 0 ustar 00 .site-editor__preview-iframe { height: 50vh; position: relative; &__iframe { top: 0; left: 0; position: absolute; border: none; transform-origin: 0 0; height: 100%; } &--header, &--footer{ height: 15vh; } } app/modules/site-editor/assets/js/part-actions/dialog-delete.js 0000755 00000002113 15111667160 0020607 0 ustar 00 import { Dialog } from '@elementor/app-ui'; import { Context as TemplatesContext } from '../context/templates'; export default function DialogDelete( props ) { const { deleteTemplate, findTemplateItemInState } = React.useContext( TemplatesContext ); const closeDialog = ( shouldUpdate ) => { props.setId( null ); if ( shouldUpdate ) { deleteTemplate( props.id ); } }; if ( ! props.id ) { return ''; } const template = findTemplateItemInState( props.id ); return ( <Dialog title={ __( 'Move Item To Trash', 'elementor-pro' ) } text={ __( 'Are you sure you want to move this item to trash:', 'elementor-pro' ) + ` "${ template.title }"` } onSubmit={ () => closeDialog( true ) } approveButtonText={ __( 'Move to Trash', 'elementor-pro' ) } approveButtonOnClick={ () => closeDialog( true ) } approveButtonColor="danger" dismissButtonText={ __( 'Cancel', 'elementor-pro' ) } dismissButtonOnClick={ () => closeDialog() } onClose={ () => closeDialog() } /> ); } DialogDelete.propTypes = { id: PropTypes.number, setId: PropTypes.func.isRequired, }; app/modules/site-editor/assets/js/part-actions/dialog-rename.js 0000755 00000002724 15111667160 0020624 0 ustar 00 import { useEffect } from 'react'; import { Dialog } from '@elementor/app-ui'; import { Context as TemplatesContext } from '../context/templates'; export default function DialogRename( props ) { const { findTemplateItemInState, updateTemplate } = React.useContext( TemplatesContext ), template = findTemplateItemInState( props.id ); const [ title, setTitle ] = React.useState( '' ); useEffect( () => { // The "title" state should be updated if the template title changed. if ( template ) { setTitle( template.title ); } }, [ template ] ); const closeDialog = ( shouldUpdate ) => { props.setId( null ); if ( shouldUpdate ) { updateTemplate( props.id, { post_title: title } ); } }; if ( ! props.id ) { return ''; } return ( <Dialog title={ __( 'Rename Site Part', 'elementor-pro' ) } approveButtonText={ __( 'Change', 'elementor-pro' ) } onSubmit={ () => closeDialog( true ) } approveButtonOnClick={ () => closeDialog( true ) } approveButtonColor="primary" dismissButtonText={ __( 'Cancel', 'elementor-pro' ) } dismissButtonOnClick={ () => closeDialog() } onClose={ () => closeDialog() } > <input type="text" className="eps-input eps-input-text eps-input--block" // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus value={ title } onChange={ ( e ) => setTitle( e.target.value ) } /> </Dialog> ); } DialogRename.propTypes = { id: PropTypes.number, setId: PropTypes.func.isRequired, }; app/modules/site-editor/assets/js/part-actions/dialogs-and-buttons.js 0000755 00000003671 15111667160 0022000 0 ustar 00 import DialogRename from './dialog-rename'; import DialogDelete from './dialog-delete'; import { Button, Popover } from '@elementor/app-ui'; export const handlers = { rename: null, delete: null, }; // TODO: Think about refactor to portals: https://reactjs.org/docs/portals.html export function PartActionsDialogs() { const [ DialogRenameId, setDialogRenameId ] = React.useState( null ); const [ DialogDeleteId, setDialogDeleteId ] = React.useState( null ); handlers.rename = setDialogRenameId; handlers.delete = setDialogDeleteId; return ( <> <DialogRename id={ DialogRenameId } setId={ setDialogRenameId } /> <DialogDelete id={ DialogDeleteId } setId={ setDialogDeleteId } /> </> ); } export default function PartActionsButtons( props ) { const [ showMenu, setShowMenu ] = React.useState( false ); let SiteTemplatePopover = ''; if ( showMenu ) { SiteTemplatePopover = ( <Popover closeFunction={ () => setShowMenu( ! showMenu ) }> <li> <Button className="eps-popover__item" icon="eicon-sign-out" text={ __( 'Export', 'elementor-pro' ) } url={ props.exportLink } /> </li> <li> <Button className="eps-popover__item eps-popover__item--danger" icon="eicon-trash-o" text={ __( 'Trash', 'elementor-pro' ) } onClick={ () => handlers.delete( props.id ) } /> </li> <li> <Button className="eps-popover__item" icon="eicon-edit" text={ __( 'Rename', 'elementor-pro' ) } onClick={ () => handlers.rename( props.id ) } /> </li> </Popover> ); } return ( <div className="eps-popover__container"> <Button text={ __( 'Toggle', 'elementor-pro' ) } hideText={ true } icon="eicon-ellipsis-h" size="lg" onClick={ () => setShowMenu( ! showMenu ) } /> { SiteTemplatePopover } </div> ); } PartActionsButtons.propTypes = { id: PropTypes.number.isRequired, exportLink: PropTypes.string.isRequired, }; app/modules/site-editor/assets/js/editor.js 0000755 00000000652 15111667160 0015000 0 ustar 00 import Component from './data/component'; import { Templates } from './data/commands'; export default class Module extends elementorModules.editor.utils.Module { onElementorInit() { const config = elementor.documents.getCurrent().config; if ( config.support_site_editor ) { $e.components.register( new Component() ); $e.data.deleteCache( $e.components.get( Component.namespace ), Templates.signature ); } } } app/modules/site-editor/assets/js/hooks/use-templates-screenshot.js 0000755 00000003626 15111667160 0021604 0 ustar 00 import { Context as TemplatesContext } from '../context/templates'; import useScreenshot, { SCREENSHOT_STATUS_SUCCEED, SCREENSHOT_STATUS_FAILED } from 'modules/screenshots/app/assets/js/hooks/use-screenshot'; /** * Wrapper function that was made to take screenshots specific for template. * it will capture a screenshot and update the templates context with the new screenshot. * * @param {any} templateType */ export default function useTemplatesScreenshot( templateType = null ) { const { updateTemplateItemState, templates } = React.useContext( TemplatesContext ); const templatesForScreenshot = Object.values( templates ).filter( ( template ) => shouldScreenshotTemplate( template, templateType ), ); // Start to capture screenshots. const screenshot = useScreenshot( templatesForScreenshot ); // Update the thumbnail url when screenshot created. React.useEffect( () => { screenshot.posts .filter( ( post ) => post.status === SCREENSHOT_STATUS_SUCCEED ) .forEach( ( post ) => updateTemplateItemState( post.id, { thumbnail: post.imageUrl } ) ); }, [ screenshot.succeed ] ); // Update the screenshot url that was failed. // When the user will hit the route on the second time it will avoid trying to take another screenshot. React.useEffect( () => { screenshot.posts .filter( ( post ) => post.status === SCREENSHOT_STATUS_FAILED ) .forEach( ( post ) => updateTemplateItemState( post.id, { screenshot_url: null } ) ); }, [ screenshot.failed ] ); return screenshot; } /** * Filter handler. * will remove all the drafts and private and also will filter by template type if exists. * * @param {any} template * @param {any} templateType * @return {boolean} should screenshot template */ function shouldScreenshotTemplate( template, templateType = null ) { if ( templateType ) { return false; } return 'publish' === template.status && ! template.thumbnail && template.screenshot_url; } app/modules/site-editor/assets/js/data/commands/templates.js 0000755 00000000313 15111667160 0020214 0 ustar 00 export class Templates extends $e.modules.CommandData { static signature = 'site-editor/templates'; static getEndpointFormat() { return 'site-editor/templates/{id}'; } } export default Templates; app/modules/site-editor/assets/js/data/commands/templates-conditions.js 0000755 00000000365 15111667160 0022372 0 ustar 00 export class TemplatesConditions extends $e.modules.CommandData { static signature = 'site-editor/templates-conditions'; static getEndpointFormat() { return 'site-editor/templates-conditions/{id}'; } } export default TemplatesConditions; app/modules/site-editor/assets/js/data/commands/templates-conditions-conflicts.js 0000755 00000000434 15111667160 0024351 0 ustar 00 export class TemplatesConditionsConflicts extends $e.modules.CommandData { static signature = 'site-editor/templates-conditions-conflicts'; static getEndpointFormat() { return `${ TemplatesConditionsConflicts.signature }/{id}`; } } export default TemplatesConditionsConflicts; app/modules/site-editor/assets/js/data/commands/index.js 0000755 00000000360 15111667160 0017327 0 ustar 00 export { Templates } from './templates'; export { ConditionsConfig } from './conditions-config'; export { TemplatesConditions } from './templates-conditions'; export { TemplatesConditionsConflicts } from './templates-conditions-conflicts'; app/modules/site-editor/assets/js/data/commands/conditions-config.js 0000755 00000000351 15111667160 0021634 0 ustar 00 export class ConditionsConfig extends $e.modules.CommandData { static signature = 'site-editor/conditions-config'; static getEndpointFormat() { return 'site-editor/conditions-config/{id}'; } } export default ConditionsConfig; app/modules/site-editor/assets/js/data/component.js 0000755 00000000422 15111667160 0016420 0 ustar 00 import * as dataCommands from './commands'; export default class Component extends $e.modules.ComponentBase { static namespace = 'site-editor'; getNamespace() { return this.constructor.namespace; } defaultData() { return this.importCommands( dataCommands ); } } app/modules/site-editor/assets/js/site-editor.js 0000755 00000005750 15111667160 0015746 0 ustar 00 import { Router, LocationProvider, Redirect } from '@reach/router'; import Templates from './pages/templates'; import TemplateType from './pages/template-type'; import AddNew from './pages/add-new'; import Conditions from './pages/conditions/conditions'; import Import from './pages/import'; import TemplatesProvider, { Context as TemplatesContext } from './context/templates'; import { Layout, AllPartsButton, NotFound } from '@elementor/site-editor'; import { ErrorBoundary, Grid, Button } from '@elementor/app-ui'; import router from '@elementor/router'; import Component from './data/component'; import useFeatureLock from 'elementor-pro-app/hooks/use-feature-lock'; import './site-editor.scss'; function SiteEditor() { const { isLocked } = useFeatureLock( 'site-editor' ); const basePath = 'site-editor'; const headerButtons = [ { id: 'import', text: __( 'import', 'elementor-pro' ), hideText: true, icon: 'eicon-upload-circle-o', onClick: () => router.appHistory.navigate( basePath + '/import' ), }, ]; // Remove Core cache. elementorCommon.ajax.invalidateCache( { unique_id: 'app_site_editor_template_types', } ); const SiteEditorDefault = () => { const { templates } = React.useContext( TemplatesContext ); if ( Object.keys( templates ).length ) { return <Redirect from={ '/' } to={ '/' + basePath + '/templates' } noThrow={ true } />; } return <Redirect from={ '/' } to={ '/' + basePath + '/add-new' } noThrow={ true } />; }; return ( <ErrorBoundary title={ __( 'Theme Builder could not be loaded', 'elementor-pro' ) } learnMoreUrl="https://go.elementor.com/app-theme-builder-load-issue" > <Layout allPartsButton={ <AllPartsButton url={ '/' + basePath } /> } headerButtons={ headerButtons } titleRedirectRoute={ '/' + basePath } promotion={ isLocked }> <Grid container className="e-site-editor__content_container"> <Grid item className="e-site-editor__content_container_main"> <TemplatesProvider> <LocationProvider history={ router.appHistory }> <Router> <SiteEditorDefault path={ basePath } /> <Templates path={ basePath + '/templates' } /> <TemplateType path={ basePath + '/templates/:type/*id' } /> <AddNew path={ basePath + '/add-new' } /> <Conditions path={ basePath + '/conditions/:id' } /> <Import path={ basePath + '/import' } /> <NotFound default /> </Router> </LocationProvider> </TemplatesProvider> </Grid> <Grid item className="e-site-editor__content_container_secondary"> <Button text={ __( 'Switch to table view', 'elementor-pro' ) } url={ elementorAppProConfig[ 'site-editor' ]?.urls?.legacy_view } /> </Grid> </Grid> </Layout> </ErrorBoundary> ); } export default class Module { constructor() { elementorCommon.debug.addURLToWatch( 'elementor-pro/assets' ); $e.components.register( new Component() ); router.addRoute( { path: '/site-editor/*', component: SiteEditor, } ); } } app/modules/site-editor/assets/js/context/base-context.js 0000755 00000002021 15111667160 0017562 0 ustar 00 export class BaseContext extends React.Component { constructor( props ) { super( props ); this.state = { action: { current: null, loading: false, error: null, errorMeta: {}, }, updateActionState: this.updateActionState.bind( this ), resetActionState: this.resetActionState.bind( this ), }; } executeAction( name, handler ) { this.updateActionState( { current: name, loading: true, error: null, errorMeta: {} } ); return handler() .then( ( response ) => { this.resetActionState(); return Promise.resolve( response ); } ) .catch( ( error ) => { this.updateActionState( { current: name, loading: false, error: error.message, errorMeta: error } ); return Promise.reject( error ); } ); } updateActionState( data ) { return this.setState( ( prev ) => ( { action: { ...prev.action, ...data, }, } ) ); } resetActionState() { this.updateActionState( { current: null, loading: false, error: null, errorMeta: {} } ); } } export default BaseContext; app/modules/site-editor/assets/js/context/templates.js 0000755 00000006744 15111667160 0017204 0 ustar 00 import BaseContext from './base-context'; import { Templates } from '../data/commands'; import Component from '../data/component'; export const Context = React.createContext(); export class TemplatesProvider extends BaseContext { static propTypes = { children: PropTypes.object.isRequired, }; static actions = { FETCH: 'fetch', DELETE: 'delete', UPDATE: 'update', IMPORT: 'import', }; constructor( props ) { super( props ); this.state = { ...this.state, action: { ...this.state.action, current: TemplatesProvider.actions.FETCH, loading: true, }, templates: {}, updateTemplateItemState: this.updateTemplateItemState.bind( this ), findTemplateItemInState: this.findTemplateItemInState.bind( this ), fetchTemplates: this.fetchTemplates.bind( this ), deleteTemplate: this.deleteTemplate.bind( this ), updateTemplate: this.updateTemplate.bind( this ), importTemplates: this.importTemplates.bind( this ), }; } componentDidMount() { this.fetchTemplates(); } importTemplates( { fileName, fileData } ) { return this.executeAction( TemplatesProvider.actions.IMPORT, () => $e.data.create( Templates.signature, { fileName, fileData } ), ).then( ( response ) => { this.updateTemplatesState( ( prev ) => ( { ...prev, ...Object.values( response.data ).reduce( ( current, template ) => { if ( ! template.supportsSiteEditor ) { return current; } return { ...current, [ template.id ]: template }; }, {}, ), } ) ); return response; } ); } deleteTemplate( id ) { return this.executeAction( TemplatesProvider.actions.DELETE, () => $e.data.delete( Templates.signature, { id } ), ).then( () => { this.updateTemplatesState( ( prev ) => { const newTemplates = { ...prev }; delete newTemplates[ id ]; return newTemplates; } ); } ); } updateTemplate( id, args ) { return this.executeAction( TemplatesProvider.actions.UPDATE, () => $e.data.update( Templates.signature, args, { id } ), ).then( ( response ) => { this.updateTemplateItemState( id, response.data ); } ); } fetchTemplates() { return this.executeAction( TemplatesProvider.actions.FETCH, () => $e.data.get( Templates.signature, {}, { refresh: true } ), ).then( ( response ) => { this.updateTemplatesState( () => Object.values( response.data ).reduce( ( current, template ) => ( { ...current, [ template.id ]: template } ), {}, ), false ); } ); } updateTemplateItemState( id, args ) { return this.updateTemplatesState( ( prev ) => { const template = { ...prev[ id ], ...args, }; return { ...prev, [ id ]: template, }; } ); } updateTemplatesState( callback, clearCache = true ) { if ( clearCache ) { $e.data.deleteCache( $e.components.get( Component.namespace ), Templates.signature ); } return this.setState( ( prev ) => { return { templates: callback( prev.templates ) }; } ); } findTemplateItemInState( id ) { return this.state.templates[ id ]; } render() { if ( this.state.action.current === TemplatesProvider.actions.FETCH ) { if ( this.state.action.error ) { return <h3>{ __( 'Error:', 'elementor-pro' ) } { this.state.action.error }</h3>; } if ( this.state.action.loading ) { return <h3>{ __( 'Loading', 'elementor-pro' ) }...</h3>; } } return ( <Context.Provider value={ this.state }> { this.props.children } </Context.Provider> ); } } export default TemplatesProvider; app/modules/site-editor/assets/js/context/models/condition.js 0000755 00000002162 15111667160 0020445 0 ustar 00 export default class Condition { id = elementorCommon.helpers.getUniqueId(); default = ''; type = 'include'; name = ''; sub = ''; subId = ''; options = []; subOptions = []; subIdAutocomplete = []; subIdOptions = []; conflictErrors = []; constructor( args ) { this.set( args ); } set( args ) { Object.assign( this, args ); return this; } clone() { return Object.assign( new Condition(), this ); } remove( keys ) { if ( ! Array.isArray( keys ) ) { keys = [ keys ]; } keys.forEach( ( key ) => { delete this[ key ]; } ); return this; } only( keys ) { if ( ! Array.isArray( keys ) ) { keys = [ keys ]; } const keysToRemove = Object.keys( this ) .filter( ( conditionKey ) => ! keys.includes( conditionKey ) ); this.remove( keysToRemove ); return this; } toJson() { return JSON.stringify( this ); } toString() { return this.forDb().filter( ( item ) => item ).join( '/' ); } forDb() { return [ this.type, this.name, this.sub, this.subId ]; } forContext() { return { type: this.type, name: this.name, sub: this.sub, subId: this.subId, }; } } app/modules/site-editor/assets/js/context/services/conditions-config.js 0000755 00000005270 15111667160 0022436 0 ustar 00 import { ConditionsConfig as ConditionsConfigCommand } from '../../data/commands'; export class ConditionsConfig { static instance; config = null; constructor( config ) { this.config = config; } /** * @return {Promise<ConditionsConfig>} Conditions config */ static create() { if ( ConditionsConfig.instance ) { return Promise.resolve( ConditionsConfig.instance ); } return $e.data.get( ConditionsConfigCommand.signature, {}, { refresh: true } ) .then( ( response ) => { ConditionsConfig.instance = new ConditionsConfig( response.data ); return ConditionsConfig.instance; } ); } /** * Get main options for condition name. * * @return {Array} Condition options */ getOptions() { return this.getSubOptions( 'general', true ) .map( ( { label, value } ) => { return { label, value, }; } ); } /** * Get the sub options for the select. * * @param {string} itemName * @param {boolean} isSubItem * @return {Array} Sub options */ getSubOptions( itemName, isSubItem = false ) { const config = this.config[ itemName ]; if ( ! config ) { return []; } return [ { label: config.all_label, value: isSubItem ? itemName : '' }, ...config.sub_conditions.map( ( subName ) => { const subConfig = this.config[ subName ]; return { label: subConfig.label, value: subName, children: subConfig.sub_conditions.length ? this.getSubOptions( subName, true ) : null, }; } ), ]; } /** * Get the autocomplete property from the conditions config * * @param {string} sub * @return {{}|any} Conditions autocomplete */ getSubIdAutocomplete( sub ) { const config = this.config[ sub ]; if ( ! config || ! ( 'object' === typeof ( config.controls ) ) ) { return {}; } const controls = Object.values( config.controls ); if ( ! controls?.[ 0 ]?.autocomplete ) { return {}; } return controls[ 0 ].autocomplete; } /** * Calculate instances from the conditions. * * @param {Array} conditions * @return {Object} Conditions Instances */ calculateInstances( conditions ) { let instances = conditions.reduce( ( current, condition ) => { if ( 'exclude' === condition.type ) { return current; } const key = condition.sub || condition.name, config = this.config[ key ]; if ( ! config ) { return current; } const instanceLabel = condition.subId ? `${ config.label } #${ condition.subId }` : config.all_label; return { ...current, [ key ]: instanceLabel, }; }, {} ); if ( 0 === Object.keys( instances ).length ) { instances = [ __( 'No instances', 'elementor-pro' ) ]; } return instances; } } export default ConditionsConfig; app/modules/site-editor/assets/js/context/conditions.js 0000755 00000017735 15111667160 0017361 0 ustar 00 import Condition from './models/condition'; import ConditionsConfig from './services/conditions-config'; import BaseContext from './base-context'; import { TemplatesConditions, TemplatesConditionsConflicts } from '../data/commands'; export const Context = React.createContext(); export class ConditionsProvider extends BaseContext { static propTypes = { children: PropTypes.any.isRequired, currentTemplate: PropTypes.object.isRequired, onConditionsSaved: PropTypes.func.isRequired, validateConflicts: PropTypes.bool, }; static defaultProps = { validateConflicts: true, }; static actions = { FETCH_CONFIG: 'fetch-config', SAVE: 'save', CHECK_CONFLICTS: 'check-conflicts', }; /** * Holds the conditions config object. * * @type {ConditionsConfig} */ conditionsConfig = null; /** * ConditionsProvider constructor. * * @param {any} props */ constructor( props ) { super( props ); this.state = { ...this.state, conditions: {}, updateConditionItemState: this.updateConditionItemState.bind( this ), removeConditionItemInState: this.removeConditionItemInState.bind( this ), createConditionItemInState: this.createConditionItemInState.bind( this ), findConditionItemInState: this.findConditionItemInState.bind( this ), saveConditions: this.saveConditions.bind( this ), }; } /** * Fetch the conditions config, then normalize the conditions and then setup titles for * the subIds. */ componentDidMount() { this.executeAction( ConditionsProvider.actions.FETCH_CONFIG, () => ConditionsConfig.create() ) .then( ( conditionsConfig ) => this.conditionsConfig = conditionsConfig ) .then( this.normalizeConditionsState.bind( this ) ) .then( this.setSubIdTitles.bind( this ) ); } /** * Execute a request to save the template conditions. * * @return {any} Saved conditions */ saveConditions() { const conditions = Object.values( this.state.conditions ) .map( ( condition ) => condition.forDb() ); return this.executeAction( ConditionsProvider.actions.SAVE, () => $e.data.update( TemplatesConditions.signature, { conditions }, { id: this.props.currentTemplate.id } ), ).then( () => { const contextConditions = Object.values( this.state.conditions ) .map( ( condition ) => condition.forContext() ); this.props.onConditionsSaved( this.props.currentTemplate.id, { conditions: contextConditions, instances: this.conditionsConfig.calculateInstances( Object.values( this.state.conditions ) ), isActive: !! ( Object.keys( this.state.conditions ).length && 'publish' === this.props.currentTemplate.status ), } ); } ); } /** * Check for conflicts in the server and mark the condition if there * is a conflict. * * @param {any} condition */ checkConflicts( condition ) { return this.executeAction( ConditionsProvider.actions.CHECK_CONFLICTS, () => $e.data.get( TemplatesConditionsConflicts.signature, { post_id: this.props.currentTemplate.id, condition: condition.clone().toString(), } ), ).then( ( response ) => this.updateConditionItemState( condition.id, { conflictErrors: Object.values( response.data ) }, false ), ); } /** * Fetching subId titles. * * @param {any} condition * @return {Promise<unknown>} Titles */ fetchSubIdsTitles( condition ) { return new Promise( ( resolve ) => { return elementorCommon.ajax.loadObjects( { action: 'query_control_value_titles', ids: _.isArray( condition.subId ) ? condition.subId : [ condition.subId ], data: { get_titles: condition.subIdAutocomplete, unique_id: elementorCommon.helpers.getUniqueId(), }, success( response ) { resolve( response ); }, } ); } ); } /** * Get the conditions from the template and normalize it to data structure * that the components can work with. */ normalizeConditionsState() { this.updateConditionsState( () => { return this.props.currentTemplate.conditions.reduce( ( current, condition ) => { const conditionObj = new Condition( { ...condition, default: this.props.currentTemplate.defaultCondition, options: this.conditionsConfig.getOptions(), subOptions: this.conditionsConfig.getSubOptions( condition.name ), subIdAutocomplete: this.conditionsConfig.getSubIdAutocomplete( condition.sub ), supIdOptions: condition.subId ? [ { value: condition.subId, label: condition.subId } ] : [], } ); return { ...current, [ conditionObj.id ]: conditionObj, }; }, {} ); } ).then( () => { Object.values( this.state.conditions ).forEach( ( condition ) => this.checkConflicts( condition ) ); } ); } /** * Set titles to the subIds, * for the first render of the component. */ setSubIdTitles() { return Object.values( this.state.conditions ).forEach( ( condition ) => { if ( ! condition.subId ) { return; } return this.fetchSubIdsTitles( condition ) .then( ( response ) => this.updateConditionItemState( condition.id, { subIdOptions: [ { label: Object.values( response )[ 0 ], value: condition.subId, } ], }, false ), ); } ); } /** * Update state of specific condition item. * * @param {any} id * @param {any} args * @param {boolean} shouldCheckConflicts */ updateConditionItemState( id, args, shouldCheckConflicts = true ) { if ( args.name ) { args.subOptions = this.conditionsConfig.getSubOptions( args.name ); } if ( args.sub || args.name ) { args.subIdAutocomplete = this.conditionsConfig.getSubIdAutocomplete( args.sub ); // In case that the condition has been changed, it will set the options of the subId // to empty array to let select2 autocomplete handle the options. args.subIdOptions = []; } this.updateConditionsState( ( prev ) => { const condition = prev[ id ]; return { ...prev, [ id ]: condition.clone().set( args ), }; } ).then( () => { if ( shouldCheckConflicts ) { this.checkConflicts( this.findConditionItemInState( id ) ); } } ); } /** * Remove a condition item from the state. * * @param {any} id */ removeConditionItemInState( id ) { this.updateConditionsState( ( prev ) => { const newConditions = { ...prev }; delete newConditions[ id ]; return newConditions; } ); } /** * Add a new condition item into the state. * * @param {boolean} shouldCheckConflicts */ createConditionItemInState( shouldCheckConflicts = true ) { const defaultCondition = this.props.currentTemplate.defaultCondition, newCondition = new Condition( { name: defaultCondition, default: defaultCondition, options: this.conditionsConfig.getOptions(), subOptions: this.conditionsConfig.getSubOptions( defaultCondition ), subIdAutocomplete: this.conditionsConfig.getSubIdAutocomplete( '' ), } ); this.updateConditionsState( ( prev ) => ( { ...prev, [ newCondition.id ]: newCondition } ) ) .then( () => { if ( shouldCheckConflicts ) { this.checkConflicts( newCondition ); } } ); } /** * Find a condition item from the conditions state. * * @param {any} id * @return {Condition|null} Condition */ findConditionItemInState( id ) { return Object.values( this.state.conditions ).find( ( c ) => c.id === id ); } /** * Update the whole conditions state. * * @param {Function} callback * @return {Promise<undefined>} Conditions state */ updateConditionsState( callback ) { return new Promise( ( resolve ) => this.setState( ( prev ) => ( { conditions: callback( prev.conditions ) } ), resolve ), ); } /** * Renders the provider. * * @return {any} Element */ render() { if ( this.state.action.current === ConditionsProvider.actions.FETCH_CONFIG ) { if ( this.state.error ) { return <h3>{ __( 'Error:', 'elementor-pro' ) } { this.state.error }</h3>; } if ( this.state.loading ) { return <h3>{ __( 'Loading', 'elementor-pro' ) }...</h3>; } } return ( <Context.Provider value={ this.state }> { this.props.children } </Context.Provider> ); } } export default ConditionsProvider; app/modules/site-editor/assets/js/pages/add-new.scss 0000755 00000000234 15111667160 0016463 0 ustar 00 .eps-add-new__overlay { display: flex; align-items: center; justify-content: center; opacity: 1; --card-image-overlay-background-color: transparent; } app/modules/site-editor/assets/js/pages/template-type.scss 0000755 00000000257 15111667160 0017743 0 ustar 00 .e-site-editor__templates { .page-header { margin-block-end: spacing(10); > a { align-self: baseline; } } .eps-separator { margin-block-end: spacing(44); } } app/modules/site-editor/assets/js/pages/add-new.js 0000755 00000003216 15111667160 0016127 0 ustar 00 import { AddNewButton, Heading, Grid, CardOverlay } from '@elementor/app-ui'; import { SiteParts } from '@elementor/site-editor'; import './add-new.scss'; import { Context as TemplatesContext } from '../context/templates'; import BackButton from '../molecules/back-button'; import useFeatureLock from 'elementor-pro-app/hooks/use-feature-lock'; export default function AddNew() { const { templates } = React.useContext( TemplatesContext ), hasTemplates = 1 <= Object.keys( templates ).length; const { isLocked, ConnectButton } = useFeatureLock( 'site-editor' ); /** * An hover element for each site part. * * @param {any} props */ const HoverElement = ( props ) => { if ( isLocked ) { return ( <CardOverlay className="e-site-editor__promotion-overlay"> <div className="e-site-editor__promotion-overlay__link"> <i className="e-site-editor__promotion-overlay__icon eicon-lock" /> </div> </CardOverlay> ); } return ( <a href={ props.urls.create } className="eps-card__image-overlay eps-add-new__overlay"> <AddNewButton hideText={ true } /> </a> ); }; HoverElement.propTypes = { urls: PropTypes.object.isRequired, }; return ( <section className="e-site-editor__add-new"> <Grid container direction="column" className="e-site-editor__header"> { hasTemplates && <Grid item><BackButton /></Grid> } <Grid item container justify="space-between" alignItems="start"> <Heading variant="h1">{ __( 'Start customizing every part of your site', 'elementor-pro' ) }</Heading> { isLocked && <ConnectButton /> } </Grid> </Grid> <SiteParts hoverElement={ HoverElement } /> </section> ); } app/modules/site-editor/assets/js/pages/templates.js 0000755 00000001300 15111667160 0016576 0 ustar 00 import SiteTemplates from '../organisms/site-templates'; import { AddNewButton, Grid } from '@elementor/app-ui'; import useFeatureLock from 'elementor-pro-app/hooks/use-feature-lock'; export default function Templates() { const { isLocked, ConnectButton } = useFeatureLock( 'site-editor' ); return ( <section className="e-site-editor__site-templates"> <Grid container justify="space-between" alignItems="start" className="page-header"> <h1>{ __( 'Your Site\'s Global Parts', 'elementor-pro' ) }</h1> { isLocked ? <ConnectButton /> : <AddNewButton url="/site-editor/add-new" /> } </Grid> <hr className="eps-separator" /> <SiteTemplates /> </section> ); } app/modules/site-editor/assets/js/pages/template-type.js 0000755 00000002330 15111667160 0017376 0 ustar 00 import { TemplateTypesContext } from '@elementor/site-editor'; import { AddNewButton, Grid, Heading, NotFound } from '@elementor/app-ui'; import SiteTemplates from '../organisms/site-templates'; import useFeatureLock from 'elementor-pro-app/hooks/use-feature-lock'; import './template-type.scss'; export default function TemplateType( props ) { const { templateTypes } = React.useContext( TemplateTypesContext ), currentType = templateTypes.find( ( item ) => item.type === props.type ), { isLocked, ConnectButton } = useFeatureLock( 'site-editor' ); if ( ! currentType ) { return <NotFound />; } return ( <section className={ `e-site-editor__templates e-site-editor__templates--type-${ props.type }` }> <Grid className="page-header" container justify="space-between"> <Heading variant="h1">{ currentType.page_title }</Heading> { isLocked ? <ConnectButton /> : <AddNewButton url={ currentType.urls.create } text={ __( 'Add New', 'elementor-pro' ) } /> } </Grid> <hr className="eps-separator" /> <SiteTemplates type={ currentType.type } id={ props.id } /> </section> ); } TemplateType.propTypes = { type: PropTypes.string, page_title: PropTypes.string, id: PropTypes.string, }; app/modules/site-editor/assets/js/pages/import.js 0000755 00000010310 15111667160 0016113 0 ustar 00 import { DropZone, Dialog, Checkbox } from '@elementor/app-ui'; import { Context as TemplatesContext, TemplatesProvider } from '../context/templates'; import BackButton from '../molecules/back-button'; import { useConfirmAction as useConfirmActionBase } from '@elementor/hooks'; // The hook `useConfirmAction` comes from the core plugin, so it is possible that it is not available. const useConfirmActionFallback = ( { action } ) => ( { runAction: action, dialog: { isOpen: false }, } ); const useConfirmAction = useConfirmActionBase ?? useConfirmActionFallback; export default function Import() { const { importTemplates, action, resetActionState } = React.useContext( TemplatesContext ), [ importedTemplate, setImportedTemplate ] = React.useState( null ), isImport = React.useMemo( () => action.current === TemplatesProvider.actions.IMPORT, [ action ] ), isUploading = React.useMemo( () => isImport && action.loading, [ action ] ), hasError = React.useMemo( () => isImport && action.error, [ action ] ); const upload = React.useCallback( ( file ) => { if ( isUploading ) { return; } readFile( file ) .then( ( fileData ) => importTemplates( { fileName: file.name, fileData } ) ) .then( ( response ) => { // For now it show a dialog for the first template ONLY! setImportedTemplate( response.data[ 0 ] ); } ); }, [] ); const { runAction: uploadFile, dialog, checkbox, } = useConfirmAction( { doNotShowAgainKey: 'upload_json_warning_generic_message', action: upload, } ); return ( <section className="site-editor__import"> { importedTemplate && <Dialog title={ __( 'Your template was imported', 'elementor-pro' ) } approveButtonText={ __( 'Preview', 'elementor-pro' ) } approveButtonUrl={ importedTemplate.url } approveButtonTarget="_blank" dismissButtonText={ __( 'Edit', 'elementor-pro' ) } dismissButtonUrl={ importedTemplate.editURL } dismissButtonTarget="_top" onClose={ () => setImportedTemplate( null ) } /> } { hasError && <Dialog title={ action.error } approveButtonText={ __( 'Learn More', 'elementor-pro' ) } approveButtonUrl="https://go.elementor.com/app-theme-builder-import-issue" approveButtonTarget="_blank" approveButtonColor="link" dismissButtonText={ __( 'Go Back', 'elementor-pro' ) } dismissButtonOnClick={ resetActionState } onClose={ resetActionState } /> } { dialog.isOpen && <Dialog title={ __( 'Warning: JSON or ZIP files may be unsafe', 'elementor-pro' ) } text={ __( 'Uploading JSON or ZIP files from unknown sources can be harmful and put your site at risk. For maximum safety, upload only JSON or ZIP files from trusted sources.', 'elementor-pro' ) } approveButtonColor="link" approveButtonText={ __( 'Continue', 'elementor-pro' ) } approveButtonOnClick={ dialog.approve } dismissButtonText={ __( 'Cancel', 'elementor-pro' ) } dismissButtonOnClick={ dialog.dismiss } onClose={ dialog.dismiss } > <label htmlFor="do-not-show-upload-json-warning-again" style={ { display: 'flex', alignItems: 'center', gap: '5px' } }> <Checkbox id="do-not-show-upload-json-warning-again" type="checkbox" value={ checkbox.isChecked } onChange={ ( event ) => checkbox.setIsChecked( !! event.target.checked ) } /> { __( 'Do not show this message again', 'elementor-pro' ) } </label> </Dialog> } <BackButton /> <DropZone heading={ __( 'Import Template To Your Library', 'elementor-pro' ) } text={ __( 'Drag & Drop your .JSON or .zip template file', 'elementor-pro' ) } secondaryText={ __( 'or', 'elementor-pro' ) } onFileSelect={ uploadFile } isLoading={ isUploading } filetypes={ [ 'zip', 'json' ] } /> </section> ); } function readFile( file ) { return new Promise( ( ( resolve ) => { const fileReader = new FileReader(); fileReader.readAsDataURL( file ); fileReader.onload = ( event ) => { // Replace the mime type that prepended to the base64 with empty string and return a // resolved promise only with the base64 string. resolve( event.target.result.replace( /^[^,]+,/, '' ) ); }; } ) ); } app/modules/site-editor/assets/js/pages/conditions/condition-sub.js 0000755 00000001330 15111667160 0021531 0 ustar 00 import { Select } from '@elementor/app-ui'; export default function ConditionSub( props ) { if ( 'general' === props.name || ! props.subOptions.length ) { return ''; } const onChange = ( e ) => props.updateConditions( props.id, { sub: e.target.value, subId: '' } ); return ( <div className="e-site-editor-conditions__input-wrapper"> <Select options={ props.subOptions } value={ props.sub } onChange={ onChange } /> </div> ); } ConditionSub.propTypes = { updateConditions: PropTypes.func.isRequired, id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, sub: PropTypes.string.isRequired, subOptions: PropTypes.array.isRequired, }; ConditionSub.defaultProps = { sub: '', subOptions: {}, }; app/modules/site-editor/assets/js/pages/conditions/condition-conflicts.js 0000755 00000001244 15111667160 0022730 0 ustar 00 import { Button, Text } from '@elementor/app-ui'; export default function ConditionConflicts( props ) { if ( ! props.conflicts.length ) { return ''; } const conflictLinks = props.conflicts.map( ( conflict ) => { return ( <Button key={ conflict.template_id } target="_blank" url={ conflict.edit_url } text={ conflict.template_title } /> ); } ); return ( <Text className="e-site-editor-conditions__conflict" variant="sm"> { __( 'Elementor recognized that you have set this location for other templates: ', 'elementor-pro' ) } { conflictLinks } </Text> ); } ConditionConflicts.propTypes = { conflicts: PropTypes.array.isRequired, }; app/modules/site-editor/assets/js/pages/conditions/conditions.scss 0000755 00000011517 15111667160 0021474 0 ustar 00 @import "conditions-api"; .e-site-editor-conditions { &__header { text-align: center; } &__header-image { display: block; margin: 0 auto $e-site-editor-conditions-header-image-button-spacing; width: $e-site-editor-conditions-header-image-width; } &__rows { margin: $e-site-editor-conditions-rows-y-spacing auto; max-width: $e-site-editor-conditions-rows-max-width; } &__row { display: flex; flex-grow: 1; margin-block-start: $e-site-editor-conditions-row-block-start-spacing; } &__remove-condition { color: $e-site-editor-conditions-remove-condition-color; font-size: $e-site-editor-conditions-remove-condition-font-size; display: flex; align-items: center; justify-content: center; } &__row-controls { overflow: hidden; margin-inline-end: $e-site-editor-conditions-row-controls-spacing-end; background-color: var(--e-site-editor-conditions-row-controls-background); display: flex; width: 100%; border: var(--e-site-editor-conditions-row-controls-border); border-radius: $e-site-editor-conditions-row-controls-radius; &--error { border: $e-site-editor-conditions-row-controls-error-border; } } &__conflict { text-align: center; margin-block-start: $e-site-editor-conditions-conflict-block-start-spacing; color: $e-site-editor-conditions-conflict-color; } &__row-controls-inner { width: 100%; display: flex; div { flex: 1; } } &__add-button-container { text-align: center; } &__add-button { margin-block-start: $e-site-editor-add-button-margin-block-start; background-color: var(--e-site-editor-add-button-background-color); color: $e-site-editor-add-button-color; text-transform: uppercase; &:hover { background-color: var(--e-site-editor-add-button-color-hover-background-color); color: $e-site-editor-add-button-color-hover-color; } } &__footer { display: flex; justify-content: flex-end; position: absolute; bottom: 0; right: 0; left: 0; padding: $e-site-editor-save-button-container-spacing; border-block-start: 1px solid var(--hr-color); } &__input-wrapper { position: relative; padding-inline-start: $e-site-editor-input-wrapper-border-width $e-site-editor-input-wrapper-border-style; border-color: var(--e-site-editor-input-wrapper-border-color); &:first-child { border: none; } select { appearance: none; -webkit-appearance: none; font-size: $e-site-editor-input-wrapper-select-font-size; height: $e-site-editor-input-wrapper-select-height; border-width: 0; padding: 0 $e-site-editor-input-wrapper-select-y-padding; width: 100%; position: relative; color: var(--e-site-editor-input-wrapper-select-color); outline: none; background: transparent; } &:after { font-family: eicons; content: '\e8ad'; font-size: $e-site-editor-input-wrapper-select-arrow-font-size; pointer-events: none; position: absolute; top: 50%; transform: translateY(-50%); @include end($e-site-editor-input-wrapper-select-arrow-margin-end); } .select2-container--default .select2-selection--single { border: none; line-height: $e-site-editor-input-wrapper-select-height; } .select2-container--default .select2-selection--single .select2-selection__rendered { line-height: $e-site-editor-input-wrapper-select-height; font-size: $e-site-editor-input-wrapper-select-font-size; } .select2-selection { outline: none; background: transparent; height: $e-site-editor-input-wrapper-select-height; } .select2-selection__arrow { display: none; } } &__input-wrapper--condition-type { position: relative; &:before { font-family: eicons; position: absolute; top: 50%; transform: translateY(-50%); @include start($e-site-editor-input-wrapper-condition-type-icon-start-spacing); font-size: $e-site-editor-input-wrapper-condition-type-icon-font-size; //color: $e-site-editor-input-wrapper-condition-type-icon-color; pointer-events: none; z-index: $e-site-editor-input-wrapper-condition-type-icon-z-index; } select { text-transform: uppercase; padding-inline-start: $e-site-editor-input-wrapper-condition-type-start-padding; width: $e-site-editor-input-wrapper-condition-type-width; font-size: $e-site-editor-input-wrapper-condition-type-font-size; border-inline-end: $e-site-editor-input-wrapper-border-width $e-site-editor-input-wrapper-border-style; border-color: var(--e-site-editor-input-wrapper-border-color); } &[data-elementor-condition-type="include"] { &:before { content: '\e8cc'; } } &[data-elementor-condition-type="exclude"] { &:before { content: '\e8cd'; } } } } // This is a temporary fix that handles dark mode in select2. // TODO: Remove it if already handled by the Select2 component. .select2-search__field { background-color: transparent; color: var(--e-site-editor-input-select2-search-field-color); } app/modules/site-editor/assets/js/pages/conditions/conditions-rows.js 0000755 00000005544 15111667160 0022130 0 ustar 00 import { Context as ConditionsContext, ConditionsProvider } from '../../context/conditions'; import { Button, Dialog } from '@elementor/app-ui'; import ConditionType from './condition-type'; import ConditionName from './condition-name'; import ConditionSub from './condition-sub'; import ConditionSubId from './condition-sub-id'; import ConditionConflicts from './condition-conflicts'; export default function ConditionsRows( props ) { const { conditions, createConditionItemInState: create, updateConditionItemState: update, removeConditionItemInState: remove, saveConditions: save, action, resetActionState, } = React.useContext( ConditionsContext ); const rows = Object.values( conditions ).map( ( condition ) => <div key={ condition.id }> <div className="e-site-editor-conditions__row"> <div className={ `e-site-editor-conditions__row-controls ${ condition.conflictErrors.length && 'e-site-editor-conditions__row-controls--error' }` }> <ConditionType { ...condition } updateConditions={ update } /> <div className="e-site-editor-conditions__row-controls-inner"> <ConditionName { ...condition } updateConditions={ update } /> <ConditionSub { ...condition } updateConditions={ update } /> <ConditionSubId { ...condition } updateConditions={ update } /> </div> </div> <Button className="e-site-editor-conditions__remove-condition" text={ __( 'Delete', 'elementor-pro' ) } icon="eicon-close" hideText={ true } onClick={ () => remove( condition.id ) } /> </div> <ConditionConflicts conflicts={ condition.conflictErrors } /> </div>, ); const isSaving = action.current === ConditionsProvider.actions.SAVE && action.loading; return ( <> { action.error && <Dialog text={ action.error } dismissButtonText={ __( 'Go Back', 'elementor-pro' ) } dismissButtonOnClick={ resetActionState } approveButtonText={ __( 'Learn More', 'elementor-pro' ) } approveButtonColor="link" approveButtonUrl="https://go.elementor.com/app-theme-builder-conditions-load-issue" approveButtonTarget="_target" /> } <div className="e-site-editor-conditions__rows"> { rows } </div> <div className="e-site-editor-conditions__add-button-container"> <Button className="e-site-editor-conditions__add-button" variant="contained" size="lg" text={ __( 'Add Condition', 'elementor-pro' ) } onClick={ create } /> </div> <div className="e-site-editor-conditions__footer"> <Button variant="contained" color="primary" size="lg" hideText={ isSaving } icon={ isSaving ? 'eicon-loading eicon-animation-spin' : '' } text={ __( 'Save & Close', 'elementor-pro' ) } onClick={ () => save().then( props.onAfterSave ) } /> </div> </> ); } ConditionsRows.propTypes = { onAfterSave: PropTypes.func, }; app/modules/site-editor/assets/js/pages/conditions/conditions.js 0000755 00000003150 15111667160 0021127 0 ustar 00 import { Heading, Text } from '@elementor/app-ui'; import ConditionsProvider from '../../context/conditions'; import { Context as TemplatesContext } from '../../context/templates'; import ConditionsRows from './conditions-rows'; import './conditions.scss'; import BackButton from '../../molecules/back-button'; export default function Conditions( props ) { const { findTemplateItemInState, updateTemplateItemState } = React.useContext( TemplatesContext ), template = findTemplateItemInState( parseInt( props.id ) ); if ( ! template ) { return <div>{ __( 'Not Found', 'elementor-pro' ) }</div>; } return ( <section className="e-site-editor-conditions"> <BackButton /> <div className="e-site-editor-conditions__header"> <img className="e-site-editor-conditions__header-image" src={ `${ elementorAppProConfig.baseUrl }/modules/theme-builder/assets/images/conditions-tab.svg` } alt={ __( 'Import template', 'elementor-pro' ) } /> <Heading variant="h1" tag="h1"> { __( 'Where Do You Want to Display Your Template?', 'elementor-pro' ) } </Heading> <Text variant="p"> { __( 'Set the conditions that determine where your template is used throughout your site.', 'elementor-pro' ) } <br /> { __( 'For example, choose \'Entire Site\' to display the template across your site.', 'elementor-pro' ) } </Text> </div> <ConditionsProvider currentTemplate={ template } onConditionsSaved={ updateTemplateItemState }> <ConditionsRows onAfterSave={ () => history.back() } /> </ConditionsProvider> </section> ); } Conditions.propTypes = { id: PropTypes.string, }; app/modules/site-editor/assets/js/pages/conditions/conditions-api.scss 0000755 00000013411 15111667160 0022236 0 ustar 00 $e-site-editor-conditions-header-image-button-spacing: spacing(44); $e-site-editor-conditions-header-image-width: px-to-rem(70); $e-site-editor-conditions-rows-max-width: px-to-rem(700); $e-site-editor-conditions-rows-y-spacing: spacing(44); $e-site-editor-conditions-row-block-start-spacing: spacing(12); $e-site-editor-conditions-remove-condition-color: tints(400); $e-site-editor-conditions-remove-condition-font-size: type(size, '18'); $e-site-editor-conditions-row-controls-spacing-end: spacing(10); $e-site-editor-conditions-row-controls-background: theme-colors(light); $e-site-editor-conditions-row-controls-dark-background: dark-tints(600); $e-site-editor-conditions-row-controls-radius: $eps-radius; $e-site-editor-conditions-row-controls-border: $eps-border-width $eps-border-style tints(100); $e-site-editor-conditions-row-controls-dark-border: $eps-border-width $eps-border-style tints(700); //$e-site-editor-conditions-row-controls-dark-border: $eps-border-width $eps-border-style tints(725); //merge after 3.12 is out $e-site-editor-conditions-row-controls-error-border: $eps-border-width $eps-border-style theme-colors(danger); $e-site-editor-conditions-conflict-block-start-spacing: spacing(5); $e-site-editor-conditions-conflict-color: theme-colors(danger); $e-site-editor-add-button-margin-block-start: spacing(44); $e-site-editor-add-button-background-color: tints(500); $e-site-editor-add-button-background-dark-color: dark-tints(500); $e-site-editor-add-button-color: theme-colors(light); $e-site-editor-add-button-color-hover-background-color: tints(600); $e-site-editor-add-button-color-hover-dark-background-color: dark-tints(600); $e-site-editor-add-button-color-hover-color: theme-colors(light); $e-site-editor-save-button-container-spacing: spacing(8); $e-site-editor-input-wrapper-border-width: $eps-border-width; $e-site-editor-input-wrapper-border-style: $eps-border-style; $e-site-editor-input-wrapper-border-color: tints(100); $e-site-editor-input-wrapper-border-dark-color: dark-tints(700); //$e-site-editor-input-wrapper-border-dark-color: dark-tints(725); //merge after 3.12 is out $e-site-editor-input-wrapper-select-font-size: type(size, "12"); $e-site-editor-input-wrapper-select-height: px-to-rem(40); $e-site-editor-input-wrapper-select-y-padding: spacing(10); $e-site-editor-input-wrapper-select-color: tints(700); //$e-site-editor-input-wrapper-select-color: tints(725); //merge after 3.12 is out $e-site-editor-input-wrapper-select-dark-color: dark-tints(200); $e-site-editor-input-wrapper-select-arrow-font-size: type(size, "12"); $e-site-editor-input-wrapper-select-arrow-margin-end: spacing(10); $e-site-editor-input-wrapper-condition-type-icon-start-spacing: spacing(12); $e-site-editor-input-wrapper-condition-type-icon-font-size: type(size, "15"); $e-site-editor-input-wrapper-condition-type-icon-color: theme-colors(light); $e-site-editor-input-wrapper-condition-type-icon-z-index: z-index(dropdown); $e-site-editor-input-wrapper-condition-type-arrow-color: theme-colors(light); $e-site-editor-input-wrapper-condition-type-start-padding: px-to-rem(34); $e-site-editor-input-wrapper-condition-type-width: px-to-rem(120); $e-site-editor-input-wrapper-condition-type-font-size: type(size, '12'); $e-site-editor-input-wrapper-condition-type-color: theme-colors(light); $e-site-editor-input-wrapper-condition-include-background-color: tints(500); $e-site-editor-input-wrapper-condition-include-background-dark-color: dark-tints(600); $e-site-editor-input-wrapper-condition-exclude-background-color: tints(400); $e-site-editor-input-wrapper-condition-exclude-background-dark-color: dark-tints(600); $e-site-editor-input-select2-search-field-color: theme-elements-colors(text-base-color); $e-site-editor-input-select2-search-field-dark-color: theme-colors(light); :root { --e-site-editor-conditions-row-controls-background: #{$e-site-editor-conditions-row-controls-background}; --e-site-editor-input-wrapper-border-color: #{$e-site-editor-input-wrapper-border-color}; --e-site-editor-input-wrapper-select-color: #{$e-site-editor-input-wrapper-select-color}; --e-site-editor-conditions-row-controls-border: #{$e-site-editor-conditions-row-controls-border}; --e-site-editor-add-button-background-color: #{$e-site-editor-add-button-background-color}; --e-site-editor-add-button-color-hover-background-color: #{$e-site-editor-add-button-color-hover-background-color}; --e-site-editor-input-wrapper-condition-include-background-color: #{$e-site-editor-input-wrapper-condition-include-background-color}; --e-site-editor-input-wrapper-condition-exclude-background-color: #{$e-site-editor-input-wrapper-condition-exclude-background-color}; --e-site-editor-input-select2-search-field-color: #{$e-site-editor-input-select2-search-field-color} } .eps-theme-dark { --select2-selection-background-color: tints(600); --e-site-editor-conditions-row-controls-background: #{$e-site-editor-conditions-row-controls-dark-background}; --e-site-editor-input-wrapper-border-color: #{$e-site-editor-input-wrapper-border-dark-color}; --e-site-editor-input-wrapper-select-color: #{$e-site-editor-input-wrapper-select-dark-color}; --e-site-editor-conditions-row-controls-border: #{$e-site-editor-conditions-row-controls-dark-border}; --e-site-editor-add-button-background-color: #{$e-site-editor-add-button-background-dark-color}; --e-site-editor-add-button-color-hover-background-color: #{$e-site-editor-add-button-color-hover-dark-background-color}; --e-site-editor-input-wrapper-condition-include-background-color: #{$e-site-editor-input-wrapper-condition-include-background-dark-color}; --e-site-editor-input-wrapper-condition-exclude-background-color: #{$e-site-editor-input-wrapper-condition-exclude-background-dark-color}; --e-site-editor-input-select2-search-field-color: #{$e-site-editor-input-select2-search-field-dark-color} } app/modules/site-editor/assets/js/pages/conditions/condition-sub-id.js 0000755 00000003403 15111667160 0022126 0 ustar 00 import { Select2 } from '@elementor/app-ui'; /** * Main component. * * @param {any} props * @return {any} Element * @class */ export default function ConditionSubId( props ) { const settings = React.useMemo( () => ( Object.keys( props.subIdAutocomplete ).length ? getSettings( props.subIdAutocomplete ) : null ), [ props.subIdAutocomplete ] ); if ( ! props.sub || ! settings ) { return ''; } const onChange = ( e ) => props.updateConditions( props.id, { subId: e.target.value } ); return ( <div className="e-site-editor-conditions__input-wrapper"> <Select2 onChange={ onChange } value={ props.subId } settings={ settings } options={ props.subIdOptions } /> </div> ); } /** * Get settings for the select2 base on the autocomplete settings, * that passes as a prop * * @param {any} autocomplete * @return {Object} Settings */ function getSettings( autocomplete ) { return { allowClear: false, placeholder: __( 'All', 'elementor-pro' ), dir: elementorCommon.config.isRTL ? 'rtl' : 'ltr', ajax: { transport( params, success, failure ) { return elementorCommon.ajax.addRequest( 'pro_panel_posts_control_filter_autocomplete', { data: { q: params.data.q, autocomplete, }, success, error: failure, } ); }, data( params ) { return { q: params.term, page: params.page, }; }, cache: true, }, escapeMarkup( markup ) { return markup; }, minimumInputLength: 1, }; } ConditionSubId.propTypes = { subIdAutocomplete: PropTypes.object, id: PropTypes.string.isRequired, sub: PropTypes.string, subId: PropTypes.string, updateConditions: PropTypes.func, subIdOptions: PropTypes.array, }; ConditionSubId.defaultProps = { subId: '', subIdOptions: [], }; app/modules/site-editor/assets/js/pages/conditions/condition-type.js 0000755 00000001666 15111667160 0021735 0 ustar 00 import { Select } from '@elementor/app-ui'; export default function ConditionType( props ) { const wrapperRef = React.createRef(); const options = [ { label: __( 'Include', 'elementor-pro' ), value: 'include', }, { label: __( 'Exclude', 'elementor-pro' ), value: 'exclude', }, ]; const onChange = ( e ) => { props.updateConditions( props.id, { type: e.target.value } ); }; React.useEffect( () => { wrapperRef.current.setAttribute( 'data-elementor-condition-type', props.type ); } ); return ( <div className="e-site-editor-conditions__input-wrapper e-site-editor-conditions__input-wrapper--condition-type" ref={ wrapperRef }> <Select options={ options } value={ props.type } onChange={ onChange } /> </div> ); } ConditionType.propTypes = { updateConditions: PropTypes.func.isRequired, id: PropTypes.string.isRequired, type: PropTypes.string.isRequired, }; ConditionType.defaultProps = { type: '', }; app/modules/site-editor/assets/js/pages/conditions/condition-name.js 0000755 00000001407 15111667160 0021665 0 ustar 00 import { Select } from '@elementor/app-ui'; export default function ConditionName( props ) { // Hide for template types that has another default, like single & archive. if ( 'general' !== props.default ) { return ''; } const onChange = ( e ) => props.updateConditions( props.id, { name: e.target.value, sub: '', subId: '' } ); return ( <div className="e-site-editor-conditions__input-wrapper"> <Select options={ props.options } value={ props.name } onChange={ onChange } /> </div> ); } ConditionName.propTypes = { updateConditions: PropTypes.func.isRequired, id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, options: PropTypes.array.isRequired, default: PropTypes.string.isRequired, }; ConditionName.defaultProps = { name: '', }; app/modules/site-editor/assets/js/organisms/site-templates.js 0000755 00000005454 15111667160 0020461 0 ustar 00 import { CssGrid, Dialog } from '@elementor/app-ui'; import SiteTemplate from '../molecules/site-template'; import { PartActionsDialogs } from '../part-actions/dialogs-and-buttons'; import { Context as TemplatesContext } from '../context/templates'; import useTemplatesScreenshot from '../hooks/use-templates-screenshot'; export default function SiteTemplates( props ) { const { templates: contextTemplates, action, resetActionState } = React.useContext( TemplatesContext ); let gridColumns, templates; // Make the templates object a memorize value, will re run again only if // templates has been changed, also sort the templates by `isActive`. templates = React.useMemo( () => { return Object.values( contextTemplates ) .sort( ( a, b ) => { // This sort make sure to show first the active templates, second the // inactive templates that are not draft, and then the drafts, // in each category it sorts it inside by date. if ( ! b.isActive && ! a.isActive ) { if ( ( 'draft' === b.status && 'draft' === a.status ) || ( 'draft' !== b.status && 'draft' !== a.status ) ) { return b.date < a.date ? 1 : -1; } return 'draft' === a.status ? 1 : -1; } if ( b.isActive && a.isActive ) { return b.date < a.date ? 1 : -1; } return b.isActive ? 1 : -1; } ); }, [ contextTemplates ] ); // Start to capture screenshots. useTemplatesScreenshot( props.type ); const siteTemplateConfig = {}; if ( props.type ) { templates = templates.filter( ( item ) => item.type === props.type ); siteTemplateConfig.extended = true; siteTemplateConfig.type = props.type; switch ( props.type ) { case 'header': case 'footer': gridColumns = 1; siteTemplateConfig.aspectRatio = 'wide'; break; default: gridColumns = 2; } } if ( ! templates || ! templates.length ) { return <h3>{ __( 'No Templates found. Want to create one?', 'elementor-pro' ) }...</h3>; } return ( <section className="e-site-editor__site-templates"> <PartActionsDialogs /> { action.error && <Dialog text={ action.error } dismissButtonText={ __( 'Go Back', 'elementor-pro' ) } dismissButtonOnClick={ resetActionState } approveButtonText={ __( 'Learn More', 'elementor-pro' ) } approveButtonColor="link" approveButtonUrl="https://go.elementor.com/app-theme-builder-template-load-issue" approveButtonTarget="_target" /> } <CssGrid columns={ gridColumns } spacing={ 24 } colMinWidth={ 200 }> { templates.map( ( item ) => <SiteTemplate key={ item.id } { ... item } { ... siteTemplateConfig } isSelected={ parseInt( props.id ) === item.id } />, ) } </CssGrid> </section> ); } SiteTemplates.propTypes = { type: PropTypes.string, id: PropTypes.string, }; app/modules/site-editor/module.php 0000755 00000015076 15111667160 0013242 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\SiteEditor; use Elementor\Core\Admin\Menu\Admin_Menu_Manager; use Elementor\Core\Experiments\Manager as ExperimentsManager; use Elementor\Core\Frontend\Render_Mode_Manager; use Elementor\Core\Base\Module as BaseModule; use Elementor\Core\Common\Modules\Ajax\Module as Ajax; use Elementor\TemplateLibrary\Source_Local; use ElementorPro\Core\App\Modules\SiteEditor\Data\Controller; use ElementorPro\Core\Behaviors\Feature_Lock; use ElementorPro\Modules\ThemeBuilder\AdminMenuItems\Theme_Builder_Menu_Item; use ElementorPro\Modules\ThemeBuilder\Module as Theme_Builder_Table_View; use ElementorPro\Modules\ThemeBuilder\Module as ThemeBuilderModule; use ElementorPro\Plugin; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } /** * Site Editor Module * * Responsible for initializing Elementor Pro App functionality */ class Module extends BaseModule { /** * @var Feature_Lock */ private $lock; /** * Get name. * * @access public * * @return string */ public function get_name() { return 'site-editor'; } /** * @throws \Exception */ public function get_template_types() { // Same as admin menu capabilities. if ( ! current_user_can( 'publish_posts' ) ) { throw new \Exception( 'Access denied' ); } $document_types = Plugin::elementor()->documents->get_document_types( [ 'support_site_editor' => true, ] ); // Keep 404 at end of array. $error_404 = $document_types['error-404']; unset( $document_types['error-404'] ); $document_types['error-404'] = $error_404; // Currently the `single` itself is not supported in site editor. // Don't use `support_site_editor=false` in order to support documents that extend it. unset( $document_types['single'] ); $types = []; foreach ( $document_types as $type => $class ) { $types[] = $class::get_site_editor_config(); } return $types; } /** * Register ajax actions. * * @access public * * @param Ajax $ajax */ public function register_ajax_actions( Ajax $ajax ) { $ajax->register_ajax_action( 'app_site_editor_template_types', [ $this, 'get_template_types' ] ); } /** * @param Render_Mode_Manager $manager * * @throws \Exception */ public function register_render_mode( Render_Mode_Manager $manager ) { $manager->register_render_mode( Render_Mode_Template_Preview::class ); } protected function get_init_settings() { $settings = [ 'urls' => [ 'legacy_view' => add_query_arg( 'tabs_group', ThemeBuilderModule::ADMIN_LIBRARY_TAB_GROUP, admin_url( Source_Local::ADMIN_MENU_SLUG ) ), ], 'utms' => [ 'utm_source' => 'theme-builder', 'utm_medium' => 'wp-dash', ], ]; if ( $this->lock->is_locked() ) { $settings['lock'] = $this->lock->get_config(); } return $settings; } private function add_default_new_site_editor_experiments( ExperimentsManager $manager ) { $manager->add_feature( [ 'name' => 'theme_builder_v2', 'title' => __( 'Default to New Theme Builder', 'elementor-pro' ), 'description' => __( 'Entering the Theme Builder through WP Dashboard > Templates > Theme Builder opens the New theme builder by default. But don’t worry, you can always view the WP styled version of the screen with a simple click of a button.', 'elementor-pro' ), 'release_status' => ExperimentsManager::RELEASE_STATUS_STABLE, 'default' => ExperimentsManager::STATE_ACTIVE, ] ); } /** * Get site editor url. * * @return string */ private function get_site_editor_url() : string { return Plugin::elementor()->app->get_base_url() . '#/site-editor'; } private function register_site_editor_menu() { $experiments_manager = Plugin::elementor()->experiments; // Unique case when the experiments manager is not initialized yet. if ( ! $experiments_manager || ! $experiments_manager->is_feature_active( 'theme_builder_v2' ) ) { return; } // Remove the old theme builder link and add the new one. remove_submenu_page( Source_Local::ADMIN_MENU_SLUG, add_query_arg( 'tabs_group', ThemeBuilderModule::ADMIN_LIBRARY_TAB_GROUP, Source_Local::ADMIN_MENU_SLUG ) ); add_submenu_page( Source_Local::ADMIN_MENU_SLUG, '', __( 'Theme Builder', 'elementor-pro' ), 'publish_posts', $this->get_site_editor_url() ); } private function register_admin_menu( Admin_Menu_Manager $admin_menu_manager ) { $experiments_manager = Plugin::elementor()->experiments; // Unique case when the experiments manager is not initialized yet. if ( ! $experiments_manager || ! $experiments_manager->is_feature_active( 'theme_builder_v2' ) ) { return; } $admin_menu_manager->unregister( add_query_arg( 'tabs_group', ThemeBuilderModule::ADMIN_LIBRARY_TAB_GROUP, Source_Local::ADMIN_MENU_SLUG ) ); $admin_menu_manager->register( $this->get_site_editor_url(), new Theme_Builder_Menu_Item() ); } private function add_finder_item( array $categories ) { if ( ! Plugin::elementor()->experiments->is_feature_active( 'theme_builder_v2' ) ) { return $categories; } // Replace the old theme builder "create-new" link with the new site-editor. $categories['create']['items']['theme-template'] = [ 'title' => __( 'Add New Theme Template', 'elementor-pro' ), 'icon' => 'plus-circle-o', 'url' => $this->get_site_editor_url() . '/add-new', 'keywords' => [ 'template', 'theme', 'new', 'create' ], ]; return $categories; } /** * Module constructor. * * @access public */ public function __construct() { $this->lock = new Feature_Lock( [ 'type' => 'theme-builder' ] ); Plugin::elementor()->data_manager->register_controller( Controller::class ); add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ], 11 /* Override core actions */ ); add_action( 'elementor/frontend/render_mode/register', [ $this, 'register_render_mode' ] ); add_action( 'elementor/experiments/default-features-registered', function ( ExperimentsManager $manager ) { $this->add_default_new_site_editor_experiments( $manager ); } ); add_action( 'elementor/admin/menu/register', function ( Admin_Menu_Manager $admin_menu ) { $this->register_admin_menu( $admin_menu ); }, Theme_Builder_Table_View::ADMIN_MENU_PRIORITY + 1 ); // TODO: BC - Remove after `Admin_Menu_Manager` will be the standard. add_action( 'admin_menu', function () { if ( did_action( 'elementor/admin/menu/register' ) ) { return; } $this->register_site_editor_menu(); }, 23 /* After old theme builder */ ); add_filter( 'elementor/finder/categories', function ( array $categories ) { return $this->add_finder_item( $categories ); }, 11 /* After old theme builder */ ); } } app/modules/kit-library/module.php 0000755 00000004640 15111667160 0013236 0 ustar 00 <?php namespace ElementorPro\Core\App\Modules\KitLibrary; use ElementorPro\Plugin; use ElementorPro\License\API; use ElementorPro\License\Admin; use Elementor\Core\Base\Module as BaseModule; use ElementorPro\Core\Connect\Apps\Activate; use Elementor\Core\App\Modules\KitLibrary\Connect\Kit_Library; 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 set_kit_library_settings() { $common = Plugin::elementor()->common; $app = Plugin::elementor()->app; $prev_settings = $app->get_settings( 'kit-library' ); // BC Support. if ( ! $prev_settings || ! $common ) { return; } /** @var Activate $activate */ $activate = $common->get_component( 'connect' )->get_app( 'activate' ); /** @var Kit_Library $kit_library */ $kit_library = $common->get_component( 'connect' )->get_app( 'kit-library' ); $app->set_settings( 'kit-library', array_merge( $prev_settings, [ 'is_pro' => true, 'is_library_connected' => API::is_license_active() && $kit_library && $kit_library->is_connected(), 'library_connect_url' => $activate->get_admin_url( 'authorize', [ 'utm_source' => 'kit-library', 'utm_medium' => 'wp-dash', 'utm_campaign' => 'connect-and-activate-license', 'utm_term' => '%%page%%', // Will be replaced in the frontend. ] ), 'access_level' => API::get_library_access_level( 'kit' ), ] ) ); } /** * @param array $connect_info * @param $app * * @return array */ private function add_license_to_connect_info( array $connect_info, $app ) { $license_key = Admin::get_license_key(); // In elementor 3.3.0-beta it does not send the $app parameter and it should add the license. $bc_support = ! $app; $is_kit_library_request = $app && Kit_Library::class === get_class( $app ); if ( ! empty( $license_key ) && ( $bc_support || $is_kit_library_request ) ) { $connect_info['license'] = $license_key; } return $connect_info; } public function __construct() { add_action( 'elementor/init', function () { $this->set_kit_library_settings(); }, 13 /** after elementor core */ ); add_filter( 'elementor/connect/additional-connect-info', function ( array $connect_info, $app = null ) { return $this->add_license_to_connect_info( $connect_info, $app ); }, 10, 2 ); } } app/app.php 0000755 00000004573 15111667160 0006635 0 ustar 00 <?php namespace ElementorPro\Core\App; use Elementor\Core\Base\App as BaseApp; use ElementorPro\Plugin; use ElementorPro\Core\App\Modules\SiteEditor\Module as SiteEditor; use ElementorPro\Core\App\Modules\KitLibrary\Module as KitLibrary; use ElementorPro\Core\App\Modules\Onboarding\Module as Onboarding; use ElementorPro\Core\App\Modules\ImportExport\Module as ImportExport; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class App extends BaseApp { /** * Get module name. * * Retrieve the module name. * * @since 3.0.0 * @access public * * @return string Module name. */ public function get_name() { return 'app-pro'; } public function init() { $this->enqueue_assets(); } public function set_menu_url() { Plugin::elementor()->app->set_settings( 'menu_url', Plugin::elementor()->app->get_base_url() . '#/site-editor' ); } protected function get_init_settings() { return [ 'baseUrl' => $this->get_assets_base_url(), ]; } protected function get_assets_base_url() { return ELEMENTOR_PRO_URL; } private function enqueue_assets() { wp_enqueue_style( 'elementor-pro-app', $this->get_css_assets_url( 'app', null, 'default', true ), [ 'elementor-app', 'select2', ], ELEMENTOR_VERSION ); wp_enqueue_script( 'elementor-pro-app', $this->get_js_assets_url( 'app' ), [ 'wp-i18n', 'elementor-app-packages', 'elementor-common', 'select2', ], ELEMENTOR_PRO_VERSION, true ); wp_set_script_translations( 'elementor-pro-app', 'elementor-pro' ); } private function enqueue_config() { // If script didn't loaded, config is still relevant, enqueue without a file. if ( ! wp_script_is( 'elementor-pro-app' ) ) { wp_register_script( 'elementor-pro-app', false, [], ELEMENTOR_PRO_VERSION ); wp_enqueue_script( 'elementor-pro-app' ); } $this->print_config( 'elementor-pro-app' ); } public function __construct() { $this->add_component( 'site-editor', new SiteEditor() ); $this->add_component( 'kit-library', new KitLibrary() ); $this->add_component( 'onboarding', new Onboarding() ); $this->add_component( 'import-export', new ImportExport() ); add_action( 'elementor/app/init', [ $this, 'init' ] ); add_action( 'elementor/common/after_register_scripts', function () { $this->enqueue_config(); } ); add_action( 'elementor/init', [ $this, 'set_menu_url' ] ); } } app/assets/js/index.js 0000755 00000000125 15111667160 0010714 0 ustar 00 import Module from '../../modules/site-editor/assets/js/site-editor'; new Module(); app/assets/js/ui/connect-button.js 0000755 00000001643 15111667160 0013172 0 ustar 00 import React, { useRef, useEffect } from 'react'; import { Button } from '@elementor/app-ui'; import { arrayToClassName } from '../utils.js'; const ConnectButton = ( props ) => { const className = arrayToClassName( [ 'e-app-connect-button', props.className, ] ); const buttonRef = useRef( null ); useEffect( () => { if ( ! buttonRef.current ) { return; } jQuery( buttonRef.current ).elementorConnect(); }, [] ); return ( <Button { ...props } elRef={ buttonRef } className={ className } /> ); }; ConnectButton.propTypes = { ...Button.propTypes, text: PropTypes.string.isRequired, url: PropTypes.string.isRequired, className: PropTypes.string, }; ConnectButton.defaultProps = { className: '', variant: 'contained', size: 'sm', color: 'cta', target: '_blank', rel: 'noopener noreferrer', text: __( 'Connect & Activate', 'elementor' ), }; export default React.memo( ConnectButton ); app/assets/js/hooks/use-feature-lock.js 0000755 00000001155 15111667160 0014107 0 ustar 00 import ConnectButtonUI from '../ui/connect-button'; import { htmlDecodeTextContent, replaceUtmPlaceholders } from '../utils'; export default function useFeatureLock( featureName ) { const appConfig = elementorAppProConfig[ featureName ] ?? {}, isLocked = appConfig.lock?.is_locked ?? false; const buttonText = htmlDecodeTextContent( appConfig.lock?.button.text ); const buttonLink = replaceUtmPlaceholders( appConfig.lock?.button.url ?? '', appConfig.utms ?? {}, ); const ConnectButton = () => ( <ConnectButtonUI text={ buttonText } url={ buttonLink } /> ); return { isLocked, ConnectButton, }; } app/assets/js/utils.js 0000755 00000001452 15111667160 0010751 0 ustar 00 // Copied from Core. export const arrayToClassName = ( array, action ) => { return array .filter( ( item ) => 'object' === typeof ( item ) ? Object.entries( item )[ 0 ][ 1 ] : item ) .map( ( item ) => { const value = 'object' === typeof ( item ) ? Object.entries( item )[ 0 ][ 0 ] : item; return action ? action( value ) : value; } ) .join( ' ' ); }; export const htmlDecodeTextContent = ( input ) => { const doc = new DOMParser().parseFromString( input, 'text/html' ); return doc.documentElement.textContent; }; export const replaceUtmPlaceholders = ( link = '', utms = {} ) => { if ( ! link || ! utms ) { return link; } Object.keys( utms ).forEach( ( key ) => { const match = new RegExp( `%%${ key }%%`, 'g' ); link = link.replace( match, utms[ key ] ); } ); return link; }; app/assets/styles/app-imports.scss 0000755 00000001104 15111667160 0013324 0 ustar 00 @import "../../modules/site-editor/assets/js/molecules/site-template.scss"; @import "../../modules/site-editor/assets/js/pages/add-new.scss"; @import "../../modules/site-editor/assets/js/pages/template-type.scss"; @import "../../modules/site-editor/assets/js/pages/conditions/conditions.scss"; @import "../../modules/site-editor/assets/js/molecules/back-button.scss"; @import "../../modules/site-editor/assets/js/atoms/indicator-bullet.scss"; @import "../../modules/site-editor/assets/js/atoms/preview-iframe.scss"; @import "../../modules/site-editor/assets/js/site-editor.scss"; editor/template.php 0000755 00000001226 15111667160 0010366 0 ustar 00 <?php use ElementorPro\License\Admin as LicenseAdmin; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } ?> <script type="text/template" id="tmpl-elementor-pro-template-library-activate-license-button"> <a class="elementor-template-library-template-action elementor-button go-pro" href="<?php // PHPCS - the function LicenseAdmin::get_url() is safe. echo LicenseAdmin::get_url(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" target="_blank"> <i class="eicon-external-link-square"></i> <span class="elementor-button-title"><?php echo esc_html__( 'Activate License', 'elementor-pro' ); ?></span> </a> </script> editor/notice-bar.php 0000755 00000011407 15111667160 0010600 0 ustar 00 <?php namespace ElementorPro\Core\Editor; use Elementor\Core\Editor\Notice_Bar as Base_Notice_Bar; use ElementorPro\License\Admin; use ElementorPro\License\API as License_API; use ElementorPro\Plugin; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Notice_Bar extends Base_Notice_Bar { const ELEMENTOR_PRO_EDITOR_GO_PRO_TRIAL_ABOUT_TO_EXPIRE_LICENSE_NOTICE_DISMISSED = '_elementor_pro_editor_go_pro_trial_about_to_expire_license_notice_dismissed'; const ELEMENTOR_PRO_EDITOR_GO_PRO_TRIAL_EXPIRED_LICENSE_NOTICE_DISMISSED = '_elementor_pro_editor_go_pro_trial_expired_license_notice_dismissed'; const ELEMENTOR_PRO_EDITOR_RENEW_LICENSE_NOTICE_DISMISSED = '_elementor_pro_editor_renew_license_notice_dismissed'; const ELEMENTOR_PRO_EDITOR_ACTIVATE_LICENSE_NOTICE_DISMISSED = '_elementor_pro_editor_activate_license_notice_dismissed'; const ELEMENTOR_PRO_EDITOR_RENEW_ABOUT_TO_EXPIRE_LICENSE_NOTICE_DISMISSED = '_elementor_pro_editor_renew_about_to_expire_license_notice_dismissed'; protected function get_init_settings() { $license_data = License_API::get_license_data(); $license_admin = Plugin::instance()->license_admin; if ( License_API::is_license_active() && License_API::is_licence_pro_trial() ) { return [ 'option_key' => self::ELEMENTOR_PRO_EDITOR_GO_PRO_TRIAL_ABOUT_TO_EXPIRE_LICENSE_NOTICE_DISMISSED, 'message' => esc_html__( 'Heads up! You are using a free trial. Want to enjoy Pro widgets & templates for a whole year?', 'elementor-pro' ) . sprintf( ' <a href="https://my.elementor.com/upgrade-subscription/?utm_source=editor-notice-bar&utm_medium=wp-dash&utm_campaign=pro-trial&utm_content=trial-period" target="_blank">%s</a>', esc_html__( 'Go Pro now', 'elementor-pro' ) ), 'action_title' => '', 'action_url' => '', 'muted_period' => 0, ]; } if ( License_API::is_license_expired() && License_API::is_licence_pro_trial() ) { return [ 'option_key' => self::ELEMENTOR_PRO_EDITOR_GO_PRO_TRIAL_EXPIRED_LICENSE_NOTICE_DISMISSED, 'message' => esc_html__( 'Your trial has expired. Miss your favorite Elementor Pro features?', 'elementor-pro' ) . sprintf( ' <a href="https://my.elementor.com/upgrade-subscription/?utm_source=editor-notice-bar&utm_medium=wp-dash&utm_campaign=pro-trial&utm_content=trial-expired" target="_blank">%s</a>', esc_html__( 'Upgrade now', 'elementor-pro' ) ), 'action_title' => '', 'action_url' => '', 'muted_period' => 0, ]; } if ( License_API::is_license_expired() ) { return [ 'option_key' => self::ELEMENTOR_PRO_EDITOR_RENEW_LICENSE_NOTICE_DISMISSED, 'icon' => 'eicon-lock', 'message' => esc_html__( 'Renew to unlock all Elementor Pro features', 'elementor-pro' ), 'action_title' => esc_html__( 'Renew now', 'elementor-pro' ), 'action_url' => 'https://go.elementor.com/editor-notice-bar-renew/', 'secondary_message' => esc_html__( 'Already renewed?', 'elementor-pro' ), 'secondary_action_title' => esc_html__( 'Reload Editor', 'elementor-pro' ), 'secondary_action_url' => Admin::get_url() . '&redirect-to-document=' . Plugin::elementor()->documents->get_current()->get_id(), 'secondary_action_target' => '_self', 'muted_period' => 0, ]; } if ( ! License_API::is_license_active() ) { return [ 'option_key' => self::ELEMENTOR_PRO_EDITOR_ACTIVATE_LICENSE_NOTICE_DISMISSED, 'message' => esc_html__( 'Activate Your License and Get Access to Premium Elementor Templates, Support & Plugin Updates.', 'elementor-pro' ), 'action_title' => esc_html__( 'Connect & Activate', 'elementor-pro' ), 'action_url' => $license_admin->get_connect_url( [ 'mode' => 'popup', 'callback_id' => 'editor-pro-activate', // UTM 'utm_source' => 'editor-notice-bar', 'utm_medium' => 'wp-dash', 'utm_campaign' => 'connect-and-activate-license', ] ), 'muted_period' => 0, ]; } if ( ! License_API::is_license_about_to_expire() ) { return []; } if ( isset( $license_data['renewal_discount'] ) && 0 < $license_data['renewal_discount'] ) { $message = sprintf( /* translators: %s: Renewal discount. */ esc_html__( 'Your Elementor Pro license is about to expire. Renew now and get an exclusive, time-limited %s discount.', 'elementor-pro' ), $license_data['renewal_discount'] . '%' ); } else { $message = esc_html__( 'Your Elementor Pro license is about to expire. Renew now and get updates, support, Pro widgets & templates for another year.', 'elementor-pro' ); } return [ 'option_key' => self::ELEMENTOR_PRO_EDITOR_RENEW_ABOUT_TO_EXPIRE_LICENSE_NOTICE_DISMISSED, 'message' => $message, 'action_title' => esc_html__( 'Renew now', 'elementor-pro' ), 'action_url' => 'https://go.elementor.com/editor-notice-bar-renew/', 'muted_period' => 1, ]; } } editor/editor.php 0000755 00000012030 15111667160 0010034 0 ustar 00 <?php namespace ElementorPro\Core\Editor; use Elementor\Core\Base\App; use Elementor\Core\Utils\Assets_Config_Provider; use Elementor\Core\Utils\Assets_Translation_Loader; use ElementorPro\License\Admin as License_Admin; use ElementorPro\License\API as License_API; use ElementorPro\Plugin; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Editor extends App { const EDITOR_V2_PACKAGES = [ 'editor-documents-extended', 'editor-site-navigation-extended', ]; /** * Get app name. * * Retrieve the app name. * * @return string app name. * @since 2.6.0 * @access public * */ public function get_name() { return 'pro-editor'; } public function __construct() { add_action( 'elementor/init', [ $this, 'on_elementor_init' ] ); add_action( 'elementor/editor/init', [ $this, 'on_elementor_editor_init' ] ); add_action( 'elementor/editor/after_enqueue_styles', [ $this, 'enqueue_editor_styles' ] ); add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue_editor_scripts' ] ); add_filter( 'elementor/editor/localize_settings', [ $this, 'localize_settings' ] ); add_filter( 'elementor/editor/panel/get_pro_details', function( $get_pro_details ) { if ( defined( '\Elementor\Modules\Apps\Module::PAGE_ID' ) ) { $get_pro_details['link'] = admin_url( 'admin.php?page=' . \Elementor\Modules\Apps\Module::PAGE_ID ); $get_pro_details['message'] = __( 'Extend Elementor With Apps', 'elementor-pro' ); $get_pro_details['button_text'] = __( 'Explore Apps', 'elementor-pro' ); } return $get_pro_details; } ); add_action( 'elementor/editor/v2/scripts/enqueue', function () { $this->enqueue_editor_v2_scripts(); } ); } public function get_init_settings() { $settings = [ 'isActive' => License_API::is_license_active(), 'urls' => [ 'modules' => ELEMENTOR_PRO_MODULES_URL, 'connect' => License_Admin::get_url(), ], ]; /** * Localized editor settings. * * Filters the localized settings used in the editor as JavaScript variables. * * By default Elementor Pro passes some editor settings to be consumed as JavaScript * variables. This hook allows developers to add extra settings values to be consumed * using JavaScript in the editor. * * @since 1.0.0 * * @param array $settings Localized editor settings. */ $settings = apply_filters( 'elementor_pro/editor/localize_settings', $settings ); return $settings; } public function enqueue_editor_styles() { wp_enqueue_style( 'elementor-pro', $this->get_css_assets_url( 'editor', null, 'default', true ), [ 'elementor-editor', ], ELEMENTOR_PRO_VERSION ); } public function enqueue_editor_scripts() { wp_enqueue_script( 'elementor-pro', $this->get_js_assets_url( 'editor' ), [ 'backbone-marionette', 'elementor-common', 'elementor-editor-modules', 'elementor-editor-document', ], ELEMENTOR_PRO_VERSION, true ); wp_set_script_translations( 'elementor-pro', 'elementor-pro' ); $this->print_config( 'elementor-pro' ); } public function enqueue_editor_v2_scripts() { $assets_config = ( new Assets_Config_Provider() ) ->set_path_resolver( function ( $name ) { return ELEMENTOR_PRO_ASSETS_PATH . "js/packages/{$name}/{$name}.asset.php"; } ); $packages = apply_filters( 'elementor-pro/editor/v2/packages', self::EDITOR_V2_PACKAGES ); foreach ( $packages as $package ) { $assets_config->load( $package ); } foreach ( $assets_config->all() as $package => $config ) { wp_enqueue_script( $config['handle'], $this->get_js_assets_url( "packages/${package}/${package}" ), $config['deps'], ELEMENTOR_PRO_VERSION, true ); wp_set_script_translations( $config['handle'], 'elementor-pro' ); } if ( class_exists( Assets_Translation_Loader::class ) ) { $packages_handles = $assets_config->pluck( 'handle' )->all(); Assets_Translation_Loader::for_handles( $packages_handles ); } } public function localize_settings( array $settings ) { $settings['elementPromotionURL'] = Plugin::instance()->license_admin->get_connect_url([ 'utm_source' => '%s', // Will be replaced in the frontend to the widget name 'utm_medium' => 'wp-dash', 'utm_campaign' => 'connect-and-activate-license', 'utm_content' => 'editor-widget-promotion', ]); $settings['dynamicPromotionURL'] = Plugin::instance()->license_admin->get_connect_url( [ 'utm_source' => '%s', // Will be replaced in the frontend to the control name 'utm_medium' => 'wp-dash', 'utm_campaign' => 'connect-and-activate-license', 'utm_content' => 'editor-dynamic-promotion', ] ); return $settings; } public function on_elementor_init() { Plugin::elementor()->editor->notice_bar = new Notice_Bar(); if ( isset( Plugin::elementor()->editor->promotion ) ) { Plugin::elementor()->editor->promotion = new Promotion(); } } public function on_elementor_editor_init() { Plugin::elementor()->common->add_template( __DIR__ . '/template.php' ); } protected function get_assets_base_url() { return ELEMENTOR_PRO_URL; } } editor/promotion.php 0000755 00000002547 15111667160 0010610 0 ustar 00 <?php namespace ElementorPro\Core\Editor; use ElementorPro\License\API; use ElementorPro\License\Admin; use Elementor\Core\Editor\Promotion as Base_Promotion; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Promotion extends Base_Promotion { public function get_elements_promotion() { if ( API::is_license_active() ) { return parent::get_elements_promotion(); } $is_license_expired = API::is_license_expired(); return [ /* translators: %s: Widget title. */ 'title' => __( '%s Widget', 'elementor-pro' ), 'content' => $is_license_expired /* translators: %s: Widget title. */ ? __( 'Renew your Elementor Pro subscription to get %s and dozens more Pro widgets to expand your web-creation toolbox.', 'elementor-pro' ) /* translators: %s: Widget title. */ : __( 'Use %s widget and dozens more pro features to extend your toolbox and build sites faster and better.', 'elementor-pro' ), 'action_button' => $is_license_expired ? [ 'text' => __( 'Renew now', 'elementor-pro' ), 'url' => 'https://my.elementor.com/subscriptions/?utm_source=%s-pro-widget&utm_medium=wp-dash&utm_campaign=renew-license', 'classes' => [ 'elementor-button', 'elementor-button-brand' ], ] : [ 'text' => __( 'Connect & Activate', 'elementor-pro' ), 'url' => Admin::get_url(), ], ]; } } connect/apps/activate.php 0000755 00000007275 15111667160 0011473 0 ustar 00 <?php namespace ElementorPro\Core\Connect\Apps; use Elementor\Core\Common\Modules\Connect\Apps\Common_App; use ElementorPro\License; use ElementorPro\License\API; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Activate extends Common_App { public function get_title() { return esc_html__( 'Activate', 'elementor-pro' ); } public function get_slug() { return 'activate'; } protected function after_connect() { $this->action_activate_license(); } /** * @since 2.3.0 * @access public */ public function action_authorize() { // In case the first connect was not from Activate App - require a new authorization. if ( $this->is_connected() && ! License\Admin::get_license_key() ) { $this->disconnect(); } parent::action_authorize(); } public function action_activate_pro() { $this->action_activate_license(); } public function action_switch_license() { $this->disconnect(); $this->action_authorize(); } public function action_deactivate() { License\Admin::deactivate(); $this->disconnect(); wp_safe_redirect( License\Admin::get_url() ); die; } public function action_activate_license() { if ( ! $this->is_connected() ) { $this->add_notice( esc_html__( 'Please connect to Elementor in order to activate license.', 'elementor-pro' ), 'error' ); $this->redirect_to_admin_page(); } $license = $this->request( 'get_connected_license' ); if ( empty( $license ) ) { // TODO: add suggestions how to check/resolve. wp_die( 'License not found for user ' . esc_attr( $this->get( 'user' )->email ), esc_html__( 'Elementor Pro', 'elementor-pro' ), [ 'back_link' => true, ] ); } if ( is_wp_error( $license ) ) { wp_die( $license, esc_html__( 'Elementor Pro', 'elementor-pro' ), [ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'back_link' => true, ] ); } $license_key = trim( $license->key ); if ( empty( $license_key ) ) { wp_die( esc_html__( 'License key is missing.', 'elementor-pro' ), esc_html__( 'Elementor Pro', 'elementor-pro' ), [ 'back_link' => true, ] ); } $data = License\API::activate_license( $license_key ); if ( is_wp_error( $data ) ) { wp_die( sprintf( '%s (%s) ', $data->get_error_message(), $data->get_error_code() ), esc_html__( 'Elementor Pro', 'elementor-pro' ), [ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'back_link' => true, ] ); } if ( empty( $data['success'] ) ) { $error_msg = License\API::get_error_message( $data['error'] ); // get_error_message() escapes html wp_die( $error_msg, esc_html__( 'Elementor Pro', 'elementor-pro' ), [ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'back_link' => true, ] ); } License\Admin::set_license_key( $license_key ); License\API::set_license_data( $data ); $this->add_notice( esc_html__( 'License has been activated successfully.', 'elementor-pro' ) ); $this->redirect_to_admin_page( License\Admin::get_url() ); die; } public function action_reset() { if ( current_user_can( 'manage_options' ) ) { delete_option( 'elementor_pro_license_key' ); delete_transient( 'elementor_pro_license_data' ); } $this->redirect_to_admin_page(); } protected function get_popup_success_event_data() { return [ 'templates_access_level' => API::get_library_access_level( 'template' ), 'kits_access_level' => API::get_library_access_level( 'kit' ), ]; } protected function get_app_info() { return [ 'license_data' => [ 'label' => 'License Data', 'value' => get_option( '_elementor_pro_license_data' ), ], 'license_key' => [ 'label' => 'License Key', 'value' => get_option( 'elementor_pro_license_key' ), ], ]; } } connect/manager.php 0000755 00000001113 15111667160 0010323 0 ustar 00 <?php namespace ElementorPro\Core\Connect; use ElementorPro\Core\Connect\Apps\Activate; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Manager { /** * @param \Elementor\Core\Common\Modules\Connect\Module $apps_manager */ public function register_apps( $apps_manager ) { $apps = [ 'activate' => Activate::get_class_name(), ]; foreach ( $apps as $slug => $class ) { $apps_manager->register_app( $slug, $class ); } } public function __construct() { add_action( 'elementor/connect/apps/register', [ $this, 'register_apps' ] ); } } modules-manager.php 0000755 00000004264 15111667160 0010352 0 ustar 00 <?php namespace ElementorPro\Core; use ElementorPro\Plugin; use ElementorPro\Base\Module_Base; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } final class Modules_Manager { /** * @var Module_Base[] */ private $modules = []; public function __construct() { $modules = [ 'query-control', 'custom-attributes', 'custom-css', 'page-transitions', // role-manager Must be before Global Widget 'role-manager', 'global-widget', 'assets-manager', 'popup', 'motion-fx', 'usage', 'screenshots', 'compatibility-tag', 'admin-top-bar', 'notes', 'announcements', // Modules with Widgets. 'theme-builder', 'loop-builder', 'posts', 'gallery', 'forms', 'slides', 'nav-menu', 'animated-headline', 'hotspot', 'pricing', 'flip-box', 'call-to-action', 'carousel', 'table-of-contents', 'countdown', 'share-buttons', 'theme-elements', 'blockquote', 'custom-code', 'woocommerce', 'social', 'library', 'dynamic-tags', 'scroll-snap', 'sticky', 'wp-cli', 'lottie', 'code-highlight', 'video-playlist', 'payments', 'progress-tracker', 'mega-menu', 'nested-carousel', 'loop-filter', ]; foreach ( $modules as $module_name ) { $class_name = str_replace( '-', ' ', $module_name ); $class_name = str_replace( ' ', '', ucwords( $class_name ) ); $class_name = '\ElementorPro\Modules\\' . $class_name . '\Module'; /** @var Module_Base $class_name */ $experimental_data = $class_name::get_experimental_data(); if ( $experimental_data ) { Plugin::elementor()->experiments->add_feature( $experimental_data ); if ( ! Plugin::elementor()->experiments->is_feature_active( $experimental_data['name'] ) ) { continue; } } if ( $class_name::is_active() ) { $this->modules[ $module_name ] = $class_name::instance(); } } } /** * @param string $module_name * * @return Module_Base|Module_Base[] */ public function get_modules( $module_name ) { if ( $module_name ) { if ( isset( $this->modules[ $module_name ] ) ) { return $this->modules[ $module_name ]; } return null; } return $this->modules; } } notifications/notifications-manager.php 0000755 00000001063 15111667160 0014416 0 ustar 00 <?php namespace ElementorPro\Core\Notifications; use ElementorPro\Plugin; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Notifications_Manager { /** * Send a notification. * * @param \ElementorPro\Core\Notifications\Notification $notification * @param $notifiable * * @throws \Exception * * @return $this */ public function send( Notification $notification, $notifiable ) { $payloads = $notification->get_payloads( $notifiable ); Plugin::instance()->integrations->run( $payloads ); return $this; } } notifications/notification.php 0000755 00000001403 15111667160 0012621 0 ustar 00 <?php namespace ElementorPro\Core\Notifications; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } abstract class Notification { /** * Get the payloads of the notification data shape (e.g. `Email_Message`, `Database_Message`). Those will automatically * be sent over to the appropriate `Actions` under the `Integration_Manager` (using the `notify()` method). * This method is also used to determine notification channels based on user ($notifiable) preferences. * * Returned shape: * [ * $payload1_instance, * $payload2_instance, * ] * * @param \ElementorPro\Core\Notifications\Traits\Notifiable $notifiable - The notified model. * * @return array */ public function get_payloads( $notifiable ) { return []; } } notifications/traits/notifiable.php 0000755 00000001246 15111667160 0013562 0 ustar 00 <?php namespace ElementorPro\Core\Notifications\Traits; use ElementorPro\Core\Notifications\Notification; use ElementorPro\Plugin; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } trait Notifiable { /** * Notify a Model with a notification. * Syntactic sugar for sending notifications via the `Notifications_Manager`. * * Usage: * $model->notify( new User_Created_Notification( $new_user ) ); * * @param Notification $notification - Notification to send. * * @throws \Exception * * @return void */ public function notify( Notification $notification ) { Plugin::instance()->notifications->send( $notification, $this ); } } utils.php 0000755 00000027353 15111667160 0006436 0 ustar 00 <?php namespace ElementorPro\Core; use ElementorPro\Plugin; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Utils { public static function get_public_post_types( $args = [] ) { $post_type_args = [ // Default is the value $public. 'show_in_nav_menus' => true, ]; // Keep for backwards compatibility if ( ! empty( $args['post_type'] ) ) { $post_type_args['name'] = $args['post_type']; unset( $args['post_type'] ); } $post_type_args = wp_parse_args( $post_type_args, $args ); $_post_types = get_post_types( $post_type_args, 'objects' ); $post_types = []; foreach ( $_post_types as $post_type => $object ) { $post_types[ $post_type ] = $object->label; } /** * Supported post types. * * Filters the allowed post types Elementor should work on. * * By default Elementor can be applied on publicly available post * types. This hook allows developers to alter those post types to * add new and remove existing types. * * @since 2.3.0 * * @param array $post_types Elementor supported post types. */ $post_types = apply_filters( 'elementor_pro/utils/get_public_post_types', $post_types ); return $post_types; } public static function get_client_ip() { $server_ip_keys = [ 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR', ]; foreach ( $server_ip_keys as $key ) { $value = self::_unstable_get_super_global_value( $_SERVER, $key ); if ( $value && filter_var( $value, FILTER_VALIDATE_IP ) ) { return $value; } } // Fallback local ip. return '127.0.0.1'; } public static function get_site_domain() { return str_ireplace( 'www.', '', parse_url( home_url(), PHP_URL_HOST ) ); } public static function get_current_post_id() { if ( isset( Plugin::elementor()->documents ) ) { return Plugin::elementor()->documents->get_current()->get_main_id(); } return get_the_ID(); } public static function get_the_archive_url() { $url = ''; if ( is_category() || is_tag() || is_tax() ) { $url = get_term_link( get_queried_object() ); } elseif ( is_author() ) { $url = get_author_posts_url( get_queried_object_id() ); } elseif ( is_year() ) { $url = get_year_link( get_query_var( 'year' ) ); } elseif ( is_month() ) { $url = get_month_link( get_query_var( 'year' ), get_query_var( 'monthnum' ) ); } elseif ( is_day() ) { $url = get_day_link( get_query_var( 'year' ), get_query_var( 'monthnum' ), get_query_var( 'day' ) ); } elseif ( is_post_type_archive() ) { $url = get_post_type_archive_link( get_post_type() ); } return $url; } public static function get_page_title( $include_context = true ) { $title = ''; if ( is_singular() ) { /* translators: %s: Search term. */ $title = get_the_title(); if ( $include_context ) { $post_type_obj = get_post_type_object( get_post_type() ); $title = sprintf( '%s: %s', $post_type_obj->labels->singular_name, $title ); } } elseif ( is_search() ) { /* translators: %s: Search term. */ $title = sprintf( esc_html__( 'Search Results for: %s', 'elementor-pro' ), get_search_query() ); if ( get_query_var( 'paged' ) ) { /* translators: %s: Page number. */ $title .= sprintf( esc_html__( ' – Page %s', 'elementor-pro' ), get_query_var( 'paged' ) ); } } elseif ( is_category() ) { $title = single_cat_title( '', false ); if ( $include_context ) { /* translators: Category archive title. %s: Category name. */ $title = sprintf( esc_html__( 'Category: %s', 'elementor-pro' ), $title ); } } elseif ( is_tag() ) { $title = single_tag_title( '', false ); if ( $include_context ) { /* translators: Tag archive title. %s: Tag name. */ $title = sprintf( esc_html__( 'Tag: %s', 'elementor-pro' ), $title ); } } elseif ( is_author() ) { $title = '<span class="vcard">' . get_the_author() . '</span>'; if ( $include_context ) { /* translators: Author archive title. %s: Author name. */ $title = sprintf( esc_html__( 'Author: %s', 'elementor-pro' ), $title ); } } elseif ( is_year() ) { $title = get_the_date( _x( 'Y', 'yearly archives date format', 'elementor-pro' ) ); if ( $include_context ) { /* translators: Yearly archive title. %s: Year. */ $title = sprintf( esc_html__( 'Year: %s', 'elementor-pro' ), $title ); } } elseif ( is_month() ) { $title = get_the_date( _x( 'F Y', 'monthly archives date format', 'elementor-pro' ) ); if ( $include_context ) { /* translators: Monthly archive title. %s: Month name and a year. */ $title = sprintf( esc_html__( 'Month: %s', 'elementor-pro' ), $title ); } } elseif ( is_day() ) { $title = get_the_date( _x( 'F j, Y', 'daily archives date format', 'elementor-pro' ) ); if ( $include_context ) { /* translators: Daily archive title. %s: Date. */ $title = sprintf( esc_html__( 'Day: %s', 'elementor-pro' ), $title ); } } elseif ( is_tax( 'post_format' ) ) { if ( is_tax( 'post_format', 'post-format-aside' ) ) { $title = _x( 'Asides', 'post format archive title', 'elementor-pro' ); } elseif ( is_tax( 'post_format', 'post-format-gallery' ) ) { $title = _x( 'Galleries', 'post format archive title', 'elementor-pro' ); } elseif ( is_tax( 'post_format', 'post-format-image' ) ) { $title = _x( 'Images', 'post format archive title', 'elementor-pro' ); } elseif ( is_tax( 'post_format', 'post-format-video' ) ) { $title = _x( 'Videos', 'post format archive title', 'elementor-pro' ); } elseif ( is_tax( 'post_format', 'post-format-quote' ) ) { $title = _x( 'Quotes', 'post format archive title', 'elementor-pro' ); } elseif ( is_tax( 'post_format', 'post-format-link' ) ) { $title = _x( 'Links', 'post format archive title', 'elementor-pro' ); } elseif ( is_tax( 'post_format', 'post-format-status' ) ) { $title = _x( 'Statuses', 'post format archive title', 'elementor-pro' ); } elseif ( is_tax( 'post_format', 'post-format-audio' ) ) { $title = _x( 'Audio', 'post format archive title', 'elementor-pro' ); } elseif ( is_tax( 'post_format', 'post-format-chat' ) ) { $title = _x( 'Chats', 'post format archive title', 'elementor-pro' ); } } elseif ( is_post_type_archive() ) { $title = post_type_archive_title( '', false ); if ( $include_context ) { /* translators: Post type archive title. %s: Post type name. */ $title = sprintf( esc_html__( 'Archives: %s', 'elementor-pro' ), $title ); } } elseif ( is_tax() ) { $title = single_term_title( '', false ); if ( $include_context ) { $tax = get_taxonomy( get_queried_object()->taxonomy ); /* translators: Taxonomy term archive title. 1: Taxonomy singular name, 2: Current taxonomy term. */ $title = sprintf( esc_html__( '%1$s: %2$s', 'elementor-pro' ), $tax->labels->singular_name, $title ); } } elseif ( is_archive() ) { $title = esc_html__( 'Archives', 'elementor-pro' ); } elseif ( is_404() ) { $title = esc_html__( 'Page Not Found', 'elementor-pro' ); } // End if(). /** * Page title. * * Filters the title of the page. * * By default different pages have different titles depending of the page * context (archive, singular, 404 etc.). This hook allows developers to * alter those titles. * * @since 1.0.0 * * @param string $title Page title to be displayed. */ $title = apply_filters( 'elementor/utils/get_the_archive_title', $title ); return $title; } public static function set_global_authordata() { global $authordata; if ( ! isset( $authordata->ID ) ) { $post = get_post(); $authordata = get_userdata( $post->post_author ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } } /** * Used to overcome core bug when taxonomy is in more then one post type * * @see https://core.trac.wordpress.org/ticket/27918 * * @global array $wp_taxonomies The registered taxonomies. * * * @param array $args * @param string $output * @param string $operator * * @return array */ public static function get_taxonomies( $args = [], $output = 'names', $operator = 'and' ) { global $wp_taxonomies; $field = ( 'names' === $output ) ? 'name' : false; // Handle 'object_type' separately. if ( isset( $args['object_type'] ) ) { $object_type = (array) $args['object_type']; unset( $args['object_type'] ); } $taxonomies = wp_filter_object_list( $wp_taxonomies, $args, $operator ); if ( isset( $object_type ) ) { foreach ( $taxonomies as $tax => $tax_data ) { if ( ! array_intersect( $object_type, $tax_data->object_type ) ) { unset( $taxonomies[ $tax ] ); } } } if ( $field ) { $taxonomies = wp_list_pluck( $taxonomies, $field ); } return $taxonomies; } public static function get_ensure_upload_dir( $path ) { if ( file_exists( $path . '/index.php' ) ) { return $path; } wp_mkdir_p( $path ); $files = [ [ 'file' => 'index.php', 'content' => [ '<?php', '// Silence is golden.', ], ], [ 'file' => '.htaccess', 'content' => [ 'Options -Indexes', '<ifModule mod_headers.c>', ' <Files *.*>', ' Header set Content-Disposition attachment', ' </Files>', '</IfModule>', ], ], ]; foreach ( $files as $file ) { if ( ! file_exists( trailingslashit( $path ) . $file['file'] ) ) { $content = implode( PHP_EOL, $file['content'] ); @ file_put_contents( trailingslashit( $path ) . $file['file'], $content ); } } return $path; } /** * Remove words from a sentence. * * @param string $text * @param integer $length * * @return string */ public static function trim_words( $text, $length ) { if ( $length && str_word_count( $text ) > $length ) { $text = explode( ' ', $text, $length + 1 ); unset( $text[ $length ] ); $text = implode( ' ', $text ); } return $text; } /** * Get a user option with default value as fallback. * TODO: Use `\Elementor\User::get_user_option_with_default()` after this PR is merged: * https://github.com/elementor/elementor/pull/17745 * * @param string $option - Option key. * @param int $user_id - User ID * @param mixed $default - Default fallback value. * * @return mixed */ public static function get_user_option_with_default( $option, $user_id, $default ) { $value = get_user_option( $option, $user_id ); return ( false === $value ) ? $default : $value; } /** * TODO: Use core method instead (after merging PR of the original function in core). * PR URL: https://github.com/elementor/elementor/pull/18670. * * @param $file * @param mixed ...$args * @return false|string */ public static function _unstable_file_get_contents( $file, ...$args ) { if ( ! is_file( $file ) || ! is_readable( $file ) ) { return false; } return file_get_contents( $file, ...$args ); } /** * TODO: Use core method instead (after Pro minimum requirements is updated). * PR URL: https://github.com/elementor/elementor/pull/20392 */ public static function _unstable_get_super_global_value( $super_global, $key ) { if ( ! isset( $super_global[ $key ] ) ) { return null; } if ( $_FILES === $super_global ) { $super_global[ $key ]['name'] = sanitize_file_name( $super_global[ $key ]['name'] ); return $super_global[ $key ]; } return wp_kses_post_deep( wp_unslash( $super_global[ $key ] ) ); } /** * TODO: Use a core method instead (after Pro minimum requirements is updated). * @throws \Exception */ public static function _unstable_get_document_for_edit( $id ) { $document = Plugin::elementor()->documents->get( $id ); if ( ! $document ) { throw new \Exception( 'Not found.' ); } if ( ! $document->is_editable_by_current_user() ) { throw new \Exception( 'Access denied.' ); } return $document; } } admin/canary-deployment.php 0000755 00000001164 15111667160 0012011 0 ustar 00 <?php namespace ElementorPro\Core\Admin; use ElementorPro\License\API; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Canary_Deployment extends \Elementor\Core\Admin\Canary_Deployment { const CURRENT_VERSION = ELEMENTOR_PRO_VERSION; const PLUGIN_BASE = ELEMENTOR_PRO_PLUGIN_BASE; protected function get_canary_deployment_remote_info( $force ) { $version_info = API::get_version( false ); $canary_info = []; if ( ! is_wp_error( $version_info ) && ! empty( $version_info['canary_deployment'] ) ) { $canary_info = $version_info['canary_deployment']; } return $canary_info; } } admin/admin.php 0000755 00000021567 15111667160 0007457 0 ustar 00 <?php namespace ElementorPro\Core\Admin; use Elementor\Core\Base\App; use Elementor\Rollback; use Elementor\Settings; use Elementor\Tools; use Elementor\Utils; use ElementorPro\Core\Utils as ProUtils; use ElementorPro\License\API; use ElementorPro\Plugin; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Admin extends App { const USAGE_PARAM_INSTALL_TIME = 'install_time_pro'; /** * Get module name. * * Retrieve the module name. * * @since 2.3.0 * @access public * * @return string Module name. */ public function get_name() { return 'admin'; } /** * Enqueue admin styles. * * @since 1.0.0 * @return void */ public function enqueue_styles() { $suffix = Utils::is_script_debug() ? '' : '.min'; $direction_suffix = is_rtl() ? '-rtl' : ''; wp_register_style( 'elementor-pro-admin', ELEMENTOR_PRO_ASSETS_URL . 'css/admin' . $direction_suffix . $suffix . '.css', [], ELEMENTOR_PRO_VERSION ); wp_enqueue_style( 'elementor-pro-admin' ); } public function enqueue_scripts() { $suffix = Utils::is_script_debug() ? '' : '.min'; wp_enqueue_script( 'elementor-pro-admin', ELEMENTOR_PRO_URL . 'assets/js/admin' . $suffix . '.js', [ 'elementor-admin', ], ELEMENTOR_PRO_VERSION, true ); $locale_settings = []; /** * Localized admin settings. * * Filters the localized settings used in the admin as JavaScript variables. * * By default Elementor Pro passes some admin settings to be consumed as JavaScript * variables. This hook allows developers to add extra settings values to be consumed * using JavaScript in WordPress admin. * * @since 1.0.0 * * @param array $locale_settings Localized settings. */ $locale_settings = apply_filters( 'elementor_pro/admin/localize_settings', $locale_settings ); Utils::print_js_config( 'elementor-pro-admin', 'ElementorProConfig', $locale_settings ); } public function remove_go_pro_menu() { remove_action( 'admin_menu', [ Plugin::elementor()->settings, 'register_pro_menu' ], Settings::MENU_PRIORITY_GO_PRO ); } private function get_rollback_versions() { $rollback_versions = get_transient( 'elementor_pro_rollback_versions_' . ELEMENTOR_PRO_VERSION ); if ( false === $rollback_versions ) { $max_versions = 30; $versions = apply_filters( 'elementor-pro/settings/rollback/versions', [] ); if ( empty( $versions ) ) { $versions = API::get_previous_versions(); if ( is_wp_error( $versions ) ) { return []; } } $rollback_versions = []; $current_index = 0; foreach ( $versions as $version ) { if ( $max_versions <= $current_index ) { break; } $lowercase_version = strtolower( $version ); $is_valid_rollback_version = ! preg_match( '/(trunk|beta|rc|dev)/i', $lowercase_version ); /** * Is valid rollback version. * * Filters whether the version of the rollback is valid or not. * * By default Elementor doesn't allow to rollback for trunk/beta/rc/dev versions. * This hook allows developers to enable a rollback for thise kind of versions by * returning `true`. * * @param bool $is_valid_rollback_version Whether a rollback version is valid. * @param array $lowercase_version A list of previous versions. */ $is_valid_rollback_version = apply_filters( 'elementor-pro/settings/tools/rollback/is_valid_rollback_version', $is_valid_rollback_version, $lowercase_version ); if ( ! $is_valid_rollback_version ) { continue; } if ( version_compare( $version, ELEMENTOR_VERSION, '>=' ) ) { continue; } $current_index++; $rollback_versions[] = $version; } set_transient( 'elementor_pro_rollback_versions_' . ELEMENTOR_PRO_VERSION, $rollback_versions, WEEK_IN_SECONDS ); } return $rollback_versions; } public function register_admin_tools_fields( Tools $tools ) { $rollback_html = '<select class="elementor-rollback-select">'; foreach ( $this->get_rollback_versions() as $version ) { $rollback_html .= "<option value='{$version}'>$version</option>"; } $rollback_html .= '</select>'; // Rollback $tools->add_fields( 'versions', 'rollback', [ 'rollback_pro_separator' => [ 'field_args' => [ 'type' => 'raw_html', 'html' => '<hr>', ], ], 'rollback_pro' => [ 'label' => esc_html__( 'Rollback Pro Version', 'elementor-pro' ), 'field_args' => [ 'type' => 'raw_html', 'html' => sprintf( $rollback_html . '<a data-placeholder-text="' . esc_html__( 'Reinstall', 'elementor-pro' ) . ' v{VERSION}" href="#" data-placeholder-url="%s" class="button elementor-button-spinner elementor-rollback-button">%s</a>', wp_nonce_url( admin_url( 'admin-post.php?action=elementor_pro_rollback&version=VERSION' ), 'elementor_pro_rollback' ), __( 'Reinstall', 'elementor-pro' ) ), 'desc' => '<span style="color: red;">' . esc_html__( 'Warning: Please backup your database before making the rollback.', 'elementor-pro' ) . '</span>', ], ], ] ); } public function post_elementor_pro_rollback() { check_admin_referer( 'elementor_pro_rollback' ); $rollback_versions = $this->get_rollback_versions(); $version = ProUtils::_unstable_get_super_global_value( $_GET, 'version' ); if ( ! $version || ! in_array( $version, $rollback_versions, true ) ) { wp_die( esc_html__( 'Error occurred, The version selected is invalid. Try selecting different version.', 'elementor-pro' ) ); } /** * Filter to allow override the rollback process. * Should return an instance of `Rollback` class. * * @since 3.16.0 * * @param Rollback|null $rollback The rollback instance. * @param string $version The version to roll back to. */ $rollback = apply_filters( 'elementor-pro/settings/rollback', null, $version ); if ( ! ( $rollback instanceof Rollback ) ) { $package_url = API::get_plugin_package_url( $version ); if ( is_wp_error( $package_url ) ) { wp_die( $package_url ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } $rollback = new Rollback( [ 'version' => $version, 'plugin_name' => ELEMENTOR_PRO_PLUGIN_BASE, 'plugin_slug' => basename( ELEMENTOR_PRO__FILE__, '.php' ), 'package_url' => $package_url, ] ); } $rollback->run(); wp_die( '', esc_html__( 'Rollback to Previous Version', 'elementor-pro' ), [ 'response' => 200 ] ); } public function plugin_action_links( $links ) { unset( $links['go_pro'] ); return $links; } public function plugin_row_meta( $plugin_meta, $plugin_file ) { if ( ELEMENTOR_PRO_PLUGIN_BASE === $plugin_file ) { $row_meta = [ 'changelog' => '<a href="https://go.elementor.com/pro-changelog/" title="' . esc_attr( esc_html__( 'View Elementor Pro Changelog', 'elementor-pro' ) ) . '" target="_blank">' . esc_html__( 'Changelog', 'elementor-pro' ) . '</a>', ]; $plugin_meta = array_merge( $plugin_meta, $row_meta ); } return $plugin_meta; } public function change_tracker_params( $params ) { unset( $params['is_first_time'] ); if ( ! isset( $params['events'] ) ) { $params['events'] = []; } $params['events'] = array_merge( $params['events'], [ self::USAGE_PARAM_INSTALL_TIME => gmdate( 'Y-m-d H:i:s', Plugin::instance()->license_admin->get_installed_time() ), ] ); return $params; } public function add_finder_items( array $categories ) { $settings_url = Settings::get_url(); $categories['settings']['items']['integrations'] = [ 'title' => esc_html__( 'Integrations', 'elementor-pro' ), 'icon' => 'integration', 'url' => $settings_url . '#tab-integrations', 'keywords' => [ 'integrations', 'settings', 'typekit', 'facebook', 'recaptcha', 'mailchimp', 'drip', 'activecampaign', 'getresponse', 'convertkit', 'elementor' ], ]; return $categories; } /** * Admin constructor. */ public function __construct() { $this->add_component( 'canary-deployment', new Canary_Deployment() ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_styles' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); add_action( 'admin_menu', [ $this, 'remove_go_pro_menu' ], 0 ); add_action( 'elementor/admin/after_create_settings/' . Tools::PAGE_ID, [ $this, 'register_admin_tools_fields' ], 50 ); add_filter( 'plugin_action_links_' . ELEMENTOR_PLUGIN_BASE, [ $this, 'plugin_action_links' ], 50 ); add_filter( 'plugin_row_meta', [ $this, 'plugin_row_meta' ], 10, 2 ); add_filter( 'elementor/finder/categories', [ $this, 'add_finder_items' ] ); add_filter( 'elementor/tracker/send_tracking_data_params', [ $this, 'change_tracker_params' ], 200 ); add_action( 'admin_post_elementor_pro_rollback', [ $this, 'post_elementor_pro_rollback' ] ); add_action( 'in_plugin_update_message-' . ELEMENTOR_PRO_PLUGIN_BASE, function( $plugin_data ) { Plugin::elementor()->admin->version_update_warning( ELEMENTOR_PRO_VERSION, $plugin_data['new_version'] ); } ); } } upgrade/manager.php 0000755 00000001616 15111667160 0010331 0 ustar 00 <?php namespace ElementorPro\Core\Upgrade; use Elementor\Core\Upgrade\Manager as Upgrades_Manager; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Manager extends Upgrades_Manager { public function get_action() { return 'elementor_pro_updater'; } public function get_plugin_name() { return 'elementor-pro'; } public function get_plugin_label() { return esc_html__( 'Elementor Pro', 'elementor-pro' ); } public function get_updater_label() { return esc_html__( 'Elementor Pro Data Updater', 'elementor-pro' ); } public function get_new_version() { return ELEMENTOR_PRO_VERSION; } public function get_version_option_name() { return 'elementor_pro_version'; } public function get_upgrades_class() { return 'ElementorPro\Core\Upgrade\Upgrades'; } public static function get_install_history_meta() { return 'elementor_pro_install_history'; } } upgrade/upgrades.php 0000755 00000102330 15111667160 0010524 0 ustar 00 <?php namespace ElementorPro\Core\Upgrade; use Elementor\Core\Base\Document; use Elementor\Core\Upgrade\Updater; use Elementor\Icons_Manager; use Elementor\Core\Upgrade\Upgrades as Core_Upgrades; use ElementorPro\License\API; use ElementorPro\Plugin; use Elementor\Modules\History\Revisions_Manager; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Upgrades { public static $typography_control_names = [ 'typography', // The popover toggle ('starter_name'). 'font_family', 'font_size', 'font_weight', 'text_transform', 'font_style', 'text_decoration', 'line_height', 'letter_spacing', ]; public static function _on_each_version( $updater ) { self::_remove_remote_info_api_data(); } public static function _v_1_3_0() { global $wpdb; // Fix Button widget to new sizes options $post_ids = $wpdb->get_col( 'SELECT `post_id` FROM `' . $wpdb->postmeta . '` WHERE `meta_key` = "_elementor_data" AND `meta_value` LIKE \'%"widgetType":"form"%\';' ); if ( empty( $post_ids ) ) { return; } foreach ( $post_ids as $post_id ) { $document = Plugin::elementor()->documents->get( $post_id ); if ( $document ) { $data = $document->get_elements_data(); } if ( empty( $data ) ) { continue; } $data = Plugin::elementor()->db->iterate_data( $data, function( $element ) { if ( empty( $element['widgetType'] ) || 'form' !== $element['widgetType'] ) { return $element; } if ( ! isset( $element['settings']['submit_actions'] ) ) { $element['settings']['submit_actions'] = [ 'email' ]; } if ( ! empty( $element['settings']['redirect_to'] ) ) { if ( ! in_array( 'redirect', $element['settings']['submit_actions'] ) ) { $element['settings']['submit_actions'][] = 'redirect'; } } if ( ! empty( $element['settings']['webhooks'] ) ) { if ( ! in_array( 'webhook', $element['settings']['submit_actions'] ) ) { $element['settings']['submit_actions'][] = 'webhook'; } } return $element; } ); self::save_editor( $post_id, $data ); } } public static function _v_1_4_0() { global $wpdb; // Move all posts columns to classic skin (Just add prefix) $post_ids = $wpdb->get_col( 'SELECT `post_id` FROM `' . $wpdb->postmeta . '` WHERE `meta_key` = "_elementor_data" AND `meta_value` LIKE \'%"widgetType":"posts"%\';' ); if ( empty( $post_ids ) ) { return; } foreach ( $post_ids as $post_id ) { $document = Plugin::elementor()->documents->get( $post_id ); if ( $document ) { $data = $document->get_elements_data(); } if ( empty( $data ) ) { continue; } $data = Plugin::elementor()->db->iterate_data( $data, function( $element ) { if ( empty( $element['widgetType'] ) || 'posts' !== $element['widgetType'] ) { return $element; } $fields_to_change = [ 'columns', 'columns_mobile', 'columns_tablet', ]; foreach ( $fields_to_change as $field ) { // TODO: Remove old value later $new_field_key = 'classic_' . $field; if ( isset( $element['settings'][ $field ] ) && ! isset( $element['settings'][ $new_field_key ] ) ) { $element['settings'][ $new_field_key ] = $element['settings'][ $field ]; } } return $element; } ); $document = Plugin::elementor()->documents->get( $post_id ); $document->save( [ 'elements' => $data, ] ); } } public static function _v_1_12_0() { global $wpdb; // Set `mailchimp_api_key_source` to `custom`. $post_ids = $wpdb->get_col( 'SELECT `post_id` FROM `' . $wpdb->postmeta . '` WHERE `meta_key` = "_elementor_data" AND `meta_value` LIKE \'%"widgetType":"form"%\';' ); if ( empty( $post_ids ) ) { return; } foreach ( $post_ids as $post_id ) { $do_update = false; $document = Plugin::elementor()->documents->get( $post_id ); if ( $document ) { $data = $document->get_elements_data(); } if ( empty( $data ) ) { continue; } $data = Plugin::elementor()->db->iterate_data( $data, function( $element ) use ( &$do_update ) { if ( empty( $element['widgetType'] ) || 'form' !== $element['widgetType'] ) { return $element; } if ( ! empty( $element['settings']['mailchimp_api_key'] ) && ! isset( $element['settings']['mailchimp_api_key_source'] ) ) { $element['settings']['mailchimp_api_key_source'] = 'custom'; $do_update = true; } return $element; } ); // Only update if form has mailchimp if ( ! $do_update ) { continue; } // We need the `wp_slash` in order to avoid the unslashing during the `update_post_meta` $json_value = wp_slash( wp_json_encode( $data ) ); update_metadata( 'post', $post_id, '_elementor_data', $json_value ); } } /** * Replace 'sticky' => 'yes' with 'sticky' => 'top' in sections. */ public static function _v_2_0_3() { global $wpdb; $post_ids = $wpdb->get_col( 'SELECT `post_id` FROM `' . $wpdb->postmeta . '` WHERE `meta_key` = "_elementor_data" AND `meta_value` LIKE \'%"sticky":"yes"%\';' ); if ( empty( $post_ids ) ) { return; } foreach ( $post_ids as $post_id ) { $do_update = false; $document = Plugin::elementor()->documents->get( $post_id ); if ( ! $document ) { continue; } $data = $document->get_elements_data(); if ( empty( $data ) ) { continue; } $data = Plugin::elementor()->db->iterate_data( $data, function( $element ) use ( &$do_update ) { if ( empty( $element['elType'] ) || 'section' !== $element['elType'] ) { return $element; } if ( ! empty( $element['settings']['sticky'] ) && 'yes' === $element['settings']['sticky'] ) { $element['settings']['sticky'] = 'top'; $do_update = true; } return $element; } ); if ( ! $do_update ) { continue; } // We need the `wp_slash` in order to avoid the unslashing during the `update_metadata` $json_value = wp_slash( wp_json_encode( $data ) ); update_metadata( 'post', $post_id, '_elementor_data', $json_value ); } // End foreach(). } private static function save_editor( $post_id, $posted ) { // Change the global post to current library post, so widgets can use `get_the_ID` and other post data if ( isset( $GLOBALS['post'] ) ) { $global_post = $GLOBALS['post']; } $GLOBALS['post'] = get_post( $post_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $editor_data = self::get_editor_data( $posted ); // We need the `wp_slash` in order to avoid the unslashing during the `update_post_meta` $json_value = wp_slash( wp_json_encode( $editor_data ) ); $is_meta_updated = update_metadata( 'post', $post_id, '_elementor_data', $json_value ); if ( $is_meta_updated ) { Revisions_Manager::handle_revision(); } // Restore global post if ( isset( $global_post ) ) { $GLOBALS['post'] = $global_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } else { unset( $GLOBALS['post'] ); } /** * After editor saves data. * * Fires after Elementor editor data was saved. * * @since 1.0.0 * * @param int $post_id The ID of the post. * @param array $editor_data The editor data. */ do_action( 'elementor/editor/after_save', $post_id, $editor_data ); } private static function get_editor_data( $data, $with_html_content = false ) { $editor_data = []; foreach ( $data as $element_data ) { $element = Plugin::elementor()->elements_manager->create_element_instance( $element_data ); if ( ! $element ) { continue; } $editor_data[] = $element->get_raw_data( $with_html_content ); } // End Section return $editor_data; } public static function _v_2_5_0_form( $updater ) { $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_rename_repeater_settings' ], 'control_ids' => [ 'form_fields' => [ '_id' => 'custom_id', ], ], ], ]; return self::_update_widget_settings( 'form', $updater, $changes ); } public static function _v_2_5_0_woocommerce_menu_cart( $updater ) { $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_rename_widget_settings' ], 'control_ids' => [ 'checkout_button_border_color' => 'checkout_border_color', 'view_cart_button_border_color' => 'view_cart_border_color', ], ], [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_slider_to_border_settings' ], 'control_ids' => [ 'checkout_button_border_width' => [ 'new' => 'checkout_border_width', 'add' => 'checkout_border_border', ], 'view_cart_button_border_width' => [ 'new' => 'view_cart_border_width', 'add' => 'view_cart_border_border', ], ], ], ]; return self::_update_widget_settings( 'woocommerce-menu-cart', $updater, $changes ); } public static function _v_3_7_2_woocommerce_rename_related_to_related_products( $updater ) { $changes = self::get_woocommerce_rename_related_to_related_products_changes(); return self::_update_widget_settings( 'woocommerce-products', $updater, $changes ); } public static function _slider_to_border_settings( $element, $args ) { $widget_id = $args['widget_id']; $changes = $args['control_ids']; if ( empty( $element['widgetType'] ) || $widget_id !== $element['widgetType'] ) { return $element; } foreach ( $changes as $old => $new ) { if ( ! empty( $element['settings'][ $old ] ) && ! isset( $element['settings'][ $new['new'] ] ) ) { $new_border_width = [ 'unit' => $element['settings'][ $old ]['unit'], 'top' => $element['settings'][ $old ]['size'], 'bottom' => $element['settings'][ $old ]['size'], 'left' => $element['settings'][ $old ]['size'], 'right' => $element['settings'][ $old ]['size'], 'isLinked' => true, ]; $element['settings'][ $new ['new'] ] = $new_border_width; $element['settings'][ $new ['add'] ] = 'solid'; $args['do_update'] = true; } } return $element; } /** * @param $element * @param $args * * @return mixed */ public static function _rename_repeater_settings( $element, $args ) { $widget_id = $args['widget_id']; $changes = $args['control_ids']; if ( empty( $element['widgetType'] ) || $widget_id !== $element['widgetType'] ) { return $element; } foreach ( $changes as $change_key => $change ) { foreach ( $change as $old => $new ) { foreach ( $element['settings'][ $change_key ] as &$repeater ) { if ( ! empty( $repeater[ $old ] ) && ! isset( $repeater[ $new ] ) ) { $repeater[ $new ] = $repeater[ $old ]; $args['do_update'] = true; } } } } return $element; } private static function taxonomies_mapping( $prefix, $map_to ) { $taxonomy_filter_args = [ 'show_in_nav_menus' => true, ]; $taxonomies = get_taxonomies( $taxonomy_filter_args ); $mapping = []; foreach ( $taxonomies as $taxonomy ) { $mapping[ $prefix . $taxonomy . '_ids' ] = $map_to; } return $mapping; } public static function _v_2_5_0_posts( $updater ) { $add_taxonomies = self::taxonomies_mapping( 'posts_', [ 'posts_include' => 'terms' ] ); $merge_taxonomies = self::taxonomies_mapping( 'posts_', 'posts_include_term_ids' ); $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_rename_widget_settings' ], 'control_ids' => [ 'orderby' => 'posts_orderby', 'order' => 'posts_order', 'offset' => 'posts_offset', 'exclude' => 'posts_exclude', 'exclude_ids' => 'posts_exclude_ids', 'posts_query_id' => 'posts_posts_query_id', 'avoid_duplicates' => 'posts_avoid_duplicates', 'posts_authors' => 'posts_include_authors', ], ], [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_add_widget_settings_to_array' ], 'control_ids' => array_merge( $add_taxonomies, [ 'posts_authors' => [ 'posts_include' => 'authors' ], ] ), ], [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_merge_widget_settings' ], 'control_ids' => $merge_taxonomies, ], ]; return self::_update_widget_settings( 'posts', $updater, $changes ); } public static function _v_2_5_0_portfolio( $updater ) { $add_taxonomies = self::taxonomies_mapping( 'posts_', [ 'posts_include' => 'terms' ] ); $merge_taxonomies = self::taxonomies_mapping( 'posts_', 'posts_include_term_ids' ); $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_rename_widget_settings' ], 'control_ids' => [ 'orderby' => 'posts_orderby', 'order' => 'posts_order', 'offset' => 'posts_offset', 'exclude' => 'posts_exclude', 'exclude_ids' => 'posts_exclude_ids', 'posts_query_id' => 'posts_posts_query_id', 'avoid_duplicates' => 'posts_avoid_duplicates', 'posts_authors' => 'posts_include_authors', ], ], [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_add_widget_settings_to_array' ], 'control_ids' => array_merge( $add_taxonomies, [ 'posts_authors' => [ 'posts_include' => 'authors' ], ] ), ], [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_merge_widget_settings' ], 'control_ids' => $merge_taxonomies, ], ]; return self::_update_widget_settings( 'portfolio', $updater, $changes ); } public static function _v_2_5_0_products( $updater ) { $add_taxonomies = self::taxonomies_mapping( 'query_', [ 'query_include' => 'terms' ] ); $merge_taxonomies = self::taxonomies_mapping( 'query_', 'query_include_term_ids' ); $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_rename_widget_settings' ], 'control_ids' => [ 'orderby' => 'query_orderby', 'order' => 'query_order', 'exclude' => 'query_exclude', 'exclude_ids' => 'query_exclude_ids', 'query_authors' => 'query_include_authors', 'query_product_tag_ids' => 'query_include_term_ids', 'query_product_cat_ids' => 'query_include_term_ids', ], ], [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_add_widget_settings_to_array' ], 'control_ids' => array_merge( $add_taxonomies, [ 'query_authors' => [ 'query_include' => 'authors' ], ] ), ], [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_merge_widget_settings' ], 'control_ids' => $merge_taxonomies, ], ]; return self::_update_widget_settings( 'woocommerce-products', $updater, $changes ); } /** * @param $updater * * @return bool Should run again. */ public static function _v_2_5_0_sitemap( $updater ) { $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_rename_widget_settings' ], 'control_ids' => [ 'exclude' => 'sitemap_exclude', 'exclude_ids' => 'sitemap_exclude_ids', ], ], ]; return self::_update_widget_settings( 'sitemap', $updater, $changes ); } /** * @param Updater $updater * * @return bool */ public static function _v_2_5_0_popup_border_radius( $updater ) { global $wpdb; $post_ids = $updater->query_col( "SELECT pm1.post_id FROM {$wpdb->postmeta} AS pm1 LEFT JOIN {$wpdb->postmeta} AS pm2 ON (pm1.post_id = pm2.post_id) WHERE pm1.meta_key = '_elementor_template_type' AND pm1.meta_value = 'popup' AND pm2.`meta_key` = '" . Document::PAGE_META_KEY . "' AND pm2.`meta_value` LIKE '%border_radius%';" ); if ( empty( $post_ids ) ) { return false; } foreach ( $post_ids as $post_id ) { // Clear WP cache for next step. $document = Plugin::elementor()->documents->get( $post_id ); if ( ! $document ) { continue; } $page_settings = $document->get_settings(); // Check if there isn't 'border_radius' setting or if it has already been upgraded if ( empty( $page_settings['border_radius']['size'] ) ) { continue; } $border_radius = $page_settings['border_radius']; $new_border_radius = [ 'unit' => $border_radius['unit'], 'top' => $border_radius['size'], 'bottom' => $border_radius['size'], 'left' => $border_radius['size'], 'right' => $border_radius['size'], 'isLinked' => true, ]; $page_settings['border_radius'] = $new_border_radius; // TODO: `$document->update_settings`. $document->update_meta( Document::PAGE_META_KEY, $page_settings ); wp_cache_flush(); } // End foreach(). return $updater->should_run_again( $post_ids ); } public static function _v_2_5_4_posts( $updater ) { $merge_taxonomies = self::taxonomies_mapping( 'posts_', 'posts_include_term_ids' ); $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_convert_term_id_to_term_taxonomy_id' ], 'control_ids' => $merge_taxonomies, 'prefix' => 'posts_', 'new_id' => 'include_term_ids', ], ]; return self::_update_widget_settings( 'posts', $updater, $changes ); } public static function _v_2_5_4_portfolio( $updater ) { $merge_taxonomies = self::taxonomies_mapping( 'posts_', 'posts_include_term_ids' ); $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_convert_term_id_to_term_taxonomy_id' ], 'control_ids' => $merge_taxonomies, 'prefix' => 'posts_', 'new_id' => 'include_term_ids', ], ]; return self::_update_widget_settings( 'portfolio', $updater, $changes ); } public static function _v_2_5_4_products( $updater ) { $merge_taxonomies = self::taxonomies_mapping( 'query_', 'query_include_term_ids' ); $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_convert_term_id_to_term_taxonomy_id' ], 'control_ids' => $merge_taxonomies, 'prefix' => 'query_', 'new_id' => 'include_term_ids', ], ]; return self::_update_widget_settings( 'woocommerce-products', $updater, $changes ); } public static function _v_2_5_4_form( $updater ) { $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_missing_form_custom_id_settings' ], 'control_ids' => [], ], ]; return self::_update_widget_settings( 'form', $updater, $changes ); } public static function _v_3_1_0_media_carousel( $updater ) { $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_convert_progress_to_progressbar' ], 'control_ids' => [], ], ]; return self::_update_widget_settings( 'media-carousel', $updater, $changes ); } public static function _v_3_1_0_reviews( $updater ) { $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_convert_progress_to_progressbar' ], 'control_ids' => [], ], ]; return self::_update_widget_settings( 'reviews', $updater, $changes ); } public static function _v_3_1_0_testimonial_carousel( $updater ) { $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_convert_progress_to_progressbar' ], 'control_ids' => [], ], ]; return self::_update_widget_settings( 'testimonial-carousel', $updater, $changes ); } public static function _v_3_1_0_slides( $updater ) { $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_migrate_slides_button_color_settings' ], 'control_ids' => [], ], ]; return self::_update_widget_settings( 'slides', $updater, $changes ); } public static function _v_3_3_0_nav_menu_icon( $updater ) { $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_migrate_indicator_control_to_submenu_icon' ], 'control_ids' => [], ], ]; return self::_update_widget_settings( 'nav-menu', $updater, $changes ); } public static function _v_3_3_0_recalc_usage_data( $updater ) { return Core_Upgrades::recalc_usage_data( $updater ); } public static function _v_3_5_0_price_list( $updater ) { $changes = [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_copy_title_styles_to_new_price_controls' ], 'control_ids' => [], ], ]; return self::_update_widget_settings( 'price-list', $updater, $changes ); } /** * $changes is an array of arrays in the following format: * [ * 'control_ids' => array of control ids * 'callback' => user callback to manipulate the control_ids * ] * * @param $widget_id * @param $updater * @param array $changes * * @return bool */ public static function _update_widget_settings( $widget_id, $updater, $changes ) { global $wpdb; $post_ids = $updater->query_col( 'SELECT `post_id` FROM `' . $wpdb->postmeta . '` WHERE `meta_key` = "_elementor_data" AND `meta_value` LIKE \'%"widgetType":"' . $widget_id . '"%\';' ); if ( empty( $post_ids ) ) { return false; } foreach ( $post_ids as $post_id ) { $do_update = false; $document = Plugin::elementor()->documents->get( $post_id ); if ( ! $document ) { continue; } $data = $document->get_elements_data(); if ( empty( $data ) ) { continue; } // loop through callbacks & array foreach ( $changes as $change ) { $args = [ 'do_update' => &$do_update, 'widget_id' => $widget_id, 'control_ids' => $change['control_ids'], ]; if ( isset( $change['prefix'] ) ) { $args['prefix'] = $change['prefix']; $args['new_id'] = $change['new_id']; } $data = Plugin::elementor()->db->iterate_data( $data, $change['callback'], $args ); if ( ! $do_update ) { continue; } // We need the `wp_slash` in order to avoid the unslashing during the `update_metadata` $json_value = wp_slash( wp_json_encode( $data ) ); update_metadata( 'post', $post_id, '_elementor_data', $json_value ); } } // End foreach(). return $updater->should_run_again( $post_ids ); } /** * @param $element * @param $args * * @return mixed */ public static function _rename_widget_settings( $element, $args ) { $widget_id = $args['widget_id']; $changes = $args['control_ids']; if ( empty( $element['widgetType'] ) || $widget_id !== $element['widgetType'] ) { return $element; } foreach ( $changes as $old => $new ) { if ( ! empty( $element['settings'][ $old ] ) && ! isset( $element['settings'][ $new ] ) ) { $element['settings'][ $new ] = $element['settings'][ $old ]; $args['do_update'] = true; } } return $element; } /** * @param $element * @param $args * * @return mixed */ public static function _rename_widget_settings_value( $element, $args ) { $widget_id = $args['widget_id']; $changes = $args['control_ids']; if ( self::is_widget_matched( $element, $widget_id ) ) { $element = self::apply_rename( $changes, $element, $args ); } return $element; } /** * @param $element * @param $args * * @return mixed */ public static function _add_widget_settings_to_array( $element, $args ) { $widget_id = $args['widget_id']; $changes = $args['control_ids']; if ( empty( $element['widgetType'] ) || $widget_id !== $element['widgetType'] ) { return $element; } foreach ( $changes as $old_key => $added_key ) { if ( ! empty( $element['settings'][ $old_key ] ) ) { foreach ( $added_key as $control_id => $val ) { if ( ! in_array( $val, $element['settings'][ $control_id ], true ) ) { $element['settings'][ $control_id ][] = $val; $args['do_update'] = true; } } } } return $element; } /** * @param $element * @param $args * * @return mixed */ public static function _merge_widget_settings( $element, $args ) { $widget_id = $args['widget_id']; $changes = $args['control_ids']; if ( empty( $element['widgetType'] ) || $widget_id !== $element['widgetType'] ) { return $element; } foreach ( $changes as $old => $new ) { if ( ! empty( $element['settings'][ $old ] ) ) { if ( ! isset( $element['settings'][ $new ] ) ) { $element['settings'][ $new ] = $element['settings'][ $old ]; } else { $element['settings'][ $new ] = array_unique( array_merge( $element['settings'][ $old ], $element['settings'][ $new ] ) ); } $args['do_update'] = true; } } return $element; } /** * Possible scenarios: * 1) custom_id is not empty --> do nothing * 2) Existing _id: Empty or Missing custom_id --> create custom_id and set the value to the value of _id * 3) Missing _id: Empty or Missing custom_id --> generate a unique key and set it as custom_id value * @param $element * @param $args * * @return mixed */ public static function _missing_form_custom_id_settings( $element, $args ) { $widget_id = $args['widget_id']; if ( empty( $element['widgetType'] ) || $widget_id !== $element['widgetType'] ) { return $element; } $random_id = (int) substr( time(), -5 ); //form_fields loop: foreach ( $element['settings']['form_fields'] as &$repeater_item ) { if ( ! empty( $repeater_item['custom_id'] ) ) { // Scenario 1 continue; } if ( ! empty( $repeater_item['_id'] ) ) { // Scenario 2 $repeater_item['custom_id'] = $repeater_item['_id']; } else { // Scenario 3 $repeater_item['custom_id'] = 'field_' . $random_id; $random_id++; } $args['do_update'] = true; } return $element; } /** * Migrates the value saved for the 'indicator' SELECT control in the Nav Menu Widget to the new replacement * 'submenu_icon' ICONS control. * * @param $element * @param $args * * @return mixed; */ public static function _migrate_indicator_control_to_submenu_icon( $element, $args ) { $widget_id = $args['widget_id']; // If the current element is not a Nav Menu widget, go to the next one. if ( empty( $element['widgetType'] ) || $widget_id !== $element['widgetType'] ) { return $element; } // If this Nav Menu widget's 'indicator' control value is the default one (there is no value in the DB), // there is nothing to migrate, since the default icon is identical in the new control. Go to the next element. if ( ! isset( $element['settings']['indicator'] ) ) { return $element; } $new_value = ''; $new_library = 'fa-solid'; switch ( $element['settings']['indicator'] ) { case 'none': $new_library = ''; break; case 'classic': $new_value = 'fa-caret-down'; break; case 'chevron': $new_value = 'fa-chevron-down'; break; case 'angle': $new_value = 'fa-angle-down'; break; case 'plus': $new_value = 'e-plus-icon'; $new_library = ''; break; } // This is done in order to make sure that the menu will not look any different for users who upgrade. // The 'None' option should be completely empty. if ( $new_value ) { if ( Icons_Manager::is_migration_allowed() ) { // If the site has been migrated to FA5, add the new FA Solid class. $new_value = 'fas ' . $new_value; } else { // If the site has not been migrated, add the old generic 'fa' class. $new_value = 'fa ' . $new_value; } } // Set the migrated value for the new control. $element['settings']['submenu_icon'] = [ 'value' => $new_value, 'library' => $new_library, ]; $args['do_update'] = true; return $element; } /** * @param $element * @param $args * * @return mixed */ public static function _convert_term_id_to_term_taxonomy_id( $element, $args ) { $widget_id = $args['widget_id']; $changes = $args['control_ids']; $prefix = $args['prefix']; $new_id = $prefix . $args['new_id']; if ( empty( $element['widgetType'] ) || $widget_id !== $element['widgetType'] ) { return $element; } // Exit if new is empty (should not happen) if ( empty( $element['settings'][ $new_id ] ) ) { return $element; } // 1) Convert each term-id to the equivalent term_taxonomy_id $term_taxonomy_ids = []; $old_term_ids = []; foreach ( $changes as $old => $new ) { if ( ! empty( $element['settings'][ $old ] ) ) { $start = strlen( $prefix ); $end = -strlen( '_ids' ); $taxonomy = substr( $old, $start, $end ); foreach ( $element['settings'][ $old ] as $term_id ) { $old_term_ids[] = $term_id; $term_obj = get_term( $term_id, $taxonomy, OBJECT ); if ( $term_obj && ! is_wp_error( $term_obj ) ) { $term_taxonomy_ids[] = $term_obj->term_taxonomy_id; } } } } // 2) Check if the widget's settings were changed after the u/g to 2.5.0 $diff = array_diff( $element['settings'][ $new_id ], array_unique( $old_term_ids ) ); if ( empty( $diff ) ) { // Nothing was changed $element['settings'][ $new_id . '_backup' ] = $element['settings'][ $new_id ]; $element['settings'][ $new_id ] = $term_taxonomy_ids; $args['do_update'] = true; } return $element; } /** * Convert 'progress' to 'progressbar' * * Before Elementor 2.2.0, the progress bar option key was 'progress'. In Elementor 2.2.0, * it was changed to 'progressbar'. This upgrade script migrated the DB data for old websites using 'progress'. * * @param $element * @param $args * @return mixed */ public static function _convert_progress_to_progressbar( $element, $args ) { $widget_id = $args['widget_id']; if ( empty( $element['widgetType'] ) || $widget_id !== $element['widgetType'] ) { return $element; } if ( 'progress' === $element['settings']['pagination'] ) { $element['settings']['pagination'] = 'progressbar'; $args['do_update'] = true; } return $element; } /** * Migrate Slides Button Color Settings * * Move Slides Widget's 'button_color' settings to 'button_text_color' and 'button_border_color' as necessary, * to allow for removing the redundant control. * * @param $element * @param $args * @return mixed */ public static function _migrate_slides_button_color_settings( $element, $args ) { if ( empty( $element['widgetType'] ) || $args['widget_id'] !== $element['widgetType'] ) { return $element; } // If the element doesn't use the 'button_color' control, no need to do anything. if ( ! isset( $element['settings']['button_color'] ) ) { return $element; } // Check if button_text_color is set. If it is not set, transfer the value from button_color to button_text_color. if ( ! isset( $element['settings']['button_text_color'] ) ) { $element['settings']['button_text_color'] = $element['settings']['button_color']; $args['do_update'] = true; } // Check if button_border_color is set. If it is not set, transfer the value from button_color to button_border_color. if ( ! isset( $element['settings']['button_border_color'] ) ) { $element['settings']['button_border_color'] = $element['settings']['button_color']; $args['do_update'] = true; } return $element; } /** * Copy Title Styles to New Price Controls * * Copy the values from the Price List widget's Title Style controls to new Price Style controls. * * @param $element * @param $args * @return mixed * @since 3.4.0 * */ public static function _copy_title_styles_to_new_price_controls( $element, $args ) { if ( empty( $element['widgetType'] ) || $args['widget_id'] !== $element['widgetType'] ) { return $element; } if ( ! empty( $element['settings']['heading_color'] ) ) { $element['settings']['price_color'] = $element['settings']['heading_color']; $args['do_update'] = true; } $old_control_prefix = 'heading_typography_'; $new_control_prefix = 'price_typography_'; foreach ( self::$typography_control_names as $control_name ) { if ( ! empty( $element['settings'][ $old_control_prefix . $control_name ] ) ) { $element['settings'][ $new_control_prefix . $control_name ] = $element['settings'][ $old_control_prefix . $control_name ]; $args['do_update'] = true; } } return $element; } public static function _remove_remote_info_api_data() { global $wpdb; $key = API::TRANSIENT_KEY_PREFIX; return $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '{$key}%';"); // phpcs:ignore } /** * @param $element * @param $to * @param $control_id * @param $args * @return array */ protected static function set_new_value( $element, $to, $control_id, $args ) { $element['settings'][ $control_id ] = $to; $args['do_update'] = true; return $element; } /** * * @param $change * @param array $element * @param $args * @return array */ protected static function replace_value_if_found( $change, array $element, $args ) { $control_id = key( $args['control_ids'] ); $from = $change['from']; $to = $change['to']; if ( self::is_control_exist_in_settings( $element, $control_id ) && self::is_need_to_replace_value( $element, $control_id, $from ) ) { $element = self::set_new_value( $element, $to, $control_id, $args ); } return $element; } /** * @param $element * @param $widget_id * @return bool */ protected static function is_widget_matched( $element, $widget_id ) { return ! empty( $element['widgetType'] ) && $widget_id === $element['widgetType']; } /** * @param $changes * @param $element * @param $args * @return array|mixed */ protected static function apply_rename( $changes, $element, $args ) { foreach ( $changes as $change ) { $element = self::replace_value_if_found( $change, $element, $args ); } return $element; } /** * @param $element * @param $control_id * @return bool */ protected static function is_control_exist_in_settings( $element, $control_id ) { return ! empty( $element['settings'][ $control_id ] ); } /** * @param $element * @param $new * @return bool */ protected static function is_need_to_replace_value( $element, $control_id, $value_to_replace ) { return $element['settings'][ $control_id ] === $value_to_replace; } /** * @return array[] */ public static function get_woocommerce_rename_related_to_related_products_changes() { return [ [ 'callback' => [ 'ElementorPro\Core\Upgrade\Upgrades', '_rename_widget_settings_value' ], 'control_ids' => [ 'query_post_type' => [ 'from' => 'related', 'to' => 'related_products', ], ], ], ]; } } compatibility/compatibility.php 0000755 00000000771 15111667160 0013013 0 ustar 00 <?php namespace ElementorPro\Core\Compatibility; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Compatibility { public static function register_actions() { add_action( 'init', [ __CLASS__, 'on_init' ] ); } public static function on_init() { static::translate_press(); } private static function translate_press() { if ( ! class_exists( 'TRP_Translate_Press' ) ) { return; } add_filter( 'elementor_pro/license/api/use_home_url', '__return_false' ); } } behaviors/temp-lock-behavior.php 0000755 00000001320 15111667160 0012732 0 ustar 00 <?php namespace ElementorPro\Core\Behaviors; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } // TODO: Used here for testing. Should be removed when it'll be available in the Core. interface Temp_Lock_Behavior { /** * @return bool */ public function is_locked(); /** * @return array { * * @type bool $is_locked * * @type array $badge { * @type string $icon * @type string $text * } * * @type array $content { * @type string $heading * @type string $description * } * * @type array $button { * @type string $text * @type string $url * } * * } */ public function get_config(); } behaviors/feature-lock.php 0000755 00000003100 15111667160 0011621 0 ustar 00 <?php namespace ElementorPro\Core\Behaviors; use ElementorPro\License\API; use ElementorPro\Plugin; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Feature_Lock implements Temp_Lock_Behavior { private $config; public function __construct( $config = [] ) { $this->config = $config; } public function is_locked() { return ! API::is_license_active(); } public function get_config() { $utm_args = [ 'utm_source' => '%%utm_source%%', // Will be replaced in the frontend. 'utm_medium' => '%%utm_medium%%', 'utm_campaign' => API::is_license_expired() ? 'renew-license' : 'connect-and-activate-license', 'utm_term' => $this->config['type'], ]; $connect_url = Plugin::instance()->license_admin->get_connect_url( $utm_args ); $renew_url = add_query_arg( $utm_args, 'https://my.elementor.com/subscriptions/' ); return [ 'is_locked' => $this->is_locked(), 'badge' => [ 'icon' => 'eicon-lock', 'text' => esc_html__( 'Pro', 'elementor-pro' ), ], 'content' => [ 'heading' => esc_html__( 'You need an active Elementor Pro license', 'elementor-pro' ), 'description' => esc_html__( 'Your Elementor Pro license is inactive. To access premium Elementor widgets, templates, support & plugin updates activate your Pro license.', 'elementor-pro' ), ], 'button' => [ 'text' => API::is_license_expired() ? esc_html__( 'Renew now', 'elementor-pro' ) : esc_html__( 'Connect & Activate', 'elementor-pro' ), 'url' => API::is_license_expired() ? $renew_url : $connect_url, ], ]; } } utils/collection.php 0000755 00000002740 15111667160 0010562 0 ustar 00 <?php namespace ElementorPro\Core\Utils; use \Elementor\Core\Utils\Collection as Collection_Base; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } // TODO: Move to Core. class Collection extends Collection_Base implements \JsonSerializable { /** * Change the items key by an item field. * * @param string $key * * @return Collection */ public function key_by( $key ) { return $this->map_with_keys( function ( $item ) use ( $key ) { return [ $item->{$key} => $item ]; } ); } /** * Flatten the items recursively. * * @return array */ public function flatten_recursive() { $output = []; $items = $this->all(); array_walk_recursive($items, function( $item ) use ( &$output ) { $output[] = $item; } ); return $output; } /** * Run array_diff between the collection and other array or collection. * * @param $filter * * @return $this */ public function diff( $filter ) { if ( $filter instanceof Collection_Base ) { $filter = $filter->all(); } return new static( array_diff( $this->all(), $filter ) ); } /** * Reverse the array * * @param false $preserve_keys * * @return $this */ public function reverse( $preserve_keys = false ) { return new static( array_reverse( $this->all(), $preserve_keys ) ); } /** * Return a JSON serialized representation of the Collection. * * @return array */ #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->all(); } } utils/registrar.php 0000755 00000002217 15111667160 0010430 0 ustar 00 <?php namespace ElementorPro\Core\Utils; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Basic items registrar. * * TODO: Move to Core. */ class Registrar { /** * @var array */ private $items; /** * Registrar constructor. * * @return void */ public function __construct() { $this->items = []; } /** * Register a new item. * * @param $instance - Item instance. * @param string $id - Optional - For BC - Deprecated. * * @return boolean - Whether the item was registered. */ public function register( $instance, $id = null ) { // TODO: For BC. Remove in the future. if ( ! $id ) { // Get the ID or default to the class name. $id = ( method_exists( $instance, 'get_id' ) ) ? $instance->get_id() : get_class( $instance ); } if ( $this->get( $id ) ) { return false; } $this->items[ $id ] = $instance; return true; } /** * Get an item by ID. * * @param string $id * * @return array|null */ public function get( $id = null ) { if ( ! $id ) { return $this->items; } return isset( $this->items[ $id ] ) ? $this->items[ $id ] : null; } } integrations/exceptions/action-validation-failed-exception.php 0000755 00000000532 15111667160 0020776 0 ustar 00 <?php namespace ElementorPro\Core\Integrations\Exceptions; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Action_Validation_Failed_Exception extends Exception_Base { protected function format_message( $message ) { return sprintf( 'Action `%s` failed validation: %s', $this->action, $message ); } } integrations/exceptions/exception-base.php 0000755 00000002471 15111667160 0015065 0 ustar 00 <?php namespace ElementorPro\Core\Integrations\Exceptions; use ElementorPro\Plugin; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } abstract class Exception_Base extends \Exception { /** * @var string */ protected $action; /** * @var array */ protected $meta = []; /** * Get a formatted message specific to the current exception type. * * @param string $message * * @return string */ abstract protected function format_message( $message ); /** * Exception_Base constructor. * * @param string $action - Action name that failed (ideally the class name, e.g. Email::class). * @param string $message - Message to show. * @param array $meta - Exception meta data. Used for logging. * */ public function __construct( $action, $message = '', $meta = [] ) { $this->action = $action; $this->meta = $meta; $message = $this->format_message( $message ); parent::__construct( $message ); } /** * Log the exception to Elementor's log. * * @return void */ public function log() { Plugin::elementor()->logger->get_logger()->error( $this->getMessage(), [ 'meta' => $this->meta ] ); } /** * Get the error format. * * @return string */ public function __toString() { return sprintf( '%s: %s', __CLASS__, $this->getMessage() ); } } integrations/exceptions/action-failed-exception.php 0000755 00000000513 15111667160 0016645 0 ustar 00 <?php namespace ElementorPro\Core\Integrations\Exceptions; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Action_Failed_Exception extends Exception_Base { protected function format_message( $message ) { return sprintf( 'Action `%s` failed to run: %s', $this->action, $message ); } } integrations/integrations-manager.php 0000755 00000005343 15111667160 0014115 0 ustar 00 <?php namespace ElementorPro\Core\Integrations; use ElementorPro\Core\Integrations\Actions\Action_Base; use ElementorPro\Core\Integrations\Actions\Email\Email; use ElementorPro\Core\Integrations\Actions\Email\Email_Message; use ElementorPro\Core\Integrations\Exceptions\Action_Failed_Exception; use ElementorPro\Core\Integrations\Exceptions\Action_Validation_Failed_Exception; use ElementorPro\Core\Utils\Registrar; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Integrations_Manager { /** * Registered action types. * * @var Registrar */ protected $actions_registrar; /** * Integrations_Manager constructor. * * @return void */ public function __construct() { $this->actions_registrar = new Registrar(); } /** * Get an action instance. * * @shortcut `Registrar->get()`. * * @return \ElementorPro\Core\Integrations\Actions\Action_Base|null */ public function get_action( $id ) { if ( ! $this->is_initialized() ) { $this->init_actions(); } return $this->actions_registrar->get( $id ); } /** * Run an action for a selected payload. * * @param array|mixed $payloads - Payloads instances to run the actions on. * @param null|string $id - If `$payloads` is not an array, a custom action ID can be provided. * * @return void */ public function run( $payloads, $id = null ) { if ( ! is_array( $payloads ) ) { $payloads = $id ? [ $id => $payloads ] : [ $payloads ]; } foreach ( $payloads as $key => $payload ) { // Get the action ID for the provided payload type. $action_id = is_numeric( $key ) ? get_class( $payload ) : $key; /** * @type Action_Base $action */ $action = $this->get_action( $action_id ); if ( ! $action ) { throw new \Exception( "{$action_id} doesn't have an associated `Action`." ); } if ( ! ( $action instanceof Action_Base ) ) { $action_class = get_class( $action ); throw new \Exception( "{$action_class} is not a valid `Action_Base`." ); } try { $action->run( $payload ); } catch ( Action_Validation_Failed_Exception $e ) { $e->log(); } catch ( Action_Failed_Exception $e ) { $e->log(); } } } /** * Initialize the manager actions. * * @return void */ protected function init_actions() { add_action( 'elementor_pro/core/integrations/actions/register', function ( Registrar $actions_registrar ) { $actions_registrar->register( new Email(), Email_Message::class ); } ); do_action( 'elementor_pro/core/integrations/actions/register', $this->actions_registrar ); } /** * Determine if the manager is initialized. * * @return boolean */ protected function is_initialized() { return ! ! did_action( 'elementor_pro/core/integrations/actions/register' ); } } integrations/actions/action-base.php 0000755 00000002012 15111667160 0013612 0 ustar 00 <?php namespace ElementorPro\Core\Integrations\Actions; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } abstract class Action_Base { /** * Validate a payload. * * @param mixed $payload - Payload object instance. * * @throws \ElementorPro\Core\Integrations\Exceptions\Action_Validation_Failed_Exception * * @return mixed */ abstract public function validate( $payload ); /** * Apply the action. * * @param mixed $payload - Payload object instance. * * @throws \ElementorPro\Core\Integrations\Exceptions\Action_Failed_Exception * * @return void */ abstract public function apply( $payload ); /** * Run the action. * * @param mixed $payload - Payload object instance. * * @throws \ElementorPro\Core\Integrations\Exceptions\Action_Validation_Failed_Exception * @throws \ElementorPro\Core\Integrations\Exceptions\Action_Failed_Exception * * @return void */ public function run( $payload ) { $this->validate( $payload ); $this->apply( $payload ); } } integrations/actions/email/email-message.php 0000755 00000010015 15111667160 0015227 0 ustar 00 <?php namespace ElementorPro\Core\Integrations\Actions\Email; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Email_Message { /** * Email sender. * * @var Email_Address */ public $from; /** * Email recipient. * * @var Email_Address */ public $to; /** * Email reply to address. * * @var Email_Address[] */ public $reply_to = []; /** * Email CC recipient. * * @var Email_Address[] */ public $cc = []; /** * Email BCC recipient. * * @var Email_Address[] */ public $bcc = []; /** * Email subject. * * @var string */ public $subject; /** * Email content type. * * @var string */ public $content_type; /** * Email body. * * @var string */ public $body; /** * Email attachments. * * @var array */ public $attachments = []; /** * Email_Message constructor. * * @return void */ public function __construct() { // Set defaults. $this->from( get_bloginfo( 'admin_email' ), get_bloginfo( 'name' ) ); } /** * Set the email sender. * * @param string $email * @param string|null $name * * @return $this */ public function from( $email, $name = null ) { $this->from = new Email_Address( $email, $name ); return $this; } /** * Set the email recipient. * * @param string $email * @param string|null $name * * @return $this */ public function to( $email, $name = null ) { $this->to = new Email_Address( $email, $name ); return $this; } /** * Add a reply to. * * @param string $email * @param string|null $name * * @return $this */ public function reply_to( $email, $name = null ) { $this->reply_to[] = new Email_Address( $email, $name ); return $this; } /** * Add a CC. * * @param string $email * @param string|null $name * * @return $this */ public function cc( $email, $name = null ) { $this->cc[] = new Email_Address( $email, $name ); return $this; } /** * Add a BCC. * * @param string $email * @param string|null $name * * @return $this */ public function bcc( $email, $name = null ) { $this->bcc[] = new Email_Address( $email, $name ); return $this; } /** * Set the email subject. * * @param string $subject * * @return $this */ public function subject( $subject ) { $this->subject = (string) $subject; return $this; } /** * Set the email content type. * * @param string $content_type * * @return $this */ public function content_type( $content_type ) { $this->content_type = (string) $content_type; return $this; } /** * Set the email body using plain text. * * @param string $body * @param string $content_type * * @return $this */ public function body( $body, $content_type = 'text/html' ) { $this->body = (string) $body; return $this->content_type( $content_type ); } /** * Set the email body using a view. * * @param string $path - View path, * @param array $data - Data that will be passes to the view. * * @return $this * @throws \Exception */ public function view( $path, $data = [] ) { if ( ! is_file( $path ) ) { throw new \Exception( "`{$path}` is not a valid view." ); } ob_start(); // Inspired from Laravel's view mechanism: // [1] https://github.dev/illuminate/filesystem/blob/b179f9ea3b3195d1f4b5ae2aee67e42eac6ceb5e/Filesystem.php#L98 // [2] https://github.dev/illuminate/view/blob/6dd315634a44450c5e443fa8735d4a526833fad3/Engines/PhpEngine.php#L48 call_user_func( function( $__view_path, $__view_data ) { extract( $__view_data, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract unset( $__view_data ); // `$__view_data` keys are available in the file as variables. require $__view_path; }, $path, $data ); $this->body = ob_get_clean(); return $this->content_type( 'text/html' ); } /** * Add an attachment. * * @param string $path - Attachment path on the server. * * @return $this */ public function attach( $path ) { $this->attachments[] = (string) $path; return $this; } } integrations/actions/email/email-address.php 0000755 00000001526 15111667160 0015237 0 ustar 00 <?php namespace ElementorPro\Core\Integrations\Actions\Email; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Email_Address { /** * Recipient email address. * * @var array */ public $address; /** * Recipient name. * * @var string */ public $name; /** * Email_Address constructor. * * @param string $address * @param string $name * * @return void */ public function __construct( $address, $name ) { $this->address = (string) $address; $this->name = (string) $name; } /** * Format an email to be ready for header (e.g. `Recipient Name <user@email.com>` or `user@email.com`) * * @return string */ public function format() { if ( ! empty( $this->name ) ) { return sprintf( '%s <%s>', $this->name, $this->address ); } return sprintf( '%s', $this->address ); } } integrations/actions/email/email.php 0000755 00000005321 15111667160 0013611 0 ustar 00 <?php namespace ElementorPro\Core\Integrations\Actions\Email; use ElementorPro\Core\Integrations\Actions\Action_Base; use ElementorPro\Core\Integrations\Exceptions\Action_Failed_Exception; use ElementorPro\Core\Integrations\Exceptions\Action_Validation_Failed_Exception; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Email extends Action_Base { /** * @param Email_Message $payload * * @return void * @throws \Exception */ public function apply( $payload ) { // Set default headers. $headers = [ sprintf( 'Content-Type: %s; charset=UTF-8', $payload->content_type ), sprintf( 'From: %s', $payload->from->format() ), ]; foreach ( $payload->reply_to as $recipient ) { $headers[] = sprintf( 'Reply-To: %s', $recipient->format() ); } // Set CC headers. $cc_headers = []; foreach ( $payload->cc as $recipient ) { $cc_headers[] = sprintf( 'Cc: %s', $recipient->format() ); } // Send email. $this->send_mail( $payload->to->format(), $payload->subject, $payload->body, implode( PHP_EOL, array_merge( $headers, $cc_headers ) ), $payload->attachments ); // Send BCC emails. foreach ( $payload->bcc as $bcc ) { $this->send_mail( $bcc->format(), $payload->subject, $payload->body, implode( PHP_EOL, $headers ), $payload->attachments ); } } /** * @alias `$this->run()` * * @param Email_Message $payload * * @return void *@throws \Exception * */ public function send( Email_Message $payload ) { $this->run( $payload ); } /** * Validate the email message DTO. * * @param Email_Message $payload * * @throws \ElementorPro\Core\Integrations\Exceptions\Action_Validation_Failed_Exception * * @return void */ public function validate( $payload ) { $required_fields = [ 'from', 'to', 'subject', 'body', 'content_type', ]; foreach ( $required_fields as $field ) { if ( empty( $payload->{$field} ) ) { throw new Action_Validation_Failed_Exception( static::class, "`Email_Message::\${$field}` is required." ); } } } /** * Calls `wp_mail()`. Used for testing. * * @param mixed ...$args * * @return void */ protected function send_mail( ...$args ) { add_action( 'wp_mail_failed', [ $this, 'on_wp_mail_error' ] ); wp_mail( ...$args ); remove_action( 'wp_mail_failed', [ $this, 'on_wp_mail_error' ] ); } /** * Throw exception on `wp_mail()` error. * * @param \WP_Error $error * * @throws \ElementorPro\Core\Integrations\Exceptions\Action_Failed_Exception * * @return void */ public function on_wp_mail_error( \WP_Error $error ) { throw new Action_Failed_Exception( static::class, '`wp_mail()` cannot send email', $error ); } } preview/preview.php 0000755 00000001147 15111667160 0010431 0 ustar 00 <?php namespace ElementorPro\Core\Preview; use Elementor\Core\Base\App; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Preview extends App { public function __construct() { add_action( 'elementor/preview/enqueue_styles', [ $this, 'enqueue_styles' ] ); } public function get_name() { return 'pro-preview'; } public function enqueue_styles() { wp_enqueue_style( 'pro-editor-preview', $this->get_css_assets_url( 'preview', null, 'default', true ), [], ELEMENTOR_PRO_VERSION ); } protected function get_assets_base_url() { return ELEMENTOR_PRO_URL; } } database/base-migration.php 0000755 00000010705 15111667160 0011734 0 ustar 00 <?php namespace ElementorPro\Core\Database; use Elementor\Core\Utils\Collection; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } abstract class Base_Migration { /* * @see https://github.com/WordPress/WordPress/blob/d2694aa46647af48d1bcaff48a4f6cac7f5cf470/wp-admin/includes/schema.php#L49 */ const MAX_INDEX_LENGTH = 191; /** * @var \wpdb */ protected $wpdb; /** * @param \wpdb|null $wpdb_instance */ public function __construct( \wpdb $wpdb_instance = null ) { if ( ! $wpdb_instance ) { global $wpdb; $this->wpdb = $wpdb; } else { $this->wpdb = $wpdb_instance; } } /** * Runs when upgrading the database * * @return void */ abstract public function up(); /** * Runs when downgrading the database. * * @return void */ abstract public function down(); /** * A util to run SQL for creating tables. * * @param $table_name * @param array $columns */ protected function create_table( $table_name, array $columns ) { $table_name = "{$this->wpdb->prefix}{$table_name}"; $columns_sql = ( new Collection( $columns ) ) ->map( function( $definition, $col_name ) { return "`{$col_name}` {$definition}"; } ) ->implode( ', ' ); $query = "CREATE TABLE `{$table_name}` ({$columns_sql}) {$this->wpdb->get_charset_collate()};"; $this->run_db_delta( $query ); } /** * Add columns. * * @param $table_name * @param array $columns */ protected function add_columns( $table_name, array $columns ) { $table_name = "{$this->wpdb->prefix}{$table_name}"; $add_columns_sql = ( new Collection( $columns ) ) ->map( function ( $definition, $column_name ) { return "ADD COLUMN `{$column_name}` {$definition}"; } ) ->implode( ', ' ); $this->wpdb->query( "ALTER TABLE `{$table_name}` {$add_columns_sql};" ); // phpcs:ignore } /** * Drop columns * * @param $table_name * @param array $columns */ protected function drop_columns( $table_name, array $columns ) { $table_name = "{$this->wpdb->prefix}{$table_name}"; $drop_columns_sql = ( new Collection( $columns ) ) ->map( function ( $column_name ) { return "DROP COLUMN `{$column_name}`"; } ) ->implode( ', ' ); $this->wpdb->query( "ALTER TABLE `{$table_name}` {$drop_columns_sql};" ); // phpcs:ignore } /** * A util to run SQL for dropping tables. * * @param $table_name */ protected function drop_table( $table_name ) { $table_name = "{$this->wpdb->prefix}{$table_name}"; $query = "DROP TABLE IF EXISTS `{$table_name}`;"; // Safe query that shouldn't be escaped. $this->wpdb->query( $query ); // phpcs:ignore } /** * A util to run SQL for creating indexes. * * @param $table_name * @param array $column_names */ protected function create_indexes( $table_name, array $column_names ) { $max_index_length = static::MAX_INDEX_LENGTH; $table_name = "{$this->wpdb->prefix}{$table_name}"; // Safe query that shouldn't be escaped. $column_definition = $this->get_column_definition( $table_name ); if ( ! $column_definition ) { return; } $should_set_max_length_index = ( new Collection( $column_definition ) ) ->filter( function ( $value ) { preg_match( '/\((\d+)\)/', $value['Type'], $match ); return ( isset( $match[1] ) && intval( $match[1] ) > Base_Migration::MAX_INDEX_LENGTH ) || in_array( strtolower( $value['Type'] ), [ 'text', 'longtext' ], true ); } ) ->pluck( 'Field' ) ->values(); $indexes_sql = ( new Collection( $column_names ) ) ->map( function( $col_name ) use ( $should_set_max_length_index, $max_index_length ) { $max_index_length_sql = ''; if ( in_array( $col_name, $should_set_max_length_index, true ) ) { $max_index_length_sql = " ({$max_index_length})"; } return "ADD INDEX `{$col_name}_index` (`{$col_name}`{$max_index_length_sql})"; } ) ->implode( ', ' ); // Safe query that shouldn't be escaped. $this->wpdb->query( "ALTER TABLE `{$table_name}` {$indexes_sql};" ); // phpcs:ignore } /** * @param $table_name * * @return array */ protected function get_column_definition( $table_name ) { return $this->wpdb->get_results( "SHOW COLUMNS FROM `{$table_name}`;", ARRAY_A ); // phpcs:ignore } /** * Runs global dbDelta function (wrapped into method to allowing mock for testing). * * @param $query * * @return array */ protected function run_db_delta( $query ) { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; return dbDelta( $query ); } } database/model-base.php 0000755 00000006757 15111667160 0011057 0 ustar 00 <?php namespace ElementorPro\Core\Database; use ElementorPro\Core\Utils\Collection; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } abstract class Model_Base implements \JsonSerializable { // Casting types. const TYPE_BOOLEAN = 'boolean'; const TYPE_COLLECTION = 'collection'; const TYPE_INTEGER = 'integer'; const TYPE_STRING = 'string'; const TYPE_JSON = 'json'; const TYPE_DATETIME = 'datetime'; const TYPE_DATETIME_GMT = 'datetime_gmt'; /** * Casts array. * Used to automatically cast values from DB to the appropriate property type. * * @var array */ protected static $casts = []; /** * Model_Base constructor. * * @param array $fields - Fields from the DB to fill. * * @return void */ public function __construct( array $fields ) { foreach ( $fields as $key => $value ) { if ( ! property_exists( $this, $key ) ) { continue; } $this->{$key} = ( empty( static::$casts[ $key ] ) ) ? $value : static::cast( $value, static::$casts[ $key ] ); } } /** * Get the model's table name. * Throws an exception by default in order to require implementation, * since abstract static functions are not allowed. * * @return string */ public static function get_table() { throw new \Exception( 'You must implement `get_table()` inside ' . static::class ); } /** * Create a Query Builder for the model's table. * * @param \wpdb|null $connection - MySQL connection to use. * * @return Query_Builder */ public static function query( \wpdb $connection = null ) { $builder = new Model_Query_Builder( static::class, $connection ); return $builder->from( static::get_table() ); } /** * Cast value into specific type. * * @param $value - Value to cast. * @param $type - Type to cast into. * * @return mixed */ protected static function cast( $value, $type ) { if ( null === $value ) { return null; } switch ( $type ) { case self::TYPE_BOOLEAN: return boolval( $value ); case self::TYPE_COLLECTION: return new Collection( $value ); case self::TYPE_INTEGER: return intval( $value ); case self::TYPE_STRING: return strval( $value ); case self::TYPE_JSON: return json_decode( $value, true ); case self::TYPE_DATETIME: return new \DateTime( $value ); case self::TYPE_DATETIME_GMT: return new \DateTime( $value, new \DateTimeZone( 'GMT' ) ); } return $value; } /** * Cast a model property value into a JSON compatible data type. * * @param $value - Value to cast. * @param $type - Type to cast into. * @param $property_name - The model property name. * * @return mixed */ protected static function json_serialize_property( $value, $type, $property_name ) { switch ( $type ) { case self::TYPE_DATETIME: case self::TYPE_DATETIME_GMT: /** @var \DateTime $value */ return $value->format( 'c' ); } /** @var mixed $value */ return $value; } /** * @return array */ #[\ReturnTypeWillChange] public function jsonSerialize() { return ( new Collection( (array) $this ) ) ->map( function ( $_, $key ) { $value = $this->{$key}; $type = array_key_exists( $key, static::$casts ) ? static::$casts[ $key ] : null; if ( null === $value ) { return $value; } // Can be overridden by child model. $value = static::json_serialize_property( $value, $type, $key ); if ( $value instanceof \JsonSerializable ) { return $value->jsonSerialize(); } return $value; } ) ->all(); } } database/model-query-builder.php 0000755 00000003613 15111667160 0012722 0 ustar 00 <?php namespace ElementorPro\Core\Database; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Model_Query_Builder extends Query_Builder { /** * The Query Builder associated model. * * @var string */ public $model; /** * Whether the returned value should be hydrated into a model. * * @var bool */ public $return_as_model = true; /** * Model_Query_Builder constructor. * * @param string $model_classname - Model to use inside the builder. * @param \wpdb|null $connection - MySQL connection. */ public function __construct( $model_classname, \wpdb $connection = null ) { $this->set_model( $model_classname ); parent::__construct( $connection ); } /** * Set the model the generated from the query builder. * * @param $model_classname * * @return $this */ public function set_model( $model_classname ) { $this->model = $model_classname; return $this; } /** * Disable model hydration. * * @return $this */ public function disable_model_initiation() { $this->return_as_model = false; return $this; } /** * Disable hydration before calling the original count. * * @param string $column * * @return int */ public function count( $column = '*' ) { $this->disable_model_initiation(); return parent::count( $column ); } /** * Disable hydration before calling the original pluck. * * @inheritDoc */ public function pluck( $column = null ) { $this->disable_model_initiation(); return parent::pluck( $column ); } /** * Override the parent `get()` and make Models from the results. * * @return \ElementorPro\Core\Utils\Collection */ public function get() { $items = parent::get(); if ( ! $this->return_as_model ) { return $items; } // Convert the SQL results to Model instances. return $items->map( function ( $comment ) { return new $this->model( $comment ); } ); } } database/base-database-updater.php 0000755 00000006203 15111667160 0013147 0 ustar 00 <?php namespace ElementorPro\Core\Database; use ElementorPro\Core\Utils\Collection; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } abstract class Base_Database_Updater { /** * Run all the 'up' method of the migrations classes if needed, and update the db version. * * @param bool $force When passing true, it ignores the current version and run all the up migrations. */ public function up( $force = false ) { $installed_version = $this->get_installed_version(); // Up to date. Nothing to do. if ( ! $force && $this->get_db_version() <= $installed_version ) { return; } $migrations = $this->get_collected_migrations(); if ( ! $force ) { $migrations = $migrations->filter( function ( $_, $version ) use ( $installed_version ) { // Filter all the migrations that already done. return $version > $installed_version; } ); } $migrations->map( function ( Base_Migration $migration, $version ) { $migration->up(); // In case some migration failed it updates version every migration. $this->update_db_version_option( $version ); } ); $this->update_db_version_option( $this->get_db_version() ); } /** * Run all the 'down' method of the migrations classes if can, and update the db version. * * @param bool $force When passing true, it ignores the current version and run all the down migrations. */ public function down( $force = false ) { $installed_version = $this->get_installed_version(); $migrations = $this->get_collected_migrations(); if ( ! $force ) { $migrations = $migrations->filter( function ( $_, $version ) use ( $installed_version ) { // Filter all the migrations that was not installed. return $version <= $installed_version; } ); } $migrations->reverse( true ) ->map( function ( Base_Migration $migration, $version ) { $migration->down(); // In case some migration failed it updates version every migration. $this->update_db_version_option( $version ); } ); $this->update_db_version_option( 0 ); } /** * Register hooks to activate the migrations. */ public function register() { add_action( 'admin_init', function () { $this->up(); } ); } /** * Update the version in the users DB. * * @param $version */ protected function update_db_version_option( $version ) { update_option( $this->get_db_version_option_name(), $version ); } /** * Get the version that already installed. * * @return int */ protected function get_installed_version() { return intval( get_option( $this->get_db_version_option_name() ) ); } /** * Get all migrations inside a Collection. * * @return Collection */ protected function get_collected_migrations() { return new Collection( $this->get_migrations() ); } /** * The most updated version of the DB. * * @return numeric */ abstract protected function get_db_version(); /** * The name of the option that saves the current user DB version. * * @return string */ abstract protected function get_db_version_option_name(); /** * Array of migration classes. * * @return Base_Migration[] */ abstract protected function get_migrations(); } database/query-builder.php 0000755 00000074042 15111667160 0011630 0 ustar 00 <?php namespace ElementorPro\Core\Database; use ElementorPro\Core\Utils\Collection; use InvalidArgumentException; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } class Query_Builder { // Relation types. const RELATION_AND = 'AND'; const RELATION_OR = 'OR'; // Column types. const COLUMN_BASIC = 'basic'; // Regular column - will be automatically escaped. const COLUMN_RAW = 'raw'; // Raw column - SHOULD BE ESCAPED BY THE DEVELOPER. const COLUMN_SUB_SELECT = 'sub-select'; // Sub select - will be automatically bind & escaped. const COLUMN_COUNT = 'count'; // Count - wrap the column with a COUNT function. // WHERE types. const WHERE_BASIC = 'basic'; const WHERE_NULL = 'null'; const WHERE_COLUMN = 'column'; const WHERE_IN = 'in'; const WHERE_NOT_IN = 'not-in'; const WHERE_SUB = 'sub'; const WHERE_NESTED = 'nested'; const WHERE_EXISTS = 'exists'; const WHERE_NOT_EXISTS = 'not-exists'; // HAVING types. const HAVING_RAW = 'raw'; /** * MySQL connection. * * @var \wpdb */ protected $connection; /** * Current query value binding. * * @var array[] */ protected $bindings = [ 'select' => [], 'join' => [], 'where' => [], ]; /** * Current query columns to return. * * @var array */ protected $columns = [ [ 'type' => self::COLUMN_RAW, 'column' => '*', 'as' => null, ], ]; /** * Table to select from. * * @var array */ protected $from = []; /** * Current query joins. * * @var array */ protected $joins = []; /** * The where constraints for the query. * * @var array */ protected $wheres = []; /** * The having constraints for the query. * * @var array */ protected $havings = []; /** * The groupings for the query. * * @var array */ protected $groups = []; /** * The orderings for the query. * * @var array */ protected $orders = []; /** * The maximum number of records to return. * * @var int */ protected $limit; /** * The number of records to skip. * * @var int */ protected $offset; /** * Aggregations. * * @var array */ protected $with = []; /** * Query_Builder constructor. * * @param \wpdb|null $connection - The Mysql connection instance to use. */ public function __construct( \wpdb $connection = null ) { if ( $connection ) { $this->connection = $connection; return; } global $wpdb; $this->connection = $wpdb; } /** * Add columns to the SELECT clause. * * @param string[] $columns - Array of column names. * @param string $type - Select type. * * @return $this */ public function select( $columns = [ '*' ], $type = self::COLUMN_BASIC ) { $this->columns = []; $this->bindings['select'] = []; foreach ( $columns as $as => $column ) { $this->columns[ $as ] = [ 'type' => $type, 'as' => is_string( $as ) ? $as : null, 'column' => $column, ]; } return $this; } /** * @shortcut `$this->select()`. */ public function select_raw( $raw_columns = [ '*' ] ) { return $this->select( $raw_columns, self::COLUMN_RAW ); } /** * Add a `(SELECT ...) AS alias` statement to the SELECT clause. * * @param callable $callback - Callback that gets a `Query_Builder` and modifies it. * @param string $as - Alias for the sub select. * * @return $this */ public function add_sub_select( callable $callback, $as ) { call_user_func( $callback, $query = $this->new_query() ); $this->add_binding( $query->get_bindings(), 'select' ); $this->columns[] = [ 'type' => self::COLUMN_SUB_SELECT, 'column' => $query->to_sql(), 'as' => $as, ]; return $this; } /** * Add a `COUNT({col}) AS {alias}` statement to the SELECT clause. * * @param $column_name * @param $as * * @return $this */ public function add_count_select( $column_name, $as = null ) { $this->columns[] = [ 'type' => self::COLUMN_COUNT, 'column' => $column_name, 'as' => $as, ]; return $this; } /** * Set the table to select from. * * @param string $table - Table name. * @param string|null $as - Table alias. * * @return $this */ public function from( $table, $as = null ) { // Default the alias to the table name without prefix. $as = $as ? $as : $table; // Get the prefixed table name from the connection. $table = $this->connection->$table; $this->from = [ 'table' => $table, 'as' => $as, ]; return $this; } /** * @shortcut $this->from() * * Used for readability with UPDATE / INSERT / DELETE statements. */ public function table( $table, $as = null ) { return $this->from( $table, $as ); } /** * Execute a query operation only on specific condition. * For example: * * $query->when( 1 === $a, function( Query_Builder $builder ) { * // Runs if $a = 1. * $builder->where( ... ); * }, function( Query_Builder $builder ) { * // Runs if $a != 1. * $builder->where( ... ); * } ) * * @param mixed $condition - Condition to check. * @param callable $true_callback - Callback if the condition is truthy. * @param callable|null $false_callback - Callback if the condition is falsy. Optional. * * @return $this */ public function when( $condition, callable $true_callback, callable $false_callback = null ) { if ( $condition ) { call_user_func( $true_callback, $this, $condition ); } elseif ( $false_callback instanceof \Closure ) { call_user_func( $false_callback, $this, $condition ); } return $this; } /** * Add a `WHERE` statement. * * @param string|callable $column - Column name to check. * @param string $operator - Statement operator. * @param string|callable $value - Value as string or callback. * @param string $and_or - Boolean relation, one of `and` / `or`. * * @return $this */ public function where( $column, $operator = null, $value = null, $and_or = self::RELATION_AND ) { // `$column` is a function, create a nested where. if ( $column instanceof \Closure ) { return $this->where_nested( $column, $and_or ); } // `$value` is a function, create a sub select. if ( $value instanceof \Closure ) { return $this->where_sub( $column, $operator, $value, $and_or ); } // Validate relation. if ( ! in_array( strtoupper( $and_or ), [ self::RELATION_AND, self::RELATION_OR ], true ) ) { throw new InvalidArgumentException( 'Relation must be "and" or "or".' ); } // If it's a `LIKE` statement, escape it using WP's `esc_like`. if ( 'like' === strtolower( $operator ) ) { $value = $this->escape_like( $value ); } // Create an `IS NULL` statement if the `$value` is null. if ( null === $value ) { $type = self::WHERE_NULL; } else { $this->add_binding( $value, 'where' ); $type = self::WHERE_BASIC; } $this->wheres[] = [ 'type' => $type, 'column' => $column, 'operator' => $operator, 'value' => $value, 'and_or' => $and_or, ]; return $this; } /** * Add an `OR WHERE` statement. * * @shortcut $this->where(). */ public function or_where( $column, $operator = null, $value = null ) { return $this->where( $column, $operator, $value, self::RELATION_OR ); } /** * @shortcut `$this->where()`. */ public function where_null( $column, $and_or = self::RELATION_AND ) { return $this->where( $column, '=', null ); } /** * @shortcut `$this->where_null()`. */ public function or_where_null( $column ) { return $this->where_null( $column, self::RELATION_OR ); } /** * Add a `WHERE col1 = col2` statement. * * @param string $first - First column name to check. * @param string $operator - Statement operator. * @param string $second - Second column name to check. * @param string $and_or - Boolean relation, one of `and` / `or`. * * @return $this */ public function where_column( $first, $operator, $second, $and_or = self::RELATION_AND ) { // Validate relation. if ( ! in_array( strtoupper( $and_or ), [ self::RELATION_AND, self::RELATION_OR ], true ) ) { throw new InvalidArgumentException( 'Relation must be "and" or "or".' ); } $this->wheres[] = [ 'type' => self::WHERE_COLUMN, 'first' => $first, 'second' => $second, 'operator' => $operator, 'and_or' => $and_or, ]; return $this; } /** * Add an `OR WHERE col1 = col2` statement. * * @shortcut $this->where_column(). */ public function or_where_column( $first, $operator, $second ) { return $this->where_column( $first, $operator, $second, self::RELATION_OR ); } /** * Add a `WHERE IN()` statement. * * @param string $column - Column name to check. * @param string[]|callable $values - Array of values. * @param string $and_or - Boolean relation, one of `and` / `or`. * @param boolean $in - Whether it's `IN` or `NOT IN`. * * @return $this */ public function where_in( $column, $values, $and_or = self::RELATION_AND, $in = true ) { $type = $in ? self::WHERE_IN : self::WHERE_NOT_IN; // Support `WHERE IN ( SELECT ... FROM )`. if ( $values instanceof \Closure ) { $operator = $in ? 'IN' : 'NOT IN'; return $this->where( $column, $operator, $values ); } $this->wheres[] = [ 'type' => $type, 'column' => $column, 'value' => $values, 'and_or' => $and_or, ]; $this->add_binding( $values, 'where' ); return $this; } /** * Add an `OR WHERE IN()` statement. * * @shortcut $this->where_in(). */ public function or_where_in( $column, $values ) { return $this->where_in( $column, $values, self::RELATION_OR ); } /** * Add a `WHERE NOT IN()` statement. * * @shortcut $this->where_in(). */ public function where_not_in( $column, $values, $and_or = self::RELATION_AND ) { return $this->where_in( $column, $values, $and_or, false ); } /** * Add an `OR WHERE NOT IN()` statement. * * @shortcut $this->where_in(). */ public function or_where_not_in( $column, $values ) { return $this->where_not_in( $column, $values, self::RELATION_OR ); } /** * Add a `WHERE EXISTS()` statement. * * @param callable $callback - Callback that gets a `Query_Builder` and modifies it. * @param string $and_or - Boolean relation, one of `and` / `or`. * @param bool $exists - Whether to use `EXISTS` or `NOT EXISTS` statement. * * @return $this */ public function where_exists( callable $callback, $and_or = self::RELATION_AND, $exists = true ) { call_user_func( $callback, $query = $this->new_query() ); $type = $exists ? self::WHERE_EXISTS : self::WHERE_NOT_EXISTS; $this->wheres[] = [ 'type' => $type, 'query' => $query, 'and_or' => $and_or, ]; $this->add_binding( $query->get_bindings(), 'where' ); return $this; } /** * Add an `OR WHERE EXISTS()` statement. * * @shortcut $this->where_exists(). */ public function or_where_exists( callable $callback, $exists = true ) { return $this->where_exists( $callback, self::RELATION_OR, $exists ); } /** * Add a `WHERE NOT EXISTS()` statement. * * @shortcut $this->where_exists(). */ public function where_not_exists( callable $callback, $and_or = self::RELATION_AND ) { return $this->where_exists( $callback, $and_or, false ); } /** * Add an `OR WHERE NOT EXISTS()` statement. * * @shortcut $this->where_exists(). */ public function or_where_not_exists( callable $callback ) { return $this->or_where_exists( $callback, false ); } /** * Add a sub query. * * @param string $column - Column name to check. * @param string $operator - Statement operator. * @param callable $callback - Callback that gets a `Query_Builder` and modifies it. * @param string $and_or - Boolean relation, one of `and` / `or`. * * @return $this */ public function where_sub( $column, $operator, callable $callback, $and_or = self::RELATION_AND ) { call_user_func( $callback, $query = $this->new_query() ); $this->wheres[] = [ 'type' => self::WHERE_SUB, 'column' => $column, 'operator' => $operator, 'query' => $query, 'and_or' => $and_or, ]; $this->add_binding( $query->get_bindings(), 'where' ); return $this; } /** * Add a nested `WHERE` query. * * @param callable $callback - Callback that gets a `Query_Builder` and modifies it. * @param string $and_or - Boolean relation, one of `and` / `or`. * * @return $this */ public function where_nested( callable $callback, $and_or = self::RELATION_AND ) { call_user_func( $callback, $query = $this->new_query() ); $this->wheres[] = [ 'type' => self::WHERE_NESTED, 'query' => $query, 'and_or' => $and_or, ]; $this->add_binding( $query->get_bindings( 'where' ), 'where' ); return $this; } /** * Add `HAVING` statement. * * @param string $sql - RAW SQL having clause. * @param string $and_or - Boolean relation, one of `and` / `or`. * * @return $this */ public function having_raw( $sql, $and_or = self::RELATION_AND ) { $this->havings[] = [ 'type' => self::HAVING_RAW, 'and_or' => $and_or, 'sql' => $sql, ]; return $this; } /** * Add `OR HAVING` statement. * * @param string $sql - RAW SQL having clause. * * @return $this */ public function or_having_raw( $sql ) { return $this->having_raw( $sql, self::RELATION_OR ); } /** * Add a `JOIN ... ON` statement. * * @param callable $callback - Closure that builds the JOIN clause. * @param string $type - JOIN type. * * @return $this */ public function join( callable $callback, $type = Join_Clause::TYPE_INNER ) { // Validate type. if ( ! in_array( strtolower( $type ), [ Join_Clause::TYPE_INNER, Join_Clause::TYPE_LEFT, Join_Clause::TYPE_RIGHT ], true ) ) { throw new InvalidArgumentException( 'Join type must be "inner", "left" or "right".' ); } call_user_func( $callback, $join = $this->new_join_clause( $type ) ); $this->add_binding( $join->get_bindings(), 'join' ); $this->joins[] = $join; return $this; } /** * @shortcut `$this->join()` */ public function left_join( callable $callback ) { return $this->join( $callback, Join_Clause::TYPE_LEFT ); } /** * @shortcut `$this->join()` */ public function right_join( callable $callback ) { return $this->join( $callback, Join_Clause::TYPE_RIGHT ); } /** * Creates a new Query Builder instance using the same connection as the initiator. * * @return self */ public function new_query() { // Make sure this is `new self` and not `new static`. // When extending the Query Builder, sometimes it comes with default table or queries. // For that reason it should be avoided passing those defaults to `nested` or `sub-queries`. return new self( $this->connection ); } /** * Creates a new Join Clause instance using the same connection as the initiator. * * @param string $type - JOIN type. * * @return Join_Clause */ public function new_join_clause( $type ) { return new Join_Clause( $type, $this->connection ); } /** * Limit the returned results. * Adds a `LIMIT` statement. * * @param int $limit - Max count of results to return. * * @return $this */ public function limit( $limit ) { $this->limit = (int) $limit; return $this; } /** * Add and `OFFSET` statement. * * @param int $offset - Count of results to skip. * * @return $this */ public function offset( $offset ) { $this->offset = (int) $offset; return $this; } /** * Adds an `ORDER BY` statement. * NOTE: `$column` IS NOT ESCAPED & SHOULD BE WHITELISTED! * * @param string $column - Column to order by. * @param string $direction - Direction (`asc` / `desc`). * * @return $this */ public function order_by( $column, $direction = 'asc' ) { if ( ! in_array( strtolower( $direction ), [ 'asc', 'desc' ], true ) ) { throw new InvalidArgumentException( 'Order direction must be "asc" or "desc".' ); } $this->orders[] = [ 'column' => $column, 'direction' => $direction, ]; return $this; } /** * Adds a `GROUP BY` statement. * NOTE: `$column` IS NOT ESCAPED & SHOULD BE WHITELISTED! * * @param string $column - Column to group by. * * @return $this */ public function group_by( $column ) { $this->groups[] = [ 'column' => $column, ]; return $this; } /** * Get the raw bindings array. * * @return array[] */ public function get_raw_bindings() { return $this->bindings; } /** * Get the columns to use inside the SELECT statement. * Defaults to `*` if non are selected. * * @return string */ public function compile_columns() { if ( 0 === count( $this->columns ) ) { return '*'; }; $columns = []; foreach ( $this->columns as $column ) { switch ( $column['type'] ) { case self::COLUMN_BASIC: $column_name = $this->parse_column( $column['column'] ); $as = $this->parse_as( $column['as'] ); $columns[] = "{$column_name}{$as}"; break; case self::COLUMN_SUB_SELECT: $as = $this->parse_as( $column['as'] ); $columns[] = "( {$column['column']} ){$as}"; break; case self::COLUMN_RAW: $columns[] = $column['column']; break; case self::COLUMN_COUNT: $column_name = $this->parse_column( $column['column'] ); $as = $this->parse_as( $column['as'] ); $columns[] = "COUNT({$column_name}){$as}"; break; } } return $this->concatenate( $columns, ', ' ); } /** * Get the raw columns array. * * @return string[] */ public function get_raw_columns() { return $this->columns; } /** * Compile the `columns` & `from` attributes into an actual `SELECT` statement. * * @return string */ public function compile_select() { return $this->concatenate( [ 'SELECT', $this->compile_columns(), 'FROM', $this->compile_from(), ] ); } /** * Compile the table name and alias. * * @return string */ public function compile_from() { $table = $this->wrap_with_backticks( $this->from['table'] ); $as = $this->parse_as( $this->from['as'] ); return "{$table}{$as}"; } /** * Compile the `joins` array into an actual `JOIN` statement. * * @return string */ public function compile_joins() { $joins = []; foreach ( $this->joins as $join ) { /** * @var Join_Clause $join */ $table = $join->compile_from(); $ons = $join->compile_wheres(); switch ( $join->type ) { case Join_Clause::TYPE_INNER: $joins[] = "INNER JOIN {$table} ON {$ons}"; break; case Join_Clause::TYPE_LEFT: $joins[] = "LEFT JOIN {$table} ON {$ons}"; break; case Join_Clause::TYPE_RIGHT: $joins[] = "RIGHT JOIN {$table} ON {$ons}"; break; } } return $this->concatenate( $joins ); } /** * Compile the `wheres` array into an actual `WHERE` statement. * * @return string */ public function compile_wheres() { $wheres = [ '1 = 1', // A default statement for easier `WHERE` concatenation. ]; foreach ( $this->wheres as $where ) { switch ( $where['type'] ) { case self::WHERE_BASIC: $column = $this->parse_column( $where['column'] ); $binding = $this->get_binding_type( $where['value'] ); $wheres[] = "{$where['and_or']} {$column} {$where['operator']} {$binding}"; break; case self::WHERE_NULL: $column = $this->parse_column( $where['column'] ); $wheres[] = "{$where['and_or']} {$column} IS NULL"; break; case self::WHERE_COLUMN: $first = $this->parse_column( $where['first'] ); $second = $this->parse_column( $where['second'] ); $wheres[] = "{$where['and_or']} {$first} {$where['operator']} {$second}"; break; case self::WHERE_IN: // Handle invalid `WHERE IN` - Force the SQL to fail. if ( empty( $where['value'] ) ) { $wheres[] = "{$where['and_or']} 0 = 1"; break; } $column = $this->parse_column( $where['column'] ); $binding = $this->get_binding_type( $where['value'] ); $wheres[] = "{$where['and_or']} {$column} IN( {$binding} )"; break; case self::WHERE_NOT_IN: // Handle invalid `WHERE IN` - Force the SQL to fail. if ( empty( $where['value'] ) ) { $wheres[] = "{$where['and_or']} 1 = 1"; break; } $column = $this->parse_column( $where['column'] ); $binding = $this->get_binding_type( $where['value'] ); $wheres[] = "{$where['and_or']} {$column} NOT IN( {$binding} )"; break; case self::WHERE_SUB: $column = $this->parse_column( $where['column'] ); $sub_query = $where['query']->to_sql(); $wheres[] = "{$where['and_or']} {$column} {$where['operator']} ( {$sub_query} )"; break; case self::WHERE_NESTED: $nested_query = $where['query']->compile_wheres(); $wheres[] = "{$where['and_or']} ( {$nested_query} )"; break; case self::WHERE_EXISTS: $sub_query = $where['query']->to_sql(); $wheres[] = "{$where['and_or']} EXISTS ( {$sub_query} )"; break; case self::WHERE_NOT_EXISTS: $sub_query = $where['query']->to_sql(); $wheres[] = "{$where['and_or']} NOT EXISTS ( {$sub_query} )"; break; } } return $this->concatenate( $wheres ); } /** * Compile the `havings` array into an actual `HAVING` statement. * TODO: Add more types. * * @return string */ public function compile_having() { if ( 0 === count( $this->havings ) ) { return ''; } $havings = [ 'HAVING', '1 = 1', // A default statement for easier `HAVING` concatenation. ]; foreach ( $this->havings as $having ) { switch ( $having['type'] ) { case self::HAVING_RAW: $havings[] = "{$having['and_or']} {$having['sql']}"; break; } } return $this->concatenate( $havings ); } /** * Compile the `groups` array into an actual `GROUP BY` statement. * * @return string */ public function compile_group_by() { if ( 0 === count( $this->groups ) ) { return ''; } $groups = []; foreach ( $this->groups as $group ) { $groups[] = $this->parse_column( $group['column'] ); } return $this->concatenate( [ 'GROUP BY', $this->concatenate( $groups, ', ' ), ] ); } /** * Compile the `orders` array into an actual `ORDER BY` statement. * * @return string */ public function compile_order_by() { if ( 0 === count( $this->orders ) ) { return ''; } $orders = []; foreach ( $this->orders as $order ) { $column = $this->parse_column( $order['column'] ); $orders[] = "{$column} {$order['direction']}"; } return $this->concatenate( [ 'ORDER BY', $this->concatenate( $orders, ', ' ), ] ); } /** * Compile the `limit` attribute into an actual `LIMIT` statement. * * @return string */ public function compile_limit() { return $this->limit ? "LIMIT {$this->limit}" : ''; } /** * Compile the `offset` attribute into an actual `OFFSET` statement. * * @return string */ public function compile_offset() { return $this->offset ? "OFFSET {$this->offset}" : ''; } /** * Get the final SQL of the query, with bindings placeholders. * * @return string */ public function to_sql() { $select = $this->compile_select(); $join = $this->compile_joins(); $where = $this->compile_wheres(); $group_by = $this->compile_group_by(); $having = $this->compile_having(); $order_by = $this->compile_order_by(); $limit = $this->compile_limit(); $offset = $this->compile_offset(); return $this->concatenate( [ $select, $join, 'WHERE', $where, $group_by, $having, $order_by, $limit, $offset, ] ); } /** * Find & get by id. * * @param int $id - ID to search for. * @param string $field - Field name. Defaults to `id`. * * @return array|null */ public function find( $id, $field = 'id' ) { return $this->where( $field, '=', $id )->first(); } /** * Return the first matching row or null otherwise. * * @return array|null */ public function first() { return $this->limit( 1 )->get()->first(); } /** * Pluck a specific column from the query results. * * @param string $column - The column to pluck. * * @return Collection */ public function pluck( $column ) { return $this ->select( [ $column ] ) ->get() ->pluck( $column ); } /** * Return the count of rows based on the query. * * @param string $column * * @return int */ public function count( $column = '*' ) { return (int) ( new Collection( $this->select( [] ) ->add_count_select( $column ) ->first() ) )->first( 0 ); } /** * Get the query result. * * @return Collection */ public function get() { $sql = $this->to_sql(); $bindings = $this->get_bindings(); if ( 0 !== count( $bindings ) ) { $sql = $this->connection->prepare( $sql, $bindings ); } $result = $this->connection->get_results( $sql, ARRAY_A ); $result = new Collection( $result ); // Add aggregations. foreach ( $this->with as $resolver ) { $result = $resolver( $result ); } return $result; } /** * Insert data to a table. * * @param array $values - Array of [ `column` => `value` ] pairs. Non-escaped. * * @return int * @throws \Exception */ public function insert( array $values ) { // Take the raw table name since `wpdb` wraps it with backticks. $table = $this->from['table']; // Data should be escaped since `wpdb` escapes it. // https://developer.wordpress.org/reference/classes/wpdb/insert/ $succeed = $this->connection->insert( $table, $values ); if ( ! $succeed ) { throw new \Exception( $this->connection->last_error ); } return $this->connection->insert_id; } /** * Update data in the table. * * @param array $values - Array of [ `column` => `value` ] pairs. Non-escaped. * * @return bool|int */ public function update( array $values ) { $this->add_binding( array_values( $values ), 'select' ); $columns = []; foreach ( $values as $column => $value ) { $binding_type = $this->get_binding_type( $value ); $column = $this->wrap_with_backticks( $column ); $columns[] = "{$column} = {$binding_type}"; } $table = $this->compile_from(); $columns = $this->concatenate( $columns, ', ' ); $where = $this->compile_wheres(); $sql = $this->concatenate( [ 'UPDATE', $table, 'SET', $columns, 'WHERE', $where, ] ); $prepared = $this->connection->prepare( $sql, $this->get_bindings() ); return $this->connection->query( $prepared ); } /** * Delete data from the table. * * @return bool|int */ public function delete() { $where = $this->compile_wheres(); $table = $this->wrap_with_backticks( $this->from['table'] ); $sql = $this->concatenate( [ 'DELETE FROM', $table, 'WHERE', $where, ] ); $prepared = $this->connection->prepare( $sql, $this->get_bindings() ); return $this->connection->query( $prepared ); } /** * Add an eager loaded relation. * * @param string $key - Array key to store the resolver in. * @param callable $resolver - Resolve function that gets the results and adds the eager loaded relation. * * @return $this */ protected function add_with( $key, callable $resolver ) { $this->with[ $key ] = $resolver; return $this; } /** * Escape a value for `LIKE` statement. * * @param string $value - Value to escape. * * @return string */ protected function escape_like( $value ) { $value = explode( '%', $value ); $value = array_map( function ( $str ) { return $this->connection->esc_like( $str ); }, $value ); return implode( '%', $value ); } /** * Get a flat array of the current bindings. * * @param null|string $type - The binding type to get. * * @return array */ protected function get_bindings( $type = null ) { if ( $type && isset( $this->bindings[ $type ] ) ) { return $this->bindings[ $type ]; } return ( new Collection( $this->bindings ) )->flatten_recursive(); } /** * Add a binding to the bindings array by a sector. * * @param string|array $value - Raw value that needs to be bind. * @param string $type - Bind type (the sector in the SQL query). * * @return $this */ protected function add_binding( $value, $type ) { if ( is_array( $value ) ) { $this->bindings[ $type ] = array_values( array_merge( $this->bindings[ $type ], $value ) ); } else { $this->bindings[ $type ][] = $value; } return $this; } /** * Get the type of the binding type for SQL `prepare` function. * * @param array|string|numeric $value - The value to get the binding for. * * @return string - One of `%d` / `%f` / `%s`. */ protected function get_binding_type( $value ) { if ( is_array( $value ) ) { $bindings = array_map( function( $value ) { return $this->get_binding_type( $value ); }, array_values( $value ) ); return $this->concatenate( $bindings, ', ' ); } return is_float( $value ) ? '%f' : ( is_int( $value ) ? '%d' : '%s' ); } /** * Wrap a value with backticks. * * @param numeric|string|string[] $value - Value to wrap. * * @return string|string[] */ protected function wrap_with_backticks( $value ) { if ( is_array( $value ) ) { return array_map( [ $this, 'wrap_with_backticks' ], $value ); } // It should not wrap '*' with backticks. if ( '*' === $value ) { return $value; } $sanitized_value = is_scalar( $value ) ? preg_replace( '/[^a-zA-Z0-9_\-]/', '', $value ) : ''; return "`{$sanitized_value}`"; } /** * Concatenate an array of segments, removing empties. * * @param array $segments - Segments to concatenate. * @param array $separator - Separator string. Defaults to empty space. * * @return string */ protected function concatenate( array $segments, $separator = ' ' ) { return implode( $separator, array_filter( $segments, function ( $value ) { return '' !== (string) $value; } ) ); } /** * Parse a column by splitting it to table & column names, and wrapping it with backticks. * * @param $column - Column to parse. * * @return string */ protected function parse_column( $column ) { $parsed = explode( '.', $column ); $parsed = $this->wrap_with_backticks( $parsed ); return $this->concatenate( $parsed, '.' ); } protected function parse_as( $as ) { if ( ! $as ) { return ''; } $as = $this->wrap_with_backticks( $as ); return " AS {$as}"; } /** * Determine if a column is already selected. * * @param string $name - Column name to check. * * @return mixed|null */ protected function is_column_selected( $name ) { return ( new Collection( $this->columns ) ) ->find( function ( $column ) use ( $name ) { // Check for aliases. if ( ! empty( $column['as'] ) ) { return $name === $column['as']; } return $name === $column['column']; } ); } } database/join-clause.php 0000755 00000003156 15111667160 0011246 0 ustar 00 <?php namespace ElementorPro\Core\Database; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } /** * JOIN clause builder. * * Essentially, it uses the regular Builder's capabilities while wrapping some method * for syntactic sugar and better readability. */ class Join_Clause extends Query_Builder { // JOIN types. const TYPE_INNER = 'inner'; const TYPE_LEFT = 'left'; const TYPE_RIGHT = 'right'; /** * JOIN type. * * @var string */ public $type; /** * Join_Clause constructor. * * @param string $type - JOIN type. * @param \wpdb|null $connection - MySQL connection to use. * * @return void */ public function __construct( $type, \wpdb $connection = null ) { parent::__construct( $connection ); $this->type = $type; } /** * @uses `$this->where()`. * * @return Join_Clause */ public function on( $column, $operator, $value, $and_or = self::RELATION_AND ) { return $this->where( $column, $operator, $value, $and_or ); } /** * @shortcut `$this->on()`. * * @return Join_Clause */ public function or_on( $first, $operator, $second ) { return $this->on( $first, $operator, $second, self::RELATION_OR ); } /** * @uses `$this->where_column()`. * * @return Join_Clause */ public function on_column( $first, $operator, $second, $and_or = self::RELATION_AND ) { return $this->where_column( $first, $operator, $second, $and_or ); } /** * @shortcut `$this->on_column()`. * * @return Join_Clause */ public function or_on_column( $first, $operator, $second ) { return $this->on_column( $first, $operator, $second, self::RELATION_OR ); } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.02 |
proxy
|
phpinfo
|
Settings