PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
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 0.7.0 All 126 releases
extendify / app / Shared / Admin.php

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

461 lines 19.1 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 ]);
178 }
179 }
180
181 return $plugin;
182 }, $productActivationPlugins);
183
184 // Visitors read the stamp and the alt text, so both resolve in the site's locale.
185 $switchedLocale = \switch_to_locale(\get_locale());
186 // translators: Short label stamped onto an image marking it as AI-generated.
187 // Give the all-caps form your language uses.
188 $aiImageLabel = \_x('AI GENERATED', 'uppercase', 'extendify-local');
189 // translators: %s is the image description. Alt text prefix marking an image as AI-generated.
190 $aiImageAltPattern = \__('AI Generated: %s', 'extendify-local');
191 if ($switchedLocale) {
192 \restore_previous_locale();
193 }
194
195 $extendifyCodeData = (array) PartnerData::setting('extendifyCodeData');
196
197 // esc_url() strips the {DESCRIPTION} braces; shield the placeholder across it.
198 $extendifyCodeLink = str_replace(
199 '__EXTENDIFY_DESCRIPTION__',
200 '{DESCRIPTION}',
201 \esc_url_raw(str_replace(
202 '{DESCRIPTION}',
203 '__EXTENDIFY_DESCRIPTION__',
204 (string) ($extendifyCodeData['link'] ?? '')
205 ))
206 );
207
208 \wp_add_inline_script(
209 Config::$slug . '-shared-scripts',
210 'window.extSharedData = ' . \wp_json_encode([
211 'root' => \esc_url_raw(rest_url(Config::$slug . '/' . Config::$apiVersion)),
212 'homeUrl' => \esc_url_raw(\get_home_url()),
213 'adminUrl' => \esc_url_raw(\admin_url()),
214 'nonce' => \esc_attr(\wp_create_nonce('wp_rest')),
215 'devbuild' => (bool) constant('EXTENDIFY_DEVMODE'),
216 'assetPath' => \esc_url(EXTENDIFY_URL . 'public/assets'),
217 'siteId' => \esc_attr(\get_option('extendify_site_id', '')),
218 'siteCreatedAt' => \esc_attr(SiteSettings::getSiteCreatedAt()),
219 'themeSlug' => \esc_attr(\get_option('stylesheet')),
220 'version' => \esc_attr(Config::$version),
221 'siteTitle' => \esc_attr(\get_bloginfo('name')),
222 'siteProfile' => \get_option('extendify_site_profile', []),
223 // Empty when the launch-time image fetch failed.
224 'siteImages' => SiteImages::normalize(\get_option('extendify_site_images', [])),
225 'wpLanguage' => \esc_attr(\get_locale()),
226 'aiImageLabel' => $aiImageLabel,
227 'aiImageAltPattern' => $aiImageAltPattern,
228 'wpVersion' => \esc_attr(\get_bloginfo('version')),
229 'isBlockTheme' => function_exists('wp_is_block_theme') ? (bool) wp_is_block_theme() : false,
230 'userId' => \esc_attr(\get_current_user_id()),
231 // phpcs:ignore WordPress.Security.NonceVerification
232 'userEmail' => isset($_GET['extendify-launch-success'])
233 ? \esc_attr(\wp_get_current_user()->user_email)
234 : null,
235 'partnerLogo' => \esc_attr(PartnerData::$logo),
236 'partnerId' => \esc_attr(PartnerData::$id),
237 'partnerName' => \esc_attr(PartnerData::$name),
238 'launchDataLegacy' => \wp_json_encode((UserSelectionController::get()->get_data() ?? [])),
239 'resourceData' => \wp_json_encode((new ResourceData())->getData()),
240 'notifications' => \wp_json_encode(
241 Availability::available(NotificationData::get())
242 ),
243 'notificationState' => \get_user_meta(
244 \get_current_user_id(),
245 'extendify_notification_state',
246 true
247 ) ?: ['cards' => []],
248 'showAIConsent' => isset($partnerData['showAIConsent']) ? (bool) $partnerData['showAIConsent'] : false,
249 'showChat' => (bool) (PartnerData::setting('showChat') || constant('EXTENDIFY_DEVMODE')),
250 'useAgentOnboarding' => (bool) (
251 PartnerData::setting('useAgentOnboarding') ||
252 Config::preview('agent-onboarding') ||
253 constant('EXTENDIFY_DEVMODE')
254 ),
255 'showAIPageCreation' => (bool) (
256 PartnerData::setting('showAIPageCreation') || constant('EXTENDIFY_DEVMODE')
257 ),
258 'showAILogo' => (bool) PartnerData::setting('showAILogo'),
259 'showImprint' => array_map('esc_attr', (array) PartnerData::setting('showImprint')),
260 'showProductActivation' => array_values($productActivationPlugins),
261 'consentTermsCustom' => \wp_kses((html_entity_decode(
262 ($partnerData['consentTermsCustom'] ?? ''),
263 ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
264 ) ?? ''), $htmlAllowlist),
265 'userGaveConsent' => $userConsent ? (bool) $userConsent : false,
266 'installedPlugins' => array_map('esc_attr', array_keys(\get_plugins())),
267 'activePlugins' => $activePlugins,
268 'installedPluginsSlugs' => array_values(array_filter(array_map(function ($p) {
269 return $p['TextDomain'] ?? '';
270 }, \get_plugins()))),
271 'frontPage' => \esc_attr(\get_option('page_on_front', 0)),
272 'globalStylesPostID' => \esc_attr(\WP_Theme_JSON_Resolver::get_user_global_styles_post_id()),
273 'showLocalizedCopy' => (bool) array_key_exists('showLocalizedCopy', $partnerData),
274 'activity' => \wp_json_encode(\get_option('extendify_shared_activity', null)),
275 'showDraft' => isset($partnerData['showDraft']) ? (bool) $partnerData['showDraft'] : false,
276 'showLaunch' => Config::$showLaunch,
277 'phpVersion' => \esc_attr(PHP_VERSION),
278 'apexDomain' => PartnerData::setting('enableApexDomain')
279 ? rawurlencode(ApexDomain::getApexDomain(\get_home_url()))
280 : null,
281 'launchCompletedAt' => \esc_attr(\get_option('extendify_onboarding_completed', false)),
282 'showSiteQuestions' => (bool) (
283 PartnerData::setting('showLaunchQuestions') || Config::preview('launch-questions')
284 ),
285 'products' => ProductsData::get(),
286 'showAIAgents' => (bool) (PartnerData::setting('showAIAgents') || Config::preview('ai-agent')),
287 'showExtendifyCode' => (bool) PartnerData::setting('showExtendifyCode'),
288 'extendifyCodeData' => [
289 'link' => $extendifyCodeLink,
290 'title' => $extendifyCodeData['title'] ?? '',
291 'message' => $extendifyCodeData['message'] ?? '',
292 'ctaPrimary' => $extendifyCodeData['cta-primary'] ?? '',
293 ],
294 'pluginGroupId' => Escaper::recursiveEscAttr(PartnerData::setting('pluginGroupId')),
295 'adminPagesMenuList' => get_option('_transient_extendify_admin_pages_menu', []),
296 'globalState' => ImageGenerationController::get()->get_data(),
297 ]),
298 'before'
299 );
300
301 \wp_set_script_translations('extendify-common', 'extendify-local', EXTENDIFY_PATH . 'languages/js');
302 \wp_set_script_translations(
303 Config::$slug . '-shared-scripts',
304 'extendify-local',
305 EXTENDIFY_PATH . 'languages/js'
306 );
307
308 \wp_enqueue_style(
309 Config::$slug . '-shared-common-styles',
310 EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-shared.css'],
311 [],
312 Config::$version,
313 'all'
314 );
315 $cssColorVars = PartnerData::cssVariableMapping();
316 $cssString = implode('; ', array_map(function ($k, $v) {
317 return "$k: $v";
318 }, array_keys($cssColorVars), $cssColorVars));
319 \wp_add_inline_style(
320 Config::$slug . '-shared-common-styles',
321 wp_strip_all_tags(":root { $cssString; }")
322 );
323 }
324
325 /**
326 * Adds additional meta fields to post types
327 *
328 * @return void
329 */
330 public function addExtraMetaFields()
331 {
332 // Add a tag to pages that were made with Launch.
333 register_post_meta('page', 'made_with_extendify_launch', [
334 'single' => true,
335 'type' => 'boolean',
336 'show_in_rest' => true,
337 ]);
338 register_post_meta('post', 'made_with_extendify_launch', [
339 'single' => true,
340 'type' => 'boolean',
341 'show_in_rest' => true,
342 ]);
343
344 // Marks AI-generated images for EU AI Act disclosure.
345 register_post_meta('attachment', 'extendify_ai_generated', [
346 'single' => true,
347 'type' => 'boolean',
348 'show_in_rest' => true,
349 ]);
350 }
351
352 /**
353 * Records plugin search terms from the WordPress plugin search page
354 * Stores terms in the 'extendify_plugin_search_terms' option
355 *
356 * @return void
357 */
358 public function recordPluginsSearchTerms()
359 {
360 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.NonceVerification.Missing
361 $searchTerm = isset($_POST['s']) ? \sanitize_text_field(\wp_unslash(urldecode($_POST['s']))) : '';
362 if (empty($searchTerm)) {
363 return;
364 }
365
366 $searchTerms = \get_option('extendify_plugin_search_terms', []);
367 $searchTerms[] = $searchTerm;
368 $searchTerms = array_unique($searchTerms);
369
370 \update_option('extendify_plugin_search_terms', $searchTerms);
371 }
372
373 /**
374 * Records block search terms from the WordPress the editor's block search
375 * Stores terms in the 'extendify_block_search_terms' option
376 *
377 * @return void
378 */
379 public function recordBlocksSearchTerms()
380 {
381 // Exits early if it is not a REST API request.
382 if (!\wp_is_serving_rest_request()) {
383 return;
384 }
385
386 // Exits early if it is not a GET request.
387 if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] !== 'GET') {
388 return;
389 }
390
391 $wp = $GLOBALS['wp'];
392
393 $restRoute = ($wp->query_vars['rest_route'] ?? '');
394 // Exits early if it's not the blocks search route.
395 if ($restRoute !== '/wp/v2/block-directory/search') {
396 return;
397 }
398
399 $searchTerm = \sanitize_text_field(\wp_unslash(($wp->query_vars['term'] ?? '')));
400 // Exits early if the search term is empty or is not user input.
401 if (empty($searchTerm) || str_starts_with($searchTerm, 'block:')) {
402 return;
403 }
404
405 // Get current search term from the url and merge it with existing search terms.
406 $searchTerms = \get_option('extendify_block_search_terms', []);
407 \update_option('extendify_block_search_terms', array_merge($searchTerms, [$searchTerm]));
408 }
409
410 /**
411 * Updates the user meta to disable the welcome guide from the Gutenberg editor
412 * and close the pattern modal.
413 *
414 * @return void
415 */
416 public function updateUserMeta()
417 {
418 $currentPreferences = get_user_meta(get_current_user_id(), 'wp_persisted_preferences', true);
419 if (!$currentPreferences) {
420 $currentPreferences = [];
421 }
422
423 $postPreferences = array_key_exists('core/edit-post', $currentPreferences)
424 ? $currentPreferences['core/edit-post']
425 : [];
426 $corePreferences = array_key_exists('core', $currentPreferences) ? $currentPreferences['core'] : [];
427
428 $newPreferences = array_merge($currentPreferences, [
429 'core/edit-post' => array_merge($postPreferences, ['welcomeGuide' => false]),
430 'core' => array_merge($corePreferences, ['enableChoosePatternModal' => false]),
431 '_modified' => wp_date('Y-m-d\TH:i:s.v\Z'),
432 ]);
433
434 update_user_meta(get_current_user_id(), 'wp_persisted_preferences', $newPreferences);
435 }
436
437 /**
438 * Records search terms used when browsing themes in the admin interface.
439 *
440 * This method listens for the 'query-themes' AJAX action, extracts the search term
441 * from the request, and stores it in the 'extendify_theme_search_terms' option.
442 * Duplicate terms are filtered out.
443 *
444 * @return void
445 */
446 public function recordThemesSearchTerms()
447 {
448 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.NonceVerification.Missing
449 $searchTerm = \sanitize_text_field(\wp_unslash(urldecode(($_POST['request']['search'] ?? ''))));
450 if (empty($searchTerm)) {
451 return;
452 }
453
454 $searchTerms = \get_option('extendify_theme_search_terms', []);
455 $searchTerms[] = $searchTerm;
456 $searchTerms = array_unique($searchTerms);
457
458 \update_option('extendify_theme_search_terms', $searchTerms);
459 }
460 }
461