PluginProbe
Extendify / 3.2.0
Extendify v3.2.0
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / app / Shared / Services / Import / BlocksUpdater.php

BlocksUpdater.php in Extendify 3.2.0, at app/Shared/Services/Import/BlocksUpdater.php

333 lines 11.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Blocks uploader class
5 */
6
7 namespace Extendify\Shared\Services\Import;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 /**
12 * This class responsible for updating the blocks.
13 */
14
15 class BlocksUpdater
16 {
17 /**
18 * The class to target.
19 *
20 * @var array The class names that we want to target.
21 */
22 protected $classesToTarget = ['extendify-image-import', 'ext-import'];
23
24 /**
25 * Update the content of the blocks in a specific post.
26 *
27 * @param \WP_Post $post WordPress post.
28 * @return string The updated post content.
29 */
30 public function getModifiedBlocksInPost($post)
31 {
32 $blocks = parse_blocks($post->post_content);
33
34 $updatedBlocks = $this->processAndMutateBlocks($blocks, $post->post_author);
35
36 return str_replace('\u002d\u002d', '--', serialize_blocks($updatedBlocks));
37 }
38
39 /**
40 * The logic for the update blocks code.
41 *
42 * @param array $blocks WordPress post blocks.
43 * @param string $author WordPress post author.
44 * @return array
45 */
46 protected function processAndMutateBlocks($blocks, $author)
47 {
48 return array_map(function ($block) use ($author) {
49 $block = $this->processBlock($block, $author);
50
51 if (is_wp_error($block)) {
52 return $block;
53 }
54
55 if (!empty($block['innerBlocks']) && !is_null($block['blockName'])) {
56 $block['innerBlocks'] = $this->processAndMutateBlocks($block['innerBlocks'], $author);
57 }
58
59 return $block;
60 }, $blocks);
61 }
62
63 /**
64 * Check if the block should not be processed.
65 *
66 * @param array $block the core/image block that we need to update.
67 * @return bool
68 */
69 protected function needsImageProcessing($block)
70 {
71 // Check if the attributes has an element called `className` with the value of `extendify-image-import`.
72 // if the returned array is empty, then the block should not be processed.
73 $attrs = ($block['attrs'] ?? []);
74 if (array_key_exists('className', $attrs)) {
75 $className = is_array($attrs['className']) ? $attrs['className'] : explode(' ', $attrs['className']);
76 return !empty(array_intersect($this->classesToTarget, $className));
77 }
78
79 return false;
80 }
81
82 /**
83 * This function process the image block and return the new code.
84 *
85 * @param array $block the core/image block that we need to update.
86 * @param string $author the post author.
87 * @return array|\WP_Error
88 */
89 protected function processBlock($block, $author)
90 {
91 // Check if the block has the targeted class anywhere (even in unexpected places).
92 $needsToRemoveClassName = $this->hasTargetedClassName($block);
93
94 // Return the block unmodified if we don't find the class.
95 if (!$this->needsImageProcessing($block) && !$needsToRemoveClassName) {
96 return $block;
97 }
98
99 // Check if the block has an image.
100 $image = $this->getImageSource($block['innerHTML']);
101
102 if (!$image) {
103 // If we found the class, but no image, then just remove the class.
104 if ($needsToRemoveClassName) {
105 $block = $this->removeTargetedClassAttribute($block);
106 $block = $this->removeClassAttributeFromAttrs($block);
107 // In some cases the block might become unformatted.
108 $block['innerHTML'] = $this->stripClassTokens($block['innerHTML']);
109 $block['innerContent'] = array_map(function ($item) {
110 return !is_null($item) ? $this->stripClassTokens($item) : null;
111 }, ($block['innerContent'] ?? []));
112 }
113
114 return $block;
115 }
116
117 $upload = (new ImageUploader())->uploadImage($image, $author);
118
119 if (is_wp_error($upload)) {
120 // This is used for recording the error in the logs.
121 return new \WP_Error($upload->get_error_code(), $upload->get_error_message());
122 }
123
124 $block = $this->updateNewBlockAttributes($block, $upload);
125 $block = $this->addImageAttributes($block, $upload);
126 $block = $this->removeTargetedClassAttribute($block);
127 $block = $this->removeClassAttributeFromAttrs($block);
128
129 return $block;
130 }
131
132 /**
133 * Return the image source link or an empty string.
134 *
135 * @param string $htmlContent The html tag that contains the image tag.
136 * @return string
137 */
138 protected function getImageSource($htmlContent)
139 {
140 $html = new \WP_HTML_Tag_Processor($htmlContent);
141 $html->next_tag('img');
142 $src = $html->get_attribute('src');
143
144 // Feeds a server-side fetch, so a path-matched domain would let any host through.
145 return ($src && ImageUploader::isAllowedImageHost($src)) ? $src : '';
146 }
147
148 /**
149 * Update the content of the block to remove the targeted class attribute.
150 *
151 * @param array $block The block we need to update.
152 * @return array The parsed block after updates.
153 */
154 protected function removeTargetedClassAttribute(array $block)
155 {
156 $block['innerContent'] = array_map(function ($item) {
157 return !is_null($item) ? $this->removeClassAttributeFromContent($item) : null;
158 }, ($block['innerContent'] ?? []));
159
160 $block['innerHTML'] = $this->removeClassAttributeFromContent($block['innerHTML']);
161
162 return $block;
163 }
164
165 /**
166 * Remove the targeted class from the html content.
167 *
168 * @param string $content The html tag that contains the targeted class.
169 * @return string
170 */
171 protected function removeClassAttributeFromContent($content)
172 {
173 foreach ($this->classesToTarget as $targetedClass) {
174 $html = new \WP_HTML_Tag_Processor($content);
175 do {
176 $html->remove_class($targetedClass);
177 } while ($html->next_tag(['class' => $targetedClass]));
178 $content = $html->get_updated_html();
179 }
180
181 return $content;
182 }
183
184 /**
185 * Build a pattern matching the class only where it stands as a whole token.
186 *
187 * @param string $targetedClass The class name to match.
188 * @return string
189 */
190 protected function classTokenPattern($targetedClass)
191 {
192 // Uploaded filenames embed the class name (ext-imported-*.jpg); a bare substring match eats the src.
193 return '/(?<=[\s"\'])' . preg_quote($targetedClass, '/') . '(?=[\s"\'])/';
194 }
195
196 /**
197 * Remove the targeted classes from html the tag processor left untouched.
198 *
199 * @param string $content The html content.
200 * @return string
201 */
202 protected function stripClassTokens($content)
203 {
204 foreach ($this->classesToTarget as $targetedClass) {
205 $content = preg_replace($this->classTokenPattern($targetedClass), '', $content);
206 }
207
208 return $content;
209 }
210
211 /**
212 * Remove the targeted class from the className attrs.
213 *
214 * @param array $block The block.
215 * @return array The parsed block after updates.
216 */
217 protected function removeClassAttributeFromAttrs($block)
218 {
219 if (isset($block['attrs']['className'])) {
220 $className = is_array($block['attrs']['className'])
221 ? $block['attrs']['className']
222 : explode(' ', $block['attrs']['className']);
223 $className = array_diff($className, $this->classesToTarget);
224 $block['attrs']['className'] = implode(' ', $className);
225 }
226
227 return $block;
228 }
229
230 /**
231 * Update the block attributes with information about the image.
232 *
233 * @param array $block Block.
234 * @param array $upload The uploaded file information.
235 * @return array The parse block after updates.
236 */
237 protected function updateNewBlockAttributes(array $block, array $upload)
238 {
239 $block['attrs']['id'] = $upload['attachment_id'];
240
241 if ($block['blockName'] === 'core/media-text') {
242 $block['attrs']['mediaId'] = $upload['attachment_id'];
243 $block['attrs']['mediaLink'] = $upload['url'];
244 }
245
246 if ($block['blockName'] === 'core/cover') {
247 $block['attrs']['url'] = $upload['url'];
248 }
249
250 return $block;
251 }
252
253 /**
254 * Update the inner content for the block.
255 *
256 * @param array $block Block inner content.
257 * @param array $upload The uploaded file information.
258 * @return array
259 */
260 protected function addImageAttributes($block, $upload)
261 {
262 $isMediaText = $block['blockName'] === 'core/media-text';
263
264 $block['innerContent'] = array_map(function ($item) use ($upload, $isMediaText) {
265 return !is_null($item) ? $this->updateImageTagAttributes($item, $upload, $isMediaText) : null;
266 }, ($block['innerContent'] ?? []));
267
268 $block['innerHTML'] = $this->updateImageTagAttributes($block['innerHTML'], $upload, $isMediaText);
269
270 return $block;
271 }
272
273 /**
274 * Checks the html and content for the class name.
275 *
276 * @param array $block The block.
277 * @return boolean
278 */
279 protected function hasTargetedClassName(array $block)
280 {
281 $innerHTML = ($block['innerHTML'] ?? '');
282 foreach ($this->classesToTarget as $targetedClass) {
283 if (preg_match($this->classTokenPattern($targetedClass), $innerHTML)) {
284 return true;
285 }
286 }
287
288 $classList = is_array(($block['attrs']['className'] ?? null))
289 ? $block['attrs']['className']
290 : explode(' ', ($block['attrs']['className'] ?? ''));
291
292 return !empty(array_intersect($this->classesToTarget, $classList));
293 }
294
295 /**
296 * Return the new html content after making the required changes.
297 *
298 * @param string $htmlContent The html tag that contains the image tag.
299 * @param array $upload The uploaded file information.
300 * @param bool $isMediaText Is the block a media text block, if so, we need to update the style attribute.
301 * @return string
302 */
303 protected function updateImageTagAttributes($htmlContent, $upload, $isMediaText = false)
304 {
305 $html = new \WP_HTML_Tag_Processor($htmlContent);
306
307 // Media text block needs a bit more work to update the style attribute.
308 if (
309 $isMediaText && $html->next_tag([
310 'tag_name' => 'figure',
311 'class_name' => 'wp-block-media-text__media',
312 ])
313 ) {
314 $style = $html->get_attribute('style');
315 if (preg_match('/:url\(*.+\)/m', $style, $matches)) {
316 $result = str_replace($matches[0], ':url(' . $upload['url'] . ')', $style);
317 $html->set_attribute('style', ($result ?? ''));
318 }
319 }
320
321 // Update the image tag attributes.
322 if ($html->next_tag('img')) {
323 $html->set_attribute('src', $upload['url']);
324 $html->add_class('wp-image-' . $upload['attachment_id']);
325 if ($isMediaText && !$html->has_class('size-full')) {
326 $html->add_class('size-full');
327 }
328 }
329
330 return $html->get_updated_html();
331 }
332 }
333