From 2f9cc4227cea9cb450b483838780f80a8afa8130 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Mon, 11 May 2026 10:57:41 +0200 Subject: [PATCH 1/6] add elementor 4 support (WP-1000) Co-Authored-By: Claude Sonnet 4.6 --- .../ContentTypes/Elementor/Element.php | 5 +- .../Elementor/ElementAbstract.php | 12 +- .../Elementor/ElementAbstract4.php | 72 ++++ ...ElementFactory.php => ElementFactory3.php} | 14 +- .../Elementor/ElementFactory4.php | 13 + .../Elementor/Elements/Gallery.php | 4 +- .../Elementor/Elements/IconList.php | 4 +- .../Elementor/Elements/ImageGallery.php | 2 - .../Elementor/Elements/MegaMenu.php | 4 +- .../Elementor/Elements/NestedAccordion.php | 4 +- .../Elementor/Elements/Reviews.php | 4 +- .../Elementor/Elements/SocialIcons.php | 2 - .../ContentTypes/Elementor/Elements/Tabs.php | 4 +- .../Elementor/Elements/Unknown.php | 2 +- .../Elementor/Elements4/EButton.php | 35 ++ .../Elementor/Elements4/EFormInput.php | 35 ++ .../Elementor/Elements4/EFormLabel.php | 35 ++ .../Elementor/Elements4/EFormTextarea.php | 35 ++ .../Elementor/Elements4/EHeading.php | 35 ++ .../Elementor/Elements4/EImage.php | 40 ++ .../Elementor/Elements4/EParagraph.php | 35 ++ .../ExternalContentElementorInterface.php | 18 + ...ntor.php => ExternalContentElementor3.php} | 8 +- .../ExternalContentElementor4.php | 276 +++++++++++++ inc/config/services.yml | 29 +- .../Elementor/ElementAbstractTest.php | 3 +- .../ContentTypes/Elementor/GalleryTest.php | 5 +- .../Elementor/ImageGalleryTest.php | 3 +- .../Elementor/LoopCarouselTest.php | 5 +- .../ContentTypes/Elementor/MegaMenuTest.php | 9 +- .../Elementor/NestedAccordionTest.php | 7 +- .../ContentTypes/Elementor/PostsTest.php | 3 +- .../ContentTypes/Elementor/ShortcodeTest.php | 3 +- ....php => ExternalContentElementor3Test.php} | 16 +- .../ExternalContentElementor4Test.php | 390 ++++++++++++++++++ 35 files changed, 1102 insertions(+), 69 deletions(-) create mode 100644 inc/Smartling/ContentTypes/Elementor/ElementAbstract4.php rename inc/Smartling/ContentTypes/Elementor/{ElementFactory.php => ElementFactory3.php} (73%) create mode 100644 inc/Smartling/ContentTypes/Elementor/ElementFactory4.php create mode 100644 inc/Smartling/ContentTypes/Elementor/Elements4/EButton.php create mode 100644 inc/Smartling/ContentTypes/Elementor/Elements4/EFormInput.php create mode 100644 inc/Smartling/ContentTypes/Elementor/Elements4/EFormLabel.php create mode 100644 inc/Smartling/ContentTypes/Elementor/Elements4/EFormTextarea.php create mode 100644 inc/Smartling/ContentTypes/Elementor/Elements4/EHeading.php create mode 100644 inc/Smartling/ContentTypes/Elementor/Elements4/EImage.php create mode 100644 inc/Smartling/ContentTypes/Elementor/Elements4/EParagraph.php create mode 100644 inc/Smartling/ContentTypes/Elementor/ExternalContentElementorInterface.php rename inc/Smartling/ContentTypes/{ExternalContentElementor.php => ExternalContentElementor3.php} (97%) create mode 100644 inc/Smartling/ContentTypes/ExternalContentElementor4.php rename tests/Smartling/ContentTypes/{ExternalContentElementorTest.php => ExternalContentElementor3Test.php} (98%) create mode 100644 tests/Smartling/ContentTypes/ExternalContentElementor4Test.php 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..b1eb5344 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,10 +124,11 @@ protected function getTranslatableStringsByKeys(array $keys, Element $element = public function setRelations( Content $content, - ExternalContentElementor $externalContentElementor, + ExternalContentElementorInterface $externalContentElementor, string $path, SubmissionEntity $submission, ): static { + $this->getLogger()->debug("Set relations for contentType={$content->getType()} contentId={$content->getId()} path=$path"); $arrayHelper = new ArrayHelper(); $result = clone $this; @@ -136,8 +136,11 @@ public function setRelations( if ($content->getType() === ContentTypeHelper::CONTENT_TYPE_POST) { $contentType = $wpProxy->get_post_type($content->getId()); } elseif ($content->getType() === ContentTypeHelper::CONTENT_TYPE_TAXONOMY) { + $this->getLogger()->debug("Getting content type for taxonomy id={$content->getId()}"); $term = $wpProxy->getTerm($content->getId()); + $this->getLogger()->debug(json_encode($term)); $contentType = (is_array($term) && isset($term['taxonomy'])) ? $term['taxonomy'] : $content->getType(); + $this->getLogger()->debug("Got content type for taxonomy id={$content->getId()}: $contentType"); } else { $contentType = $content->getType(); } @@ -152,6 +155,7 @@ public function setRelations( $submission->getTargetBlogId(), $contentType, ); + $this->getLogger()->debug("Got targetId for contentId={$content->getId()}: $targetId"); if ($targetId !== null) { if (is_string($this->getSettingByKey($path, $this->raw ?? []))) { $targetId = (string)$targetId; @@ -169,7 +173,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/ElementFactory3.php similarity index 73% rename from inc/Smartling/ContentTypes/Elementor/ElementFactory.php rename to inc/Smartling/ContentTypes/Elementor/ElementFactory3.php index 30c9cde0..42781614 100644 --- a/inc/Smartling/ContentTypes/Elementor/ElementFactory.php +++ b/inc/Smartling/ContentTypes/Elementor/ElementFactory3.php @@ -2,20 +2,26 @@ namespace Smartling\ContentTypes\Elementor; -class ElementFactory { +class ElementFactory3 { public const UNKNOWN_ELEMENT = 'unknown'; private const ELEMENTS = 'Elements'; /** * @var Element[] */ - private array $elements = []; + protected array $elements = []; public function __construct() { - foreach (new \DirectoryIterator(__DIR__ . DIRECTORY_SEPARATOR . self::ELEMENTS) as $fileInfo) { + $this->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__, self::ELEMENTS, $className])); + $element = new (implode('\\', [__NAMESPACE__, $namespace, $className])); if ($element instanceof Element) { $this->elements[$element->getType()] = $element; } 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..c449259c 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; 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, diff --git a/inc/Smartling/ContentTypes/ExternalContentElementor4.php b/inc/Smartling/ContentTypes/ExternalContentElementor4.php new file mode 100644 index 00000000..2f85a360 --- /dev/null +++ b/inc/Smartling/ContentTypes/ExternalContentElementor4.php @@ -0,0 +1,276 @@ + [ + 'post_content', + ], + 'meta' => [ + self::META_FIELD_NAME, + '_elementor_element_cache', + ] + ]; + + public function __construct( + private ContentTypeHelper $contentTypeHelper, + private ElementFactory4 $elementFactory, + private FieldsFilterHelper $fieldsFilterHelper, + PluginHelper $pluginHelper, + private SiteHelper $siteHelper, + SubmissionManager $submissionManager, + private UserHelper $userHelper, + WordpressFunctionProxyHelper $wpProxy, + private LinkProcessor $linkProcessor, + ) + { + $wpProxy->add_action(ExportedAPI::ACTION_AFTER_TARGET_METADATA_WRITTEN, [$this, 'afterMetaWritten']); + parent::__construct($pluginHelper, $submissionManager, $wpProxy); + } + + public function getWpProxy(): WordpressFunctionProxyHelper + { + return $this->wpProxy; + } + + public function afterMetaWritten(SubmissionEntity $submission): void + { + if ($submission->getTargetId() === 0) { + $this->getLogger()->debug('Processing Elementor after meta written hook skipped, targetId=0'); + return; + } + $this->siteHelper->withBlog($submission->getTargetBlogId(), function () use ($submission) { + $supportLevel = $this->getSupportLevel($submission->getContentType(), $submission->getTargetId()); + $documentsManager = $this->getDocumentsManager(); + if ($supportLevel !== Pluggable::NOT_SUPPORTED && $documentsManager !== null) { + $this->getLogger()->debug(sprintf('Processing Elementor after content written hook, contentType=%s, sourceBlogId=%d, sourceId=%d, submissionId=%d, targetBlogId=%d, targetId=%d, supportLevel=%s', $submission->getContentType(), $submission->getSourceBlogId(), $submission->getSourceId(), $submission->getId(), $submission->getTargetBlogId(), $submission->getTargetId(), $supportLevel)); + $data = $this->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, + true, + 512, + JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE), + 'status' => $this->wpProxy->get_post($submission->getTargetId())->post_status, + ]); + } catch (\Throwable $e) { + $this->getLogger()->notice(sprintf("Unable to do Elementor save actions for contentType=%s, submissionId=%d, targetBlogId=%d, targetId=%d: %s (%s)", $submission->getContentType(), $submission->getId(), $submission->getTargetBlogId(), $submission->getTargetId(), $e->getMessage(), $e->getTraceAsString())); + } + }); + $this->wpProxy->updatePostMeta($submission->getTargetId(), self::META_FIELD_NAME, addslashes($data)); + } + }); + $this->getLogger()->info("Done processing Elementor after content written hook"); + } + + public function removeUntranslatableFieldsForUpload(array $source, SubmissionEntity $submission): array + { + if ($this->getSupportLevel($submission->getContentType(), $submission->getSourceId()) !== Pluggable::NOT_SUPPORTED) { + $this->getLogger()->info('Detected elementor data, removing post content and elementor related meta fields'); + foreach (array_merge_recursive( + ['meta' => array_merge($this->copyFields, [self::META_CONDITIONS_NAME])], + $this->removeOnUploadFields, + ) as $key => $value) { + if (array_key_exists($key, $source)) { + foreach ($value as $field) { + unset($source[$key][$field]); + } + } + } + } + + return $source; + } + + public function getSupportLevel(string $contentType, ?int $contentId = null): string + { + if ($this->contentTypeHelper->isPost($contentType) && $this->getDataFromPostMeta($contentId) !== '') { + return parent::getSupportLevel($contentType, $contentId); + } + return Pluggable::NOT_SUPPORTED; + } + + private function getData(array $data): ExternalData + { + $related = new RelatedContentInfo(); + $strings = []; + + foreach ($data as $array) { + $element = $this->elementFactory->fromArray($array); + $related = $related->merge($element->getRelated()); + $strings[] = $element->getTranslatableStrings(); + } + + return new ExternalData(strings: $strings, relatedContentInfo: $related); + } + + public function getContentFields(SubmissionEntity $submission, bool $raw): array + { + return $this->fieldsFilterHelper->flattenArray( + (new ArrayHelper())->add( + ...$this->getData($this->readMeta($submission->getSourceId()))->getStrings() + ) + ); + } + + private function readMeta(int $id): array + { + return json_decode($this->getDataFromPostMeta($id), true, 512, JSON_THROW_ON_ERROR); + } + + public function getMaxVersion(): string + { + return '4'; + } + + public function getMinVersion(): string + { + return '4'; + } + + public function getPluginId(): string + { + return 'elementor'; + } + + public function getPluginPaths(): array + { + return ['elementor/elementor.php']; + } + + public function getRelatedContent(string $contentType, int $contentId): array + { + return $this->getData($this->readMeta($contentId))->getRelatedContentInfo()->getRelatedContentList(); + } + + private function mergeElementorData(array $original, array $strings, SubmissionEntity $submission): array + { + $result = []; + foreach ($original as $array) { + $element = $this->elementFactory->fromArray($array); + $result[] = $element->setTargetContent( + $this, + $this->getData($original)->getRelatedContentInfo(), + $strings, + $submission, + )->toArray(); + } + + return $result; + } + + public function setContentFields(array $original, array $translation, SubmissionEntity $submission): array + { + if (array_key_exists('meta', $original)) { + foreach ($this->copyFields as $field) { + if (array_key_exists($field, $original['meta'])) { + $value = $original['meta'][$field]; + $translation['meta'][$field] = is_string($value) ? $this->wpProxy->maybe_unserialize($value) : $value; + } + } + if (array_key_exists(self::META_CONDITIONS_NAME, $original['meta'])) { + $value = $original['meta'][self::META_CONDITIONS_NAME]; + if (is_string($value)) { + $translation['meta'][self::META_CONDITIONS_NAME] = $this->rebuildConditionsField($value, $submission); + } + } + } + $translation['meta'][self::META_FIELD_NAME] = json_encode($this->mergeElementorData( + json_decode($original['meta'][self::META_FIELD_NAME] ?? '[]', true, 512, JSON_THROW_ON_ERROR), + $translation[$this->getPluginId()] ?? [], + $submission, + ), JSON_THROW_ON_ERROR); + unset($translation[$this->getPluginId()]); + return $translation; + } + + private function rebuildConditionsField(string $conditions, SubmissionEntity $submission): string + { + $value = $this->wpProxy->maybe_unserialize($conditions); + if (is_array($value)) { + foreach ($value as $index => $condition) { + $matches = []; + preg_match('~(\d+)$~', $condition, $matches); + $id = $matches[0] ?? null; + if ($id === null) { + continue; + } + $related = $this->submissionManager->findOne([ + SubmissionEntity::FIELD_SOURCE_BLOG_ID => $submission->getSourceBlogId(), + SubmissionEntity::FIELD_SOURCE_ID => $id, + SubmissionEntity::FIELD_TARGET_BLOG_ID => $submission->getTargetBlogId(), + ]); + if ($related !== null) { + $value[$index] = str_replace($id, $related->getTargetId(), $condition); + } + } + return serialize($value); + } + return $value; + } + + private function getDocumentsManager(): ?Documents_Manager + { + if (class_exists(Plugin::class)) { + return (new DocumentsManagerWrapper(Plugin::elementor()->documents))->getManagerWithoutDocuments(); + } + $documentsManagerPath = WP_PLUGIN_DIR . '/elementor/core/documents-manager.php'; + if (file_exists($documentsManagerPath)) { + try { + require_once $documentsManagerPath; + + $manager = new Documents_Manager(); + do_action('elementor/documents/register', $manager); + return $manager; + } catch (\Throwable) { + // No documents manager available + } + } + if (class_exists(Documents_Manager::class)) { + return new Documents_Manager(); + } + + return null; + } +} 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/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 98% rename from tests/Smartling/ContentTypes/ExternalContentElementorTest.php rename to tests/Smartling/ContentTypes/ExternalContentElementor3Test.php index e091064f..1cf0423a 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); @@ -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..75dd88be --- /dev/null +++ b/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php @@ -0,0 +1,390 @@ +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 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__ . '/../../../issues/WP-1000/source.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__ . '/../../../issues/WP-1000/source.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]); + } +} From 830b5855d947610384c5a0bdd9e88aa23f3fd0e4 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Mon, 11 May 2026 11:34:21 +0200 Subject: [PATCH 2/6] remove debug, extract abstract class, add test fixture (WP-1000) Co-Authored-By: Claude Sonnet 4.6 --- .../Elementor/ElementAbstract.php | 5 - .../Elementor/Elements/Unknown.php | 2 +- .../ExternalContentElementor3.php | 261 +------- .../ExternalContentElementor4.php | 263 +------- .../ExternalContentElementorAbstract.php | 267 ++++++++ .../ExternalContentElementor4Test.php | 4 +- tests/Smartling/ContentTypes/wp-1000.json | 612 ++++++++++++++++++ 7 files changed, 901 insertions(+), 513 deletions(-) create mode 100644 inc/Smartling/ContentTypes/ExternalContentElementorAbstract.php create mode 100644 tests/Smartling/ContentTypes/wp-1000.json diff --git a/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php b/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php index b1eb5344..2d3a6920 100644 --- a/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php +++ b/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php @@ -128,7 +128,6 @@ public function setRelations( string $path, SubmissionEntity $submission, ): static { - $this->getLogger()->debug("Set relations for contentType={$content->getType()} contentId={$content->getId()} path=$path"); $arrayHelper = new ArrayHelper(); $result = clone $this; @@ -136,11 +135,8 @@ public function setRelations( if ($content->getType() === ContentTypeHelper::CONTENT_TYPE_POST) { $contentType = $wpProxy->get_post_type($content->getId()); } elseif ($content->getType() === ContentTypeHelper::CONTENT_TYPE_TAXONOMY) { - $this->getLogger()->debug("Getting content type for taxonomy id={$content->getId()}"); $term = $wpProxy->getTerm($content->getId()); - $this->getLogger()->debug(json_encode($term)); $contentType = (is_array($term) && isset($term['taxonomy'])) ? $term['taxonomy'] : $content->getType(); - $this->getLogger()->debug("Got content type for taxonomy id={$content->getId()}: $contentType"); } else { $contentType = $content->getType(); } @@ -155,7 +151,6 @@ public function setRelations( $submission->getTargetBlogId(), $contentType, ); - $this->getLogger()->debug("Got targetId for contentId={$content->getId()}: $targetId"); if ($targetId !== null) { if (is_string($this->getSettingByKey($path, $this->raw ?? []))) { $targetId = (string)$targetId; diff --git a/inc/Smartling/ContentTypes/Elementor/Elements/Unknown.php b/inc/Smartling/ContentTypes/Elementor/Elements/Unknown.php index c449259c..bfa7216b 100644 --- a/inc/Smartling/ContentTypes/Elementor/Elements/Unknown.php +++ b/inc/Smartling/ContentTypes/Elementor/Elements/Unknown.php @@ -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/ExternalContentElementor3.php b/inc/Smartling/ContentTypes/ExternalContentElementor3.php index 1fc7d23b..0af7b334 100644 --- a/inc/Smartling/ContentTypes/ExternalContentElementor3.php +++ b/inc/Smartling/ContentTypes/ExternalContentElementor3.php @@ -2,162 +2,8 @@ namespace Smartling\ContentTypes; -use Elementor\Core\Documents_Manager; -use ElementorPro\Plugin; -use Smartling\Base\ExportedAPI; -use Smartling\ContentTypes\Elementor\DocumentsManagerWrapper; -use Smartling\ContentTypes\Elementor\ElementFactory3; -use Smartling\ContentTypes\Elementor\ExternalContentElementorInterface; -use Smartling\Extensions\Pluggable; -use Smartling\Helpers\ArrayHelper; -use Smartling\Helpers\FieldsFilterHelper; -use Smartling\Helpers\LinkProcessor; -use Smartling\Helpers\LoggerSafeTrait; -use Smartling\Helpers\PluginHelper; -use Smartling\Helpers\SiteHelper; -use Smartling\Helpers\UserHelper; -use Smartling\Helpers\WordpressFunctionProxyHelper; -use Smartling\Models\ExternalData; -use Smartling\Models\RelatedContentInfo; -use Smartling\Submissions\SubmissionEntity; -use Smartling\Submissions\SubmissionManager; - -class ExternalContentElementor3 extends ExternalContentAbstract implements ContentTypeModifyingInterface, ExternalContentElementorInterface +class ExternalContentElementor3 extends ExternalContentElementorAbstract { - use LoggerSafeTrait; - - public const META_FIELD_NAME = '_elementor_data'; - private const META_CONDITIONS_NAME = '_elementor_conditions'; - - private array $copyFields = [ - '_elementor_controls_usage', - '_elementor_css', - '_elementor_edit_mode', - '_elementor_page_assets', - '_elementor_page_settings', - '_elementor_pro_version', - '_elementor_template_type', - '_elementor_version', - ]; - - private array $removeOnUploadFields = [ - 'entity' => [ - 'post_content', - ], - 'meta' => [ - self::META_FIELD_NAME, - '_elementor_element_cache', - ] - ]; - - public function __construct( - private ContentTypeHelper $contentTypeHelper, - private ElementFactory3 $elementFactory, - private FieldsFilterHelper $fieldsFilterHelper, - PluginHelper $pluginHelper, - private SiteHelper $siteHelper, - SubmissionManager $submissionManager, - private UserHelper $userHelper, - WordpressFunctionProxyHelper $wpProxy, - private LinkProcessor $linkProcessor, - ) - { - $wpProxy->add_action(ExportedAPI::ACTION_AFTER_TARGET_METADATA_WRITTEN, [$this, 'afterMetaWritten']); - parent::__construct($pluginHelper, $submissionManager, $wpProxy); - } - - public function getWpProxy(): WordpressFunctionProxyHelper - { - return $this->wpProxy; - } - - public function afterMetaWritten(SubmissionEntity $submission): void - { - if ($submission->getTargetId() === 0) { - $this->getLogger()->debug('Processing Elementor after meta written hook skipped, targetId=0'); - return; - } - $this->siteHelper->withBlog($submission->getTargetBlogId(), function () use ($submission) { - $supportLevel = $this->getSupportLevel($submission->getContentType(), $submission->getTargetId()); - $documentsManager = $this->getDocumentsManager(); - if ($supportLevel !== Pluggable::NOT_SUPPORTED && $documentsManager !== null) { - $this->getLogger()->debug(sprintf('Processing Elementor after content written hook, contentType=%s, sourceBlogId=%d, sourceId=%d, submissionId=%d, targetBlogId=%d, targetId=%d, supportLevel=%s', $submission->getContentType(), $submission->getSourceBlogId(), $submission->getSourceId(), $submission->getId(), $submission->getTargetBlogId(), $submission->getTargetId(), $supportLevel)); - $data = $this->getDataFromPostMeta($submission->getTargetId()); - $this->userHelper->asAdministratorOrEditor(function () use ($data, $documentsManager, $submission) { - try { - $documentsManager->ajax_save([ - 'editor_post_id' => $submission->getTargetId(), - 'elements' => json_decode($data, - true, - 512, - JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE), - 'status' => $this->wpProxy->get_post($submission->getTargetId())->post_status, - ]); - } catch (\Throwable $e) { - $this->getLogger()->notice(sprintf("Unable to do Elementor save actions for contentType=%s, submissionId=%d, targetBlogId=%d, targetId=%d: %s (%s)", $submission->getContentType(), $submission->getId(), $submission->getTargetBlogId(), $submission->getTargetId(), $e->getMessage(), $e->getTraceAsString())); - } - }); - $this->wpProxy->updatePostMeta($submission->getTargetId(), self::META_FIELD_NAME, addslashes($data)); - } - }); - $this->getLogger()->info("Done processing Elementor after content written hook"); - } - - public function removeUntranslatableFieldsForUpload(array $source, SubmissionEntity $submission): array - { - if ($this->getSupportLevel($submission->getContentType(), $submission->getSourceId()) !== Pluggable::NOT_SUPPORTED) { - $this->getLogger()->info('Detected elementor data, removing post content and elementor related meta fields'); - foreach (array_merge_recursive( - ['meta' => array_merge($this->copyFields, [self::META_CONDITIONS_NAME])], - $this->removeOnUploadFields, - ) as $key => $value) { - if (array_key_exists($key, $source)) { - foreach ($value as $field) { - unset($source[$key][$field]); - } - } - } - } - - return $source; - } - - public function getSupportLevel(string $contentType, ?int $contentId = null): string - { - if ($this->contentTypeHelper->isPost($contentType) && $this->getDataFromPostMeta($contentId) !== '') { - return parent::getSupportLevel($contentType, $contentId); - } - return Pluggable::NOT_SUPPORTED; - } - - private function getData(array $data): ExternalData - { - $related = new RelatedContentInfo(); - $strings = []; - - foreach ($data as $array) { - $element = $this->elementFactory->fromArray($array); - $related = $related->merge($element->getRelated()); - $strings[] = $element->getTranslatableStrings(); - } - - return new ExternalData(strings: $strings, relatedContentInfo: $related); - } - - public function getContentFields(SubmissionEntity $submission, bool $raw): array - { - return $this->fieldsFilterHelper->flattenArray( - (new ArrayHelper())->add( - ...$this->getData($this->readMeta($submission->getSourceId()))->getStrings() - ) - ); - } - - private function readMeta(int $id): array - { - return json_decode($this->getDataFromPostMeta($id), true, 512, JSON_THROW_ON_ERROR); - } - public function getMaxVersion(): string { return '3'; @@ -167,109 +13,4 @@ public function getMinVersion(): string { return '3'; } - - public function getPluginId(): string - { - return 'elementor'; - } - - public function getPluginPaths(): array - { - return ['elementor/elementor.php']; - } - - public function getRelatedContent(string $contentType, int $contentId): array - { - return $this->getData($this->readMeta($contentId))->getRelatedContentInfo()->getRelatedContentList(); - } - - private function mergeElementorData(array $original, array $strings, SubmissionEntity $submission): array - { - $result = []; - foreach ($original as $array) { - $element = $this->elementFactory->fromArray($array); - $result[] = $element->setTargetContent( - $this, - $this->getData($original)->getRelatedContentInfo(), - $strings, - $submission, - )->toArray(); - } - - return $result; - } - - public function setContentFields(array $original, array $translation, SubmissionEntity $submission): array - { - if (array_key_exists('meta', $original)) { - foreach ($this->copyFields as $field) { - if (array_key_exists($field, $original['meta'])) { - $value = $original['meta'][$field]; - $translation['meta'][$field] = is_string($value) ? $this->wpProxy->maybe_unserialize($value) : $value; - } - } - if (array_key_exists(self::META_CONDITIONS_NAME, $original['meta'])) { - $value = $original['meta'][self::META_CONDITIONS_NAME]; - if (is_string($value)) { - $translation['meta'][self::META_CONDITIONS_NAME] = $this->rebuildConditionsField($value, $submission); - } - } - } - $translation['meta'][self::META_FIELD_NAME] = json_encode($this->mergeElementorData( - json_decode($original['meta'][self::META_FIELD_NAME] ?? '[]', true, 512, JSON_THROW_ON_ERROR), - $translation[$this->getPluginId()] ?? [], - $submission, - ), JSON_THROW_ON_ERROR); - unset($translation[$this->getPluginId()]); - return $translation; - } - - private function rebuildConditionsField(string $conditions, SubmissionEntity $submission): string - { - $value = $this->wpProxy->maybe_unserialize($conditions); - if (is_array($value)) { - foreach ($value as $index => $condition) { - $matches = []; - preg_match('~(\d+)$~', $condition, $matches); - $id = $matches[0] ?? null; - if ($id === null) { - continue; - } - $related = $this->submissionManager->findOne([ - SubmissionEntity::FIELD_SOURCE_BLOG_ID => $submission->getSourceBlogId(), - SubmissionEntity::FIELD_SOURCE_ID => $id, - SubmissionEntity::FIELD_TARGET_BLOG_ID => $submission->getTargetBlogId(), - ]); - if ($related !== null) { - $value[$index] = str_replace($id, $related->getTargetId(), $condition); - } - } - return serialize($value); - } - return $value; - } - - private function getDocumentsManager(): ?Documents_Manager - { - if (class_exists(Plugin::class)) { - return (new DocumentsManagerWrapper(Plugin::elementor()->documents))->getManagerWithoutDocuments(); - } - $documentsManagerPath = WP_PLUGIN_DIR . '/elementor/core/documents-manager.php'; - if (file_exists($documentsManagerPath)) { - try { - require_once $documentsManagerPath; - - $manager = new Documents_Manager(); - do_action('elementor/documents/register', $manager); - return $manager; - } catch (\Throwable) { - // No documents manager available - } - } - if (class_exists(Documents_Manager::class)) { - return new Documents_Manager(); - } - - return null; - } } diff --git a/inc/Smartling/ContentTypes/ExternalContentElementor4.php b/inc/Smartling/ContentTypes/ExternalContentElementor4.php index 2f85a360..7f976992 100644 --- a/inc/Smartling/ContentTypes/ExternalContentElementor4.php +++ b/inc/Smartling/ContentTypes/ExternalContentElementor4.php @@ -2,163 +2,41 @@ namespace Smartling\ContentTypes; -use Elementor\Core\Documents_Manager; -use ElementorPro\Plugin; -use Smartling\Base\ExportedAPI; -use Smartling\ContentTypes\Elementor\DocumentsManagerWrapper; use Smartling\ContentTypes\Elementor\ElementFactory4; -use Smartling\ContentTypes\Elementor\ExternalContentElementorInterface; -use Smartling\Extensions\Pluggable; -use Smartling\Helpers\ArrayHelper; use Smartling\Helpers\FieldsFilterHelper; use Smartling\Helpers\LinkProcessor; -use Smartling\Helpers\LoggerSafeTrait; use Smartling\Helpers\PluginHelper; use Smartling\Helpers\SiteHelper; use Smartling\Helpers\UserHelper; use Smartling\Helpers\WordpressFunctionProxyHelper; -use Smartling\Models\ExternalData; -use Smartling\Models\RelatedContentInfo; -use Smartling\Submissions\SubmissionEntity; use Smartling\Submissions\SubmissionManager; -class ExternalContentElementor4 extends ExternalContentAbstract implements ContentTypeModifyingInterface, ExternalContentElementorInterface +class ExternalContentElementor4 extends ExternalContentElementorAbstract { - use LoggerSafeTrait; - - public const META_FIELD_NAME = '_elementor_data'; - private const META_CONDITIONS_NAME = '_elementor_conditions'; - - private array $copyFields = [ - '_elementor_controls_usage', - '_elementor_css', - '_elementor_edit_mode', - '_elementor_page_assets', - '_elementor_page_settings', - '_elementor_pro_version', - '_elementor_template_type', - '_elementor_version', - ]; - - private array $removeOnUploadFields = [ - 'entity' => [ - 'post_content', - ], - 'meta' => [ - self::META_FIELD_NAME, - '_elementor_element_cache', - ] - ]; - public function __construct( - private ContentTypeHelper $contentTypeHelper, - private ElementFactory4 $elementFactory, - private FieldsFilterHelper $fieldsFilterHelper, + ContentTypeHelper $contentTypeHelper, + ElementFactory4 $elementFactory, + FieldsFilterHelper $fieldsFilterHelper, PluginHelper $pluginHelper, - private SiteHelper $siteHelper, + SiteHelper $siteHelper, SubmissionManager $submissionManager, - private UserHelper $userHelper, + UserHelper $userHelper, WordpressFunctionProxyHelper $wpProxy, - private LinkProcessor $linkProcessor, - ) - { - $wpProxy->add_action(ExportedAPI::ACTION_AFTER_TARGET_METADATA_WRITTEN, [$this, 'afterMetaWritten']); - parent::__construct($pluginHelper, $submissionManager, $wpProxy); - } - - public function getWpProxy(): WordpressFunctionProxyHelper - { - return $this->wpProxy; - } - - public function afterMetaWritten(SubmissionEntity $submission): void - { - if ($submission->getTargetId() === 0) { - $this->getLogger()->debug('Processing Elementor after meta written hook skipped, targetId=0'); - return; - } - $this->siteHelper->withBlog($submission->getTargetBlogId(), function () use ($submission) { - $supportLevel = $this->getSupportLevel($submission->getContentType(), $submission->getTargetId()); - $documentsManager = $this->getDocumentsManager(); - if ($supportLevel !== Pluggable::NOT_SUPPORTED && $documentsManager !== null) { - $this->getLogger()->debug(sprintf('Processing Elementor after content written hook, contentType=%s, sourceBlogId=%d, sourceId=%d, submissionId=%d, targetBlogId=%d, targetId=%d, supportLevel=%s', $submission->getContentType(), $submission->getSourceBlogId(), $submission->getSourceId(), $submission->getId(), $submission->getTargetBlogId(), $submission->getTargetId(), $supportLevel)); - $data = $this->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, - true, - 512, - JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE), - 'status' => $this->wpProxy->get_post($submission->getTargetId())->post_status, - ]); - } catch (\Throwable $e) { - $this->getLogger()->notice(sprintf("Unable to do Elementor save actions for contentType=%s, submissionId=%d, targetBlogId=%d, targetId=%d: %s (%s)", $submission->getContentType(), $submission->getId(), $submission->getTargetBlogId(), $submission->getTargetId(), $e->getMessage(), $e->getTraceAsString())); - } - }); - $this->wpProxy->updatePostMeta($submission->getTargetId(), self::META_FIELD_NAME, addslashes($data)); - } - }); - $this->getLogger()->info("Done processing Elementor after content written hook"); - } - - public function removeUntranslatableFieldsForUpload(array $source, SubmissionEntity $submission): array - { - if ($this->getSupportLevel($submission->getContentType(), $submission->getSourceId()) !== Pluggable::NOT_SUPPORTED) { - $this->getLogger()->info('Detected elementor data, removing post content and elementor related meta fields'); - foreach (array_merge_recursive( - ['meta' => array_merge($this->copyFields, [self::META_CONDITIONS_NAME])], - $this->removeOnUploadFields, - ) as $key => $value) { - if (array_key_exists($key, $source)) { - foreach ($value as $field) { - unset($source[$key][$field]); - } - } - } - } - - return $source; - } - - public function getSupportLevel(string $contentType, ?int $contentId = null): string - { - if ($this->contentTypeHelper->isPost($contentType) && $this->getDataFromPostMeta($contentId) !== '') { - return parent::getSupportLevel($contentType, $contentId); - } - return Pluggable::NOT_SUPPORTED; - } - - private function getData(array $data): ExternalData - { - $related = new RelatedContentInfo(); - $strings = []; - - foreach ($data as $array) { - $element = $this->elementFactory->fromArray($array); - $related = $related->merge($element->getRelated()); - $strings[] = $element->getTranslatableStrings(); - } - - return new ExternalData(strings: $strings, relatedContentInfo: $related); - } - - public function getContentFields(SubmissionEntity $submission, bool $raw): array - { - return $this->fieldsFilterHelper->flattenArray( - (new ArrayHelper())->add( - ...$this->getData($this->readMeta($submission->getSourceId()))->getStrings() - ) + LinkProcessor $linkProcessor, + ) { + parent::__construct( + $contentTypeHelper, + $elementFactory, + $fieldsFilterHelper, + $pluginHelper, + $siteHelper, + $submissionManager, + $userHelper, + $wpProxy, + $linkProcessor, ); } - private function readMeta(int $id): array - { - return json_decode($this->getDataFromPostMeta($id), true, 512, JSON_THROW_ON_ERROR); - } - public function getMaxVersion(): string { return '4'; @@ -168,109 +46,4 @@ public function getMinVersion(): string { return '4'; } - - public function getPluginId(): string - { - return 'elementor'; - } - - public function getPluginPaths(): array - { - return ['elementor/elementor.php']; - } - - public function getRelatedContent(string $contentType, int $contentId): array - { - return $this->getData($this->readMeta($contentId))->getRelatedContentInfo()->getRelatedContentList(); - } - - private function mergeElementorData(array $original, array $strings, SubmissionEntity $submission): array - { - $result = []; - foreach ($original as $array) { - $element = $this->elementFactory->fromArray($array); - $result[] = $element->setTargetContent( - $this, - $this->getData($original)->getRelatedContentInfo(), - $strings, - $submission, - )->toArray(); - } - - return $result; - } - - public function setContentFields(array $original, array $translation, SubmissionEntity $submission): array - { - if (array_key_exists('meta', $original)) { - foreach ($this->copyFields as $field) { - if (array_key_exists($field, $original['meta'])) { - $value = $original['meta'][$field]; - $translation['meta'][$field] = is_string($value) ? $this->wpProxy->maybe_unserialize($value) : $value; - } - } - if (array_key_exists(self::META_CONDITIONS_NAME, $original['meta'])) { - $value = $original['meta'][self::META_CONDITIONS_NAME]; - if (is_string($value)) { - $translation['meta'][self::META_CONDITIONS_NAME] = $this->rebuildConditionsField($value, $submission); - } - } - } - $translation['meta'][self::META_FIELD_NAME] = json_encode($this->mergeElementorData( - json_decode($original['meta'][self::META_FIELD_NAME] ?? '[]', true, 512, JSON_THROW_ON_ERROR), - $translation[$this->getPluginId()] ?? [], - $submission, - ), JSON_THROW_ON_ERROR); - unset($translation[$this->getPluginId()]); - return $translation; - } - - private function rebuildConditionsField(string $conditions, SubmissionEntity $submission): string - { - $value = $this->wpProxy->maybe_unserialize($conditions); - if (is_array($value)) { - foreach ($value as $index => $condition) { - $matches = []; - preg_match('~(\d+)$~', $condition, $matches); - $id = $matches[0] ?? null; - if ($id === null) { - continue; - } - $related = $this->submissionManager->findOne([ - SubmissionEntity::FIELD_SOURCE_BLOG_ID => $submission->getSourceBlogId(), - SubmissionEntity::FIELD_SOURCE_ID => $id, - SubmissionEntity::FIELD_TARGET_BLOG_ID => $submission->getTargetBlogId(), - ]); - if ($related !== null) { - $value[$index] = str_replace($id, $related->getTargetId(), $condition); - } - } - return serialize($value); - } - return $value; - } - - private function getDocumentsManager(): ?Documents_Manager - { - if (class_exists(Plugin::class)) { - return (new DocumentsManagerWrapper(Plugin::elementor()->documents))->getManagerWithoutDocuments(); - } - $documentsManagerPath = WP_PLUGIN_DIR . '/elementor/core/documents-manager.php'; - if (file_exists($documentsManagerPath)) { - try { - require_once $documentsManagerPath; - - $manager = new Documents_Manager(); - do_action('elementor/documents/register', $manager); - return $manager; - } catch (\Throwable) { - // No documents manager available - } - } - if (class_exists(Documents_Manager::class)) { - return new Documents_Manager(); - } - - return null; - } } diff --git a/inc/Smartling/ContentTypes/ExternalContentElementorAbstract.php b/inc/Smartling/ContentTypes/ExternalContentElementorAbstract.php new file mode 100644 index 00000000..3484a23b --- /dev/null +++ b/inc/Smartling/ContentTypes/ExternalContentElementorAbstract.php @@ -0,0 +1,267 @@ + [ + 'post_content', + ], + 'meta' => [ + self::META_FIELD_NAME, + '_elementor_element_cache', + ] + ]; + + public function __construct( + private ContentTypeHelper $contentTypeHelper, + protected ElementFactory3 $elementFactory, + private FieldsFilterHelper $fieldsFilterHelper, + PluginHelper $pluginHelper, + private SiteHelper $siteHelper, + SubmissionManager $submissionManager, + private UserHelper $userHelper, + WordpressFunctionProxyHelper $wpProxy, + private LinkProcessor $linkProcessor, + ) + { + $wpProxy->add_action(ExportedAPI::ACTION_AFTER_TARGET_METADATA_WRITTEN, [$this, 'afterMetaWritten']); + parent::__construct($pluginHelper, $submissionManager, $wpProxy); + } + + public function getWpProxy(): WordpressFunctionProxyHelper + { + return $this->wpProxy; + } + + public function afterMetaWritten(SubmissionEntity $submission): void + { + if ($submission->getTargetId() === 0) { + $this->getLogger()->debug('Processing Elementor after meta written hook skipped, targetId=0'); + return; + } + $this->siteHelper->withBlog($submission->getTargetBlogId(), function () use ($submission) { + $supportLevel = $this->getSupportLevel($submission->getContentType(), $submission->getTargetId()); + $documentsManager = $this->getDocumentsManager(); + if ($supportLevel !== Pluggable::NOT_SUPPORTED && $documentsManager !== null) { + $this->getLogger()->debug(sprintf('Processing Elementor after content written hook, contentType=%s, sourceBlogId=%d, sourceId=%d, submissionId=%d, targetBlogId=%d, targetId=%d, supportLevel=%s', $submission->getContentType(), $submission->getSourceBlogId(), $submission->getSourceId(), $submission->getId(), $submission->getTargetBlogId(), $submission->getTargetId(), $supportLevel)); + $data = $this->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, + true, + 512, + JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE), + 'status' => $this->wpProxy->get_post($submission->getTargetId())->post_status, + ]); + } catch (\Throwable $e) { + $this->getLogger()->notice(sprintf("Unable to do Elementor save actions for contentType=%s, submissionId=%d, targetBlogId=%d, targetId=%d: %s (%s)", $submission->getContentType(), $submission->getId(), $submission->getTargetBlogId(), $submission->getTargetId(), $e->getMessage(), $e->getTraceAsString())); + } + }); + $this->wpProxy->updatePostMeta($submission->getTargetId(), self::META_FIELD_NAME, addslashes($data)); + } + }); + $this->getLogger()->info("Done processing Elementor after content written hook"); + } + + public function removeUntranslatableFieldsForUpload(array $source, SubmissionEntity $submission): array + { + if ($this->getSupportLevel($submission->getContentType(), $submission->getSourceId()) !== Pluggable::NOT_SUPPORTED) { + $this->getLogger()->info('Detected elementor data, removing post content and elementor related meta fields'); + foreach (array_merge_recursive( + ['meta' => array_merge($this->copyFields, [self::META_CONDITIONS_NAME])], + $this->removeOnUploadFields, + ) as $key => $value) { + if (array_key_exists($key, $source)) { + foreach ($value as $field) { + unset($source[$key][$field]); + } + } + } + } + + return $source; + } + + public function getSupportLevel(string $contentType, ?int $contentId = null): string + { + if ($this->contentTypeHelper->isPost($contentType) && $this->getDataFromPostMeta($contentId) !== '') { + return parent::getSupportLevel($contentType, $contentId); + } + return Pluggable::NOT_SUPPORTED; + } + + private function getData(array $data): ExternalData + { + $related = new RelatedContentInfo(); + $strings = []; + + foreach ($data as $array) { + $element = $this->elementFactory->fromArray($array); + $related = $related->merge($element->getRelated()); + $strings[] = $element->getTranslatableStrings(); + } + + return new ExternalData(strings: $strings, relatedContentInfo: $related); + } + + public function getContentFields(SubmissionEntity $submission, bool $raw): array + { + return $this->fieldsFilterHelper->flattenArray( + (new ArrayHelper())->add( + ...$this->getData($this->readMeta($submission->getSourceId()))->getStrings() + ) + ); + } + + private function readMeta(int $id): array + { + return json_decode($this->getDataFromPostMeta($id), true, 512, JSON_THROW_ON_ERROR); + } + + public function getPluginId(): string + { + return 'elementor'; + } + + public function getPluginPaths(): array + { + return ['elementor/elementor.php']; + } + + public function getRelatedContent(string $contentType, int $contentId): array + { + return $this->getData($this->readMeta($contentId))->getRelatedContentInfo()->getRelatedContentList(); + } + + 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, + $relatedContentInfo, + $strings, + $submission, + )->toArray(); + } + + return $result; + } + + public function setContentFields(array $original, array $translation, SubmissionEntity $submission): array + { + if (array_key_exists('meta', $original)) { + foreach ($this->copyFields as $field) { + if (array_key_exists($field, $original['meta'])) { + $value = $original['meta'][$field]; + $translation['meta'][$field] = is_string($value) ? $this->wpProxy->maybe_unserialize($value) : $value; + } + } + if (array_key_exists(self::META_CONDITIONS_NAME, $original['meta'])) { + $value = $original['meta'][self::META_CONDITIONS_NAME]; + if (is_string($value)) { + $translation['meta'][self::META_CONDITIONS_NAME] = $this->rebuildConditionsField($value, $submission); + } + } + } + $translation['meta'][self::META_FIELD_NAME] = json_encode($this->mergeElementorData( + json_decode($original['meta'][self::META_FIELD_NAME] ?? '[]', true, 512, JSON_THROW_ON_ERROR), + $translation[$this->getPluginId()] ?? [], + $submission, + ), JSON_THROW_ON_ERROR); + unset($translation[$this->getPluginId()]); + return $translation; + } + + private function rebuildConditionsField(string $conditions, SubmissionEntity $submission): string + { + $value = $this->wpProxy->maybe_unserialize($conditions); + if (is_array($value)) { + foreach ($value as $index => $condition) { + $matches = []; + preg_match('~(\d+)$~', $condition, $matches); + $id = $matches[0] ?? null; + if ($id === null) { + continue; + } + $related = $this->submissionManager->findOne([ + SubmissionEntity::FIELD_SOURCE_BLOG_ID => $submission->getSourceBlogId(), + SubmissionEntity::FIELD_SOURCE_ID => $id, + SubmissionEntity::FIELD_TARGET_BLOG_ID => $submission->getTargetBlogId(), + ]); + if ($related !== null) { + $value[$index] = str_replace($id, $related->getTargetId(), $condition); + } + } + return serialize($value); + } + return $value; + } + + private function getDocumentsManager(): ?Documents_Manager + { + if (class_exists(Plugin::class)) { + return (new DocumentsManagerWrapper(Plugin::elementor()->documents))->getManagerWithoutDocuments(); + } + $documentsManagerPath = WP_PLUGIN_DIR . '/elementor/core/documents-manager.php'; + if (file_exists($documentsManagerPath)) { + try { + require_once $documentsManagerPath; + + $manager = new Documents_Manager(); + do_action('elementor/documents/register', $manager); + return $manager; + } catch (\Throwable) { + // No documents manager available + } + } + if (class_exists(Documents_Manager::class)) { + return new Documents_Manager(); + } + + return null; + } +} diff --git a/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php b/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php index 75dd88be..4c4b3ac2 100644 --- a/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php +++ b/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php @@ -350,7 +350,7 @@ public function testMixedElementorVersionsInSamePage(): void public function testSourceJsonExtractsAllExpectedStrings(): void { - $data = file_get_contents(__DIR__ . '/../../../issues/WP-1000/source.json'); + $data = file_get_contents(__DIR__ . '/wp-1000.json'); $this->assertNotFalse($data); $proxy = $this->makeProxy($data); @@ -377,7 +377,7 @@ public function testSourceJsonExtractsAllExpectedStrings(): void public function testSourceJsonExtractsImageAttachment(): void { - $data = file_get_contents(__DIR__ . '/../../../issues/WP-1000/source.json'); + $data = file_get_contents(__DIR__ . '/wp-1000.json'); $this->assertNotFalse($data); $proxy = $this->makeProxy($data); diff --git a/tests/Smartling/ContentTypes/wp-1000.json b/tests/Smartling/ContentTypes/wp-1000.json new file mode 100644 index 00000000..0b8f5661 --- /dev/null +++ b/tests/Smartling/ContentTypes/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 From 62c622f25c1ba915f989056162aa67bef98e40da Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Mon, 11 May 2026 12:00:08 +0200 Subject: [PATCH 3/6] move fixtures, extract interface, bump version (WP-1000) Co-Authored-By: Claude Sonnet 4.6 --- composer.json | 3 +- .../Elementor/ElementAbstract.php | 2 +- .../ContentTypes/Elementor/ElementFactory.php | 8 +++++ .../Elementor/ElementFactory3.php | 3 +- .../ExternalContentElementor4.php | 33 ------------------- .../ExternalContentElementorAbstract.php | 4 +-- readme.txt | 5 ++- smartling-connector.php | 2 +- .../ExternalContentElementor3Test.php | 14 ++++---- .../ExternalContentElementor4Test.php | 4 +-- .../ContentTypes/{ => fixtures}/icon.json | 0 .../ucaddon_ue_listing_grid.json | 0 .../ContentTypes/{ => fixtures}/wp-1000.json | 0 .../ContentTypes/{ => fixtures}/wp-834.json | 0 .../ContentTypes/{ => fixtures}/wp-836.json | 0 .../ContentTypes/{ => fixtures}/wp-944.json | 0 .../ContentTypes/{ => fixtures}/wp-952.json | 0 .../ContentTypes/{ => fixtures}/wp-975.json | 0 18 files changed, 29 insertions(+), 49 deletions(-) create mode 100644 inc/Smartling/ContentTypes/Elementor/ElementFactory.php rename tests/Smartling/ContentTypes/{ => fixtures}/icon.json (100%) rename tests/Smartling/ContentTypes/{ => fixtures}/ucaddon_ue_listing_grid.json (100%) rename tests/Smartling/ContentTypes/{ => fixtures}/wp-1000.json (100%) rename tests/Smartling/ContentTypes/{ => fixtures}/wp-834.json (100%) rename tests/Smartling/ContentTypes/{ => fixtures}/wp-836.json (100%) rename tests/Smartling/ContentTypes/{ => fixtures}/wp-944.json (100%) rename tests/Smartling/ContentTypes/{ => fixtures}/wp-952.json (100%) rename tests/Smartling/ContentTypes/{ => fixtures}/wp-975.json (100%) diff --git a/composer.json b/composer.json index f71d6cbc..4f0d0b06 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": [ @@ -27,6 +27,7 @@ }, "require-dev": { "phpunit/phpunit": "~9", + "clue/graph-composer": "*", "vsolovei-smartling/namespacer": "dev-master" }, "config": { diff --git a/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php b/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php index 2d3a6920..651d2ab3 100644 --- a/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php +++ b/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php @@ -141,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; } diff --git a/inc/Smartling/ContentTypes/Elementor/ElementFactory.php b/inc/Smartling/ContentTypes/Elementor/ElementFactory.php new file mode 100644 index 00000000..9153847b --- /dev/null +++ b/inc/Smartling/ContentTypes/Elementor/ElementFactory.php @@ -0,0 +1,8 @@ +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, 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/ExternalContentElementor3Test.php b/tests/Smartling/ContentTypes/ExternalContentElementor3Test.php index 1cf0423a..40fc31af 100644 --- a/tests/Smartling/ContentTypes/ExternalContentElementor3Test.php +++ b/tests/Smartling/ContentTypes/ExternalContentElementor3Test.php @@ -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 => [ diff --git a/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php b/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php index 4c4b3ac2..ba03cb15 100644 --- a/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php +++ b/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php @@ -350,7 +350,7 @@ public function testMixedElementorVersionsInSamePage(): void public function testSourceJsonExtractsAllExpectedStrings(): void { - $data = file_get_contents(__DIR__ . '/wp-1000.json'); + $data = file_get_contents(__DIR__ . '/fixtures/wp-1000.json'); $this->assertNotFalse($data); $proxy = $this->makeProxy($data); @@ -377,7 +377,7 @@ public function testSourceJsonExtractsAllExpectedStrings(): void public function testSourceJsonExtractsImageAttachment(): void { - $data = file_get_contents(__DIR__ . '/wp-1000.json'); + $data = file_get_contents(__DIR__ . '/fixtures/wp-1000.json'); $this->assertNotFalse($data); $proxy = $this->makeProxy($data); 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/wp-1000.json b/tests/Smartling/ContentTypes/fixtures/wp-1000.json similarity index 100% rename from tests/Smartling/ContentTypes/wp-1000.json rename to tests/Smartling/ContentTypes/fixtures/wp-1000.json 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 From 07377f2a9b0530ca4b25e2058be29e7ebcc62e00 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Mon, 11 May 2026 13:09:11 +0200 Subject: [PATCH 4/6] fix typo (WP-1000) Co-Authored-By: Claude Sonnet 4.6 --- inc/Smartling/ContentTypes/Elementor/ElementAbstract.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php b/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php index 651d2ab3..c2dd8084 100644 --- a/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php +++ b/inc/Smartling/ContentTypes/Elementor/ElementAbstract.php @@ -141,7 +141,7 @@ public function setRelations( $contentType = $content->getType(); } - if (is_string($contentType)) { + 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; } From 810e14dc75011ea5921cb2ad539dfd4d525d9300 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Mon, 11 May 2026 13:19:02 +0200 Subject: [PATCH 5/6] remove unused dependency, update docs, fix regression (WP-1000) Co-Authored-By: Claude Sonnet 4.6 --- composer.json | 1 - composer.lock | 213 +++++++++++++++++++++- docs/ELEMENTOR_DEVELOPMENT.md | 320 +++++++++++++++++++++++++--------- 3 files changed, 453 insertions(+), 81 deletions(-) diff --git a/composer.json b/composer.json index 4f0d0b06..87541bf7 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,6 @@ }, "require-dev": { "phpunit/phpunit": "~9", - "clue/graph-composer": "*", "vsolovei-smartling/namespacer": "dev-master" }, "config": { 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 From 9d0ba3b1a5eaacb20ec8e6721dcd1e55a4f7f29d Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Mon, 11 May 2026 13:32:03 +0200 Subject: [PATCH 6/6] add test (WP-1000) Co-Authored-By: Claude Sonnet 4.6 --- .../ContentTypes/ExternalContentElementor4Test.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php b/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php index ba03cb15..83402ff7 100644 --- a/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php +++ b/tests/Smartling/ContentTypes/ExternalContentElementor4Test.php @@ -6,6 +6,7 @@ use Smartling\ContentTypes\ContentTypeHelper; use Smartling\ContentTypes\Elementor\ElementFactory4; use Smartling\ContentTypes\ExternalContentElementor4; +use Smartling\Extensions\Pluggable; use Smartling\Helpers\FieldsFilterHelper; use Smartling\Helpers\LinkProcessor; use Smartling\Helpers\PluginHelper; @@ -59,6 +60,19 @@ private function mockSubmission(): SubmissionEntity 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