PluginProbe
Metricool – Social media and site statistics / 2.1.1
Metricool – Social media and site statistics v2.1.1
2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.27 trunk
← All changes | app/Features/Onboarding/OnboardingController.php +210 -0 2.0.0 → 2.1.1 View file →
@@ -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 +}