diff --git a/composer.json b/composer.json index f71d6cbc..87541bf7 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "smartling/wordpress-connector", "license": "GPL-2.0-or-later", - "version": "5.3.6", + "version": "5.4.0", "description": "", "type": "wordpress-plugin", "repositories": [ diff --git a/composer.lock b/composer.lock index d536c5fa..6543864c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f4704f444748fa44662ad7ed202c040b", + "content-hash": "ef6d98f04379baf269d96e2d0cb5ae49", "packages": [ { "name": "composer/semver", @@ -2188,6 +2188,132 @@ } ], "packages-dev": [ + { + "name": "clue/graph", + "version": "v0.9.3", + "source": { + "type": "git", + "url": "https://github.com/graphp/graph.git", + "reference": "d1661c0a0e011a8550fa60ae5354f230d1555909" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/graphp/graph/zipball/d1661c0a0e011a8550fa60ae5354f230d1555909", + "reference": "d1661c0a0e011a8550fa60ae5354f230d1555909", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + }, + "suggest": { + "graphp/algorithms": "Common graph algorithms, such as Dijkstra and Moore-Bellman-Ford (shortest path), minimum spanning tree (MST), Kruskal, Prim and many more..", + "graphp/graphviz": "GraphViz graph drawing / DOT output" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fhaculty\\Graph\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "GraPHP is the mathematical graph/network library written in PHP.", + "homepage": "https://github.com/graphp/graph", + "keywords": [ + "edge", + "graph", + "mathematical", + "network", + "vertex" + ], + "support": { + "issues": "https://github.com/graphp/graph/issues", + "source": "https://github.com/graphp/graph/tree/v0.9.3" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-12-30T09:22:01+00:00" + }, + { + "name": "clue/graph-composer", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/clue/graph-composer.git", + "reference": "eff70fe2af7704b15cf675fcad663abe42034153" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/graph-composer/zipball/eff70fe2af7704b15cf675fcad663abe42034153", + "reference": "eff70fe2af7704b15cf675fcad663abe42034153", + "shasum": "" + }, + "require": { + "clue/graph": "^0.9.1", + "graphp/graphviz": "^0.2.2", + "jms/composer-deps-analyzer": "^1.0.1", + "php": ">=5.3.6", + "symfony/console": "^5.0 || ^4.0 || ^3.0 || ^2.1" + }, + "require-dev": { + "clue/phar-composer": "^1.1", + "phpunit/phpunit": "^4.8.36" + }, + "bin": [ + "bin/graph-composer" + ], + "type": "library", + "autoload": { + "psr-0": { + "Clue\\GraphComposer": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Dependency graph visualization for composer.json", + "homepage": "https://github.com/clue/graph-composer", + "keywords": [ + "dependency graph", + "visualize composer", + "visualize dependencies" + ], + "support": { + "issues": "https://github.com/clue/graph-composer/issues", + "source": "https://github.com/clue/graph-composer/tree/v1.1.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2020-03-26T12:02:56+00:00" + }, { "name": "doctrine/instantiator", "version": "2.1.0", @@ -2257,6 +2383,91 @@ ], "time": "2026-01-05T06:47:08+00:00" }, + { + "name": "graphp/graphviz", + "version": "v0.2.2", + "source": { + "type": "git", + "url": "https://github.com/graphp/graphviz.git", + "reference": "5cc4466223ca46fffa196d1e762fae164319c229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/graphp/graphviz/zipball/5cc4466223ca46fffa196d1e762fae164319c229", + "reference": "5cc4466223ca46fffa196d1e762fae164319c229", + "shasum": "" + }, + "require": { + "clue/graph": "~0.9.0|~0.8.0", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "Graphp\\GraphViz\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "GraphViz graph drawing for the mathematical graph/network library GraPHP.", + "homepage": "https://github.com/graphp/graphviz", + "keywords": [ + "dot output", + "graph drawing", + "graph image", + "graphp", + "graphviz" + ], + "support": { + "issues": "https://github.com/graphp/graphviz/issues", + "source": "https://github.com/graphp/graphviz/tree/v0.2.2" + }, + "time": "2019-10-04T13:30:55+00:00" + }, + { + "name": "jms/composer-deps-analyzer", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/composer-deps-analyzer.git", + "reference": "6e72a866c40a98e63efb6bb059a2bbaadcb8aa15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/composer-deps-analyzer/zipball/6e72a866c40a98e63efb6bb059a2bbaadcb8aa15", + "reference": "6e72a866c40a98e63efb6bb059a2bbaadcb8aa15", + "shasum": "" + }, + "require": { + "php": ">= 5.3.2" + }, + "require-dev": { + "composer/composer": "dev-master", + "symfony/filesystem": "2.1.*", + "symfony/process": "2.1.*" + }, + "type": "library", + "autoload": { + "psr-0": { + "JMS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "description": "Builds a Dependency Graph from a composer.json file", + "support": { + "issues": "https://github.com/schmittjoh/composer-deps-analyzer/issues", + "source": "https://github.com/schmittjoh/composer-deps-analyzer/tree/master" + }, + "time": "2016-11-09T16:59:19+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.13.4", diff --git a/docs/ELEMENTOR_DEVELOPMENT.md b/docs/ELEMENTOR_DEVELOPMENT.md index fb416e4c..f0670698 100644 --- a/docs/ELEMENTOR_DEVELOPMENT.md +++ b/docs/ELEMENTOR_DEVELOPMENT.md @@ -4,10 +4,41 @@ This document captures key patterns and information for developing Elementor wid ## Architecture Overview +### Version Support + +The plugin supports two major Elementor format versions through separate but related handlers: + +| Class | Factory | Version | Format | +|-------|---------|---------|--------| +| `ExternalContentElementor3` | `ElementFactory3` | 3.x | Flat string settings | +| `ExternalContentElementor4` | `ElementFactory4` | 4.x | `$$type`-annotated settings | + +Both extend `ExternalContentElementorAbstract`, which contains all shared logic (upload/download pipeline, meta field management, conditions field handling). Version detection uses `getMinVersion()`/`getMaxVersion()` from the parent `ExternalContentAbstract`. +Changes to Elementor 3 related files are discouraged, the support should continue for the latest version. + ### Element Class Hierarchy -- **ElementAbstract** - Base class for all Elementor elements -- **Unknown** - Default handler for unrecognized widgets, provides common functionality -- **Specific Elements** (e.g., LoopCarousel, ImageGallery, Template) - Extend Unknown to add widget-specific behavior + +**Elementor 3 (flat settings)** +- `ElementAbstract` — Base class for all elements +- `Unknown` — Default handler; passes child elements through recursively +- `Elements/` — Specific widget handlers (Gallery, Tabs, LoopCarousel, etc.) + +**Elementor 4 (`$$type` settings)** +- `ElementAbstract4` — Extends `ElementAbstract`; adds `$$type`-aware extraction and write-back +- `Elements4/` — Specific widget handlers for `e-*` widget types (EHeading, EParagraph, EButton, EImage, EFormLabel, EFormInput, EFormTextarea) + +`ElementFactory4` loads both `Elements/` and `Elements4/` so that an Elementor 4 page containing old-format widgets (e.g., `container`, `gallery`, `blockquote`) falls through to the existing v3 handlers automatically. + +### Shared Interface + +`ExternalContentElementorInterface` is implemented by both `ExternalContentElementor3` and `ExternalContentElementor4`. Element handlers receive this interface in `setRelations()` and `setTargetContent()`, decoupling them from a specific handler version. + +```php +interface ExternalContentElementorInterface { + public function getWpProxy(): WordpressFunctionProxyHelper; + public function getTargetId(int $sourceBlogId, int $sourceId, int $targetBlogId, string $contentType = ...): ?int; +} +``` ### Key Methods @@ -20,8 +51,57 @@ Returns translatable text strings from the widget settings. #### `setRelations()` Replaces source content IDs with target (translated) content IDs during download. This method automatically handles content type detection: - For `CONTENT_TYPE_POST`: Calls `get_post_type()` to determine the actual post type -- For `CONTENT_TYPE_TAXONOMY`: Calls `get_term()` to determine the actual taxonomy name (e.g., 'product_tag', 'category') -- For specific types (e.g., 'attachment', 'product_tag'): Uses the type as-is +- For `CONTENT_TYPE_TAXONOMY`: Calls `get_term()` to determine the actual taxonomy name (e.g., `product_tag`, `category`) +- For specific types (e.g., `attachment`, `product_tag`): Uses the type as-is + +## Elementor 4 `$$type` Format + +Elementor 4 stores settings as typed objects rather than flat values: + +```json +{ + "title": { + "$$type": "html-v3", + "value": { + "content": { "$$type": "string", "value": "Hello World" }, + "children": [] + } + }, + "placeholder": { "$$type": "string", "value": "your@email.com" }, + "image": { + "$$type": "image", + "value": { + "src": { + "$$type": "image-src", + "value": { + "id": { "$$type": "image-attachment-id", "value": 23 }, + "url": null + } + } + } + } +} +``` + +### Extraction Paths + +| `$$type` | Translatable value path | Notes | +|----------|------------------------|-------| +| `string` | `value` | Plain string | +| `html-v3` | `value.content.value` | Rich text; only the leaf string is extracted | +| `image` | `value.src.value.id.value` | Integer attachment ID (related content, not string) | + +`ElementAbstract4::extractTypedValue()` handles `string` and `html-v3` cases. `ElementAbstract4::setTypedSettingValue()` writes translations back to the correct path while preserving all sibling `$$type` keys. + +### Flattened Key Format + +After `getContentFields()` flattens the extracted strings, keys follow the pattern: + +``` +{containerId}/{widgetId}/{settingKey} +``` + +Example: `container1/heading1/title` → `"Hello World"` ## Common Patterns @@ -73,28 +153,60 @@ public function getRelated(): RelatedContentInfo **Example:** `Elements/ImageGallery.php` (wp_gallery), `Elements/LoopCarousel.php` (post_query_include_term_ids) -### Pattern 3: Processing Nested Arrays +### Pattern 3: Elementor 4 — Typed Translatable Strings -When content is nested deeper in the settings structure: +Extend `ElementAbstract4`. Override `getTranslatableStrings()` using `extractTypedValue()`: ```php -foreach ($this->settings['items'] ?? [] as $index => $item) { - $key = "items/$index/image/id"; - $id = $this->getIntSettingByKey($key, $this->settings); - // ... process +class EHeading extends ElementAbstract4 +{ + public function getType(): string { return 'e-heading'; } + + public function getTranslatableStrings(): array + { + $strings = parent::getTranslatableStrings(); + $value = $this->extractTypedValue($this->settings['title'] ?? null); + if ($value !== null) { + $strings[$this->id]['title'] = $value; + } + return $strings; + } } ``` -**Example:** `Elements/IconList.php` +`setTargetContent()` is handled by `ElementAbstract4` — it calls `setTypedSettingValue()` for each key returned by `getTranslatableStrings()`, so subclasses only need to implement extraction. + +### Pattern 4: Elementor 4 — Related Content (`$$type: image`) + +```php +class EImage extends ElementAbstract4 +{ + public function getType(): string { return 'e-image'; } + + public function getRelated(): RelatedContentInfo + { + $return = parent::getRelated(); + $id = $this->settings['image']['value']['src']['value']['id']['value'] ?? null; + if (is_int($id) && $id > 0) { + $return->addContent( + new Content($id, ContentTypeHelper::POST_TYPE_ATTACHMENT), + $this->id, + 'settings/image/value/src/value/id/value' + ); + } + return $return; + } +} +``` ## Content Types ### Available Content Type Constants From `ContentTypeHelper`: -- `ContentTypeHelper::CONTENT_TYPE_POST` - For posts, pages, custom post types -- `ContentTypeHelper::CONTENT_TYPE_TAXONOMY` - For taxonomy terms (generic) -- `ContentTypeHelper::POST_TYPE_ATTACHMENT` - For media attachments +- `ContentTypeHelper::CONTENT_TYPE_POST` — For posts, pages, custom post types +- `ContentTypeHelper::CONTENT_TYPE_TAXONOMY` — For taxonomy terms (generic) +- `ContentTypeHelper::POST_TYPE_ATTACHMENT` — For media attachments ### Taxonomy Terms @@ -105,20 +217,11 @@ When processing taxonomy terms (categories, tags, custom taxonomies): new Content($termId, ContentTypeHelper::CONTENT_TYPE_TAXONOMY) ``` -The `ElementAbstract::setRelations()` method automatically detects the actual taxonomy name (e.g., 'product_tag', 'category') by calling `get_term()` on the term ID. This is the preferred approach as it's more flexible and doesn't require knowing the taxonomy type in advance. - -**Alternative: Use specific taxonomy name (if known)** -```php -new Content($termId, 'product_tag') // When taxonomy type is known -``` - -This approach can be used when you know the exact taxonomy upfront, but the generic `CONTENT_TYPE_TAXONOMY` is generally preferred. +The `ElementAbstract::setRelations()` method automatically detects the actual taxonomy name by calling `get_term()`. This is preferred as it works for all taxonomy types without knowing them in advance. ## Widget Settings Structure -### LoopCarousel Widget Example - -The LoopCarousel widget filters content using these key settings: +### Elementor 3 — LoopCarousel Widget Example ```json { @@ -129,28 +232,43 @@ The LoopCarousel widget filters content using these key settings: } ``` -- `template_id` - The Elementor template used for each item (post reference) -- `post_query_include_term_ids` - Array of term IDs used to filter displayed posts (taxonomy references) +### Elementor 4 — Form Widget Example + +```json +{ + "id": "form1", + "elType": "e-form", + "elements": [ + { + "id": "input1", + "elType": "widget", + "widgetType": "e-form-input", + "settings": { + "placeholder": { "$$type": "string", "value": "First name" }, + "type": { "$$type": "string", "value": "text" }, + "_cssid": { "$$type": "string", "value": "e-form-first-name" } + } + } + ] +} +``` ### Common Settings Patterns -- IDs are often stored as strings even when numeric: `"14"` not `14` -- Arrays use numeric indices: `post_query_include_term_ids/0`, `post_query_include_term_ids/1` +- Elementor 3: IDs are often stored as strings even when numeric — `"14"` not `14` +- Elementor 4: Every setting is wrapped in `{ "$$type": "...", "value": ... }` +- Arrays use numeric indices: `post_query_include_term_ids/0` - Nested paths use forward slashes: `settings/wp_gallery/1/id` ## Testing Patterns ### Test 1: Verify Related Content Discovery -Tests that related content is properly identified: - ```php public function testRelatedContent(): void { $relatedList = (new MyWidget([ - 'settings' => [ - 'some_ids' => ['14', '15', '16'] - ] + 'settings' => ['some_ids' => ['14', '15', '16']] ]))->getRelated()->getRelatedContentList(); $this->assertArrayHasKey('expected_type', $relatedList); @@ -160,7 +278,7 @@ public function testRelatedContent(): void ### Test 2: Verify Content Translation -Tests that IDs are replaced with translated equivalents: +Use `ExternalContentElementorInterface` (not `ExternalContentElementor3` or `ExternalContentElementor4`): ```php public function testTranslation(): void @@ -168,7 +286,7 @@ public function testTranslation(): void $sourceId = 14; $targetId = 28; - $externalContentElementor = $this->createMock(ExternalContentElementor::class); + $externalContentElementor = $this->createMock(ExternalContentElementorInterface::class); $externalContentElementor->method('getTargetId') ->with(0, $sourceId, 0, 'content_type')->willReturn($targetId); @@ -185,29 +303,72 @@ public function testTranslation(): void } ``` +### Test 3: Verify Elementor 4 String Extraction + +```php +public function testExtractsHeadingTitle(): void +{ + $data = json_encode([[ + 'id' => 'container1', 'elType' => 'e-flexbox', 'settings' => [], 'elements' => [[ + 'id' => 'heading1', 'elType' => 'widget', 'widgetType' => 'e-heading', + 'settings' => [ + 'title' => ['$$type' => 'html-v3', 'value' => [ + 'content' => ['$$type' => 'string', 'value' => 'Hello World'], + 'children' => [], + ]], + ], + 'elements' => [], 'styles' => [], 'interactions' => [], 'editor_settings' => [], 'version' => '0.0', + ]], + 'isInner' => false, 'styles' => [], 'interactions' => [], 'editor_settings' => [], 'version' => '0.0', + ]]); + + $fields = $handler->getContentFields($submission, false); + $this->assertEquals('Hello World', $fields['container1/heading1/title']); +} +``` + ## Development Workflow -### Adding Support for a New Widget +### Adding Support for a New Elementor 3 Widget 1. **Create Element Class** in `inc/Smartling/ContentTypes/Elementor/Elements/` - Extend `Unknown` - - Override `getType()` to return widget type string - - Override `getRelated()` if widget has related content + - Override `getType()` to return the widget type string + - Override `getRelated()` if the widget has related content 2. **Identify Widget Structure** - - Export Elementor page JSON to examine widget settings + - Export Elementor page JSON and examine the widget's `settings` object - Look for ID fields: `*_id`, `*_ids`, nested arrays - - Determine content types (posts, attachments, terms) 3. **Implement `getRelated()`** - Call `parent::getRelated()` first - Loop through settings to find content references - - Use `addContent()` with proper content type and path + - Use `addContent()` with the correct content type and path 4. **Create Tests** in `tests/Smartling/ContentTypes/Elementor/` - - Test related content discovery - - Test ID translation - - Include edge cases (empty arrays, missing settings) + +### Adding Support for a New Elementor 4 Widget + +1. **Create Element Class** in `inc/Smartling/ContentTypes/Elementor/Elements4/` + - Extend `ElementAbstract4` + - Override `getType()` to return the `e-*` widget type string + +2. **For translatable text fields** — override `getTranslatableStrings()`: + - Call `parent::getTranslatableStrings()` first + - Use `extractTypedValue($this->settings['fieldKey'] ?? null)` for each field + - Add to `$strings[$this->id]['fieldKey']` if non-null + - `setTargetContent()` is inherited and handles write-back automatically + +3. **For related content (attachment IDs)** — override `getRelated()`: + - Call `parent::getRelated()` first + - Navigate the `$$type` wrapper path to reach the integer ID + - Use `addContent()` with `POST_TYPE_ATTACHMENT` and the full dotted path + +4. **Non-translatable settings** — prefix the key with `_` or skip it; `EFormInput` excludes `type` and `_cssid` by only extracting `placeholder` + +5. **Container types** (`e-flexbox`, `e-form`, `e-form-success-message`, etc.) have no own translatable settings and are handled automatically by the `Unknown` fallback, which recursively processes child elements. No handler needed. + +6. **The factory picks up the new handler automatically** — `ElementFactory4` uses `DirectoryIterator` to load all `.php` files from `Elements4/`. No registration step required. ## Real-World Examples @@ -234,49 +395,50 @@ foreach ($this->settings['post_query_include_term_ids'] ?? [] as $index => $term - Use array iteration with index to build proper paths - Generic `CONTENT_TYPE_TAXONOMY` works for all taxonomy types -### Case Study: ImageGallery +### Case Study: Elementor 4 Support (WP-1000) -**Problem:** Image galleries contain multiple attachments that need translation. +**Problem:** Elementor 4 changed `_elementor_data` to wrap all settings in `$$type` objects. The existing Elementor 3 handler couldn't extract strings or write translations back. -**Implementation:** -```php -foreach ($this->settings['wp_gallery'] ?? [] as $index => $listItem) { - $key = "wp_gallery/$index/id"; - $id = $this->getIntSettingByKey($key, $this->settings); - if ($id !== null) { - $return->addContent( - new Content($id, ContentTypeHelper::POST_TYPE_ATTACHMENT), - $this->id, - "settings/$key" - ); - } -} -``` +**Solution:** +- `ExternalContentElementorAbstract` extracted as shared base for both v3 and v4 handlers +- `ElementFactory4` loads `Elements/` (v3 handlers) + `Elements4/` (v4-specific handlers), so mixed-format pages work correctly +- `ElementAbstract4` adds `extractTypedValue()` and `setTypedSettingValue()` for `$$type`-aware processing +- Seven `Elements4/` handlers cover the `e-*` widget types present in Elementor 4 pages ## Tips and Best Practices 1. **Always use null-coalescing operator** (`??`) when accessing settings arrays -2. **Build proper paths** - They must match the exact structure in settings JSON -3. **Use `getIntSettingByKey()`** - Handles nested path resolution and type casting -4. **Call parent methods** - Don't skip `parent::getRelated()` unless you have a specific reason -5. **Check for numeric before casting** - Settings often store numbers as strings -6. **Write tests** - Test both discovery and translation of related content -7. **Use constants** - Prefer `ContentTypeHelper::CONTENT_TYPE_*` over magic strings -8. **Document widget structure** - Add comments showing the expected settings structure +2. **Build proper paths** — They must match the exact structure in settings JSON +3. **Use `getIntSettingByKey()`** — Handles nested path resolution and type casting (v3) +4. **Call parent methods** — Don't skip `parent::getRelated()` or `parent::getTranslatableStrings()` +5. **Check for numeric before casting** — Elementor 3 settings often store numbers as strings +6. **Write tests** — Test both discovery and translation of related content +7. **Use constants** — Prefer `ContentTypeHelper::CONTENT_TYPE_*` over magic strings +8. **Mock the interface, not the class** — Use `createMock(ExternalContentElementorInterface::class)` in tests ## Debugging Tips ### Common Issues -- **Path mismatch** - Ensure path in `addContent()` matches exact JSON structure -- **Type mismatch** - Check if IDs are stored as strings vs integers -- **Missing null checks** - Always use `??` for optional settings -- **Wrong content type** - Verify you're using the correct type constant +- **Path mismatch** — Ensure path in `addContent()` matches exact JSON structure +- **Type mismatch** — Check if IDs are stored as strings vs integers (v3) or integers vs `$$type` wrappers (v4) +- **Missing null checks** — Always use `??` for optional settings +- **Wrong content type** — Verify you're using the correct type constant +- **v4 write-back broken** — Check `setTypedSettingValue()` handles the correct `$$type` variant for the field ## Related Files -- `inc/Smartling/ContentTypes/Elementor/ElementAbstract.php` - Base element class -- `inc/Smartling/ContentTypes/Elementor/Elements/Unknown.php` - Default handler -- `inc/Smartling/ContentTypes/ExternalContentElementor.php` - Main Elementor integration -- `inc/Smartling/Models/RelatedContentInfo.php` - Related content container -- `inc/Smartling/Models/Content.php` - Content reference model -- `tests/Smartling/ContentTypes/Elementor/` - Test examples +- `inc/Smartling/ContentTypes/Elementor/ExternalContentElementorInterface.php` — Shared interface for v3/v4 handlers +- `inc/Smartling/ContentTypes/ExternalContentElementorAbstract.php` — Shared handler logic (upload/download pipeline) +- `inc/Smartling/ContentTypes/ExternalContentElementor3.php` — Elementor 3.x handler +- `inc/Smartling/ContentTypes/ExternalContentElementor4.php` — Elementor 4.x handler +- `inc/Smartling/ContentTypes/Elementor/ElementAbstract.php` — Base element class (v3) +- `inc/Smartling/ContentTypes/Elementor/ElementAbstract4.php` — Base element class (v4, `$$type`-aware) +- `inc/Smartling/ContentTypes/Elementor/ElementFactory3.php` — Factory that loads `Elements/` +- `inc/Smartling/ContentTypes/Elementor/ElementFactory4.php` — Factory that loads `Elements/` + `Elements4/` +- `inc/Smartling/ContentTypes/Elementor/Elements/Unknown.php` — Default handler (recursive child processing) +- `inc/Smartling/ContentTypes/Elementor/Elements/` — Elementor 3 widget handlers +- `inc/Smartling/ContentTypes/Elementor/Elements4/` — Elementor 4 widget handlers +- `inc/Smartling/Models/RelatedContentInfo.php` — Related content container +- `inc/Smartling/Models/Content.php` — Content reference model +- `tests/Smartling/ContentTypes/Elementor/` — Test examples +- `tests/Smartling/ContentTypes/ExternalContentElementor4Test.php` — Elementor 4 handler tests diff --git a/inc/Smartling/ContentTypes/Elementor/Element.php b/inc/Smartling/ContentTypes/Elementor/Element.php index b9c8e00b..63195911 100644 --- a/inc/Smartling/ContentTypes/Elementor/Element.php +++ b/inc/Smartling/ContentTypes/Elementor/Element.php @@ -2,7 +2,6 @@ namespace Smartling\ContentTypes\Elementor; -use Smartling\ContentTypes\ExternalContentElementor; use Smartling\Models\Content; use Smartling\Models\RelatedContentInfo; use Smartling\Submissions\SubmissionEntity; @@ -15,12 +14,12 @@ public function getTranslatableStrings(): array; public function getType(): string; public function setRelations( Content $content, - ExternalContentElementor $externalContentElementor, + ExternalContentElementorInterface $externalContentElementor, string $path, SubmissionEntity $submission, ): self; public function setTargetContent( - ExternalContentElementor $externalContentElementor, + ExternalContentElementorInterface $externalContentElementor, RelatedContentInfo $info, array $strings, SubmissionEntity $submission, diff --git a/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php b/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php index ca3d9e9c..c2dd8084 100644 --- a/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php +++ b/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php @@ -4,7 +4,6 @@ use Elementor\Core\DynamicTags\Manager; use Smartling\ContentTypes\ContentTypeHelper; -use Smartling\ContentTypes\ExternalContentElementor; use Smartling\Helpers\ArrayHelper; use Smartling\Helpers\LoggerSafeTrait; use Smartling\Models\Content; @@ -32,7 +31,7 @@ public function __construct(array $array = []) $this->id = $array['id'] ?? ''; $this->raw = $array; $this->settings = $array['settings'] ?? []; - $this->type = $array['elType'] ?? ElementFactory::UNKNOWN_ELEMENT; + $this->type = $array['elType'] ?? ElementFactory3::UNKNOWN_ELEMENT; } public function fromArray(array $array): static @@ -125,7 +124,7 @@ protected function getTranslatableStringsByKeys(array $keys, Element $element = public function setRelations( Content $content, - ExternalContentElementor $externalContentElementor, + ExternalContentElementorInterface $externalContentElementor, string $path, SubmissionEntity $submission, ): static { @@ -142,7 +141,7 @@ public function setRelations( $contentType = $content->getType(); } - if ($contentType === false) { + if (!is_string($contentType)) { $this->getLogger()->debug("Unable to get content type for contentId={$content->getId()}, proceeding with unknown type"); $contentType = ContentTypeHelper::CONTENT_TYPE_UNKNOWN; } @@ -169,7 +168,7 @@ public function setRelations( } public function setTargetContent( - ExternalContentElementor $externalContentElementor, + ExternalContentElementorInterface $externalContentElementor, RelatedContentInfo $info, array $strings, SubmissionEntity $submission, diff --git a/inc/Smartling/ContentTypes/Elementor/ElementAbstract4.php b/inc/Smartling/ContentTypes/Elementor/ElementAbstract4.php new file mode 100644 index 00000000..94465a54 --- /dev/null +++ b/inc/Smartling/ContentTypes/Elementor/ElementAbstract4.php @@ -0,0 +1,72 @@ + isset($value['value']) && is_string($value['value']) ? $value['value'] : null, + 'html-v3' => isset($value['value']['content']['value']) && is_string($value['value']['content']['value']) + ? $value['value']['content']['value'] + : null, + default => null, + }; + } + + public function setTargetContent( + ExternalContentElementorInterface $externalContentElementor, + RelatedContentInfo $info, + array $strings, + SubmissionEntity $submission, + ): static { + foreach ($this->elements as $key => $element) { + if ($element instanceof Element) { + $this->elements[$key] = $element->setTargetContent( + $externalContentElementor, + new RelatedContentInfo($info->getInfo()[$this->id] ?? []), + $strings[$this->id] ?? $strings[$element->id] ?? [], + $submission, + ); + } + } + + foreach ($strings[$this->id] ?? [] as $settingKey => $string) { + if (!is_array($string)) { + $this->setTypedSettingValue($settingKey, $string); + } + } + + if (count($this->settings) > 0) { + $this->raw['settings'] = $this->settings; + } + $this->raw['elements'] = $this->elements; + + foreach ($info->getOwnRelatedContent($this->id) as $path => $content) { + $this->raw = $this->setRelations($content, $externalContentElementor, $path, $submission)->toArray(); + } + + return new static($this->raw); + } + + private function setTypedSettingValue(string $settingKey, string $value): void + { + $existing = $this->settings[$settingKey] ?? null; + if (!is_array($existing) || !isset($existing['$$type'])) { + $this->settings[$settingKey] = $value; + return; + } + match ($existing['$$type']) { + 'string' => $this->settings[$settingKey]['value'] = $value, + 'html-v3' => $this->settings[$settingKey]['value']['content']['value'] = $value, + default => null, + }; + } +} diff --git a/inc/Smartling/ContentTypes/Elementor/ElementFactory.php b/inc/Smartling/ContentTypes/Elementor/ElementFactory.php index 30c9cde0..9153847b 100644 --- a/inc/Smartling/ContentTypes/Elementor/ElementFactory.php +++ b/inc/Smartling/ContentTypes/Elementor/ElementFactory.php @@ -2,42 +2,7 @@ namespace Smartling\ContentTypes\Elementor; -class ElementFactory { - public const UNKNOWN_ELEMENT = 'unknown'; - private const ELEMENTS = 'Elements'; - /** - * @var Element[] - */ - private array $elements = []; - - public function __construct() - { - foreach (new \DirectoryIterator(__DIR__ . DIRECTORY_SEPARATOR . self::ELEMENTS) as $fileInfo) { - if ($fileInfo->isFile() && $fileInfo->getExtension() === 'php') { - $className = $fileInfo->getFileInfo()->getBasename('.php'); - $element = new (implode('\\', [__NAMESPACE__, self::ELEMENTS, $className])); - if ($element instanceof Element) { - $this->elements[$element->getType()] = $element; - } - } - } - } - - public function fromArray(array $array): Element - { - foreach ($array['elements'] as &$element) { - $element = $this->fromArray($element); - } - unset($element); - - $type = self::UNKNOWN_ELEMENT; - - if (array_key_exists('elType', $array)) { - $type = $array['elType'] === 'widget' ? ($array['widgetType'] ?? self::UNKNOWN_ELEMENT) : $array['elType']; - } - - return array_key_exists($type, $this->elements) ? - $this->elements[$type]->fromArray($array) : - $this->elements[self::UNKNOWN_ELEMENT]->fromArray($array); - } -} +interface ElementFactory +{ + public function fromArray(array $array): Element; +} \ No newline at end of file diff --git a/inc/Smartling/ContentTypes/Elementor/ElementFactory3.php b/inc/Smartling/ContentTypes/Elementor/ElementFactory3.php new file mode 100644 index 00000000..9f106494 --- /dev/null +++ b/inc/Smartling/ContentTypes/Elementor/ElementFactory3.php @@ -0,0 +1,50 @@ +loadElements(__DIR__ . DIRECTORY_SEPARATOR . self::ELEMENTS); + } + + protected function loadElements(string $directory): void + { + $namespace = basename($directory); + foreach (new \DirectoryIterator($directory) as $fileInfo) { + if ($fileInfo->isFile() && $fileInfo->getExtension() === 'php') { + $className = $fileInfo->getFileInfo()->getBasename('.php'); + $element = new (implode('\\', [__NAMESPACE__, $namespace, $className])); + if ($element instanceof Element) { + $this->elements[$element->getType()] = $element; + } + } + } + } + + public function fromArray(array $array): Element + { + foreach ($array['elements'] as &$element) { + $element = $this->fromArray($element); + } + unset($element); + + $type = self::UNKNOWN_ELEMENT; + + if (array_key_exists('elType', $array)) { + $type = $array['elType'] === 'widget' ? ($array['widgetType'] ?? self::UNKNOWN_ELEMENT) : $array['elType']; + } + + return array_key_exists($type, $this->elements) ? + $this->elements[$type]->fromArray($array) : + $this->elements[self::UNKNOWN_ELEMENT]->fromArray($array); + } +} diff --git a/inc/Smartling/ContentTypes/Elementor/ElementFactory4.php b/inc/Smartling/ContentTypes/Elementor/ElementFactory4.php new file mode 100644 index 00000000..fe26f519 --- /dev/null +++ b/inc/Smartling/ContentTypes/Elementor/ElementFactory4.php @@ -0,0 +1,13 @@ +loadElements(__DIR__ . DIRECTORY_SEPARATOR . self::ELEMENTS4); + } +} diff --git a/inc/Smartling/ContentTypes/Elementor/Elements/Gallery.php b/inc/Smartling/ContentTypes/Elementor/Elements/Gallery.php index d5866d54..f3467636 100644 --- a/inc/Smartling/ContentTypes/Elementor/Elements/Gallery.php +++ b/inc/Smartling/ContentTypes/Elementor/Elements/Gallery.php @@ -3,7 +3,7 @@ namespace Smartling\ContentTypes\Elementor\Elements; use Smartling\ContentTypes\ContentTypeHelper; -use Smartling\ContentTypes\ExternalContentElementor; +use Smartling\ContentTypes\Elementor\ExternalContentElementorInterface; use Smartling\Models\Content; use Smartling\Models\RelatedContentInfo; use Smartling\Submissions\SubmissionEntity; @@ -41,7 +41,7 @@ public function getTranslatableStrings(): array return [$this->getId() => $return]; } - public function setTargetContent(ExternalContentElementor $externalContentElementor, RelatedContentInfo $info, array $strings, SubmissionEntity $submission): static + public function setTargetContent(ExternalContentElementorInterface $externalContentElementor, RelatedContentInfo $info, array $strings, SubmissionEntity $submission): static { $this->raw = parent::setTargetContent($externalContentElementor, $info, $strings, $submission)->toArray(); $this->settings = $this->raw['settings'] ?? []; diff --git a/inc/Smartling/ContentTypes/Elementor/Elements/IconList.php b/inc/Smartling/ContentTypes/Elementor/Elements/IconList.php index 15cacf21..8a09cc61 100644 --- a/inc/Smartling/ContentTypes/Elementor/Elements/IconList.php +++ b/inc/Smartling/ContentTypes/Elementor/Elements/IconList.php @@ -3,7 +3,7 @@ namespace Smartling\ContentTypes\Elementor\Elements; use Smartling\ContentTypes\ContentTypeHelper; -use Smartling\ContentTypes\ExternalContentElementor; +use Smartling\ContentTypes\Elementor\ExternalContentElementorInterface; use Smartling\Models\Content; use Smartling\Models\RelatedContentInfo; use Smartling\Submissions\SubmissionEntity; @@ -50,7 +50,7 @@ public function getTranslatableStrings(): array } public function setTargetContent( - ExternalContentElementor $externalContentElementor, + ExternalContentElementorInterface $externalContentElementor, RelatedContentInfo $info, array $strings, SubmissionEntity $submission, diff --git a/inc/Smartling/ContentTypes/Elementor/Elements/ImageGallery.php b/inc/Smartling/ContentTypes/Elementor/Elements/ImageGallery.php index 9d74e043..efbeabdf 100644 --- a/inc/Smartling/ContentTypes/Elementor/Elements/ImageGallery.php +++ b/inc/Smartling/ContentTypes/Elementor/Elements/ImageGallery.php @@ -3,10 +3,8 @@ namespace Smartling\ContentTypes\Elementor\Elements; use Smartling\ContentTypes\ContentTypeHelper; -use Smartling\ContentTypes\ExternalContentElementor; use Smartling\Models\Content; use Smartling\Models\RelatedContentInfo; -use Smartling\Submissions\SubmissionEntity; class ImageGallery extends Unknown { public function getType(): string diff --git a/inc/Smartling/ContentTypes/Elementor/Elements/MegaMenu.php b/inc/Smartling/ContentTypes/Elementor/Elements/MegaMenu.php index c5720cba..0dcd31cf 100644 --- a/inc/Smartling/ContentTypes/Elementor/Elements/MegaMenu.php +++ b/inc/Smartling/ContentTypes/Elementor/Elements/MegaMenu.php @@ -3,7 +3,7 @@ namespace Smartling\ContentTypes\Elementor\Elements; use Smartling\ContentTypes\ContentTypeHelper; -use Smartling\ContentTypes\ExternalContentElementor; +use Smartling\ContentTypes\Elementor\ExternalContentElementorInterface; use Smartling\Models\Content; use Smartling\Models\RelatedContentInfo; use Smartling\Submissions\SubmissionEntity; @@ -65,7 +65,7 @@ public function getTranslatableStrings(): array } public function setTargetContent( - ExternalContentElementor $externalContentElementor, + ExternalContentElementorInterface $externalContentElementor, RelatedContentInfo $info, array $strings, SubmissionEntity $submission, diff --git a/inc/Smartling/ContentTypes/Elementor/Elements/NestedAccordion.php b/inc/Smartling/ContentTypes/Elementor/Elements/NestedAccordion.php index 0c573000..89dd84c9 100644 --- a/inc/Smartling/ContentTypes/Elementor/Elements/NestedAccordion.php +++ b/inc/Smartling/ContentTypes/Elementor/Elements/NestedAccordion.php @@ -3,7 +3,7 @@ namespace Smartling\ContentTypes\Elementor\Elements; use Smartling\ContentTypes\ContentTypeHelper; -use Smartling\ContentTypes\ExternalContentElementor; +use Smartling\ContentTypes\Elementor\ExternalContentElementorInterface; use Smartling\Models\Content; use Smartling\Models\RelatedContentInfo; use Smartling\Submissions\SubmissionEntity; @@ -42,7 +42,7 @@ public function getTranslatableStrings(): array } public function setTargetContent( - ExternalContentElementor $externalContentElementor, + ExternalContentElementorInterface $externalContentElementor, RelatedContentInfo $info, array $strings, SubmissionEntity $submission, diff --git a/inc/Smartling/ContentTypes/Elementor/Elements/Reviews.php b/inc/Smartling/ContentTypes/Elementor/Elements/Reviews.php index 6c8085dd..cf062e88 100644 --- a/inc/Smartling/ContentTypes/Elementor/Elements/Reviews.php +++ b/inc/Smartling/ContentTypes/Elementor/Elements/Reviews.php @@ -3,7 +3,7 @@ namespace Smartling\ContentTypes\Elementor\Elements; use Smartling\ContentTypes\ContentTypeHelper; -use Smartling\ContentTypes\ExternalContentElementor; +use Smartling\ContentTypes\Elementor\ExternalContentElementorInterface; use Smartling\Models\Content; use Smartling\Models\RelatedContentInfo; use Smartling\Submissions\SubmissionEntity; @@ -39,7 +39,7 @@ public function getTranslatableStrings(): array return [$this->getId() => $return]; } - public function setTargetContent(ExternalContentElementor $externalContentElementor, RelatedContentInfo $info, array $strings, SubmissionEntity $submission): static + public function setTargetContent(ExternalContentElementorInterface $externalContentElementor, RelatedContentInfo $info, array $strings, SubmissionEntity $submission): static { foreach ($strings[$this->id] ?? [] as $key => $array) { if (is_array($array)) { diff --git a/inc/Smartling/ContentTypes/Elementor/Elements/SocialIcons.php b/inc/Smartling/ContentTypes/Elementor/Elements/SocialIcons.php index ac072f99..86987486 100644 --- a/inc/Smartling/ContentTypes/Elementor/Elements/SocialIcons.php +++ b/inc/Smartling/ContentTypes/Elementor/Elements/SocialIcons.php @@ -3,10 +3,8 @@ namespace Smartling\ContentTypes\Elementor\Elements; use Smartling\ContentTypes\ContentTypeHelper; -use Smartling\ContentTypes\ExternalContentElementor; use Smartling\Models\Content; use Smartling\Models\RelatedContentInfo; -use Smartling\Submissions\SubmissionEntity; class SocialIcons extends Unknown { private string $settingsKey = 'social_icon_list'; diff --git a/inc/Smartling/ContentTypes/Elementor/Elements/Tabs.php b/inc/Smartling/ContentTypes/Elementor/Elements/Tabs.php index e0e31866..34ef7f24 100644 --- a/inc/Smartling/ContentTypes/Elementor/Elements/Tabs.php +++ b/inc/Smartling/ContentTypes/Elementor/Elements/Tabs.php @@ -2,7 +2,7 @@ namespace Smartling\ContentTypes\Elementor\Elements; -use Smartling\ContentTypes\ExternalContentElementor; +use Smartling\ContentTypes\Elementor\ExternalContentElementorInterface; use Smartling\Models\RelatedContentInfo; use Smartling\Submissions\SubmissionEntity; @@ -28,7 +28,7 @@ public function getTranslatableStrings(): array return [$this->getId() => $return]; } - public function setTargetContent(ExternalContentElementor $externalContentElementor, RelatedContentInfo $info, array $strings, SubmissionEntity $submission,): static + public function setTargetContent(ExternalContentElementorInterface $externalContentElementor, RelatedContentInfo $info, array $strings, SubmissionEntity $submission,): static { foreach ($strings[$this->id] ?? [] as $array) { if (is_array($array)) { diff --git a/inc/Smartling/ContentTypes/Elementor/Elements/Unknown.php b/inc/Smartling/ContentTypes/Elementor/Elements/Unknown.php index c96094f6..bfa7216b 100644 --- a/inc/Smartling/ContentTypes/Elementor/Elements/Unknown.php +++ b/inc/Smartling/ContentTypes/Elementor/Elements/Unknown.php @@ -5,7 +5,7 @@ use Smartling\ContentTypes\ContentTypeHelper; use Smartling\ContentTypes\Elementor\Element; use Smartling\ContentTypes\Elementor\ElementAbstract; -use Smartling\ContentTypes\Elementor\ElementFactory; +use Smartling\ContentTypes\Elementor\ElementFactory3; use Smartling\Helpers\ArrayHelper; use Smartling\Helpers\LoggerSafeTrait; use Smartling\Models\Content; @@ -71,6 +71,6 @@ public function getTranslatableStrings(): array public function getType(): string { - return ElementFactory::UNKNOWN_ELEMENT; + return ElementFactory3::UNKNOWN_ELEMENT; } } diff --git a/inc/Smartling/ContentTypes/Elementor/Elements4/EButton.php b/inc/Smartling/ContentTypes/Elementor/Elements4/EButton.php new file mode 100644 index 00000000..f8348cb8 --- /dev/null +++ b/inc/Smartling/ContentTypes/Elementor/Elements4/EButton.php @@ -0,0 +1,35 @@ +elements as $element) { + $return = $return->include($element->getRelated(), $this->id); + } + return $return; + } + + public function getTranslatableStrings(): array + { + $result = []; + foreach (['text'] as $key) { + $value = $this->extractTypedValue($this->settings[$key] ?? null); + if ($value !== null) { + $result[$key] = $value; + } + } + return [$this->id => $result]; + } +} diff --git a/inc/Smartling/ContentTypes/Elementor/Elements4/EFormInput.php b/inc/Smartling/ContentTypes/Elementor/Elements4/EFormInput.php new file mode 100644 index 00000000..ce19a7db --- /dev/null +++ b/inc/Smartling/ContentTypes/Elementor/Elements4/EFormInput.php @@ -0,0 +1,35 @@ +elements as $element) { + $return = $return->include($element->getRelated(), $this->id); + } + return $return; + } + + public function getTranslatableStrings(): array + { + $result = []; + foreach (['placeholder'] as $key) { + $value = $this->extractTypedValue($this->settings[$key] ?? null); + if ($value !== null) { + $result[$key] = $value; + } + } + return [$this->id => $result]; + } +} diff --git a/inc/Smartling/ContentTypes/Elementor/Elements4/EFormLabel.php b/inc/Smartling/ContentTypes/Elementor/Elements4/EFormLabel.php new file mode 100644 index 00000000..55cb2da5 --- /dev/null +++ b/inc/Smartling/ContentTypes/Elementor/Elements4/EFormLabel.php @@ -0,0 +1,35 @@ +elements as $element) { + $return = $return->include($element->getRelated(), $this->id); + } + return $return; + } + + public function getTranslatableStrings(): array + { + $result = []; + foreach (['text'] as $key) { + $value = $this->extractTypedValue($this->settings[$key] ?? null); + if ($value !== null) { + $result[$key] = $value; + } + } + return [$this->id => $result]; + } +} diff --git a/inc/Smartling/ContentTypes/Elementor/Elements4/EFormTextarea.php b/inc/Smartling/ContentTypes/Elementor/Elements4/EFormTextarea.php new file mode 100644 index 00000000..0677d367 --- /dev/null +++ b/inc/Smartling/ContentTypes/Elementor/Elements4/EFormTextarea.php @@ -0,0 +1,35 @@ +elements as $element) { + $return = $return->include($element->getRelated(), $this->id); + } + return $return; + } + + public function getTranslatableStrings(): array + { + $result = []; + foreach (['placeholder'] as $key) { + $value = $this->extractTypedValue($this->settings[$key] ?? null); + if ($value !== null) { + $result[$key] = $value; + } + } + return [$this->id => $result]; + } +} diff --git a/inc/Smartling/ContentTypes/Elementor/Elements4/EHeading.php b/inc/Smartling/ContentTypes/Elementor/Elements4/EHeading.php new file mode 100644 index 00000000..95f7f240 --- /dev/null +++ b/inc/Smartling/ContentTypes/Elementor/Elements4/EHeading.php @@ -0,0 +1,35 @@ +elements as $element) { + $return = $return->include($element->getRelated(), $this->id); + } + return $return; + } + + public function getTranslatableStrings(): array + { + $result = []; + foreach (['title'] as $key) { + $value = $this->extractTypedValue($this->settings[$key] ?? null); + if ($value !== null) { + $result[$key] = $value; + } + } + return [$this->id => $result]; + } +} diff --git a/inc/Smartling/ContentTypes/Elementor/Elements4/EImage.php b/inc/Smartling/ContentTypes/Elementor/Elements4/EImage.php new file mode 100644 index 00000000..3d52dc45 --- /dev/null +++ b/inc/Smartling/ContentTypes/Elementor/Elements4/EImage.php @@ -0,0 +1,40 @@ +elements as $element) { + $return = $return->include($element->getRelated(), $this->id); + } + $id = $this->getIntSettingByKey(self::IMAGE_ID_PATH, $this->settings); + if ($id !== null) { + $return->addContent( + new Content($id, ContentTypeHelper::POST_TYPE_ATTACHMENT), + $this->id, + 'settings/' . self::IMAGE_ID_PATH, + ); + } + return $return; + } + + public function getTranslatableStrings(): array + { + return [$this->id => []]; + } +} diff --git a/inc/Smartling/ContentTypes/Elementor/Elements4/EParagraph.php b/inc/Smartling/ContentTypes/Elementor/Elements4/EParagraph.php new file mode 100644 index 00000000..f0cf44e5 --- /dev/null +++ b/inc/Smartling/ContentTypes/Elementor/Elements4/EParagraph.php @@ -0,0 +1,35 @@ +elements as $element) { + $return = $return->include($element->getRelated(), $this->id); + } + return $return; + } + + public function getTranslatableStrings(): array + { + $result = []; + foreach (['paragraph'] as $key) { + $value = $this->extractTypedValue($this->settings[$key] ?? null); + if ($value !== null) { + $result[$key] = $value; + } + } + return [$this->id => $result]; + } +} diff --git a/inc/Smartling/ContentTypes/Elementor/ExternalContentElementorInterface.php b/inc/Smartling/ContentTypes/Elementor/ExternalContentElementorInterface.php new file mode 100644 index 00000000..57075ffb --- /dev/null +++ b/inc/Smartling/ContentTypes/Elementor/ExternalContentElementorInterface.php @@ -0,0 +1,18 @@ +getDataFromPostMeta($submission->getTargetId()); $this->userHelper->asAdministratorOrEditor(function () use ($data, $documentsManager, $submission) { try { - /** @noinspection PhpParamsInspection */ $documentsManager->ajax_save([ 'editor_post_id' => $submission->getTargetId(), 'elements' => json_decode($data, @@ -158,16 +159,6 @@ private function readMeta(int $id): array return json_decode($this->getDataFromPostMeta($id), true, 512, JSON_THROW_ON_ERROR); } - public function getMaxVersion(): string - { - return '3'; - } - - public function getMinVersion(): string - { - return '3'; - } - public function getPluginId(): string { return 'elementor'; @@ -186,11 +177,12 @@ public function getRelatedContent(string $contentType, int $contentId): array private function mergeElementorData(array $original, array $strings, SubmissionEntity $submission): array { $result = []; + $relatedContentInfo = $this->getData($original)->getRelatedContentInfo(); foreach ($original as $array) { $element = $this->elementFactory->fromArray($array); $result[] = $element->setTargetContent( $this, - $this->getData($original)->getRelatedContentInfo(), + $relatedContentInfo, $strings, $submission, )->toArray(); diff --git a/inc/config/services.yml b/inc/config/services.yml index aa49fd98..34d701f1 100644 --- a/inc/config/services.yml +++ b/inc/config/services.yml @@ -133,11 +133,11 @@ services: - "@manager.submission" - "@wp.proxy" - content.elementor: - class: Smartling\ContentTypes\ExternalContentElementor + content.elementor3: + class: Smartling\ContentTypes\ExternalContentElementor3 arguments: - "@content.type.helper" - - "@elementor.factory" + - "@elementor.factory3" - "@fields-filter.helper" - "@helper.plugins" - "@site.helper" @@ -146,8 +146,24 @@ services: - "@wp.proxy" - "@link.processor" - elementor.factory: - class: Smartling\ContentTypes\Elementor\ElementFactory + elementor.factory3: + class: Smartling\ContentTypes\Elementor\ElementFactory3 + + content.elementor4: + class: Smartling\ContentTypes\ExternalContentElementor4 + arguments: + - "@content.type.helper" + - "@elementor.factory4" + - "@fields-filter.helper" + - "@helper.plugins" + - "@site.helper" + - "@manager.submission" + - "@helper.user" + - "@wp.proxy" + - "@link.processor" + + elementor.factory4: + class: Smartling\ContentTypes\Elementor\ElementFactory4 content.gravity.forms: class: Smartling\ContentTypes\ExternalContentGravityForms @@ -241,7 +257,8 @@ services: calls: - ["addHandler", ["@content.aioseo"]] - ["addHandler", ["@content.beaver.builder"]] - - ["addHandler", ["@content.elementor"]] + - ["addHandler", ["@content.elementor3"]] + - ["addHandler", ["@content.elementor4"]] - ["addHandler", ["@content.gravity.forms"]] - ["addHandler", ["@content.yoast"]] diff --git a/readme.txt b/readme.txt index 985aa757..0849f307 100755 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: translation, localization, multilingual, internationalization, smartling Requires at least: 5.5 Tested up to: 6.9 Requires PHP: 8.0 -Stable tag: 5.3.6 +Stable tag: 5.4.0 License: GPLv2 or later Translate content in WordPress quickly and seamlessly with Smartling, the industry-leading Translation Management System. @@ -62,6 +62,9 @@ Additional information on the Smartling Connector for WordPress can be found [he 3. Track translation status within WordPress from the Submissions Board. View overall progress of submitted translation requests as well as resend updated content. == Changelog == += 5.4.0 = +* Added support for Elementor 4 + = 5.3.6 = * Added support for Elementor posts widget diff --git a/smartling-connector.php b/smartling-connector.php index 5821dd48..357b58aa 100755 --- a/smartling-connector.php +++ b/smartling-connector.php @@ -11,7 +11,7 @@ * Plugin Name: Smartling Connector * Plugin URI: https://www.smartling.com/products/automate/integrations/wordpress/ * Description: Integrate your WordPress site with Smartling to upload your content and download translations. - * Version: 5.3.6 + * Version: 5.4.0 * Author: Smartling * Author URI: https://www.smartling.com * License: GPL-2.0+ diff --git a/tests/Smartling/ContentTypes/Elementor/ElementAbstractTest.php b/tests/Smartling/ContentTypes/Elementor/ElementAbstractTest.php index b3312c11..e34d76a7 100644 --- a/tests/Smartling/ContentTypes/Elementor/ElementAbstractTest.php +++ b/tests/Smartling/ContentTypes/Elementor/ElementAbstractTest.php @@ -3,7 +3,6 @@ namespace Smartling\ContentTypes\Elementor; use PHPUnit\Framework\TestCase; -use Smartling\ContentTypes\ExternalContentElementor; use Smartling\ContentTypes\Elementor\Elements\Unknown; use Smartling\Models\RelatedContentInfo; use Smartling\Submissions\SubmissionEntity; @@ -56,7 +55,7 @@ public function testSetTargetContentWithDynamicSettingsAndElementorTags(): void ] ]); - $externalContentElementor = $this->createMock(ExternalContentElementor::class); + $externalContentElementor = $this->createMock(ExternalContentElementorInterface::class); $targetId = 13; $externalContentElementor->method('getTargetId')->willReturn($targetId); diff --git a/tests/Smartling/ContentTypes/Elementor/GalleryTest.php b/tests/Smartling/ContentTypes/Elementor/GalleryTest.php index 0edb9cac..3e6503b8 100644 --- a/tests/Smartling/ContentTypes/Elementor/GalleryTest.php +++ b/tests/Smartling/ContentTypes/Elementor/GalleryTest.php @@ -5,7 +5,6 @@ use PHPUnit\Framework\TestCase; use Smartling\ContentTypes\ContentTypeHelper; use Smartling\ContentTypes\Elementor\Elements\Gallery; -use Smartling\ContentTypes\ExternalContentElementor; use Smartling\Models\Content; use Smartling\Models\RelatedContentInfo; use Smartling\Submissions\SubmissionEntity; @@ -28,7 +27,7 @@ public function testRelated(): void $imageSourceId = 21162; $imageTargetId = 31162; - $externalContentElementor = $this->createMock(ExternalContentElementor::class); + $externalContentElementor = $this->createMock(ExternalContentElementorInterface::class); $externalContentElementor->method('getTargetId') ->with(0, $imageSourceId, 0, ContentTypeHelper::POST_TYPE_ATTACHMENT)->willReturn($imageTargetId); @@ -65,7 +64,7 @@ public function testSetTargetContent(): void ['gallery_title' => 'New Gallery', '_id' => '04c68ec'], ['gallery_title' => 'Second Gallery', '_id' => 'ab12345'], ]])->setTargetContent( - $this->createMock(ExternalContentElementor::class), + $this->createMock(ExternalContentElementorInterface::class), new RelatedContentInfo([]), [ '14d5abc' => [ diff --git a/tests/Smartling/ContentTypes/Elementor/ImageGalleryTest.php b/tests/Smartling/ContentTypes/Elementor/ImageGalleryTest.php index ec5f3b40..0b7359d0 100644 --- a/tests/Smartling/ContentTypes/Elementor/ImageGalleryTest.php +++ b/tests/Smartling/ContentTypes/Elementor/ImageGalleryTest.php @@ -5,7 +5,6 @@ use PHPUnit\Framework\TestCase; use Smartling\ContentTypes\ContentTypeHelper; use Smartling\ContentTypes\Elementor\Elements\ImageGallery; -use Smartling\ContentTypes\ExternalContentElementor; use Smartling\Models\Content; use Smartling\Submissions\SubmissionEntity; @@ -16,7 +15,7 @@ public function testRelated(): void $imageSourceId = 7; $imageTargetId = 11; - $externalContentElementor = $this->createMock(ExternalContentElementor::class); + $externalContentElementor = $this->createMock(ExternalContentElementorInterface::class); $externalContentElementor->method('getTargetId') ->with(0, $imageSourceId, 0, ContentTypeHelper::POST_TYPE_ATTACHMENT)->willReturn($imageTargetId); diff --git a/tests/Smartling/ContentTypes/Elementor/LoopCarouselTest.php b/tests/Smartling/ContentTypes/Elementor/LoopCarouselTest.php index e2abe645..0149a18d 100644 --- a/tests/Smartling/ContentTypes/Elementor/LoopCarouselTest.php +++ b/tests/Smartling/ContentTypes/Elementor/LoopCarouselTest.php @@ -5,7 +5,6 @@ use PHPUnit\Framework\TestCase; use Smartling\ContentTypes\ContentTypeHelper; use Smartling\ContentTypes\Elementor\Elements\LoopCarousel; -use Smartling\ContentTypes\ExternalContentElementor; use Smartling\Helpers\WordpressFunctionProxyHelper; use Smartling\Models\Content; use Smartling\Submissions\SubmissionEntity; @@ -20,7 +19,7 @@ public function testTemplateIdType(): void $proxy = $this->createMock(WordpressFunctionProxyHelper::class); $proxy->expects($this->once())->method('get_post_type')->with($templateSourceId)->willReturn('post'); - $externalContentElementor = $this->createMock(ExternalContentElementor::class); + $externalContentElementor = $this->createMock(ExternalContentElementorInterface::class); $externalContentElementor->method('getTargetId')->willReturn($templateTargetId); $externalContentElementor->method('getWpProxy')->willReturn($proxy); @@ -56,7 +55,7 @@ public function testTermIdTranslation(): void $termSourceId = 14; $termTargetId = 28; - $externalContentElementor = $this->createMock(ExternalContentElementor::class); + $externalContentElementor = $this->createMock(ExternalContentElementorInterface::class); $externalContentElementor->method('getTargetId') ->with(0, $termSourceId, 0, ContentTypeHelper::CONTENT_TYPE_TAXONOMY)->willReturn($termTargetId); diff --git a/tests/Smartling/ContentTypes/Elementor/MegaMenuTest.php b/tests/Smartling/ContentTypes/Elementor/MegaMenuTest.php index a838e151..2f41dad4 100644 --- a/tests/Smartling/ContentTypes/Elementor/MegaMenuTest.php +++ b/tests/Smartling/ContentTypes/Elementor/MegaMenuTest.php @@ -5,7 +5,6 @@ use PHPUnit\Framework\TestCase; use Smartling\ContentTypes\ContentTypeHelper; use Smartling\ContentTypes\Elementor\Elements\MegaMenu; -use Smartling\ContentTypes\ExternalContentElementor; use Smartling\Helpers\WordpressFunctionProxyHelper; use Smartling\Models\RelatedContentInfo; use Smartling\Submissions\SubmissionEntity; @@ -105,7 +104,7 @@ public function testGetTranslatableStringsReturnsItemTitles(): void public function testSetTargetContentAppliesTranslatedMenuName(): void { $proxy = $this->createMock(WordpressFunctionProxyHelper::class); - $externalContentElementor = $this->createMock(ExternalContentElementor::class); + $externalContentElementor = $this->createMock(ExternalContentElementorInterface::class); $externalContentElementor->method('getWpProxy')->willReturn($proxy); $widget = $this->makeWidget(['menu_name' => 'Menu']); @@ -127,7 +126,7 @@ public function testSetTargetContentAppliesTranslatedMenuName(): void public function testSetTargetContentAppliesTranslatedItemTitles(): void { $proxy = $this->createMock(WordpressFunctionProxyHelper::class); - $externalContentElementor = $this->createMock(ExternalContentElementor::class); + $externalContentElementor = $this->createMock(ExternalContentElementorInterface::class); $externalContentElementor->method('getWpProxy')->willReturn($proxy); $widget = $this->makeWidget([ @@ -160,7 +159,7 @@ public function testSetTargetContentAppliesTranslatedItemTitles(): void public function testSetTargetContentAppliesTranslatedMenuNameAndItemTitlesTogether(): void { $proxy = $this->createMock(WordpressFunctionProxyHelper::class); - $externalContentElementor = $this->createMock(ExternalContentElementor::class); + $externalContentElementor = $this->createMock(ExternalContentElementorInterface::class); $externalContentElementor->method('getWpProxy')->willReturn($proxy); $widget = $this->makeWidget([ @@ -199,7 +198,7 @@ public function testSetTargetContentUpdatesIconId(): void $targetId = 99999; $proxy = $this->createMock(WordpressFunctionProxyHelper::class); - $externalContentElementor = $this->createMock(ExternalContentElementor::class); + $externalContentElementor = $this->createMock(ExternalContentElementorInterface::class); $externalContentElementor->method('getTargetId') ->with(0, $sourceId, 0, ContentTypeHelper::POST_TYPE_ATTACHMENT) ->willReturn($targetId); diff --git a/tests/Smartling/ContentTypes/Elementor/NestedAccordionTest.php b/tests/Smartling/ContentTypes/Elementor/NestedAccordionTest.php index 8710697b..4432927c 100644 --- a/tests/Smartling/ContentTypes/Elementor/NestedAccordionTest.php +++ b/tests/Smartling/ContentTypes/Elementor/NestedAccordionTest.php @@ -5,7 +5,6 @@ use PHPUnit\Framework\TestCase; use Smartling\ContentTypes\ContentTypeHelper; use Smartling\ContentTypes\Elementor\Elements\NestedAccordion; -use Smartling\ContentTypes\ExternalContentElementor; use Smartling\Helpers\WordpressFunctionProxyHelper; use Smartling\Models\Content; use Smartling\Models\RelatedContentInfo; @@ -57,7 +56,7 @@ public function testSetRelationsUpdatesIconId(): void $targetId = 500; $proxy = $this->createMock(WordpressFunctionProxyHelper::class); - $externalContentElementor = $this->createMock(ExternalContentElementor::class); + $externalContentElementor = $this->createMock(ExternalContentElementorInterface::class); $externalContentElementor->method('getTargetId')->willReturn($targetId); $externalContentElementor->method('getWpProxy')->willReturn($proxy); @@ -92,7 +91,7 @@ public function testGetTranslatableStringsReturnsItemTitles(): void public function testSetTargetContentAppliesTranslatedItemTitles(): void { $proxy = $this->createMock(WordpressFunctionProxyHelper::class); - $externalContentElementor = $this->createMock(ExternalContentElementor::class); + $externalContentElementor = $this->createMock(ExternalContentElementorInterface::class); $externalContentElementor->method('getWpProxy')->willReturn($proxy); $widget = $this->makeWidget([ @@ -128,7 +127,7 @@ public function testSetTargetContentAppliesIconIdTranslation(): void $targetId = 500; $proxy = $this->createMock(WordpressFunctionProxyHelper::class); - $externalContentElementor = $this->createMock(ExternalContentElementor::class); + $externalContentElementor = $this->createMock(ExternalContentElementorInterface::class); $externalContentElementor->method('getTargetId') ->with(0, $sourceId, 0, ContentTypeHelper::POST_TYPE_ATTACHMENT) ->willReturn($targetId); diff --git a/tests/Smartling/ContentTypes/Elementor/PostsTest.php b/tests/Smartling/ContentTypes/Elementor/PostsTest.php index a0825f8d..d1d58abb 100644 --- a/tests/Smartling/ContentTypes/Elementor/PostsTest.php +++ b/tests/Smartling/ContentTypes/Elementor/PostsTest.php @@ -5,7 +5,6 @@ use PHPUnit\Framework\TestCase; use Smartling\ContentTypes\ContentTypeHelper; use Smartling\ContentTypes\Elementor\Elements\Posts; -use Smartling\ContentTypes\ExternalContentElementor; use Smartling\Models\RelatedContentInfo; use Smartling\Submissions\SubmissionEntity; @@ -116,7 +115,7 @@ public function testSetTargetContent(): void 'text' => 'Load More', 'loadmore_loading_text' => 'Loading...', ])->setTargetContent( - $this->createMock(ExternalContentElementor::class), + $this->createMock(ExternalContentElementorInterface::class), new RelatedContentInfo([]), ['abc123' => ['text' => 'Cargar más', 'loadmore_loading_text' => 'Cargando...']], $this->createMock(SubmissionEntity::class), diff --git a/tests/Smartling/ContentTypes/Elementor/ShortcodeTest.php b/tests/Smartling/ContentTypes/Elementor/ShortcodeTest.php index e08b7b82..88d67fb6 100644 --- a/tests/Smartling/ContentTypes/Elementor/ShortcodeTest.php +++ b/tests/Smartling/ContentTypes/Elementor/ShortcodeTest.php @@ -4,7 +4,6 @@ use PHPUnit\Framework\TestCase; use Smartling\ContentTypes\Elementor\Elements\Shortcode; -use Smartling\ContentTypes\ExternalContentElementor; use Smartling\Models\RelatedContentInfo; use Smartling\Submissions\SubmissionEntity; @@ -47,7 +46,7 @@ public function testSetTargetContent(): void $result = $this->makeWidget(['shortcode' => '[contact-form-7 id="123" title="Contact form 1"]']) ->setTargetContent( - $this->createMock(ExternalContentElementor::class), + $this->createMock(ExternalContentElementorInterface::class), new RelatedContentInfo([]), ['abc123' => ['shortcode' => $translatedShortcode]], $this->createMock(SubmissionEntity::class), diff --git a/tests/Smartling/ContentTypes/ExternalContentElementorTest.php b/tests/Smartling/ContentTypes/ExternalContentElementor3Test.php similarity index 97% rename from tests/Smartling/ContentTypes/ExternalContentElementorTest.php rename to tests/Smartling/ContentTypes/ExternalContentElementor3Test.php index e091064f..40fc31af 100644 --- a/tests/Smartling/ContentTypes/ExternalContentElementorTest.php +++ b/tests/Smartling/ContentTypes/ExternalContentElementor3Test.php @@ -3,8 +3,8 @@ namespace Smartling\Tests\Smartling\ContentTypes; use Smartling\ContentTypes\ContentTypeHelper; -use Smartling\ContentTypes\Elementor\ElementFactory; -use Smartling\ContentTypes\ExternalContentElementor; +use Smartling\ContentTypes\Elementor\ElementFactory3; +use Smartling\ContentTypes\ExternalContentElementor3; use PHPUnit\Framework\TestCase; use Smartling\Extensions\Pluggable; use Smartling\Helpers\FieldsFilterHelper; @@ -16,7 +16,7 @@ use Smartling\Submissions\SubmissionEntity; use Smartling\Submissions\SubmissionManager; -class ExternalContentElementorTest extends TestCase { +class ExternalContentElementor3Test extends TestCase { public function testCanHandle() { $proxy = $this->createMock(WordpressFunctionProxyHelper::class); @@ -58,7 +58,7 @@ public function extractElementorDataProvider(): array [ContentTypeHelper::POST_TYPE_ATTACHMENT => [597]], ], 'icon' => [ - file_get_contents(__DIR__ . '/icon.json'), + file_get_contents(__DIR__ . '/fixtures/icon.json'), [], [ContentTypeHelper::POST_TYPE_ATTACHMENT => [1033]], ], @@ -79,7 +79,7 @@ public function extractElementorDataProvider(): array ], ], 'realistic content with background images' => [ - file_get_contents(__DIR__ . '/wp-834.json'), + file_get_contents(__DIR__ . '/fixtures/wp-834.json'), [ '10733aaf/215ff951/background_image/alt' => '', '10733aaf/43212dc7/14c1dc16/title' => 'Now in Company Wallet and Company: Breezy and secure workforce access.', @@ -132,7 +132,7 @@ public function extractElementorDataProvider(): array ], ], 'realistic content with icon lists' => [ - file_get_contents(__DIR__ . '/wp-836.json'), + file_get_contents(__DIR__ . '/fixtures/wp-836.json'), [ '6ff0959b/160f1f6a/background_image/alt' => '', '6ff0959b/1e8393/7a705d82/title' => 'Connect physical assets to identities. Securely.', @@ -243,7 +243,7 @@ public function extractElementorDataProvider(): array ], ], 'Social icons widget, menus, icons, templates' => [ - file_get_contents(__DIR__ . '/wp-952.json'), + file_get_contents(__DIR__ . '/fixtures/wp-952.json'), [ 'b966541/0841fb9/2dd1556/7399cf4/85f7501/259735a/5ad501f/fbb4f70/title' => 'Products', 'b966541/0841fb9/2dd1556/7399cf4/85f7501/259735a/5ad501f/f530aca/editor' => '
System Integration Automation.
', @@ -308,7 +308,7 @@ public function extractElementorDataProvider(): array ] ], 'Unlimited Elements addon Logo Marquee' => [ - file_get_contents(__DIR__ . '/wp-944.json'), + file_get_contents(__DIR__ . '/fixtures/wp-944.json'), [], [ 'attachment' => [ @@ -322,7 +322,7 @@ public function extractElementorDataProvider(): array ], ], 'Unlimited Elements addon Listing Grid' => [ - file_get_contents(__DIR__ . '/ucaddon_ue_listing_grid.json'), + file_get_contents(__DIR__ . '/fixtures/ucaddon_ue_listing_grid.json'), ['7bb0b763/no_posts_found' => 'No posts found'], [ContentTypeHelper::CONTENT_TYPE_UNKNOWN => [1531]], ], @@ -347,7 +347,7 @@ public function extractElementorDataProvider(): array [ContentTypeHelper::POST_TYPE_ATTACHMENT => [329, 330]], ], 'dynamic content in elements' => [ - file_get_contents(__DIR__ . '/wp-975.json'), + file_get_contents(__DIR__ . '/fixtures/wp-975.json'), [], [ ContentTypeHelper::POST_TYPE_ATTACHMENT => [ @@ -425,18 +425,18 @@ public function testSetContentFieldsRelationsChange() 'post_content' => 'irrelevant', ], 'meta' => [ - ExternalContentElementor::META_FIELD_NAME => $elementorData, + ExternalContentElementor3::META_FIELD_NAME => $elementorData, ], ]; $this->assertEquals( str_replace($sourceAttachmentId, $targetAttachmentId, $elementorData), $this->getExternalContentElementor($proxy, $submissionManager) - ->setContentFields($original, $original, $submission)['meta'][ExternalContentElementor::META_FIELD_NAME] + ->setContentFields($original, $original, $submission)['meta'][ExternalContentElementor3::META_FIELD_NAME] ); } - private function getExternalContentElementor(?WordpressFunctionProxyHelper $proxy = null, ?SubmissionManager $submissionManager = null): ExternalContentElementor + private function getExternalContentElementor(?WordpressFunctionProxyHelper $proxy = null, ?SubmissionManager $submissionManager = null): ExternalContentElementor3 { $contentTypeHelper = $this->createMock(ContentTypeHelper::class); $contentTypeHelper->method('isPost')->willReturn(true); @@ -452,9 +452,9 @@ private function getExternalContentElementor(?WordpressFunctionProxyHelper $prox $siteHelper = $this->createPartialMock(SiteHelper::class, ['restoreBlogId', 'switchBlogId']); - return new ExternalContentElementor( + return new ExternalContentElementor3( $contentTypeHelper, - new ElementFactory(), + new ElementFactory3(), $fieldsFilterHelper, $pluginHelper, $siteHelper, diff --git a/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php b/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php new file mode 100644 index 00000000..83402ff7 --- /dev/null +++ b/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php @@ -0,0 +1,404 @@ +createMock(ContentTypeHelper::class); + $contentTypeHelper->method('isPost')->willReturn(true); + $pluginHelper = $this->createMock(PluginHelper::class); + $pluginHelper->method('versionInRange')->willReturn(true); + if ($proxy === null) { + $proxy = $this->createMock(WordpressFunctionProxyHelper::class); + } + $submissionManager = $this->createMock(SubmissionManager::class); + $fieldsFilterHelper = $this->getMockBuilder(FieldsFilterHelper::class)->disableOriginalConstructor()->onlyMethods([])->getMock(); + $siteHelper = $this->createPartialMock(SiteHelper::class, ['restoreBlogId', 'switchBlogId']); + + return new ExternalContentElementor4( + $contentTypeHelper, + new ElementFactory4(), + $fieldsFilterHelper, + $pluginHelper, + $siteHelper, + $submissionManager, + $this->createMock(UserHelper::class), + $proxy, + $this->createMock(LinkProcessor::class), + ); + } + + private function makeProxy(string $data): WordpressFunctionProxyHelper + { + $proxy = $this->createMock(WordpressFunctionProxyHelper::class); + $proxy->method('getPostMeta')->willReturn($data); + $proxy->method('get_plugins')->willReturn(['elementor/elementor.php' => ['Version' => '4.0.0']]); + $proxy->method('is_plugin_active')->willReturn(true); + return $proxy; + } + + private function mockSubmission(): SubmissionEntity + { + $submission = $this->createMock(SubmissionEntity::class); + $submission->method('getSourceId')->willReturn(1); + return $submission; + } + + public function testCanHandle(): void + { + $proxy = $this->createMock(WordpressFunctionProxyHelper::class); + $proxy->method('getPostMeta')->willReturnOnConsecutiveCalls('', json_encode([[ + 'id' => 'c1', 'elType' => 'e-flexbox', 'settings' => [], 'elements' => [], + 'isInner' => false, 'styles' => [], 'interactions' => [], 'editor_settings' => [], 'version' => '0.0', + ]])); + $proxy->method('get_plugins')->willReturn(['elementor/elementor.php' => ['Version' => '4.0.0']]); + $proxy->method('is_plugin_active')->willReturn(true); + $this->assertEquals(Pluggable::NOT_SUPPORTED, $this->getHandler($proxy)->getSupportLevel('post', 1)); + $this->assertEquals(Pluggable::SUPPORTED, $this->getHandler($proxy)->getSupportLevel('post', 1)); + } + + public function testExtractsHeadingTitle(): void + { + // Strings are keyed as containerId/widgetId/settingKey after flattening + $data = json_encode([[ + 'id' => 'container1', + 'elType' => 'e-flexbox', + 'settings' => [], + 'elements' => [[ + 'id' => 'heading1', + 'elType' => 'widget', + 'widgetType' => 'e-heading', + 'settings' => [ + 'title' => [ + '$$type' => 'html-v3', + 'value' => [ + 'content' => ['$$type' => 'string', 'value' => 'Hello World'], + 'children' => [], + ], + ], + ], + 'elements' => [], + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ]], + 'isInner' => false, + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ]]); + + $fields = $this->getHandler($this->makeProxy($data))->getContentFields($this->mockSubmission(), false); + + $this->assertArrayHasKey('container1/heading1/title', $fields); + $this->assertEquals('Hello World', $fields['container1/heading1/title']); + } + + public function testExtractsParagraphContent(): void + { + $data = json_encode([[ + 'id' => 'container1', + 'elType' => 'e-flexbox', + 'settings' => [], + 'elements' => [[ + 'id' => 'para1', + 'elType' => 'widget', + 'widgetType' => 'e-paragraph', + 'settings' => [ + 'paragraph' => [ + '$$type' => 'html-v3', + 'value' => [ + 'content' => ['$$type' => 'string', 'value' => 'Atomic paragraph'], + 'children' => [], + ], + ], + ], + 'elements' => [], + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ]], + 'isInner' => false, + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ]]); + + $fields = $this->getHandler($this->makeProxy($data))->getContentFields($this->mockSubmission(), false); + $this->assertEquals('Atomic paragraph', $fields['container1/para1/paragraph']); + } + + public function testExtractsButtonText(): void + { + $data = json_encode([[ + 'id' => 'container1', + 'elType' => 'e-flexbox', + 'settings' => [], + 'elements' => [[ + 'id' => 'btn1', + 'elType' => 'widget', + 'widgetType' => 'e-button', + 'settings' => [ + 'text' => ['$$type' => 'html-v3', 'value' => ['content' => ['$$type' => 'string', 'value' => 'Click me'], 'children' => []]], + ], + 'elements' => [], + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ]], + 'isInner' => false, + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ]]); + + $fields = $this->getHandler($this->makeProxy($data))->getContentFields($this->mockSubmission(), false); + $this->assertEquals('Click me', $fields['container1/btn1/text']); + } + + public function testExtractsFormInputPlaceholderButNotInternalFields(): void + { + $data = json_encode([[ + 'id' => 'form1', + 'elType' => 'e-form', + 'settings' => [], + 'elements' => [[ + 'id' => 'input1', + 'elType' => 'widget', + 'widgetType' => 'e-form-input', + 'settings' => [ + 'placeholder' => ['$$type' => 'string', 'value' => 'First name'], + 'type' => ['$$type' => 'string', 'value' => 'text'], + '_cssid' => ['$$type' => 'string', 'value' => 'e-form-first-name'], + ], + 'elements' => [], + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ]], + 'isInner' => false, + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ]]); + + $fields = $this->getHandler($this->makeProxy($data))->getContentFields($this->mockSubmission(), false); + $this->assertEquals('First name', $fields['form1/input1/placeholder']); + $this->assertArrayNotHasKey('form1/input1/type', $fields); + $this->assertArrayNotHasKey('form1/input1/_cssid', $fields); + } + + public function testExtractsImageAttachmentId(): void + { + $data = json_encode([[ + 'id' => 'container1', + 'elType' => 'e-flexbox', + 'settings' => [], + 'elements' => [[ + 'id' => 'img1', + 'elType' => 'widget', + 'widgetType' => 'e-image', + 'settings' => [ + 'image' => [ + '$$type' => 'image', + 'value' => [ + 'src' => [ + '$$type' => 'image-src', + 'value' => [ + 'id' => ['$$type' => 'image-attachment-id', 'value' => 23], + 'url' => null, + ], + ], + ], + ], + ], + 'elements' => [], + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ]], + 'isInner' => false, + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ]]); + + $related = $this->getHandler($this->makeProxy($data))->getRelatedContent('post', 1); + // getRelatedContentList() returns {contentType => [ids]} + $this->assertArrayHasKey(ContentTypeHelper::POST_TYPE_ATTACHMENT, $related); + $this->assertContains(23, $related[ContentTypeHelper::POST_TYPE_ATTACHMENT]); + } + + public function testSetContentFieldsWritesTranslationBackIntoTypedStructure(): void + { + $elementData = [[ + 'id' => 'container1', + 'elType' => 'e-flexbox', + 'settings' => [], + 'elements' => [[ + 'id' => 'heading1', + 'elType' => 'widget', + 'widgetType' => 'e-heading', + 'settings' => [ + 'title' => [ + '$$type' => 'html-v3', + 'value' => [ + 'content' => ['$$type' => 'string', 'value' => 'Original heading'], + 'children' => [], + ], + ], + ], + 'elements' => [], + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ]], + 'isInner' => false, + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ]]; + + $proxy = $this->makeProxy(json_encode($elementData)); + $original = ['meta' => [ExternalContentElementor4::META_FIELD_NAME => json_encode($elementData)]]; + + // Translation strings are keyed as {containerId: {widgetId: {settingKey: translatedValue}}} + $translation = [ + 'meta' => [ExternalContentElementor4::META_FIELD_NAME => json_encode($elementData)], + 'elementor' => [ + 'container1' => [ + 'heading1' => ['title' => 'Translated heading'], + ], + ], + ]; + + $submission = $this->createMock(SubmissionEntity::class); + $submission->method('getSourceBlogId')->willReturn(1); + $submission->method('getTargetBlogId')->willReturn(2); + $submission->method('getSourceId')->willReturn(10); + + $result = $this->getHandler($proxy)->setContentFields($original, $translation, $submission); + $resultData = json_decode($result['meta'][ExternalContentElementor4::META_FIELD_NAME], true); + + $headingSettings = $resultData[0]['elements'][0]['settings']; + $this->assertEquals('html-v3', $headingSettings['title']['$$type']); + $this->assertEquals('Translated heading', $headingSettings['title']['value']['content']['value']); + } + + public function testMixedElementorVersionsInSamePage(): void + { + $data = json_encode([ + [ + 'id' => 'newContainer', + 'elType' => 'e-flexbox', + 'settings' => [], + 'elements' => [[ + 'id' => 'newHeading', + 'elType' => 'widget', + 'widgetType' => 'e-heading', + 'settings' => [ + 'title' => ['$$type' => 'html-v3', 'value' => ['content' => ['$$type' => 'string', 'value' => 'New heading'], 'children' => []]], + ], + 'elements' => [], + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ]], + 'isInner' => false, + 'styles' => [], + 'interactions' => [], + 'editor_settings' => [], + 'version' => '0.0', + ], + [ + 'id' => 'oldContainer', + 'elType' => 'container', + 'settings' => [], + 'elements' => [[ + 'id' => 'blockquote1', + 'elType' => 'widget', + 'widgetType' => 'blockquote', + 'settings' => [ + 'blockquote_content' => 'Old style content', + 'author_name' => 'John Doe', + 'tweet_button_label' => 'Tweet', + ], + 'elements' => [], + ]], + 'isInner' => false, + ], + ]); + + $fields = $this->getHandler($this->makeProxy($data))->getContentFields($this->mockSubmission(), false); + $this->assertEquals('New heading', $fields['newContainer/newHeading/title']); + } + + public function testSourceJsonExtractsAllExpectedStrings(): void + { + $data = file_get_contents(__DIR__ . '/fixtures/wp-1000.json'); + $this->assertNotFalse($data); + + $proxy = $this->makeProxy($data); + $fields = $this->getHandler($proxy)->getContentFields($this->mockSubmission(), false); + + // e-heading: title + $this->assertContains('Atomic heading', $fields); + // e-paragraph: paragraph + $this->assertContains('Atomic paragraph', $fields); + // e-button: text + $this->assertContains('Text', $fields); + // e-form-label: text (various) + $this->assertContains('First name', $fields); + $this->assertContains('Last name', $fields); + $this->assertContains('Email', $fields); + // e-form-input: placeholder + $this->assertContains('your@mail.com', $fields); + // e-form-textarea: placeholder + $this->assertContains('Your message', $fields); + // e-paragraph inside form success/error messages + $this->assertContains("Great! We\u{2019}ve received your information.", $fields); + $this->assertContains("We couldn\u{2019}t process your submission. Please retry", $fields); + } + + public function testSourceJsonExtractsImageAttachment(): void + { + $data = file_get_contents(__DIR__ . '/fixtures/wp-1000.json'); + $this->assertNotFalse($data); + + $proxy = $this->makeProxy($data); + $related = $this->getHandler($proxy)->getRelatedContent('post', 1); + + $this->assertArrayHasKey(ContentTypeHelper::POST_TYPE_ATTACHMENT, $related); + // ID 23 is the e-image attachment ID; 24, 23, 21 are gallery IDs (old format) + $this->assertContains(23, $related[ContentTypeHelper::POST_TYPE_ATTACHMENT]); + } +} diff --git a/tests/Smartling/ContentTypes/icon.json b/tests/Smartling/ContentTypes/fixtures/icon.json similarity index 100% rename from tests/Smartling/ContentTypes/icon.json rename to tests/Smartling/ContentTypes/fixtures/icon.json diff --git a/tests/Smartling/ContentTypes/ucaddon_ue_listing_grid.json b/tests/Smartling/ContentTypes/fixtures/ucaddon_ue_listing_grid.json similarity index 100% rename from tests/Smartling/ContentTypes/ucaddon_ue_listing_grid.json rename to tests/Smartling/ContentTypes/fixtures/ucaddon_ue_listing_grid.json diff --git a/tests/Smartling/ContentTypes/fixtures/wp-1000.json b/tests/Smartling/ContentTypes/fixtures/wp-1000.json new file mode 100644 index 00000000..0b8f5661 --- /dev/null +++ b/tests/Smartling/ContentTypes/fixtures/wp-1000.json @@ -0,0 +1,612 @@ +[ + { + "id": "3a1c5ee", + "elType": "e-flexbox", + "settings": [], + "elements": [ + { + "id": "9ef487f", + "elType": "widget", + "settings": { + "title": { + "$$type": "html-v3", + "value": { + "content": { + "$$type": "string", + "value": "Atomic heading" + }, + "children": [] + } + } + }, + "elements": [], + "widgetType": "e-heading", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "f4176fc", + "elType": "widget", + "settings": { + "paragraph": { + "$$type": "html-v3", + "value": { + "content": { + "$$type": "string", + "value": "Atomic paragraph" + }, + "children": [] + } + }, + "link": { + "$$type": "link", + "value": [] + } + }, + "elements": [], + "widgetType": "e-paragraph", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + } + ], + "isInner": false, + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "e666e8e", + "elType": "e-flexbox", + "settings": [], + "elements": [ + { + "id": "b1c6de9", + "elType": "widget", + "settings": { + "image": { + "$$type": "image", + "value": { + "src": { + "$$type": "image-src", + "value": { + "id": { + "$$type": "image-attachment-id", + "value": 23 + }, + "url": null + } + } + } + }, + "link": { + "$$type": "link", + "value": [] + }, + "attributes": { + "$$type": "attributes", + "value": [ + { + "$$type": "key-value", + "value": { + "key": { + "$$type": "string", + "value": "" + }, + "value": { + "$$type": "string", + "value": "" + } + } + } + ] + } + }, + "elements": [], + "widgetType": "e-image", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + } + ], + "isInner": false, + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "292753b", + "elType": "e-flexbox", + "settings": [], + "elements": [ + { + "id": "48c6c32", + "elType": "widget", + "settings": { + "text": { + "$$type": "html-v3", + "value": { + "content": { + "$$type": "string", + "value": "Text" + }, + "children": [] + } + }, + "link": { + "$$type": "link", + "value": [] + } + }, + "elements": [], + "widgetType": "e-button", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + } + ], + "isInner": false, + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "04bc392", + "elType": "e-form", + "settings": [], + "elements": [ + { + "id": "e4bc94d", + "elType": "widget", + "settings": { + "text": { + "$$type": "html-v3", + "value": { + "content": { + "$$type": "string", + "value": "First name" + }, + "children": [] + } + }, + "input-id": { + "$$type": "string", + "value": "e-form-first-name" + } + }, + "elements": [], + "widgetType": "e-form-label", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "270be38", + "elType": "widget", + "settings": { + "placeholder": { + "$$type": "string", + "value": "First name" + }, + "type": { + "$$type": "string", + "value": "text" + }, + "_cssid": { + "$$type": "string", + "value": "e-form-first-name" + } + }, + "elements": [], + "widgetType": "e-form-input", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "acffba9", + "elType": "widget", + "settings": { + "text": { + "$$type": "html-v3", + "value": { + "content": { + "$$type": "string", + "value": "Last name" + }, + "children": [] + } + }, + "input-id": { + "$$type": "string", + "value": "e-form-last-name" + } + }, + "elements": [], + "widgetType": "e-form-label", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "0b28a8e", + "elType": "widget", + "settings": { + "placeholder": { + "$$type": "string", + "value": "Last name" + }, + "type": { + "$$type": "string", + "value": "text" + }, + "_cssid": { + "$$type": "string", + "value": "e-form-last-name" + } + }, + "elements": [], + "widgetType": "e-form-input", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "0665d2b", + "elType": "widget", + "settings": { + "text": { + "$$type": "html-v3", + "value": { + "content": { + "$$type": "string", + "value": "Email" + }, + "children": [] + } + }, + "input-id": { + "$$type": "string", + "value": "e-form-email" + } + }, + "elements": [], + "widgetType": "e-form-label", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "0f9061b", + "elType": "widget", + "settings": { + "placeholder": { + "$$type": "string", + "value": "your@mail.com" + }, + "type": { + "$$type": "string", + "value": "email" + }, + "_cssid": { + "$$type": "string", + "value": "e-form-email" + } + }, + "elements": [], + "widgetType": "e-form-input", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "2461de4", + "elType": "widget", + "settings": { + "text": { + "$$type": "html-v3", + "value": { + "content": { + "$$type": "string", + "value": "Message" + }, + "children": [] + } + }, + "input-id": { + "$$type": "string", + "value": "e-form-message" + } + }, + "elements": [], + "widgetType": "e-form-label", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "fbae7a5", + "elType": "widget", + "settings": { + "placeholder": { + "$$type": "string", + "value": "Your message" + }, + "rows": { + "$$type": "number", + "value": 4 + }, + "_cssid": { + "$$type": "string", + "value": "e-form-message" + } + }, + "elements": [], + "widgetType": "e-form-textarea", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "d761cbf", + "elType": "e-flexbox", + "settings": { + "classes": { + "$$type": "classes", + "value": [ + "e-form-checkbox-row" + ] + } + }, + "elements": [ + { + "id": "15ae248", + "elType": "widget", + "settings": { + "_cssid": { + "$$type": "string", + "value": "e-form-checkbox" + } + }, + "elements": [], + "widgetType": "e-form-checkbox", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "c861e12", + "elType": "widget", + "settings": { + "text": { + "$$type": "html-v3", + "value": { + "content": { + "$$type": "string", + "value": "Checkbox" + }, + "children": [] + } + }, + "input-id": { + "$$type": "string", + "value": "e-form-checkbox" + } + }, + "elements": [], + "widgetType": "e-form-label", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + } + ], + "isInner": false, + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "a4d4a36", + "elType": "widget", + "settings": [], + "elements": [], + "widgetType": "e-form-submit-button", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "940c446", + "elType": "e-form-success-message", + "settings": { + "attributes": { + "$$type": "attributes", + "value": [ + { + "$$type": "key-value", + "value": [] + } + ] + } + }, + "elements": [ + { + "id": "143f87f", + "elType": "widget", + "settings": { + "paragraph": { + "$$type": "html-v3", + "value": { + "content": { + "$$type": "string", + "value": "Great! We\u2019ve received your information." + }, + "children": [] + } + } + }, + "elements": [], + "widgetType": "e-paragraph", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + } + ], + "isInner": false, + "isLocked": true, + "styles": [], + "interactions": [], + "editor_settings": { + "title": "Success message" + }, + "version": "0.0" + }, + { + "id": "687e6c7", + "elType": "e-form-error-message", + "settings": { + "attributes": { + "$$type": "attributes", + "value": [ + { + "$$type": "key-value", + "value": [] + } + ] + } + }, + "elements": [ + { + "id": "4c3ef3f", + "elType": "widget", + "settings": { + "paragraph": { + "$$type": "html-v3", + "value": { + "content": { + "$$type": "string", + "value": "We couldn\u2019t process your submission. Please retry" + }, + "children": [] + } + } + }, + "elements": [], + "widgetType": "e-paragraph", + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + } + ], + "isInner": false, + "isLocked": true, + "styles": [], + "interactions": [], + "editor_settings": { + "title": "Error message" + }, + "version": "0.0" + } + ], + "isInner": false, + "styles": [], + "interactions": [], + "editor_settings": [], + "version": "0.0" + }, + { + "id": "c622ee0", + "elType": "container", + "settings": [], + "elements": [ + { + "id": "46f859d", + "elType": "widget", + "settings": { + "template_id": 2120 + }, + "elements": [], + "widgetType": "loop-carousel" + } + ], + "isInner": false + }, + { + "id": "a1a362d", + "elType": "container", + "settings": [], + "elements": [ + { + "id": "1c323bf", + "elType": "widget", + "settings": { + "gallery": [ + { + "id": 24, + "url": "http:\/\/localhost\/wp-content\/uploads\/2026\/03\/467-536x354-1.jpg" + }, + { + "id": 23, + "url": "http:\/\/localhost\/wp-content\/uploads\/2026\/03\/6-536x354-1.jpg" + }, + { + "id": 21, + "url": "http:\/\/localhost\/wp-content\/uploads\/2026\/03\/522-536x354-1.jpg" + } + ], + "galleries": [ + { + "gallery_title": "New Gallery", + "_id": "251389a" + } + ], + "show_all_galleries_label": "All" + }, + "elements": [], + "widgetType": "gallery" + } + ], + "isInner": false + }, + { + "id": "781aef5", + "elType": "container", + "settings": [], + "elements": [ + { + "id": "a67ac4d", + "elType": "widget", + "settings": { + "blockquote_content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.Lorem ipsum dolor sit amet consectetur adipiscing elit dolor", + "author_name": "John Doe", + "tweet_button_label": "Tweet" + }, + "elements": [], + "widgetType": "blockquote" + } + ], + "isInner": false + } +] \ No newline at end of file diff --git a/tests/Smartling/ContentTypes/wp-834.json b/tests/Smartling/ContentTypes/fixtures/wp-834.json similarity index 100% rename from tests/Smartling/ContentTypes/wp-834.json rename to tests/Smartling/ContentTypes/fixtures/wp-834.json diff --git a/tests/Smartling/ContentTypes/wp-836.json b/tests/Smartling/ContentTypes/fixtures/wp-836.json similarity index 100% rename from tests/Smartling/ContentTypes/wp-836.json rename to tests/Smartling/ContentTypes/fixtures/wp-836.json diff --git a/tests/Smartling/ContentTypes/wp-944.json b/tests/Smartling/ContentTypes/fixtures/wp-944.json similarity index 100% rename from tests/Smartling/ContentTypes/wp-944.json rename to tests/Smartling/ContentTypes/fixtures/wp-944.json diff --git a/tests/Smartling/ContentTypes/wp-952.json b/tests/Smartling/ContentTypes/fixtures/wp-952.json similarity index 100% rename from tests/Smartling/ContentTypes/wp-952.json rename to tests/Smartling/ContentTypes/fixtures/wp-952.json diff --git a/tests/Smartling/ContentTypes/wp-975.json b/tests/Smartling/ContentTypes/fixtures/wp-975.json similarity index 100% rename from tests/Smartling/ContentTypes/wp-975.json rename to tests/Smartling/ContentTypes/fixtures/wp-975.json