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 +156 -26 3.1.6 → 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 /**
@@ -298,8 +301,75 @@
298 301 }
299 302
300 303
301 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 + /**
302 372 * Get the HTML of a specific tagged block code
303 373 *
304 374 * @param \WP_REST_Request $request The REST API request object.
305 375 * @return \WP_REST_Response
@@ -305,10 +375,22 @@
305 375 * @return \WP_REST_Response
306 376 */
307 377 public static function getBlockCode(\WP_REST_Request $request)
308 378 {
309 - $blockId = (int) $request->get_param('blockId');
379 + $rawId = $request->get_param('blockId');
380 + $partSlug = (string) $request->get_param('partSlug');
310 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;
311 393 if ($blockId < 1) {
312 394 return new \WP_REST_Response(['error' => 'Invalid blockId'], 400);
313 395 }
314 396
@@ -313,9 +395,8 @@
313 395 }
314 396
315 397 // A template-part source resolves the part and walks it with the shared
316 398 // preorder finder, instead of the post path below.
317 - $partSlug = (string) $request->get_param('partSlug');
318 399 if ($partSlug !== '') {
319 400 return self::getTemplatePartBlockCode($partSlug, $blockId);
320 401 }
321 402
@@ -324,22 +405,27 @@
324 405 if (!$post) {
325 406 return new \WP_REST_Response(['error' => 'Post not found'], 404);
326 407 }
327 408
328 - $found = \Extendify\Agent\PostBlockFinder::find(parse_blocks($post->post_content), $blockId);
409 + $found = PostBlockFinder::find(parse_blocks($post->post_content), $blockId);
329 410
330 411 if (!$found || empty($found['block']['blockName'])) {
331 412 return new \WP_REST_Response(['error' => 'Block id not found in this post'], 404);
332 413 }
333 414
334 - $block = $found['block'];
335 - return new \WP_REST_Response([
336 - '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, [
337 423 'blockId' => $blockId,
338 424 'name' => $block['blockName'],
339 425 'attrs' => $block['attrs'] ?? (object)[],
340 426 'block' => serialize_blocks([$block]),
341 - ], 200);
427 + ]), 200);
342 428 }
343 429
344 430 // Resolves the part the same way SaveController does — active-theme-scoped
345 431 // get_block_template, not a raw get_posts by name — then walks it with the
@@ -345,35 +431,71 @@
345 431 // get_block_template, not a raw get_posts by name — then walks it with the
346 432 // shared preorder finder so the blockId lands on the block save will write.
347 433 private static function getTemplatePartBlockCode(string $slug, int $blockId)
348 434 {
349 - $stylesheet = wp_get_theme()->get_stylesheet();
350 - $template = get_block_template("{$stylesheet}//{$slug}", 'wp_template_part');
351 - if (!$template || empty($template->wp_id)) {
435 + $content = self::templatePartContent($slug);
436 + if ($content === null) {
352 437 return new \WP_REST_Response(['error' => 'Template part not found'], 404);
353 438 }
354 439
355 - $post = \get_post($template->wp_id);
356 - if (!$post) {
357 - 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);
358 446 }
359 447
360 - $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(
361 490 parse_blocks($post->post_content),
362 - $blockId
491 + $composite['seq']
363 492 );
364 493 if (!is_array($found) || empty($found['block']['blockName'])) {
365 - 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);
366 495 }
367 496
368 - $block = $found['block'];
369 - return new \WP_REST_Response([
370 - 'partSlug' => $slug,
371 - 'blockId' => $blockId,
372 - 'name' => $block['blockName'],
373 - 'attrs' => $block['attrs'] ?? (object)[],
374 - 'block' => serialize_blocks([$block]),
375 - ], 200);
497 + return self::blockCodeResponse($found['block'], ['partSlug' => $partSlug], $blockId);
376 498 }
377 499
378 500 /**
379 501 * Get the rendered HTML of some block code
@@ -451,10 +573,18 @@
451 573 1
452 574 );
453 575 }
454 576
455 - if ($description) {
577 + if (trim((string) $description) === '') {
578 + // Catalog paragraphs describe the slot, not the site.
456 579 $code = preg_replace(
580 + '/<!-- wp:paragraph[\s\S]*?<!-- \/wp:paragraph -->/m',
581 + '',
582 + $code,
583 + 1
584 + );
585 + } else {
586 + $code = preg_replace(
457 587 '/(<!-- wp:paragraph[^>]*-->[\s\S]*?<p[^>]*>)[\s\S]*?(<\/p>[\s\S]*?<!-- \/wp:paragraph -->)/m',
458 588 '${1}' . esc_html($description) . '${2}',
459 589 $code,
460 590 1
@@ -641,9 +771,9 @@
641 771
642 772 public static function getSiteDesignVariations(\WP_REST_Request $request)
643 773 {
644 774 $title = $request->get_param('title');
645 - $description = $request->get_param('description');
775 + $description = HeroDescription::resolve($request->get_param('description'));
646 776 $images = $request->get_param('images') ?? [];
647 777 $cta = $request->get_param('cta');
648 778 $featuredOnly = true; // Only show featured patterns
649 779 $currentHeroPattern = $request->get_param('currentHeroPattern');