PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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
← All changes | app/Agent/Controllers/WPController.php +162 -27 3.1.53.2.1 View file →
@@ -7,9 +7,12 @@
7 7 namespace Extendify\Agent\Controllers;
8 8
9 9 defined('ABSPATH') || die('No direct access.');
10 10
11 +use Extendify\Agent\PostBlockFinder;
12 +use Extendify\Agent\TemplatePartBlockFinder;
11 13 use Extendify\Constants;
14 +use Extendify\Shared\Services\HeroDescription;
12 15 use Extendify\Shared\Services\Sanitizer;
13 16 use Extendify\Shared\Services\SiteImages;
14 17
15 18 /**
@@ -51,8 +54,12 @@
51 54 'onyx',
52 55 'glasgow',
53 56 'royal',
54 57 'obsidian',
58 + 'heath',
59 + 'signal',
60 + 'marzipan',
61 + 'butterscotch',
55 62 ];
56 63
57 64 /**
58 65 * Recursively filter an array to include only specified properties.
@@ -284,9 +291,10 @@
284 291 'lineHeight' => $typography['lineHeight'] ?? null,
285 292 'letterSpacing' => $typography['letterSpacing'] ?? null,
286 293 'fontStyle' => $typography['fontStyle'] ?? null,
287 294 'fontWeight' => $typography['fontWeight'] ?? null,
288 - 'textTransform' => $typography['textTransform'] ?? 'none',
295 + // Defaulting to 'none' would cancel the applied vibe's uppercase.
296 + 'textTransform' => $typography['textTransform'] ?? null,
289 297 ], function ($v) {
290 298 return $v !== null;
291 299 })
292 300 ];
@@ -293,8 +301,75 @@
293 301 }
294 302
295 303
296 304 /**
305 + * Where else the synced pattern or menu a composite blockId names is used
306 + *
307 + * @param \WP_REST_Request $request The REST API request object.
308 + * @return \WP_REST_Response
309 + */
310 + public static function getSharedBlockUsage(\WP_REST_Request $request)
311 + {
312 + $composite = TemplatePartBlockFinder::parseCompositeId(
313 + $request->get_param('blockId')
314 + );
315 + if (!$composite) {
316 + return new \WP_REST_Response(['scope' => 'block', 'pages' => []], 200);
317 + }
318 +
319 + $seen = [];
320 + $pages = [];
321 + $sitewide = self::collectRefUsage($composite['ref'], $seen, $pages);
322 +
323 + return new \WP_REST_Response([
324 + 'scope' => $sitewide ? 'site' : ($pages ? 'pages' : 'block'),
325 + 'pages' => array_values($pages),
326 + ], 200);
327 + }
328 +
329 + // A template or part renders it everywhere, so a page list only matters otherwise.
330 + private static function collectRefUsage(int $ref, array &$seen, array &$pages): bool
331 + {
332 + if (isset($seen[$ref])) {
333 + return false;
334 + }
335 + $seen[$ref] = true;
336 +
337 + $sitewide = false;
338 + foreach (self::postsReferencing($ref) as $row) {
339 + $id = (int) $row->ID;
340 + if (in_array($row->post_type, ['wp_template', 'wp_template_part'], true)) {
341 + $sitewide = true;
342 + continue;
343 + }
344 + // A pattern nested in a pattern inherits wherever the outer one lands.
345 + if ($row->post_type === 'wp_block') {
346 + $sitewide = self::collectRefUsage($id, $seen, $pages) || $sitewide;
347 + continue;
348 + }
349 + $pages[$id] = ['id' => $id, 'title' => \get_the_title($id)];
350 + }
351 +
352 + return $sitewide;
353 + }
354 +
355 + // The ref is literal text inside a block comment, which WP indexes nowhere.
356 + private static function postsReferencing(int $ref)
357 + {
358 + global $wpdb;
359 +
360 + $like = $wpdb->esc_like('"ref":' . $ref);
361 + return $wpdb->get_results($wpdb->prepare(
362 + "SELECT ID, post_type FROM {$wpdb->posts}
363 + WHERE post_status IN ('publish', 'private', 'draft')
364 + AND post_type IN ('post', 'page', 'wp_block', 'wp_template', 'wp_template_part')
365 + AND (post_content LIKE %s OR post_content LIKE %s)",
366 + '%' . $like . '}%',
367 + '%' . $like . ',%'
368 + ));
369 + }
370 +
371 + /**
297 372 * Get the HTML of a specific tagged block code
298 373 *
299 374 * @param \WP_REST_Request $request The REST API request object.
300 375 * @return \WP_REST_Response
@@ -300,10 +375,22 @@
300 375 * @return \WP_REST_Response
301 376 */
302 377 public static function getBlockCode(\WP_REST_Request $request)
303 378 {
304 - $blockId = (int) $request->get_param('blockId');
379 + $rawId = $request->get_param('blockId');
380 + $partSlug = (string) $request->get_param('partSlug');
305 381
382 + $composite = TemplatePartBlockFinder::parseCompositeId($rawId);
383 + if ($composite) {
384 + return self::getRefPostBlockCode(
385 + $composite,
386 + (string) $rawId,
387 + $partSlug,
388 + (int) $request->get_param('postId')
389 + );
390 + }
391 +
392 + $blockId = (int) $rawId;
306 393 if ($blockId < 1) {
307 394 return new \WP_REST_Response(['error' => 'Invalid blockId'], 400);
308 395 }
309 396
@@ -308,9 +395,8 @@
308 395 }
309 396
310 397 // A template-part source resolves the part and walks it with the shared
311 398 // preorder finder, instead of the post path below.
312 - $partSlug = (string) $request->get_param('partSlug');
313 399 if ($partSlug !== '') {
314 400 return self::getTemplatePartBlockCode($partSlug, $blockId);
315 401 }
316 402
@@ -319,22 +405,27 @@
319 405 if (!$post) {
320 406 return new \WP_REST_Response(['error' => 'Post not found'], 404);
321 407 }
322 408
323 - $found = \Extendify\Agent\PostBlockFinder::find(parse_blocks($post->post_content), $blockId);
409 + $found = PostBlockFinder::find(parse_blocks($post->post_content), $blockId);
324 410
325 411 if (!$found || empty($found['block']['blockName'])) {
326 412 return new \WP_REST_Response(['error' => 'Block id not found in this post'], 404);
327 413 }
328 414
329 - $block = $found['block'];
330 - return new \WP_REST_Response([
331 - 'postId' => $postId,
415 + return self::blockCodeResponse($found['block'], ['postId' => $postId], $blockId);
416 + }
417 +
418 + // One response shape for every source, so the client reads the same fields
419 + // whichever post the block turned out to live in.
420 + private static function blockCodeResponse(array $block, array $scope, $blockId)
421 + {
422 + return new \WP_REST_Response(array_merge($scope, [
332 423 'blockId' => $blockId,
333 424 'name' => $block['blockName'],
334 425 'attrs' => $block['attrs'] ?? (object)[],
335 426 'block' => serialize_blocks([$block]),
336 - ], 200);
427 + ]), 200);
337 428 }
338 429
339 430 // Resolves the part the same way SaveController does — active-theme-scoped
340 431 // get_block_template, not a raw get_posts by name — then walks it with the
@@ -340,35 +431,71 @@
340 431 // get_block_template, not a raw get_posts by name — then walks it with the
341 432 // shared preorder finder so the blockId lands on the block save will write.
342 433 private static function getTemplatePartBlockCode(string $slug, int $blockId)
343 434 {
344 - $stylesheet = wp_get_theme()->get_stylesheet();
345 - $template = get_block_template("{$stylesheet}//{$slug}", 'wp_template_part');
346 - if (!$template || empty($template->wp_id)) {
435 + $content = self::templatePartContent($slug);
436 + if ($content === null) {
347 437 return new \WP_REST_Response(['error' => 'Template part not found'], 404);
348 438 }
349 439
350 - $post = \get_post($template->wp_id);
351 - if (!$post) {
352 - return new \WP_REST_Response(['error' => 'Template part not found'], 404);
440 + $found = TemplatePartBlockFinder::find(
441 + parse_blocks($content),
442 + $blockId
443 + );
444 + if (!is_array($found) || empty($found['block']['blockName'])) {
445 + return new \WP_REST_Response(['error' => 'Block id not found in this template part'], 404);
353 446 }
354 447
355 - $found = \Extendify\Agent\TemplatePartBlockFinder::find(
448 + return self::blockCodeResponse($found['block'], ['partSlug' => $slug], $blockId);
449 + }
450 +
451 + // Post and theme file number identically, so an id survives the first edit.
452 + private static function templatePartContent(string $slug)
453 + {
454 + $stylesheet = \wp_get_theme()->get_stylesheet();
455 + $template = \get_block_template("{$stylesheet}//{$slug}", 'wp_template_part');
456 + if (!$template) {
457 + return null;
458 + }
459 + $post = empty($template->wp_id) ? null : \get_post($template->wp_id);
460 + return $post ? $post->post_content : $template->content;
461 + }
462 +
463 + // A synced pattern's or menu's blocks live in the post its id names, walked
464 + // with the same finder — the part they render inside owns none of them.
465 + private static function getRefPostBlockCode(
466 + array $composite,
467 + string $blockId,
468 + string $partSlug,
469 + int $postId
470 + ) {
471 + $post = \get_post($composite['ref']);
472 + if (!$post || $post->post_type !== $composite['postType']) {
473 + return new \WP_REST_Response(['error' => 'Referenced post not found'], 404);
474 + }
475 +
476 + $containerPost = $partSlug === '' ? \get_post($postId) : null;
477 + $container = $partSlug !== ''
478 + ? self::templatePartContent($partSlug)
479 + : ($containerPost ? $containerPost->post_content : null);
480 + if ($container === null) {
481 + return new \WP_REST_Response(['error' => 'Block id not found in this post'], 404);
482 + }
483 +
484 + // Unchecked, every pattern and menu on the site reads back from any page.
485 + if (!TemplatePartBlockFinder::contentRendersRef($container, $composite['ref'])) {
486 + return new \WP_REST_Response(['error' => 'Block id not found in this post'], 404);
487 + }
488 +
489 + $found = TemplatePartBlockFinder::find(
356 490 parse_blocks($post->post_content),
357 - $blockId
491 + $composite['seq']
358 492 );
359 493 if (!is_array($found) || empty($found['block']['blockName'])) {
360 - return new \WP_REST_Response(['error' => 'Block id not found in this template part'], 404);
494 + return new \WP_REST_Response(['error' => 'Block id not found in this referenced post'], 404);
361 495 }
362 496
363 - $block = $found['block'];
364 - return new \WP_REST_Response([
365 - 'partSlug' => $slug,
366 - 'blockId' => $blockId,
367 - 'name' => $block['blockName'],
368 - 'attrs' => $block['attrs'] ?? (object)[],
369 - 'block' => serialize_blocks([$block]),
370 - ], 200);
497 + return self::blockCodeResponse($found['block'], ['partSlug' => $partSlug], $blockId);
371 498 }
372 499
373 500 /**
374 501 * Get the rendered HTML of some block code
@@ -446,10 +573,18 @@
446 573 1
447 574 );
448 575 }
449 576
450 - if ($description) {
577 + if (trim((string) $description) === '') {
578 + // Catalog paragraphs describe the slot, not the site.
451 579 $code = preg_replace(
580 + '/<!-- wp:paragraph[\s\S]*?<!-- \/wp:paragraph -->/m',
581 + '',
582 + $code,
583 + 1
584 + );
585 + } else {
586 + $code = preg_replace(
452 587 '/(<!-- wp:paragraph[^>]*-->[\s\S]*?<p[^>]*>)[\s\S]*?(<\/p>[\s\S]*?<!-- \/wp:paragraph -->)/m',
453 588 '${1}' . esc_html($description) . '${2}',
454 589 $code,
455 590 1
@@ -636,9 +771,9 @@
636 771
637 772 public static function getSiteDesignVariations(\WP_REST_Request $request)
638 773 {
639 774 $title = $request->get_param('title');
640 - $description = $request->get_param('description');
775 + $description = HeroDescription::resolve($request->get_param('description'));
641 776 $images = $request->get_param('images') ?? [];
642 777 $cta = $request->get_param('cta');
643 778 $featuredOnly = true; // Only show featured patterns
644 779 $currentHeroPattern = $request->get_param('currentHeroPattern');