first commit

This commit is contained in:
2024-07-15 11:28:08 +02:00
commit f52d538ea5
21891 changed files with 6161164 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
/* global elementorAdminBarConfig, jQuery */
class AdminBar extends elementorModules.ViewModule {
/**
* @returns {{}}
*/
getDefaultSettings() {
return {
prefixes: {
adminBarId: 'wp-admin-bar-',
},
classes: {
adminBarItem: 'ab-item',
adminBarItemTitle: 'elementor-edit-link-title',
adminBarItemSubTitle: 'elementor-edit-link-type',
adminBarNonLinkItem: 'ab-empty-item',
adminBarSubItemsWrapper: 'ab-sub-wrapper',
adminBarSubItems: 'ab-submenu',
},
selectors: {
adminBar: '#wp-admin-bar-root-default',
editMenuItem: '#wp-admin-bar-edit',
newMenuItem: '#wp-admin-bar-new-content',
},
};
}
/**
* @returns {{$adminBar: (jQuery)}}
*/
getDefaultElements() {
const { adminBar, editMenuItem, newMenuItem } = this.getSettings( 'selectors' );
return {
$adminBar: jQuery( adminBar ),
$editMenuItem: jQuery( editMenuItem ),
$newMenuItem: jQuery( newMenuItem ),
};
}
/**
* Init
*/
onInit() {
super.onInit();
this.createMenu( elementorAdminBarConfig );
}
/**
* Main method that creates the menu base on the config that provided.
*
* @param adminBarConfig
*/
createMenu( adminBarConfig ) {
const $items = this.createMenuItems( Object.values( adminBarConfig ) );
if ( this.elements.$editMenuItem.length ) {
// This is the normal case, when user visit a preview page of single post.
this.elements.$editMenuItem.after( $items );
} else if ( this.elements.$newMenuItem ) {
// This is another case, when user visit a preview page that cannot be edited e.g: archive page.
this.elements.$newMenuItem.after( $items );
} else {
// Default fallback in case there are no "new" or "edit" button.
this.elements.$adminBar.append( $items );
}
}
/**
* Creates a menu items from array of declaration.
*
* @param items
* @returns {jQuery[]}
*/
createMenuItems( items ) {
return items.map( ( item ) => this.createMenuItem( item ) );
}
/**
* Creates a menu item, both for menu and sub menu.
*
* @param item
* @returns {jQuery}
*/
createMenuItem( item ) {
const children = item.children ? Object.values( item.children ) : [];
const id = `${ this.getSettings( 'prefixes.adminBarId' ) }${ item.id }`;
const $title = jQuery( '<span>', {
class: this.getSettings( 'classes.adminBarItemTitle' ),
html: item.title,
} );
const $subTitle = item.sub_title ?
jQuery( '<span>', {
class: this.getSettings( 'classes.adminBarItemSubTitle' ),
html: item.sub_title,
} ) :
null;
const $item = jQuery( item.href ? '<a>' : '<div>', {
'aria-haspopup': children.length ? true : null,
class: [
this.getSettings( 'classes.adminBarItem' ),
item.href ? '' : this.getSettings( 'classes.adminBarNonLinkItem' ),
item.class,
].join( ' ' ),
href: item.href,
} ).append( [ $title, $subTitle ] );
return jQuery( '<li>', {
id,
class: children.length ? 'menupop' : '' + ( item.parent_class || 'elementor-general-section' ),
} ).append( [ $item, children.length ? this.createSubMenuItems( id, children ) : null ] );
}
/**
* Creates sub menu items wrapper.
*
* @param parentId
* @param children
* @returns {jQuery}
*/
createSubMenuItems( parentId, children ) {
const $list = jQuery( '<ul>', {
class: this.getSettings( 'classes.adminBarSubItems' ),
id: `${ parentId }-default`,
} ).append( this.createMenuItems( children ) );
return jQuery( '<div>', {
class: this.getSettings( 'classes.adminBarSubItemsWrapper' ),
} ).append( $list );
}
}
document.addEventListener( 'DOMContentLoaded', () => new AdminBar() );

View File

