| 1 |
<?php |
| 2 |
/** |
| 3 |
* Auth module (spec 013-authentication-profile). |
| 4 |
* |
| 5 |
* Login/logout/session, signup, Google OAuth, profile sync + verification recheck. |
| 6 |
* Relocated from the monolith: REST classes from includes/API/{Login,SignUp}.php and |
| 7 |
* the auth-owned half of Profile.php; the Google OAuth callback handler from |
| 8 |
* Plugin::google_login_handler(). Behavior byte-identical (same routes, same query |
| 9 |
* params, same option keys). |
| 10 |
* |
| 11 |
* @package Templately |
| 12 |
*/ |
| 13 |
|
| 14 |
namespace Templately\Modules\Auth; |
| 15 |
|
| 16 |
use Templately\Core\Module_Base; |
| 17 |
use Templately\Utils\Database; |
| 18 |
use Templately\Utils\Response\ErrorCode; |
| 19 |
use Templately\Modules\Auth\REST\Login; |
| 20 |
use Templately\Modules\Auth\REST\Profile; |
| 21 |
use Templately\Modules\Auth\REST\SignUp; |
| 22 |
|
| 23 |
class Module extends Module_Base { |
| 24 |
|
| 25 |
public function get_name(): string { |
| 26 |
return 'auth'; |
| 27 |
} |
| 28 |
|
| 29 |
protected function init_hooks(): void { |
| 30 |
add_action( 'init', [ $this, 'google_login_handler' ] ); |
| 31 |
} |
| 32 |
|
| 33 |
public function register_rest_routes(): void { |
| 34 |
Login::get_instance()->register_routes(); |
| 35 |
SignUp::get_instance()->register_routes(); |
| 36 |
Profile::get_instance()->register_routes(); |
| 37 |
} |
| 38 |
|
| 39 |
public function google_login_handler() { |
| 40 |
// Stop if not a templately google login request |
| 41 |
if ( empty( $_GET['templately_google_login'] ) ) { |
| 42 |
return; |
| 43 |
} |
| 44 |
|
| 45 |
if ( wp_doing_ajax() || wp_doing_cron() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { |
| 46 |
return; |
| 47 |
} |
| 48 |
|
| 49 |
// Checked before the token is consumed: the callback can land while the |
| 50 |
// auth cookie is missing (expired session, cookie not yet set), and WP |
| 51 |
// will bounce the user through wp-login and back to this same URL. |
| 52 |
// Burning the token here would fail that legitimate retry. |
| 53 |
if ( ! is_user_logged_in() ) { |
| 54 |
return; |
| 55 |
} |
| 56 |
|
| 57 |
$state = ''; |
| 58 |
if ( ! empty( $_GET['templately_state'] ) ) { |
| 59 |
$state = sanitize_text_field( wp_unslash( $_GET['templately_state'] ) ); |
| 60 |
} elseif ( ! empty( $_GET['state'] ) ) { |
| 61 |
$state = sanitize_text_field( wp_unslash( $_GET['state'] ) ); |
| 62 |
} |
| 63 |
|
| 64 |
$state_user_id = false; |
| 65 |
if ( ! empty( $state ) ) { |
| 66 |
$state_user_id = Database::get_transient( 'google_state_' . $state ); |
| 67 |
Database::delete_transient( 'google_state_' . $state ); |
| 68 |
} |
| 69 |
|
| 70 |
$is_authorized = false !== $state_user_id |
| 71 |
&& intval( $state_user_id ) === get_current_user_id() |
| 72 |
&& current_user_can( 'delete_posts' ); |
| 73 |
|
| 74 |
$redirect_url = remove_query_arg( [ 'templately_google_login', 'templately_state', 'api_key', 'error', 'state', 'redirect-to' ] ); |
| 75 |
|
| 76 |
if ( ! $is_authorized ) { |
| 77 |
$error_code = ErrorCode::AUTH_STATE_INVALID; |
| 78 |
} elseif ( ! empty( $_GET['error'] ) ) { |
| 79 |
// The provider answers with one of its own slugs (oauth_failed, |
| 80 |
// provider_error, invalid_state, …). It is DROPPED rather than mapped: |
| 81 |
// the value is attacker-controlled, none of the slugs mean anything |
| 82 |
// different to the user, and forwarding one would put an unvetted |
| 83 |
// string back into a URL the sign-in screen reads. |
| 84 |
$error_code = ErrorCode::AUTH_PROVIDER_FAILED; |
| 85 |
} elseif ( ! empty( $_GET['api_key'] ) ) { |
| 86 |
$request = new \WP_REST_Request( 'POST', '/templately/v1/login' ); |
| 87 |
$request->set_param( 'viaAPI', true ); |
| 88 |
$request->set_param( 'api_key', sanitize_text_field( $_GET['api_key'] ) ); |
| 89 |
|
| 90 |
/** |
| 91 |
* @var Login $login |
| 92 |
*/ |
| 93 |
$login = Login::get_instance(); |
| 94 |
$login->permission_check( $request ); |
| 95 |
|
| 96 |
// login() pins the write target to the acting user itself — no pin |
| 97 |
// here, or its finally would release ours mid-request. |
| 98 |
$response = $login->login(); |
| 99 |
|
| 100 |
if ( ! is_wp_error( $response ) && ! empty( $response['user'] ) ) { |
| 101 |
$redirect_path = ! empty( $_GET['redirect-to'] ) ? sanitize_text_field( wp_unslash( $_GET['redirect-to'] ) ) : ''; |
| 102 |
if ( ! empty( $redirect_path ) ) { |
| 103 |
if ( filter_var( $redirect_path, FILTER_VALIDATE_URL ) ) { |
| 104 |
$redirect_url = $redirect_path; |
| 105 |
} else { |
| 106 |
$is_templately = strpos( $redirect_url, 'page=templately' ) !== false; |
| 107 |
$is_elementor = strpos( $redirect_url, 'action=elementor' ) !== false; |
| 108 |
// Gutenberg editor usually has action=edit or is a block editor page |
| 109 |
$is_gutenberg = ( strpos( $redirect_url, 'action=edit' ) !== false || strpos( $redirect_url, 'post_type=' ) !== false ) && ! $is_elementor; |
| 110 |
|
| 111 |
if ( $is_templately || $is_elementor || $is_gutenberg ) { |
| 112 |
$redirect_url = add_query_arg( 'path', ltrim( $redirect_path, '/' ), $redirect_url ); |
| 113 |
|
| 114 |
// Always open the modal in editors after google login |
| 115 |
if ( $is_elementor || $is_gutenberg ) { |
| 116 |
$redirect_url = add_query_arg( 'templately_open_modal', '1', $redirect_url ); |
| 117 |
} |
| 118 |
} |
| 119 |
} |
| 120 |
} |
| 121 |
|
| 122 |
wp_safe_redirect( $redirect_url ); |
| 123 |
exit; |
| 124 |
} else { |
| 125 |
$error_code = Login::resolve_error_code( $response ); |
| 126 |
} |
| 127 |
} else { |
| 128 |
$error_code = ErrorCode::AUTH_MISSING_API_KEY; |
| 129 |
} |
| 130 |
|
| 131 |
// ONLY a code travels back — never a message. The message is authored |
| 132 |
// upstream, can contain markup, and anything placed in a query param is |
| 133 |
// attacker-controlled by the time the sign-in screen renders it. The |
| 134 |
// client maps the code to its own translated copy (errorCatalog) and |
| 135 |
// falls back to generic copy for a code it does not recognise. |
| 136 |
$redirect_url = add_query_arg( [ |
| 137 |
'templately_error' => rawurlencode( $error_code ), |
| 138 |
], $redirect_url ); |
| 139 |
|
| 140 |
wp_safe_redirect( $redirect_url ); |
| 141 |
exit; |
| 142 |
} |
| 143 |
} |
| 144 |
|