Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions inc/compat/class-elementor-compat.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,52 @@ public function init(): void {
}

/**
* Makes sure we force elementor to regenerate the styles when necessary.
* Makes sure we force Elementor to regenerate styles after site duplication.
*
* Uses the Elementor API when available, otherwise clears the CSS cache
* via direct database operations. This fallback is important because
* Elementor classes are typically not loaded in the network admin
* context where site duplication runs.
*
* @since 1.10.10
* @param array $site Info about the duplicated site.
* @return void
*/
public function regenerate_css($site): void {

if ( ! class_exists('\Elementor\Plugin')) {
return;
}

if ( ! isset($site['site_id'])) {
return;
}

switch_to_blog($site['site_id']);

$file_manager = \Elementor\Plugin::$instance->files_manager; // phpcs:ignore
// Try the Elementor API if available.
if (class_exists('\Elementor\Plugin') && ! empty(\Elementor\Plugin::$instance->files_manager)) {
\Elementor\Plugin::$instance->files_manager->clear_cache(); // phpcs:ignore
restore_current_blog();

if ( ! empty($file_manager)) {
$file_manager->clear_cache();
return;
}

// Fallback: clear Elementor CSS cache via direct DB operations.
// Duplication typically runs in the network admin context where
// Elementor classes are not loaded — this ensures the compiled
// CSS is regenerated on the first visit to the cloned site.
global $wpdb;

// Delete compiled CSS metadata — Elementor will regenerate on next load.
$wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->postmeta,
['meta_key' => '_elementor_css'], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
['%s']
);

// Clear the global CSS option so Elementor rebuilds it.
delete_option('_elementor_global_css');

// Reset the CSS print timestamp to force full regeneration.
delete_option('elementor_css_print_method');

restore_current_blog();
}

Expand Down
105 changes: 89 additions & 16 deletions inc/duplication/data.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

// Copy
$saved_options = self::db_copy_tables($from_site_id, $to_site_id);
$blog_meta = self::db_copy_blog_meta($from_site_id, $to_site_id);

Check warning on line 31 in inc/duplication/data.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Result of static method MUCD_Data::db_copy_blog_meta() (void) is used.
// Update
self::db_update_data($from_site_id, $to_site_id, $saved_options);
}
Expand Down Expand Up @@ -286,9 +286,42 @@
restore_current_blog();
}

/**
* Get the primary key column name for a table.
*
* Uses a static cache to avoid repeated SHOW KEYS queries for the
* same table across multiple replacement passes.
*
* @since 2.3.1
* @param string $table Full table name.
* @return string|null Primary key column name, or null if not found.
*/
public static function get_primary_key($table) {
static $cache = [];

if (array_key_exists($table, $cache)) {
return $cache[ $table ];
}

global $wpdb;

$row = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
"SHOW KEYS FROM `{$table}` WHERE Key_name = 'PRIMARY'" // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
);

$cache[ $table ] = $row ? $row->Column_name : null;

return $cache[ $table ];
}

/**
* Updates a table
*
* Identifies rows by primary key when available so that large
* serialized values (like Elementor Kit settings) are not used
* in the WHERE clause—preventing mismatches and accidental
* multi-row updates.
*
* @since 0.2.0
* @param string $table Table to update.
* @param array $fields Fields to update.
Expand All @@ -299,29 +332,55 @@
if (is_array($fields) || ! empty($fields)) {
global $wpdb;

$pk_column = self::get_primary_key($table);

foreach ($fields as $field) {

// Bugfix : escape '_' , '%' and '/' character for mysql 'like' queries
$from_string_like = $wpdb->esc_like($from_string);

$results = $wpdb->query("SET SQL_MODE='ALLOW_INVALID_DATES';"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

$sql_query = $wpdb->prepare(
'
SELECT `' . $field . '` FROM `' . $table . '` WHERE `' . $field . '` LIKE %s ', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedSimplePlaceholder
'%' . $from_string_like . '%'
);
$wpdb->query("SET SQL_MODE='ALLOW_INVALID_DATES';"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

// Include primary key in SELECT when available for reliable row identification.
if ($pk_column) {
$sql_query = $wpdb->prepare(
'SELECT `' . $pk_column . '`, `' . $field . '` FROM `' . $table . '` WHERE `' . $field . '` LIKE %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedSimplePlaceholder
'%' . $from_string_like . '%'
);
} else {
$sql_query = $wpdb->prepare(
'SELECT `' . $field . '` FROM `' . $table . '` WHERE `' . $field . '` LIKE %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedSimplePlaceholder
'%' . $from_string_like . '%'
);
}

$results = self::do_sql_query($sql_query, 'results', false);

if ($results) {
$update = 'UPDATE `' . $table . '` SET `' . $field . '` = %s WHERE `' . $field . '` = %s'; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

foreach ($results as $result => $row) {
foreach ($results as $row) {
$old_value = $row[ $field ];
$new_value = self::try_replace($row, $field, $from_string, $to_string);
$sql_query = $wpdb->prepare($update, $new_value, $old_value); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$results = self::do_sql_query($sql_query);

// Skip UPDATE when the replacement produced no change.
if ($new_value === $old_value) {
continue;
}

if ($pk_column && isset($row[ $pk_column ])) {
$update_sql = $wpdb->prepare(
'UPDATE `' . $table . '` SET `' . $field . '` = %s WHERE `' . $pk_column . '` = %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$new_value,
$row[ $pk_column ]
);
} else {
$update_sql = $wpdb->prepare(
'UPDATE `' . $table . '` SET `' . $field . '` = %s WHERE `' . $field . '` = %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$new_value,
$old_value
);
}

self::do_sql_query($update_sql);
}
}
}
Expand Down Expand Up @@ -389,13 +448,27 @@
*/
public static function try_replace($row, $field, $from_string, $to_string) {
if (is_serialized($row[ $field ])) {
$double_serialize = false;
$row[ $field ] = @unserialize($row[ $field ]);
$double_serialize = false;
$original_value = $row[ $field ];
$row[ $field ] = @unserialize($row[ $field ]);

// Safety: if unserialize failed, return the original value
// instead of re-serializing false — which would destroy the data.
if (false === $row[ $field ] && 'b:0;' !== $original_value) {
return $original_value;
}

// FOR SERIALISED OPTIONS, like in wp_carousel plugin
if (is_serialized($row[ $field ])) {
$row[ $field ] = @unserialize($row[ $field ]);
$double_serialize = true;
$inner_unserialized = @unserialize($row[ $field ]);

if (false === $inner_unserialized && 'b:0;' !== $row[ $field ]) {
// Inner unserialize failed — fall back to single-serialized handling.
$double_serialize = false;
} else {
$row[ $field ] = $inner_unserialized;
$double_serialize = true;
}
}

if (is_array($row[ $field ])) {
Expand Down
Loading
Loading