-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathwp-cli-login-server.php
More file actions
368 lines (323 loc) · 10.2 KB
/
wp-cli-login-server.php
File metadata and controls
368 lines (323 loc) · 10.2 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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
<?php
/**
* Plugin Name: WP CLI Login Command Server
* Description: Companion plugin to the WP-CLI Login Command
* Author: Evan Mattson
* Author URI: https://aaemnnost.tv
* Plugin URI: https://aaemnnost.tv/wp-cli-commands/login/
*
* Version: 1.5.0
*/
namespace WP_CLI_Login;
use WP_User;
use Exception;
/**
* Fire up the server
*/
function init_server_from_request()
{
list($endpoint, $public) = WP_CLI_Login_Server::parseUri(@$_SERVER['REQUEST_URI']);
WP_CLI_Login_Server::handle($endpoint, $public);
}
if (is_eligible_request()) {
add_action('plugins_loaded', __NAMESPACE__ . '\\init_server_from_request');
}
/**
* @return bool
*/
function is_eligible_request()
{
return ! (
(defined('WP_CLI') && WP_CLI) // ignore cli requests
|| (defined('DOING_AJAX') && DOING_AJAX) // ignore ajax requests
|| (defined('DOING_CRON') && DOING_CRON) // ignore cron requests
|| (defined('WP_INSTALLING') && WP_INSTALLING) // WP ain't ready
|| empty($_SERVER['REQUEST_METHOD']) // Invalid request
|| 'GET' !== strtoupper($_SERVER['REQUEST_METHOD']) // GET requests only
|| count($_GET) > 0 // if there is any query string
|| is_admin() // ignore admin requests
);
}
class WP_CLI_Login_Server
{
/**
* The http endpoint triggering the request.
*/
private $endpoint;
/**
* Public key passed to identify the unique magic login.
* @var string
*/
private $publicKey;
/**
* Option key for the persisted-data.
*/
const OPTION = 'wp_cli_login';
/**
* WP_CLI_Login_Server constructor.
*
* @param $endpoint
* @param $publicKey
*/
public function __construct($endpoint, $publicKey)
{
$this->endpoint = $endpoint;
$this->publicKey = $publicKey;
}
/**
* Parse the endpoint & key from the URI, if any.
*
* @param $uri string Request URI
*
* @return array [endpoint, key]
*/
public static function parseUri($uri)
{
$uri = trim($uri, '/');
$segments = explode('/', $uri);
// If there aren't at least 2 segments,
// return empty values to always return an array with the same length.
if (count($segments) < 2) {
return ['', ''];
}
return array_slice($segments, -2);
}
/**
* Handle a new magic login request.
*
* @param $endpoint
* @param $publicKey
*/
public static function handle($endpoint, $publicKey)
{
$server = new static($endpoint, $publicKey);
if ($server->checkEndpoint()) {
$server->run();
}
}
/**
* Test if endpoints match.
*
* @return bool
*/
public function checkEndpoint()
{
if ($saved = json_decode(get_option(static::OPTION))) {
return $this->endpoint === $saved->endpoint;
}
return false;
}
/**
* Attempt the magic login.
*/
public function run()
{
try {
$magic = $this->loadMagic();
$user = $this->validate($magic);
$this->loginUser($user);
$this->loginRedirect($user, $magic->redirect_url);
} catch (Exception $e) {
$this->deleteMagic();
$this->abort($e);
}
}
/**
* Validate the magic login, and return the user to login if successful.
*
* @param Magic $magic
*
* @throws InvalidUser
* @throws AuthenticationFailure
*
* @return WP_User
*/
private function validate(Magic $magic)
{
if (! $magic->user || (! $user = new WP_User($magic->user)) || ! $user->exists()) {
throw new InvalidUser('No user found or no longer exists.');
}
// We need to hash the salt to produce a key that won't exceed the maximum of 64 bytes.
$key = sodium_crypto_generichash(wp_salt('auth'));
$private_bin = sodium_crypto_generichash($this->signature($magic), $key);
if (! $magic->private
|| ! hash_equals($magic->private, sodium_bin2base64($private_bin, SODIUM_BASE64_VARIANT_URLSAFE))
) {
throw new AuthenticationFailure('Magic login authentication failed.');
}
return $user;
}
/**
* Load the saved data for the current magic login.
*
* @throws BadMagic
*
* @return Magic
*/
private function loadMagic()
{
$magic = json_decode(
get_transient($this->magicKey()),
true
);
if (is_null($magic)) {
throw new BadMagic('The attempted magic login has expired or already been used.');
}
return new Magic($magic);
}
/**
* Delete saved magic.
*/
private function deleteMagic()
{
delete_transient($this->magicKey());
}
/**
* Login the given user and redirect them to wp-admin.
*
* @param WP_User $user
*/
private function loginUser(WP_User $user)
{
$this->deleteMagic();
wp_set_auth_cookie($user->ID);
/**
* Fires after the user has successfully logged in via the WP-CLI Login Server.
*
* @param string $user_login Username.
* @param WP_User $user WP_User object of the logged-in user.
*/
do_action('wp_cli_login/login', $user->user_login, $user);
/**
* Fires after the user has successfully logged in.
*
* @param string $user_login Username.
* @param WP_User $user WP_User object of the logged-in user.
*/
do_action('wp_login', $user->user_login, $user);
}
/**
* Redirect the user after logging in.
*
* Mostly copied from wp-login.php
*
* @param WP_User $user
* @param string $redirect_url
*/
private function loginRedirect(WP_User $user, $redirect_url)
{
$redirect_to = $redirect_url ?: admin_url();
/**
* Filters the login redirect URL.
*
* @param string $redirect_to The redirect destination URL.
* @param string $requested_redirect_to The requested redirect destination URL passed as a parameter.
* @param WP_User $user WP_User object.
*/
$redirect_to = apply_filters('login_redirect', $redirect_to, '', $user);
/**
* Filters the login redirect URL for WP-CLI Login Server requests.
*
* @param string $redirect_to The redirect destination URL.
* @param string $requested_redirect_to The requested redirect destination URL passed as a parameter.
* @param WP_User $user WP_User object.
*/
$redirect_to = apply_filters('wp_cli_login/login_redirect', $redirect_to, '', $user);
/**
* Figure out where to redirect the user for the default wp-admin URL based on the user's capabilities.
*/
if ((empty($redirect_to) || $redirect_to == 'wp-admin/' || $redirect_to == admin_url())) {
// If the user doesn't belong to a blog, send them to user admin. If the user can't edit posts, send them to their profile.
if (is_multisite() && ! get_active_blog_for_user($user->ID) && ! is_super_admin($user->ID)) {
$redirect_to = user_admin_url();
} elseif (is_multisite() && ! $user->has_cap('read')) {
$redirect_to = get_dashboard_url($user->ID);
} elseif (! $user->has_cap('edit_posts')) {
$redirect_to = $user->has_cap('read') ? admin_url('profile.php') : home_url();
}
wp_redirect($redirect_to);
exit;
}
/**
* Redirect safely to the URL provided.
*/
wp_safe_redirect($redirect_to);
exit;
}
/**
* Abort the process; Explode with terrifying message.
*
* @param Exception $e
*/
private function abort(Exception $e)
{
$exception = get_class($e);
$exceptionMessage = $e->getMessage();
$common = sprintf('Try again perhaps? or <a href="%s">Go Home →</a>', esc_url(home_url()));
$message = "<strong>$exceptionMessage</strong><p>$common</p>";
wp_die($message, $exception, ['response' => 410]);
}
/**
* Get the key for retrieving magic data for the current request.
*
* @return string
*/
private function magicKey()
{
// We need to hash the salt to produce a key that won't exceed the maximum of 64 bytes.
$key = sodium_crypto_generichash(wp_salt('auth'));
$bin_hash = sodium_crypto_generichash($this->publicKey, $key);
return self::OPTION . '/' . sodium_bin2base64($bin_hash, SODIUM_BASE64_VARIANT_URLSAFE);
}
/**
* Build the signature to check against the private key for this request.
*
* @param Magic Login data.
*
* @return string
*/
private function signature(Magic $magic)
{
return join('|', [
$this->publicKey,
$this->endpoint,
parse_url($this->homeUrl(), PHP_URL_HOST),
$magic->user,
$magic->expires_at,
$magic->redirect_url,
]);
}
/**
* Get the home URL.
*
* @return string
*/
private function homeUrl()
{
/* wp-cli server-command filters home & siteurl to work and saves the original in a global. */
return isset($GLOBALS['_wp_cli_original_url'])
? $GLOBALS['_wp_cli_original_url']
: home_url();
}
}
/**
* @property-read int $user
* @property-read string $private
* @property-read int $expires_at
* @property-read string $redirect_url
*/
class Magic {
protected $data;
public function __construct(array $data) {
$this->data = $data;
}
public function __get($property) {
return isset($this->data[$property]) ? $this->data[$property] : null;
}
}
class BadMagic extends Exception
{}
class AuthenticationFailure extends Exception
{}
class InvalidUser extends Exception
{}