PluginProbe
Site Reviews / trunk
Site Reviews vtrunk
8.3.1 8.3.0 8.2.2 8.2.1 8.2.0 8.1.0 8.0.13 8.0.12 8.0.11 trunk 1.2.2 2.17.1 3.5.4 4.7.0 5.25.1 6.11.8 7.0.10 7.0.11 7.0.12 7.0.13 7.0.14 7.0.15 7.0.16 7.0.17 7.0.18 All 54 releases
site-reviews / plugin / Addons / Controller.php

Controller.php in Site Reviews trunk, at plugin/Addons/Controller.php

551 lines 17.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace GeminiLabs\SiteReviews\Addons;
4
5 use GeminiLabs\SiteReviews\Controllers\AbstractController;
6 use GeminiLabs\SiteReviews\Database\OptionManager;
7 use GeminiLabs\SiteReviews\Helpers\Arr;
8 use GeminiLabs\SiteReviews\Helpers\Cast;
9 use GeminiLabs\SiteReviews\Helpers\Str;
10 use GeminiLabs\SiteReviews\Install;
11 use GeminiLabs\SiteReviews\Modules\Assets\AssetCss;
12 use GeminiLabs\SiteReviews\Modules\Assets\AssetJs;
13 use GeminiLabs\SiteReviews\Modules\Html\Template;
14 use GeminiLabs\SiteReviews\Modules\Translation;
15 use GeminiLabs\SiteReviews\Modules\Translator;
16 use GeminiLabs\SiteReviews\Role;
17
18 abstract class Controller extends AbstractController
19 {
20 /**
21 * @action admin_enqueue_scripts
22 */
23 public function enqueueAdminAssets(): void
24 {
25 if ($this->isReviewAdminPage()) {
26 $this->enqueueAsset('css', ['suffix' => 'admin']);
27 $this->enqueueAsset('js', ['suffix' => 'admin']);
28 }
29 }
30
31 /**
32 * @action wp_enqueue_scripts
33 */
34 public function enqueuePublicAssets(): void
35 {
36 if (!glsr(AssetCss::class)->canOptimize() || !glsr(AssetCss::class)->isOptimized()) {
37 $this->enqueueAsset('css');
38 }
39 if (!glsr(AssetJs::class)->canOptimize() || !glsr(AssetJs::class)->isOptimized()) {
40 $this->enqueueAsset('js', ['in_footer' => true]);
41 }
42 }
43
44 /**
45 * @filter plugin_action_links_{$this->app()->id}/{$this->app()->id}.php
46 */
47 public function filterActionLinks(array $links): array
48 {
49 $actions = [];
50 if (glsr()->hasPermission('settings') && !empty($this->app()->config('settings'))) {
51 $actions['settings'] = glsr_admin_link("settings.addons.{$this->app()->slug}", _x('Settings', 'admin-text', 'site-reviews'));
52 }
53 return array_merge($actions, $links);
54 }
55
56 /**
57 * @filter site-reviews/capabilities
58 */
59 public function filterCapabilities(array $capabilities): array
60 {
61 if (!$this->app()->hasPostType()) {
62 return $capabilities;
63 }
64 $defaults = [
65 'create_posts',
66 'delete_others_posts',
67 'delete_posts',
68 'delete_private_posts',
69 'delete_published_posts',
70 'edit_others_posts',
71 'edit_posts',
72 'edit_private_posts',
73 'edit_published_posts',
74 'publish_posts',
75 'read_private_posts',
76 ];
77 foreach ($defaults as $capability) {
78 $capabilities[] = str_replace('post', $this->app()->post_type, $capability);
79 }
80 return $capabilities;
81 }
82
83 /**
84 * @filter site-reviews/config
85 */
86 public function filterConfigPath(string $path): string
87 {
88 $prefix = trailingslashit($this->app()->id);
89 return str_contains($path, $prefix)
90 ? $prefix.str_replace($prefix, '', $path)
91 : $path;
92 }
93
94 /**
95 * @filter site-reviews/addon/documentation
96 */
97 public function filterDocumentation(array $documentation): array
98 {
99 $notice = glsr()->build('views/partials/addons/support-notice', [
100 'addon_id' => $this->app()->id,
101 ]);
102 $support = $this->app()->build('views/documentation');
103 $documentation[$this->app()->id] = $notice.$support;
104 return $documentation;
105 }
106
107 /**
108 * @filter site-reviews/path
109 */
110 public function filterFilePaths(string $path, string $file): string
111 {
112 $addonPrefix = trailingslashit($this->app()->id);
113 return str_starts_with($file, $addonPrefix)
114 ? $this->app()->path(Str::replaceFirst($addonPrefix, '', $file))
115 : $path;
116 }
117
118 /**
119 * @param string $translation
120 * @param string $single
121 *
122 * @filter gettext_{$this->app()->id}
123 */
124 public function filterGettext($translation, $single): string
125 {
126 $translation = Cast::toString($translation);
127 return glsr(Translator::class)->translate($translation, $this->app()->id, [
128 'single' => Cast::toString($single),
129 ]);
130 }
131
132 /**
133 * @param string $translation
134 * @param string $single
135 * @param string $context
136 *
137 * @filter gettext_with_context_{$this->app()->id}
138 */
139 public function filterGettextWithContext($translation, $single, $context): string
140 {
141 $translation = Cast::toString($translation);
142 return glsr(Translator::class)->translate($translation, $this->app()->id, [
143 'context' => Cast::toString($context),
144 'single' => Cast::toString($single),
145 ]);
146 }
147
148 /**
149 * @filter site-reviews/enqueue/public/localize
150 */
151 public function filterLocalizedPublicVariables(array $variables): array
152 {
153 $variables['addons'][$this->app()->id] = null;
154 return $variables;
155 }
156
157 /**
158 * @param string $translation
159 * @param string $single
160 * @param string $plural
161 * @param int $number
162 *
163 * @filter ngettext_{$this->app()->id}
164 */
165 public function filterNgettext($translation, $single, $plural, $number): string
166 {
167 $translation = Cast::toString($translation);
168 return glsr(Translator::class)->translate($translation, $this->app()->id, [
169 'number' => Cast::toInt($number),
170 'plural' => Cast::toString($plural),
171 'single' => Cast::toString($single),
172 ]);
173 }
174
175 /**
176 * @param string $translation
177 * @param string $single
178 * @param string $plural
179 * @param int $number
180 * @param string $context
181 *
182 * @filter ngettext_with_context_{$this->app()->id}
183 */
184 public function filterNgettextWithContext($translation, $single, $plural, $number, $context): string
185 {
186 $translation = Cast::toString($translation);
187 return glsr(Translator::class)->translate($translation, $this->app()->id, [
188 'context' => Cast::toString($context),
189 'number' => Cast::toInt($number),
190 'plural' => Cast::toString($plural),
191 'single' => Cast::toString($single),
192 ]);
193 }
194
195 /**
196 * @filter {$this->app()->hookPrefix()}/render/view
197 */
198 public function filterRenderView(string $view): string
199 {
200 $style = glsr(OptionManager::class)->get('settings.general.style', 'default');
201 $styledView = sprintf('views/styles/%s/%s', $style, basename($view));
202 if (file_exists($this->app()->file($styledView))) {
203 return $styledView;
204 }
205 return $view;
206 }
207
208 /**
209 * @filter site-reviews/roles
210 */
211 public function filterRoles(array $roles): array
212 {
213 if (!$this->app()->hasPostType()) {
214 return $roles;
215 }
216 $defaults = [
217 'administrator' => [
218 'create_posts',
219 'delete_others_posts',
220 'delete_posts',
221 'delete_private_posts',
222 'delete_published_posts',
223 'edit_others_posts',
224 'edit_posts',
225 'edit_private_posts',
226 'edit_published_posts',
227 'publish_posts',
228 'read_private_posts',
229 ],
230 'editor' => [
231 'create_posts',
232 'delete_others_posts',
233 'delete_posts',
234 'delete_private_posts',
235 'delete_published_posts',
236 'edit_others_posts',
237 'edit_posts',
238 'edit_private_posts',
239 'edit_published_posts',
240 'publish_posts',
241 'read_private_posts',
242 ],
243 'author' => [
244 'create_posts',
245 'delete_posts',
246 'delete_published_posts',
247 'edit_posts',
248 'edit_published_posts',
249 'publish_posts',
250 ],
251 'contributor' => [
252 'delete_posts',
253 'edit_posts',
254 ],
255 ];
256 foreach ($defaults as $role => $capabilities) {
257 if (!array_key_exists($role, $roles)) {
258 continue;
259 }
260 foreach ($capabilities as $capability) {
261 $roles[$role][] = str_replace('post', $this->app()->post_type, $capability);
262 }
263 }
264 return $roles;
265 }
266
267 /**
268 * @filter plugin_row_meta
269 */
270 public function filterRowMeta(array $links, string $file): array
271 {
272 if ($file !== $this->app()->basename) {
273 return $links;
274 }
275 $actions = [];
276 if (glsr()->hasPermission('documentation')) {
277 $actions['documentation'] = glsr_admin_link('documentation.addons', [
278 'data-expand' => "#addon-{$this->app()->id}",
279 'text' => _x('Documentation', 'admin-text', 'site-reviews'),
280 ]);
281 }
282 $actions['support'] = glsr_premium_link('support', [
283 'aria-label' => _x('Visit premium customer support', 'admin-text', 'site-reviews'),
284 'text' => _x('Support', 'admin-text', 'site-reviews'),
285 ]);
286 return array_merge($links, $actions);
287 }
288
289 /**
290 * Merges the addon's settings config into the shared settings form,
291 * remounting keys at the addon's composed-view path. A depends_on key
292 * names a setting and is mounted the same way, so a config can be written
293 * entirely in short keys and stay correct in both shapes.
294 *
295 * @filter site-reviews/settings
296 */
297 public function filterSettings(array $settings): array
298 {
299 $config = [];
300 $standalone = "settings.addons.{$this->app()->slug}.";
301 $prefix = "settings.{$this->app()->settingsPath()}.";
302 $mount = function (string $key) use ($standalone, $prefix): string {
303 if (str_starts_with($key, 'settings.addons.')) {
304 return str_starts_with($key, $standalone)
305 ? $prefix.substr($key, strlen($standalone)) // this addon's own standalone-era spelling
306 : $key; // another addon's, already qualified
307 }
308 return $prefix.Str::removePrefix($key, 'settings.');
309 };
310 foreach ($this->app()->config('settings') as $key => $values) {
311 if (!empty($values['depends_on']) && is_array($values['depends_on'])) {
312 $values['depends_on'] = array_combine(
313 array_map($mount, array_keys($values['depends_on'])),
314 $values['depends_on']
315 );
316 }
317 $config[$mount($key)] = $values;
318 }
319 return array_merge($config, $settings);
320 }
321
322 /**
323 * @filter site-reviews/addon/subsubsub
324 */
325 public function filterSubsubsub(array $subsubsub): array
326 {
327 return $subsubsub;
328 }
329
330 /**
331 * @filter site-reviews/translation/entries
332 */
333 public function filterTranslationEntries(array $entries): array
334 {
335 $addon = $this->app();
336 if ($addon instanceof Addon && $addon->hostedBy()) {
337 return $entries; // the host's catalog carries the hosted addon's strings
338 }
339 $potFile = $addon->path("{$addon->languages}/{$addon->id}.pot");
340 return glsr(Translation::class)->extractEntriesFromPotFile($potFile, $addon->id, $entries);
341 }
342
343 /**
344 * @filter site-reviews/translator/domains
345 */
346 public function filterTranslatorDomains(array $domains): array
347 {
348 $addon = $this->app();
349 if ($addon instanceof Addon && $addon->hostedBy()) {
350 return $domains; // hosted addons translate under the host's domain
351 }
352 return [...$domains, $addon->id];
353 }
354
355 /**
356 * @action {$this->app()->hookPrefix()}/activated
357 */
358 public function install(): void
359 {
360 }
361
362 /**
363 * Migrates legacy addon settings (stored in the core plugin's option) to
364 * the addon's own option. One-shot: skipped once the addon's option exists.
365 * The legacy subtree is copied, not deleted — it is shadowed by the composed
366 * settings view and garbage-collected on the next settings save; leaving it
367 * in place protects downgrades to older addon versions.
368 *
369 * @action admin_init:5
370 * @action {$this->app()->hookPrefix()}/activated
371 */
372 public function migrateOptions(): void
373 {
374 $addon = $this->app();
375 if (!$addon instanceof Addon || 'settings' !== $addon->storagePath()) {
376 return;
377 }
378 if ($addon->isHost()) {
379 return; // a host imports its own state
380 }
381 $key = $addon->storageKey();
382 $stored = get_option($key);
383 if (is_array($stored) && !OptionManager::isAddonRow($stored)) {
384 // The key holds the addon's own data (see OptionManager::isAddonRow).
385 // Strip the stale stamps v8.2.0/v8.2.1 wrote into it; the addon's
386 // settings stay in the parent's row until the addon vacates the key.
387 $stripped = array_diff_key($stored, array_flip(['version', 'version_upgraded_from']));
388 if ($stripped === $stored) {
389 return; // the addon's own data; the key is taken
390 }
391 if (!empty($stripped)) {
392 update_option($key, $stripped, true);
393 return; // the addon's own data, now clean of the stale stamps
394 }
395 delete_option($key); // only stale stamps held the key; migrate below
396 $stored = false;
397 }
398 if (false !== $stored) {
399 return;
400 }
401 $legacy = Arr::consolidate(glsr(OptionManager::class)->wp(OptionManager::databaseKey(), []));
402 $legacy = Arr::getAs('array', $legacy, "settings.addons.{$addon->slug}");
403 add_option($key, [
404 'settings' => $legacy,
405 'version' => $addon->version,
406 ], '', true);
407 glsr()->discard('settings'); // recompose the settings view
408 }
409
410 /**
411 * @action admin_init
412 */
413 public function onActivation(): void
414 {
415 $option = glsr()->prefix."activated_{$this->app()->id}";
416 if (empty(get_option($option))) {
417 update_option($option, true);
418 if ($this->app()->post_type) {
419 glsr(Role::class)->reset($this->filterRoles([
420 'administrator' => [],
421 'author' => [],
422 'contributor' => [],
423 'editor' => [],
424 ]));
425 }
426 $this->app()->action('activated');
427 }
428 }
429
430 /**
431 * @action deactivate_{$this->app()->basename}
432 */
433 public function onDeactivation(bool $isNetworkDeactivation): void
434 {
435 $option = glsr()->prefix."activated_{$this->app()->id}";
436 if (!$isNetworkDeactivation) {
437 delete_option($option);
438 $this->app()->action('deactivated');
439 return;
440 }
441 foreach (glsr(Install::class)->sites() as $siteId) {
442 switch_to_blog($siteId);
443 delete_option($option);
444 $this->app()->action('deactivated');
445 restore_current_blog();
446 }
447 }
448
449 /**
450 * @action init
451 */
452 public function registerLanguages(): void
453 {
454 $path = plugin_basename($this->app()->path());
455 $path = trailingslashit("{$path}/{$this->app()->languages}");
456 load_plugin_textdomain($this->app()->id, false, $path);
457 }
458
459 /**
460 * @action init
461 */
462 public function registerShortcodes(): void
463 {
464 }
465
466 /**
467 * @action admin_init
468 */
469 public function registerTinymcePopups(): void
470 {
471 }
472
473 /**
474 * @action widgets_init
475 */
476 public function registerWidgets(): void
477 {
478 }
479
480 /**
481 * @action site-reviews/settings/{$this->app()->slug}
482 */
483 public function renderSettings(string $rows): void
484 {
485 glsr(Template::class)->render("{$this->app()->id}/views/settings", [
486 'context' => [
487 'rows' => $rows,
488 'title' => $this->app()->name,
489 ],
490 ]);
491 }
492
493 protected function buildAssetArgs(string $ext, array $args = []): array
494 {
495 $args = wp_parse_args($args, [
496 'defer' => true,
497 'in_footer' => false,
498 'suffix' => '',
499 ]);
500 $suffix = Str::prefix($args['suffix'], '-');
501 $path = "assets/{$this->app()->id}{$suffix}.{$ext}";
502 if (!file_exists($this->app()->path($path)) || !in_array($ext, ['css', 'js'])) {
503 return [];
504 }
505 $suffix = Str::prefix($args['suffix'], '/');
506 $dependencies = Arr::get($args, 'dependencies', [glsr()->id.$suffix]);
507 $handle = $this->app()->id.$suffix;
508 $funcArgs = [
509 $handle,
510 $this->app()->url($path),
511 Arr::consolidate($dependencies),
512 $this->app()->version,
513 ];
514 if ('js' === $ext && wp_validate_boolean($args['in_footer'])) {
515 $funcArgs[] = true; // load script in the footer
516 }
517 return $funcArgs;
518 }
519
520 protected function enqueueAsset(string $extension, array $args = []): void
521 {
522 $defer = Arr::get($args, 'defer', false);
523 if ($args = $this->buildAssetArgs($extension, $args)) {
524 if ('js' === $extension) {
525 call_user_func_array('wp_register_script', $args);
526 call_user_func('wp_enqueue_script', $args[0]);
527 } else {
528 call_user_func_array('wp_register_style', $args);
529 call_user_func('wp_enqueue_style', $args[0]);
530 }
531 if (wp_validate_boolean($defer)) {
532 wp_script_add_data($args[0], 'strategy', 'defer');
533 }
534 }
535 }
536
537 protected function registerAsset(string $extension, array $args = []): void
538 {
539 $defer = Arr::get($args, 'defer', false);
540 if ($args = $this->buildAssetArgs($extension, $args)) {
541 $function = 'js' === $extension
542 ? 'wp_register_script'
543 : 'wp_register_style';
544 call_user_func_array($function, $args);
545 if (wp_validate_boolean($defer)) {
546 wp_script_add_data($args[0], 'strategy', 'defer');
547 }
548 }
549 }
550 }
551