PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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
extendify / app / ComingSoon / Frontend.php

Frontend.php in Extendify 3.2.1, at app/ComingSoon/Frontend.php

268 lines 7.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Coming Soon page for a site the owner hasn't published yet.
5 */
6
7 namespace Extendify\ComingSoon;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 /**
12 * This class serves the Coming Soon page in place of any front-end request.
13 *
14 * Only loaded while the site is hidden, so nothing here rechecks it.
15 */
16
17 class Frontend
18 {
19 /**
20 * REST namespaces that stay open while the site is hidden
21 *
22 * Empty on purpose: a hidden site answers nothing.
23 *
24 * @var array
25 */
26 // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound -- 7.0 floor: no const visibility
27 const OPEN_NAMESPACES = [];
28
29 /**
30 * Index fields a hidden site still exposes, as an allow list because core and plugins keep adding more
31 *
32 * @var array
33 */
34 // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound -- 7.0 floor: no const visibility
35 const OPEN_INDEX_FIELDS = ['url', 'home', 'authentication'];
36
37 /**
38 * Entry points that load WordPress without ever reaching the template or the REST server
39 *
40 * @var array
41 */
42 // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound -- 7.0 floor: no const visibility
43 const CLOSED_SCRIPTS = ['wp-comments-post.php', 'wp-trackback.php'];
44
45 /**
46 * Adds various actions to set up the page
47 *
48 * @return void
49 */
50 public function __construct()
51 {
52 // Feeds and robots.txt reach template_redirect before core dispatches them.
53 \add_action('template_redirect', [$this, 'maybeRender']);
54 \add_action('admin_init', [$this, 'maybeBlockAjax']);
55 \add_filter('rest_authentication_errors', [$this, 'guardServedRest']);
56 \add_filter('rest_index', [$this, 'maybeTrimRestIndex']);
57 \add_filter('wp_sitemaps_is_enabled', '__return_false');
58 \add_filter('xmlrpc_enabled', '__return_false');
59 // xmlrpc_enabled misses pingback.ping, which reads a post without logging in.
60 \add_filter('xmlrpc_methods', '__return_empty_array');
61 \add_action('wp_loaded', [$this, 'maybeBlockClosedScripts']);
62 }
63
64 /**
65 * Arms the REST gate for requests that arrived over HTTP.
66 *
67 * Core only runs this filter from serve_request, so plugins dispatching
68 * internally keep working while the site is hidden.
69 *
70 * @param \WP_Error|null|true $errors Authentication errors so far.
71 * @return \WP_Error|null|true
72 */
73 public function guardServedRest($errors)
74 {
75 \add_filter('rest_pre_dispatch', [$this, 'maybeBlockRest'], 10, 3);
76
77 return $errors;
78 }
79
80 /**
81 * Keeps site content out of the REST API while the site is hidden.
82 *
83 * @param mixed $result The response to send instead of dispatching.
84 * @param \WP_REST_Server $server The server handling the request.
85 * @param \WP_REST_Request $request The request being dispatched.
86 * @return mixed
87 */
88 public function maybeBlockRest($result, $server, $request)
89 {
90 if ($this->isOpenRoute($request->get_route())) {
91 return $result;
92 }
93
94 return new \WP_Error(
95 'extendify_site_unpublished',
96 \__('This site is not ready for visitors yet.', 'extendify-local'),
97 ['status' => 503]
98 );
99 }
100
101 /**
102 * Whether a REST route still answers while the site is hidden.
103 *
104 * @param string $route The route being dispatched.
105 * @return boolean
106 */
107 private function isOpenRoute($route)
108 {
109 $route = trim($route, '/');
110
111 if ($route === '') {
112 return true;
113 }
114
115 foreach (self::OPEN_NAMESPACES as $namespace) {
116 if ($route === $namespace || strpos($route, $namespace . '/') === 0) {
117 return true;
118 }
119 }
120
121 return false;
122 }
123
124 /**
125 * Keeps the index from naming the site or inventorying its plugins.
126 *
127 * @param \WP_REST_Response $response The index response.
128 * @return \WP_REST_Response
129 */
130 public function maybeTrimRestIndex($response)
131 {
132 $response->set_data(array_intersect_key(
133 $response->get_data(),
134 array_flip(self::OPEN_INDEX_FIELDS)
135 ));
136
137 // The site logo returns through _links even when its field is stripped.
138 foreach (array_keys($response->get_links()) as $relation) {
139 $response->remove_link($relation);
140 }
141
142 return $response;
143 }
144
145 /**
146 * Keeps site content out of admin-ajax.php while the site is hidden.
147 *
148 * Other plugins register wp_ajax_nopriv_ handlers that read content, so the
149 * gate has to sit in front of the dispatch rather than on any one action.
150 *
151 * @return void
152 */
153 public function maybeBlockAjax()
154 {
155 if (!\wp_doing_ajax()) {
156 return;
157 }
158
159 \nocache_headers();
160 \wp_send_json_error(
161 [
162 'code' => 'extendify_site_unpublished',
163 'message' => \__('This site is not ready for visitors yet.', 'extendify-local'),
164 ],
165 503
166 );
167 }
168
169 /**
170 * Turns away the comment and trackback endpoints while the site is hidden.
171 *
172 * wp_loaded runs before either file looks up the post.
173 *
174 * @return void
175 */
176 public function maybeBlockClosedScripts()
177 {
178 if (!in_array($this->currentScript(), self::CLOSED_SCRIPTS, true)) {
179 return;
180 }
181
182 \nocache_headers();
183 \wp_die(
184 \esc_html__('This site is not ready for visitors yet.', 'extendify-local'),
185 \esc_html__('Coming soon', 'extendify-local'),
186 ['response' => 503]
187 );
188 }
189
190 /**
191 * The PHP file handling this request.
192 *
193 * @return string
194 */
195 private function currentScript()
196 {
197 if (!isset($_SERVER['SCRIPT_NAME'])) {
198 return '';
199 }
200
201 return basename(\sanitize_text_field(\wp_unslash($_SERVER['SCRIPT_NAME'])));
202 }
203
204 /**
205 * Serves the Coming Soon page instead of whatever was requested.
206 *
207 * @return void
208 */
209 public function maybeRender()
210 {
211 // 503 keeps search engines from indexing the placeholder as the site.
212 \status_header(503);
213 \nocache_headers();
214 $this->pageContent();
215 exit;
216 }
217
218 /**
219 * Coming Soon page output
220 *
221 * @return void
222 */
223 private function pageContent()
224 {
225 $title = \__('Coming soon', 'extendify-local');
226 $message = \__('This site is not ready for visitors yet. Please check back soon.', 'extendify-local');
227 ?>
228 <!DOCTYPE html>
229 <html <?php \language_attributes(); ?>>
230 <head>
231 <meta charset="<?php echo \esc_attr(\get_bloginfo('charset')); ?>">
232 <meta name="viewport" content="width=device-width, initial-scale=1">
233 <meta name="robots" content="noindex, nofollow">
234 <title><?php echo \esc_html($title); ?></title>
235 <style>
236 body {
237 align-items: center;
238 background: #fff;
239 color: #1e1e1e;
240 display: flex;
241 font-family: system-ui, sans-serif;
242 justify-content: center;
243 margin: 0;
244 min-height: 100vh;
245 padding: 1.5rem;
246 text-align: center;
247 }
248 h1 {
249 font-size: 1.75rem;
250 margin: 0 0 0.5rem;
251 }
252 p {
253 margin: 0;
254 opacity: 0.7;
255 }
256 </style>
257 </head>
258 <body>
259 <main data-test="extendify-coming-soon">
260 <h1><?php echo \esc_html($title); ?></h1>
261 <p><?php echo \esc_html($message); ?></p>
262 </main>
263 </body>
264 </html>
265 <?php
266 }
267 }
268