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
46 changes: 35 additions & 11 deletions inc/duplication/data.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?php

Check failure on line 1 in inc/duplication/data.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Missing file doc comment

Check failure on line 1 in inc/duplication/data.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Class file names should be based on the class name with "class-" prepended. Expected class-mucd-data.php, but found data.php.

use Psr\Log\LogLevel;

Expand Down Expand Up @@ -229,13 +229,30 @@
$from_site_prefix = $wpdb->get_blog_prefix($from_site_id);
$to_site_prefix = $wpdb->get_blog_prefix($to_site_id);

$from_upload_url_clean = wu_replace_scheme($from_upload_url);
$to_upload_url_clean = wu_replace_scheme($to_upload_url);
$from_upload_url_w_network_clean = wu_replace_scheme($from_upload_url_w_network);
$to_upload_url_w_network_clean = wu_replace_scheme($to_upload_url_w_network);
$from_blog_url_clean = wu_replace_scheme($from_blog_url);
$to_blog_url_clean = wu_replace_scheme($to_blog_url);

$string_to_replace = [
wu_replace_scheme($from_upload_url) => wu_replace_scheme($to_upload_url),
wu_replace_scheme($from_upload_url_w_network) => wu_replace_scheme($to_upload_url_w_network),
wu_replace_scheme($from_blog_url) => wu_replace_scheme($to_blog_url),
$from_site_prefix => $to_site_prefix,
$from_upload_url_clean => $to_upload_url_clean,
$from_upload_url_w_network_clean => $to_upload_url_w_network_clean,
$from_blog_url_clean => $to_blog_url_clean,
$from_site_prefix => $to_site_prefix,
];

// Add JSON-escaped versions of URLs (forward slashes escaped as \/).
// Page builders like Elementor store URLs in JSON where / becomes \/.
$json_replacements = [
str_replace('/', '\\/', $from_upload_url_clean) => str_replace('/', '\\/', $to_upload_url_clean),
str_replace('/', '\\/', $from_upload_url_w_network_clean) => str_replace('/', '\\/', $to_upload_url_w_network_clean),
str_replace('/', '\\/', $from_blog_url_clean) => str_replace('/', '\\/', $to_blog_url_clean),
];

$string_to_replace = array_merge($string_to_replace, $json_replacements);

$string_to_replace = apply_filters('mucd_string_to_replace', $string_to_replace, $from_site_id, $to_site_id);

