PluginProbe
Metricool – Social media and site statistics / trunk
Metricool – Social media and site statistics vtrunk
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 trunk, at app/Controllers/DashboardController.php

323 lines 12.7 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\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 }
323