-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathAbstract_REST_Controller.php
More file actions
179 lines (153 loc) · 5.65 KB
/
Copy pathAbstract_REST_Controller.php
File metadata and controls
179 lines (153 loc) · 5.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
<?php
/**
* Base REST controller class.
*
* Includes the shared namespace, version and hook registration.
*
* @package OneSearch\Modules\Rest
*/
declare( strict_types = 1 );
namespace OneSearch\Modules\Rest;
use OneSearch\Contracts\Interfaces\Registrable;
use OneSearch\Modules\Settings\Settings;
use OneSearch\Utils;
use WP_REST_Controller;
/**
* Class - Abstract_REST_Controller
*/
abstract class Abstract_REST_Controller extends WP_REST_Controller implements Registrable {
/**
* The namespace for the REST API.
*/
public const NAMESPACE = 'onesearch/v1';
/**
* {@inheritDoc}
*
* Reuses the namespace constant.
*
* @var string
*/
protected $namespace = self::NAMESPACE;
/**
* {@inheritDoc}
*/
public function register_hooks(): void {
add_action( 'rest_api_init', [ $this, 'register_routes' ] );
}
/**
* {@inheritDoc}
*
* We throw an exception here to force the child class to implement this method.
*
* @throws \Exception If method not implemented.
*
* @codeCoverageIgnore
*/
public function register_routes(): void {
throw new \Exception( __FUNCTION__ . ' Method not implemented.' );
}
/**
* Checks for the use of the OneDesign API key in the request headers.
*
* @todo this should be on a hook.
*
* @param \WP_REST_Request<array{}> $request Request.
*/
public function check_api_permissions( $request ): bool {
$request_origin = $request->get_header( 'origin' );
$request_origin = ! empty( $request_origin ) ? esc_url_raw( wp_unslash( $request_origin ) ) : '';
$parsed_origin = wp_parse_url( $request_origin );
$parsed_origin = is_array( $parsed_origin ) ? $parsed_origin : [];
$request_url = ! empty( $parsed_origin['scheme'] ) && ! empty( $parsed_origin['host'] )
? Utils::normalize_url( $request_origin )
: '';
$origin_port = isset( $parsed_origin['port'] ) ? (int) $parsed_origin['port'] : null;
// See if the `X-OneSearch-Token` header is present.
// Token-based auth takes priority so that cross-site requests from sub-directory
// multisite installs (where Origin loses the path and same-host detection fires
// on sibling sub-sites) are validated by key rather than by logged-in user.
$token = $request->get_header( 'X-OneSearch-Token' );
$token = ! empty( $token ) ? sanitize_text_field( wp_unslash( $token ) ) : '';
if ( ! empty( $token ) ) {
// When Origin is absent (same-origin browser requests in sub-directory multisite,
// where the browser omits Origin for same-host fetches), fall back to the
// explicitly-sent site URL header so token auth can still proceed.
if ( empty( $request_url ) ) {
$site_url_header = $request->get_header( 'X-OneSearch-Site-URL' );
if ( ! empty( $site_url_header ) ) {
$request_origin = esc_url_raw( wp_unslash( $site_url_header ) );
$parsed_origin = wp_parse_url( $request_origin );
$parsed_origin = is_array( $parsed_origin ) ? $parsed_origin : [];
$request_url = ! empty( $parsed_origin['scheme'] ) && ! empty( $parsed_origin['host'] )
? Utils::normalize_url( $request_origin )
: '';
$origin_port = isset( $parsed_origin['port'] ) ? (int) $parsed_origin['port'] : null;
}
}
if ( empty( $request_url ) ) {
return false;
}
$stored_key = $this->get_stored_api_key( Utils::normalize_url( $request_url ) );
if ( empty( $stored_key ) || ! hash_equals( $stored_key, $token ) ) {
return false;
}
// Governing sites were checked by ::get_stored_api_key already.
if ( Settings::is_governing_site() ) {
return true;
}
// If it's not a healthcheck, compare the origins.
$governing_site_url = Settings::get_parent_site_url();
if ( '/' . $this->namespace . '/health-check' !== $request->get_route() ) {
return ! empty( $governing_site_url ) ? $this->is_url_from_host( $governing_site_url, $parsed_origin['host'], $origin_port ) : false;
}
// For health-checks, if no governing site is set, we set it now.
Settings::set_parent_site_url( $request_origin );
return true;
}
// No token: fall back to same-domain logged-in user check.
if ( empty( $request_url ) || $this->is_url_from_host( get_site_url(), $parsed_origin['host'], $origin_port ) ) {
return current_user_can( 'manage_options' );
}
return false;
}
/**
* Check if two URLs belong to the same host.
*
* @param string $url The URL to check.
* @param string $host The host to compare against.
* @param int|null $port Optional. The port to compare against.
*
* @return bool True if both URLs belong to the same host (and port if specified), false otherwise.
*/
protected function is_url_from_host( string $url, string $host, ?int $port = null ): bool {
$parsed_url = wp_parse_url( $url );
// Compare both host and port to properly handle localhost with different ports.
if ( ! isset( $parsed_url['host'] ) || $parsed_url['host'] !== $host ) {
return false;
}
// If a port was provided, also compare ports.
if ( null !== $port ) {
$url_port = $parsed_url['port'] ?? 80;
return $url_port === $port;
}
return true;
}
/**
* Gets the locally-stored API key for comparison.
*
* @param ?string $site_url Site URL. Only used for child->governing site requests.
*
* @return string The stored API key. Empty string if not found.
*/
private function get_stored_api_key( ?string $site_url = null ): string {
if ( Settings::is_consumer_site() ) {
return Settings::get_api_key();
}
// If there's no child site URL we cannot match the API key.
if ( ! isset( $site_url ) ) {
return '';
}
$shared_sites = Settings::get_shared_sites();
return ! empty( $shared_sites[ $site_url ]['api_key'] ) ? $shared_sites[ $site_url ]['api_key'] : '';
}
}