foreach ( $tables as $table => $field) {
Expand Down Expand Up @@ -313,7 +330,6 @@

/**
* Replace $from_string with $to_string in $val
* Warning : if $to_string already in $val, no replacement is made
*
* @since 0.2.0
* @param string $val Original value to modify.
Expand All @@ -322,15 +338,11 @@
* @return string The new string.
*/
public static function replace($val, $from_string, $to_string) {
$new = $val;
if (is_string($val)) {
$pos = strpos($val, $to_string);
if (false === $pos) {
$new = str_replace($from_string, $to_string, $val);
}
return str_replace($from_string, $to_string, $val);
}

return $new;
return $val;
}

/**
Expand Down Expand Up @@ -402,6 +414,18 @@
if ($double_serialize) {
$row[ $field ] = serialize($row[ $field ]);
}
} elseif (is_array($row[ $field ])) {
$row[ $field ] = self::replace_recursive($row[ $field ], $from_string, $to_string);
} elseif (is_object($row[ $field ])) {
$array_object = (array) $row[ $field ];
$array_object = self::replace_recursive($array_object, $from_string, $to_string);
foreach ($array_object as $key => $value) {
try {
$row[ $field ]->$key = $value;
} catch (\Throwable $exception) {
// ...nothing
}
}
} else {
$row[ $field ] = self::replace($row[ $field ], $from_string, $to_string);
}
Expand Down
287 changes: 287 additions & 0 deletions tests/WP_Ultimo/Duplication/MUCD_Data_Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
<?php

Check failure on line 1 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Class file names should be based on the class name with "class-" prepended. Expected class-mucd-data-test.php, but found MUCD_Data_Test.php.

Check failure on line 1 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Filenames should be all lowercase with hyphens as word separators. Expected mucd-data-test.php, but found MUCD_Data_Test.php.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Rename the test file to satisfy naming rules.
CI reports a hard failure: filenames must be lowercase with hyphens and class files should be prefixed with class-. Rename to class-mucd-data-test.php (lowercase) and ensure test discovery still picks it up.

🧰 Tools
🪛 GitHub Actions: Code Quality

[error] 1-1: Filenames should be all lowercase with hyphens as word separators. Expected mucd-data-test.php, but found MUCD_Data_Test.php.

🪛 GitHub Check: Code Quality Checks

[failure] 1-1:
Class file names should be based on the class name with "class-" prepended. Expected class-mucd-data-test.php, but found MUCD_Data_Test.php.


[failure] 1-1:
Filenames should be all lowercase with hyphens as word separators. Expected mucd-data-test.php, but found MUCD_Data_Test.php.

🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Duplication/MUCD_Data_Test.php` at line 1, Rename the test
file from its current mixed-case name to class-mucd-data-test.php (all
lowercase, hyphenated, prefixed with class-) and update any
references/imports/requires to that filename so test discovery still finds it;
keep the test class name (e.g., MUCD_Data_Test) as-is unless your test runner
requires matching case, and verify phpunit.xml or test bootstrap does not
reference the old filename.

/**
* Test case for MUCD_Data replacement methods.
*
* @package WP_Ultimo
* @subpackage Tests
*/

namespace WP_Ultimo\Tests\Duplication;

use WP_UnitTestCase;

// Load the MUCD files.
require_once WP_ULTIMO_PLUGIN_DIR . '/inc/duplication/data.php';

/**
* Test MUCD_Data replacement functionality.
*/
class MUCD_Data_Test extends WP_UnitTestCase {

/**
* Test basic string replacement.
*/
public function test_replace_basic() {
$result = \MUCD_Data::replace(
'Visit example.com/old-site for details',
'example.com/old-site',
'example.com/new-site'
);

$this->assertEquals('Visit example.com/new-site for details', $result);
}

/**
* Test replacement works when to_string already exists in value.
*
* This was the bug: the old code skipped replacement if $to_string
* was found anywhere in $val, which broke subdirectory multisite
* where upload URL replacement happened first.
*/
public function test_replace_when_to_string_already_present() {
// Simulate: upload URL was already replaced, now blog URL replacement runs.
// Value already contains "example.com/new-site" from previous replacement,
// but still has "example.com/old-site" in button URLs.
$val = 'https://example.com/new-site/wp-content/uploads/image.jpg and https://example.com/old-site/contact';

$result = \MUCD_Data::replace(
$val,
'example.com/old-site',
'example.com/new-site'
);

$this->assertEquals(
'https://example.com/new-site/wp-content/uploads/image.jpg and https://example.com/new-site/contact',
$result
);
}

/**
* Test replacement with multiple occurrences of from_string.
*/
public function test_replace_multiple_occurrences() {
$val = 'Link: example.com/old and another: example.com/old/page';

$result = \MUCD_Data::replace($val, 'example.com/old', 'example.com/new');

$this->assertEquals('Link: example.com/new and another: example.com/new/page', $result);
}

/**
* Test replacement with non-string value.
*/
public function test_replace_non_string_returns_unchanged() {
$this->assertEquals(42, \MUCD_Data::replace(42, 'foo', 'bar'));
$this->assertNull(\MUCD_Data::replace(null, 'foo', 'bar'));
$this->assertTrue(\MUCD_Data::replace(true, 'foo', 'bar'));
}

/**
* Test replacement when from_string is not found.
*/
public function test_replace_no_match() {
$val = 'No matching content here';
$result = \MUCD_Data::replace($val, 'example.com/old', 'example.com/new');

$this->assertEquals($val, $result);
}

/**
* Test try_replace with serialized array data.
*/
public function test_try_replace_serialized_array() {
$data = serialize(['url' => 'https://example.com/old-site/page']);

Check warning on line 93 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

serialize() found. Serialized data has known vulnerability problems with Object Injection. JSON is generally a better approach for serializing data. See https://www.owasp.org/index.php/PHP_Object_Injection
$row = ['meta_value' => $data];

Check warning on line 94 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Detected usage of meta_value, possible slow query.

$result = \MUCD_Data::try_replace($row, 'meta_value', 'example.com/old-site', 'example.com/new-site');

$unserialized = unserialize($result);

Check warning on line 98 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

unserialize() found. Serialized data has known vulnerability problems with Object Injection. JSON is generally a better approach for serializing data. See https://www.owasp.org/index.php/PHP_Object_Injection
$this->assertEquals('https://example.com/new-site/page', $unserialized['url']);
}

/**
* Test try_replace with nested serialized data.
*/
public function test_try_replace_nested_serialized_array() {
$data = serialize([

Check failure on line 106 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Opening parenthesis of a multi-line function call must be the last content on the line

Check warning on line 106 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

serialize() found. Serialized data has known vulnerability problems with Object Injection. JSON is generally a better approach for serializing data. See https://www.owasp.org/index.php/PHP_Object_Injection
'settings' => [
'link' => 'https://example.com/old-site/about',
'icon' => 'fa-home',
],
'content' => 'Visit https://example.com/old-site for more',
]);

Check failure on line 112 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Closing parenthesis of a multi-line function call must be on a line by itself
Comment on lines +106 to +112

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix multiline call formatting to satisfy PHPCS.
The code style check requires the opening parenthesis to end the line and the closing parenthesis on its own line for multiline calls.

♻️ Proposed formatting fixes
-		$data = serialize([
+		$data = serialize(
+			[
 			'settings' => [
 				'link' => 'https://example.com/old-site/about',
 				'icon' => 'fa-home',
 			],
 			'content'  => 'Visit https://example.com/old-site for more',
-		]);
+			]
+		);

-		$data = serialize([
+		$data = serialize(
+			[
 			'upload_url' => 'https://example.com/new-site/wp-content/uploads/img.jpg',
 			'button_url' => 'https://example.com/old-site/contact',
 			'page_url'   => 'https://example.com/old-site/about',
-		]);
+			]
+		);

-		$elementor_data = json_encode([
+		$elementor_data = json_encode(
+			[
 			[
 				'elType'   => 'section',
 				'settings' => [],
 				'elements' => [
 					[
 						'elType'   => 'widget',
 						'settings' => [
 							'link' => [
 								'url' => 'https://example.com/old-site/services',
 							],
 							'button_link' => [
 								'url' => 'https://example.com/old-site/contact-us',
 							],
 						],
 					],
 				],
 			],
-		]);
+			]
+		);

Also applies to: 154-158, 177-195

🧰 Tools
🪛 GitHub Check: Code Quality Checks

[failure] 112-112:
Closing parenthesis of a multi-line function call must be on a line by itself


[failure] 106-106:
Opening parenthesis of a multi-line function call must be the last content on the line


[warning] 106-106:
serialize() found. Serialized data has known vulnerability problems with Object Injection. JSON is generally a better approach for serializing data. See https://www.owasp.org/index.php/PHP_Object_Injection

🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Duplication/MUCD_Data_Test.php` around lines 106 - 112, The
multiline serialize call for $data (and the other occurrences referenced)
doesn't follow PHPCS function-call formatting; change calls like serialize([ ...
]); to have the opening parenthesis on its own line after the function name and
the closing parenthesis on its own line, e.g. move to serialize( on its own
line, then the array block, then ) followed by the semicolon; update the
serialize invocation around the $data assignment and the other similar blocks
(the serialize calls at the other referenced ranges) to match this formatting.

$row = ['meta_value' => $data];

Check warning on line 113 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Detected usage of meta_value, possible slow query.

$result = \MUCD_Data::try_replace($row, 'meta_value', 'example.com/old-site', 'example.com/new-site');
$unserialized = unserialize($result);

Check warning on line 116 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

unserialize() found. Serialized data has known vulnerability problems with Object Injection. JSON is generally a better approach for serializing data. See https://www.owasp.org/index.php/PHP_Object_Injection

$this->assertEquals('https://example.com/new-site/about', $unserialized['settings']['link']);
$this->assertEquals('Visit https://example.com/new-site for more', $unserialized['content']);
$this->assertEquals('fa-home', $unserialized['settings']['icon']);
}

/**
* Test try_replace with plain (non-serialized) string.
*/
public function test_try_replace_plain_string() {
$row = ['post_content' => '<a href="https://example.com/old-site/page">Link</a>'];

$result = \MUCD_Data::try_replace($row, 'post_content', 'example.com/old-site', 'example.com/new-site');

$this->assertEquals('<a href="https://example.com/new-site/page">Link</a>', $result);
}

/**
* Test try_replace with double-serialized data.
*/
public function test_try_replace_double_serialized() {
$data = serialize(serialize(['url' => 'https://example.com/old-site/page']));

Check warning on line 138 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

serialize() found. Serialized data has known vulnerability problems with Object Injection. JSON is generally a better approach for serializing data. See https://www.owasp.org/index.php/PHP_Object_Injection

Check warning on line 138 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

serialize() found. Serialized data has known vulnerability problems with Object Injection. JSON is generally a better approach for serializing data. See https://www.owasp.org/index.php/PHP_Object_Injection
$row = ['meta_value' => $data];

Check warning on line 139 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Detected usage of meta_value, possible slow query.

$result = \MUCD_Data::try_replace($row, 'meta_value', 'example.com/old-site', 'example.com/new-site');

$unserialized = unserialize(unserialize($result));
$this->assertEquals('https://example.com/new-site/page', $unserialized['url']);
}

/**
* Test try_replace with serialized data containing the to_string already.
*
* Ensures the fix works: even when to_string is present, remaining
* from_string occurrences should still be replaced.
*/
public function test_try_replace_serialized_with_partial_replacement() {
$data = serialize([

Check failure on line 154 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Opening parenthesis of a multi-line function call must be the last content on the line
'upload_url' => 'https://example.com/new-site/wp-content/uploads/img.jpg',
'button_url' => 'https://example.com/old-site/contact',
'page_url' => 'https://example.com/old-site/about',
]);

Check failure on line 158 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Closing parenthesis of a multi-line function call must be on a line by itself
$row = ['meta_value' => $data];

$result = \MUCD_Data::try_replace($row, 'meta_value', 'example.com/old-site', 'example.com/new-site');
$unserialized = unserialize($result);

$this->assertEquals('https://example.com/new-site/wp-content/uploads/img.jpg', $unserialized['upload_url']);
$this->assertEquals('https://example.com/new-site/contact', $unserialized['button_url']);
$this->assertEquals('https://example.com/new-site/about', $unserialized['page_url']);
}

/**
* Test try_replace with Elementor-like JSON data stored in postmeta.
*
* JSON encodes forward slashes as \/, so the plain URL replacement won't
* match. The system handles this by running a separate pass with
* JSON-escaped search/replace strings at the DB level.
*/
public function test_try_replace_elementor_json_data() {
$elementor_data = json_encode([

Check failure on line 177 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Opening parenthesis of a multi-line function call must be the last content on the line
[
'elType' => 'section',
'settings' => [],
'elements' => [
[
'elType' => 'widget',
'settings' => [
'link' => [
'url' => 'https://example.com/old-site/services',
],
'button_link' => [
'url' => 'https://example.com/old-site/contact-us',
],
],
],
],
],
]);

Check failure on line 195 in tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Closing parenthesis of a multi-line function call must be on a line by itself

// Elementor stores _elementor_data as a plain JSON string (not serialized).
$row = ['meta_value' => $elementor_data];

// First pass: plain URL replacement (won't match JSON-escaped slashes).
$result = \MUCD_Data::try_replace($row, 'meta_value', 'example.com/old-site', 'example.com/new-site');

// Second pass: JSON-escaped URL replacement (matches \/ in JSON).
$row = ['meta_value' => $result];
$result = \MUCD_Data::try_replace(
$row,
'meta_value',
str_replace('/', '\\/', 'example.com/old-site'),
str_replace('/', '\\/', 'example.com/new-site')
);

$this->assertStringNotContainsString('old-site', $result);
$this->assertStringContainsString('new-site\\/services', $result);
$this->assertStringContainsString('new-site\\/contact-us', $result);
}

/**
* Test replace with JSON-escaped forward slashes.
*/
public function test_replace_json_escaped_slashes() {
// JSON-encoded URL: forward slashes become \/
$val = 'https:\\/\\/example.com\\/old-site\\/page';

$result = \MUCD_Data::replace(
$val,
str_replace('/', '\\/', 'example.com/old-site'),
str_replace('/', '\\/', 'example.com/new-site')
);

$this->assertEquals('https:\\/\\/example.com\\/new-site\\/page', $result);
}

/**
* Test replace_recursive with nested array.
*/
public function test_replace_recursive_nested_array() {
$data = [
'level1' => [
'level2' => [
'url' => 'https://example.com/old/deep-page',
],
],
'flat' => 'https://example.com/old/flat-page',
];

$result = \MUCD_Data::replace_recursive($data, 'example.com/old', 'example.com/new');

$this->assertEquals('https://example.com/new/deep-page', $result['level1']['level2']['url']);
$this->assertEquals('https://example.com/new/flat-page', $result['flat']);
}

/**
* Test replace_recursive with mixed types.
*/
public function test_replace_recursive_mixed_types() {
$data = [
'string' => 'https://example.com/old/page',
'int' => 42,
'bool' => true,
'null' => null,
'array' => ['https://example.com/old/nested'],
];

$result = \MUCD_Data::replace_recursive($data, 'example.com/old', 'example.com/new');

$this->assertEquals('https://example.com/new/page', $result['string']);
$this->assertEquals(42, $result['int']);
$this->assertTrue($result['bool']);
$this->assertNull($result['null']);
$this->assertEquals('https://example.com/new/nested', $result['array'][0]);
}

/**
* Test serialized data preserves string lengths correctly after replacement.
*/
public function test_try_replace_serialized_preserves_length() {
$data = serialize(['url' => 'https://example.com/short/page']);
$row = ['meta_value' => $data];

$result = \MUCD_Data::try_replace($row, 'meta_value', 'example.com/short', 'example.com/much-longer-path');

// Should be valid serialized data.
$unserialized = @unserialize($result);
$this->assertIsArray($unserialized);
$this->assertEquals('https://example.com/much-longer-path/page', $unserialized['url']);
}
}
Loading