PluginProbe
Extendify / trunk
Extendify vtrunk
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.5 → trunk 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');