diff --git a/CRM/Mosaico/BAO/MosaicoTemplate.php b/CRM/Mosaico/BAO/MosaicoTemplate.php index 9881387af..77f407185 100644 --- a/CRM/Mosaico/BAO/MosaicoTemplate.php +++ b/CRM/Mosaico/BAO/MosaicoTemplate.php @@ -26,24 +26,70 @@ public static function create($params) { * Helps updating the URLs in templates so they can be reused * after restoring a dump database in a new server. * + * URL's can be include either scheme/host or path or both + * * @param string $fromUrl URL of the server where the * templates were created * @param string $toUrl URL of the current server */ public static function replaceUrls($fromUrl, $toUrl) { - $replaceQuery = "UPDATE civicrm_mosaico_template - SET metadata = json_replace(metadata, '$.template', - replace( - json_unquote( - json_extract(metadata, '$.template') - ), + $from = parse_url($fromUrl); + $to = parse_url($toUrl); + + // Update 'template': only uses path without leading slash + if ($from['path']) { + $replaceQuery = "UPDATE civicrm_mosaico_template + SET metadata = JSON_REPLACE(metadata, '$.template', + REPLACE( + JSON_UNQUOTE( + JSON_EXTRACT(metadata, '$.template') + ), %1, %2) - );"; + );"; + + CRM_Core_DAO::executeQuery($replaceQuery, [ + 1 => [ltrim($from['path'], '/'), 'String'], + 2 => [ltrim($to['path'] ?? '', '/'), 'String'], + ]); + } + // Update 'html' and 'content' + $replaceQuery = "UPDATE civicrm_mosaico_template + SET html = REPLACE(html, %1, %2), + content = REPLACE(content, %1, %2) + ;"; + + // ... with unencoded strings CRM_Core_DAO::executeQuery($replaceQuery, [ 1 => [$fromUrl, 'String'], 2 => [$toUrl, 'String'], ]); + + // ... with encoded strings + CRM_Core_DAO::executeQuery($replaceQuery, [ + 1 => [urlencode($fromUrl), 'String'], + 2 => [urlencode($toUrl), 'String'], + ]); + + // Images load from https://example.com/civicrm/mosaico/img, so update the host part as well + $hostFrom = str_replace($from['path'], '', $fromUrl); + $hostTo = str_replace($to['path'], '', $toUrl); + if ($hostFrom && $hostTo) { + CRM_Core_DAO::executeQuery($replaceQuery, [ + 1 => [$hostFrom . "/civicrm/mosaico", 'String'], + 2 => [$hostTo . "/civicrm/mosaico", 'String'], + ]); + } + + // However, 'content' is a json string, and the encoded representation depends on the json encoding options + // The above works where the JSON_UNESCAPED_SLASHES flag has been used so that the encoded representation is like '/my/path' + // But without that option, the encoded representation is '\/my\/path' + // Comparing the decoded values would be better, but this should work for our purposes... + // executeQuery doesn't like backslahes in 'String', so do this directly. + + $slashedFrom = str_replace('/', '\\\/', $fromUrl); + $slashedTo = str_replace('/', '\\\/', $toUrl); + CRM_Core_DAO::executeQuery("UPDATE civicrm_mosaico_template SET content = REPLACE(content, '$slashedFrom', '$slashedTo');"); } /** @@ -65,7 +111,7 @@ public static function findBaseTemplates($ignoreCache = FALSE, $dispatchHooks = $templatesLocation[] = ['dir' => $templatesDir, 'url' => $templatesUrl]; $customTemplatesDir = \Civi::paths()->getPath(\Civi::settings()->get('mosaico_custom_templates_dir')); - $customTemplatesUrl = \Civi::paths()->getUrl(\Civi::settings()->get('mosaico_custom_templates_url'),'absolute'); + $customTemplatesUrl = \Civi::paths()->getUrl(\Civi::settings()->get('mosaico_custom_templates_url'), 'absolute'); if (!is_null($customTemplatesDir) && !is_null($customTemplatesUrl)) { if (is_dir($customTemplatesDir)) { $templatesLocation[] = ['dir' => $customTemplatesDir, 'url' => $customTemplatesUrl]; diff --git a/docs/api.md b/docs/api.md index 7fc395e9d..26e16568b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -11,10 +11,14 @@ This extension defines a few new APIs: old v1.x templates. * `MosaicoTemplate.*`: This API provides access to the user-configurable templates. It supports all standard CRUD actions (`get`, `create`, `delete`etc). Its data-structure closely adheres to Mosaico's canonical storage format. -* `MosaicoTemplate.replaceurls`: When a database is restored in a server with a different URL, templates will need to be updated. The `replaceurls` method facilitates that migration task: +* `MosaicoTemplate.replaceurls`: When a database is restored in a server with a different URL or if paths change then templates will need to be updated. The `replaceurls` method facilitates that migration task: ``` cv api MosaicoTemplate.replaceurls from_url="http://old.server.org" to_url="https://new.server.org" +``` + Migrating from Drupal 7 to WordPress: +``` +cv api MosaicoTemplate.replaceurls from_url="/sites/default/files/civicrm/" to_url="/wp-content/uploads/civicrm/" ``` * `MosaicoBaseTemplate.get`: This API provides access to the *base templates*. A base template (such as `versafix-1`) @@ -116,18 +120,18 @@ Example usage: function example_civicrm_mosaicoPlugin(&$plugins) { $plugins[] = _example_alert_plugin(); } - + /** * Example plugin to display alert on init and dispose. - */ + */ function _example_alert_plugin(){ $plugin = <<< JS function(vm) { alert("test-plugin"); return { - init: function(vm){alert("init");}, + init: function(vm){alert("init");}, dispose: function(vm){alert("dispose");} - } + } } JS; diff --git a/tests/phpunit/CRM/Mosaico/MosaicoTemplateTest.php b/tests/phpunit/CRM/Mosaico/MosaicoTemplateTest.php index f8d07089c..1d57eca1f 100644 --- a/tests/phpunit/CRM/Mosaico/MosaicoTemplateTest.php +++ b/tests/phpunit/CRM/Mosaico/MosaicoTemplateTest.php @@ -85,4 +85,92 @@ public function testClone(): void { $this->assertEquals(json_encode(['abc' => 'def']), $clone['content']); } + /** + * Test replaceUrls with full URL + */ + public function testReplaceUrlsFull(): void { + $createResult = $this->callAPISuccess('MosaicoTemplate', 'create', [ + 'title' => 'MosaicoTemplateTest baz', + 'base' => 'versafix-1', + // As extracted from a real template + 'html' => '', + 'metadata' => json_encode(['template' => '/sites/default/files/civicrm/mosaico_tpl/CiviVersafix/template-CiviVersafix.html', 'templateversion' => '1.0.5', 'editorversion' => '0.15.0'], JSON_UNESCAPED_SLASHES), + 'content' => json_encode(['src' => 'https://old-site.org/sites/default/files/civicrm/persist/contribute/images/uploads/logo_bced05c6746c32f9adb3fea8a7060dac.png']), + ]); + + $this->callAPISuccess('MosaicoTemplate', 'replaceurls', [ + 'from_url' => 'https://old-site.org/sites/default/files/civicrm/', + 'to_url' => 'https://new-site.org/wp-content/uploads/civicrm/', + ]); + + $getResult = $this->callAPISuccess('MosaicoTemplate', 'getsingle', ['id' => $createResult['id']]); + foreach (['html', 'content', 'metadata'] as $element) { + $this->assertStringNotContainsString('old-site.org', $getResult[$element]); + $this->assertStringNotContainsString('sites', $getResult[$element]); + $this->assertStringContainsString('wp-content', $getResult[$element]); + if ($element != 'metadata') { + $this->assertStringContainsString('new-site.org', $getResult[$element]); + $this->assertStringContainsString('logo_bced', $getResult[$element]); + } + } + } + + /** + * Test replaceUrls with host only + */ + public function testReplaceUrlsHost(): void { + $createResult = $this->callAPISuccess('MosaicoTemplate', 'create', [ + 'title' => 'MosaicoTemplateTest baz', + 'base' => 'versafix-1', + // As extracted from a real template + 'html' => '', + 'metadata' => json_encode(['template' => '/sites/default/files/civicrm/mosaico_tpl/CiviVersafix/template-CiviVersafix.html', 'templateversion' => '1.0.5', 'editorversion' => '0.15.0'], JSON_UNESCAPED_SLASHES), + 'content' => json_encode(['src' => 'https://old-site.org/sites/default/files/civicrm/persist/contribute/images/uploads/logo_bced05c6746c32f9adb3fea8a7060dac.png']), + ]); + + $this->callAPISuccess('MosaicoTemplate', 'replaceurls', [ + 'from_url' => 'https://old-site.org/', + 'to_url' => 'https://new-site.org/', + ]); + + $getResult = $this->callAPISuccess('MosaicoTemplate', 'getsingle', ['id' => $createResult['id']]); + foreach (['html', 'content', 'metadata'] as $element) { + $this->assertStringNotContainsString('old-site.org', $getResult[$element]); + $this->assertStringContainsString('sites', $getResult[$element]); + if ($element != 'metadata') { + $this->assertStringContainsString('new-site.org', $getResult[$element]); + $this->assertStringContainsString('logo_bced', $getResult[$element]); + } + } + } + + /** + * Test replaceUrls with path only + */ + public function testReplaceUrlsPath(): void { + $createResult = $this->callAPISuccess('MosaicoTemplate', 'create', [ + 'title' => 'MosaicoTemplateTest baz', + 'base' => 'versafix-1', + // As extracted from a real template + 'html' => '', + 'metadata' => json_encode(['template' => '/sites/default/files/civicrm/mosaico_tpl/CiviVersafix/template-CiviVersafix.html', 'templateversion' => '1.0.5', 'editorversion' => '0.15.0'], JSON_UNESCAPED_SLASHES), + 'content' => json_encode(['src' => 'https://old-site.org/sites/default/files/civicrm/persist/contribute/images/uploads/logo_bced05c6746c32f9adb3fea8a7060dac.png']), + ]); + + $this->callAPISuccess('MosaicoTemplate', 'replaceurls', [ + 'from_url' => '/sites/default/files/civicrm/', + 'to_url' => '/wp-content/uploads/civicrm/', + ]); + + $getResult = $this->callAPISuccess('MosaicoTemplate', 'getsingle', ['id' => $createResult['id']]); + foreach (['html', 'content', 'metadata'] as $element) { + $this->assertStringNotContainsString('sites', $getResult[$element]); + $this->assertStringContainsString('wp-content', $getResult[$element]); + if ($element != 'metadata') { + $this->assertStringContainsString('old-site.org', $getResult[$element]); + $this->assertStringContainsString('logo_bced', $getResult[$element]); + } + } + } + }