@@ -0,0 +1,139 @@
<?php
namespace Elementor\Modules\AdminBar;
use Elementor\Core\Base\Document;
use Elementor\Core\Base\App as BaseApp;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseApp {
/**
* @var Document[]
*/
private $documents = [];
/**
* @return bool
*/
public static function is_active() {
return is_admin_bar_showing();
}
/**
* @return string
*/
public function get_name() {
return 'admin-bar';
}
/**
* Collect the documents that was rendered in the current page.
*
* @param Document $document
* @param $is_excerpt
*/
public function add_document_to_admin_bar( Document $document, $is_excerpt ) {
if (
$is_excerpt ||
! $document::get_property( 'show_on_admin_bar' ) ||
! $document->is_editable_by_current_user()
) {
return;
}
$this->documents[ $document->get_main_id() ] = $document;
}
/**
* Scripts for module.
*/
public function enqueue_scripts() {
if ( empty( $this->documents ) ) {
return;
}
// Should load 'elementor-admin-bar' before 'admin-bar'
wp_dequeue_script( 'admin-bar' );
wp_enqueue_script(
'elementor-admin-bar',
$this->get_js_assets_url( 'elementor-admin-bar' ),
[ 'elementor-frontend-modules' ],
ELEMENTOR_VERSION,
true
);
// This is a core script of WordPress, it is not required to pass the 'ver' argument.
wp_enqueue_script( // phpcs:ignore WordPress.WP.EnqueuedResourceParameters
'admin-bar',
null,
[ 'elementor-admin-bar' ],
false,
true
);
$this->print_config( 'elementor-admin-bar' );
}
/**
* Creates admin bar menu items config.
*
* @return array
*/
public function get_init_settings() {
$settings = [];
if ( ! empty( $this->documents ) ) {
$settings['elementor_edit_page'] = $this->get_edit_button_config();
}
/**
* Register admin_bar config to parse later in the frontend and add to the admin bar with JS
*
* @param array $settings the admin_bar config
*
* @since 3.0.0
*/
return apply_filters( 'elementor/frontend/admin_bar/settings', $settings );
}
/**
* Creates the config for 'Edit with elementor' menu item.
*
* @return array
*/
private function get_edit_button_config() {
$queried_object_id = get_queried_object_id();
$href = null;
if ( is_singular() && isset( $this->documents[ $queried_object_id ] ) ) {
$href = $this->documents[ $queried_object_id ]->get_edit_url();
unset( $this->documents[ $queried_object_id ] );
}
return [
'id' => 'elementor_edit_page',
'title' => __( 'Edit with Elementor', 'elementor' ),
'href' => $href,
'children' => array_map( function ( $document ) {
return [
'id' => "elementor_edit_doc_{$document->get_main_id()}",
'title' => $document->get_post()->post_title,
'sub_title' => $document::get_title(),
'href' => $document->get_edit_url(),
];
}, $this->documents ),
];
}
/**
* Module constructor.
*/
public function __construct() {
add_action( 'elementor/frontend/before_get_builder_content', [ $this, 'add_document_to_admin_bar' ], 10, 2 );
add_action( 'wp_footer', [ $this, 'enqueue_scripts' ], 11 /* after third party scripts */ );
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Elementor\Modules\CompatibilityTag;
use Elementor\Plugin;
use Elementor\Core\Utils\Version;
use Elementor\Core\Utils\Collection;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Modules\System_Info\Module as System_Info;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_Module extends BaseModule {
const MODULE_NAME = 'compatibility-tag';
/**
* @var Compatibility_Tag
*/
private $compatibility_tag_service;
/**
* @return string
*/
public function get_name() {
return static::MODULE_NAME;
}
/**
* @return Compatibility_Tag
*/
private function get_compatibility_tag_service() {
if ( ! $this->compatibility_tag_service ) {
$this->compatibility_tag_service = new Compatibility_Tag( $this->get_plugin_header() );
}
return $this->compatibility_tag_service;
}
/**
* Add allowed headers to plugins.
*
* @param array $headers
* @param $compatibility_tag_header
*
* @return array
*/
protected function enable_elementor_headers( array $headers, $compatibility_tag_header ) {
$headers[] = $compatibility_tag_header;
return $headers;
}
/**
* @return Collection
*/
protected function get_plugins_to_check() {
return $this->get_plugins_with_header();
}
/**
* Append a compatibility message to the update plugin warning.
*
* @param array $args
*
* @throws \Exception
*/
protected function on_plugin_update_message( array $args ) {
$new_version = Version::create_from_string( $args['new_version'] );
if ( $new_version->compare( '=', $args['Version'], Version::PART_MAJOR_2 ) ) {
return;
}
$plugins = $this->get_plugins_to_check();
$plugins_compatibility = $this->get_compatibility_tag_service()->check( $new_version, $plugins->keys() );
$plugins = $plugins->filter( function ( $data, $plugin_name ) use ( $plugins_compatibility ) {
return Compatibility_Tag::COMPATIBLE !== $plugins_compatibility[ $plugin_name ];
} );
if ( $plugins->is_empty() ) {
return;
}
include __DIR__ . '/views/plugin-update-message-compatibility.php';
}
/**
* Get all plugins with specific header.
*
* @return Collection
*/
private function get_plugins_with_header() {
return Plugin::$instance->wp
->get_plugins()
->filter( function ( array $plugin ) {
return ! empty( $plugin[ $this->get_plugin_header() ] );
} );
}
/**
* @return string
*/
abstract protected function get_plugin_header();
/**
* @return string
*/
abstract protected function get_plugin_label();
/**
* @return string
*/
abstract protected function get_plugin_name();
/**
* @return string
*/
abstract protected function get_plugin_version();
/**
* Base_Module constructor.
*
* @throws \Exception
*/
public function __construct() {
add_filter( 'extra_plugin_headers', function ( array $headers ) {
return $this->enable_elementor_headers( $headers, $this->get_plugin_header() );
} );
add_action( 'in_plugin_update_message-' . $this->get_plugin_name(), function ( array $args ) {
$this->on_plugin_update_message( $args );
}, 11 /* After the warning message for backup */ );
add_action( 'admin_init', function () {
System_Info::add_report( $this->get_plugin_name() . '_compatibility', [
'file_name' => __DIR__ . '/compatibility-tag-report.php',
'class_name' => __NAMESPACE__ . '\Compatibility_Tag_Report',
'fields' => [
'compatibility_tag_service' => $this->get_compatibility_tag_service(),
'plugin_label' => $this->get_plugin_label(),
'plugin_version' => Version::create_from_string( $this->get_plugin_version() ),
'plugins_to_check' => $this->get_plugins_to_check()
->only( get_option( 'active_plugins' ) )
->keys(),
],
] );
} );
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace Elementor\Modules\CompatibilityTag;
use Elementor\Plugin;
use Elementor\Core\Utils\Version;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\System_Info\Reporters\Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Compatibility_Tag_Report extends Base {
/**
* @var Compatibility_Tag
*/
protected $compatibility_tag_service;
/**
* @var Version
*/
protected $plugin_version;
/**
* @var string
*/
protected $plugin_label;
/**
* @var array
*/
protected $plugins_to_check;
/**
* Compatibility_Tag_Report constructor.
*
* @param $properties
*/
public function __construct( $properties ) {
parent::__construct( $properties );
$this->compatibility_tag_service = $this->_properties['fields']['compatibility_tag_service'];
$this->plugin_label = $this->_properties['fields']['plugin_label'];
$this->plugin_version = $this->_properties['fields']['plugin_version'];
$this->plugins_to_check = $this->_properties['fields']['plugins_to_check'];
}
/**
* The title of the report
*
* @return string
*/
public function get_title() {
return $this->plugin_label . ' - Compatibility Tag';
}
/**
* Report fields
*
* @return string[]
*/
public function get_fields() {
return [
'report_data' => '',
];
}
/**
* Report data.
*
* @return string[]
* @throws \Exception
*/
public function get_report_data() {
$compatibility_status = $this->compatibility_tag_service->check(
$this->plugin_version,
$this->plugins_to_check
);
if ( 'html' === $this->_properties['format'] ) {
$compatibility_status = $this->get_html_from_compatibility_status( $compatibility_status );
} elseif ( 'raw' === $this->_properties['format'] ) {
$compatibility_status = $this->get_raw_from_compatibility_status( $compatibility_status );
}
return [
'value' => $compatibility_status,
];
}
/**
* Merge compatibility status with the plugins data.
*
* @param array $compatibility_status
*
* @return Collection
*/
private function merge_compatibility_status_with_plugins( array $compatibility_status ) {
$labels = $this->get_report_labels();
$compatibility_status = ( new Collection( $compatibility_status ) )
->map( function ( $value ) use ( $labels ) {
$status = isset( $labels[ $value ] ) ? $labels[ $value ] : __( 'Unknown', 'elementor' );
return [ 'compatibility_status' => $status ];
} );
return Plugin::$instance->wp
->get_plugins()
->only( $compatibility_status->keys() )
->merge_recursive( $compatibility_status );
}
/**
* Format compatibility status into HTML.
*
* @param array $compatibility_status
*
* @return string
*/
private function get_html_from_compatibility_status( array $compatibility_status ) {
return $this->merge_compatibility_status_with_plugins( $compatibility_status )
->map( function ( array $plugin ) {
return "<tr><td> {$plugin['Name']} </td><td> {$plugin['compatibility_status']} </td></tr>";
} )
->implode( '' );
}
/**
* Format compatibility status into raw string.
*
* @param array $compatibility_status
*
* @return string
*/
private function get_raw_from_compatibility_status( array $compatibility_status ) {
return PHP_EOL . $this->merge_compatibility_status_with_plugins( $compatibility_status )
->map( function ( array $plugin ) {
return "\t{$plugin['Name']}: {$plugin['compatibility_status']}";
} )
->implode( PHP_EOL );
}
/**
* @return array
*/
private function get_report_labels() {
return [
Compatibility_Tag::COMPATIBLE => __( 'Compatible', 'elementor' ),
Compatibility_Tag::INCOMPATIBLE => __( 'Incompatible', 'elementor' ),
Compatibility_Tag::HEADER_NOT_EXISTS => __( 'Compatibility not specified', 'elementor' ),
Compatibility_Tag::INVALID_VERSION => __( 'Compatibility unknown', 'elementor' ),
Compatibility_Tag::PLUGIN_NOT_EXISTS => __( 'Error', 'elementor' ),
];
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Elementor\Modules\CompatibilityTag;
use Elementor\Plugin;
use Elementor\Core\Utils\Version;
use Elementor\Core\Base\Base_Object;
use Elementor\Core\Utils\Collection;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Compatibility_Tag extends Base_Object {
const PLUGIN_NOT_EXISTS = 'plugin_not_exists';
const HEADER_NOT_EXISTS = 'header_not_exists';
const INVALID_VERSION = 'invalid_version';
const INCOMPATIBLE = 'incompatible';
const COMPATIBLE = 'compatible';
/**
* @var string Holds the header that should be checked.
*/
private $header;
/**
* Compatibility_Tag constructor.
*
* @param string $header
*/
public function __construct( $header ) {
$this->header = $header;
}
/**
* Return if plugins is compatible or not.
*
* @param Version $version
* @param array $plugins_names
*
* @return array
* @throws \Exception
*/
public function check( Version $version, array $plugins_names ) {
return ( new Collection( $plugins_names ) )
->map_with_keys( function ( $plugin_name ) use ( $version ) {
return [ $plugin_name => $this->is_compatible( $version, $plugin_name ) ];
} )
->all();
}
/**
* Check single plugin if is compatible or not.
*
* @param Version $version
* @param $plugin_name
*
* @return string
* @throws \Exception
*/
private function is_compatible( Version $version, $plugin_name ) {
$plugins = Plugin::$instance->wp->get_plugins();
if ( ! isset( $plugins[ $plugin_name ] ) ) {
return self::PLUGIN_NOT_EXISTS;
}
$requested_plugin = $plugins[ $plugin_name ];
if ( empty( $requested_plugin[ $this->header ] ) ) {
return self::HEADER_NOT_EXISTS;
}
if ( ! Version::is_valid_version( $requested_plugin[ $this->header ] ) ) {
return self::INVALID_VERSION;
}
if ( $version->compare( '>', $requested_plugin[ $this->header ], Version::PART_MAJOR_2 ) ) {
return self::INCOMPATIBLE;
}
return self::COMPATIBLE;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Elementor\Modules\CompatibilityTag;
use Elementor\Plugin;
use Elementor\Core\Utils\Version;
use Elementor\Core\Utils\Collection;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Inspired By WooCommerce.
*
* @link https://github.com/woocommerce/woocommerce/blob/master/includes/admin/plugin-updates/class-wc-plugin-updates.php
*/
class Module extends Base_Module {
/**
* This is the header used by extensions to show testing.
*
* @var string
*/
const PLUGIN_VERSION_TESTED_HEADER = 'Elementor tested up to';
/**
* @return string
*/
protected function get_plugin_header() {
return static::PLUGIN_VERSION_TESTED_HEADER;
}
/**
* @return string
*/
protected function get_plugin_label() {
return __( 'Elementor', 'elementor' );
}
/**
* @return string
*/
protected function get_plugin_name() {
return ELEMENTOR_PLUGIN_BASE;
}
/**
* @return string
*/
protected function get_plugin_version() {
return ELEMENTOR_VERSION;
}
/**
* @return Collection
*/
protected function get_plugins_to_check() {
return parent::get_plugins_to_check()
->merge( $this->get_plugins_with_plugin_title_in_their_name() );
}
/**
* Get all the plugins that has the name of the current plugin in their name.
*
* @return Collection
*/
private function get_plugins_with_plugin_title_in_their_name() {
return Plugin::$instance->wp
->get_plugins()
->except( [
'elementor/elementor.php',
'elementor-beta/elementor-beta.php',
'block-builder/block-builder.php',
] )
->filter( function ( array $data ) {
return false !== strpos( strtolower( $data['Name'] ), 'elementor' );
} );
}
}

View File

@@ -0,0 +1,68 @@
<?php
use Elementor\Core\Utils\Version;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\CompatibilityTag\Base_Module;
use Elementor\Modules\CompatibilityTag\Compatibility_Tag;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Those variables were declared in 'in_plugin_update_message' method that included the current view file.
*
* @var Base_Module $this
* @var Version $new_version
* @var Collection $plugins
* @var array $plugins_compatibility
*/
?>
<hr class="e-major-update-warning__separator" />
<div class="e-major-update-warning">
<div class="e-major-update-warning__icon">
<i class="eicon-info-circle"></i>
</div>
<div>
<div class="e-major-update-warning__message">
<strong>
<?php echo __( 'Compatibility Alert', 'elementor' ); ?>
</strong> -
<?php
/* translators: %1$s: Plugin name %s: Plugin version */
echo sprintf(
__( 'Some of the plugins youre using have not been tested with the latest version of %1$s (%2$s). To avoid issues, make sure they are all up to date and compatible before updating %1$s.', 'elementor' ),
$this->get_plugin_label(),
$new_version->__toString()
);
?>
</div>
<br />
<table class="e-compatibility-update-table">
<tr>
<th><?php echo __( 'Plugin', 'elementor' ); ?></th>
<th><?php
/* translators: %s - Elementor plugin name */
echo sprintf( __( 'Tested up to %s version', 'elementor' ), $this->get_plugin_label() );
?></th>
</tr>
<?php foreach ( $plugins as $plugin_name => $plugin_data ) : ?>
<?php
if (
in_array( $plugins_compatibility[ $plugin_name ], [
Compatibility_Tag::PLUGIN_NOT_EXISTS,
Compatibility_Tag::HEADER_NOT_EXISTS,
Compatibility_Tag::INVALID_VERSION,
], true )
) {
$plugin_data[ $this->get_plugin_header() ] = __( 'Unknown', 'elementor' );
}
?>
<tr>
<td><?php echo $plugin_data['Name']; ?></td>
<td><?php echo $plugin_data[ $this->get_plugin_header() ]; ?></td>
</tr>
<?php endforeach ?>
</table>
</div>
</div>

View File

@@ -0,0 +1,14 @@
export default class extends elementorModules.editor.utils.Module {
onElementorLoaded() {
this.notifyDeprecated();
}
notifyDeprecated() {
// eslint-disable-next-line camelcase
const notices = elementor.config.dev_tools.deprecation.soft_notices;
Object.entries( notices ).forEach( ( [ key, notice ] ) => {
elementorCommon.helpers.softDeprecated( key, ...notice );
} );
}
}

View File

@@ -0,0 +1,352 @@
<?php
namespace Elementor\Modules\DevTools;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Deprecation {
const SOFT_VERSIONS_COUNT = 4;
const HARD_VERSIONS_COUNT = 8;
private $current_version = null;
private $soft_deprecated_notices = [];
public function __construct( $current_version ) {
$this->current_version = $current_version;
}
public function get_settings() {
return [
'soft_notices' => $this->soft_deprecated_notices,
];
}
/**
* Get total of major.
*
* Since `get_total_major` cannot determine how much really versions between 2.9.0 and 3.3.0 if there is 2.10.0 version for example,
* versions with major2 more then 9 will be added to total.
*
* @since 3.1.0
*
* @param array $parsed_version
*
* @return int
*/
public function get_total_major( $parsed_version ) {
$major1 = $parsed_version['major1'];
$major2 = $parsed_version['major2'];
$major2 = $major2 > 9 ? 9 : $major2;
$minor = 0;
$total = intval( "{$major1}{$major2}{$minor}" );
if ( $total > 99 ) {
$total = $total / 10;
} else {
$total = intval( $total / 10 );
}
if ( $parsed_version['major2'] > 9 ) {
$total += $parsed_version['major2'] - 9;
}
return $total;
}
/**
* Get next version.
*
* @since 3.1.0
*
* @param string $version
* @param int $count
*
* @return string|false
*/
public function get_next_version( $version, $count = 1 ) {
$version = $this->parse_version( $version );
if ( ! $version ) {
return false;
}
$version['total'] = $this->get_total_major( $version ) + $count;
$total = $version['total'];
if ( $total > 9 ) {
$version['major1'] = intval( $total / 10 );
$version['major2'] = $total % 10;
} else {
$version['major1'] = 0;
$version['major2'] = $total;
}
$version['minor'] = 0;
return $this->implode_version( $version );
}
/**
* Implode parsed version to string version.
*
* @since 3.1.0
*
* @param array $parsed_version
*
* @return string
*/
public function implode_version( $parsed_version ) {
$major1 = $parsed_version['major1'];
$major2 = $parsed_version['major2'];
$minor = $parsed_version['minor'];
return "{$major1}.{$major2}.{$minor}";
}
/**
* Parse to an informative array.
*
* @since 3.1.0
*
* @param string $version
*
* @return array|false
*/
public function parse_version( $version ) {
$version_explode = explode( '.', $version );
$version_explode_count = count( $version_explode );
if ( $version_explode_count < 3 || $version_explode_count > 4 ) {
trigger_error( 'Invalid Semantic Version string provided' );
return false;
}
list( $major1, $major2, $minor ) = $version_explode;
$result = [
'major1' => intval( $major1 ),
'major2' => intval( $major2 ),
'minor' => intval( $minor ),
];
if ( $version_explode_count > 3 ) {
$result['build'] = $version_explode[3];
}
return $result;
}
/**
* Compare two versions, result is equal to diff of major versions.
* Notice: If you want to compare between 2.9.0 and 3.3.0, and there is also a 2.10.0 version, you cannot get the right comparison
* Since $this->deprecation->get_total_major cannot determine how much really versions between 2.9.0 and 3.3.0.
*
* @since 3.1.0
*
* @param {string} $version1
* @param {string} $version2
*
* @return int|false
*/
public function compare_version( $version1, $version2 ) {
$version1 = self::parse_version( $version1 );
$version2 = self::parse_version( $version2 );
if ( $version1 && $version2 ) {
$versions = [ &$version1, &$version2 ];
foreach ( $versions as &$version ) {
$version['total'] = self::get_total_major( $version );
}
return $version1['total'] - $version2['total'];
}
return false;
}
/**
* Check Deprecation
*
* Checks whether the given entity is valid. If valid, this method checks whether the deprecation
* should be soft (browser console notice) or hard (use WordPress' native deprecation methods).
*
* @since 3.1.0
*
* @param string $entity - The Deprecated entity (the function/hook itself)
* @param string $version
* @param string $replacement Optional
* @param string $base_version Optional. Default is `null`
*
* @return bool|void
* @throws \Exception
*/
private function check_deprecation( $entity, $version, $replacement, $base_version = null ) {
if ( null === $base_version ) {
$base_version = $this->current_version;
}
$diff = $this->compare_version( $base_version, $version );
if ( false === $diff ) {
throw new \Exception( 'Invalid deprecation diff' );
}
$print_deprecated = false;
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && $diff <= self::SOFT_VERSIONS_COUNT ) {
// Soft deprecated.
if ( ! isset( $this->soft_deprecated_notices[ $entity ] ) ) {
$this->soft_deprecated_notices[ $entity ] = [
$version,
$replacement,
];
}
if ( defined( 'ELEMENTOR_DEBUG' ) && ELEMENTOR_DEBUG ) {
$print_deprecated = true;
}
} else {
// Hard deprecated.
$print_deprecated = true;
}
return $print_deprecated;
}
/**
* Deprecated Function
*
* Handles the deprecation process for functions.
*
* @since 3.1.0
*
* @param string $function
* @param string $version
* @param string $replacement Optional. Default is ''
* @param string $base_version Optional. Default is `null`
* @throws \Exception
*/
public function deprecated_function( $function, $version, $replacement = '', $base_version = null ) {
$print_deprecated = $this->check_deprecation( $function, $version, $replacement, $base_version );
if ( $print_deprecated ) {
_deprecated_function( $function, $version, $replacement );
}
}
/**
* Deprecated Hook
*
* Handles the deprecation process for hooks.
*
* @since 3.1.0
*
* @param string $hook
* @param string $version
* @param string $replacement Optional. Default is ''
* @param string $base_version Optional. Default is `null`
* @throws \Exception
*/
public function deprecated_hook( $hook, $version, $replacement = '', $base_version = null ) {
$print_deprecated = $this->check_deprecation( $hook, $version, $replacement, $base_version );
if ( $print_deprecated ) {
_deprecated_hook( $hook, $version, $replacement );
}
}
/**
* Deprecated Argument
*
* Handles the deprecation process for function arguments.
*
* @since 3.1.0
*
* @param string $argument
* @param string $version
* @param string $replacement
* @param string $message
* @throws \Exception
*/
public function deprecated_argument( $argument, $version, $replacement = '', $message = '' ) {
$print_deprecated = $this->check_deprecation( $argument, $version, $replacement );
if ( $print_deprecated ) {
$message = empty( $message ) ? '' : ' ' . $message;
$error_message_args = [ $argument, $version ];
if ( $replacement ) {
/* translators: 1: Function argument, 2: Elementor version number, 3: Replacement argument name. */
$translation_string = __( 'The %1$s argument is <strong>deprecated</strong> since version %2$s! Use %3$s instead.', 'elementor' );
$error_message_args[] = $replacement;
} else {
/* translators: 1: Function argument, 2: Elementor version number. */
$translation_string = __( 'The %1$s argument is <strong>deprecated</strong> since version %2$s!', 'elementor' );
}
trigger_error(
vsprintf(
$translation_string,
$error_message_args
) . $message,
E_USER_DEPRECATED
);
}
}
/**
* Do Deprecated Action
*
* A method used to run deprecated actions through Elementor's deprecation process.
*
* @since 3.1.0
*
* @param string $hook
* @param array $args
* @param string $version
* @param string $replacement
* @param null|string $base_version
*
* @throws \Exception
*/
public function do_deprecated_action( $hook, $args, $version, $replacement = '', $base_version = null ) {
if ( ! has_action( $hook ) ) {
return;
}
$this->deprecated_hook( $hook, $version, $replacement, $base_version );
do_action_ref_array( $hook, $args );
}
/**
* Apply Deprecated Filter
*
* A method used to run deprecated filters through Elementor's deprecation process.
*
* @since 3.2.0
*
* @param string $hook
* @param array $args
* @param string $version
* @param string $replacement
* @param null|string $base_version
*
* @return mixed
* @throws \Exception
*/
public function apply_deprecated_filter( $hook, $args, $version, $replacement = '', $base_version = null ) {
if ( ! has_action( $hook ) ) {
return;
}
$this->deprecated_hook( $hook, $version, $replacement, $base_version );
return apply_filters_ref_array( $hook, $args );
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Elementor\Modules\DevTools;
use Elementor\Core\Base\Module as BaseModule;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Fix issue with 'Potentially polymorphic call. The code may be inoperable depending on the actual class instance passed as the argument.'.
* Its tells to the editor that instance() return right module. instead of base module.
* @method Module instance()
*/
class Module extends BaseModule {
/**
* @var Deprecation
*/
public $deprecation;
public function __construct() {
$this->deprecation = new Deprecation( ELEMENTOR_VERSION );
add_filter( 'elementor/editor/localize_settings', [ $this, 'localize_settings' ] );
}
public function get_name() {
return 'dev-tools';
}
public function localize_settings( $settings ) {
$settings = array_replace_recursive( $settings, [
'dev_tools' => [
'deprecation' => $this->deprecation->get_settings(),
],
] );
return $settings;
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace Elementor\Modules\DynamicTags;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\DynamicTags\Manager;
use Elementor\Core\DynamicTags\Tag;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor dynamic tags module.
*
* Elementor dynamic tags module handler class is responsible for registering
* and managing Elementor dynamic tags modules.
*
* @since 2.0.0
*/
class Module extends BaseModule {
/**
* Base dynamic tag group.
*/
const BASE_GROUP = 'base';
/**
* Dynamic tags text category.
*/
const TEXT_CATEGORY = 'text';
/**
* Dynamic tags URL category.
*/
const URL_CATEGORY = 'url';
/**
* Dynamic tags image category.
*/
const IMAGE_CATEGORY = 'image';
/**
* Dynamic tags media category.
*/
const MEDIA_CATEGORY = 'media';
/**
* Dynamic tags post meta category.
*/
const POST_META_CATEGORY = 'post_meta';
/**
* Dynamic tags gallery category.
*/
const GALLERY_CATEGORY = 'gallery';
/**
* Dynamic tags number category.
*/
const NUMBER_CATEGORY = 'number';
/**
* Dynamic tags number category.
*/
const COLOR_CATEGORY = 'color';
/**
* Dynamic tags module constructor.
*
* Initializing Elementor dynamic tags module.
*
* @since 2.0.0
* @access public
*/
public function __construct() {
$this->register_groups();
add_action( 'elementor/dynamic_tags/register_tags', [ $this, 'register_tags' ] );
}
/**
* Get module name.
*
* Retrieve the dynamic tags module name.
*
* @since 2.0.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'dynamic_tags';
}
/**
* Get classes names.
*
* Retrieve the dynamic tag classes names.
*
* @since 2.0.0
* @access public
*
* @return array Tag dynamic tag classes names.
*/
public function get_tag_classes_names() {
return [];
}
/**
* Get groups.
*
* Retrieve the dynamic tag groups.
*
* @since 2.0.0
* @access public
*
* @return array Tag dynamic tag groups.
*/
public function get_groups() {
return [
self::BASE_GROUP => [
'title' => 'Base Tags',
],
];
}
/**
* Register groups.
*
* Add all the available tag groups.
*
* @since 2.0.0
* @access private
*/
private function register_groups() {
foreach ( $this->get_groups() as $group_name => $group_settings ) {
Plugin::$instance->dynamic_tags->register_group( $group_name, $group_settings );
}
}
/**
* Register tags.
*
* Add all the available dynamic tags.
*
* @since 2.0.0
* @access public
*
* @param Manager $dynamic_tags
*/
public function register_tags( $dynamic_tags ) {
foreach ( $this->get_tag_classes_names() as $tag_class ) {
/** @var Tag $class_name */
$class_name = $this->get_reflection()->getNamespaceName() . '\Tags\\' . $tag_class;
$dynamic_tags->register_tag( $class_name );
}
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace Elementor\Modules\Gutenberg;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Plugin;
use Elementor\User;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
protected $is_gutenberg_editor_active = false;
/**
* @since 2.1.0
* @access public
*/
public function get_name() {
return 'gutenberg';
}
/**
* @since 2.1.0
* @access public
* @static
*/
public static function is_active() {
return function_exists( 'register_block_type' );
}
/**
* @since 2.1.0
* @access public
*/
public function register_elementor_rest_field() {
register_rest_field( get_post_types( '', 'names' ),
'gutenberg_elementor_mode', [
'update_callback' => function( $request_value, $object ) {
if ( ! User::is_current_user_can_edit( $object->ID ) ) {
return false;
}
$document = Plugin::$instance->documents->get( $object->ID );
if ( ! $document ) {
return false;
}
$document->set_is_built_with_elementor( false );
return true;
},
]
);
}
/**
* @since 2.1.0
* @access public
*/
public function enqueue_assets() {
$document = Plugin::$instance->documents->get( get_the_ID() );
if ( ! $document || ! $document->is_editable_by_current_user() ) {
return;
}
$this->is_gutenberg_editor_active = true;
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_enqueue_script( 'elementor-gutenberg', ELEMENTOR_ASSETS_URL . 'js/gutenberg' . $suffix . '.js', [ 'jquery' ], ELEMENTOR_VERSION, true );
$elementor_settings = [
'isElementorMode' => $document->is_built_with_elementor(),
'editLink' => $document->get_edit_url(),
];
Utils::print_js_config( 'elementor-gutenberg', 'ElementorGutenbergSettings', $elementor_settings );
}
/**
* @since 2.1.0
* @access public
*/
public function print_admin_js_template() {
if ( ! $this->is_gutenberg_editor_active ) {
return;
}
?>
<script id="elementor-gutenberg-button-switch-mode" type="text/html">
<div id="elementor-switch-mode">
<button id="elementor-switch-mode-button" type="button" class="button button-primary button-large">
<span class="elementor-switch-mode-on"><?php echo __( '&#8592; Back to WordPress Editor', 'elementor' ); ?></span>
<span class="elementor-switch-mode-off">
<i class="eicon-elementor-square" aria-hidden="true"></i>
<?php echo __( 'Edit with Elementor', 'elementor' ); ?>
</span>
</button>
</div>
</script>
<script id="elementor-gutenberg-panel" type="text/html">
<div id="elementor-editor"><a id="elementor-go-to-edit-page-link" href="#">
<div id="elementor-editor-button" class="button button-primary button-hero">
<i class="eicon-elementor-square" aria-hidden="true"></i>
<?php echo __( 'Edit with Elementor', 'elementor' ); ?>
</div>
<div class="elementor-loader-wrapper">
<div class="elementor-loader">
<div class="elementor-loader-boxes">
<div class="elementor-loader-box"></div>
<div class="elementor-loader-box"></div>
<div class="elementor-loader-box"></div>
<div class="elementor-loader-box"></div>
</div>
</div>
<div class="elementor-loading-title"><?php echo __( 'Loading', 'elementor' ); ?></div>
</div>
</a></div>
</script>
<?php
}
/**
* @since 2.1.0
* @access public
*/
public function __construct() {
add_action( 'rest_api_init', [ $this, 'register_elementor_rest_field' ] );
add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_assets' ] );
add_action( 'admin_footer', [ $this, 'print_admin_js_template' ] );
}
}

View File

@@ -0,0 +1,35 @@
import ComponentBase from 'elementor-api/modules/component-base';
export default class Component extends ComponentBase {
getNamespace() {
return 'panel/history';
}
defaultTabs() {
return {
actions: { title: __( 'Actions', 'elementor' ) },
revisions: { title: __( 'Revisions', 'elementor' ) },
};
}
defaultShortcuts() {
return {
actions: {
keys: 'ctrl+shift+h',
},
};
}
renderTab( tab ) {
elementor.getPanelView().setPage( 'historyPage' ).showView( tab );
}
activate() {
// Activate the tab component itself.
$e.components.activate( this.getTabRoute( this.currentTab ) );
}
getTabsWrapperSelector() {
return '#elementor-panel-elements-navigation';
}
}

View File

@@ -0,0 +1,27 @@
import ComponentBase from 'elementor-api/modules/component-base';
import * as commands from 'elementor-document/history/commands/';
export default class Component extends ComponentBase {
getNamespace() {
return 'panel/history/actions';
}
defaultCommands() {
return this.importCommands( commands );
}
defaultShortcuts() {
return {
undo: {
keys: 'ctrl+z',
exclude: [ 'input' ],
scopes: [ 'panel', 'navigator' ],
},
redo: {
keys: 'ctrl+shift+z, ctrl+y',
exclude: [ 'input' ],
scopes: [ 'panel', 'navigator' ],
},
};
}
}

View File

@@ -0,0 +1,13 @@
export default class extends Marionette.ItemView {
getTemplate() {
return '#tmpl-elementor-panel-history-no-items';
}
id() {
return 'elementor-panel-history-no-items';
}
onDestroy() {
this._parent.$el.removeClass( 'elementor-empty' );
}
}

View File

@@ -0,0 +1,15 @@
module.exports = Backbone.Model.extend( {
defaults: {
id: 0,
type: '',
status: 'not_applied',
title: '',
subTitle: '',
action: '',
history: {},
},
initialize: function() {
this.set( 'items', new Backbone.Collection() );
},
} );

View File

@@ -0,0 +1,15 @@
export default class extends Marionette.ItemView {
getTemplate() {
return '#tmpl-elementor-panel-history-item';
}
className() {
return 'elementor-history-item elementor-history-item-' + this.model.get( 'status' );
}
triggers() {
return {
click: 'click',
};
}
}

View File

@@ -0,0 +1,259 @@
import ItemModel from './item-model';
/**
* TODO: consider refactor this class.
* TODO: should be `Document/History` component.
* TODO: should be attached to elementor.history.history + BC.
*/
export default class HistoryManager {
currentItemID = null;
items = new Backbone.Collection( [], { model: ItemModel } );
active = true;
translations = {
add: __( 'Added', 'elementor' ),
change: __( 'Edited', 'elementor' ),
disable: __( 'Disabled', 'elementor' ),
duplicate: __( 'Duplicate', 'elementor' ),
enable: __( 'Enabled', 'elementor' ),
move: __( 'Moved', 'elementor' ),
paste: __( 'Pasted', 'elementor' ),
paste_style: __( 'Style Pasted', 'elementor' ),
remove: __( 'Removed', 'elementor' ),
reset_style: __( 'Style Reset', 'elementor' ),
reset_settings: __( 'Settings Reset', 'elementor' ),
};
constructor( document ) {
this.document = document;
this.currentItem = new Backbone.Model( {
id: 0,
} );
}
getActionLabel( itemData ) {
// TODO: this function should be static.
if ( this.translations[ itemData.type ] ) {
return this.translations[ itemData.type ];
}
return itemData.type;
}
navigate( isRedo ) {
const currentItem = this.items.find( ( model ) => {
return 'not_applied' === model.get( 'status' );
} ),
currentItemIndex = this.items.indexOf( currentItem ),
requiredIndex = isRedo ? currentItemIndex - 1 : currentItemIndex + 1;
if ( ( ! isRedo && ! currentItem ) || requiredIndex < 0 || requiredIndex >= this.items.length ) {
return;
}
this.doItem( requiredIndex );
}
setActive( value ) {
this.active = value;
}
getActive( value ) {
return this.active;
}
getItems() {
return this.items;
}
startItem( itemData ) {
this.currentItemID = this.addItem( itemData );
return this.currentItemID;
}
endItem( id ) {
if ( this.currentItemID !== id ) {
return;
}
this.currentItemID = null;
}
deleteItem( id ) {
const item = this.items.findWhere( {
id: id,
} );
this.items.remove( item );
this.currentItemID = null;
}
isItemStarted() {
return null !== this.currentItemID;
}
getCurrentId() {
return this.currentItemID;
}
addItem( itemData ) {
if ( ! this.getActive() ) {
return;
}
if ( ! this.items.length ) {
this.items.add( {
status: 'not_applied',
title: __( 'Editing Started', 'elementor' ),
subTitle: '',
action: '',
editing_started: true,
} );
}
// Remove old applied items from top of list
while ( this.items.length && 'applied' === this.items.first().get( 'status' ) ) {
this.items.shift();
}
const id = this.currentItemID ? this.currentItemID : new Date().getTime();
let currentItem = this.items.findWhere( {
id: id,
} );
if ( ! currentItem ) {
currentItem = new ItemModel( {
id: id,
title: itemData.title,
subTitle: itemData.subTitle,
action: this.getActionLabel( itemData ),
type: itemData.type,
} );
this.startItemTitle = '';
this.startItemAction = '';
}
currentItem.get( 'items' ).add( itemData, { at: 0 } );
this.items.add( currentItem, { at: 0 } );
this.updateCurrentItem( currentItem );
return id;
}
doItem( index ) {
// Don't track while restoring the item
this.setActive( false );
const item = this.items.at( index );
if ( 'not_applied' === item.get( 'status' ) ) {
this.undoItem( index );
} else {
this.redoItem( index );
}
this.setActive( true );
const panel = elementor.getPanelView(),
panelPage = panel.getCurrentPageView(),
editedElementView = panelPage.getOption( 'editedElementView' );
let viewToScroll;
if ( $e.routes.isPartOf( 'panel/editor' ) && editedElementView ) {
if ( editedElementView.isDestroyed ) {
// If the the element isn't exist - show the history panel
$e.route( 'panel/history/actions' );
} else {
// If element exist - render again, maybe the settings has been changed
viewToScroll = editedElementView;
}
} else if ( item instanceof Backbone.Model && item.get( 'items' ).length ) {
const historyItem = item.get( 'items' ).first();
if ( historyItem.get( 'restore' ) ) {
let container = 'sub-add' === historyItem.get( 'type' ) ?
historyItem.get( 'data' ).containerToRestore :
historyItem.get( 'container' ) || historyItem.get( 'containers' );
if ( Array.isArray( container ) ) {
container = container[ 0 ];
}
if ( container ) {
viewToScroll = container.lookup().view;
}
}
}
$e.internal( 'document/save/set-is-modified', {
status: item.get( 'id' ) !== this.document.editor.lastSaveHistoryId,
} );
this.updateCurrentItem( item );
if ( viewToScroll && ! elementor.helpers.isInViewport( viewToScroll.$el[ 0 ], elementor.$previewContents.find( 'html' )[ 0 ] ) ) {
elementor.helpers.scrollToView( viewToScroll.$el );
}
}
undoItem( index ) {
for ( let stepNum = 0; stepNum < index; stepNum++ ) {
const item = this.items.at( stepNum );
if ( 'not_applied' === item.get( 'status' ) ) {
item.get( 'items' ).each( function( subItem ) {
const restore = subItem.get( 'restore' );
if ( restore ) {
restore( subItem );
}
} );
item.set( 'status', 'applied' );
}
}
}
redoItem( index ) {
for ( let stepNum = this.items.length - 1; stepNum >= index; stepNum-- ) {
const item = this.items.at( stepNum );
if ( 'applied' === item.get( 'status' ) ) {
var reversedSubItems = _.toArray( item.get( 'items' ).models ).reverse();
_( reversedSubItems ).each( function( subItem ) {
const restore = subItem.get( 'restore' );
if ( restore ) {
restore( subItem, true );
}
} );
item.set( 'status', 'not_applied' );
}
}
}
updateCurrentItem( item ) {
// Save last selected item.
this.currentItem = item;
this.updatePanelPageCurrentItem();
}
updatePanelPageCurrentItem() {
if ( $e.routes.is( 'panel/history/actions' ) ) {
elementor.getPanelView().getCurrentPageView().getCurrentTab().updateCurrentItem();
}
}
}

View File

@@ -0,0 +1,63 @@
import ItemView from './item-view';
import EmptyView from './empty';
module.exports = Marionette.CompositeView.extend( {
id: 'elementor-panel-history',
template: '#tmpl-elementor-panel-history-tab',
childView: ItemView,
childViewContainer: '#elementor-history-list',
emptyView: EmptyView,
currentItem: null,
updateCurrentItem: function() {
if ( this.children.length <= 1 ) {
return;
}
_.defer( () => {
// Set current item - the first not applied item
const currentItem = this.collection.find( function( model ) {
return 'not_applied' === model.get( 'status' );
} ),
currentView = this.children.findByModel( currentItem );
if ( ! currentView ) {
return;
}
const currentItemClass = 'elementor-history-item-current';
if ( this.currentItem ) {
this.currentItem.removeClass( currentItemClass );
}
this.currentItem = currentView.$el;
this.currentItem.addClass( currentItemClass );
} );
},
onRender: function() {
this.updateCurrentItem();
},
onRenderEmpty: function() {
this.$el.addClass( 'elementor-empty' );
},
onChildviewClick: function( childView, event ) {
if ( childView.$el === this.currentItem ) {
return;
}
const collection = event.model.collection,
index = collection.findIndex( event.model );
$e.run( 'panel/history/actions/do', { index } );
},
} );

View File

@@ -0,0 +1,33 @@
import Component from './component';
import HistoryComponent from './history/component';
import RevisionsComponent from './revisions/component';
import PanelPage from './panel-page';
export default class Manager {
constructor() {
elementorCommon.elements.$window.on( 'elementor:loaded', this.init );
}
init() {
$e.components.register( new Component() );
$e.components.register( new HistoryComponent() );
$e.components.register( new RevisionsComponent() );
elementor.on( 'panel:init', () => {
elementor.getPanelView().addPage( 'historyPage', {
view: PanelPage,
title: __( 'History', 'elementor' ),
} );
} );
}
get history() {
elementorCommon.helpers.softDeprecated( 'elementor.history.history', '2.9.0', 'elementor.documents.getCurrent().history' );
return elementor.documents.getCurrent().history;
}
get revisions() {
elementorCommon.helpers.softDeprecated( 'elementor.history.revisions', '2.9.0', 'elementor.documents.getCurrent().revisions' );
return elementor.documents.getCurrent().revisions;
}
}

View File

@@ -0,0 +1,84 @@
var TabHistoryView = require( './history/panel-tab' );
import TabRevisionsLoadingView from './revisions/panel/loading';
import TabRevisionsView from './revisions/panel/tab';
import TabRevisionsEmptyView from './revisions/panel/empty';
module.exports = Marionette.LayoutView.extend( {
template: '#tmpl-elementor-panel-history-page',
regions: {
content: '#elementor-panel-history-content',
},
ui: {
tabs: '.elementor-panel-navigation-tab',
},
regionViews: {},
currentTab: null,
/**
* @type {Document}
*/
document: null,
initialize: function( options ) {
this.document = options.document || elementor.documents.getCurrent();
this.initRegionViews();
},
initRegionViews: function() {
const historyItems = this.document.history.getItems();
this.regionViews = {
actions: {
view: () => {
return TabHistoryView;
},
options: {
collection: historyItems,
history: this.document.history,
},
},
revisions: {
view: () => {
const revisionsItems = this.document.revisions.getItems();
if ( ! revisionsItems ) {
return TabRevisionsLoadingView;
}
if ( 1 === revisionsItems.length && 'current' === revisionsItems.models[ 0 ].get( 'type' ) ) {
return TabRevisionsEmptyView;
}
return TabRevisionsView;
},
options: {
document: this.document,
},
},
};
},
getCurrentTab: function() {
return this.currentTab;
},
showView: function( viewName ) {
const viewDetails = this.regionViews[ viewName ],
options = viewDetails.options || {},
View = viewDetails.view();
if ( this.currentTab && this.currentTab.constructor === View ) {
return;
}
this.currentTab = new View( options );
this.content.show( this.currentTab );
},
} );

View File

@@ -0,0 +1,8 @@
var RevisionModel = require( './model' );
module.exports = Backbone.Collection.extend( {
model: RevisionModel,
comparator: function( model ) {
return -model.get( 'timestamp' );
},
} );

View File

@@ -0,0 +1,9 @@
import CommandBase from 'elementor-api/modules/command-base';
export class Down extends CommandBase {
apply() {
this.component.navigate();
}
}
export default Down;

View File

@@ -0,0 +1,2 @@
export { Down } from './down';
export { Up } from './up';

View File

@@ -0,0 +1,9 @@
import CommandBase from 'elementor-api/modules/command-base';
export class Up extends CommandBase {
apply() {
this.component.navigate( true );
}
}
export default Up;

View File

@@ -0,0 +1,36 @@
import ComponentBase from 'elementor-api/modules/component-base';
import * as commands from './commands/';
import * as hooks from './hooks/';
export default class Component extends ComponentBase {
getNamespace() {
return 'panel/history/revisions';
}
defaultCommands() {
return this.importCommands( commands );
}
defaultHooks() {
return this.importHooks( hooks );
}
defaultShortcuts() {
return {
up: {
keys: 'up',
scopes: [ this.getNamespace() ],
},
down: {
keys: 'down',
scopes: [ this.getNamespace() ],
},
};
}
navigate( up ) {
if ( elementor.documents.getCurrent().revisions.getItems().length > 1 ) {
elementor.getPanelView().getCurrentPageView().currentTab.navigate( up );
}
}
}

View File

@@ -0,0 +1,32 @@
import HookDataAfter from 'elementor-api/modules/hooks/data/after';
export class RevisionsAfterSave extends HookDataAfter {
getCommand() {
return 'document/save/save';
}
getId() {
return 'revisions-after-save';
}
apply( args, result ) {
const { data } = result,
revisionsModule = elementor.documents.getCurrent().revisions;
if ( data.latest_revisions ) {
revisionsModule.addRevisions( data.latest_revisions );
}
revisionsModule.requestRevisions( () => {
if ( data.revisions_ids ) {
const revisionsToKeep = revisionsModule.revisions.filter( ( revision ) => {
return -1 !== data.revisions_ids.indexOf( revision.get( 'id' ) );
} );
revisionsModule.revisions.reset( revisionsToKeep );
}
} );
}
}
export default RevisionsAfterSave;

View File

@@ -0,0 +1 @@
export { RevisionsAfterSave } from './data/save';

View File

@@ -0,0 +1,97 @@
const RevisionsCollection = require( './collection' );
/**
* TODO: consider refactor this class.
* TODO: Rename to RevisionsModule.
*/
export default class RevisionsManager {
document;
revisions;
constructor( document ) {
this.document = document;
}
getItems() {
return this.revisions;
}
requestRevisions( callback ) {
if ( this.revisions ) {
callback( this.revisions );
return;
}
elementorCommon.ajax.addRequest( 'get_revisions', {
success: ( data ) => {
this.revisions = new RevisionsCollection( data );
this.revisions.on( 'update', this.onRevisionsUpdate.bind( this ) );
callback( this.revisions );
},
} );
}
setEditorData( data ) {
const collection = elementor.getPreviewView().collection;
collection.reset( data );
}
getRevisionDataAsync( id, options ) {
_.extend( options, {
data: {
id: id,
},
} );
return elementorCommon.ajax.addRequest( 'get_revision_data', options );
}
addRevisions( items ) {
this.requestRevisions( () => {
items.forEach( ( item ) => {
const existedModel = this.revisions.findWhere( {
id: item.id,
} );
if ( existedModel ) {
this.revisions.remove( existedModel, { silent: true } );
}
this.revisions.add( item, { silent: true } );
} );
this.revisions.trigger( 'update' );
} );
}
deleteRevision( revisionModel, options ) {
const params = {
data: {
id: revisionModel.get( 'id' ),
},
success: () => {
if ( options.success ) {
options.success();
}
revisionModel.destroy();
},
};
if ( options.error ) {
params.error = options.error;
}
elementorCommon.ajax.addRequest( 'delete_revision', params );
}
onRevisionsUpdate() {
if ( $e.routes.is( 'panel/history/revisions' ) ) {
$e.routes.refreshContainer( 'panel' );
}
}
}

View File

@@ -0,0 +1,9 @@
var RevisionModel;
RevisionModel = Backbone.Model.extend();
RevisionModel.prototype.sync = function() {
return null;
};
module.exports = RevisionModel;

View File

@@ -0,0 +1,5 @@
module.exports = Marionette.ItemView.extend( {
template: '#tmpl-elementor-panel-revisions-no-revisions',
id: 'elementor-panel-revisions-no-revisions',
className: 'elementor-nerd-box',
} );

View File

@@ -0,0 +1,15 @@
export default class extends Marionette.ItemView {
getTemplate() {
return '#tmpl-elementor-panel-revisions-loading';
}
id() {
return 'elementor-panel-revisions-loading';
}
onRender() {
this.options.document.revisions.requestRevisions( () => {
setTimeout( () => $e.routes.refreshContainer( 'panel' ) );
} );
}
}

View File

@@ -0,0 +1,208 @@
module.exports = Marionette.CompositeView.extend( {
id: 'elementor-panel-revisions',
template: '#tmpl-elementor-panel-revisions',
childView: require( './view' ),
childViewContainer: '#elementor-revisions-list',
ui: {
discard: '.elementor-panel-scheme-discard .elementor-button',
apply: '.elementor-panel-scheme-save .elementor-button',
},
events: {
'click @ui.discard': 'onDiscardClick',
'click @ui.apply': 'onApplyClick',
},
isRevisionApplied: false,
currentPreviewId: null,
currentPreviewItem: null,
document: null,
initialize: function( options ) {
this.document = options.document;
this.collection = this.document.revisions.getItems();
this.listenTo( elementor.channels.editor, 'saved', this.onEditorSaved );
this.currentPreviewId = elementor.config.document.revisions.current_id;
},
getRevisionViewData: function( revisionView ) {
this.document.revisions.getRevisionDataAsync( revisionView.model.get( 'id' ), {
success: ( data ) => {
if ( this.document.config.panel.has_elements ) {
this.document.revisions.setEditorData( data.elements );
}
elementor.settings.page.model.set( data.settings );
this.setRevisionsButtonsActive( true );
revisionView.$el.removeClass( 'elementor-revision-item-loading' );
this.enterReviewMode();
},
error: ( errorMessage ) => {
revisionView.$el.removeClass( 'elementor-revision-item-loading' );
this.currentPreviewItem = null;
this.currentPreviewId = null;
alert( errorMessage );
},
} );
},
setRevisionsButtonsActive: function( active ) {
// Check the tab is open.
if ( ! this.isDestroyed ) {
this.ui.apply.add( this.ui.discard ).prop( 'disabled', ! active );
}
},
deleteRevision: function( revisionView ) {
revisionView.$el.addClass( 'elementor-revision-item-loading' );
this.document.revisions.deleteRevision( revisionView.model, {
success: () => {
if ( revisionView.model.get( 'id' ) === this.currentPreviewId ) {
this.onDiscardClick();
}
this.currentPreviewId = null;
},
error: () => {
revisionView.$el.removeClass( 'elementor-revision-item-loading' );
alert( 'An error occurred' );
},
} );
},
enterReviewMode: function() {
elementor.changeEditMode( 'review' );
},
exitReviewMode: function() {
elementor.changeEditMode( 'edit' );
},
navigate: function( reverse ) {
if ( ! this.currentPreviewId || ! this.currentPreviewItem || this.children.length <= 1 ) {
return;
}
var currentPreviewItemIndex = this.collection.indexOf( this.currentPreviewItem.model ),
requiredIndex = reverse ? currentPreviewItemIndex - 1 : currentPreviewItemIndex + 1;
if ( requiredIndex < 0 ) {
requiredIndex = this.collection.length - 1;
}
if ( requiredIndex >= this.collection.length ) {
requiredIndex = 0;
}
this.children.findByIndex( requiredIndex ).ui.detailsArea.trigger( 'click' );
},
onEditorSaved: function() {
this.exitReviewMode();
this.setRevisionsButtonsActive( false );
this.currentPreviewId = elementor.config.document.revisions.current_id;
},
onApplyClick: function() {
$e.internal( 'document/save/set-is-modified', { status: true } );
$e.run( 'document/save/auto', { force: true } );
this.isRevisionApplied = true;
this.currentPreviewId = null;
this.document.history.getItems().reset();
},
onDiscardClick: function() {
if ( this.document.config.panel.has_elements ) {
this.document.revisions.setEditorData( elementor.config.document.elements );
}
$e.internal( 'document/save/set-is-modified', { status: this.isRevisionApplied } );
this.isRevisionApplied = false;
this.setRevisionsButtonsActive( false );
this.currentPreviewId = null;
this.exitReviewMode();
if ( this.currentPreviewItem ) {
this.currentPreviewItem.$el.removeClass( 'elementor-revision-current-preview' );
}
},
onDestroy: function() {
if ( this.currentPreviewId && this.currentPreviewId !== elementor.config.document.revisions.current_id ) {
this.onDiscardClick();
}
},
onRenderCollection: function() {
if ( ! this.currentPreviewId ) {
return;
}
var currentPreviewModel = this.collection.findWhere( { id: this.currentPreviewId } );
// Ensure the model is exist and not deleted during a save.
if ( currentPreviewModel ) {
this.currentPreviewItem = this.children.findByModelCid( currentPreviewModel.cid );
this.currentPreviewItem.$el.addClass( 'elementor-revision-current-preview' );
}
},
onChildviewDetailsAreaClick: function( childView ) {
const revisionID = childView.model.get( 'id' );
if ( revisionID === this.currentPreviewId ) {
return;
}
if ( this.currentPreviewItem ) {
this.currentPreviewItem.$el.removeClass( 'elementor-revision-current-preview elementor-revision-item-loading' );
}
childView.$el.addClass( 'elementor-revision-current-preview elementor-revision-item-loading' );
const revision = ( null === this.currentPreviewId || elementor.config.document.revisions.current_id === this.currentPreviewId );
if ( revision && elementor.saver.isEditorChanged() ) {
// TODO: Change to 'document/save/auto' ?.
$e.internal( 'document/save/save', {
status: 'autosave',
onSuccess: () => {
this.getRevisionViewData( childView );
},
} );
} else {
this.getRevisionViewData( childView );
}
this.currentPreviewItem = childView;
this.currentPreviewId = revisionID;
},
} );

View File

@@ -0,0 +1,13 @@
module.exports = Marionette.ItemView.extend( {
template: '#tmpl-elementor-panel-revisions-revision-item',
className: 'elementor-revision-item',
ui: {
detailsArea: '.elementor-revision-item__details',
},
triggers: {
'click @ui.detailsArea': 'detailsArea:click',
},
} );

View File

@@ -0,0 +1,91 @@
.elementor-history- {
&item {
display: flex;
align-items: center;
border: 1px solid $editor-lightest;
padding: 10px 15px;
margin-bottom: 10px;
font-size: 11px;
line-height: 1.4;
cursor: pointer;
transition: $transition-hover;
&:hover {
background-color: fade_out($editor-background, 0.7) ;
.elementor-history-item__icon{
.eicon:before {
content:'\e924';
}
}
}
&-applied {
color: $editor-light;
}
&-current {
background: $editor-background;
cursor: default;
.elementor-history-item__icon, &:hover .elementor-history-item__icon{
.eicon:before {
content:'\e90e';
}
}
}
&__details{
width: 95%;
}
&__title {
font-weight: bold;
}
&__subtitle, &__action {
font-weight: lighter;
}
&__action {
font-style: italic;
text-decoration: underline;
}
.__icon {
float: $end;
}
}
&revisions-message{
font-size: 11px;
text-align: center;
padding-top: 5px;
}
}
#elementor-panel-history {
padding: 20px 20px 15px;
&.elementor-empty {
.elementor-history-revisions-message {
padding-top: 20px;
}
}
&:not(.elementor-empty) {
background-color: #fff;
margin-top: 10px;
}
}
#elementor-panel-history-no-items,
#elementor-panel-revisions-no-revisions {
text-align: center;
.elementor-nerd-box-icon {
margin-top: 20px;
}
}

View File

@@ -0,0 +1,78 @@
.elementor-revision- {
&item {
&__wrapper {
display: flex;
align-items: center;
border: 1px solid $editor-lightest;
padding: 10px 15px;
margin-bottom: 10px;
font-size: 11px;
transition: $transition-hover;
&.current {
font-weight: bold;
}
.elementor-revision-item__tools-current {
color: $editor-info
}
}
&:hover:not(.elementor-revision-current-preview) {
background-color: fade_out($editor-background, 0.7) ;
}
&-loading {
.elementor-revision-item__tools-current {
display: none;
}
}
&:not(.elementor-revision-item-loading) {
.elementor-revision-item__tools-spinner {
display: none;
}
}
&__gravatar {
border-radius: 50%;
overflow: hidden;
img {
display: block;
}
}
&__details {
@include padding-start(15px);
flex-grow: 1;
cursor: pointer;
}
}
&meta {
padding-top: 5px;
font-size: 10px;
font-weight: bold;
}
&current-preview {
background-color: $editor-background;
}
}
#elementor-restore-autosave-dialog.dialog-widget {
background-color: rgba(0, 0, 0, 0.3);
}
#elementor-panel-revisions-loading {
@include absolute-center;
.eicon-loading {
font-size: 50px;
color: $editor-light;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Elementor\Modules\History;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor history module.
*
* Elementor history module handler class is responsible for registering and
* managing Elementor history modules.
*
* @since 1.7.0
*/
class Module extends BaseModule {
/**
* Get module name.
*
* Retrieve the history module name.
*
* @since 1.7.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'history';
}
/**
* Localize settings.
*
* Add new localized settings for the history module.
*
* Fired by `elementor/editor/localize_settings` filter.
*
* @since 1.7.0
* @access public
* @deprecated 3.1.0
*
* @return array Localized settings.
*/
public function localize_settings() {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function( __METHOD__, '3.1.0' );
return [];
}
/**
* @since 2.3.0
* @access public
*/
public function add_templates() {
Plugin::$instance->common->add_template( __DIR__ . '/views/history-panel-template.php' );
Plugin::$instance->common->add_template( __DIR__ . '/views/revisions-panel-template.php' );
}
/**
* History module constructor.
*
* Initializing Elementor history module.
*
* @since 1.7.0
* @access public
*/
public function __construct() {
add_action( 'elementor/editor/init', [ $this, 'add_templates' ] );
}
}

View File

@@ -0,0 +1,421 @@
<?php
namespace Elementor\Modules\History;
use Elementor\Core\Base\Document;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Core\Files\CSS\Post as Post_CSS;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor history revisions manager.
*
* Elementor history revisions manager handler class is responsible for
* registering and managing Elementor revisions manager.
*
* @since 1.7.0
*/
class Revisions_Manager {
/**
* Maximum number of revisions to display.
*/
const MAX_REVISIONS_TO_DISPLAY = 100;
/**
* Authors list.
*
* Holds all the authors.
*
* @access private
*
* @var array
*/
private static $authors = [];
/**
* History revisions manager constructor.
*
* Initializing Elementor history revisions manager.
*
* @since 1.7.0
* @access public
*/
public function __construct() {
self::register_actions();
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function handle_revision() {
add_filter( 'wp_save_post_revision_check_for_changes', '__return_false' );
}
/**
* @since 2.0.0
* @access public
* @static
*
* @param $post_content
* @param $post_id
*
* @return string
*/
public static function avoid_delete_auto_save( $post_content, $post_id ) {
// Add a temporary string in order the $post will not be equal to the $autosave
// in edit-form-advanced.php:210
$document = Plugin::$instance->documents->get( $post_id );
if ( $document && $document->is_built_with_elementor() ) {
$post_content .= '<!-- Created with Elementor -->';
}
return $post_content;
}
/**
* @since 2.0.0
* @access public
* @static
*/
public static function remove_temp_post_content() {
global $post;
$document = Plugin::$instance->documents->get( $post->ID );
if ( ! $document || ! $document->is_built_with_elementor() ) {
return;
}
$post->post_content = str_replace( '<!-- Created with Elementor -->', '', $post->post_content );
}
/**
* @since 1.7.0
* @access public
* @static
*
* @param int $post_id
* @param array $query_args
* @param bool $parse_result
*
* @return array
*/
public static function get_revisions( $post_id = 0, $query_args = [], $parse_result = true ) {
$post = get_post( $post_id );
if ( ! $post || empty( $post->ID ) ) {
return [];
}
$revisions = [];
$default_query_args = [
'posts_per_page' => self::MAX_REVISIONS_TO_DISPLAY,
'meta_key' => '_elementor_data',
];
$query_args = array_merge( $default_query_args, $query_args );
$posts = wp_get_post_revisions( $post->ID, $query_args );
if ( ! wp_revisions_enabled( $post ) ) {
$autosave = Utils::get_post_autosave( $post->ID );
if ( $autosave ) {
if ( $parse_result ) {
array_unshift( $posts, $autosave );
} else {
array_unshift( $posts, $autosave->ID );
}
}
}
if ( $parse_result ) {
array_unshift( $posts, $post );
} else {
array_unshift( $posts, $post->ID );
return $posts;
}
$current_time = current_time( 'timestamp' );
/** @var \WP_Post $revision */
foreach ( $posts as $revision ) {
$date = date_i18n( _x( 'M j @ H:i', 'revision date format', 'elementor' ), strtotime( $revision->post_modified ) );
$human_time = human_time_diff( strtotime( $revision->post_modified ), $current_time );
if ( $revision->ID === $post->ID ) {
$type = 'current';
$type_label = __( 'Current Version', 'elementor' );
} elseif ( false !== strpos( $revision->post_name, 'autosave' ) ) {
$type = 'autosave';
$type_label = __( 'Autosave', 'elementor' );
} else {
$type = 'revision';
$type_label = __( 'Revision', 'elementor' );
}
if ( ! isset( self::$authors[ $revision->post_author ] ) ) {
self::$authors[ $revision->post_author ] = [
'avatar' => get_avatar( $revision->post_author, 22 ),
'display_name' => get_the_author_meta( 'display_name', $revision->post_author ),
];
}
$revisions[] = [
'id' => $revision->ID,
'author' => self::$authors[ $revision->post_author ]['display_name'],
'timestamp' => strtotime( $revision->post_modified ),
'date' => sprintf(
/* translators: 1: Human readable time difference, 2: Date */
__( '%1$s ago (%2$s)', 'elementor' ),
$human_time,
$date
),
'type' => $type,
'typeLabel' => $type_label,
'gravatar' => self::$authors[ $revision->post_author ]['avatar'],
];
}
return $revisions;
}
/**
* @since 1.9.2
* @access public
* @static
*/
public static function update_autosave( $autosave_data ) {
self::save_revision( $autosave_data['ID'] );
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function save_revision( $revision_id ) {
$parent_id = wp_is_post_revision( $revision_id );
if ( $parent_id ) {
Plugin::$instance->db->safe_copy_elementor_meta( $parent_id, $revision_id );
}
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function restore_revision( $parent_id, $revision_id ) {
$parent = Plugin::$instance->documents->get( $parent_id );
$revision = Plugin::$instance->documents->get( $revision_id );
if ( ! $parent || ! $revision ) {
return;
}
$is_built_with_elementor = $revision->is_built_with_elementor();
$parent->set_is_built_with_elementor( $is_built_with_elementor );
if ( ! $is_built_with_elementor ) {
return;
}
Plugin::$instance->db->copy_elementor_meta( $revision_id, $parent_id );
$post_css = Post_CSS::create( $parent_id );
$post_css->update();
}
/**
* @since 2.3.0
* @access public
* @static
*
* @param $data
*
* @return array
* @throws \Exception
*/
public static function ajax_get_revision_data( array $data ) {
if ( ! isset( $data['id'] ) ) {
throw new \Exception( 'You must set the revision ID.' );
}
$revision = Plugin::$instance->documents->get( $data['id'] );
if ( ! $revision ) {
throw new \Exception( 'Invalid revision.' );
}
if ( ! current_user_can( 'edit_post', $revision->get_id() ) ) {
throw new \Exception( __( 'Access denied.', 'elementor' ) );
}
$revision_data = [
'settings' => $revision->get_settings(),
'elements' => $revision->get_elements_data(),
];
return $revision_data;
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function add_revision_support_for_all_post_types() {
$post_types = get_post_types_by_support( 'elementor' );
foreach ( $post_types as $post_type ) {
add_post_type_support( $post_type, 'revisions' );
}
}
/**
* @since 2.0.0
* @access public
* @static
* @param array $return_data
* @param Document $document
*
* @return array
*/
public static function on_ajax_save_builder_data( $return_data, $document ) {
$post_id = $document->get_main_id();
$latest_revisions = self::get_revisions(
$post_id, [
'posts_per_page' => 1,
]
);
$all_revision_ids = self::get_revisions(
$post_id, [
'fields' => 'ids',
], false
);
// Send revisions data only if has revisions.
if ( ! empty( $latest_revisions ) ) {
$current_revision_id = self::current_revision_id( $post_id );
$return_data = array_replace_recursive( $return_data, [
'config' => [
'document' => [
'revisions' => [
'current_id' => $current_revision_id,
],
],
],
'latest_revisions' => $latest_revisions,
'revisions_ids' => $all_revision_ids,
] );
}
return $return_data;
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function db_before_save( $status, $has_changes ) {
if ( $has_changes ) {
self::handle_revision();
}
}
public static function document_config( $settings, $post_id ) {
$settings['revisions'] = [
'enabled' => ( $post_id && wp_revisions_enabled( get_post( $post_id ) ) ),
'current_id' => self::current_revision_id( $post_id ),
];
return $settings;
}
/**
* Localize settings.
*
* Add new localized settings for the revisions manager.
*
* Fired by `elementor/editor/editor_settings` filter.
*
* @since 1.7.0
* @access public
* @static
* @deprecated 3.1.0
*/
public static function editor_settings() {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function( __METHOD__, '3.1.0' );
return [];
}
public static function ajax_get_revisions() {
return self::get_revisions();
}
/**
* @since 2.3.0
* @access public
* @static
*/
public static function register_ajax_actions( Ajax $ajax ) {
$ajax->register_ajax_action( 'get_revisions', [ __CLASS__, 'ajax_get_revisions' ] );
$ajax->register_ajax_action( 'get_revision_data', [ __CLASS__, 'ajax_get_revision_data' ] );
}
/**
* @since 1.7.0
* @access private
* @static
*/
private static function register_actions() {
add_action( 'wp_restore_post_revision', [ __CLASS__, 'restore_revision' ], 10, 2 );
add_action( 'init', [ __CLASS__, 'add_revision_support_for_all_post_types' ], 9999 );
add_filter( 'elementor/document/config', [ __CLASS__, 'document_config' ], 10, 2 );
add_action( 'elementor/db/before_save', [ __CLASS__, 'db_before_save' ], 10, 2 );
add_action( '_wp_put_post_revision', [ __CLASS__, 'save_revision' ] );
add_action( 'wp_creating_autosave', [ __CLASS__, 'update_autosave' ] );
add_action( 'elementor/ajax/register_actions', [ __CLASS__, 'register_ajax_actions' ] );
// Hack to avoid delete the auto-save revision in WP editor.
add_filter( 'edit_post_content', [ __CLASS__, 'avoid_delete_auto_save' ], 10, 2 );
add_action( 'edit_form_after_title', [ __CLASS__, 'remove_temp_post_content' ] );
if ( wp_doing_ajax() ) {
add_filter( 'elementor/documents/ajax_save/return_data', [ __CLASS__, 'on_ajax_save_builder_data' ], 10, 2 );
}
}
/**
* @since 1.9.0
* @access private
* @static
*/
private static function current_revision_id( $post_id ) {
$current_revision_id = $post_id;
$autosave = Utils::get_post_autosave( $post_id );
if ( is_object( $autosave ) ) {
$current_revision_id = $autosave->ID;
}
return $current_revision_id;
}
}

View File

@@ -0,0 +1,35 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<script type="text/template" id="tmpl-elementor-panel-history-page">
<div id="elementor-panel-elements-navigation" class="elementor-panel-navigation">
<div class="elementor-component-tab elementor-panel-navigation-tab" data-tab="actions"><?php echo __( 'Actions', 'elementor' ); ?></div>
<div class="elementor-component-tab elementor-panel-navigation-tab" data-tab="revisions"><?php echo __( 'Revisions', 'elementor' ); ?></div>
</div>
<div id="elementor-panel-history-content"></div>
</script>
<script type="text/template" id="tmpl-elementor-panel-history-tab">
<div id="elementor-history-list"></div>
<div class="elementor-history-revisions-message"><?php echo __( 'Switch to Revisions tab for older versions', 'elementor' ); ?></div>
</script>
<script type="text/template" id="tmpl-elementor-panel-history-no-items">
<img class="elementor-nerd-box-icon" src="<?php echo ELEMENTOR_ASSETS_URL . 'images/information.svg'; ?>" />
<div class="elementor-nerd-box-title"><?php echo __( 'No History Yet', 'elementor' ); ?></div>
<div class="elementor-nerd-box-message"><?php echo __( 'Once you start working, you\'ll be able to redo / undo any action you make in the editor.', 'elementor' ); ?></div>
</script>
<script type="text/template" id="tmpl-elementor-panel-history-item">
<div class="elementor-history-item__details">
<span class="elementor-history-item__title">{{{ title }}}</span>
<span class="elementor-history-item__subtitle">{{{ subTitle }}}</span>
<span class="elementor-history-item__action">{{{ action }}}</span>
</div>
<div class="elementor-history-item__icon">
<span class="eicon" aria-hidden="true"></span>
</div>
</script>

View File

@@ -0,0 +1,69 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<script type="text/template" id="tmpl-elementor-panel-revisions">
<div class="elementor-panel-box">
<div class="elementor-panel-scheme-buttons">
<div class="elementor-panel-scheme-button-wrapper elementor-panel-scheme-discard">
<button class="elementor-button" disabled>
<i class="eicon-close" aria-hidden="true"></i>
<?php echo __( 'Discard', 'elementor' ); ?>
</button>
</div>
<div class="elementor-panel-scheme-button-wrapper elementor-panel-scheme-save">
<button class="elementor-button elementor-button-success" disabled>
<?php echo __( 'Apply', 'elementor' ); ?>
</button>
</div>
</div>
</div>
<div class="elementor-panel-box">
<div class="elementor-panel-heading">
<div class="elementor-panel-heading-title"><?php echo __( 'Revisions', 'elementor' ); ?></div>
</div>
<div id="elementor-revisions-list" class="elementor-panel-box-content"></div>
</div>
</script>
<script type="text/template" id="tmpl-elementor-panel-revisions-no-revisions">
<#
var no_revisions_1 = '<?php echo __( 'Revision history lets you save your previous versions of your work, and restore them any time.', 'elementor' ); ?>',
no_revisions_2 = '<?php echo __( 'Start designing your page and you will be able to see the entire revision history here.', 'elementor' ); ?>',
revisions_disabled_1 = '<?php echo __( 'It looks like the post revision feature is unavailable in your website.', 'elementor' ); ?>',
revisions_disabled_2 = '<?php printf( __( 'Learn more about <a target="_blank" href="%s">WordPress revisions</a>', 'elementor' ), 'https://go.elementor.com/wordpress-revisions/' ); /* translators: %s: Codex URL */ ?>';
#>
<img class="elementor-nerd-box-icon" src="<?php echo ELEMENTOR_ASSETS_URL . 'images/information.svg'; ?>" />
<div class="elementor-nerd-box-title"><?php echo __( 'No Revisions Saved Yet', 'elementor' ); ?></div>
<div class="elementor-nerd-box-message">{{{ elementor.config.document.revisions.enabled ? no_revisions_1 : revisions_disabled_1 }}}</div>
<div class="elementor-nerd-box-message">{{{ elementor.config.document.revisions.enabled ? no_revisions_2 : revisions_disabled_2 }}}</div>
</script>
<script type="text/template" id="tmpl-elementor-panel-revisions-loading">
<i class="eicon-loading eicon-animation-spin" aria-hidden="true"></i>
</script>
<script type="text/template" id="tmpl-elementor-panel-revisions-revision-item">
<div class="elementor-revision-item__wrapper {{ type }}">
<div class="elementor-revision-item__gravatar">{{{ gravatar }}}</div>
<div class="elementor-revision-item__details">
<div class="elementor-revision-date" title="{{{ new Date( timestamp * 1000 ) }}}">{{{ date }}}</div>
<div class="elementor-revision-meta">
<span>{{{ typeLabel }}}</span>
<?php echo __( 'By', 'elementor' ); ?> {{{ author }}}
<span>(#{{{ id }}})</span>&nbsp;
</div>
</div>
<div class="elementor-revision-item__tools">
<# if ( 'current' === type ) { #>
<i class="elementor-revision-item__tools-current eicon-star" aria-hidden="true"></i>
<span class="elementor-screen-only"><?php echo __( 'Current', 'elementor' ); ?></span>
<# } #>
<i class="elementor-revision-item__tools-spinner eicon-loading eicon-animation-spin" aria-hidden="true"></i>
</div>
</div>
</script>

View File

@@ -0,0 +1,53 @@
import AdminMenuHandler from 'elementor-admin/admin-menu';
export default class LandingPagesHandler extends AdminMenuHandler {
getDefaultSettings() {
const pageName = 'e-landing-page',
adminMenuSelectors = {
// The escaping is done because jQuery requires it for selectors.
landingPagesTablePage: 'a[href=\"edit.php?post_type=' + pageName + '\"]',
landingPagesAddNewPage: 'a[href=\"edit.php?post_type=elementor_library&page=' + pageName + '\"]',
};
return {
selectors: {
addButton: '.page-title-action:first',
pagesMenuItemAndLink: '#menu-pages, #menu-pages > a',
landingPagesMenuItem: `${ adminMenuSelectors.landingPagesTablePage }, ${ adminMenuSelectors.landingPagesAddNewPage }`,
templatesMenuItem: '.menu-icon-elementor_library',
},
};
}
getDefaultElements() {
const selectors = this.getSettings( 'selectors' ),
elements = super.getDefaultElements();
elements.$landingPagesMenuItem = jQuery( selectors.landingPagesMenuItem );
elements.$templatesMenuItem = jQuery( selectors.templatesMenuItem );
elements.$pagesMenuItemAndLink = jQuery( selectors.pagesMenuItemAndLink );
return elements;
}
onInit() {
super.onInit();
const settings = this.getSettings(),
isLandingPagesTablePage = !! window.location.href.includes( settings.paths.landingPagesTablePage ),
isLandingPagesTrashPage = !! window.location.href.includes( settings.paths.landingPagesTrashPage ),
isLandingPagesCreateYourFirstPage = !! window.location.href.includes( settings.paths.landingPagesAddNewPage );
// If the current page is a Landing Pages Page (the Posts Table page, "Create Your First.." page, or a native
// WordPress dashboard page edit screen when using WordPress' Classic Editor).
if ( isLandingPagesTablePage || isLandingPagesTrashPage || isLandingPagesCreateYourFirstPage || settings.isLandingPageAdminEdit ) {
// Make sure the active admin top level menu item is 'Templates', and not 'Pages'.
this.highlightTopLevelMenuItem( this.elements.$templatesMenuItem, this.elements.$pagesMenuItemAndLink );
this.highlightSubMenuItem( this.elements.$landingPagesMenuItem );
// Overwrite the 'Add New' button at the top of the page to open in Elementor with the library module open.
jQuery( settings.selectors.addButton ).attr( 'href', elementorAdminConfig.urls.addNewLandingPageUrl );
}
}
}

View File

@@ -0,0 +1,29 @@
import LandingPagesHandler from './landing-pages';
export default class extends elementorModules.Module {
constructor() {
super();
elementorCommon.elements.$window.on( 'elementor/admin/init', () => {
this.runHandler();
} );
}
runHandler() {
const pageName = 'e-landing-page',
paths = {
landingPagesTablePage: 'edit.php?post_type=' + pageName,
landingPagesAddNewPage: 'edit.php?post_type=elementor_library&page=' + pageName,
landingPagesTrashPage: 'edit.php?post_status=trash&post_type=' + pageName,
},
args = {
path: elementorAdmin.config.landingPages?.landingPagesHasPages ? paths.landingPagesTablePage : paths.landingPagesAddNewPage,
isLandingPageAdminEdit: elementorAdmin.config.landingPages?.isLandingPageAdminEdit,
paths,
};
// This class modifies elements in the WordPress admin that are rendered "wrong" by the WordPress core
// and could not be modified in the backend.
new LandingPagesHandler( args );
}
}

View File

@@ -0,0 +1,11 @@
import * as hooks from './hooks/';
export default class LandingPageComponent extends $e.modules.ComponentBase {
getNamespace() {
return 'document/landing-page';
}
defaultHooks() {
return this.importHooks( hooks );
}
}

View File

@@ -0,0 +1,2 @@
export { LandingPageAddLibraryTab } from './ui/editor/documents/open/add-landing-pages-tab';
export { LandingPageRemoveLibraryTab } from './ui/editor/documents/close/remove-landing-pages-tab';

View File

@@ -0,0 +1,23 @@
export class LandingPageRemoveLibraryTab extends $e.modules.hookUI.After {
getCommand() {
return 'editor/documents/unload';
}
getId() {
return 'elementor-landing-pages-remove-library-tab';
}
getConditions( args ) {
const { document } = args;
return 'landing-page' === document.config.type;
}
apply() {
$e.components.get( 'library' ).removeTab( 'templates/landing-pages' );
// Pages are replaced by landing pages so when Landing Pages are removed, the Pages have to be re-added.
$e.components.get( 'library' ).addTab( 'templates/pages' );
}
}
export default LandingPageRemoveLibraryTab;

View File

@@ -0,0 +1,29 @@
export class LandingPageAddLibraryTab extends $e.modules.hookUI.After {
getCommand() {
return 'editor/documents/open';
}
getId() {
return 'elementor-landing-pages-add-library-tab';
}
getConditions( args ) {
const document = elementor.documents.get( args.id );
return 'landing-page' === document.config.type;
}
apply() {
$e.components.get( 'library' ).addTab( 'templates/landing-pages', {
title: __( 'Landing Pages', 'elementor' ),
filter: {
source: 'remote',
type: 'lp',
},
}, 2 );
// Pages are replaced by landing pages so they need to be removed.
$e.components.get( 'library' ).removeTab( 'templates/pages' );
}
}
export default LandingPageAddLibraryTab;

View File

@@ -0,0 +1,9 @@
import Component from './component';
class LandingPageLibraryModule extends elementorModules.editor.utils.Module {
onElementorLoaded() {
this.component = $e.components.register( new Component( { manager: this } ) );
}
}
export default LandingPageLibraryModule;

View File

@@ -0,0 +1,90 @@
<?php
namespace Elementor\Modules\LandingPages\Documents;
use Elementor\Core\DocumentTypes\PageBase;
use Elementor\Modules\LandingPages\Module as Landing_Pages_Module;
use Elementor\Modules\Library\Traits\Library;
use Elementor\Modules\PageTemplates\Module as Page_Templates_Module;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Landing_Page extends PageBase {
// Library Document Trait
use Library;
public static function get_properties() {
$properties = parent::get_properties();
$properties['support_kit'] = true;
$properties['show_in_library'] = true;
$properties['cpt'] = [ Landing_Pages_Module::CPT ];
return $properties;
}
/**
* @access public
*/
public function get_name() {
return Landing_Pages_Module::DOCUMENT_TYPE;
}
/**
* @access public
* @static
*/
public static function get_title() {
return __( 'Landing Page', 'elementor' );
}
/**
* Save Document.
*
* Save an Elementor document.
*
* @since 3.1.0
* @access public
*
* @param $data
*
* @return bool
*/
public function save( $data ) {
// This is for the first time a Landing Page is created. It is done in order to load a new Landing Page with
// 'Canvas' as the default page template.
if ( empty( $data['settings']['template'] ) ) {
$data['settings']['template'] = Page_Templates_Module::TEMPLATE_CANVAS;
}
parent::save( $data );
}
/**
* Admin Columns Content
*
* @since 3.1.0
*
* @param $column_name
* @access public
*/
public function admin_columns_content( $column_name ) {
if ( 'elementor_library_type' === $column_name ) {
$this->print_admin_column_type();
}
}
protected function get_remote_library_config() {
$config = [
'type' => 'lp',
'default_route' => 'templates/landing-pages',
'autoImportSettings' => true,
];
return array_replace_recursive( parent::get_remote_library_config(), $config );
}
}

View File

@@ -0,0 +1,481 @@
<?php
namespace Elementor\Modules\LandingPages;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Documents_Manager;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Modules\LandingPages\Documents\Landing_Page;
use Elementor\Modules\LandingPages\Module as Landing_Pages_Module;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const DOCUMENT_TYPE = 'landing-page';
const CPT = 'e-landing-page';
const ADMIN_PAGE_SLUG = 'edit.php?post_type=' . self::CPT;
private $posts;
private $trashed_posts;
private $new_lp_url;
private $permalink_structure;
public function get_name() {
return 'landing-pages';
}
/**
* Get Experimental Data
*
* Implementation of this method makes the module an experiment.
*
* @since 3.1.0
*
* @return array
*/
public static function get_experimental_data() {
return [
'name' => 'landing-pages',
'title' => __( 'Landing Pages', 'elementor' ),
'description' => __( 'Adds a new Elementor content type that allows creating beautiful landing pages instantly in a streamlined workflow.', 'elementor' ),
'release_status' => Experiments_Manager::RELEASE_STATUS_BETA,
'new_site' => [
'default_active' => true,
'minimum_installation_version' => '3.1.0-beta',
],
];
}
/**
* Get Trashed Landing Pages Posts
*
* Returns the posts property of a WP_Query run for Landing Pages with post_status of 'trash'.
*
* @since 3.1.0
*
* @return array trashed posts
*/
private function get_trashed_landing_page_posts() {
if ( $this->trashed_posts ) {
return $this->trashed_posts;
}
// `'posts_per_page' => 1` is because this is only used as an indicator to whether there are any trashed landing pages.
$trashed_posts_query = new \WP_Query( [
'post_type' => self::CPT,
'post_status' => 'trash',
'posts_per_page' => 1,
'meta_key' => '_elementor_template_type',
'meta_value' => self::DOCUMENT_TYPE,
] );
$this->trashed_posts = $trashed_posts_query->posts;
return $this->trashed_posts;
}
/**
* Get Landing Pages Posts
*
* Returns the posts property of a WP_Query run for posts with the Landing Pages CPT.
*
* @since 3.1.0
*
* @return array posts
*/
private function get_landing_page_posts() {
if ( $this->posts ) {
return $this->posts;
}
// `'posts_per_page' => 1` is because this is only used as an indicator to whether there are any landing pages.
$posts_query = new \WP_Query( [
'post_type' => self::CPT,
'post_status' => 'any',
'posts_per_page' => 1,
'meta_key' => '_elementor_template_type',
'meta_value' => self::DOCUMENT_TYPE,
] );
$this->posts = $posts_query->posts;
return $this->posts;
}
/**
* Is Elementor Landing Page.
*
* Check whether the post is an Elementor Landing Page.
*
* @since 3.1.0
* @access public
*
* @param \WP_Post $post Post Object
*
* @return bool Whether the post was built with Elementor.
*/
public function is_elementor_landing_page( $post ) {
return self::CPT === $post->post_type;
}
/**
* Add Submenu Page
*
* Adds the 'Landing Pages' submenu item to the 'Templates' menu item.
*
* @since 3.1.0
*/
private function add_submenu_page() {
$posts = $this->get_landing_page_posts();
// If there are no Landing Pages, show the "Create Your First Landing Page" page.
// If there are, show the pages table.
if ( ! empty( $posts ) ) {
$landing_page_menu_slug = self::ADMIN_PAGE_SLUG;
$landing_page_menu_callback = null;
} else {
$landing_page_menu_slug = self::CPT;
$landing_page_menu_callback = [ $this, 'print_empty_landing_pages_page' ];
}
$landing_pages_title = __( 'Landing Pages', 'elementor' );
add_submenu_page(
Source_Local::ADMIN_MENU_SLUG,
$landing_pages_title,
$landing_pages_title,
'manage_options',
$landing_page_menu_slug,
$landing_page_menu_callback
);
}
/**
* Get 'Add New' Landing Page URL
*
* Retrieves the custom URL for the admin dashboard's 'Add New' button in the Landing Pages admin screen. This URL
* creates a new Landing Pages and directly opens the Elementor Editor with the Template Library modal open on the
* Landing Pages tab.
*
* @since 3.1.0
*
* @return string
*/
private function get_add_new_landing_page_url() {
if ( ! $this->new_lp_url ) {
$this->new_lp_url = Utils::get_create_new_post_url( self::CPT, self::DOCUMENT_TYPE ) . '#library';
}
return $this->new_lp_url;
}
/**
* Get Empty Landing Pages Page
*
* Prints the HTML content of the page that is displayed when there are no existing landing pages in the DB.
* Added as the callback to add_submenu_page.
*
* @since 3.1.0
*/
public function print_empty_landing_pages_page() {
$template_sources = Plugin::$instance->templates_manager->get_registered_sources();
$source_local = $template_sources['local'];
$trashed_posts = $this->get_trashed_landing_page_posts();
?>
<div class="e-landing-pages-empty">
<?php
/** @var Source_Local $source_local */
$source_local->print_blank_state_template( __( 'Landing Page', 'elementor' ), $this->get_add_new_landing_page_url(), __( 'Build Effective Landing Pages for your business\' marketing campaigns.', 'elementor' ) );
if ( ! empty( $trashed_posts ) ) : ?>
<div class="e-trashed-items">
<?php echo sprintf( __( 'Or view <a href="%s">Trashed Items</a>', 'elementor' ), admin_url( 'edit.php?post_status=trash&post_type=' . self::CPT ) ); ?>
</div>
<?php endif; ?>
</div>
<?php
}
/**
* Is Current Admin Page Edit LP
*
* Checks whether the current page is a native WordPress edit page for a landing page.
*/
private function is_landing_page_admin_edit() {
$screen = get_current_screen();
if ( 'post' === $screen->base ) {
return $this->is_elementor_landing_page( get_post() );
}
return false;
}
/**
* Admin Localize Settings
*
* Enables adding properties to the globally available elementorAdmin.config JS object in the Admin Dashboard.
* Runs on the 'elementor/admin/localize_settings' filter.
*
* @since 3.1.0
*
* @param $settings
* @return array|null
*/
private function admin_localize_settings( $settings ) {
$additional_settings = [
'urls' => [
'addNewLandingPageUrl' => $this->get_add_new_landing_page_url(),
],
'landingPages' => [
'landingPagesHasPages' => [] !== $this->get_landing_page_posts(),
'isLandingPageAdminEdit' => $this->is_landing_page_admin_edit(),
],
];
return array_replace_recursive( $settings, $additional_settings );
}
/**
* Register Landing Pages CPT
*
* @since 3.1.0
*/
private function register_landing_page_cpt() {
$labels = [
'name' => __( 'Landing Pages', 'elementor' ),
'singular_name' => __( 'Landing Page', 'elementor' ),
'add_new' => __( 'Add New', 'elementor' ),
'add_new_item' => __( 'Add New Landing Page', 'elementor' ),
'edit_item' => __( 'Edit Landing Page', 'elementor' ),
'new_item' => __( 'New Landing Page', 'elementor' ),
'all_items' => __( 'All Landing Pages', 'elementor' ),
'view_item' => __( 'View Landing Page', 'elementor' ),
'search_items' => __( 'Search Landing Pages', 'elementor' ),
'not_found' => __( 'No landing pages found', 'elementor' ),
'not_found_in_trash' => __( 'No landing pages found in trash', 'elementor' ),
'parent_item_colon' => '',
'menu_name' => __( 'Landing Pages', 'elementor' ),
];
$args = [
'labels' => $labels,
'public' => true,
'show_in_menu' => 'edit.php?post_type=elementor_library&tabs_group=library',
'capability_type' => 'page',
'taxonomies' => [ Source_Local::TAXONOMY_TYPE_SLUG ],
'supports' => [ 'title', 'editor', 'comments', 'revisions', 'trackbacks', 'author', 'excerpt', 'page-attributes', 'thumbnail', 'custom-fields', 'post-formats', 'elementor' ],
];
register_post_type( self::CPT, $args );
}
/**
* Remove Post Type Slug
*
* Landing Pages are supposed to act exactly like pages. This includes their URLs being directly under the site's
* domain name. Since "Landing Pages" is a CPT, WordPress automatically adds the landing page slug as a prefix to
* it's posts' permalinks. This method checks if the post's post type is Landing Pages, and if it is, it removes
* the CPT slug from the requested post URL.
*
* Runs on the 'post_type_link' filter.
*
* @since 3.1.0
*
* @param $post_link
* @param $post
* @param $leavename
* @return string|string[]
*/
private function remove_post_type_slug( $post_link, $post, $leavename ) {
// Only try to modify the permalink if the post is a Landing Page.
if ( self::CPT !== $post->post_type || 'publish' !== $post->post_status ) {
return $post_link;
}
// Any slug prefixes need to be removed from the post link.
return get_home_url() . '/' . $post->post_name . '/';
}
/**
* Adjust Landing Page Query
*
* Since Landing Pages are a CPT but should act like pages, the WP_Query that is used to fetch the page from the
* database needs to be adjusted. This method adds the Landing Pages CPT to the list of queried post types, to
* make sure the database query finds the correct Landing Page to display.
* Runs on the 'pre_get_posts' action.
*
* @since 3.1.0
*
* @param \WP_Query $query
*/
private function adjust_landing_page_query( \WP_Query $query ) {
// Only handle actual pages.
if (
! $query->is_main_query()
// If the query is not for a page.
|| ! isset( $query->query['page'] )
// If the query is for a static home/blog page.
|| is_home()
// If the post type comes already set, the main query is probably a custom one made by another plugin.
// In this case we do not want to intervene in order to not cause a conflict.
|| isset( $query->query['post_type'] )
) {
return;
}
// Create the post types property as an array and include the landing pages CPT in it.
$query_post_types = [ 'post', 'page', self::CPT ];
// Since WordPress determined this is supposed to be a page, we'll pre-set the post_type query arg to make sure
// it includes the Landing Page CPT, so when the query is parsed, our CPT will be a legitimate match to the
// Landing Page's permalink (that is directly under the domain, without a CPT slug prefix). In some cases,
// The 'name' property will be set, and in others it is the 'pagename', so we have to cover both cases.
if ( ! empty( $query->query['name'] ) ) {
$query->set( 'post_type', $query_post_types );
} elseif ( ! empty( $query->query['pagename'] ) && false === strpos( $query->query['pagename'], '/' ) ) {
$query->set( 'post_type', $query_post_types );
// We also need to set the name query var since redirect_guess_404_permalink() relies on it.
$query->set( 'name', $query->query['pagename'] );
}
}
/**
* Handle 404
*
* This method runs after a page is not found in the database, but before a page is returned as a 404.
* These cases are handled in this filter callback, that runs on the 'pre_handle_404' filter.
*
* In some cases (such as when a site uses custom permalink structures), WordPress's WP_Query does not identify a
* Landing Page's URL as a post belonging to the Landing Page CPT. Some cases are handled successfully by the
* adjust_landing_page_query() method, but some are not and still trigger a 404 process. This method handles such
* cases by overriding the $wp_query global to fetch the correct landing page post entry.
*
* For example, since Landing Pages slugs come directly after the site domain name, WP_Query might parse the post
* as a category page. Since there is no category matching the slug, it triggers a 404 process. In this case, we
* run a query for a Landing Page post with the passed slug ($query->query['category_name']. If a Landing Page
* with the passed slug is found, we override the global $wp_query with the new, correct query.
*
* @param $current_value
* @param $query
* @return false
*/
private function handle_404( $current_value, $query ) {
global $wp_query;
// If another plugin/theme already used this filter, exit here to avoid conflicts.
if ( $current_value ) {
return $current_value;
}
if (
// Make sure we only intervene in the main query.
! $query->is_main_query()
// If a post was found, this is not a 404 case, so do not intervene.
|| ! empty( $query->posts )
// This filter is only meant to deal with wrong queries where the only query var is 'category_name'.
// If there is no 'category_name' query var, do not intervene.
|| empty( $query->query['category_name'] )
// If the query is for a real taxonomy (determined by it including a table to search in, such as the
// wp_term_relationships table), do not intervene.
|| ! empty( $query->tax_query->table_aliases )
) {
return false;
}
// Search for a Landing Page with the same name passed as the 'category name'.
$possible_new_query = new \WP_Query( [
'post_type' => self::CPT,
'name' => $query->query['category_name'],
] );
// Only if such a Landing Page is found, override the query to fetch the correct page.
if ( ! empty( $possible_new_query->posts ) ) {
$wp_query = $possible_new_query; //phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
}
return false;
}
public function __construct() {
$this->permalink_structure = get_option( 'permalink_structure' );
$this->register_landing_page_cpt();
// If there is a permalink structure set to the site, run the hooks that modify the Landing Pages permalinks to
// match WordPress' native 'Pages' post type.
if ( '' !== $this->permalink_structure ) {
// Landing Pages' post link needs to be modified to be identical to the pages permalink structure. This
// needs to happen in both the admin and the front end, since post links are also used in the admin pages.
add_filter( 'post_type_link', function( $post_link, $post, $leavename ) {
return $this->remove_post_type_slug( $post_link, $post, $leavename );
}, 10, 3 );
// The query itself only has to be manipulated when pages are viewed in the front end.
if ( ! is_admin() || wp_doing_ajax() ) {
add_action( 'pre_get_posts', function ( $query ) {
$this->adjust_landing_page_query( $query );
} );
// Handle cases where visiting a Landing Page's URL returns 404.
add_filter( 'pre_handle_404', function ( $value, $query ) {
return $this->handle_404( $value, $query );
}, 10, 2 );
}
}
add_action( 'elementor/documents/register', function( Documents_Manager $documents_manager ) {
$documents_manager->register_document_type( self::DOCUMENT_TYPE, Landing_Page::get_class_full_name() );
} );
add_action( 'admin_menu', function() {
$this->add_submenu_page();
}, 30 );
// Add the custom 'Add New' link for Landing Pages into Elementor's admin config.
add_action( 'elementor/admin/localize_settings', function( array $settings ) {
return $this->admin_localize_settings( $settings );
} );
add_filter( 'elementor/template_library/sources/local/register_taxonomy_cpts', function( array $cpts ) {
$cpts[] = self::CPT;
return $cpts;
} );
// In the Landing Pages Admin Table page - Overwrite Template type column header title.
add_action( 'manage_' . Landing_Pages_Module::CPT . '_posts_columns', function( $posts_columns ) {
/** @var Source_Local $source_local */
$source_local = Plugin::$instance->templates_manager->get_source( 'local' );
return $source_local->admin_columns_headers( $posts_columns );
} );
// In the Landing Pages Admin Table page - Overwrite Template type column row values.
add_action( 'manage_' . Landing_Pages_Module::CPT . '_posts_custom_column', function( $column_name, $post_id ) {
/** @var Landing_Page $document */
$document = Plugin::$instance->documents->get( $post_id );
$document->admin_columns_content( $column_name );
}, 10, 2 );
// Overwrite the Admin Bar's 'New +' Landing Page URL with the link that creates the new LP in Elementor
// with the Template Library modal open.
add_action( 'admin_bar_menu', function( $admin_bar ) {
// Get the Landing Page menu node.
$new_landing_page_node = $admin_bar->get_node( 'new-e-landing-page' );
if ( $new_landing_page_node ) {
$new_landing_page_node->href = $this->get_add_new_landing_page_url();
$admin_bar->add_node( $new_landing_page_node );
}
}, 100 );
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Elementor\Modules\Library\Documents;
use Elementor\Core\Base\Document;
use Elementor\Modules\Library\Traits\Library;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor library document.
*
* Elementor library document handler class is responsible for handling
* a document of the library type.
*
* @since 2.0.0
*/
abstract class Library_Document extends Document {
// Library Document Trait
use Library;
/**
* The taxonomy type slug for the library document.
*/
const TAXONOMY_TYPE_SLUG = 'elementor_library_type';
/**
* Get document properties.
*
* Retrieve the document properties.
*
* @since 2.0.0
* @access public
* @static
*
* @return array Document properties.
*/
public static function get_properties() {
$properties = parent::get_properties();
$properties['admin_tab_group'] = 'library';
$properties['show_in_library'] = true;
$properties['register_type'] = true;
return $properties;
}
/**
* Get initial config.
*
* Retrieve the current element initial configuration.
*
* Adds more configuration on top of the controls list and the tabs assigned
* to the control. This method also adds element name, type, icon and more.
*
* @since 2.9.0
* @access protected
*
* @return array The initial config.
*/
public function get_initial_config() {
$config = parent::get_initial_config();
$config['library'] = [
'save_as_same_type' => true,
];
return $config;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Elementor\Modules\Library\Documents;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor section library document.
*
* Elementor section library document handler class is responsible for
* handling a document of a section type.
*
*/
class Not_Supported extends Library_Document {
/**
* Get document properties.
*
* Retrieve the document properties.
*
* @access public
* @static
*
* @return array Document properties.
*/
public static function get_properties() {
$properties = parent::get_properties();
$properties['admin_tab_group'] = '';
$properties['register_type'] = false;
$properties['is_editable'] = false;
$properties['show_in_library'] = false;
$properties['cpt'] = [
Source_Local::CPT,
];
return $properties;
}
/**
* Get document name.
*
* Retrieve the document name.
*
* @access public
*
* @return string Document name.
*/
public function get_name() {
return 'not-supported';
}
/**
* Get document title.
*
* Retrieve the document title.
*
* @access public
* @static
*
* @return string Document title.
*/
public static function get_title() {
return __( 'Not Supported', 'elementor' );
}
public function save_template_type() {
// Do nothing.
}
public function print_admin_column_type() {
echo self::get_title();
}
public function filter_admin_row_actions( $actions ) {
unset( $actions['view'] );
return $actions;
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Elementor\Modules\Library\Documents;
use Elementor\Core\DocumentTypes\Post;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor page library document.
*
* Elementor page library document handler class is responsible for
* handling a document of a page type.
*
* @since 2.0.0
*/
class Page extends Library_Document {
/**
* Get document properties.
*
* Retrieve the document properties.
*
* @since 2.0.0
* @access public
* @static
*
* @return array Document properties.
*/
public static function get_properties() {
$properties = parent::get_properties();
$properties['support_wp_page_templates'] = true;
$properties['support_kit'] = true;
return $properties;
}
/**
* Get document name.
*
* Retrieve the document name.
*
* @since 2.0.0
* @access public
*
* @return string Document name.
*/
public function get_name() {
return 'page';
}
/**
* Get document title.
*
* Retrieve the document title.
*
* @since 2.0.0
* @access public
* @static
*
* @return string Document title.
*/
public static function get_title() {
return __( 'Page', 'elementor' );
}
/**
* @since 2.1.3
* @access public
*/
public function get_css_wrapper_selector() {
return 'body.elementor-page-' . $this->get_main_id();
}
/**
* @since 3.1.0
* @access protected
*/
protected function register_controls() {
parent::register_controls();
Post::register_hide_title_control( $this );
Post::register_style_controls( $this );
}
protected function get_remote_library_config() {
$config = parent::get_remote_library_config();
$config['type'] = 'page';
$config['default_route'] = 'templates/pages';
return $config;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Elementor\Modules\Library\Documents;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor section library document.
*
* Elementor section library document handler class is responsible for
* handling a document of a section type.
*
* @since 2.0.0
*/
class Section extends Library_Document {
public static function get_properties() {
$properties = parent::get_properties();
$properties['support_kit'] = true;
return $properties;
}
/**
* Get document name.
*
* Retrieve the document name.
*
* @since 2.0.0
* @access public
*
* @return string Document name.
*/
public function get_name() {
return 'section';
}
/**
* Get document title.
*
* Retrieve the document title.
*
* @since 2.0.0
* @access public
* @static
*
* @return string Document title.
*/
public static function get_title() {
return __( 'Section', 'elementor' );
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Elementor\Modules\Library;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Modules\Library\Documents;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor library module.
*
* Elementor library module handler class is responsible for registering and
* managing Elementor library modules.
*
* @since 2.0.0
*/
class Module extends BaseModule {
/**
* Get module name.
*
* Retrieve the library module name.
*
* @since 2.0.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'library';
}
/**
* Library module constructor.
*
* Initializing Elementor library module.
*
* @since 2.0.0
* @access public
*/
public function __construct() {
Plugin::$instance->documents
->register_document_type( 'not-supported', Documents\Not_Supported::get_class_full_name() )
->register_document_type( 'page', Documents\Page::get_class_full_name() )
->register_document_type( 'section', Documents\Section::get_class_full_name() );
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Elementor\Modules\Library\Traits;
use Elementor\TemplateLibrary\Source_Local;
/**
* Elementor Library Trait
*
* This trait is used by all Library Documents and Landing Pages.
*
* @since 3.1.0
*/
trait Library {
/**
* Print Admin Column Type
*
* Runs on WordPress' 'manage_{custom post type}_posts_custom_column' hook to modify each row's content.
*
* @since 3.1.0
* @access public
*/
public function print_admin_column_type() {
$admin_filter_url = admin_url( Source_Local::ADMIN_MENU_SLUG . '&elementor_library_type=' . $this->get_name() );
printf( '<a href="%s">%s</a>', $admin_filter_url, $this->get_title() );
}
/**
* Save document type.
*
* Set new/updated document type.
*
* @since 3.1.0
* @access public
*/
public function save_template_type() {
parent::save_template_type();
wp_set_object_terms( $this->post->ID, $this->get_name(), Source_Local::TAXONOMY_TYPE_SLUG );
}
}

View File

@@ -0,0 +1,424 @@
<?php
namespace Elementor\Modules\PageTemplates;
use Elementor\Controls_Manager;
use Elementor\Core\Base\Document;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Kits\Documents\Kit;
use Elementor\Plugin;
use Elementor\Utils;
use Elementor\Core\DocumentTypes\PageBase as PageBase;
use Elementor\Modules\Library\Documents\Page as LibraryPageDocument;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor page templates module.
*
* Elementor page templates module handler class is responsible for registering
* and managing Elementor page templates modules.
*
* @since 2.0.0
*/
class Module extends BaseModule {
/**
* The of the theme.
*/
const TEMPLATE_THEME = 'elementor_theme';
/**
* Elementor Canvas template name.
*/
const TEMPLATE_CANVAS = 'elementor_canvas';
/**
* Elementor Header & Footer template name.
*/
const TEMPLATE_HEADER_FOOTER = 'elementor_header_footer';
/**
* Print callback.
*
* Holds the page template callback content.
*
* @since 2.0.0
* @access protected
*
* @var callable
*/
protected $print_callback;
/**
* Get module name.
*
* Retrieve the page templates module name.
*
* @since 2.0.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'page-templates';
}
/**
* Template include.
*
* Update the path for the Elementor Canvas template.
*
* Fired by `template_include` filter.
*
* @since 2.0.0
* @access public
*
* @param string $template The path of the template to include.
*
* @return string The path of the template to include.
*/
public function template_include( $template ) {
if ( is_singular() ) {
$document = Plugin::$instance->documents->get_doc_for_frontend( get_the_ID() );
if ( $document && $document::get_property( 'support_wp_page_templates' ) ) {
$page_template = $document->get_meta( '_wp_page_template' );
$template_path = $this->get_template_path( $page_template );
if ( self::TEMPLATE_THEME !== $page_template && ! $template_path && $document->is_built_with_elementor() ) {
$kit_default_template = Plugin::$instance->kits_manager->get_current_settings( 'default_page_template' );
$template_path = $this->get_template_path( $kit_default_template );
}
if ( $template_path ) {
$template = $template_path;
Plugin::$instance->inspector->add_log( 'Page Template', Plugin::$instance->inspector->parse_template_path( $template ), $document->get_edit_url() );
}
}
}
return $template;
}
/**
* Add WordPress templates.
*
* Adds Elementor templates to all the post types that support
* Elementor.
*
* Fired by `init` action.
*
* @since 2.0.0
* @access public
*/
public function add_wp_templates_support() {
$post_types = get_post_types_by_support( 'elementor' );
foreach ( $post_types as $post_type ) {
add_filter( "theme_{$post_type}_templates", [ $this, 'add_page_templates' ], 10, 4 );
}
}
/**
* Add page templates.
*
* Add the Elementor page templates to the theme templates.
*
* Fired by `theme_{$post_type}_templates` filter.
*
* @since 2.0.0
* @access public
* @static
*
* @param array $page_templates Array of page templates. Keys are filenames,
* checks are translated names.
*
* @param \WP_Theme $wp_theme
* @param \WP_Post $post
*
* @return array Page templates.
*/
public function add_page_templates( $page_templates, $wp_theme, $post ) {
if ( $post ) {
// FIX ME: Gutenberg not send $post as WP_Post object, just the post ID.
$post_id = ! empty( $post->ID ) ? $post->ID : $post;
$document = Plugin::$instance->documents->get( $post_id );
if ( $document && ! $document::get_property( 'support_wp_page_templates' ) ) {
return $page_templates;
}
}
$page_templates = [
self::TEMPLATE_CANVAS => _x( 'Elementor Canvas', 'Page Template', 'elementor' ),
self::TEMPLATE_HEADER_FOOTER => _x( 'Elementor Full Width', 'Page Template', 'elementor' ),
self::TEMPLATE_THEME => _x( 'Theme', 'Page Template', 'elementor' ),
] + $page_templates;
return $page_templates;
}
/**
* Set print callback.
*
* Set the page template callback.
*
* @since 2.0.0
* @access public
*
* @param callable $callback
*/
public function set_print_callback( $callback ) {
$this->print_callback = $callback;
}
/**
* Print callback.
*
* Prints the page template content using WordPress loop.
*
* @since 2.0.0
* @access public
*/
public function print_callback() {
while ( have_posts() ) :
the_post();
the_content();
endwhile;
}
/**
* Print content.
*
* Prints the page template content.
*
* @since 2.0.0
* @access public
*/
public function print_content() {
if ( ! $this->print_callback ) {
$this->print_callback = [ $this, 'print_callback' ];
}
call_user_func( $this->print_callback );
}
/**
* Get page template path.
*
* Retrieve the path for any given page template.
*
* @since 2.0.0
* @access public
*
* @param string $page_template The page template name.
*
* @return string Page template path.
*/
public function get_template_path( $page_template ) {
$template_path = '';
switch ( $page_template ) {
case self::TEMPLATE_CANVAS:
$template_path = __DIR__ . '/templates/canvas.php';
break;
case self::TEMPLATE_HEADER_FOOTER:
$template_path = __DIR__ . '/templates/header-footer.php';
break;
}
return $template_path;
}
/**
* Register template control.
*
* Adds custom controls to any given document.
*
* Fired by `update_post_metadata` action.
*
* @since 2.0.0
* @access public
*
* @param Document $document The document instance.
*/
public function action_register_template_control( $document ) {
if ( $document instanceof PageBase || $document instanceof LibraryPageDocument ) {
$this->register_template_control( $document );
}
}
/**
* Register template control.
*
* Adds custom controls to any given document.
*
* @since 2.0.0
* @access public
*
* @param Document $document The document instance.
* @param string $control_id Optional. The control ID. Default is `template`.
*/
public function register_template_control( $document, $control_id = 'template' ) {
if ( ! Utils::is_cpt_custom_templates_supported() ) {
return;
}
require_once ABSPATH . '/wp-admin/includes/template.php';
$document->start_injection( [
'of' => 'post_status',
'fallback' => [
'of' => 'post_title',
],
] );
$control_options = [
'options' => array_flip( get_page_templates( null, $document->get_main_post()->post_type ) ),
];
$this->add_template_controls( $document, $control_id, $control_options );
$document->end_injection();
}
// The $options variable is an array of $control_options to overwrite the default
public function add_template_controls( Document $document, $control_id, $control_options ) {
// Default Control Options
$default_control_options = [
'label' => __( 'Page Layout', 'elementor' ),
'type' => Controls_Manager::SELECT,
'default' => 'default',
'options' => [
'default' => __( 'Default', 'elementor' ),
],
];
$control_options = array_replace_recursive( $default_control_options, $control_options );
$document->add_control(
$control_id,
$control_options
);
$document->add_control(
$control_id . '_default_description',
[
'type' => Controls_Manager::RAW_HTML,
'raw' => '<b>' . __( 'Default Page Template from your theme', 'elementor' ) . '</b>',
'content_classes' => 'elementor-descriptor',
'condition' => [
$control_id => 'default',
],
]
);
$document->add_control(
$control_id . '_canvas_description',
[
'type' => Controls_Manager::RAW_HTML,
'raw' => '<b>' . __( 'No header, no footer, just Elementor', 'elementor' ) . '</b>',
'content_classes' => 'elementor-descriptor',
'condition' => [
$control_id => self::TEMPLATE_CANVAS,
],
]
);
$document->add_control(
$control_id . '_header_footer_description',
[
'type' => Controls_Manager::RAW_HTML,
'raw' => '<b>' . __( 'This template includes the header, full-width content and footer', 'elementor' ) . '</b>',
'content_classes' => 'elementor-descriptor',
'condition' => [
$control_id => self::TEMPLATE_HEADER_FOOTER,
],
]
);
if ( $document instanceof Kit ) {
$document->add_control(
'reload_preview_description',
[
'type' => Controls_Manager::RAW_HTML,
'raw' => __( 'Changes will be reflected in the preview only after the page reloads.', 'elementor' ),
'content_classes' => 'elementor-descriptor',
]
);
}
}
/**
* Filter metadata update.
*
* Filters whether to update metadata of a specific type.
*
* Elementor don't allow WordPress to update the parent page template
* during `wp_update_post`.
*
* Fired by `update_{$meta_type}_metadata` filter.
*
* @since 2.0.0
* @access public
*
* @param bool $check Whether to allow updating metadata for the given type.
* @param int $object_id Object ID.
* @param string $meta_key Meta key.
*
* @return bool Whether to allow updating metadata of a specific type.
*/
public function filter_update_meta( $check, $object_id, $meta_key ) {
if ( '_wp_page_template' === $meta_key && Plugin::$instance->common ) {
/** @var \Elementor\Core\Common\Modules\Ajax\Module $ajax */
$ajax = Plugin::$instance->common->get_component( 'ajax' );
$ajax_data = $ajax->get_current_action_data();
$is_autosave_action = $ajax_data && 'save_builder' === $ajax_data['action'] && Document::STATUS_AUTOSAVE === $ajax_data['data']['status'];
// Don't allow WP to update the parent page template.
// (during `wp_update_post` from page-settings or save_plain_text).
if ( $is_autosave_action && ! wp_is_post_autosave( $object_id ) && Document::STATUS_DRAFT !== get_post_status( $object_id ) ) {
$check = false;
}
}
return $check;
}
/**
* Support `wp_body_open` action, available since WordPress 5.2.
*
* @since 2.7.0
* @access public
*/
public static function body_open() {
if ( function_exists( 'wp_body_open' ) ) {
wp_body_open();
} else {
do_action( 'wp_body_open' );
}
}
/**
* Page templates module constructor.
*
* Initializing Elementor page templates module.
*
* @since 2.0.0
* @access public
*/
public function __construct() {
add_action( 'init', [ $this, 'add_wp_templates_support' ] );
add_filter( 'template_include', [ $this, 'template_include' ], 11 /* After Plugins/WooCommerce */ );
add_action( 'elementor/documents/register_controls', [ $this, 'action_register_template_control' ] );
add_filter( 'update_post_metadata', [ $this, 'filter_update_meta' ], 10, 3 );
}
}

View File

@@ -0,0 +1,50 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
\Elementor\Plugin::$instance->frontend->add_body_class( 'elementor-template-canvas' );
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<?php if ( ! current_theme_supports( 'title-tag' ) ) : ?>
<title><?php echo wp_get_document_title(); ?></title>
<?php endif; ?>
<?php wp_head(); ?>
<?php
// Keep the following line after `wp_head()` call, to ensure it's not overridden by another templates.
echo \Elementor\Utils::get_meta_viewport( 'canvas' );
?>
</head>
<body <?php body_class(); ?>>
<?php
Elementor\Modules\PageTemplates\Module::body_open();
/**
* Before canvas page template content.
*
* Fires before the content of Elementor canvas page template.
*
* @since 1.0.0
*/
do_action( 'elementor/page_templates/canvas/before_content' );
\Elementor\Plugin::$instance->modules_manager->get_modules( 'page-templates' )->print_content();
/**
* After canvas page template content.
*
* Fires after the content of Elementor canvas page template.
*
* @since 1.0.0
*/
do_action( 'elementor/page_templates/canvas/after_content' );
wp_footer();
?>
</body>
</html>

View File

@@ -0,0 +1,30 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
\Elementor\Plugin::$instance->frontend->add_body_class( 'elementor-template-full-width' );
get_header();
/**
* Before Header-Footer page template content.
*
* Fires before the content of Elementor Header-Footer page template.
*
* @since 2.0.0
*/
do_action( 'elementor/page_templates/header-footer/before_content' );
\Elementor\Plugin::$instance->modules_manager->get_modules( 'page-templates' )->print_content();
/**
* After Header-Footer page template content.
*
* Fires after the content of Elementor Header-Footer page template.
*
* @since 2.0.0
*/
do_action( 'elementor/page_templates/header-footer/after_content' );
get_footer();

View File

@@ -0,0 +1,545 @@
<?php
namespace Elementor\Modules\SafeMode;
use Elementor\Plugin;
use Elementor\Settings;
use Elementor\Tools;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Module extends \Elementor\Core\Base\Module {
const OPTION_ENABLED = 'elementor_safe_mode';
const OPTION_TOKEN = self::OPTION_ENABLED . '_token';
const MU_PLUGIN_FILE_NAME = 'elementor-safe-mode.php';
const DOCS_HELPED_URL = 'https://go.elementor.com/safe-mode-helped/';
const DOCS_DIDNT_HELP_URL = 'https://go.elementor.com/safe-mode-didnt-helped/';
const DOCS_MU_PLUGINS_URL = 'https://go.elementor.com/safe-mode-mu-plugins/';
const DOCS_TRY_SAFE_MODE_URL = 'https://go.elementor.com/safe-mode/';
const EDITOR_NOTICE_TIMEOUT = 30000; /* ms */
public function get_name() {
return 'safe-mode';
}
public function register_ajax_actions( Ajax $ajax ) {
$ajax->register_ajax_action( 'enable_safe_mode', [ $this, 'ajax_enable_safe_mode' ] );
$ajax->register_ajax_action( 'disable_safe_mode', [ $this, 'disable_safe_mode' ] );
}
/**
* @param Tools $tools_page
*/
public function add_admin_button( $tools_page ) {
$tools_page->add_fields( Settings::TAB_GENERAL, 'tools', [
'safe_mode' => [
'label' => __( 'Safe Mode', 'elementor' ),
'field_args' => [
'type' => 'select',
'std' => $this->is_enabled(),
'options' => [
'' => __( 'Disable', 'elementor' ),
'global' => __( 'Enable', 'elementor' ),
],
'desc' => __( 'Safe Mode allows you to troubleshoot issues by only loading the editor, without loading the theme or any other plugin.', 'elementor' ),
],
],
] );
}
public function on_update_safe_mode( $value ) {
if ( 'yes' === $value || 'global' === $value ) {
$this->enable_safe_mode();
} else {
$this->disable_safe_mode();
}
return $value;
}
public function ajax_enable_safe_mode( $data ) {
// It will run `$this->>update_safe_mode`.
update_option( 'elementor_safe_mode', 'yes' );
$document = Plugin::$instance->documents->get( $data['editor_post_id'] );
if ( $document ) {
return add_query_arg( 'elementor-mode', 'safe', $document->get_edit_url() );
}
return false;
}
public function enable_safe_mode() {
if ( ! current_user_can( 'install_plugins' ) ) {
return;
}
WP_Filesystem();
$this->update_allowed_plugins();
if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
wp_mkdir_p( WPMU_PLUGIN_DIR );
add_option( 'elementor_safe_mode_created_mu_dir', true );
}
if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
wp_die( __( 'Cannot enable Safe Mode', 'elementor' ) );
}
$results = copy_dir( __DIR__ . '/mu-plugin/', WPMU_PLUGIN_DIR );
if ( is_wp_error( $results ) ) {
return;
}
$token = md5( wp_rand() );
// Only who own this key can use 'elementor-safe-mode'.
update_option( self::OPTION_TOKEN, $token );
// Save for later use.
setcookie( self::OPTION_TOKEN, $token, time() + HOUR_IN_SECONDS, COOKIEPATH );
}
public function disable_safe_mode() {
if ( ! current_user_can( 'install_plugins' ) ) {
return;
}
$file_path = WP_CONTENT_DIR . '/mu-plugins/elementor-safe-mode.php';
if ( file_exists( $file_path ) ) {
unlink( $file_path );
}
if ( get_option( 'elementor_safe_mode_created_mu_dir' ) ) {
// It will be removed only if it's empty and don't have other mu-plugins.
@rmdir( WPMU_PLUGIN_DIR );
}
delete_option( 'elementor_safe_mode' );
delete_option( 'elementor_safe_mode_allowed_plugins' );
delete_option( 'theme_mods_elementor-safe' );
delete_option( 'elementor_safe_mode_created_mu_dir' );
delete_option( self::OPTION_TOKEN );
setcookie( self::OPTION_TOKEN, '', 1 );
}
public function filter_preview_url( $url ) {
return add_query_arg( 'elementor-mode', 'safe', $url );
}
public function filter_template() {
return ELEMENTOR_PATH . 'modules/page-templates/templates/canvas.php';
}
public function print_safe_mode_css() {
?>
<style>
.elementor-safe-mode-toast {
position: absolute;
z-index: 10000; /* Over the loading layer */
bottom: 10px;
width: 400px;
line-height: 30px;
background: white;
padding: 20px 25px 25px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
border-radius: 5px;
font-family: Roboto, Arial, Helvetica, Verdana, sans-serif;
}
body.rtl .elementor-safe-mode-toast {
left: 10px;
}
body:not(.rtl) .elementor-safe-mode-toast {
right: 10px;
}
#elementor-try-safe-mode {
display: none;
}
.elementor-safe-mode-toast .elementor-toast-content {
font-size: 13px;
line-height: 22px;
color: #6D7882;
}
.elementor-safe-mode-toast .elementor-toast-content a {
color: #138FFF;
}
.elementor-safe-mode-toast .elementor-toast-content hr {
margin: 15px auto;
border: 0 none;
border-top: 1px solid #F1F3F5;
}
.elementor-safe-mode-toast header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 20px;
}
.elementor-safe-mode-toast header > * {
margin-top: 10px;
}
.elementor-safe-mode-toast .elementor-safe-mode-button {
display: inline-block;
font-weight: 500;
font-size: 11px;
text-transform: uppercase;
color: white;
padding: 10px 15px;
line-height: 1;
background: #A4AFB7;
border-radius: 3px;
}
#elementor-try-safe-mode .elementor-safe-mode-button {
background: #39B54A;
}
.elementor-safe-mode-toast header i {
font-size: 25px;
color: #fcb92c;
}
body:not(.rtl) .elementor-safe-mode-toast header i {
margin-right: 10px;
}
body.rtl .elementor-safe-mode-toast header i {
margin-left: 10px;
}
.elementor-safe-mode-toast header h2 {
flex-grow: 1;
font-size: 18px;
color: #6D7882;
}
.elementor-safe-mode-list-item {
margin-top: 10px;
list-style: outside;
}
body:not(.rtl) .elementor-safe-mode-list-item {
margin-left: 15px;
}
body.rtl .elementor-safe-mode-list-item {
margin-right: 15px;
}
.elementor-safe-mode-list-item b {
font-size: 14px;
}
.elementor-safe-mode-list-item-content {
font-style: italic;
color: #a4afb7;
}
.elementor-safe-mode-list-item-title {
font-weight: 500;
}
.elementor-safe-mode-mu-plugins {
background-color: #f1f3f5;
margin-top: 20px;
padding: 10px 15px;
}
</style>
<?php
}
public function print_safe_mode_notice() {
echo $this->print_safe_mode_css();
?>
<div class="elementor-safe-mode-toast" id="elementor-safe-mode-message">
<header>
<i class="eicon-warning"></i>
<h2><?php echo __( 'Safe Mode ON', 'elementor' ); ?></h2>
<a class="elementor-safe-mode-button elementor-disable-safe-mode" target="_blank" href="<?php echo $this->get_admin_page_url(); ?>">
<?php echo __( 'Disable Safe Mode', 'elementor' ); ?>
</a>
</header>
<div class="elementor-toast-content">
<ul class="elementor-safe-mode-list">
<li class="elementor-safe-mode-list-item">
<div class="elementor-safe-mode-list-item-title"><?php echo __( 'Editor successfully loaded?', 'elementor' ); ?></div>
<div class="elementor-safe-mode-list-item-content"><?php echo __( 'The issue was probably caused by one of your plugins or theme.', 'elementor' ); ?> <?php printf( __( '<a href="%s" target="_blank">Click here</a> to troubleshoot', 'elementor' ), self::DOCS_HELPED_URL ); ?></div>
</li>
<li class="elementor-safe-mode-list-item">
<div class="elementor-safe-mode-list-item-title"><?php echo __( 'Still experiencing issues?', 'elementor' ); ?></div>
<div class="elementor-safe-mode-list-item-content"><?php printf( __( '<a href="%s" target="_blank">Click here</a> to troubleshoot', 'elementor' ), self::DOCS_DIDNT_HELP_URL ); ?></div>
</li>
</ul>
<?php
$mu_plugins = wp_get_mu_plugins();
if ( 1 < count( $mu_plugins ) ) : ?>
<div class="elementor-safe-mode-mu-plugins"><?php printf( __( 'Please note! We couldn\'t deactivate all of your plugins on Safe Mode. Please <a href="%s" target="_blank">read more</a> about this issue.', 'elementor' ), self::DOCS_MU_PLUGINS_URL ); ?></div>
<?php endif; ?>
</div>
</div>
<script>
var ElementorSafeMode = function() {
var attachEvents = function() {
jQuery( '.elementor-disable-safe-mode' ).on( 'click', function( e ) {
if ( ! elementorCommon || ! elementorCommon.ajax ) {
return;
}
e.preventDefault();
elementorCommon.ajax.addRequest(
'disable_safe_mode', {
success: function() {
if ( -1 === location.href.indexOf( 'elementor-mode=safe' ) ) {
location.reload();
} else {
// Need to remove the URL from browser history.
location.replace( location.href.replace( '&elementor-mode=safe', '' ) );
}
},
error: function() {
alert( 'An error occurred' );
},
},
true
);
} );
};
var init = function() {
attachEvents();
};
init();
};
new ElementorSafeMode();
</script>
<?php
}
public function print_try_safe_mode() {
if ( ! $this->is_allowed_post_type() ) {
return;
}
echo $this->print_safe_mode_css();
?>
<div class="elementor-safe-mode-toast" id="elementor-try-safe-mode">
<?php if ( current_user_can( 'install_plugins' ) ) : ?>
<header>
<i class="eicon-warning"></i>
<h2><?php echo __( 'Can\'t Edit?', 'elementor' ); ?></h2>
<a class="elementor-safe-mode-button elementor-enable-safe-mode" target="_blank" href="<?php echo $this->get_admin_page_url(); ?>">
<?php echo __( 'Enable Safe Mode', 'elementor' ); ?>
</a>
</header>
<div class="elementor-toast-content">
<?php echo __( 'Having problems loading Elementor? Please enable Safe Mode to troubleshoot.', 'elementor' ); ?>
<a href="<?php echo self::DOCS_TRY_SAFE_MODE_URL; ?>" target="_blank"><?php echo __( 'Learn More', 'elementor' ); ?></a>
</div>
<?php else : ?>
<header>
<i class="eicon-warning"></i>
<h2><?php echo __( 'Can\'t Edit?', 'elementor' ); ?></h2>
</header>
<div class="elementor-toast-content">
<?php echo __( 'If you are experiencing a loading issue, contact your site administrator to troubleshoot the problem using Safe Mode.', 'elementor' ); ?>
<a href="<?php echo self::DOCS_TRY_SAFE_MODE_URL; ?>" target="_blank"><?php echo __( 'Learn More', 'elementor' ); ?></a>
</div>
<?php endif; ?>
</div>
<script>
var ElementorTrySafeMode = function() {
var attachEvents = function() {
jQuery( '.elementor-enable-safe-mode' ).on( 'click', function( e ) {
if ( ! elementorCommon || ! elementorCommon.ajax ) {
return;
}
e.preventDefault();
elementorCommon.ajax.addRequest(
'enable_safe_mode', {
data: {
editor_post_id: '<?php echo Plugin::$instance->editor->get_post_id(); ?>',
},
success: function( url ) {
location.assign( url );
},
error: function() {
alert( 'An error occurred' );
},
},
true
);
} );
};
var isElementorLoaded = function() {
if ( 'undefined' === typeof elementor ) {
return false;
}
if ( ! elementor.loaded ) {
return false;
}
if ( jQuery( '#elementor-loading' ).is( ':visible' ) ) {
return false;
}
return true;
};
var handleTrySafeModeNotice = function() {
var $notice = jQuery( '#elementor-try-safe-mode' );
if ( isElementorLoaded() ) {
$notice.remove();
return;
}
if ( ! $notice.data( 'visible' ) ) {
$notice.show().data( 'visible', true );
}
// Re-check after 500ms.
setTimeout( handleTrySafeModeNotice, 500 );
};
var init = function() {
setTimeout( handleTrySafeModeNotice, <?php echo self::EDITOR_NOTICE_TIMEOUT; ?> );
attachEvents();
};
init();
};
new ElementorTrySafeMode();
</script>
<?php
}
public function run_safe_mode() {
remove_action( 'elementor/editor/footer', [ $this, 'print_try_safe_mode' ] );
// Avoid notices like for comment.php.
add_filter( 'deprecated_file_trigger_error', '__return_false' );
add_filter( 'template_include', [ $this, 'filter_template' ], 999 );
add_filter( 'elementor/document/urls/preview', [ $this, 'filter_preview_url' ] );
add_action( 'elementor/editor/footer', [ $this, 'print_safe_mode_notice' ] );
add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'register_scripts' ], 11 /* After Common Scripts */ );
}
public function register_scripts() {
wp_add_inline_script( 'elementor-common', 'elementorCommon.ajax.addRequestConstant( "elementor-mode", "safe" );' );
}
private function is_enabled() {
return get_option( self::OPTION_ENABLED, '' );
}
private function get_admin_page_url() {
// A fallback URL if the Js doesn't work.
return Tools::get_url();
}
public function plugin_action_links( $actions ) {
$actions['disable'] = '<a href="' . self::get_admin_page_url() . '">' . __( 'Disable Safe Mode', 'elementor' ) . '</a>';
return $actions;
}
public function on_deactivated_plugin( $plugin ) {
if ( ELEMENTOR_PLUGIN_BASE === $plugin ) {
$this->disable_safe_mode();
return;
}
$allowed_plugins = get_option( 'elementor_safe_mode_allowed_plugins', [] );
$plugin_key = array_search( $plugin, $allowed_plugins, true );
if ( $plugin_key ) {
unset( $allowed_plugins[ $plugin_key ] );
update_option( 'elementor_safe_mode_allowed_plugins', $allowed_plugins );
}
}
public function update_allowed_plugins() {
$allowed_plugins = [
'elementor' => ELEMENTOR_PLUGIN_BASE,
];
if ( defined( 'ELEMENTOR_PRO_PLUGIN_BASE' ) ) {
$allowed_plugins['elementor_pro'] = ELEMENTOR_PRO_PLUGIN_BASE;
}
if ( defined( 'WC_PLUGIN_BASENAME' ) ) {
$allowed_plugins['woocommerce'] = WC_PLUGIN_BASENAME;
}
update_option( 'elementor_safe_mode_allowed_plugins', $allowed_plugins );
}
public function __construct() {
if ( current_user_can( 'install_plugins' ) ) {
add_action( 'elementor/admin/after_create_settings/elementor-tools', [ $this, 'add_admin_button' ] );
}
add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
$plugin_file = self::MU_PLUGIN_FILE_NAME;
add_filter( "plugin_action_links_{$plugin_file}", [ $this, 'plugin_action_links' ] );
// Use pre_update, in order to catch cases that $value === $old_value and it not updated.
add_filter( 'pre_update_option_elementor_safe_mode', [ $this, 'on_update_safe_mode' ], 10, 2 );
add_action( 'elementor/safe_mode/init', [ $this, 'run_safe_mode' ] );
add_action( 'elementor/editor/footer', [ $this, 'print_try_safe_mode' ] );
if ( $this->is_enabled() ) {
add_action( 'activated_plugin', [ $this, 'update_allowed_plugins' ] );
add_action( 'deactivated_plugin', [ $this, 'on_deactivated_plugin' ] );
}
}
private function is_allowed_post_type() {
$allowed_post_types = [
'post',
'page',
'product',
Source_Local::CPT,
];
$current_post_type = get_post_type( Plugin::$instance->editor->get_post_id() );
return in_array( $current_post_type, $allowed_post_types );
}
}

View File

@@ -0,0 +1,133 @@
<?php
/**
* Plugin Name: Elementor Safe Mode
* Description: Safe Mode allows you to troubleshoot issues by only loading the editor, without loading the theme or any other plugin.
* Plugin URI: https://elementor.com/?utm_source=safe-mode&utm_campaign=plugin-uri&utm_medium=wp-dash
* Author: Elementor.com
* Version: 1.0.0
* Author URI: https://elementor.com/?utm_source=safe-mode&utm_campaign=author-uri&utm_medium=wp-dash
*
* Text Domain: elementor
*
* @package Elementor
* @category Safe Mode
*
* Elementor is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Elementor is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Safe_Mode {
const OPTION_ENABLED = 'elementor_safe_mode';
const OPTION_TOKEN = self::OPTION_ENABLED . '_token';
public function is_enabled() {
return get_option( self::OPTION_ENABLED );
}
public function is_valid_token() {
$token = isset( $_COOKIE[ self::OPTION_TOKEN ] ) ? $_COOKIE[ self::OPTION_TOKEN ] : null;
if ( $token && get_option( self::OPTION_TOKEN ) === $token ) {
return true;
}
return false;
}
public function is_requested() {
return ! empty( $_REQUEST['elementor-mode'] ) && 'safe' === $_REQUEST['elementor-mode'];
}
public function is_editor() {
return is_admin() && isset( $_GET['action'] ) && 'elementor' === $_GET['action'];
}
public function is_editor_preview() {
return isset( $_GET['elementor-preview'] );
}
public function is_editor_ajax() {
return is_admin() && isset( $_POST['action'] ) && 'elementor_ajax' === $_POST['action'];
}
public function add_hooks() {
add_filter( 'pre_option_active_plugins', function () {
return get_option( 'elementor_safe_mode_allowed_plugins' );
} );
add_filter( 'pre_option_stylesheet', function () {
return 'elementor-safe';
} );
add_filter( 'pre_option_template', function () {
return 'elementor-safe';
} );
add_action( 'elementor/init', function () {
do_action( 'elementor/safe_mode/init' );
} );
}
/**
* Plugin row meta.
*
* Adds row meta links to the plugin list table
*
* Fired by `plugin_row_meta` filter.
*
* @access public
*
* @param array $plugin_meta An array of the plugin's metadata, including
* the version, author, author URI, and plugin URI.
* @param string $plugin_file Path to the plugin file, relative to the plugins
* directory.
*
* @return array An array of plugin row meta links.
*/
public function plugin_row_meta( $plugin_meta, $plugin_file, $plugin_data, $status ) {
if ( basename( __FILE__ ) === $plugin_file ) {
$row_meta = [
'docs' => '<a href="https://go.elementor.com/safe-mode/" aria-label="' . esc_attr( __( 'Learn More', 'elementor' ) ) . '" target="_blank">' . __( 'Learn More', 'elementor' ) . '</a>',
];
$plugin_meta = array_merge( $plugin_meta, $row_meta );
}
return $plugin_meta;
}
public function __construct() {
add_filter( 'plugin_row_meta', [ $this, 'plugin_row_meta' ], 10, 4 );
$enabled_type = $this->is_enabled();
if ( ! $enabled_type || ! $this->is_valid_token() ) {
return;
}
if ( ! $this->is_requested() && 'global' !== $enabled_type ) {
return;
}
if ( ! $this->is_editor() && ! $this->is_editor_preview() && ! $this->is_editor_ajax() ) {
return;
}
$this->add_hooks();
}
}
new Safe_Mode();

View File

@@ -0,0 +1,7 @@
export default class extends elementorModules.Module {
constructor() {
super();
elementorFrontend.elementsHandler.attachHandler( 'text-path', () => import( /* webpackChunkName: 'text-path' */ './handlers/text-path' ) );
}
}

View File

@@ -0,0 +1,224 @@
import { escapeHTML } from 'elementor-frontend/utils/utils';
export default class TextPathHandler extends elementorModules.frontend.handlers.Base {
getDefaultSettings() {
return {
selectors: {
pathContainer: '.e-text-path',
svg: '.e-text-path > svg',
},
};
}
getDefaultElements() {
const { selectors } = this.getSettings();
const element = this.$element[ 0 ];
return {
widgetWrapper: element,
pathContainer: element.querySelector( selectors.pathContainer ),
svg: element.querySelector( selectors.svg ),
textPath: element.querySelector( selectors.textPath ),
};
}
/**
* Initialize the object.
*
* @returns {void}
*/
onInit() {
this.elements = this.getDefaultElements();
// Generate unique IDs using the wrapper's `data-id`.
this.pathId = `e-path-${ this.elements.widgetWrapper.dataset.id }`;
this.textPathId = `e-text-path-${ this.elements.widgetWrapper.dataset.id }`;
if ( ! this.elements.svg ) {
return;
}
this.initTextPath();
}
/**
* Set the start offset for the text.
*
* @param offset {string|int} The text start offset.
*
* @returns {void}
*/
setOffset( offset ) {
if ( ! this.elements.textPath ) {
return;
}
if ( this.isRTL() ) {
offset = 100 - parseInt( offset );
}
this.elements.textPath.setAttribute( 'startOffset', offset + '%' );
}
/**
* Handle element settings changes.
*
* @param setting {Object} The settings object from the editor.
*
* @returns {void}
*/
onElementChange( setting ) {
const {
start_point: startPoint,
text,
} = this.getElementSettings();
switch ( setting ) {
case 'start_point':
this.setOffset( startPoint.size );
break;
case 'text':
this.setText( text );
break;
case 'text_path_direction':
this.setOffset( startPoint.size );
this.setText( text );
break;
default:
break;
}
}
/**
* Attach a unique id to the path.
*
* @returns {void}
*/
attachIdToPath() {
// Prioritize the custom `data` attribute over the `path` element, and fallback to the first `path`.
const path = this.elements.svg.querySelector( '[data-path-anchor]' ) || this.elements.svg.querySelector( 'path' );
path.id = this.pathId;
}
/**
* Initialize the text path element.
*
* @returns {void}
*/
initTextPath() {
const {
start_point: startPoint,
text,
} = this.getElementSettings();
this.attachIdToPath();
// Generate the `textPath` element with its settings.
this.elements.svg.innerHTML += `
<text>
<textPath id="${ this.textPathId }" href="#${ this.pathId }"></textPath>
</text>
`;
// Regenerate the elements object to have access to `this.elements.textPath`.
this.elements.textPath = this.elements.svg.querySelector( `#${ this.textPathId }` );
this.setOffset( startPoint.size );
this.setText( text );
}
/**
* Set the new text into the path.
*
* @param newText {string} The new text to put in the text path.
*
* @returns {void}
*/
setText( newText ) {
const {
url,
is_external: isExternal,
nofollow,
} = this.getElementSettings()?.link;
const target = isExternal ? '_blank' : '',
rel = nofollow ? 'nofollow' : '';
// Add link attributes.
if ( url ) {
newText = `<a href="${ escapeHTML( url ) }" rel="${ rel }" target="${ target }">${ escapeHTML( newText ) }</a>`;
}
// Set the text.
this.elements.textPath.innerHTML = newText;
// Remove the cloned element if exists.
const existingClone = this.elements.svg.querySelector( `#${ this.textPathId }-clone` );
if ( existingClone ) {
existingClone.remove();
}
// Reverse the text if needed.
if ( this.shouldReverseText() ) {
// Keep an invisible selectable copy of original element for better a11y.
const clone = this.elements.textPath.cloneNode();
clone.id += '-clone';
clone.classList.add( 'elementor-hidden' );
clone.textContent = newText;
this.elements.textPath.parentNode.appendChild( clone );
this.reverseToRTL();
}
}
/**
* Determine if the current layout should be RTL.
*
* @returns {boolean}
*/
isRTL() {
const { text_path_direction: direction } = this.getElementSettings();
let isRTL = elementorFrontend.config.is_rtl;
if ( direction ) {
isRTL = ( 'rtl' === direction );
}
return isRTL;
}
/**
* Determine if it should RTL the text (reversing it, etc.).
*
* @returns {boolean}
*/
shouldReverseText() {
return ( this.isRTL() && -1 === navigator.userAgent.indexOf( 'Firefox' ) );
}
/**
* Reverse the text path to support RTL.
*
* @returns {void}
*/
reverseToRTL() {
// Make sure to use the inner `a` tag if exists.
let parentElement = this.elements.textPath;
parentElement = parentElement.querySelector( 'a' ) || parentElement;
// Catch all RTL chars and reverse their order.
const pattern = /([\u0591-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC\s$&+,:;=?@#|'<>.^*()%!-]+)/ig;
// Reverse the text.
parentElement.textContent = parentElement.textContent.replace( pattern, ( word ) => {
return word.split( '' ).reverse().join( '' );
} );
// Add a11y attributes.
parentElement.setAttribute( 'aria-hidden', true );
}
}

View File

@@ -0,0 +1 @@
@import "widgets/text-path.scss";

View File

@@ -0,0 +1,43 @@
.elementor-widget-text-path {
font-size: 20px;
text-align: var( --alignment, #{$start} );
svg {
width: var( --width );
max-width: 100%;
height: auto;
overflow: visible;
word-spacing: var( --word-spacing );
transform: rotate( var( --rotate, 0 ) ) scaleX( var( --scale-x, 1 ) ) scaleY( var( --scale-y, 1 ) );
path {
vector-effect: non-scaling-stroke; /* Prevent stroke size scaling when resizing the SVG. */
fill: var( --path-fill, transparent );
stroke: var( --stroke-color, transparent );
stroke-width: var( --stroke-width, 1px );
transition: var( --stroke-transition ) stroke, var( --stroke-transition ) fill;
}
&:hover {
path {
--path-fill: var( --path-fill-hover );
--stroke-color: var( --stroke-color-hover );
--stroke-width: var( --stroke-width-hover );
}
}
text {
--fill: var( --text-color );
fill: var( --fill );
direction: var( --direction, $direction );
transition: var( --transition ) stroke,
var( --transition ) stroke-width,
var( --transition ) fill;
&:hover {
--text-color: var( --text-color-hover );
}
}
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Elementor\Modules\Shapes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Module extends \Elementor\Core\Base\Module {
/**
* @param bool $add_custom Determine if the output should include the `Custom` option.
*
* @return array List of paths.
*/
public static function get_paths( $add_custom = true ) {
$paths = [
'wave' => __( 'Wave', 'elementor' ),
'arc' => __( 'Arc', 'elementor' ),
'circle' => __( 'Circle', 'elementor' ),
'line' => __( 'Line', 'elementor' ),
'oval' => __( 'Oval', 'elementor' ),
'spiral' => __( 'Spiral', 'elementor' ),
];
if ( $add_custom ) {
$paths['custom'] = __( 'Custom', 'elementor' );
}
return $paths;
}
/**
* @param $path string Path name.
*
* @return string The path SVG markup.
*/
public static function get_path_svg( $path ) {
$file_name = ELEMENTOR_ASSETS_PATH . 'svg-paths/' . $path . '.svg';
if ( ! is_file( $file_name ) ) {
return '';
}
return file_get_contents( $file_name );
}
/**
* Get the module's associated widgets.
*
* @return string[]
*/
protected function get_widgets() {
return [
'TextPath',
];
}
/**
* Retrieve the module name.
*
* @return string
*/
public function get_name() {
return 'shapes';
}
}

View File

@@ -0,0 +1,679 @@
<?php
namespace Elementor\Modules\Shapes\Widgets;
use Elementor\Controls_Manager;
use Elementor\Core\Kits\Documents\Tabs\Global_Typography;
use Elementor\Group_Control_Typography;
use Elementor\Modules\Shapes\Module as Shapes_Module;
use Elementor\Utils;
use Elementor\Widget_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor WordArt widget.
*
* Elementor widget that displays text along SVG path.
*
*/
class TextPath extends Widget_Base {
const DEFAULT_PATH_FILL = '#E8178A';
/**
* Get widget name.
*
* Retrieve Text Path widget name.
*
* @return string Widget name.
* @access public
*
*/
public function get_name() {
return 'text-path';
}
/**
* Get widget title.
*
* Retrieve Text Path widget title.
*
* @return string Widget title.
* @access public
*
*/
public function get_title() {
return __( 'Text Path', 'elementor' );
}
/**
* Get widget icon.
*
* Retrieve Text Path widget icon.
*
* @return string Widget icon.
* @access public
*
*/
public function get_icon() {
return 'eicon-wordart';
}
/**
* Get widget keywords.
*
* Retrieve the list of keywords the widget belongs to.
*
* @return array Widget keywords.
* @access public
*
*/
public function get_keywords() {
return [ 'text path', 'word path', 'text on path', 'wordart', 'word art' ];
}
/**
* Register content controls under content tab.
*/
protected function register_content_tab() {
$this->start_controls_section(
'section_content_text_path',
[
'label' => __( 'Text Path', 'elementor' ),
'tab' => Controls_Manager::TAB_CONTENT,
]
);
$this->add_control(
'text',
[
'label' => __( 'Text', 'elementor' ),
'type' => Controls_Manager::TEXT,
'label_block' => true,
'default' => __( 'Add Your Curvy Text Here', 'elementor' ),
'frontend_available' => true,
'render_type' => 'none',
'dynamic' => [
'active' => true,
],
]
);
$this->add_control(
'path',
[
'label' => __( 'Path Type', 'elementor' ),
'type' => Controls_Manager::SELECT,
'options' => Shapes_Module::get_paths(),
'default' => 'wave',
]
);
$this->add_control(
'custom_path',
[
'label' => __( 'SVG', 'elementor' ),
'type' => Controls_Manager::MEDIA,
'media_types' => [
'svg',
],
'condition' => [
'path' => 'custom',
],
'dynamic' => [
'active' => true,
],
'description' => sprintf( __( 'Want to create custom text paths with SVG? <a target="_blank" href="%s">Learn More</a>', 'elementor' ), 'https://go.elementor.com/text-path-create-paths' ),
]
);
$this->add_control(
'link',
[
'label' => __( 'Link', 'elementor' ),
'type' => Controls_Manager::URL,
'label_block' => true,
'dynamic' => [
'active' => true,
],
'placeholder' => __( 'Paste URL or type', 'elementor' ),
'frontend_available' => true,
]
);
$this->add_responsive_control(
'align',
[
'label' => __( 'Alignment', 'elementor' ),
'type' => Controls_Manager::CHOOSE,
'default' => '',
'options' => [
'left' => [
'title' => __( 'Left', 'elementor' ),
'icon' => 'eicon-text-align-left',
],
'center' => [
'title' => __( 'Center', 'elementor' ),
'icon' => 'eicon-text-align-center',
],
'right' => [
'title' => __( 'Right', 'elementor' ),
'icon' => 'eicon-text-align-right',
],
],
'selectors' => [
'{{WRAPPER}}' => '--alignment: {{VALUE}}',
],
'frontend_available' => true,
]
);
$this->add_control(
'text_path_direction',
[
'label' => __( 'Text Direction', 'elementor' ),
'type' => Controls_Manager::SELECT,
'default' => '',
'options' => [
'' => __( 'Default', 'elementor' ),
'rtl' => __( 'RTL', 'elementor' ),
'ltr' => __( 'LTR', 'elementor' ),
],
'selectors' => [
'{{WRAPPER}}' => '--direction: {{VALUE}}',
],
'frontend_available' => true,
]
);
$this->add_control(
'show_path',
[
'label' => __( 'Show Path', 'elementor' ),
'type' => Controls_Manager::SWITCHER,
'label_on' => __( 'On', 'elementor' ),
'label_off' => __( 'Off', 'elementor' ),
'return_value' => self::DEFAULT_PATH_FILL,
'separator' => 'before',
'default' => '',
'selectors' => [
'{{WRAPPER}}' => '--path-stroke: {{VALUE}}; --path-fill: transparent;',
],
]
);
$this->end_controls_section();
}
/**
* Register style controls under style tab.
*/
protected function register_style_tab() {
/**
* Text Path styling section.
*/
$this->start_controls_section(
'section_style_text_path',
[
'label' => __( 'Text Path', 'elementor' ),
'tab' => Controls_Manager::TAB_STYLE,
]
);
$this->add_responsive_control(
'size',
[
'label' => __( 'Size', 'elementor' ),
'type' => Controls_Manager::SLIDER,
'size_units' => [ '%', 'px' ],
'range' => [
'%' => [
'min' => 0,
'max' => 100,
'step' => 10,
],
'px' => [
'min' => 0,
'max' => 800,
'step' => 50,
],
],
'default' => [
'unit' => 'px',
'size' => 500,
],
'tablet_default' => [
'unit' => 'px',
'size' => 500,
],
'mobile_default' => [
'unit' => 'px',
'size' => 500,
],
'selectors' => [
'{{WRAPPER}}' => '--width: {{SIZE}}{{UNIT}};',
],
]
);
$this->add_responsive_control(
'rotation',
[
'label' => __( 'Rotate', 'elementor' ),
'type' => Controls_Manager::SLIDER,
'size_units' => [ 'deg' ],
'range' => [
'deg' => [
'min' => 0,
'max' => 360,
'step' => 1,
],
],
'default' => [
'unit' => 'deg',
'size' => '',
],
'tablet_default' => [
'unit' => 'deg',
'size' => '',
],
'mobile_default' => [
'unit' => 'deg',
'size' => '',
],
'selectors' => [
'{{WRAPPER}}' => '--rotate: {{SIZE}}{{UNIT}};',
],
]
);
$this->add_control(
'text_heading',
[
'label' => __( 'Text', 'elementor' ),
'type' => Controls_Manager::HEADING,
]
);
$this->add_group_control(
Group_Control_Typography::get_type(),
[
'name' => 'text_typography',
'selector' => '{{WRAPPER}}',
'global' => [
'default' => Global_Typography::TYPOGRAPHY_TEXT,
],
'fields_options' => [
'font_size' => [
'default' => [
'size' => '20',
'unit' => 'px',
],
'size_units' => [ 'px' ],
],
],
]
);
$this->add_responsive_control(
'word_spacing',
[
'label' => __( 'Word Spacing', 'elementor' ),
'type' => Controls_Manager::SLIDER,
'size_units' => [ 'px' ],
'range' => [
'px' => [
'min' => -20,
'max' => 20,
'step' => 1,
],
],
'default' => [
'unit' => 'px',
'size' => '',
],
'tablet_default' => [
'unit' => 'px',
'size' => '',
],
'mobile_default' => [
'unit' => 'px',
'size' => '',
],
'selectors' => [
'{{WRAPPER}}' => '--word-spacing: {{SIZE}}{{UNIT}};',
],
]
);
$this->add_control(
'start_point',
[
'label' => __( 'Starting Point', 'elementor' ),
'type' => Controls_Manager::SLIDER,
'size_units' => [ '%' ],
'range' => [
'px' => [
'min' => -100,
'max' => 100,
'step' => 1,
],
],
'default' => [
'unit' => '%',
'size' => 0,
],
'frontend_available' => true,
'render_type' => 'none',
]
);
$this->start_controls_tabs( 'text_style' );
/**
* Normal tab.
*/
$this->start_controls_tab(
'text_normal',
[
'label' => __( 'Normal', 'elementor' ),
]
);
$this->add_control(
'text_color_normal',
[
'label' => __( 'Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'default' => '',
'selectors' => [
'{{WRAPPER}}' => '--text-color: {{VALUE}};',
],
]
);
$this->end_controls_tab();
/**
* Hover tab.
*/
$this->start_controls_tab(
'text_hover',
[
'label' => __( 'Hover', 'elementor' ),
]
);
$this->add_control(
'text_color_hover',
[
'label' => __( 'Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'default' => '',
'selectors' => [
'{{WRAPPER}}' => '--text-color-hover: {{VALUE}};',
],
]
);
$this->add_control(
'hover_animation',
[
'label' => __( 'Hover Animation', 'elementor' ),
'type' => Controls_Manager::HOVER_ANIMATION,
]
);
$this->add_control(
'hover_transition',
[
'label' => __( 'Transition Duration', 'elementor' ),
'type' => Controls_Manager::SLIDER,
'default' => [
'size' => 0.3,
'unit' => 's',
],
'range' => [
's' => [
'min' => 0,
'max' => 3,
'step' => 0.1,
],
],
'selectors' => [
'{{WRAPPER}}' => '--transition: {{SIZE}}{{UNIT}}',
],
]
);
$this->end_controls_tab();
$this->end_controls_tabs();
$this->end_controls_section();
/**
* Path styling section.
*/
$this->start_controls_section(
'section_style_path',
[
'label' => __( 'Path', 'elementor' ),
'tab' => Controls_Manager::TAB_STYLE,
'condition' => [
'show_path!' => '',
],
]
);
$this->start_controls_tabs( 'path_style' );
/**
* Normal tab.
*/
$this->start_controls_tab(
'path_normal',
[
'label' => __( 'Normal', 'elementor' ),
]
);
$this->add_control(
'path_fill_normal',
[
'label' => __( 'Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'default' => '',
'selectors' => [
'{{WRAPPER}}' => '--path-fill: {{VALUE}};',
],
]
);
$this->add_control(
'stroke_heading_normal',
[
'label' => __( 'Stroke', 'elementor' ),
'type' => Controls_Manager::HEADING,
]
);
$this->add_control(
'stroke_color_normal',
[
'label' => __( 'Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'default' => self::DEFAULT_PATH_FILL,
'selectors' => [
'{{WRAPPER}}' => '--stroke-color: {{VALUE}};',
],
]
);
$this->add_control(
'stroke_width_normal',
[
'label' => __( 'Width', 'elementor' ),
'type' => Controls_Manager::SLIDER,
'default' => [
'size' => 1,
'unit' => 'px',
],
'range' => [
'px' => [
'min' => 0,
'max' => 20,
'step' => 1,
],
],
'selectors' => [
'{{WRAPPER}}' => '--stroke-width: {{SIZE}}{{UNIT}}',
],
]
);
$this->end_controls_tab();
/**
* Hover tab.
*/
$this->start_controls_tab(
'path_hover',
[
'label' => __( 'Hover', 'elementor' ),
]
);
$this->add_control(
'path_fill_hover',
[
'label' => __( 'Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'default' => '',
'selectors' => [
'{{WRAPPER}}' => '--path-fill-hover: {{VALUE}};',
],
]
);
$this->add_control(
'stroke_heading_hover',
[
'label' => __( 'Stroke', 'elementor' ),
'type' => Controls_Manager::HEADING,
]
);
$this->add_control(
'stroke_color_hover',
[
'label' => __( 'Color', 'elementor' ),
'type' => Controls_Manager::COLOR,
'default' => '',
'selectors' => [
'{{WRAPPER}}' => '--stroke-color-hover: {{VALUE}};',
],
]
);
$this->add_control(
'stroke_width_hover',
[
'label' => __( 'Width', 'elementor' ),
'type' => Controls_Manager::SLIDER,
'default' => [
'size' => '',
'unit' => 'px',
],
'range' => [
'px' => [
'min' => 0,
'max' => 20,
'step' => 1,
],
],
'selectors' => [
'{{WRAPPER}}' => '--stroke-width-hover: {{SIZE}}{{UNIT}}',
],
]
);
$this->add_control(
'stroke_transition',
[
'label' => __( 'Transition Duration', 'elementor' ),
'type' => Controls_Manager::SLIDER,
'default' => [
'size' => 0.3,
'unit' => 's',
],
'range' => [
's' => [
'min' => 0,
'max' => 3,
'step' => 0.1,
],
],
'selectors' => [
'{{WRAPPER}}' => '--stroke-transition: {{SIZE}}{{UNIT}}',
],
]
);
$this->end_controls_tab();
$this->end_controls_tabs();
$this->end_controls_section();
}
/**
* Register Text Path widget controls.
*
* Adds different input fields to allow the user to change and customize the widget settings.
*
* @access protected
*/
protected function register_controls() {
$this->register_content_tab();
$this->register_style_tab();
}
/**
* Render Text Path widget output on the frontend.
*
* Written in PHP and used to generate the final HTML.
*
* @access protected
*/
protected function render() {
$settings = $this->get_settings_for_display();
// Get the shape SVG markup.
if ( 'custom' !== $settings['path'] ) {
$path_svg = Shapes_Module::get_path_svg( $settings['path'] );
} else {
$url = $settings['custom_path']['url'];
// Get the file contents only if it's svg.
$path_svg = ( 'svg' === pathinfo( $url, PATHINFO_EXTENSION ) ) ? file_get_contents( $url ) : '';
}
// Add Text Path text.
$this->add_render_attribute( 'text_path', 'class', 'e-text-path' );
// Add hover animation.
if ( ! empty( $settings['hover_animation'] ) ) {
$this->add_render_attribute( 'text_path', 'class', 'elementor-animation-' . $settings['hover_animation'] );
}
// Render.
?>
<div <?php echo $this->get_render_attribute_string( 'text_path' ); ?>>
<?php echo $path_svg; ?>
</div>
<?php
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Elementor\Modules\System_Info\Helpers;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor model helper.
*
* Elementor model helper handler class is responsible for filtering properties.
*
* @since 1.0.0
*/
final class Model_Helper {
/**
* Model helper constructor.
*
* Initializing the model helper class.
*
* @since 1.0.0
* @access private
*/
private function __construct() {}
/**
* Filter possible properties.
*
* Retrieve possible properties filtered by property intersect key.
*
* @since 1.0.0
* @access public
* @static
*
* @param array $possible_properties All the possible properties.
* @param array $properties Properties to filter.
*
* @return array Possible properties filtered by property intersect key.
*/
public static function filter_possible_properties( $possible_properties, $properties ) {
$properties_keys = array_flip( $possible_properties );
return array_intersect_key( $properties, $properties_keys );
}
/**
* Prepare properties.
*
* Combine the possible properties with the user properties and filter them.
*
* @since 1.0.0
* @access public
* @static
*
* @param array $possible_properties All the possible properties.
* @param array $user_properties User properties.
*
* @return array Possible properties and user properties filtered by property intersect key.
*/
public static function prepare_properties( $possible_properties, $user_properties ) {
$properties = array_fill_keys( $possible_properties, null );
$properties = array_merge( $properties, $user_properties );
return self::filter_possible_properties( $possible_properties, $properties );
}
}

View File

@@ -0,0 +1,362 @@
<?php
namespace Elementor\Modules\System_Info;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Modules\System_Info\Reporters\Base;
use Elementor\Modules\System_Info\Helpers\Model_Helper;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor system info module.
*
* Elementor system info module handler class is responsible for registering and
* managing Elementor system info reports.
*
* @since 2.9.0
*/
class Module extends BaseModule {
/**
* Get module name.
*
* Retrieve the system info module name.
*
* @since 2.9.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'system-info';
}
/**
* Required user capabilities.
*
* Holds the user capabilities required to manage Elementor menus.
*
* @since 2.9.0
* @access private
*
* @var string
*/
private $capability = 'manage_options';
/**
* Elementor system info reports.
*
* Holds an array of available reports in Elementor system info page.
*
* @since 2.9.0
* @access private
*
* @var array
*/
private static $reports = [
'server' => [],
'wordpress' => [],
'theme' => [],
'user' => [],
'plugins' => [],
'network_plugins' => [],
'mu_plugins' => [],
];
/**
* Main system info page constructor.
*
* Initializing Elementor system info page.
*
* @since 2.9.0
* @access public
*/
public function __construct() {
$this->add_actions();
}
/**
* Get default settings.
*
* Retrieve the default settings. Used to reset the report settings on
* initialization.
*
* @since 2.9.0
* @access protected
*
* @return array Default settings.
*/
protected function get_init_settings() {
$settings = [];
$reporter_properties = Base::get_properties_keys();
array_push( $reporter_properties, 'category', 'name', 'class_name' );
$settings['reporter_properties'] = $reporter_properties;
$settings['reportFilePrefix'] = '';
return $settings;
}
/**
* Add actions.
*
* Register filters and actions for the main system info page.
*
* @since 2.9.0
* @access private
*/
private function add_actions() {
add_action( 'admin_menu', [ $this, 'register_menu' ], 500 );
add_action( 'wp_ajax_elementor_system_info_download_file', [ $this, 'download_file' ] );
}
/**
* Register admin menu.
*
* Add new Elementor system info admin menu.
*
* Fired by `admin_menu` action.
*
* @since 2.9.0
* @access public
*/
public function register_menu() {
$system_info_text = __( 'System Info', 'elementor' );
add_submenu_page(
'elementor',
$system_info_text,
$system_info_text,
$this->capability,
'elementor-system-info',
[ $this, 'display_page' ]
);
}
/**
* Display page.
*
* Output the content for the main system info page.
*
* @since 2.9.0
* @access public
*/
public function display_page() {
$reports_info = self::get_allowed_reports();
$reports = $this->load_reports( $reports_info, 'html' );
$raw_reports = $this->load_reports( $reports_info, 'raw' );
?>
<div id="elementor-system-info">
<h3><?php echo __( 'System Info', 'elementor' ); ?></h3>
<div><?php $this->print_report( $reports, 'html' ); ?></div>
<h3><?php echo __( 'Copy & Paste Info', 'elementor' ); ?></h3>
<div id="elementor-system-info-raw">
<label id="elementor-system-info-raw-code-label" for="elementor-system-info-raw-code"><?php echo __( 'You can copy the below info as simple text with Ctrl+C / Ctrl+V:', 'elementor' ); ?></label>
<textarea id="elementor-system-info-raw-code" readonly>
<?php
unset( $raw_reports['wordpress']['report']['admin_email'] );
$this->print_report( $raw_reports, 'raw' );
?>
</textarea>
<script>
var textarea = document.getElementById( 'elementor-system-info-raw-code' );
var selectRange = function() {
textarea.setSelectionRange( 0, textarea.value.length );
};
textarea.onfocus = textarea.onblur = textarea.onclick = selectRange;
textarea.onfocus();
</script>
</div>
<hr>
<form action="<?php echo admin_url( 'admin-ajax.php' ); ?>" method="post">
<input type="hidden" name="action" value="elementor_system_info_download_file">
<input type="submit" class="button button-primary" value="<?php echo __( 'Download System Info', 'elementor' ); ?>">
</form>
</div>
<?php
}
/**
* Download file.
*
* Download the reports files.
*
* Fired by `wp_ajax_elementor_system_info_download_file` action.
*
* @since 2.9.0
* @access public
*/
public function download_file() {
if ( ! current_user_can( $this->capability ) ) {
wp_die( __( 'You don\'t have permissions to download this file', 'elementor' ) );
}
$reports_info = self::get_allowed_reports();
$reports = $this->load_reports( $reports_info, 'raw' );
$domain = parse_url( site_url(), PHP_URL_HOST );
header( 'Content-Type: text/plain' );
header( 'Content-Disposition:attachment; filename=system-info-' . $domain . '-' . gmdate( 'd-m-Y' ) . '.txt' );
$this->print_report( $reports );
die;
}
/**
* Get report class.
*
* Retrieve the class of the report for any given report type.
*
* @since 2.9.0
* @access public
*
* @param string $reporter_type The type of the report.
*
* @return string The class of the report.
*/
public function get_reporter_class( $reporter_type ) {
return __NAMESPACE__ . '\Reporters\\' . ucfirst( $reporter_type );
}
/**
* Load reports.
*
* Retrieve the system info reports.
*
* @since 2.9.0
* @access public
*
* @param array $reports An array of system info reports.
* @param string $format - possible values: 'raw' or empty string, meaning 'html'
*
* @return array An array of system info reports.
*/
public function load_reports( $reports, $format = '' ) {
$result = [];
foreach ( $reports as $report_name => $report_info ) {
$reporter_params = [
'name' => $report_name,
'format' => $format,
];
$reporter_params = array_merge( $reporter_params, $report_info );
$reporter = $this->create_reporter( $reporter_params );
if ( ! $reporter instanceof Base ) {
continue;
}
$result[ $report_name ] = [
'report' => $reporter->get_report( $format ),
'label' => $reporter->get_title(),
];
if ( ! empty( $report_info['sub'] ) ) {
$result[ $report_name ]['sub'] = $this->load_reports( $report_info['sub'] );
}
}
return $result;
}
/**
* Create a report.
*
* Register a new report that will be displayed in Elementor system info page.
*
* @param array $properties Report properties.
*
* @return \WP_Error|false|Base Base instance if the report was created,
* False or WP_Error otherwise.
*@since 2.9.0
* @access public
*
*/
public function create_reporter( array $properties ) {
$properties = Model_Helper::prepare_properties( $this->get_settings( 'reporter_properties' ), $properties );
$reporter_class = $properties['class_name'] ? $properties['class_name'] : $this->get_reporter_class( $properties['name'] );
$reporter = new $reporter_class( $properties );
if ( ! ( $reporter instanceof Base ) ) {
return new \WP_Error( 'Each reporter must to be an instance or sub-instance of `Base` class.' );
}
if ( ! $reporter->is_enabled() ) {
return false;
}
return $reporter;
}
/**
* Print report.
*
* Output the system info page reports using an output template.
*
* @since 2.9.0
* @access public
*
* @param array $reports An array of system info reports.
* @param string $template Output type from the templates folder. Available
* templates are `raw` and `html`. Default is `raw`.
*/
public function print_report( $reports, $template = 'raw' ) {
static $tabs_count = 0;
static $required_plugins_properties = [
'Name',
'Version',
'URL',
'Author',
];
$template_path = __DIR__ . '/templates/' . $template . '.php';
require $template_path;
}
/**
* Get allowed reports.
*
* Retrieve the available reports in Elementor system info page.
*
* @since 2.9.0
* @access public
* @static
*
* @return array Available reports in Elementor system info page.
*/
public static function get_allowed_reports() {
return self::$reports;
}
/**
* Add report.
*
* Register a new report to Elementor system info page.
*
* @since 2.9.0
* @access public
* @static
*
* @param string $report_name The name of the report.
* @param array $report_info Report info.
*/
public static function add_report( $report_name, $report_info ) {
self::$reports[ $report_name ] = $report_info;
}
}

View File

@@ -0,0 +1,199 @@
<?php
namespace Elementor\Modules\System_Info\Reporters;
use Elementor\Modules\System_Info\Helpers\Model_Helper;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor base reporter.
*
* A base abstract class that provides the needed properties and methods to
* manage and handle reporter in inheriting classes.
*
* @since 2.9.0
* @abstract
*/
abstract class Base {
/**
* Reporter properties.
*
* Holds the list of all the properties of the report.
*
* @access protected
* @static
*
* @var array
*/
protected $_properties;
/**
* Get report title.
*
* Retrieve the title of the report.
*
* @since 2.9.0
* @access public
* @abstract
*/
abstract public function get_title();
/**
* Get report fields.
*
* Retrieve the required fields for the report.
*
* @since 2.9.0
* @access public
* @abstract
*/
abstract public function get_fields();
/**
* Is report enabled.
*
* Whether the report is enabled.
*
* @since 2.9.0
* @access public
*
* @return bool Whether the report is enabled.
*/
public function is_enabled() {
return true;
}
/**
* Get report.
*
* Retrieve the report with all it's containing fields.
*
* @since 2.9.0
* @access public
*
* @return \WP_Error | array {
* Report fields.
*
* @type string $name Field name.
* @type string $label Field label.
* }
*/
final public function get_report( $format = '' ) {
$result = [];
$format = ( empty( $format ) ) ? '' : $format . '_';
foreach ( $this->get_fields() as $field_name => $field_label ) {
$method = 'get_' . $format . $field_name;
if ( ! method_exists( $this, $method ) ) {
$method = 'get_' . $field_name;
//fallback:
if ( ! method_exists( $this, $method ) ) {
return new \WP_Error( sprintf( "Getter method for the field '%s' wasn't found in %s.", $field_name, get_called_class() ) );
}
}
$reporter_field = [
'name' => $field_name,
'label' => $field_label,
];
$reporter_field = array_merge( $reporter_field, $this->$method() );
$result[ $field_name ] = $reporter_field;
}
return $result;
}
/**
* Get properties keys.
*
* Retrieve the keys of the properties.
*
* @since 2.9.0
* @access public
* @static
*
* @return array {
* Property keys.
*
* @type string $name Property name.
* @type string $fields Property fields.
* }
*/
public static function get_properties_keys() {
return [
'name',
'format',
'fields',
];
}
/**
* Filter possible properties.
*
* Retrieve possible properties filtered by property keys.
*
* @since 2.9.0
* @access public
* @static
*
* @param array $properties Properties to filter.
*
* @return array Possible properties filtered by property keys.
*/
final public static function filter_possible_properties( $properties ) {
return Model_Helper::filter_possible_properties( self::get_properties_keys(), $properties );
}
/**
* Set properties.
*
* Add/update properties to the report.
*
* @since 2.9.0
* @access public
*
* @param array $key Property key.
* @param array $value Optional. Property value. Default is `null`.
*/
final public function set_properties( $key, $value = null ) {
if ( is_array( $key ) ) {
$key = self::filter_possible_properties( $key );
foreach ( $key as $sub_key => $sub_value ) {
$this->set_properties( $sub_key, $sub_value );
}
return;
}
if ( ! in_array( $key, self::get_properties_keys(), true ) ) {
return;
}
$this->_properties[ $key ] = $value;
}
/**
* Reporter base constructor.
*
* Initializing the reporter base class.
*
* @since 2.9.0
* @access public
*
* @param array $properties Optional. Properties to filter. Default is `null`.
*/
public function __construct( $properties = null ) {
$this->_properties = array_fill_keys( self::get_properties_keys(), null );
if ( $properties ) {
$this->set_properties( $properties, null );
}
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Elementor\Modules\System_Info\Reporters;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor must-use plugins report.
*
* Elementor system report handler class responsible for generating a report for
* must-use plugins.
*
* @since 1.0.0
*/
class MU_Plugins extends Base {
/**
* Must-Use plugins.
*
* Holds the sites must-use plugins list.
*
* @since 1.0.0
* @access private
*
* @var array
*/
private $plugins;
/**
* Get must-use plugins.
*
* Retrieve the must-use plugins.
*
* @since 2.0.0
* @access private
*
* @return array Must-Use plugins.
*/
private function get_mu_plugins() {
if ( ! $this->plugins ) {
$this->plugins = get_mu_plugins();
}
return $this->plugins;
}
/**
* Is enabled.
*
* Whether there are must-use plugins or not.
*
* @since 1.0.0
* @access public
*
* @return bool True if the site has must-use plugins, False otherwise.
*/
public function is_enabled() {
return ! ! $this->get_mu_plugins();
}
/**
* Get must-use plugins reporter title.
*
* Retrieve must-use plugins reporter title.
*
* @since 1.0.0
* @access public
*
* @return string Reporter title.
*/
public function get_title() {
return 'Must-Use Plugins';
}
/**
* Get must-use plugins report fields.
*
* Retrieve the required fields for the must-use plugins report.
*
* @since 1.0.0
* @access public
*
* @return array Required report fields with field ID and field label.
*/
public function get_fields() {
return [
'must_use_plugins' => 'Must-Use Plugins',
];
}
/**
* Get must-use plugins.
*
* Retrieve the sites must-use plugins.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value The must-use plugins list.
* }
*/
public function get_must_use_plugins() {
return [
'value' => $this->get_mu_plugins(),
];
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Elementor\Modules\System_Info\Reporters;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor network plugins report.
*
* Elementor system report handler class responsible for generating a report for
* network plugins.
*
* @since 1.0.0
*/
class Network_Plugins extends Base {
/**
* Network plugins.
*
* Holds the sites network plugins list.
*
* @since 1.0.0
* @access private
*
* @var array
*/
private $plugins;
/**
* Get network plugins reporter title.
*
* Retrieve network plugins reporter title.
*
* @since 1.0.0
* @access public
*
* @return string Reporter title.
*/
public function get_title() {
return 'Network Plugins';
}
/**
* Get active network plugins.
*
* Retrieve the active network plugins from the list of active site-wide plugins.
*
* @since 2.0.0
* @access private
*
* @return array Active network plugins.
*/
private function get_network_plugins() {
if ( ! $this->plugins ) {
$active_plugins = get_site_option( 'active_sitewide_plugins' );
$this->plugins = array_intersect_key( get_plugins(), $active_plugins );
}
return $this->plugins;
}
/**
* Is enabled.
*
* Whether there are active network plugins or not.
*
* @since 1.0.0
* @access public
*
* @return bool True if the site has active network plugins, False otherwise.
*/
public function is_enabled() {
if ( ! is_multisite() ) {
return false;
};
return ! ! $this->get_network_plugins();
}
/**
* Get network plugins report fields.
*
* Retrieve the required fields for the network plugins report.
*
* @since 1.0.0
* @access public
*
* @return array Required report fields with field ID and field label.
*/
public function get_fields() {
return [
'network_active_plugins' => 'Network Plugins',
];
}
/**
* Get active network plugins.
*
* Retrieve the sites active network plugins.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value The active network plugins list.
* }
*/
public function get_network_active_plugins() {
return [
'value' => $this->get_network_plugins(),
];
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Elementor\Modules\System_Info\Reporters;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor active plugins report.
*
* Elementor system report handler class responsible for generating a report for
* active plugins.
*
* @since 1.0.0
*/
class Plugins extends Base {
/**
* Active plugins.
*
* Holds the sites active plugins list.
*
* @since 1.0.0
* @access private
*
* @var array
*/
private $plugins;
/**
* Get active plugins.
*
* Retrieve the active plugins from the list of all the installed plugins.
*
* @since 2.0.0
* @access private
*
* @return array Active plugins.
*/
private function get_plugins() {
if ( ! $this->plugins ) {
$this->plugins = Plugin::$instance->wp->get_active_plugins()->all();
}
return $this->plugins;
}
/**
* Get active plugins reporter title.
*
* Retrieve active plugins reporter title.
*
* @since 1.0.0
* @access public
*
* @return string Reporter title.
*/
public function get_title() {
return 'Active Plugins';
}
/**
* Is enabled.
*
* Whether there are active plugins or not.
*
* @since 1.0.0
* @access public
*
* @return bool True if the site has active plugins, False otherwise.
*/
public function is_enabled() {
return ! ! $this->get_plugins();
}
/**
* Get active plugins report fields.
*
* Retrieve the required fields for the active plugins report.
*
* @since 1.0.0
* @access public
*
* @return array Required report fields with field ID and field label.
*/
public function get_fields() {
return [
'active_plugins' => 'Active Plugins',
];
}
/**
* Get active plugins.
*
* Retrieve the sites active plugins.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value The active plugins list.
* }
*/
public function get_active_plugins() {
return [
'value' => $this->get_plugins(),
];
}
}

View File

@@ -0,0 +1,362 @@
<?php
namespace Elementor\Modules\System_Info\Reporters;
use Elementor\Api;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor server environment report.
*
* Elementor system report handler class responsible for generating a report for
* the server environment.
*
* @since 1.0.0
*/
class Server extends Base {
/**
* Get server environment reporter title.
*
* Retrieve server environment reporter title.
*
* @since 1.0.0
* @access public
*
* @return string Reporter title.
*/
public function get_title() {
return 'Server Environment';
}
/**
* Get server environment report fields.
*
* Retrieve the required fields for the server environment report.
*
* @since 1.0.0
* @access public
*
* @return array Required report fields with field ID and field label.
*/
public function get_fields() {
return [
'os' => 'Operating System',
'software' => 'Software',
'mysql_version' => 'MySQL version',
'php_version' => 'PHP Version',
'php_max_input_vars' => 'PHP Max Input Vars',
'php_max_post_size' => 'PHP Max Post Size',
'gd_installed' => 'GD Installed',
'zip_installed' => 'ZIP Installed',
'write_permissions' => 'Write Permissions',
'elementor_library' => 'Elementor Library',
];
}
/**
* Get server operating system.
*
* Retrieve the server operating system.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value Server operating system.
* }
*/
public function get_os() {
return [
'value' => PHP_OS,
];
}
/**
* Get server software.
*
* Retrieve the server software.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value Server software.
* }
*/
public function get_software() {
return [
'value' => $_SERVER['SERVER_SOFTWARE'],
];
}
/**
* Get PHP version.
*
* Retrieve the PHP version.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value PHP version.
* @type string $recommendation Minimum PHP version recommendation.
* @type bool $warning Whether to display a warning.
* }
*/
public function get_php_version() {
$result = [
'value' => PHP_VERSION,
];
if ( version_compare( $result['value'], '5.4', '<' ) ) {
$result['recommendation'] = _x( 'We recommend to use php 5.4 or higher', 'System Info', 'elementor' );
$result['warning'] = true;
}
return $result;
}
/**
* Get PHP `max_input_vars`.
*
* Retrieve the value of `max_input_vars` from `php.ini` configuration file.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value PHP `max_input_vars`.
* }
*/
public function get_php_max_input_vars() {
return [
'value' => ini_get( 'max_input_vars' ),
];
}
/**
* Get PHP `post_max_size`.
*
* Retrieve the value of `post_max_size` from `php.ini` configuration file.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value PHP `post_max_size`.
* }
*/
public function get_php_max_post_size() {
return [
'value' => ini_get( 'post_max_size' ),
];
}
/**
* Get GD installed.
*
* Whether the GD extension is installed.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value Yes if the GD extension is installed, No otherwise.
* @type bool $warning Whether to display a warning. True if the GD extension is installed, False otherwise.
* }
*/
public function get_gd_installed() {
$gd_installed = extension_loaded( 'gd' );
return [
'value' => $gd_installed ? 'Yes' : 'No',
'warning' => ! $gd_installed,
];
}
/**
* Get ZIP installed.
*
* Whether the ZIP extension is installed.
*
* @since 2.1.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value Yes if the ZIP extension is installed, No otherwise.
* @type bool $warning Whether to display a warning. True if the ZIP extension is installed, False otherwise.
* }
*/
public function get_zip_installed() {
$zip_installed = extension_loaded( 'zip' );
return [
'value' => $zip_installed ? 'Yes' : 'No',
'warning' => ! $zip_installed,
];
}
/**
* Get MySQL version.
*
* Retrieve the MySQL version.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value MySQL version.
* }
*/
public function get_mysql_version() {
global $wpdb;
$db_server_version = $wpdb->get_results( "SHOW VARIABLES WHERE `Variable_name` IN ( 'version_comment', 'innodb_version' )", OBJECT_K );
return [
'value' => $db_server_version['version_comment']->Value . ' v' . $db_server_version['innodb_version']->Value,
];
}
/**
* Get write permissions.
*
* Check whether the required folders has writing permissions.
*
* @since 1.9.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value Writing permissions status.
* @type bool $warning Whether to display a warning. True if some required
* folders don't have writing permissions, False otherwise.
* }
*/
public function get_write_permissions() {
$paths_to_check = [
ABSPATH => 'WordPress root directory',
];
$write_problems = [];
$wp_upload_dir = wp_upload_dir();
if ( $wp_upload_dir['error'] ) {
$write_problems[] = 'WordPress root uploads directory';
}
$elementor_uploads_path = $wp_upload_dir['basedir'] . '/elementor';
if ( is_dir( $elementor_uploads_path ) ) {
$paths_to_check[ $elementor_uploads_path ] = 'Elementor uploads directory';
}
$htaccess_file = ABSPATH . '/.htaccess';
if ( file_exists( $htaccess_file ) ) {
$paths_to_check[ $htaccess_file ] = '.htaccess file';
}
foreach ( $paths_to_check as $dir => $description ) {
if ( ! is_writable( $dir ) ) {
$write_problems[] = $description;
}
}
if ( $write_problems ) {
$value = 'There are some writing permissions issues with the following directories/files:' . "\n\t\t - ";
$value .= implode( "\n\t\t - ", $write_problems );
} else {
$value = 'All right';
}
return [
'value' => $value,
'warning' => ! ! $write_problems,
];
}
/**
* Check for elementor library connectivity.
*
* Check whether the remote elementor library is reachable.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value The status of elementor library connectivity.
* @type bool $warning Whether to display a warning. True if elementor
* * library is not reachable, False otherwise.
* }
*/
public function get_elementor_library() {
$response = wp_remote_get(
Api::$api_info_url, [
'timeout' => 5,
'body' => [
// Which API version is used
'api_version' => ELEMENTOR_VERSION,
// Which language to return
'site_lang' => get_bloginfo( 'language' ),
],
]
);
if ( is_wp_error( $response ) ) {
return [
'value' => 'Not connected (' . $response->get_error_message() . ')',
'warning' => true,
];
}
$http_response_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== (int) $http_response_code ) {
$error_msg = 'HTTP Error (' . $http_response_code . ')';
return [
'value' => 'Not connected (' . $error_msg . ')',
'warning' => true,
];
}
$info_data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $info_data ) ) {
return [
'value' => 'Not connected (Returns invalid JSON)',
'warning' => true,
];
}
return [
'value' => 'Connected',
];
}
}

View File

@@ -0,0 +1,274 @@
<?php
namespace Elementor\Modules\System_Info\Reporters;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor theme report.
*
* Elementor system report handler class responsible for generating a report for
* the theme.
*
* @since 1.0.0
*/
class Theme extends Base {
/**
* Theme.
*
* Holds the sites theme object.
*
* @since 1.0.0
* @access private
*
* @var \WP_Theme WordPress theme object.
*/
private $theme = null;
/**
* Get theme reporter title.
*
* Retrieve theme reporter title.
*
* @since 1.0.0
* @access public
*
* @return string Reporter title.
*/
public function get_title() {
return 'Theme';
}
/**
* Get theme report fields.
*
* Retrieve the required fields for the theme report.
*
* @since 1.0.0
* @access public
*
* @return array Required report fields with field ID and field label.
*/
public function get_fields() {
$fields = [
'name' => 'Name',
'version' => 'Version',
'author' => 'Author',
'is_child_theme' => 'Child Theme',
];
if ( $this->get_parent_theme() ) {
$parent_fields = [
'parent_name' => 'Parent Theme Name',
'parent_version' => 'Parent Theme Version',
'parent_author' => 'Parent Theme Author',
];
$fields = array_merge( $fields, $parent_fields );
}
return $fields;
}
/**
* Get theme.
*
* Retrieve the theme.
*
* @since 1.0.0
* @access protected
* @deprecated 3.1.0
*
* @return \WP_Theme WordPress theme object.
*/
protected function _get_theme() {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function( __METHOD__, '3.1.0', __CLASS__ . '::get_theme()' );
return $this->get_theme();
}
/**
* Get theme.
*
* Retrieve the theme.
*
* @since 3.1.0
* @access private
*
* @return \WP_Theme WordPress theme object.
*/
private function get_theme() {
if ( is_null( $this->theme ) ) {
$this->theme = wp_get_theme();
}
return $this->theme;
}
/**
* Get parent theme.
*
* Retrieve the parent theme.
*
* @since 1.0.0
* @access protected
*
* @return \WP_Theme|false WordPress theme object, or false if the current theme is not a child theme.
*/
protected function get_parent_theme() {
return $this->get_theme()->parent();
}
/**
* Get theme name.
*
* Retrieve the theme name.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value The theme name.
* }
*/
public function get_name() {
return [
'value' => $this->get_theme()->get( 'Name' ),
];
}
/**
* Get theme author.
*
* Retrieve the theme author.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value The theme author.
* }
*/
public function get_author() {
return [
'value' => $this->get_theme()->get( 'Author' ),
];
}
/**
* Get theme version.
*
* Retrieve the theme version.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value The theme version.
* }
*/
public function get_version() {
return [
'value' => $this->get_theme()->get( 'Version' ),
];
}
/**
* Is the theme is a child theme.
*
* Whether the theme is a child theme.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value Yes if the theme is a child theme, No otherwise.
* @type string $recommendation Theme source code modification recommendation.
* }
*/
public function get_is_child_theme() {
$is_child_theme = is_child_theme();
$result = [
'value' => $is_child_theme ? 'Yes' : 'No',
];
if ( ! $is_child_theme ) {
$result['recommendation'] = sprintf(
/* translators: %s: Codex URL */
_x( 'If you want to modify the source code of your theme, we recommend using a <a href="%s">child theme</a>.', 'System Info', 'elementor' ),
'https://go.elementor.com/wordpress-child-themes/'
);
}
return $result;
}
/**
* Get parent theme version.
*
* Retrieve the parent theme version.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value The parent theme version.
* }
*/
public function get_parent_version() {
return [
'value' => $this->get_parent_theme()->get( 'Version' ),
];
}
/**
* Get parent theme author.
*
* Retrieve the parent theme author.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value The parent theme author.
* }
*/
public function get_parent_author() {
return [
'value' => $this->get_parent_theme()->get( 'Author' ),
];
}
/**
* Get parent theme name.
*
* Retrieve the parent theme name.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value The parent theme name.
* }
*/
public function get_parent_name() {
return [
'value' => $this->get_parent_theme()->get( 'Name' ),
];
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Elementor\Modules\System_Info\Reporters;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor user report.
*
* Elementor system report handler class responsible for generating a report for
* the user.
*
* @since 1.0.0
*/
class User extends Base {
/**
* Get user reporter title.
*
* Retrieve user reporter title.
*
* @since 1.0.0
* @access public
*
* @return string Reporter title.
*/
public function get_title() {
return 'User';
}
/**
* Get user report fields.
*
* Retrieve the required fields for the user report.
*
* @since 1.0.0
* @access public
*
* @return array Required report fields with field ID and field label.
*/
public function get_fields() {
return [
'role' => 'Role',
'locale' => 'WP Profile lang',
'agent' => 'User Agent',
];
}
/**
* Get user role.
*
* Retrieve the user role.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value The user role.
* }
*/
public function get_role() {
$role = null;
$current_user = wp_get_current_user();
if ( ! empty( $current_user->roles ) ) {
$role = $current_user->roles[0];
}
return [
'value' => $role,
];
}
/**
* Get user profile language.
*
* Retrieve the user profile language.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value User profile language.
* }
*/
public function get_locale() {
return [
'value' => get_locale(),
];
}
/**
* Get user agent.
*
* Retrieve user agent.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value HTTP user agent.
* }
*/
public function get_agent() {
return [
'value' => esc_html( $_SERVER['HTTP_USER_AGENT'] ),
];
}
}

View File

@@ -0,0 +1,312 @@
<?php
namespace Elementor\Modules\System_Info\Reporters;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor WordPress environment report.
*
* Elementor system report handler class responsible for generating a report for
* the WordPress environment.
*
* @since 1.0.0
*/
class WordPress extends Base {
/**
* Get WordPress environment reporter title.
*
* Retrieve WordPress environment reporter title.
*
* @since 1.0.0
* @access public
*
* @return string Reporter title.
*/
public function get_title() {
return 'WordPress Environment';
}
/**
* Get WordPress environment report fields.
*
* Retrieve the required fields for the WordPress environment report.
*
* @since 1.0.0
* @access public
*
* @return array Required report fields with field ID and field label.
*/
public function get_fields() {
return [
'version' => 'Version',
'site_url' => 'Site URL',
'home_url' => 'Home URL',
'is_multisite' => 'WP Multisite',
'max_upload_size' => 'Max Upload Size',
'memory_limit' => 'Memory limit',
'permalink_structure' => 'Permalink Structure',
'language' => 'Language',
'timezone' => 'Timezone',
'admin_email' => 'Admin Email',
'debug_mode' => 'Debug Mode',
];
}
/**
* Get WordPress memory limit.
*
* Retrieve the WordPress memory limit.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value WordPress memory limit.
* @type string $recommendation Recommendation memory limit.
* @type bool $warning Whether to display a warning. True if the limit
* is below the recommended 64M, False otherwise.
* }
*/
public function get_memory_limit() {
$result = [
'value' => ini_get( 'memory_limit' ),
];
$min_recommended_memory = '64M';
$memory_limit_bytes = wp_convert_hr_to_bytes( $result['value'] );
$min_recommended_bytes = wp_convert_hr_to_bytes( $min_recommended_memory );
if ( $memory_limit_bytes < $min_recommended_bytes ) {
$result['recommendation'] = sprintf(
/* translators: 1: Minimum recommended_memory, 2: Codex URL */
_x( 'We recommend setting memory to at least %1$s. For more information, read about <a href="%2$s">how to Increase memory allocated to PHP</a>.', 'System Info', 'elementor' ),
$min_recommended_memory,
'https://go.elementor.com/wordpress-wp-config-memory/'
);
$result['warning'] = true;
}
return $result;
}
/**
* Get WordPress version.
*
* Retrieve the WordPress version.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value WordPress version.
* }
*/
public function get_version() {
return [
'value' => get_bloginfo( 'version' ),
];
}
/**
* Is multisite.
*
* Whether multisite is enabled or not.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value Yes if multisite is enabled, No otherwise.
* }
*/
public function get_is_multisite() {
return [
'value' => is_multisite() ? 'Yes' : 'No',
];
}
/**
* Get site URL.
*
* Retrieve WordPress site URL.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value WordPress site URL.
* }
*/
public function get_site_url() {
return [
'value' => get_site_url(),
];
}
/**
* Get home URL.
*
* Retrieve WordPress home URL.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value WordPress home URL.
* }
*/
public function get_home_url() {
return [
'value' => get_home_url(),
];
}
/**
* Get permalink structure.
*
* Retrieve the permalink structure
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value WordPress permalink structure.
* }
*/
public function get_permalink_structure() {
global $wp_rewrite;
$structure = $wp_rewrite->permalink_structure;
if ( ! $structure ) {
$structure = 'Plain';
}
return [
'value' => $structure,
];
}
/**
* Get site language.
*
* Retrieve the site language.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value WordPress site language.
* }
*/
public function get_language() {
return [
'value' => get_bloginfo( 'language' ),
];
}
/**
* Get PHP `max_upload_size`.
*
* Retrieve the value of maximum upload file size defined in `php.ini` configuration file.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value Maximum upload file size allowed.
* }
*/
public function get_max_upload_size() {
return [
'value' => size_format( wp_max_upload_size() ),
];
}
/**
* Get WordPress timezone.
*
* Retrieve WordPress timezone.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value WordPress timezone.
* }
*/
public function get_timezone() {
$timezone = get_option( 'timezone_string' );
if ( ! $timezone ) {
$timezone = get_option( 'gmt_offset' );
}
return [
'value' => $timezone,
];
}
/**
* Get WordPress administrator email.
*
* Retrieve WordPress administrator email.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value WordPress administrator email.
* }
*/
public function get_admin_email() {
return [
'value' => get_option( 'admin_email' ),
];
}
/**
* Get debug mode.
*
* Whether WordPress debug mode is enabled or not.
*
* @since 1.0.0
* @access public
*
* @return array {
* Report data.
*
* @type string $value Active if debug mode is enabled, Inactive otherwise.
* }
*/
public function get_debug_mode() {
return [
'value' => WP_DEBUG ? 'Active' : 'Inactive',
];
}
}

View File

@@ -0,0 +1,75 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* @var array $reports
*/
foreach ( $reports as $report_name => $report ) : ?>
<div class="elementor-system-info-section elementor-system-info-<?php echo esc_attr( $report_name ); ?>">
<table class="widefat">
<thead>
<tr>
<th><?php echo $report['label']; ?></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<?php
foreach ( $report['report'] as $field_name => $field ) :
if ( in_array( $report_name, [ 'plugins', 'network_plugins', 'mu_plugins' ], true ) ) {
foreach ( $field['value'] as $plugin_info ) :
?>
<tr>
<td><?php
if ( $plugin_info['PluginURI'] ) :
$plugin_name = "<a href='{$plugin_info['PluginURI']}'>{$plugin_info['Name']}</a>";
else :
$plugin_name = $plugin_info['Name'];
endif;
if ( $plugin_info['Version'] ) :
$plugin_name .= ' - ' . $plugin_info['Version'];
endif;
echo $plugin_name;
?></td>
<td><?php
if ( $plugin_info['Author'] ) :
if ( $plugin_info['AuthorURI'] ) :
$author = "<a href='{$plugin_info['AuthorURI']}'>{$plugin_info['Author']}</a>";
else :
$author = $plugin_info['Author'];
endif;
echo "By $author";
endif;
?></td>
<td></td>
</tr>
<?php
endforeach;
} else {
$warning_class = ! empty( $field['warning'] ) ? ' class="elementor-warning"' : '';
$log_label = ! empty( $field['label'] ) ? $field['label'] . ':' : '';
?>
<tr<?php echo $warning_class; ?>>
<td><?php echo $log_label; ?></td>
<td><?php echo $field['value']; ?></td>
<td><?php
if ( ! empty( $field['recommendation'] ) ) :
echo $field['recommendation'];
endif;
?></td>
</tr>
<?php
}
endforeach;
?>
</tbody>
</table>
</div>
<?php
endforeach;

View File

@@ -0,0 +1,65 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* @var array $reports
* @var array $required_plugins_properties
* @var int $tabs_count
*/
$tabs_count++;
$required_plugins_properties = array_flip( $required_plugins_properties );
unset( $required_plugins_properties['Name'] );
foreach ( $reports as $report_name => $report ) :
$indent = str_repeat( "\t", $tabs_count - 1 );
$is_plugins = in_array( $report_name, [
'plugins',
'network_plugins',
'mu_plugins',
] );
if ( ! $is_plugins ) :
echo PHP_EOL . $indent . '== ' . $report['label'] . ' ==';
endif;
echo PHP_EOL;
foreach ( $report['report'] as $field_name => $field ) :
$sub_indent = str_repeat( "\t", $tabs_count );
if ( $is_plugins ) {
echo "== {$field['label']} ==" . PHP_EOL;
foreach ( $field['value'] as $plugin_info ) :
$plugin_properties = array_intersect_key( $plugin_info, $required_plugins_properties );
echo $sub_indent . $plugin_info['Name'];
foreach ( $plugin_properties as $property_name => $property ) :
echo PHP_EOL . "{$sub_indent}\t{$property_name}: {$property}";
endforeach;
echo PHP_EOL . PHP_EOL;
endforeach;
} else {
$label = $field['label'];
if ( ! empty( $label ) ) {
$label .= ': ';
}
echo "{$sub_indent}{$label}{$field['value']}" . PHP_EOL;
}
endforeach;
if ( ! empty( $report['sub'] ) ) :
$this->print_report( $report['sub'], $template, true );
endif;
endforeach;
$tabs_count--;

View File

@@ -0,0 +1,606 @@
<?php
namespace Elementor\Modules\Usage;
use Elementor\Core\Base\Document;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\DynamicTags\Manager;
use Elementor\Modules\System_Info\Module as System_Info;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor usage module.
*
* Elementor usage module handler class is responsible for registering and
* managing Elementor usage data.
*
*/
class Module extends BaseModule {
const GENERAL_TAB = 'general';
const META_KEY = '_elementor_controls_usage';
const OPTION_NAME = 'elementor_controls_usage';
/**
* @var bool
*/
private $is_document_saving = false;
/**
* Get module name.
*
* Retrieve the usage module name.
*
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'usage';
}
/**
* Get doc type count.
*
* Get count of documents based on doc type
*
* Remove 'wp-' from $doc_type for BC, support doc type change since 2.7.0.
*
* @param \Elementor\Core\Documents_Manager $doc_class
* @param String $doc_type
*
* @return int
*/
public function get_doc_type_count( $doc_class, $doc_type ) {
static $posts = null;
static $library = null;
if ( null === $posts ) {
$posts = \Elementor\Tracker::get_posts_usage();
}
if ( null === $library ) {
$library = \Elementor\Tracker::get_library_usage();
}
$posts_usage = $posts;
if ( $doc_class::get_property( 'show_in_library' ) ) {
$posts_usage = $library;
}
$doc_type_common = str_replace( 'wp-', '', $doc_type );
$doc_usage = isset( $posts_usage[ $doc_type_common ] ) ? $posts_usage[ $doc_type_common ] : 0;
return is_array( $doc_usage ) ? $doc_usage['publish'] : $doc_usage;
}
/**
* Get formatted usage.
*
* Retrieve formatted usage, for frontend.
*
* @param String format
*
* @return array
*/
public function get_formatted_usage( $format = 'html' ) {
$usage = [];
foreach ( get_option( self::OPTION_NAME, [] ) as $doc_type => $elements ) {
$doc_class = Plugin::$instance->documents->get_document_type( $doc_type );
if ( 'html' === $format && $doc_class ) {
$doc_title = $doc_class::get_title();
} else {
$doc_title = $doc_type;
}
$doc_count = $this->get_doc_type_count( $doc_class, $doc_type );
$tab_group = $doc_class::get_property( 'admin_tab_group' );
if ( 'html' === $format && $tab_group ) {
$doc_title = ucwords( $tab_group ) . ' - ' . $doc_title;
}
// Replace element type with element title.
foreach ( $elements as $element_type => $data ) {
unset( $elements[ $element_type ] );
if ( in_array( $element_type, [ 'section', 'column' ], true ) ) {
continue;
}
$widget_instance = Plugin::$instance->widgets_manager->get_widget_types( $element_type );
if ( 'html' === $format && $widget_instance ) {
$widget_title = $widget_instance->get_title();
} else {
$widget_title = $element_type;
}
$elements[ $widget_title ] = $data['count'];
}
// Sort elements by key.
ksort( $elements );
$usage[ $doc_type ] = [
'title' => $doc_title,
'elements' => $elements,
'count' => $doc_count,
];
// ' ? 1 : 0;' In sorters is compatibility for PHP8.0.
// Sort usage by title.
uasort( $usage, function( $a, $b ) {
return ( $a['title'] > $b['title'] ) ? 1 : 0;
} );
// If title includes '-' will have lower priority.
uasort( $usage, function( $a ) {
return strpos( $a['title'], '-' ) ? 1 : 0;
} );
}
return $usage;
}
/**
* Before document Save.
*
* Called on elementor/document/before_save, remove document from global & set saving flag.
*
* @param Document $document
* @param array $data new settings to save.
*/
public function before_document_save( $document, $data ) {
$current_status = get_post_status( $document->get_post() );
$new_status = isset( $data['settings']['post_status'] ) ? $data['settings']['post_status'] : '';
if ( $current_status === $new_status ) {
$this->remove_from_global( $document );
}
$this->is_document_saving = true;
}
/**
* After document save.
*
* Called on elementor/document/after_save, adds document to global & clear saving flag.
*
* @param Document $document
*/
public function after_document_save( $document ) {
if ( Document::STATUS_PUBLISH === $document->get_post()->post_status || Document::STATUS_PRIVATE === $document->get_post()->post_status ) {
$this->save_document_usage( $document );
}
$this->is_document_saving = false;
}
/**
* On status change.
*
* Called on transition_post_status.
*
* @param string $new_status
* @param string $old_status
* @param \WP_Post $post
*/
public function on_status_change( $new_status, $old_status, $post ) {
if ( wp_is_post_autosave( $post ) ) {
return;
}
// If it's from elementor editor, the usage should be saved via `before_document_save`/`after_document_save`.
if ( $this->is_document_saving ) {
return;
}
$document = Plugin::$instance->documents->get( $post->ID );
if ( ! $document ) {
return;
}
$is_public_unpublish = 'publish' === $old_status && 'publish' !== $new_status;
$is_private_unpublish = 'private' === $old_status && 'private' !== $new_status;
if ( $is_public_unpublish || $is_private_unpublish ) {
$this->remove_from_global( $document );
}
$is_public_publish = 'publish' !== $old_status && 'publish' === $new_status;
$is_private_publish = 'private' !== $old_status && 'private' === $new_status;
if ( $is_public_publish || $is_private_publish ) {
$this->save_document_usage( $document );
}
}
/**
* On before delete post.
*
* Called on on_before_delete_post.
*
* @param int $post_id
*/
public function on_before_delete_post( $post_id ) {
$document = Plugin::$instance->documents->get( $post_id );
if ( $document->get_id() !== $document->get_main_id() ) {
return;
}
$this->remove_from_global( $document );
}
/**
* Add's tracking data.
*
* Called on elementor/tracker/send_tracking_data_params.
*
* @param array $params
*
* @return array
*/
public function add_tracking_data( $params ) {
$params['usages']['elements'] = get_option( self::OPTION_NAME );
return $params;
}
/**
* Recalculate usage.
*
* Recalculate usage for all elementor posts.
*
* @param int $limit
* @param int $offset
*
* @return int
*/
public function recalc_usage( $limit = -1, $offset = 0 ) {
// While requesting recalc_usage, data should be deleted.
// if its in a batch the data should be deleted only on the first batch.
if ( 0 === $offset ) {
delete_option( self::OPTION_NAME );
}
$post_types = get_post_types( array( 'public' => true ) );
$query = new \WP_Query( [
'meta_key' => '_elementor_data',
'post_type' => $post_types,
'post_status' => [ 'publish', 'private' ],
'posts_per_page' => $limit,
'offset' => $offset,
] );
foreach ( $query->posts as $post ) {
$document = Plugin::$instance->documents->get( $post->ID );
if ( ! $document ) {
continue;
}
$this->after_document_save( $document );
}
// Clear query memory before leave.
wp_cache_flush();
return count( $query->posts );
}
/**
* Increase controls count.
*
* Increase controls count, for each element.
*
* @param array &$element_ref
* @param string $tab
* @param string $section
* @param string $control
* @param int $count
*/
private function increase_controls_count( &$element_ref, $tab, $section, $control, $count ) {
if ( ! isset( $element_ref['controls'][ $tab ] ) ) {
$element_ref['controls'][ $tab ] = [];
}
if ( ! isset( $element_ref['controls'][ $tab ][ $section ] ) ) {
$element_ref['controls'][ $tab ][ $section ] = [];
}
if ( ! isset( $element_ref['controls'][ $tab ][ $section ][ $control ] ) ) {
$element_ref['controls'][ $tab ][ $section ][ $control ] = 0;
}
$element_ref['controls'][ $tab ][ $section ][ $control ] += $count;
}
/**
* Add Controls
*
* Add's controls to this element_ref, returns changed controls count.
*
* @param array $settings_controls
* @param array $element_controls
* @param array &$element_ref
*
* @return int ($changed_controls_count).
*/
private function add_controls( $settings_controls, $element_controls, &$element_ref ) {
$changed_controls_count = 0;
// Loop over all element settings.
foreach ( $settings_controls as $control => $value ) {
if ( empty( $element_controls[ $control ] ) ) {
continue;
}
$control_config = $element_controls[ $control ];
if ( ! isset( $control_config['section'], $control_config['default'] ) ) {
continue;
}
$tab = $control_config['tab'];
$section = $control_config['section'];
// If setting value is not the control default.
if ( $value !== $control_config['default'] ) {
$this->increase_controls_count( $element_ref, $tab, $section, $control, 1 );
$changed_controls_count++;
}
}
return $changed_controls_count;
}
/**
* Add general controls.
*
* Extract general controls to element ref, return clean `$settings_control`.
*
* @param array $settings_controls
* @param array &$element_ref
*
* @return array ($settings_controls).
*/
private function add_general_controls( $settings_controls, &$element_ref ) {
if ( ! empty( $settings_controls[ Manager::DYNAMIC_SETTING_KEY ] ) ) {
$settings_controls = array_merge( $settings_controls, $settings_controls[ Manager::DYNAMIC_SETTING_KEY ] );
// Add dynamic count to controls under `general` tab.
$this->increase_controls_count(
$element_ref,
self::GENERAL_TAB,
Manager::DYNAMIC_SETTING_KEY,
'count',
count( $settings_controls[ Manager::DYNAMIC_SETTING_KEY ] )
);
}
return $settings_controls;
}
/**
* Add to global.
*
* Add's usage to global (update database).
*
* @param string $doc_name
* @param array $doc_usage
*/
private function add_to_global( $doc_name, $doc_usage ) {
$global_usage = get_option( self::OPTION_NAME, [] );
foreach ( $doc_usage as $element_type => $element_data ) {
if ( ! isset( $global_usage[ $doc_name ] ) ) {
$global_usage[ $doc_name ] = [];
}
if ( ! isset( $global_usage[ $doc_name ][ $element_type ] ) ) {
$global_usage[ $doc_name ][ $element_type ] = [
'count' => 0,
'controls' => [],
];
}
$global_element_ref = &$global_usage[ $doc_name ][ $element_type ];
$global_element_ref['count'] += $element_data['count'];
if ( empty( $element_data['controls'] ) ) {
continue;
}
foreach ( $element_data['controls'] as $tab => $sections ) {
foreach ( $sections as $section => $controls ) {
foreach ( $controls as $control => $count ) {
$this->increase_controls_count( $global_element_ref, $tab, $section, $control, $count );
}
}
}
}
update_option( self::OPTION_NAME, $global_usage, false );
}
/**
* Remove from global.
*
* Remove's usage from global (update database).
*
* @param Document $document
*/
private function remove_from_global( $document ) {
$prev_usage = $document->get_meta( self::META_KEY );
if ( empty( $prev_usage ) ) {
return;
}
$doc_name = $document->get_name();
$global_usage = get_option( self::OPTION_NAME, [] );
foreach ( $prev_usage as $element_type => $doc_value ) {
if ( isset( $global_usage[ $doc_name ][ $element_type ]['count'] ) ) {
$global_usage[ $doc_name ][ $element_type ]['count'] -= $prev_usage[ $element_type ]['count'];
if ( 0 === $global_usage[ $doc_name ][ $element_type ]['count'] ) {
unset( $global_usage[ $doc_name ][ $element_type ] );
if ( 0 === count( $global_usage[ $doc_name ] ) ) {
unset( $global_usage[ $doc_name ] );
}
continue;
}
foreach ( $prev_usage[ $element_type ]['controls'] as $tab => $sections ) {
foreach ( $sections as $section => $controls ) {
foreach ( $controls as $control => $count ) {
if ( isset( $global_usage[ $doc_name ][ $element_type ]['controls'][ $tab ][ $section ][ $control ] ) ) {
$section_ref = &$global_usage[ $doc_name ][ $element_type ]['controls'][ $tab ][ $section ];
$section_ref[ $control ] -= $count;
if ( 0 === $section_ref[ $control ] ) {
unset( $section_ref[ $control ] );
}
}
}
}
}
}
}
update_option( self::OPTION_NAME, $global_usage, false );
$document->delete_meta( self::META_KEY );
}
/**
* Get elements usage.
*
* Get's the current elements usage by passed elements array parameter.
*
* @param array $elements
*
* @return array
*/
private function get_elements_usage( $elements ) {
$usage = [];
Plugin::$instance->db->iterate_data( $elements, function ( $element ) use ( &$usage ) {
if ( empty( $element['widgetType'] ) ) {
$type = $element['elType'];
$element_instance = Plugin::$instance->elements_manager->get_element_types( $type );
} else {
$type = $element['widgetType'];
$element_instance = Plugin::$instance->widgets_manager->get_widget_types( $type );
}
if ( ! isset( $usage[ $type ] ) ) {
$usage[ $type ] = [
'count' => 0,
'control_percent' => 0,
'controls' => [],
];
}
$usage[ $type ]['count']++;
if ( ! $element_instance ) {
return $element;
}
$element_controls = $element_instance->get_controls();
if ( isset( $element['settings'] ) ) {
$settings_controls = $element['settings'];
$element_ref = &$usage[ $type ];
// Add dynamic values.
$settings_controls = $this->add_general_controls( $settings_controls, $element_ref );
$changed_controls_count = $this->add_controls( $settings_controls, $element_controls, $element_ref );
$percent = $changed_controls_count / ( count( $element_controls ) / 100 );
$usage[ $type ] ['control_percent'] = (int) round( $percent );
}
return $element;
} );
return $usage;
}
/**
* Save document usage.
*
* Save requested document usage, and update global.
*
* @param Document $document
*/
private function save_document_usage( Document $document ) {
if ( ! $document::get_property( 'is_editable' ) && ! $document->is_built_with_elementor() ) {
return;
}
// Get data manually to avoid conflict with `\Elementor\Core\Base\Document::get_elements_data... convert_to_elementor`.
$data = $document->get_json_meta( '_elementor_data' );
if ( ! empty( $data ) ) {
try {
$usage = $this->get_elements_usage( $document->get_elements_raw_data( $data ) );
$document->update_meta( self::META_KEY, $usage );
$this->add_to_global( $document->get_name(), $usage );
} catch ( \Exception $exception ) {
return; // Do nothing.
};
}
}
/**
* Add system info report.
*/
public function add_system_info_report() {
System_Info::add_report( 'usage', [
'file_name' => __DIR__ . '/usage-reporter.php',
'class_name' => __NAMESPACE__ . '\Usage_Reporter',
] );
}
/**
* Usage module constructor.
*
* Initializing Elementor usage module.
*
* @access public
*/
public function __construct() {
add_action( 'transition_post_status', [ $this, 'on_status_change' ], 10, 3 );
add_action( 'before_delete_post', [ $this, 'on_before_delete_post' ] );
add_action( 'elementor/document/before_save', [ $this, 'before_document_save' ], 10, 2 );
add_action( 'elementor/document/after_save', [ $this, 'after_document_save' ] );
add_filter( 'elementor/tracker/send_tracking_data_params', [ $this, 'add_tracking_data' ] );
add_action( 'admin_init', [ $this, 'add_system_info_report' ], 50 );
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace Elementor\Modules\Usage;
use Elementor\Modules\System_Info\Reporters\Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor usage report.
*
* Elementor system report handler class responsible for generating a report for
* the user.
*/
class Usage_Reporter extends Base {
const RECALC_ACTION = 'elementor_usage_recalc';
public function get_title() {
$title = 'Elements Usage';
if ( 'html' === $this->_properties['format'] ) {
if ( empty( $_GET[ self::RECALC_ACTION ] ) ) { // phpcs:ignore -- nonce validation is not required here.
$nonce = wp_create_nonce( self::RECALC_ACTION );
$url = add_query_arg( [
self::RECALC_ACTION => 1,
'_wpnonce' => $nonce,
] );
$title .= '<a id="elementor-usage-recalc" href="' . esc_url( $url ) . '#elementor-usage-recalc" class="box-title-tool">Recalculate</a>';
} else {
$title .= $this->get_remove_recalc_query_string_script();
}
}
return $title;
}
public function get_fields() {
return [
'usage' => '',
];
}
public function get_usage() {
/** @var Module $module */
$module = Module::instance();
if ( ! empty( $_GET[ self::RECALC_ACTION ] ) ) {
if ( empty( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], self::RECALC_ACTION ) ) {
wp_die( 'Invalid Nonce', 'Invalid Nonce', [
'back_link' => true,
] );
}
$module->recalc_usage();
}
$usage = '';
foreach ( $module->get_formatted_usage() as $doc_type => $data ) {
$usage .= '<tr><td>' . $data['title'] . ' ( ' . $data['count'] . ' )</td><td>';
foreach ( $data['elements'] as $element => $count ) {
$usage .= $element . ': ' . $count . PHP_EOL;
}
$usage .= '</td></tr>';
}
return [
'value' => $usage,
];
}
public function get_raw_usage() {
/** @var Module $module */
$module = Module::instance();
$usage = PHP_EOL;
foreach ( $module->get_formatted_usage( 'raw' ) as $doc_type => $data ) {
$usage .= "\t{$data['title']} : " . $data['count'] . PHP_EOL;
foreach ( $data['elements'] as $element => $count ) {
$usage .= "\t\t{$element} : {$count}" . PHP_EOL;
}
}
return [
'value' => $usage,
];
}
/**
* Removes the "elementor_usage_recalc" param from the query string to avoid recalc every refresh.
* When using a redirect header in place of this approach it throws an error because some components have already output some content.
*
* @return string
*/
private function get_remove_recalc_query_string_script() {
ob_start();
?>
<script>
// Origin file: modules/usage/usage-reporter.php - get_remove_recalc_query_string_script()
{
const url = new URL( window.location );
url.hash = '';
url.searchParams.delete( 'elementor_usage_recalc' );
url.searchParams.delete( '_wpnonce' );
history.replaceState( '', window.title, url.toString() );
}
</script>
<?php
return ob_get_clean();
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Elementor\Modules\WpCli;
use Elementor\Core\Logger\Loggers\Db;
use Elementor\Core\Logger\Items\Log_Item_Interface as Log_Item_Interface;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Cli_Logger extends Db {
public function save_log( Log_Item_Interface $item ) {
$message = $item->format( 'raw' );
switch ( $item->type ) {
case self::LEVEL_WARNING:
\WP_CLI::warning( $message );
break;
case self::LEVEL_ERROR:
\WP_CLI::error( $message, false );
break;
default:
\WP_CLI::log( $message );
break;
}
parent::save_log( $item );
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace Elementor\Modules\WpCli;
use Elementor\Api;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor Page Builder cli tools.
*/
class Command extends \WP_CLI_Command {
/**
* Flush the Elementor Page Builder CSS Cache.
*
* [--network]
* Flush CSS Cache for all the sites in the network.
*
* ## EXAMPLES
*
* 1. wp elementor flush-css
* - This will flush the CSS files for elementor page builder.
*
* 2. wp elementor flush-css --network
* - This will flush the CSS files for elementor page builder for all the sites in the network.
*
* @since 2.1.0
* @access public
* @alias flush-css
*/
public function flush_css( $args, $assoc_args ) {
$network = ! empty( $assoc_args['network'] ) && is_multisite();
if ( $network ) {
/** @var \WP_Site[] $blogs */
$blogs = get_sites();
foreach ( $blogs as $keys => $blog ) {
// Cast $blog as an array instead of object
$blog_id = $blog->blog_id;
switch_to_blog( $blog_id );
Plugin::$instance->files_manager->clear_cache();
\WP_CLI::success( 'Flushed the Elementor CSS Cache for site - ' . get_option( 'home' ) );
restore_current_blog();
}
} else {
Plugin::$instance->files_manager->clear_cache();
\WP_CLI::success( 'Flushed the Elementor CSS Cache' );
}
}
/**
* Print system info powered by Elementor
*
* ## EXAMPLES
*
* 1. wp elementor system-info
* - This will print the System Info in JSON format
*
* @since 3.0.11
* @access public
* @alias system-info
*/
public function system_info() {
echo wp_json_encode( \Elementor\Tracker::get_tracking_data() );
}
/**
* Replace old URLs with new URLs in all Elementor pages.
*
* [--force]
* Suppress error messages. instead, return "0 affected rows.".
*
* ## EXAMPLES
*
* 1. wp elementor replace-urls <old> <new>
* - This will replace all <old> URLs with the <new> URL.
*
* 2. wp elementor replace-urls <old> <new> --force
* - This will replace all <old> URLs with the <new> URL without throw errors.
*
* @access public
* @alias replace-urls
*/
public function replace_urls( $args, $assoc_args ) {
if ( empty( $args[0] ) ) {
\WP_CLI::error( 'Please set the `old` URL' );
}
if ( empty( $args[1] ) ) {
\WP_CLI::error( 'Please set the `new` URL' );
}
try {
$results = Utils::replace_urls( $args[0], $args[1] );
\WP_CLI::success( $results );
} catch ( \Exception $e ) {
if ( isset( $assoc_args['force'] ) ) {
\WP_CLI::success( '0 rows affected.' );
} else {
\WP_CLI::error( $e->getMessage() );
}
}
}
/**
* Sync Elementor Library.
*
* ## EXAMPLES
*
* 1. wp elementor sync-library
* - This will sync the library with Elementor cloud library.
*
* @since 2.1.0
* @access public
* @alias sync-library
*/
public function sync_library( $args, $assoc_args ) {
// TODO:
// \WP_CLI::warning( 'command is deprecated since 2.8.0 Please use: wp elementor library sync' );
$data = Api::get_library_data( true );
if ( empty( $data ) ) {
\WP_CLI::error( 'Cannot sync library.' );
}
\WP_CLI::success( 'Library has been synced.' );
}
/**
* Import template files to the Library.
*
* ## EXAMPLES
*
* 1. wp elementor import-library <file-path>
* - This will import a file or a zip of multiple files to the library.
*
* @since 2.1.0
* @access public
* @alias import-library
*/
public function import_library( $args, $assoc_args ) {
// TODO:
// \WP_CLI::warning( 'command is deprecated since 2.8.0 Please use: wp elementor library import' );
if ( empty( $args[0] ) ) {
\WP_CLI::error( 'Please set file path.' );
}
/** @var Source_Local $source */
$source = Plugin::$instance->templates_manager->get_source( 'local' );
$imported_items = $source->import_template( basename( $args[0] ), $args[0] );
if ( is_wp_error( $imported_items ) ) {
\WP_CLI::error( $imported_items->get_error_message() );
}
\WP_CLI::success( count( $imported_items ) . ' item(s) has been imported.' );
}
}

View File

@@ -0,0 +1,189 @@
<?php
namespace Elementor\Modules\WpCli;
use Elementor\Api;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor Page Builder cli tools.
*/
class Library extends \WP_CLI_Command {
/**
* Sync Elementor Library.
*
* [--network]
* Sync library in all the sites in the network.
*
* [--force]
* Force sync even if it's looks like that the library is already up to date.
*
* ## EXAMPLES
*
* 1. wp elementor library sync
* - This will sync the library with Elementor cloud library.
*
* 2. wp elementor library sync --force
* - This will sync the library with Elementor cloud even if it's looks like that the library is already up to date.
*
* 3. wp elementor library sync --network
* - This will sync the library with Elementor cloud library for each site in the network if needed.
*
* @since 2.8.0
* @access public
*/
public function sync( $args, $assoc_args ) {
$network = isset( $assoc_args['network'] ) && is_multisite();
if ( $network ) {
/** @var \WP_Site[] $sites */
$sites = get_sites();
foreach ( $sites as $keys => $blog ) {
// Cast $blog as an array instead of object
$blog_id = $blog->blog_id;
switch_to_blog( $blog_id );
\WP_CLI::line( 'Site #' . $blog_id . ' - ' . get_option( 'blogname' ) );
$this->do_sync( isset( $assoc_args['force'] ) );
\WP_CLI::success( 'Done! - ' . get_option( 'home' ) );
restore_current_blog();
}
} else {
$this->do_sync( isset( $assoc_args['force'] ) );
\WP_CLI::success( 'Done!' );
}
}
/**
* Import template files to the Library.
*
* ## EXAMPLES
*
* 1. wp elementor library import <file-path>
* - This will import a file or a zip of multiple files to the library.
*
* @param $args
* @param $assoc_args
*
* @since 2.8.0
* @access public
*/
public function import( $args ) {
if ( empty( $args[0] ) ) {
\WP_CLI::error( 'Please set file path.' );
}
$file = $args[0];
/** @var Source_Local $source */
$source = Plugin::$instance->templates_manager->get_source( 'local' );
$imported_items = $source->import_template( basename( $file ), $file );
if ( is_wp_error( $imported_items ) ) {
\WP_CLI::error( $imported_items->get_error_message() );
}
\WP_CLI::success( count( $imported_items ) . ' item(s) has been imported.' );
}
/**
* Connect site to Elementor Library.
* (Network is not supported)
*
* --user
* The user to connect <id|login|email>
*
* --token
* A connect token from Elementor Account Dashboard.
*
* ## EXAMPLES
*
* 1. wp elementor library connect --user=admin --token=<connect-cli-token>
* - This will connect the admin to Elementor library.
*
* @param $args
* @param $assoc_args
*
* @since 2.8.0
* @access public
*/
public function connect( $args, $assoc_args ) {
if ( ! get_current_user_id() ) {
\WP_CLI::error( 'Please set user to connect (--user=<id|login|email>).' );
}
if ( empty( $assoc_args['token'] ) ) {
\WP_CLI::error( 'Please set connect token.' );
}
$_REQUEST['mode'] = 'cli';
$_REQUEST['token'] = $assoc_args['token'];
$app = $this->get_library_app();
$app->action_authorize();
$app->action_get_token();
}
/**
* Disconnect site from Elementor Library.
*
* --user
* The user to disconnect <id|login|email>
*
* ## EXAMPLES
*
* 1. wp elementor library disconnect --user=admin
* - This will disconnect the admin from Elementor library.
*
* @param $args
* @param $assoc_args
*
* @since 2.8.0
* @access public
*/
public function disconnect() {
if ( ! get_current_user_id() ) {
\WP_CLI::error( 'Please set user to connect (--user=<id|login|email>).' );
}
$_REQUEST['mode'] = 'cli';
$this->get_library_app()->action_disconnect();
}
private function do_sync() {
$data = Api::get_library_data( true );
if ( empty( $data ) ) {
\WP_CLI::error( 'Cannot sync library.' );
}
}
/**
* @return \Elementor\Core\Common\Modules\Connect\Apps\Library
*/
private function get_library_app() {
$connect = Plugin::$instance->common->get_component( 'connect' );
$app = $connect->get_app( 'library' );
// Before init.
if ( ! $app ) {
$connect->init();
$app = $connect->get_app( 'library' );
}
return $app;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Elementor\Modules\WpCli;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Logger\Manager as Logger;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Module extends BaseModule {
/**
* Get module name.
*
* @since 2.0.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'wp-cli';
}
/**
* @since 2.1.0
* @access public
* @static
*/
public static function is_active() {
return defined( 'WP_CLI' ) && WP_CLI;
}
/**
* @param Logger $logger
* @access public
*/
public function register_cli_logger( $logger ) {
$logger->register_logger( 'cli', 'Elementor\Modules\WpCli\Cli_Logger' );
$logger->set_default_logger( 'cli' );
}
public function init_common() {
Plugin::$instance->init_common();
}
/**
*
* @since 2.1.0
* @access public
*/
public function __construct() {
add_action( 'cli_init', [ $this, 'init_common' ] );
add_action( 'elementor/loggers/register', [ $this, 'register_cli_logger' ] );
\WP_CLI::add_command( 'elementor', '\Elementor\Modules\WpCli\Command' );
\WP_CLI::add_command( 'elementor update', '\Elementor\Modules\WpCli\Update' );
\WP_CLI::add_command( 'elementor library', '\Elementor\Modules\WpCli\Library' );
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Elementor\Modules\WpCli;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Elementor Page Builder cli tools.
*/
class Update extends \WP_CLI_Command {
/**
* Update the DB after plugin upgrade.
*
* [--network]
* Update DB in all the sites in the network.
*
* [--force]
* Force update even if it's looks like that update is in progress.
*
*
* ## EXAMPLES
*
* 1. wp elementor update db
* - This will Upgrade the DB if needed.
*
* 2. wp elementor update db --force
* - This will Upgrade the DB even if another process is running.
*
* 3. wp elementor update db --network
* - This will Upgrade the DB for each site in the network if needed.
*
* @since 2.4.0
* @access public
*
* @param $args
* @param $assoc_args
*/
public function db( $args, $assoc_args ) {
$network = ! empty( $assoc_args['network'] ) && is_multisite();
if ( $network ) {
/** @var \WP_Site[] $sites */
$sites = get_sites();
foreach ( $sites as $keys => $blog ) {
// Cast $blog as an array instead of object
$blog_id = $blog->blog_id;
switch_to_blog( $blog_id );
\WP_CLI::line( 'Site #' . $blog_id . ' - ' . get_option( 'blogname' ) );
$this->do_db_upgrade( $assoc_args );
\WP_CLI::success( 'Done! - ' . get_option( 'home' ) );
restore_current_blog();
}
} else {
$this->do_db_upgrade( $assoc_args );
}
}
protected function get_update_db_manager_class() {
return '\Elementor\Core\Upgrade\Manager';
}
protected function do_db_upgrade( $assoc_args ) {
$manager_class = $this->get_update_db_manager_class();
/** @var \Elementor\Core\Upgrade\Manager $manager */
$manager = new $manager_class();
$updater = $manager->get_task_runner();
if ( $updater->is_process_locked() && empty( $assoc_args['force'] ) ) {
\WP_CLI::warning( 'Oops! Process is already running. Use --force to force run.' );
return;
}
if ( ! $manager->should_upgrade() ) {
\WP_CLI::success( 'The DB is already updated!' );
return;
}
$callbacks = $manager->get_upgrade_callbacks();
$did_tasks = false;
if ( ! empty( $callbacks ) ) {
Plugin::$instance->logger->get_logger()->info( 'Update DB has been started', [
'meta' => [
'plugin' => $manager->get_plugin_label(),
'from' => $manager->get_current_version(),
'to' => $manager->get_new_version(),
],
] );
$updater->handle_immediately( $callbacks );
$did_tasks = true;
}
$manager->on_runner_complete( $did_tasks );
\WP_CLI::success( count( $callbacks ) . ' updates(s) has been applied.' );
}
}