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/Controllers/DashboardController.php +322 -0 2.0.1 → 2.1.1 View file →
@@ -1,0 +1,322 @@
1 +<?php
2 +
3 +declare(strict_types=1);
4 +
5 +namespace Metricool\Controllers;
6 +
7 +if (!defined('ABSPATH')) {
8 + exit;
9 +}
10 +
11 +use Metricool\Support\Helpers\Storages\LanguagesConfig;
12 +use Metricool\Traits\HasViews;
13 +use Metricool\Traits\HasUserAccess;
14 +use Metricool\Services\DashboardService;
15 +use Metricool\Traits\HasAllowlistControl;
16 +use Metricool\Http\Metricool\MetricoolApi;
17 +use Metricool\Interfaces\ControllerInterface;
18 +use Metricool\Services\MetricoolAccountService;
19 +use Metricool\Support\Helpers\Storages\EnvironmentConfig;
20 +
21 +class DashboardController implements ControllerInterface
22 +{
23 + use HasViews;
24 + use HasUserAccess;
25 + use HasAllowlistControl;
26 +
27 + private EnvironmentConfig $env;
28 + private MetricoolApi $metricool;
29 + private DashboardService $service;
30 + private MetricoolAccountService $account;
31 + private LanguagesConfig $languages;
32 +
33 + public function __construct(EnvironmentConfig $env, MetricoolApi $metricool, DashboardService $service, MetricoolAccountService $account, LanguagesConfig $languages)
34 + {
35 + $this->env = $env;
36 + $this->metricool = $metricool;
37 + $this->service = $service;
38 + $this->account = $account;
39 + $this->languages = $languages;
40 + }
41 +
42 + public function register(): void
43 + {
44 + // Show forced login screen when coming from legacy
45 + add_action('metricool_plugin_legacy_upgrade', [$this->service, 'enableForcedLogin']);
46 + add_action('metricool_onboarding_completed', [$this->service, 'disableForcedLogin']);
47 +
48 + // Redirect on the activation hook, but do it after anything else.
49 + add_action('metricool_activation', [$this, 'maybeRedirectToDashboard'], 9999);
50 +
51 + add_action('admin_menu', [$this, 'addDashboardPage']);
52 + }
53 +
54 + /**
55 + * Redirect to metricool dashboard page on activation, but only if the user
56 + * manually activated the plugin via the plugins overview. React will handle
57 + * redirect to onboarding if needed.
58 + *
59 + * @param string $pageSource The page where the activation was triggered,
60 + * usually 'plugins.php' but can be other pages as well.
61 + */
62 + public function maybeRedirectToDashboard(string $pageSource = ''): void
63 + {
64 + if ($pageSource !== 'plugins.php') {
65 + return;
66 + }
67 +
68 + wp_safe_redirect($this->env->getUrl('plugin.dashboard_url'));
69 + exit;
70 + }
71 +
72 + /**
73 + * Add the dashboard page to the admin menu of WordPress. Also triggers the
74 + * action to enqueue scripts and styles
75 + * @uses apply_filters metricool_menu_position
76 + */
77 + public function addDashboardPage(): void
78 + {
79 + /**
80 + * Filter: metricool_menu_position
81 + * Can be used to change the position of the menu item in the admin menu.
82 + * @param int $menuPosition
83 + * @return int Default 59 to be positioned after "wp-menu-separator" and
84 + * before "Appearance".
85 + */
86 + $menuPosition = apply_filters('metricool_menu_position', 59);
87 +
88 + $pageHookSuffix = add_menu_page(
89 + esc_html__('Metricool', 'metricool'),
90 + esc_html__('Metricool', 'metricool'),
91 + 'metricool_manage',
92 + 'metricool',
93 + [$this, 'renderReactApp'],
94 + $this->getMenuItemIconSVG(),
95 + $menuPosition,
96 + );
97 +
98 + add_action("admin_print_scripts-$pageHookSuffix", [$this, 'enqueueReactScripts']);
99 + }
100 +
101 + /**
102 + * Render the React app in the WordPress admin
103 + */
104 + public function renderReactApp(): void
105 + {
106 + $this->render('admin/dashboard', [], 'html');
107 + }
108 +
109 + /**
110 + * Returns the SVG icon for the menu item in the admin menu.
111 + * @return string Base64 encoded SVG image
112 + */
113 + public function getMenuItemIconSVG(): string
114 + {
115 + $svg = '<svg width="20" height="20" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
116 + <path d="M44 0C47.3137 1.28851e-07 50 2.68629 50 6V44C50 47.3137 47.3137 50 44 50H6C2.68629 50 0 47.3137 0 44V6C1.28855e-07 2.68629 2.68629 0 6 0H44ZM34.7744 17.001C32.8644 16.9787 30.9998 17.5888 29.4648 18.7383L23.3008 23.0029C22.4171 19.344 19.1215 17.001 15.3154 17.001C10.7691 17.001 7 20.2068 7 24.9854C7.00016 29.7938 10.769 33 15.3154 33C17.2729 33.0189 19.1774 32.3565 20.71 31.125L26.7002 26.9805C27.5862 30.6571 30.8925 33 34.7744 33C39.2309 32.9999 42.9998 29.7937 43 24.9854C43 20.2068 39.2314 17.001 34.7744 17.001ZM15.3154 21.0527C17.1699 21.0528 18.9346 22.6264 18.9346 24.9854C18.9344 27.3743 17.1698 28.9169 15.3154 28.917C13.3715 28.917 11.6067 27.3742 11.6064 24.9854C11.6064 22.6263 13.3714 21.0527 15.3154 21.0527ZM34.7744 21.0527C36.629 21.0528 38.3936 22.6264 38.3936 24.9854C38.3933 27.3743 36.6289 28.9169 34.7744 28.917C32.8305 28.917 31.0656 27.3742 31.0654 24.9854C31.0654 22.6263 32.8303 21.0527 34.7744 21.0527Z" fill="#a7aaad"/>
117 + </svg>';
118 +
119 + return 'data:image/svg+xml;base64,' . base64_encode($svg);
120 + }
121 +
122 + /**
123 + * Enqueue the Tailwind CSS for the dashboard in the header
124 + */
125 + public function enqueueDashboardStyles(): void
126 + {
127 + $chunkTranslation = $this->getReactChunkTranslations();
128 + if (empty($chunkTranslation)) {
129 + return;
130 + }
131 +
132 + wp_enqueue_style(
133 + 'metricool-tailwind',
134 + $this->env->getUrl('plugin.assets_url') . '/css/tailwind.generated.css', // todo - feel free to place this in a different location
135 + [],
136 + ($chunkTranslation['version'] ?? '')
137 + );
138 + }
139 +
140 + /**
141 + * Enqueue the React scripts and styles for the dashboard:
142 + * All files to enqueue are listed in manifest.json, generated by Vite,
143 + * and available in react/build/assets
144 + *
145 + * Load translations for the React app
146 + */
147 + public function enqueueReactScripts(): void
148 + {
149 + $manifest = $this->env->getString('plugin.react_path') . '/build/.vite/manifest.json';
150 + $json_translations = [];
151 + if (file_exists($manifest)) {
152 + $manifest_contents = file_get_contents($manifest);
153 + $decoded_manifest = json_decode($manifest_contents, true);
154 + foreach ($decoded_manifest as $key => $value) {
155 + if (substr($value['file'], -3) === '.js') {
156 + wp_register_script(
157 + $key,
158 + $this->env->getUrl('plugin.react_url') . '/build/' . $value['file'],
159 + [],
160 + null,
161 + false,
162 + );
163 + $translation_data = load_script_textdomain($key, 'metricool');
164 + if (!empty($translation_data)) {
165 + $json_translations[] = $translation_data;
166 + }
167 +
168 + if (!empty($value['isEntry']) && $value['isEntry'] === true) {
169 + wp_enqueue_script(
170 + 'metricool-main-script',
171 + $this->env->getUrl('plugin.react_url') . '/build/' . $value['file'],
172 + [],
173 + null,
174 + false,
175 + );
176 + }
177 + }
178 + if (!empty($value['css'])) {
179 + wp_enqueue_style(
180 + 'metricool-tailwind',
181 + $this->env->getUrl('plugin.react_url') . '/build/' . $value['css'][0],
182 + [],
183 + null,
184 + );
185 + }
186 + }
187 + }
188 +
189 + add_filter('script_loader_tag', [$this, 'loadMainScriptsAsModule'], 10, 2);
190 +
191 + wp_localize_script(
192 + 'metricool-main-script',
193 + 'metricool',
194 + ['values' => $this->localizedReactSettings(['json_translations' => $json_translations])],
195 + );
196 + }
197 +
198 + /**
199 + * WordPress doesn't allow for translation of chunks resulting of code
200 + * splitting. Several workarounds have popped up in JetPack and Woocommerce.
201 + * Below is mainly based on the Woocommerce solution, which seems to be the
202 + * simplest approach. Simplicity is king here.
203 + * @see https://wordpress.com/blog/2022/01/06/wordpress-plugin-i18n-webpack-and-composer/
204 + */
205 + private function getReactChunkTranslations(): array
206 + {
207 + $cacheName = 'metricool-react-chunk-translations';
208 + if ($cache = wp_cache_get($cacheName, 'metricool')) {
209 + return $cache;
210 + }
211 +
212 + // get all files from the settings/build folder
213 + $buildDirPath = $this->env->getString('plugin.react_path') . '/build';
214 + $filenames = scandir($buildDirPath);
215 +
216 + $jsFileName = '';
217 + $assetFilename = '';
218 + $jsonTranslations = [];
219 +
220 + // filter the filenames to get the JavaScript and asset filenames
221 + foreach ($filenames as $filename) {
222 + if (strpos($filename, 'index.') === 0) {
223 + if (substr($filename, -3) === '.js') {
224 + $jsFileName = $filename;
225 + } elseif (substr($filename, -10) === '.asset.php') {
226 + $assetFilename = $filename;
227 + }
228 + }
229 +
230 + if (strpos($filename, '.js') === false) {
231 + continue;
232 + }
233 +
234 + // remove extension from $filename
235 + $chunkHandle = str_replace('.js', '', $filename);
236 + // temporarily register the script, so we can get a translations object.
237 + $chunkSource = $this->env->getUrl('plugin.react_url') . '/build/' . $filename;
238 + wp_register_script($chunkHandle, $chunkSource, [], $this->env->getString('plugin.version'), true);
239 +
240 + //as there is no pro version of this plugin, no need to declare a path
241 + $localeData = load_script_textdomain($chunkHandle, 'metricool');
242 + if (!empty($localeData)) {
243 + $jsonTranslations[] = $localeData;
244 + }
245 +
246 + wp_deregister_script($chunkHandle);
247 + }
248 +
249 + if (empty($jsFileName)) {
250 + return [];
251 + }
252 +
253 + $assetFileData = require $buildDirPath . '/' . $assetFilename;
254 + $chunkTranslations = [
255 + 'json_translations' => $jsonTranslations,
256 + 'js_file_name' => $jsFileName,
257 + 'dependencies' => $assetFileData['dependencies'] ?? [],
258 + 'version' => $assetFileData['version'] ?? '',
259 + ];
260 +
261 + wp_cache_set($cacheName, $chunkTranslations, 'metricool');
262 + return $chunkTranslations;
263 + }
264 +
265 + /**
266 + * Build the localization array for the React script with the translations
267 + * @uses apply_filters metricool_localize_dashboard_script
268 + */
269 + private function localizedReactSettings(array $chunkTranslation): array
270 + {
271 + $settings = [
272 + 'nonce' => wp_create_nonce('metricool_nonce'),
273 + 'x_wp_nonce' => wp_create_nonce('wp_rest'),
274 + 'ajax_url' => admin_url('admin-ajax.php'),
275 + 'rest_url' => get_rest_url(),
276 + 'rest_namespace' => $this->env->getString('http.namespace'),
277 + 'rest_version' => $this->env->getString('http.version'),
278 + 'api_url' => trailingslashit(
279 + get_rest_url(null, $this->env->getString('http.namespace') . '/' . $this->env->getString('http.version'))
280 + ),
281 + 'dashboard_url' => $this->env->getString('plugin.dashboard_url'),
282 + 'site_url' => site_url(),
283 + 'assets_url' => $this->env->getUrl('plugin.assets_url'),
284 + 'json_translations' => ($chunkTranslation['json_translations'] ?? []),
285 + 'trusted_urls' => $this->env->get('frontend.trusted_urls'),
286 + 'onboarding' => [
287 + 'state' => $this->service->state(),
288 + 'mode' => $this->service->mode(),
289 + ],
290 + 'support' => $this->env->getUrl('metricool.support'),
291 + 'metricool_base_url' => $this->env->getUrl('metricool.base_url'),
292 + 'metricool_help_url' => $this->env->getUrl('metricool.help_url'),
293 + 'locale' => str_replace("_", "-", get_user_locale()),
294 + 'supported_languages' => $this->languages->all(),
295 + 'google_recaptcha_url' => $this->getGoogleRecaptchaUrl(),
296 + 'google_recaptcha_key' => $this->env->getString('metricool.google_recaptcha_key'),
297 + ];
298 +
299 + if ($this->metricool->hasAuthentication()) {
300 + $settings['account'] = $this->account->fetch();
301 + }
302 +
303 + return apply_filters('metricool_localize_dashboard_script', $settings);
304 + }
305 +
306 + /**
307 + * Get the Google reCAPTCHA URL with the key from the environment config.
308 + */
309 + private function getGoogleRecaptchaUrl(): string
310 + {
311 + return 'https://www.google.com/recaptcha/enterprise.js?render=' . $this->env->getString('metricool.google_recaptcha_key');
312 + }
313 +
314 + public function loadMainScriptsAsModule(string $tag, string $handle): string
315 + {
316 + if (str_contains($handle, 'metricool-main-script') === false) {
317 + return $tag;
318 + }
319 +
320 + return str_replace('<script ', '<script type="module" ', $tag);
321 + }
322 +}