| 1 |
<?php |
| 2 |
|
| 3 |
namespace Templately\Modules\BlockRecovery; |
| 4 |
|
| 5 |
use Templately\Utils\Helper; |
| 6 |
|
| 7 |
/** |
| 8 |
* What the browser pass could not repair, aggregated into something a user can act on. |
| 9 |
* |
| 10 |
* Skips are not failures of the pass — they are the pass refusing to destroy data. A block is |
| 11 |
* skipped when the installed plugin does not declare an attribute the stored markup carries |
| 12 |
* (`parse` would drop it, and rebuilding would write it out of existence permanently). The |
| 13 |
* useful message is therefore not "we could not fix this" but "this plugin is behind the |
| 14 |
* version this pack was built with; update it and these blocks fix themselves". |
| 15 |
*/ |
| 16 |
class Report { |
| 17 |
|
| 18 |
const OPTION = 'templately_block_rebuild_report'; |
| 19 |
|
| 20 |
/** Which trigger produced an outcome (060 FR-013). Rows from before 060 read as the batch. */ |
| 21 |
const TRIGGER_BATCH = 'batch'; |
| 22 |
const TRIGGER_ON_OPEN = 'on-open'; |
| 23 |
|
| 24 |
/** Reports older than this are dropped, matching the queue's own retention. */ |
| 25 |
const MAX_AGE = 7 * DAY_IN_SECONDS; |
| 26 |
|
| 27 |
/** |
| 28 |
* Fold one post's result into the stored report. |
| 29 |
* |
| 30 |
* @param int $post_id Post that was visited. |
| 31 |
* @param string $status 'clean' | 'rebuilt' | 'skipped' | 'failed'. |
| 32 |
* @param int $rebuilt Number of blocks regenerated. |
| 33 |
* @param array $skipped `[['block' => 'name', 'droppedAttrs' => ['x']], …]` |
| 34 |
* @param int $preserved Blocks rebuilt whose undeclared attributes were merged back. |
| 35 |
* @param int $restored Individual attributes merged back. |
| 36 |
* @param string $trigger Which trigger produced this: 'batch' | 'on-open'. Unknown → 'batch'. |
| 37 |
* @param int $duration Wall time the trigger spent on this post, in ms (0 = not measured). |
| 38 |
*/ |
| 39 |
public static function record( int $post_id, string $status, int $rebuilt, array $skipped, int $preserved = 0, int $restored = 0, string $trigger = self::TRIGGER_BATCH, int $duration = 0 ) { |
| 40 |
$report = self::get(); |
| 41 |
|
| 42 |
if ( ! in_array( $trigger, [ self::TRIGGER_BATCH, self::TRIGGER_ON_OPEN ], true ) ) { |
| 43 |
$trigger = self::TRIGGER_BATCH; |
| 44 |
} |
| 45 |
$duration = max( 0, $duration ); |
| 46 |
|
| 47 |
$report['created_at'] = $report['created_at'] ?? time(); |
| 48 |
$report['posts'][ $post_id ] = [ |
| 49 |
'status' => $status, |
| 50 |
'rebuilt' => $rebuilt, |
| 51 |
'skipped' => count( $skipped ), |
| 52 |
// Recorded so a summary rendered from the STORED report (a remount, a later visit) |
| 53 |
// says the same thing as the one rendered from the live run. |
| 54 |
'preserved' => $preserved, |
| 55 |
'restored' => $restored, |
| 56 |
'trigger' => $trigger, |
| 57 |
'durationMs' => $duration, |
| 58 |
]; |
| 59 |
|
| 60 |
$report['totals']['rebuilt'] = ( $report['totals']['rebuilt'] ?? 0 ) + $rebuilt; |
| 61 |
$report['totals']['skipped'] = ( $report['totals']['skipped'] ?? 0 ) + count( $skipped ); |
| 62 |
$report['totals']['preserved'] = ( $report['totals']['preserved'] ?? 0 ) + $preserved; |
| 63 |
$report['totals']['restored'] = ( $report['totals']['restored'] ?? 0 ) + $restored; |
| 64 |
|
| 65 |
$by_trigger = $report['totals']['byTrigger'][ $trigger ] ?? [ 'posts' => 0, 'durationMs' => 0 ]; |
| 66 |
$by_trigger['posts']++; |
| 67 |
$by_trigger['durationMs'] += $duration; |
| 68 |
$report['totals']['byTrigger'][ $trigger ] = $by_trigger; |
| 69 |
|
| 70 |
foreach ( $skipped as $entry ) { |
| 71 |
$block = isset( $entry['block'] ) ? (string) $entry['block'] : ''; |
| 72 |
$slug = self::plugin_slug_for_block( $block ); |
| 73 |
|
| 74 |
if ( '' === $slug ) { |
| 75 |
continue; |
| 76 |
} |
| 77 |
|
| 78 |
$bucket = $report['plugins'][ $slug ] ?? [ |
| 79 |
'slug' => $slug, |
| 80 |
'blocks' => 0, |
| 81 |
'unknownAttributes' => [], |
| 82 |
'postIds' => [], |
| 83 |
]; |
| 84 |
|
| 85 |
$bucket['blocks']++; |
| 86 |
|
| 87 |
foreach ( (array) ( $entry['droppedAttrs'] ?? [] ) as $attribute ) { |
| 88 |
$attribute = (string) $attribute; |
| 89 |
if ( '' !== $attribute && ! in_array( $attribute, $bucket['unknownAttributes'], true ) ) { |
| 90 |
$bucket['unknownAttributes'][] = $attribute; |
| 91 |
} |
| 92 |
} |
| 93 |
|
| 94 |
if ( ! in_array( $post_id, $bucket['postIds'], true ) ) { |
| 95 |
$bucket['postIds'][] = $post_id; |
| 96 |
} |
| 97 |
|
| 98 |
$report['plugins'][ $slug ] = $bucket; |
| 99 |
} |
| 100 |
|
| 101 |
update_option( self::OPTION, $report, false ); |
| 102 |
|
| 103 |
if ( ! empty( $skipped ) ) { |
| 104 |
Helper::log( |
| 105 |
[ |
| 106 |
'context' => 'block_rebuild_skipped', |
| 107 |
'post_id' => $post_id, |
| 108 |
'skipped' => $skipped, |
| 109 |
], |
| 110 |
'fsi_event' |
| 111 |
); |
| 112 |
} |
| 113 |
} |
| 114 |
|
| 115 |
/** |
| 116 |
* @return array{created_at?:int,posts?:array,totals?:array,plugins?:array} |
| 117 |
*/ |
| 118 |
public static function get(): array { |
| 119 |
$report = get_option( self::OPTION, [] ); |
| 120 |
|
| 121 |
if ( ! is_array( $report ) ) { |
| 122 |
return []; |
| 123 |
} |
| 124 |
|
| 125 |
if ( ! empty( $report['created_at'] ) && ( time() - (int) $report['created_at'] ) > self::MAX_AGE ) { |
| 126 |
self::clear(); |
| 127 |
|
| 128 |
return []; |
| 129 |
} |
| 130 |
|
| 131 |
return $report; |
| 132 |
} |
| 133 |
|
| 134 |
public static function clear() { |
| 135 |
delete_option( self::OPTION ); |
| 136 |
} |
| 137 |
|
| 138 |
/** |
| 139 |
* Map a block name to the plugin the user has to update. |
| 140 |
* |
| 141 |
* Block namespaces are not plugin slugs in general, so only the ones we can state with |
| 142 |
* confidence are mapped; anything else is reported by its namespace rather than guessed at, |
| 143 |
* because telling someone to update the wrong plugin is worse than naming none. |
| 144 |
*/ |
| 145 |
private static function plugin_slug_for_block( string $block_name ): string { |
| 146 |
if ( '' === $block_name || false === strpos( $block_name, '/' ) ) { |
| 147 |
return ''; |
| 148 |
} |
| 149 |
|
| 150 |
list( $namespace ) = explode( '/', $block_name, 2 ); |
| 151 |
|
| 152 |
$known = [ |
| 153 |
'essential-blocks' => 'essential-blocks', |
| 154 |
'core' => '', |
| 155 |
]; |
| 156 |
|
| 157 |
return array_key_exists( $namespace, $known ) ? $known[ $namespace ] : $namespace; |
| 158 |
} |
| 159 |
} |
| 160 |
|