PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.3
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.3
28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 19.1 All 128 releases
wordpress-seo / admin / class-my-yoast-proxy.php

class-my-yoast-proxy.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.3, at admin/class-my-yoast-proxy.php

219 lines 6.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WPSEO plugin file.
4 *
5 * @package WPSEO\Admin
6 */
7
8 /**
9 * Loads the MyYoast proxy.
10 *
11 * This class registers a proxy page on `admin.php`. Which is reached with the `page=PAGE_IDENTIFIER` parameter.
12 * It will read external files and serves them like they are located locally.
13 */
14 class WPSEO_MyYoast_Proxy implements WPSEO_WordPress_Integration {
15
16 /**
17 * The page identifier used in WordPress to register the MyYoast proxy page.
18 *
19 * @var string
20 */
21 public const PAGE_IDENTIFIER = 'wpseo_myyoast_proxy';
22
23 /**
24 * The cache control's max age. Used in the header of a successful proxy response.
25 *
26 * @var int
27 */
28 public const CACHE_CONTROL_MAX_AGE = DAY_IN_SECONDS;
29
30 /**
31 * Registers the hooks when the user is on the right page.
32 *
33 * @codeCoverageIgnore
34 *
35 * @return void
36 */
37 public function register_hooks() {
38 if ( ! $this->is_proxy_page() ) {
39 return;
40 }
41
42 // Register the page for the proxy.
43 add_action( 'admin_menu', [ $this, 'add_proxy_page' ] );
44 add_action( 'admin_init', [ $this, 'handle_proxy_page' ] );
45 }
46
47 /**
48 * Registers the proxy page. It does not actually add a link to the dashboard.
49 *
50 * @codeCoverageIgnore
51 *
52 * @return void
53 */
54 public function add_proxy_page() {
55 add_dashboard_page( '', '', 'read', self::PAGE_IDENTIFIER, '' );
56 }
57
58 /**
59 * Renders the requested proxy page and exits to prevent the WordPress UI from loading.
60 *
61 * @codeCoverageIgnore
62 *
63 * @return void
64 */
65 public function handle_proxy_page() {
66 $this->render_proxy_page();
67
68 // Prevent the WordPress UI from loading.
69 exit();
70 }
71
72 /**
73 * Renders the requested proxy page.
74 *
75 * This is separated from the exits to be able to test it.
76 *
77 * @return void
78 */
79 public function render_proxy_page() {
80 $proxy_options = $this->determine_proxy_options();
81 if ( $proxy_options === [] ) {
82 // Do not accept any other file than implemented.
83 $this->set_header( 'HTTP/1.0 501 Requested file not implemented' );
84 return;
85 }
86
87 // Set the headers before serving the remote file.
88 $this->set_header( 'Content-Type: ' . $proxy_options['content_type'] );
89 $this->set_header( 'Cache-Control: max-age=' . self::CACHE_CONTROL_MAX_AGE );
90
91 try {
92 echo $this->get_remote_url_body( $proxy_options['url'] );
93 } catch ( Exception $e ) {
94 /*
95 * Reset the file headers because the loading failed.
96 *
97 * Note: Due to supporting PHP 5.2 `header_remove` can not be used here.
98 * Overwrite the headers instead.
99 */
100 $this->set_header( 'Content-Type: text/plain' );
101 $this->set_header( 'Cache-Control: max-age=0' );
102
103 $this->set_header( 'HTTP/1.0 500 ' . $e->getMessage() );
104 }
105 }
106
107 /**
108 * Tries to load the given url via `wp_remote_get`.
109 *
110 * @codeCoverageIgnore
111 *
112 * @param string $url The url to load.
113 *
114 * @return string The body of the response.
115 *
116 * @throws Exception When `wp_remote_get` returned an error.
117 * @throws Exception When the response code is not 200.
118 */
119 protected function get_remote_url_body( $url ) {
120 $response = wp_remote_get( $url );
121
122 if ( $response instanceof WP_Error ) {
123 throw new Exception( 'Unable to retrieve file from MyYoast' );
124 }
125
126 if ( wp_remote_retrieve_response_code( $response ) !== 200 ) {
127 throw new Exception( 'Received unexpected response from MyYoast' );
128 }
129
130 return wp_remote_retrieve_body( $response );
131 }
132
133 /**
134 * Determines the proxy options based on the file and plugin version arguments.
135 *
136 * When the file is known it returns an array like this:
137 * <code>
138 * $array = array(
139 * 'content_type' => 'the content type'
140 * 'url' => 'the url, possibly with the plugin version'
141 * )
142 * </code>
143 *
144 * @return array Empty for an unknown file. See format above for known files.
145 */
146 protected function determine_proxy_options() {
147 if ( $this->get_proxy_file() === 'research-webworker' ) {
148 return [
149 'content_type' => 'text/javascript; charset=UTF-8',
150 'url' => 'https://my.yoast.com/api/downloads/file/analysis-worker?plugin_version=' . $this->get_plugin_version(),
151 ];
152 }
153
154 return [];
155 }
156
157 /**
158 * Checks if the current page is the MyYoast proxy page.
159 *
160 * @codeCoverageIgnore
161 *
162 * @return bool True when the page request parameter equals the proxy page.
163 */
164 protected function is_proxy_page() {
165 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
166 $page = isset( $_GET['page'] ) && is_string( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
167 return $page === self::PAGE_IDENTIFIER;
168 }
169
170 /**
171 * Returns the proxy file from the HTTP request parameters.
172 *
173 * @codeCoverageIgnore
174 *
175 * @return string The sanitized file request parameter or an empty string if it does not exist.
176 */
177 protected function get_proxy_file() {
178 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
179 if ( isset( $_GET['file'] ) && is_string( $_GET['file'] ) ) {
180 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
181 return sanitize_text_field( wp_unslash( $_GET['file'] ) );
182 }
183 return '';
184 }
185
186 /**
187 * Returns the plugin version from the HTTP request parameters.
188 *
189 * @codeCoverageIgnore
190 *
191 * @return string The sanitized plugin_version request parameter or an empty string if it does not exist.
192 */
193 protected function get_plugin_version() {
194 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
195 if ( isset( $_GET['plugin_version'] ) && is_string( $_GET['plugin_version'] ) ) {
196 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
197 $plugin_version = sanitize_text_field( wp_unslash( $_GET['plugin_version'] ) );
198 // Replace slashes to secure against requiring a file from another path.
199 return str_replace( [ '/', '\\' ], '_', $plugin_version );
200 }
201 return '';
202 }
203
204 /**
205 * Sets the HTTP header.
206 *
207 * This is a tiny helper function to enable better testing.
208 *
209 * @codeCoverageIgnore
210 *
211 * @param string $header The header to set.
212 *
213 * @return void
214 */
215 protected function set_header( $header ) {
216 header( $header );
217 }
218 }
219