PluginProbe
FlyWP Helper – Page Cache, Page Optimization, Emails for FlyWP Server Control Panel / 1.8.0
FlyWP Helper – Page Cache, Page Optimization, Emails for FlyWP Server Control Panel v1.8.0
1.8.0 1.7.1 1.7.0 1.6.0 1.5.2 trunk 0.1 0.2.0 0.2.1 0.3 0.3.1 0.3.2 0.3.3 0.3.4 0.4 0.4.1 0.4.2 0.4.3 1.0 1.1 1.2 1.3.1 1.4.0 1.4.1 1.5.0 All 27 releases
flywp / includes / StaticSite / Engine / Cli / SiteCommand.php

SiteCommand.php in FlyWP Helper – Page Cache, Page Optimization, Emails for FlyWP Server Control Panel 1.8.0, at includes/StaticSite/Engine/Cli/SiteCommand.php

265 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:disable WordPress.WP.AlternativeFunctions -- WP-CLI command: STDERR and one local output file, no WP_Filesystem.
3
4 namespace FlyWP\StaticSite\Engine\Cli;
5
6 use FlyWP\StaticSite\Bootstrap;
7 use FlyWP\StaticSite\Engine\Discovery\RedirectSourceUrls;
8 use FlyWP\StaticSite\Engine\Discovery\Url;
9 use FlyWP\StaticSite\Engine\Discovery\UrlDiscovery;
10 use FlyWP\StaticSite\Engine\Discovery\WordPressGateway;
11 use FlyWP\StaticSite\Engine\Discovery\WpGateway;
12 use FlyWP\StaticSite\Engine\Incremental\ChangeSetBuilder;
13 use FlyWP\StaticSite\Engine\Incremental\ChangeStore;
14 use FlyWP\StaticSite\Engine\Incremental\WpChangeStore;
15 use Throwable;
16
17 /**
18 * WP-CLI command `wp flywp static site`: write the site data for the FlyWP static engine (site.json, contract N1).
19 *
20 * - The engine runs outside WordPress and reads this file.
21 * - The URLs are on the origin URL that the engine fetches (--origin-url, default the home URL), as in the old engine.
22 * The change log keeps retry URLs in this form, so the origin must be the same for each build.
23 * - A URL that is not valid UTF-8 cannot be written to JSON. The command drops it and writes a warning to STDERR.
24 * - max_change_id is the highest change row that the file includes. `wp flywp static changes clear` deletes
25 * the rows up to it after the build.
26 *
27 * @since 1.8.0
28 */
29 class SiteCommand extends JsonCommand {
30
31 /**
32 * Version of the site.json format.
33 */
34 const VERSION = 1;
35
36 const MODES = [ 'full', 'incremental' ];
37
38 /**
39 * @var callable function( string $mode, string|null $origin_url ): array. The site.json data.
40 */
41 private $snapshot;
42
43 /**
44 * @var callable function( string $message ). Writes one warning.
45 */
46 private $warning;
47
48 /**
49 * @param callable|null $snapshot Data factory. self::wordpress_snapshot() when null.
50 * @param callable|null $output Line writer. STDOUT when null.
51 * @param callable|null $warning Warning writer. STDERR when null.
52 */
53 public function __construct( $snapshot = null, $output = null, $warning = null ) {
54 parent::__construct( $output );
55
56 $this->snapshot = $snapshot ? $snapshot : [ self::class, 'wordpress_snapshot' ];
57 $this->warning = $warning ? $warning : function ( $message ) {
58 fwrite( STDERR, 'Warning: ' . $message . "\n" );
59 };
60 }
61
62 /**
63 * Write the site data for a static build.
64 *
65 * ## OPTIONS
66 *
67 * [--output=<file>]
68 * : The JSON file to write. Necessary.
69 *
70 * [--mode=<mode>]
71 * : full or incremental. An incremental file also has the change set.
72 * ---
73 * default: full
74 * ---
75 *
76 * [--origin-url=<url>]
77 * : The URL that the engine fetches. The URLs in the file are on this URL. Default: the home URL.
78 *
79 * ## EXAMPLES
80 *
81 * wp flywp static site --output=/tmp/flywp-static-12.json --mode=incremental --origin-url=https://example.com
82 *
83 * @param array $args Positional arguments.
84 * @param array $assoc_args Named arguments.
85 *
86 * @return void
87 */
88 public function __invoke( $args, $assoc_args ) {
89 $this->execute( $assoc_args );
90 }
91
92 /**
93 * {@inheritdoc}
94 */
95 public function run( array $assoc_args ) {
96 $file = isset( $assoc_args['output'] ) ? (string) $assoc_args['output'] : '';
97 $mode = isset( $assoc_args['mode'] ) ? (string) $assoc_args['mode'] : 'full';
98 $origin = isset( $assoc_args['origin-url'] ) ? rtrim( (string) $assoc_args['origin-url'], '/' ) : null;
99 $site = null;
100
101 if ( $file === '' ) {
102 return $this->result( 'Set --output.' );
103 }
104
105 if ( ! in_array( $mode, self::MODES, true ) ) {
106 return $this->result( sprintf( 'Unknown mode "%s". Use full or incremental.', $mode ) );
107 }
108
109 if ( $origin !== null && ! Url::is_http( $origin ) ) {
110 return $this->result( 'The --origin-url value must be an absolute http or https URL.' );
111 }
112
113 try {
114 // Plugin hooks and filters run during discovery. Their output must not go to stdout.
115 Bootstrap::quietly(
116 function () use ( $mode, $origin, &$site ) {
117 $site = call_user_func( $this->snapshot, $mode, $origin );
118 }
119 );
120 } catch ( Throwable $e ) {
121 return $this->result( $e->getMessage() );
122 }
123
124 $json = json_encode( $this->without_invalid_urls( $site ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
125
126 if ( $json === false ) {
127 return $this->result( sprintf( 'Unable to encode the site data: %s.', json_last_error_msg() ) );
128 }
129
130 if ( file_put_contents( $file, $json ) === false ) {
131 return $this->result( sprintf( 'Unable to write %s.', $file ) );
132 }
133
134 return $this->result();
135 }
136
137 /**
138 * The site data of this WordPress site.
139 *
140 * @param string $mode full or incremental.
141 * @param string|null $origin_url Origin URL that the engine fetches, without trailing slash. Null for the home URL.
142 *
143 * @return array
144 */
145 public static function wordpress_snapshot( $mode, $origin_url = null ) {
146 $origin_url = $origin_url ? $origin_url : rtrim( home_url(), '/' );
147 $site_urls = [ home_url(), site_url() ];
148
149 return self::snapshot(
150 $mode,
151 $origin_url,
152 [
153 'plugin_version' => FLYWP_VERSION,
154 'home_url' => home_url(),
155 'site_url' => site_url(),
156 'respect_noindex' => (string) get_option( 'blog_public' ) !== '0',
157 'pretty_permalinks' => (string) get_option( 'permalink_structure' ) !== '',
158 'pagination_base' => isset( $GLOBALS['wp_rewrite']->pagination_base ) ? (string) $GLOBALS['wp_rewrite']->pagination_base : 'page',
159 'redirect_sources' => RedirectSourceUrls::all( $origin_url, $site_urls ),
160 'redirect_wildcards' => RedirectSourceUrls::wildcard_rules( $origin_url, $site_urls ),
161 ],
162 new WpGateway(),
163 new WpChangeStore()
164 );
165 }
166
167 /**
168 * The site.json data.
169 *
170 * - The URLs are on the origin URL. The discovery and the change set also accept URLs on the home URL.
171 * - The change rows are read before the URL discovery. A change during the discovery stays for the next build.
172 * - Incremental: max_change_id is the highest row of the change set, so the clear does not delete a newer row.
173 *
174 * @param string $mode full or incremental.
175 * @param string $origin_url Origin URL that the engine fetches.
176 * @param array $site Site values: plugin_version, home_url, site_url, respect_noindex,
177 * pretty_permalinks, pagination_base, redirect_sources, redirect_wildcards.
178 * @param WordPressGateway $wp Gateway.
179 * @param ChangeStore $changes Change rows.
180 *
181 * @return array
182 */
183 public static function snapshot( $mode, $origin_url, array $site, WordPressGateway $wp, ChangeStore $changes ) {
184 $discovery = new UrlDiscovery( $wp, $origin_url, UrlDiscovery::default_sources( $wp, $origin_url ) );
185 $change_set = null;
186
187 if ( $mode === 'incremental' ) {
188 $set = ( new ChangeSetBuilder( $changes, $wp, $origin_url ) )->build( 0 );
189 $max_change_id = $set->max_change_id;
190 $change_set = [
191 'full_reason' => $set->full_reason,
192 'urls' => $set->urls,
193 'deleted_urls' => $set->deleted_urls,
194 'lists' => $set->lists,
195 ];
196 } else {
197 $max_change_id = $changes->max_id();
198 }
199
200 return array_merge(
201 [ 'version' => self::VERSION ],
202 $site,
203 [
204 'urls' => iterator_to_array( $discovery->urls(), false ),
205 'excluded_urls' => iterator_to_array( $discovery->excluded_urls(), false ),
206 'max_change_id' => (int) $max_change_id,
207 'change_set' => $change_set,
208 ]
209 );
210 }
211
212 /**
213 * Remove the URLs that are not valid UTF-8, because json_encode() cannot write them.
214 *
215 * @param array $site The site.json data.
216 *
217 * @return array
218 */
219 private function without_invalid_urls( array $site ) {
220 $site['urls'] = $this->valid_items( $site['urls'], 'url' );
221 $site['excluded_urls'] = $this->valid_items( $site['excluded_urls'] );
222 $site['redirect_sources'] = $this->valid_items( $site['redirect_sources'] );
223 $site['redirect_wildcards'] = $this->valid_items( $site['redirect_wildcards'], 'from', 'to' );
224
225 if ( is_array( $site['change_set'] ) ) {
226 $site['change_set']['urls'] = $this->valid_items( $site['change_set']['urls'] );
227 $site['change_set']['deleted_urls'] = $this->valid_items( $site['change_set']['deleted_urls'] );
228 $site['change_set']['lists'] = $this->valid_items( $site['change_set']['lists'], 'url' );
229 }
230
231 return $site;
232 }
233
234 /**
235 * The items whose URLs are valid UTF-8. Writes one warning for each item that it removes.
236 *
237 * @param array $items URLs, or arrays or objects with URLs.
238 * @param string ...$keys The URL keys of an item. None when the item is the URL.
239 *
240 * @return array
241 */
242 private function valid_items( array $items, ...$keys ) {
243 $valid = [];
244
245 foreach ( $items as $item ) {
246 $urls = $keys ? array_intersect_key( (array) $item, array_flip( $keys ) ) : [ $item ];
247 $invalid = array_filter(
248 $urls,
249 function ( $url ) {
250 return is_string( $url ) && preg_match( '//u', $url ) !== 1;
251 }
252 );
253
254 if ( $invalid ) {
255 call_user_func( $this->warning, sprintf( 'Skipped a URL that is not valid UTF-8: %s', json_encode( reset( $invalid ), JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE ) ) );
256 continue;
257 }
258
259 $valid[] = $item;
260 }
261
262 return $valid;
263 }
264 }
265