PluginProbe
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More / 2.0.12
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More v2.0.12
trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.10 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 All 47 releases
content-control / vendor-prefixed / trustedlogin / client / src / SupportUser.php

SupportUser.php in Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More 2.0.12, at vendor-prefixed/trustedlogin/client/src/SupportUser.php

695 lines 20.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class SupportUser
4 *
5 * @package ContentControl\Vendor\TrustedLogin\Client
6 *
7 * @copyright 2021 Katz Web Services, Inc.
8 *
9 * @license GPL-2.0-or-later
10 * Modified by code-atlantic on 26-October-2023 using Strauss.
11 * @see https://github.com/BrianHenryIE/strauss
12 */
13 namespace ContentControl\Vendor\TrustedLogin;
14
15 // Exit if accessed directly
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 use \Exception;
21 use \WP_Error;
22 use \WP_User;
23 use \WP_Admin_Bar;
24
25 /**
26 * The TrustedLogin all-in-one drop-in class.
27 */
28 final class SupportUser {
29
30 /**
31 * @var string The query parameter used to pass the unique user ID
32 */
33 const ID_QUERY_PARAM = 'tlid';
34
35 /**
36 * @var Config $config
37 */
38 private $config;
39
40 /**
41 * @var Logging $logging
42 */
43 private $logging;
44
45 /**
46 * @var SupportRole $role
47 */
48 public $role;
49
50 /**
51 * @var string $user_identifier_meta_key The namespaced setting name for storing the unique identifier hash in user meta
52 * @since 1.0.0
53 * @example tl_{vendor/namespace}_id
54 */
55 private $user_identifier_meta_key;
56
57 /**
58 * @var string $site_hash_meta_key The namespaced setting name for storing the site identifier hash in user meta
59 * @since 1.0.0
60 * @example tl_{vendor/namespace}_site_hash
61 */
62 private $site_hash_meta_key;
63
64 /**
65 * @var int $expires_meta_key The namespaced setting name for storing the timestamp the user expires
66 * @since 1.0.0
67 * @example tl_{vendor/namespace}_expires
68 */
69 private $expires_meta_key;
70
71 /**
72 * @var int $created_by_meta_key The ID of the user who created the TrustedLogin access
73 * @since 1.0.0
74 */
75 private $created_by_meta_key;
76
77 /**
78 * SupportUser constructor.
79 */
80 public function __construct( Config $config, Logging $logging ) {
81 $this->config = $config;
82 $this->logging = $logging;
83 $this->role = new SupportRole( $config, $logging );
84
85 $this->user_identifier_meta_key = 'tl_' . $config->ns() . '_id';
86 $this->site_hash_meta_key = 'tl_' . $config->ns() . '_site_hash';
87 $this->expires_meta_key = 'tl_' . $config->ns() . '_expires';
88 $this->created_by_meta_key = 'tl_' . $config->ns() . '_created_by';
89 }
90
91 /**
92 * Allow accessing limited private properties with a magic method.
93 *
94 * @param string $name Name of property
95 *
96 * @return string|null Value of property, if defined. Otherwise, null.
97 */
98 public function __get( $name ) {
99
100 // Allow accessing limited private variables
101 switch ( $name ) {
102 case 'identifier_meta_key':
103 case 'expires_meta_key':
104 case 'created_by_meta_key':
105 return $this->{$name};
106 break;
107 }
108
109 return null;
110 }
111
112 /**
113 * Checks if a Support User for this vendor has already been created.
114 *
115 * @since 1.0.0
116 *
117 * @return int|false - WP User ID if support user exists, otherwise false.
118 */
119 public function exists() {
120
121 $args = array(
122 'number' => 1,
123 'meta_key' => $this->user_identifier_meta_key,
124 'meta_value' => '',
125 'meta_compare' => 'EXISTS',
126 'fields' => 'ID',
127 );
128
129 $user_ids = get_users( $args );
130
131 return empty( $user_ids ) ? false : (int) $user_ids[0];
132 }
133
134 /**
135 * Returns whether the support user exists and has an expiration time in the future.
136 *
137 * @since 1.0.2
138 *
139 * @return bool True: Support user exists and has an expiration time in the future. False: Any of those things aren't true.
140 */
141 public function is_active( $passed_user = null ) {
142
143 $current_user = is_a( $passed_user, '\WP_User' ) ? $passed_user : wp_get_current_user();
144
145 if ( ! $current_user || ! $current_user->exists() ) {
146 return false;
147 }
148
149 $expiration = $this->get_expiration( $current_user, false, true );
150
151 if ( ! $expiration ) {
152 return false;
153 }
154
155 if ( time() > (int) $expiration ) {
156 return false;
157 }
158
159 return true;
160 }
161
162 /**
163 * Create the Support User.
164 *
165 * @since 1.0.0
166 *
167 * @uses wp_insert_user()
168 *
169 * @return int|WP_Error - Array with login response information if created, or WP_Error object if there was an issue.
170 */
171 public function create() {
172
173 $user_id = $this->exists();
174
175 // Double-check that a user doesn't exist before trying to create a new one.
176 if ( $user_id ) {
177 $this->logging->log( 'Support User not created; already exists: User #' . $user_id, __METHOD__, 'notice' );
178
179 return new \WP_Error( 'user_exists', sprintf( 'A user with the User ID %d already exists', $user_id ) );
180 }
181
182 $role = $this->role->get();
183
184 if ( is_wp_error( $role ) ) {
185 return $role;
186 }
187
188 $user_email = $this->config->get_setting( 'vendor/email' );
189 $allow_existing_user_match = false; // Fail if the user already exists and the email is unhashed.
190
191 if ( defined( 'LOGGED_IN_KEY' ) && defined( 'NONCE_KEY' ) ) {
192 // The hash doesn't need to be secure, just persistent.
193 $user_email = str_replace( '{hash}', sha1( LOGGED_IN_KEY . NONCE_KEY . get_current_blog_id() ), $user_email );
194 $allow_existing_user_match = true; // Don't fail if the user already exists and the email matches the hash.
195 }
196
197 $user_id_of_email = email_exists( $user_email );
198
199 if ( $user_id_of_email ) {
200 $this->logging->log( 'Support User not created; a user with that email already exists: ' . $user_email, __METHOD__, 'warning' );
201
202 // Only allow the user to be created if the email is not hashed; that way, it's not possible to accidentally
203 // create a user with the same email as an existing user.
204 if ( ! $allow_existing_user_match ) {
205 return new \WP_Error( 'email_exists', esc_html__( 'User not created; User with that email already exists', 'trustedlogin' ) );
206 }
207
208 // If the user already exists and the email matches the hash, use that user.
209 return $user_id_of_email;
210 }
211
212 $user_data = array(
213 'user_login' => $this->generate_unique_username(),
214 'user_email' => $user_email,
215 'user_pass' => Encryption::get_random_hash( $this->logging ),
216 'role' => $role->name,
217 'display_name' => $this->config->get_setting( 'vendor/display_name', '' ),
218 'user_registered' => date( 'Y-m-d H:i:s', time() ),
219 );
220
221 $new_user_id = wp_insert_user( $user_data );
222
223 if ( is_wp_error( $new_user_id ) ) {
224 $this->logging->log( 'Error: User not created because: ' . $new_user_id->get_error_message(), __METHOD__, 'error' );
225
226 return $new_user_id;
227 }
228
229 $this->logging->log( 'Support User #' . $new_user_id, __METHOD__, 'info' );
230
231 return $new_user_id;
232 }
233
234 /**
235 * Always return a unique username
236 *
237 * @return string Username, with possible number trailing, if clashes exist.
238 */
239 private function generate_unique_username() {
240
241 // translators: %s is replaced with the name of the software developer (e.g. "Acme Widgets")
242 $username = sprintf( esc_html__( '%s Support', 'trustedlogin' ), $this->config->get_setting( 'vendor/title' ) );
243
244 if ( ! username_exists( $username ) ) {
245 return $username;
246 }
247
248 $i = 1;
249 $new_username = $username;
250 while ( username_exists( $new_username ) ) {
251 $new_username = sprintf( '%s %d', $username, $i + 1 );
252 }
253
254 return $new_username;
255 }
256
257 /**
258 * Returns the site secret ID connected to the support user.
259 *
260 * @param string $user_identifier
261 *
262 * @return string|WP_Error|null Returns the secret ID. WP_Error if there was a problem generating any hashes. Null: No users were found using that user identifier.
263 */
264 public function get_secret_id( $user_identifier ) {
265
266 $user = $this->get( $user_identifier );
267
268 if ( is_null( $user ) ) {
269 return null;
270 }
271
272 $site_identifier_hash = $this->get_site_hash( $user );
273
274 if ( is_wp_error( $site_identifier_hash ) ) {
275 return $site_identifier_hash;
276 }
277
278 $Endpoint = new Endpoint( $this->config, $this->logging );
279
280 return $Endpoint->generate_secret_id( $site_identifier_hash );
281 }
282
283 /**
284 * Logs in a support user, if any exist at $user_identifier and haven't expired yet
285 *
286 * If the user access has expired, deletes the user with {@see SupportUser::delete()}
287 *
288 * @param string $user_identifier Unique identifier for support user before being hashed.
289 *
290 * @return true|WP_Error
291 */
292 public function maybe_login( $user_identifier ) {
293
294 $support_user = $this->get( $user_identifier );
295
296 if ( empty( $support_user ) ) {
297
298 $this->logging->log( 'Support user not found at identifier ' . esc_attr( $user_identifier ), __METHOD__, 'notice' );
299
300 return new \WP_Error( 'user_not_found', sprintf( 'Support user not found at identifier %s.', esc_attr( $user_identifier ) ) );
301 }
302
303 $is_active = $this->is_active( $support_user );
304
305 // This user has expired, but the cron didn't run...
306 if ( ! $is_active ) {
307
308 $expires = $this->get_expiration( $support_user, false, true );
309
310 $this->logging->log( 'The user was supposed to expire on ' . $expires . '; revoking now.', __METHOD__, 'warning' );
311
312 $this->delete( $user_identifier, true, true );
313
314 return new \WP_Error( 'access_expired', 'The user was supposed to expire on ' . $expires . '; revoking now.' );
315 }
316
317 $this->login( $support_user );
318
319 return true;
320 }
321
322 /**
323 * Processes login (with extra logging) and triggers the 'trustedlogin/{ns}/login' hook
324 *
325 * @param \WP_User $support_user
326 */
327 private function login( \WP_User $support_user ) {
328
329 if ( ! $support_user->exists() ) {
330
331 $this->logging->log( sprintf( 'Login failed: Support User #%d does not exist.', $support_user->ID ), __METHOD__, 'error' );
332
333 return;
334 }
335
336 wp_set_current_user( $support_user->ID, $support_user->user_login );
337 wp_set_auth_cookie( $support_user->ID );
338
339 do_action( 'wp_login', $support_user->user_login, $support_user );
340
341 $this->logging->log( sprintf( 'Support User #%d logged in', $support_user->ID ), __METHOD__, 'notice' );
342
343 /**
344 * Action run when TrustedLogin has logged-in
345 */
346 do_action( 'trustedlogin/' . $this->config->ns() . '/logged_in', array(
347 'url' => get_site_url(),
348 'action' => 'logged_in',
349 ) );
350 }
351
352 /**
353 * Helper Function: Get the generated support user(s).
354 *
355 * @since 1.0.0
356 *
357 * @param string $user_identifier_or_hash
358 *
359 * @return \WP_User|null WP_User if found; null if not
360 */
361 public function get( $user_identifier_or_hash = '' ) {
362
363 if ( empty( $user_identifier_or_hash ) ) {
364 return null;
365 }
366
367 $user_identifier_hash = $user_identifier_or_hash;
368
369 // When passed in the endpoint URL, the unique ID will be the raw value, not the hash.
370 if ( strlen( $user_identifier_or_hash ) > 32 ) {
371 $user_identifier_hash = Encryption::hash( $user_identifier_or_hash );
372 }
373
374 $args = array(
375 'number' => 1,
376 'meta_key' => $this->user_identifier_meta_key,
377 'meta_value' => $user_identifier_hash,
378 );
379
380 $user = get_users( $args );
381
382 return empty( $user ) ? null : $user[0];
383 }
384
385 /**
386 * Returns the expiration for user access as either a human-readable string or timestamp.
387 *
388 * @param \WP_User $user
389 * @param bool $human_readable Whether to show expiration as a human_time_diff()-formatted string. Default: false.
390 * @param bool $gmt Whether to use GMT timestamp in the human-readable result. Not used if $human_readable is false. Default: false.
391 *
392 * @return int|string|false False if no expiration is set. Expiration timestamp if $human_readable is false. Time diff if $human_readable is true.
393 */
394 public function get_expiration( \WP_User $user, $human_readable = false, $gmt = false ) {
395
396 $expiration = get_user_option( $this->expires_meta_key, $user->ID );
397
398 if ( ! $expiration ) {
399 return false;
400 }
401
402 return $human_readable ? human_time_diff( current_time( 'timestamp', $gmt ), $expiration ) : $expiration;
403 }
404
405 /**
406 * Get all users with the support role.
407 *
408 * @since 1.0.0
409 *
410 * @return \WP_User[]
411 */
412 public function get_all() {
413
414 static $support_users = null;
415
416 // Only fetch once per process
417 if ( ! is_null( $support_users ) ) {
418 return $support_users;
419 }
420
421 $args = array(
422 'number' => - 1,
423 'meta_key' => $this->user_identifier_meta_key,
424 'meta_compare' => 'EXISTS',
425 'meta_value' => '',
426 );
427
428 $support_users = get_users( $args );
429
430 return $support_users;
431 }
432
433
434 /**
435 * Returns the first support user active on the site, if any.
436 *
437 * @since 1.0.0
438 *
439 * @return \WP_User|null
440 */
441 public function get_first() {
442 $support_users = $this->get_all();
443
444 if ( $support_users ) {
445 return $support_users[0];
446 }
447
448 return null;
449 }
450
451 /**
452 * Deletes support user(s) with options to delete the TrustedLogin-created user role and endpoint as well
453 *
454 * @used-by SupportUser::maybe_login() Called when user access has expired, but the cron didn't run...
455 * @used-by Client::revoke_access()
456 *
457 * @param string $user_identifier Unique identifier of the user to delete.
458 * @param bool $delete_role Should the TrustedLogin-created user role be deleted also? Default: `true`.
459 * @param bool $delete_endpoint Should the TrustedLogin endpoint for the site be deleted also? Default: `true`.
460 *
461 * @return bool|WP_Error True: Successfully removed user and role; false: There are no support users matching $user_identifier; WP_Error: something went wrong.
462 */
463 public function delete( $user_identifier = '', $delete_role = true, $delete_endpoint = true ) {
464
465 require_once ABSPATH . 'wp-admin/includes/user.php'; // Needed for wp_delete_user()
466
467 $user = $this->get( $user_identifier );
468
469 if ( empty( $user ) ) {
470 return false;
471 }
472
473 $reassign_id_or_null = $this->get_reassign_user_id();
474
475 $this->logging->log( 'Processing user ID ' . $user->ID, __METHOD__, 'debug' );
476
477 // Remove auto-cleanup hook
478 wp_clear_scheduled_hook( 'trustedlogin/' . $this->config->ns() . '/access/revoke', array( $user_identifier ) );
479
480 // Delete first using wp_delete_user() to allow for reassignment of posts
481 $deleted = wp_delete_user( $user->ID, $reassign_id_or_null );
482
483 // Also delete the user from the all sites on the WP Multisite network
484 $wpmu_deleted = \function_exists( 'wpmu_delete_user' ) ? wpmu_delete_user( $user->ID ) : false;
485
486 if ( $deleted ) {
487 $message = 'User: ' . $user->ID . ' deleted.';
488
489 if ( $wpmu_deleted ) {
490 $message .= ' Also deleted from the Multisite network.';
491 }
492
493 $this->logging->log( $message, __METHOD__, 'info' );
494 } else {
495 $this->logging->log( 'User: ' . $user->ID . ' was NOT deleted.', __METHOD__, 'error' );
496 }
497
498 if ( $delete_role ) {
499 $this->role->delete();
500 }
501
502 if ( $delete_endpoint ) {
503 $Endpoint = new Endpoint( $this->config, $this->logging );
504 $Endpoint->delete();
505 }
506
507 // Re-run to make sure there were no race conditions
508 return $this->delete( $user_identifier );
509 }
510
511 /**
512 * Get the ID of the best-guess appropriate admin user
513 *
514 * @since 1.0.0
515 *
516 * @return int|null User ID if there are admins, null if not
517 */
518 private function get_reassign_user_id() {
519
520 if ( ! $this->config->get_setting( 'reassign_posts' ) ) {
521 return null;
522 }
523
524 // TODO: Add a filter to modify who gets auto-reassigned
525 $admins = get_users( array(
526 'role' => 'administrator',
527 'orderby' => 'registered',
528 'order' => 'DESC',
529 'number' => 1,
530 ) );
531
532 $reassign_id = empty( $admins ) ? null : $admins[0]->ID;
533
534 $this->logging->log( 'Reassign user ID: ' . var_export( $reassign_id, true ), __METHOD__, 'info' );
535
536 return $reassign_id;
537 }
538
539 /**
540 * Schedules cron job to auto-revoke, adds user meta with unique ids
541 *
542 * @param int $user_id ID of generated support user
543 * @param string $site_identifier_hash
544 * @param int $decay_timestamp Timestamp when user will be removed
545 *
546 * @return string|WP_Error Value of $identifier_meta_key if worked; empty string or WP_Error if not.
547 */
548 public function setup( $user_id, $site_identifier_hash, $expiration_timestamp = null, Cron $cron = null ) {
549
550 if ( $expiration_timestamp ) {
551
552 $scheduled = $cron->schedule( $expiration_timestamp, $site_identifier_hash );
553
554 if ( $scheduled ) {
555 update_user_option( $user_id, $this->expires_meta_key, $expiration_timestamp );
556 }
557 }
558
559 $user_identifier = Encryption::hash( $site_identifier_hash );
560
561 if ( is_wp_error( $user_identifier ) ) {
562 return $user_identifier;
563 }
564
565 update_user_option( $user_id, $this->site_hash_meta_key, $site_identifier_hash, true );
566 update_user_option( $user_id, $this->user_identifier_meta_key, $user_identifier, true );
567 update_user_option( $user_id, $this->created_by_meta_key, get_current_user_id() );
568
569 // Make extra sure that the identifier was saved. Otherwise, things won't work!
570 return get_user_option( $this->user_identifier_meta_key, $user_id );
571 }
572
573 /**
574 * Updates the scheduled cron job to auto-revoke and updates the Support User's meta.
575 *
576 * @param int $user_id ID of generated support user.
577 * @param string $site_identifier_hash The unique identifier for the WP_User created {@see Encryption::get_random_hash()}
578 * @param int $expiration_timestamp Timestamp when user will be removed. Throws error if null/empty.
579 * @param Cron|null $cron Optional. The Cron object for handling scheduling. Defaults to null.
580 *
581 * @return string|WP_Error Value of $identifier_meta_key if worked; empty string or WP_Error if not.
582 */
583 public function extend( $user_id, $site_identifier_hash, $expiration_timestamp = null, $cron = null ) {
584
585 if ( ! $user_id || ! $site_identifier_hash || ! $expiration_timestamp ) {
586 return new \WP_Error( 'missing_action_parameter', 'Error extending Support User access, missing required parameter.' );
587 }
588
589 if ( ! $cron instanceof Cron ) {
590 // Avoid a Fatal error if `$cron` parameter is not provided.
591 $cron = new Cron( $this->config, $this->logging );
592 }
593
594 $rescheduled = $cron->reschedule( $expiration_timestamp, $site_identifier_hash );
595
596 if ( $rescheduled ) {
597 update_user_option( $user_id, $this->expires_meta_key, $expiration_timestamp );
598
599 return true;
600 }
601
602 return new \WP_Error( 'extend_failed', 'Error rescheduling cron task' );
603
604 }
605
606 /**
607 * @param \WP_User|int $user_id_or_object User ID or User object
608 *
609 * @return string|WP_Error User unique identifier if success; WP_Error if $user is not int or WP_User.
610 */
611 public function get_user_identifier( $user_id_or_object ) {
612
613 if ( empty( $this->user_identifier_meta_key ) ) {
614 $this->logging->log( 'The meta key to identify users is not set.', __METHOD__, 'error' );
615
616 return new \WP_Error( 'missing_meta_key', 'The SupportUser object has not been properly instantiated.' );
617 }
618
619 if ( $user_id_or_object instanceof \WP_User ) {
620 $user_id = $user_id_or_object->ID;
621 } elseif ( is_int( $user_id_or_object ) ) {
622 $user_id = $user_id_or_object;
623 } else {
624
625 $this->logging->log( 'The $user_id_or_object value must be int or WP_User: ' . var_export( $user_id_or_object, true ), __METHOD__, 'error' );
626
627 return new \WP_Error( 'invalid_type', '$user must be int or WP_User' );
628 }
629
630 return get_user_option( $this->user_identifier_meta_key, $user_id );
631 }
632
633 /**
634 * @param WP_User|int $user_id_or_object User ID or User object
635 *
636 * @return string|WP_Error User unique identifier if success; WP_Error if $user is not int or WP_User.
637 */
638 public function get_site_hash( $user_id_or_object ) {
639
640 if ( empty( $this->site_hash_meta_key ) ) {
641 $this->logging->log( 'The constructor has not been properly instantiated; the site_hash_meta_key property is not set.', __METHOD__, 'error' );
642
643 return new \WP_Error( 'missing_meta_key', 'The SupportUser object has not been properly instantiated.' );
644 }
645
646 if ( $user_id_or_object instanceof \WP_User ) {
647 $user_id = $user_id_or_object->ID;
648 } elseif ( is_int( $user_id_or_object ) ) {
649 $user_id = $user_id_or_object;
650 } else {
651
652 $this->logging->log( 'The $user_id_or_object value must be int or WP_User: ' . var_export( $user_id_or_object, true ), __METHOD__, 'error' );
653
654 return new \WP_Error( 'invalid_type', '$user must be int or WP_User' );
655 }
656
657 return get_user_option( $this->site_hash_meta_key, $user_id );
658 }
659
660 /**
661 * Returns admin URL to revoke support user
662 *
663 * @uses SupportUser::get_user_identifier()
664 *
665 * @since 1.1 Removed second parameter $current_url.
666 *
667 * @param \WP_User|int|string $user User object, user ID, or "all". If "all", will revoke all users.
668 *
669 * @return string|false Unsanitized nonce URL to revoke support user. If not able to retrieve user identifier, returns false.
670 */
671 public function get_revoke_url( $user ) {
672
673 // If "all", will revoke all support users.
674 if ( 'all' === $user ) {
675 $user_identifier = 'all';
676 } else {
677 $user_identifier = $this->get_user_identifier( $user );
678 }
679
680 if ( ! $user_identifier || is_wp_error( $user_identifier ) ) {
681 return false;
682 }
683
684 $revoke_url = add_query_arg( array(
685 Endpoint::REVOKE_SUPPORT_QUERY_PARAM => $this->config->ns(),
686 self::ID_QUERY_PARAM => $user_identifier,
687 '_wpnonce' => wp_create_nonce( Endpoint::REVOKE_SUPPORT_QUERY_PARAM ),
688 ), admin_url() );
689
690 $this->logging->log( "revoke_url: $revoke_url", __METHOD__, 'debug' );
691
692 return $revoke_url;
693 }
694 }
695