| @@ -1,0 +1,210 @@ | ||
| 1 | +<?php | |
| 2 | + | |
| 3 | +declare(strict_types=1); | |
| 4 | + | |
| 5 | +namespace Metricool\Features\Onboarding; | |
| 6 | + | |
| 7 | +if (!defined('ABSPATH')) { | |
| 8 | + exit; | |
| 9 | +} | |
| 10 | + | |
| 11 | +use Metricool\Support\Helpers\Storages\RequestStorage; | |
| 12 | +use Metricool\Traits\HasRestAccess; | |
| 13 | +use Metricool\Services\DashboardService; | |
| 14 | +use Metricool\Support\Validation\Validator; | |
| 15 | +use Metricool\Interfaces\FeatureInterface; | |
| 16 | +use Metricool\Services\MetricoolAccountService; | |
| 17 | +use Metricool\Features\Onboarding\Services\OAuthService; | |
| 18 | +use Metricool\Support\Helpers\Storages\EnvironmentConfig; | |
| 19 | +use Metricool\Features\Onboarding\Services\CreateAccountService; | |
| 20 | +use Metricool\Features\Onboarding\Exceptions\CreateAccountException; | |
| 21 | +use Metricool\Features\Onboarding\Exceptions\BrandAccessDeniedException; | |
| 22 | +use Throwable; | |
| 23 | + | |
| 24 | +class OnboardingController implements FeatureInterface | |
| 25 | +{ | |
| 26 | + use HasRestAccess; | |
| 27 | + | |
| 28 | + private OnboardingService $onboarding; | |
| 29 | + private EnvironmentConfig $env; | |
| 30 | + private CreateAccountService $accounts; | |
| 31 | + private OAuthService $oauth; | |
| 32 | + private DashboardService $dashboard; | |
| 33 | + private MetricoolAccountService $account; | |
| 34 | + private RequestStorage $request; | |
| 35 | + | |
| 36 | + public function __construct( | |
| 37 | + OnboardingService $onboarding, | |
| 38 | + CreateAccountService $accounts, | |
| 39 | + EnvironmentConfig $env, | |
| 40 | + OAuthService $oauth, | |
| 41 | + DashboardService $dashboard, | |
| 42 | + MetricoolAccountService $account, | |
| 43 | + RequestStorage $request | |
| 44 | + ) { | |
| 45 | + $this->onboarding = $onboarding; | |
| 46 | + $this->accounts = $accounts; | |
| 47 | + $this->env = $env; | |
| 48 | + $this->oauth = $oauth; | |
| 49 | + $this->dashboard = $dashboard; | |
| 50 | + $this->account = $account; | |
| 51 | + $this->request = $request; | |
| 52 | + } | |
| 53 | + | |
| 54 | + public function register(): void | |
| 55 | + { | |
| 56 | + add_filter('metricool_rest_routes', [$this, 'addRoutes']); | |
| 57 | + | |
| 58 | + // Intercept OAuth callback on the dashboard page | |
| 59 | + add_action('load-toplevel_page_metricool', [$this, 'oauthCallback']); | |
| 60 | + } | |
| 61 | + | |
| 62 | + /** | |
| 63 | + * Add onboarding routes to the existing routes of our plugin | |
| 64 | + */ | |
| 65 | + public function addRoutes(array $routes): array | |
| 66 | + { | |
| 67 | + $routes['onboarding/create_account'] = [ | |
| 68 | + 'methods' => 'POST', | |
| 69 | + 'callback' => [$this, 'createAccount'], | |
| 70 | + ]; | |
| 71 | + | |
| 72 | + $routes['onboarding/finish_onboarding'] = [ | |
| 73 | + 'methods' => 'POST', | |
| 74 | + 'callback' => [$this, 'finishOnboarding'], | |
| 75 | + ]; | |
| 76 | + | |
| 77 | + $routes['onboarding/oauth_redirect'] = [ | |
| 78 | + 'methods' => 'GET', | |
| 79 | + 'callback' => [$this, 'getOauthRedirectUrl'], | |
| 80 | + ]; | |
| 81 | + | |
| 82 | + return $routes; | |
| 83 | + } | |
| 84 | + | |
| 85 | + /** | |
| 86 | + * Create a new Metricool account. The created user is authenticated | |
| 87 | + * automatically. | |
| 88 | + */ | |
| 89 | + public function createAccount(\WP_REST_Request $request): \WP_REST_Response | |
| 90 | + { | |
| 91 | + $validated = Validator::validate($request->get_params(), [ | |
| 92 | + 'email' => 'required|email', | |
| 93 | + 'password' => 'required|string', | |
| 94 | + 'captcha' => 'required|string', | |
| 95 | + 'terms' => 'accepted', | |
| 96 | + 'marketing' => 'boolean', | |
| 97 | + ]); | |
| 98 | + | |
| 99 | + // Attempt to create the account | |
| 100 | + try { | |
| 101 | + $this->accounts->createAccount( | |
| 102 | + $validated['captcha'], | |
| 103 | + $validated['email'], | |
| 104 | + $validated['password'], | |
| 105 | + rest_sanitize_boolean($validated['marketing'] ?? false) | |
| 106 | + ); | |
| 107 | + } catch (CreateAccountException $e) { | |
| 108 | + return $this->sendHttpErrorResponse($e->getMessage(), ['reason' => $e->getReason()], $e->getCode()); | |
| 109 | + } | |
| 110 | + | |
| 111 | + return $this->onboardedResponse(); | |
| 112 | + } | |
| 113 | + | |
| 114 | + /** | |
| 115 | + * Method is used to finish the onboarding process. It is called when the | |
| 116 | + * user has completed the onboarding process and wants to finish it. | |
| 117 | + */ | |
| 118 | + public function finishOnboarding(\WP_REST_Request $request): \WP_REST_Response | |
| 119 | + { | |
| 120 | + $blogId = (string) $request->get_param('blogId'); | |
| 121 | + | |
| 122 | + if (empty($blogId)) { | |
| 123 | + return $this->onboardedResponse(); | |
| 124 | + } | |
| 125 | + | |
| 126 | + // Store the blogId if it was provided by the client, to retrieve the necessary blog information | |
| 127 | + try { | |
| 128 | + $this->onboarding->finalizeOnboarding($blogId); | |
| 129 | + } catch (BrandAccessDeniedException $e) { | |
| 130 | + return $this->sendHttpErrorResponse( | |
| 131 | + __('You don’t have sufficient permissions to connect this brand. Please pick another brand or sign in with a different account.', 'metricool'), | |
| 132 | + [], | |
| 133 | + 403 | |
| 134 | + ); | |
| 135 | + } catch (Throwable $e) { | |
| 136 | + return $this->sendHttpErrorResponse(wp_kses_post(sprintf( | |
| 137 | + /* translators: %1$s is opening link and %2$s is closing link */ | |
| 138 | + __('Something went wrong. Please try again or %1$sleave a support message%2$s.', 'metricool'), | |
| 139 | + '<a href="' . $this->env->get('frontend.trusted_urls.new_support_ticket') . '" target="_blank">', | |
| 140 | + '</a>', | |
| 141 | + )), ['data' => $e->getMessage()], $e->getCode() ?: 500); | |
| 142 | + } | |
| 143 | + | |
| 144 | + return $this->onboardedResponse(); | |
| 145 | + } | |
| 146 | + | |
| 147 | + /** | |
| 148 | + * Build and return the Metricool OAuth authorize URL. | |
| 149 | + */ | |
| 150 | + public function getOauthRedirectUrl(\WP_REST_Request $request): \WP_REST_Response | |
| 151 | + { | |
| 152 | + if (is_ssl() === false) { | |
| 153 | + return $this->sendHttpErrorResponse(__('HTTPS is required to be able to authorize this website.', 'metricool'), [], 400); | |
| 154 | + } | |
| 155 | + | |
| 156 | + return $this->sendHttpResponse([ | |
| 157 | + 'redirect_url' => $this->oauth->getAuthorizationUrl(), | |
| 158 | + ]); | |
| 159 | + } | |
| 160 | + | |
| 161 | + /** | |
| 162 | + * Handle the OAuth callback from Metricool. Exchanges the authorization | |
| 163 | + * code for tokens, authenticates the user, and redirects to the dashboard. | |
| 164 | + */ | |
| 165 | + public function oauthCallback(): void | |
| 166 | + { | |
| 167 | + $action = $this->request->getString('global.metricool_action'); | |
| 168 | + | |
| 169 | + if ($action !== OAuthService::REDIRECT_ACTION) { | |
| 170 | + return; | |
| 171 | + } | |
| 172 | + | |
| 173 | + $code = $this->request->getString('global.code'); | |
| 174 | + $state = $this->request->getString('global.state'); | |
| 175 | + | |
| 176 | + try { | |
| 177 | + $this->oauth->authenticateWithCode($code, $state); | |
| 178 | + } catch (Throwable $e) { | |
| 179 | + wp_safe_redirect(add_query_arg('oauth_error', $e->getMessage(), $this->env->getString('plugin.dashboard_url'))); | |
| 180 | + exit; | |
| 181 | + } | |
| 182 | + | |
| 183 | + // Attempt to automatically set the blog information, completes the onboarding process on success | |
| 184 | + try { | |
| 185 | + $this->onboarding->finalizeOnboarding(); | |
| 186 | + } catch (Throwable $e) { | |
| 187 | + wp_safe_redirect(add_query_arg('oauth_error', 'onboarding_failed', $this->env->getString('plugin.dashboard_url'))); | |
| 188 | + exit; | |
| 189 | + } | |
| 190 | + | |
| 191 | + // Redirect to the WordPress dashboard | |
| 192 | + wp_safe_redirect($this->env->getString('plugin.dashboard_url')); | |
| 193 | + exit; | |
| 194 | + } | |
| 195 | + | |
| 196 | + /** | |
| 197 | + * Response that is returned when the onboarding process has been completed successfully. | |
| 198 | + * Includes the onboarding state and mode, as well as the account data of the user. | |
| 199 | + */ | |
| 200 | + private function onboardedResponse(): \WP_REST_Response | |
| 201 | + { | |
| 202 | + return $this->sendHttpResponse([ | |
| 203 | + 'onboarding' => [ | |
| 204 | + 'state' => $this->dashboard->state(), | |
| 205 | + 'mode' => $this->dashboard->mode(), | |
| 206 | + ], | |
| 207 | + 'account' => $this->account->accountData(), | |
| 208 | + ]); | |
| 209 | + } | |
| 210 | +} | |