PluginProbe
Extendify / 3.2.0
Extendify v3.2.0
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / app / Shared / Admin.php

Admin.php in Extendify 3.2.0, at app/Shared/Admin.php

463 lines 19.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Help Center Script loader.
5 */
6
7 namespace Extendify\Shared;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 use Extendify\Config;
12 use Extendify\PartnerData;
13 use Extendify\Notifications\Availability;
14 use Extendify\Shared\Controllers\UserSelectionController;
15 use Extendify\Shared\DataProvider\NotificationData;
16 use Extendify\Shared\DataProvider\ResourceData;
17 use Extendify\Shared\Services\AdminMenuList;
18 use Extendify\Shared\Services\ApexDomain\ApexDomain;
19 use Extendify\Shared\Services\Escaper;
20 use Extendify\Shared\Services\PluginDependencies\SimplyBook;
21 use Extendify\Shared\Services\PluginsActivation\Imagify as ImagifyActivation;
22 use Extendify\Shared\Services\PluginsActivation\Metricool as MetricoolActivation;
23 use Extendify\Shared\Services\PluginsActivation\SimplyBook as SimplyBookActivation;
24 use Extendify\Shared\Services\PluginsActivation\TranslatePress as TranslatePressActivation;
25 use Extendify\Shared\Services\SiteImages;
26 use Extendify\SiteSettings;
27 use Extendify\Shared\Controllers\ImageGenerationController;
28 use Extendify\Shared\DataProvider\ProductsData;
29
30 /**
31 * This class handles any file loading for the admin area.
32 */
33
34 class Admin
35 {
36 /**
37 * Adds various actions to set up the page
38 *
39 * @return void
40 */
41 public function __construct()
42 {
43 \add_action('init', [$this, 'addExtraMetaFields']);
44 \add_action('admin_enqueue_scripts', [$this, 'loadGlobalScripts']);
45 \add_action('wp_enqueue_scripts', [$this, 'loadGlobalScripts']);
46 \add_action('wp_ajax_search-install-plugins', [$this, 'recordPluginsSearchTerms'], -1);
47 \add_action('rest_api_init', [$this, 'recordBlocksSearchTerms']);
48 \add_action('wp_ajax_query-themes', [$this, 'recordThemesSearchTerms'], -1);
49 AdminMenuList::init();
50 \add_action('simplybook_activation', [SimplyBook::class, 'getIndustryCode'], 10, 0);
51 }
52
53 // phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
54 /**
55 * Adds scripts to every page
56 *
57 * @return void
58 */
59 public function loadGlobalScripts()
60 {
61 \wp_enqueue_media();
62
63 $this->updateUserMeta();
64
65 $version = constant('EXTENDIFY_DEVMODE') ? uniqid() : Config::$version;
66
67 /**
68 * Enqueue shared JavaScript files if they exist in the asset manifest
69 * Ensures proper loading order: vendors -> common
70 */
71
72 // Enqueue the vendor chunk first.
73 if (isset(Config::$assetManifest['extendify-vendors.js'])) {
74 wp_enqueue_script(
75 'extendify-vendors',
76 EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-vendors.js'],
77 [],
78 $version,
79 true
80 // Load in footer.
81 );
82 }
83
84 // Enqueue the common chunk next, dependent on vendors.
85 if (isset(Config::$assetManifest['extendify-common.js'])) {
86 wp_enqueue_script(
87 'extendify-common',
88 EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-common.js'],
89 ['extendify-vendors'],
90 $version,
91 true
92 );
93 }
94
95 /*
96 * Loads a unique runtime generated by Webpack to manage all the other generated scripts.
97 * Without this runtime, the other Extendify scripts won't load.
98 */
99 $scriptAssetPath = EXTENDIFY_PATH . 'public/build/' . Config::$assetManifest['extendify-runtime.php'];
100 $fallback = [
101 'dependencies' => [],
102 'version' => $version,
103 ];
104 $scriptAsset = file_exists($scriptAssetPath) ? require $scriptAssetPath : $fallback;
105
106 foreach ($scriptAsset['dependencies'] as $style) {
107 \wp_enqueue_style($style);
108 }
109
110 \wp_enqueue_script(
111 Config::$slug . '-runtime-scripts',
112 EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-runtime.js'],
113 $scriptAsset['dependencies'],
114 $scriptAsset['version'],
115 true
116 );
117
118 $scriptAssetPath = EXTENDIFY_PATH . 'public/build/' . Config::$assetManifest['extendify-shared.php'];
119 $fallback = [
120 'dependencies' => [],
121 'version' => $version,
122 ];
123 $scriptAsset = file_exists($scriptAssetPath) ? require $scriptAssetPath : $fallback;
124
125 foreach ($scriptAsset['dependencies'] as $style) {
126 \wp_enqueue_style($style);
127 }
128
129 \wp_enqueue_script(
130 Config::$slug . '-shared-scripts',
131 EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-shared.js'],
132 $scriptAsset['dependencies'],
133 $scriptAsset['version'],
134 true
135 );
136
137 $partnerData = PartnerData::getPartnerData();
138 $userConsent = get_user_meta(get_current_user_id(), 'extendify_ai_consent', true);
139 $htmlAllowlist = [
140 'a' => [
141 'target' => [],
142 'href' => [],
143 'rel' => [],
144 ],
145 ];
146
147 if (!function_exists('get_plugins')) {
148 require_once ABSPATH . 'wp-admin/includes/plugin.php';
149 }
150
151 $activePlugins = array_values(\get_option('active_plugins', []));
152 $activePluginSlugs = array_map(function ($plugin) {
153 return dirname($plugin);
154 }, $activePlugins);
155 $productActivationPlugins = PartnerData::setting('showProductActivation') ?? [];
156
157 $productActivationPlugins = array_filter(
158 $productActivationPlugins,
159 function ($plugin) use ($activePluginSlugs) {
160 return in_array($plugin['slug'], $activePluginSlugs);
161 }
162 );
163
164 $activations = [
165 ImagifyActivation::class,
166 MetricoolActivation::class,
167 SimplyBookActivation::class,
168 TranslatePressActivation::class,
169 ];
170
171 $productActivationPlugins = array_map(function ($plugin) use ($activations) {
172 foreach ($activations as $activation) {
173 if ($plugin['slug'] === $activation::slug()) {
174 return array_merge($plugin, [
175 'scriptData' => $activation::scriptData(),
176 'eligible' => $activation::isEligible(),
177 'endpoint' => Config::$slug . '/' . Config::$apiVersion
178 . $activation::createAccountRoute(),
179 ]);
180 }
181 }
182
183 return $plugin;
184 }, $productActivationPlugins);
185
186 // Visitors read the stamp and the alt text, so both resolve in the site's locale.
187 $switchedLocale = \switch_to_locale(\get_locale());
188 // translators: Short label stamped onto an image marking it as AI-generated.
189 // Give the all-caps form your language uses.
190 $aiImageLabel = \_x('AI GENERATED', 'uppercase', 'extendify-local');
191 // translators: %s is the image description. Alt text prefix marking an image as AI-generated.
192 $aiImageAltPattern = \__('AI Generated: %s', 'extendify-local');
193 if ($switchedLocale) {
194 \restore_previous_locale();
195 }
196
197 $extendifyCodeData = (array) PartnerData::setting('extendifyCodeData');
198
199 // esc_url() strips the {DESCRIPTION} braces; shield the placeholder across it.
200 $extendifyCodeLink = str_replace(
201 '__EXTENDIFY_DESCRIPTION__',
202 '{DESCRIPTION}',
203 \esc_url_raw(str_replace(
204 '{DESCRIPTION}',
205 '__EXTENDIFY_DESCRIPTION__',
206 (string) ($extendifyCodeData['link'] ?? '')
207 ))
208 );
209
210 \wp_add_inline_script(
211 Config::$slug . '-shared-scripts',
212 'window.extSharedData = ' . \wp_json_encode([
213 'root' => \esc_url_raw(rest_url(Config::$slug . '/' . Config::$apiVersion)),
214 'homeUrl' => \esc_url_raw(\get_home_url()),
215 'adminUrl' => \esc_url_raw(\admin_url()),
216 'nonce' => \esc_attr(\wp_create_nonce('wp_rest')),
217 'devbuild' => (bool) constant('EXTENDIFY_DEVMODE'),
218 'assetPath' => \esc_url(EXTENDIFY_URL . 'public/assets'),
219 'siteId' => \esc_attr(\get_option('extendify_site_id', '')),
220 'siteCreatedAt' => \esc_attr(SiteSettings::getSiteCreatedAt()),
221 'themeSlug' => \esc_attr(\get_option('stylesheet')),
222 'version' => \esc_attr(Config::$version),
223 'siteTitle' => \esc_attr(\get_bloginfo('name')),
224 'siteProfile' => \get_option('extendify_site_profile', []),
225 // Empty when the launch-time image fetch failed.
226 'siteImages' => SiteImages::normalize(\get_option('extendify_site_images', [])),
227 'wpLanguage' => \esc_attr(\get_locale()),
228 'aiImageLabel' => $aiImageLabel,
229 'aiImageAltPattern' => $aiImageAltPattern,
230 'wpVersion' => \esc_attr(\get_bloginfo('version')),
231 'isBlockTheme' => function_exists('wp_is_block_theme') ? (bool) wp_is_block_theme() : false,
232 'userId' => \esc_attr(\get_current_user_id()),
233 // phpcs:ignore WordPress.Security.NonceVerification
234 'userEmail' => isset($_GET['extendify-launch-success'])
235 ? \esc_attr(\wp_get_current_user()->user_email)
236 : null,
237 'partnerLogo' => \esc_attr(PartnerData::$logo),
238 'partnerId' => \esc_attr(PartnerData::$id),
239 'partnerName' => \esc_attr(PartnerData::$name),
240 'launchDataLegacy' => \wp_json_encode((UserSelectionController::get()->get_data() ?? [])),
241 'resourceData' => \wp_json_encode((new ResourceData())->getData()),
242 'notifications' => \wp_json_encode(
243 Availability::available(NotificationData::get())
244 ),
245 'notificationState' => \get_user_meta(
246 \get_current_user_id(),
247 'extendify_notification_state',
248 true
249 ) ?: ['cards' => []],
250 'showAIConsent' => isset($partnerData['showAIConsent']) ? (bool) $partnerData['showAIConsent'] : false,
251 'showChat' => (bool) (PartnerData::setting('showChat') || constant('EXTENDIFY_DEVMODE')),
252 'useAgentOnboarding' => (bool) (
253 PartnerData::setting('useAgentOnboarding') ||
254 Config::preview('agent-onboarding') ||
255 constant('EXTENDIFY_DEVMODE')
256 ),
257 'showAIPageCreation' => (bool) (
258 PartnerData::setting('showAIPageCreation') || constant('EXTENDIFY_DEVMODE')
259 ),
260 'showAILogo' => (bool) PartnerData::setting('showAILogo'),
261 'showImprint' => array_map('esc_attr', (array) PartnerData::setting('showImprint')),
262 'showProductActivation' => array_values($productActivationPlugins),
263 'consentTermsCustom' => \wp_kses((html_entity_decode(
264 ($partnerData['consentTermsCustom'] ?? ''),
265 ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
266 ) ?? ''), $htmlAllowlist),
267 'userGaveConsent' => $userConsent ? (bool) $userConsent : false,
268 'installedPlugins' => array_map('esc_attr', array_keys(\get_plugins())),
269 'activePlugins' => $activePlugins,
270 'installedPluginsSlugs' => array_values(array_filter(array_map(function ($p) {
271 return $p['TextDomain'] ?? '';
272 }, \get_plugins()))),
273 'frontPage' => \esc_attr(\get_option('page_on_front', 0)),
274 'globalStylesPostID' => \esc_attr(\WP_Theme_JSON_Resolver::get_user_global_styles_post_id()),
275 'showLocalizedCopy' => (bool) array_key_exists('showLocalizedCopy', $partnerData),
276 'activity' => \wp_json_encode(\get_option('extendify_shared_activity', null)),
277 'showDraft' => isset($partnerData['showDraft']) ? (bool) $partnerData['showDraft'] : false,
278 'showLaunch' => Config::$showLaunch,
279 'phpVersion' => \esc_attr(PHP_VERSION),
280 'apexDomain' => PartnerData::setting('enableApexDomain')
281 ? rawurlencode(ApexDomain::getApexDomain(\get_home_url()))
282 : null,
283 'launchCompletedAt' => \esc_attr(\get_option('extendify_onboarding_completed', false)),
284 'showSiteQuestions' => (bool) (
285 PartnerData::setting('showLaunchQuestions') || Config::preview('launch-questions')
286 ),
287 'products' => ProductsData::get(),
288 'showAIAgents' => (bool) (PartnerData::setting('showAIAgents') || Config::preview('ai-agent')),
289 'showExtendifyCode' => (bool) PartnerData::setting('showExtendifyCode'),
290 'extendifyCodeData' => [
291 'link' => $extendifyCodeLink,
292 'title' => $extendifyCodeData['title'] ?? '',
293 'message' => $extendifyCodeData['message'] ?? '',
294 'ctaPrimary' => $extendifyCodeData['cta-primary'] ?? '',
295 ],
296 'pluginGroupId' => Escaper::recursiveEscAttr(PartnerData::setting('pluginGroupId')),
297 'adminPagesMenuList' => get_option('_transient_extendify_admin_pages_menu', []),
298 'globalState' => ImageGenerationController::get()->get_data(),
299 ]),
300 'before'
301 );
302
303 \wp_set_script_translations('extendify-common', 'extendify-local', EXTENDIFY_PATH . 'languages/js');
304 \wp_set_script_translations(
305 Config::$slug . '-shared-scripts',
306 'extendify-local',
307 EXTENDIFY_PATH . 'languages/js'
308 );
309
310 \wp_enqueue_style(
311 Config::$slug . '-shared-common-styles',
312 EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-shared.css'],
313 [],
314 Config::$version,
315 'all'
316 );
317 $cssColorVars = PartnerData::cssVariableMapping();
318 $cssString = implode('; ', array_map(function ($k, $v) {
319 return "$k: $v";
320 }, array_keys($cssColorVars), $cssColorVars));
321 \wp_add_inline_style(
322 Config::$slug . '-shared-common-styles',
323 wp_strip_all_tags(":root { $cssString; }")
324 );
325 }
326
327 /**
328 * Adds additional meta fields to post types
329 *
330 * @return void
331 */
332 public function addExtraMetaFields()
333 {
334 // Add a tag to pages that were made with Launch.
335 register_post_meta('page', 'made_with_extendify_launch', [
336 'single' => true,
337 'type' => 'boolean',
338 'show_in_rest' => true,
339 ]);
340 register_post_meta('post', 'made_with_extendify_launch', [
341 'single' => true,
342 'type' => 'boolean',
343 'show_in_rest' => true,
344 ]);
345
346 // Marks AI-generated images for EU AI Act disclosure.
347 register_post_meta('attachment', 'extendify_ai_generated', [
348 'single' => true,
349 'type' => 'boolean',
350 'show_in_rest' => true,
351 ]);
352 }
353
354 /**
355 * Records plugin search terms from the WordPress plugin search page
356 * Stores terms in the 'extendify_plugin_search_terms' option
357 *
358 * @return void
359 */
360 public function recordPluginsSearchTerms()
361 {
362 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.NonceVerification.Missing
363 $searchTerm = isset($_POST['s']) ? \sanitize_text_field(\wp_unslash(urldecode($_POST['s']))) : '';
364 if (empty($searchTerm)) {
365 return;
366 }
367
368 $searchTerms = \get_option('extendify_plugin_search_terms', []);
369 $searchTerms[] = $searchTerm;
370 $searchTerms = array_unique($searchTerms);
371
372 \update_option('extendify_plugin_search_terms', $searchTerms);
373 }
374
375 /**
376 * Records block search terms from the WordPress the editor's block search
377 * Stores terms in the 'extendify_block_search_terms' option
378 *
379 * @return void
380 */
381 public function recordBlocksSearchTerms()
382 {
383 // Exits early if it is not a REST API request.
384 if (!\wp_is_serving_rest_request()) {
385 return;
386 }
387
388 // Exits early if it is not a GET request.
389 if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] !== 'GET') {
390 return;
391 }
392
393 $wp = $GLOBALS['wp'];
394
395 $restRoute = ($wp->query_vars['rest_route'] ?? '');
396 // Exits early if it's not the blocks search route.
397 if ($restRoute !== '/wp/v2/block-directory/search') {
398 return;
399 }
400
401 $searchTerm = \sanitize_text_field(\wp_unslash(($wp->query_vars['term'] ?? '')));
402 // Exits early if the search term is empty or is not user input.
403 if (empty($searchTerm) || str_starts_with($searchTerm, 'block:')) {
404 return;
405 }
406
407 // Get current search term from the url and merge it with existing search terms.
408 $searchTerms = \get_option('extendify_block_search_terms', []);
409 \update_option('extendify_block_search_terms', array_merge($searchTerms, [$searchTerm]));
410 }
411
412 /**
413 * Updates the user meta to disable the welcome guide from the Gutenberg editor
414 * and close the pattern modal.
415 *
416 * @return void
417 */
418 public function updateUserMeta()
419 {
420 $currentPreferences = get_user_meta(get_current_user_id(), 'wp_persisted_preferences', true);
421 if (!$currentPreferences) {
422 $currentPreferences = [];
423 }
424
425 $postPreferences = array_key_exists('core/edit-post', $currentPreferences)
426 ? $currentPreferences['core/edit-post']
427 : [];
428 $corePreferences = array_key_exists('core', $currentPreferences) ? $currentPreferences['core'] : [];
429
430 $newPreferences = array_merge($currentPreferences, [
431 'core/edit-post' => array_merge($postPreferences, ['welcomeGuide' => false]),
432 'core' => array_merge($corePreferences, ['enableChoosePatternModal' => false]),
433 '_modified' => wp_date('Y-m-d\TH:i:s.v\Z'),
434 ]);
435
436 update_user_meta(get_current_user_id(), 'wp_persisted_preferences', $newPreferences);
437 }
438
439 /**
440 * Records search terms used when browsing themes in the admin interface.
441 *
442 * This method listens for the 'query-themes' AJAX action, extracts the search term
443 * from the request, and stores it in the 'extendify_theme_search_terms' option.
444 * Duplicate terms are filtered out.
445 *
446 * @return void
447 */
448 public function recordThemesSearchTerms()
449 {
450 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.NonceVerification.Missing
451 $searchTerm = \sanitize_text_field(\wp_unslash(urldecode(($_POST['request']['search'] ?? ''))));
452 if (empty($searchTerm)) {
453 return;
454 }
455
456 $searchTerms = \get_option('extendify_theme_search_terms', []);
457 $searchTerms[] = $searchTerm;
458 $searchTerms = array_unique($searchTerms);
459
460 \update_option('extendify_theme_search_terms', $searchTerms);
461 }
462 }
463