PluginProbe
Extendify / 3.1.3
Extendify v3.1.3
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.3, at app/Shared/Admin.php

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