|
| 1 | +<?php |
| 2 | +/** Immutable shared-resource plan storage for resumable artifact runs. @package StaticSiteImporter */ |
| 3 | +if ( ! defined( 'ABSPATH' ) ) { |
| 4 | + exit; |
| 5 | +} |
| 6 | + |
| 7 | +/** |
| 8 | + * Retains source-agnostic, non-page artifact resources shared by a site run. |
| 9 | + * Prepared batches can only be reused when they name this verified digest. |
| 10 | + */ |
| 11 | +final class Static_Site_Importer_Shared_Resource_Plan { |
| 12 | + private const SCHEMA = 'static-site-importer/shared-resource-plan/v1'; |
| 13 | + private Static_Site_Importer_Artifact_Run_Workspace $workspace; |
| 14 | + |
| 15 | + public function __construct( Static_Site_Importer_Artifact_Run_Workspace $workspace ) { |
| 16 | + $this->workspace = $workspace; |
| 17 | + } |
| 18 | + |
| 19 | + /** @return array<string,mixed>|null */ |
| 20 | + public function load(): ?array { |
| 21 | + $raw = $this->workspace->read_raw( 'shared-resource-plan.json' ); |
| 22 | + $plan = is_string( $raw ) ? json_decode( $raw, true ) : null; |
| 23 | + if ( ! is_array( $plan ) || self::SCHEMA !== ( $plan['schema'] ?? null ) || ! is_array( $plan['resources'] ?? null ) ) { |
| 24 | + return null; |
| 25 | + } |
| 26 | + return hash_equals( (string) ( $plan['digest'] ?? '' ), self::digest( $plan['resources'] ) ) ? $plan : null; |
| 27 | + } |
| 28 | + |
| 29 | + /** @return array<string,mixed>|WP_Error */ |
| 30 | + public function establish( array $artifact, ?array $paths = null ): array|WP_Error { |
| 31 | + $resources = self::resources( $artifact, $paths ); |
| 32 | + $plan = array( |
| 33 | + 'schema' => self::SCHEMA, |
| 34 | + 'digest' => self::digest( $resources ), |
| 35 | + 'resources' => $resources, |
| 36 | + 'verified' => true, |
| 37 | + 'created_at' => gmdate( 'c' ), |
| 38 | + ); |
| 39 | + $stored = $this->workspace->publish_json( 'shared-resource-plan.json', $plan ); |
| 40 | + if ( is_wp_error( $stored ) ) { |
| 41 | + return $stored; |
| 42 | + } |
| 43 | + $verified = $this->load(); |
| 44 | + return is_array( $verified ) && $verified['digest'] === $plan['digest'] ? $verified : new WP_Error( 'static_site_importer_shared_plan_verification_failed', 'The retained shared resource plan could not be verified.' ); |
| 45 | + } |
| 46 | + |
| 47 | + /** @return array{digest:string,changed:bool,plan:array<string,mixed>|WP_Error} */ |
| 48 | + public function reconcile( array $artifact ): array { |
| 49 | + $existing = $this->load(); |
| 50 | + if ( ! is_array( $existing ) ) { |
| 51 | + $plan = $this->establish( $artifact ); |
| 52 | + return array( |
| 53 | + 'digest' => is_array( $plan ) ? $plan['digest'] : '', |
| 54 | + 'changed' => false, |
| 55 | + 'plan' => $plan, |
| 56 | + ); |
| 57 | + } |
| 58 | + $current = self::resources( $artifact, array_column( $existing['resources'], 'path' ) ); |
| 59 | + $digest = self::digest( $current ); |
| 60 | + if ( hash_equals( $existing['digest'], $digest ) ) { |
| 61 | + return array( |
| 62 | + 'digest' => $digest, |
| 63 | + 'changed' => false, |
| 64 | + 'plan' => $existing, |
| 65 | + ); |
| 66 | + } |
| 67 | + $plan = $this->establish( $artifact, array_column( $existing['resources'], 'path' ) ); |
| 68 | + return array( |
| 69 | + 'digest' => is_array( $plan ) ? $plan['digest'] : '', |
| 70 | + 'changed' => true, |
| 71 | + 'plan' => $plan, |
| 72 | + ); |
| 73 | + } |
| 74 | + |
| 75 | + /** @return array<int,array<string,mixed>> */ |
| 76 | + private static function resources( array $artifact, ?array $paths = null ): array { |
| 77 | + $resources = array(); |
| 78 | + foreach ( $artifact['files'] ?? array() as $file ) { |
| 79 | + if ( ! is_array( $file ) || 'text/html' === strtolower( (string) ( $file['mime_type'] ?? '' ) ) ) { |
| 80 | + continue; |
| 81 | + } |
| 82 | + $path = (string) ( $file['path'] ?? '' ); |
| 83 | + if ( '' === $path || ( null !== $paths && ! in_array( $path, $paths, true ) ) ) { |
| 84 | + continue; |
| 85 | + } |
| 86 | + $resources[] = array_filter( |
| 87 | + array( |
| 88 | + 'path' => $path, |
| 89 | + 'mime_type' => (string) ( $file['mime_type'] ?? '' ), |
| 90 | + 'content' => $file['content'] ?? null, |
| 91 | + 'content_base64' => $file['content_base64'] ?? null, |
| 92 | + ), |
| 93 | + static fn( $value ): bool => null !== $value |
| 94 | + ); |
| 95 | + } |
| 96 | + usort( $resources, static fn( array $left, array $right ): int => strcmp( $left['path'], $right['path'] ) ); |
| 97 | + return $resources; |
| 98 | + } |
| 99 | + |
| 100 | + private static function digest( array $resources ): string { |
| 101 | + $json = wp_json_encode( $resources, JSON_UNESCAPED_SLASHES ); |
| 102 | + return hash( 'sha256', false === $json ? '' : $json ); |
| 103 | + } |
| 104 | +} |
0 commit comments