PluginProbe
Metricool – Social media and site statistics / 2.0.2
Metricool – Social media and site statistics v2.0.2
2.1.0 2.0.2 2.0.1 2.0.0 1.27 trunk
metricool / app / Controllers / DashboardController.php

DashboardController.php in Metricool – Social media and site statistics 2.0.2, at app/Controllers/DashboardController.php

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