PluginProbe
Extendify / 3.1.4
Extendify v3.1.4
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.1.4, at app/Shared/Admin.php

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