PluginProbe
WindPress – Tailwind CSS integration for WordPress / 3.2.89
WindPress – Tailwind CSS integration for WordPress v3.2.89
3.2.89 3.2.88 3.2.87 3.2.86 3.2.85 3.2.84 3.2.83 3.2.82 3.2.81 trunk 3.0.0 3.0.1 3.0.10 3.0.11 3.0.12 3.0.13 3.0.14 3.0.15 3.0.16 3.0.17 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 All 143 releases
windpress / src / Core / Scanner / SourceIndex.php

SourceIndex.php in WindPress – Tailwind CSS integration for WordPress 3.2.89, at src/Core/Scanner/SourceIndex.php

173 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare (strict_types=1);
4 namespace WindPress\WindPress\Core\Scanner;
5
6 use InvalidArgumentException;
7 use RuntimeException;
8 use WIND_PRESS;
9 /**
10 * Immutable source manifests and resumable, deletion-aware deltas.
11 */
12 class SourceIndex
13 {
14 private const JOB_TTL = 900;
15 private const MANIFEST_TTL = 7 * 86400;
16 private const MAX_MANIFEST_BYTES = 2 * 1024 * 1024;
17 private const MAX_MANIFESTS = 8;
18 public static function scan(string $scope, array $metadata, callable $fetch): array
19 {
20 $request = $metadata['source_index'] ?? null;
21 if (!is_array($request) || ($request['version'] ?? null) !== 1 || !array_key_exists('baseline', $request) || $request['baseline'] !== null && (!is_string($request['baseline']) || !preg_match('/^[a-f0-9]{64}$/D', $request['baseline'])) || !in_array($metadata['kind'] ?? 'incremental', ['full', 'incremental'], \true)) {
22 throw new InvalidArgumentException(__('The source index request is invalid.', 'windpress'));
23 }
24 $scope = hash('sha256', serialize([1, WIND_PRESS::VERSION, get_current_blog_id(), get_current_user_id(), get_locale(), $scope, apply_filters('f!windpress/core/scanner/source_index:scope', '', $scope)]));
25 $cursor = $metadata['next_batch'] ?? \false;
26 $context = $metadata;
27 unset($context['next_batch']);
28 ksort($context);
29 $context = hash('sha256', serialize($context));
30 $page = 0;
31 $token = bin2hex(random_bytes(16));
32 if ($cursor !== \false && $cursor !== null) {
33 if (!is_string($cursor) || !preg_match('/^idx_([a-f0-9]{32})_([0-9]+)$/D', $cursor, $matches)) {
34 throw new InvalidArgumentException(__('The source index cursor is invalid.', 'windpress'));
35 }
36 $token = $matches[1];
37 $page = (int) $matches[2];
38 }
39 $key = 'windpress_source_job_' . $token;
40 $lease = \WindPress\WindPress\Core\Scanner\ScanLock::acquire($key);
41 if ($lease === null) {
42 throw new RuntimeException(__('This source scan is already processing. Retry the same cursor.', 'windpress'), 409);
43 }
44 try {
45 if ($cursor === \false || $cursor === null) {
46 $baseline = ($metadata['kind'] ?? 'incremental') === 'full' ? null : $request['baseline'];
47 $manifest = $baseline === null ? \false : get_transient(self::manifest_key($scope, $baseline));
48 if (!is_array($manifest) || ($manifest['scope'] ?? null) !== $scope || !is_array($manifest['sources'] ?? null)) {
49 $baseline = null;
50 $manifest = ['sources' => []];
51 }
52 $state = ['scope' => $scope, 'context' => $context, 'page' => 0, 'cursor' => \false, 'baseline' => $baseline, 'previous' => $manifest['sources'], 'current' => [], 'cursors' => []];
53 } else {
54 $state = get_transient($key);
55 if (!is_array($state) || ($state['scope'] ?? null) !== $scope || ($state['context'] ?? null) !== $context) {
56 throw new RuntimeException(__('The source scan expired or its context changed. Restart the build.', 'windpress'), 409);
57 }
58 if (($state['last_page'] ?? null) === $page) {
59 return $state['last_response'];
60 }
61 if (($state['page'] ?? null) !== $page || ($state['cursor'] ?? \false) === \false) {
62 throw new RuntimeException(__('The source cursor is out of sequence. Restart the build.', 'windpress'), 409);
63 }
64 }
65 $provider_metadata = $metadata;
66 $provider_metadata['next_batch'] = $state['cursor'];
67 unset($provider_metadata['source_index']);
68 $result = $fetch($provider_metadata);
69 if (!is_array($result) || !is_array($result['contents'] ?? null) || !is_array($result['metadata'] ?? null)) {
70 throw new RuntimeException(__('The indexed provider returned an invalid response.', 'windpress'));
71 }
72 $contents = [];
73 $examined = 0;
74 foreach ($result['contents'] as $source) {
75 if (!is_array($source) || !is_string($source['source_id'] ?? null) || $source['source_id'] === '' || strlen($source['source_id']) > 2048 || !is_string($source['content'] ?? null) || isset($source['type']) && !is_string($source['type'])) {
76 throw new RuntimeException(__('An indexed source requires a stable ID and text content.', 'windpress'));
77 }
78 $id = $source['source_id'];
79 if (isset($state['current'][$id])) {
80 throw new RuntimeException(__('The provider returned a duplicate source ID: ', 'windpress') . $id);
81 }
82 $hash = hash('sha256', serialize([$source['content'], $source['type'] ?? null]));
83 $state['current'][$id] = $hash;
84 $examined++;
85 if (($state['previous'][$id] ?? null) !== $hash) {
86 $source['source_hash'] = $hash;
87 $contents[] = $source;
88 }
89 }
90 $next = $result['metadata']['next_batch'] ?? \false;
91 if ($next !== \false && (!is_string($next) || $next === '') && (!is_int($next) || $next < 1)) {
92 throw new RuntimeException(__('The indexed provider returned an invalid cursor.', 'windpress'));
93 }
94 if ($next !== \false) {
95 $cursor_key = hash('sha256', serialize($next));
96 if (isset($state['cursors'][$cursor_key])) {
97 throw new RuntimeException(__('The indexed provider repeated a cursor.', 'windpress'));
98 }
99 $state['cursors'][$cursor_key] = \true;
100 }
101 $revision = null;
102 $deleted = [];
103 if ($next === \false) {
104 ksort($state['current'], \SORT_STRING);
105 $revision = hash('sha256', serialize([$scope, $state['current']]));
106 $deleted = array_map('strval', array_keys(array_diff_key($state['previous'], $state['current'])));
107 }
108 $result['contents'] = $contents;
109 $result['metadata']['next_batch'] = $next === \false ? \false : 'idx_' . $token . '_' . ($page + 1);
110 $result['metadata']['source_index'] = ['version' => 1, 'mode' => $state['baseline'] === null ? 'snapshot' : 'delta', 'scope' => $scope, 'base_revision' => $state['baseline'], 'revision' => $revision, 'deleted' => $deleted, 'examined' => $examined, 'changed' => count($contents), 'unchanged' => $examined - count($contents)];
111 if (!\WindPress\WindPress\Core\Scanner\ScanLock::is_owner($key, $lease)) {
112 throw new RuntimeException(__('The source scan lease expired. Restart the build.', 'windpress'), 409);
113 }
114 if ($revision !== null) {
115 self::save_manifest($scope, $revision, $state['current']);
116 unset($state['previous'], $state['current'], $state['cursors']);
117 }
118 $state['cursor'] = $next;
119 $state['page'] = $page + 1;
120 $state['last_page'] = $page;
121 $state['last_response'] = $result;
122 // A single-page scan has no continuation to replay.
123 if ($next !== \false || $page > 0) {
124 self::persist($key, $state, self::JOB_TTL);
125 }
126 return $result;
127 } finally {
128 \WindPress\WindPress\Core\Scanner\ScanLock::release($key, $lease);
129 }
130 }
131 private static function manifest_key(string $scope, string $revision): string
132 {
133 return 'windpress_source_manifest_' . hash('sha256', $scope . $revision);
134 }
135 private static function save_manifest(string $scope, string $revision, array $sources): void
136 {
137 $manifest = ['scope' => $scope, 'sources' => $sources];
138 // Oversized sites can still build; their next request falls back to a snapshot.
139 if (strlen(serialize($manifest)) > self::MAX_MANIFEST_BYTES) {
140 return;
141 }
142 $registry_key = 'windpress_source_history_' . $scope;
143 $lease = \WindPress\WindPress\Core\Scanner\ScanLock::acquire($registry_key);
144 if ($lease === null) {
145 throw new RuntimeException(__('Another scan is saving its source manifest. Retry the same cursor.', 'windpress'), 409);
146 }
147 try {
148 $history = get_transient($registry_key);
149 $history = is_array($history) ? $history : [];
150 unset($history[$revision]);
151 $history[$revision] = \true;
152 $expired = [];
153 while (count($history) > self::MAX_MANIFESTS) {
154 $expired[] = array_key_first($history);
155 array_shift($history);
156 }
157 self::persist(self::manifest_key($scope, $revision), $manifest, self::MANIFEST_TTL);
158 self::persist($registry_key, $history, self::MANIFEST_TTL);
159 foreach ($expired as $old_revision) {
160 delete_transient(self::manifest_key($scope, $old_revision));
161 }
162 } finally {
163 \WindPress\WindPress\Core\Scanner\ScanLock::release($registry_key, $lease);
164 }
165 }
166 private static function persist(string $key, array $value, int $ttl): void
167 {
168 if (!set_transient($key, $value, $ttl) && get_transient($key) !== $value) {
169 throw new RuntimeException(__('Unable to save the source index. Restart the build.', 'windpress'));
170 }
171 }
172 }
173