PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 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 All 128 releases
extendify / app / PartnerData.php

PartnerData.php in Extendify 3.2.2, at app/PartnerData.php

477 lines 18.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The Partner Settings
5 */
6
7 namespace Extendify;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 use Extendify\Constants;
12 use Extendify\Shared\Services\Sanitizer;
13
14 /**
15 * Controller for handling partner settings
16 */
17
18 class PartnerData
19 {
20 /**
21 * The partner id
22 *
23 * @var string
24 */
25 public static $id;
26
27 /**
28 * The partner logo
29 *
30 * @var string
31 */
32 public static $logo = '';
33
34 /**
35 * The partner display name
36 *
37 * @var string
38 */
39 public static $name = '';
40
41 /**
42 * The partner colors
43 *
44 * @var string
45 */
46 public static $colors = [];
47
48 /**
49 * The partner configuration.
50 *
51 * @var array
52 */
53 protected static $config = [
54 'showDomainBanner' => false,
55 'showDomainTask' => false,
56 'showSecondaryDomainBanner' => false,
57 'showSecondaryDomainTask' => false,
58 'showPrimaryDomainRecommendationAgent' => false,
59 'showSecondaryDomainRecommendationAgent' => false,
60 'domainTLDs' => ['com', 'net'],
61 'priorityDomainTLDs' => [],
62 'stagingSites' => ['wordpress'],
63 'trialDomains' => [],
64 'domainSearchURL' => '',
65 'showDraft' => false,
66 'showChat' => false,
67 'showAIPageCreation' => false,
68 'mcpConfig' => [],
69 'mcpWriteList' => [],
70 'mcpReadList' => [],
71 'enableImageImports-1-14-6' => false,
72 'disableLibraryAutoOpen' => false,
73 'enableApexDomain' => false,
74 'showLaunch' => false,
75 'showLaunchTitle' => false,
76 'deactivated' => true,
77 'launchRedirectWebsite' => false,
78 'showAILogo' => false,
79 'showProductRecommendations' => false,
80 'productRecommendations' => [
81 'showPartnerBranding' => false,
82 'disabledProducts' => [],
83 'customProducts' => [],
84 ],
85 'license' => 'active',
86 'showAIAgents' => false,
87 'agentAbilitiesAllowlist' => [],
88 'showQuickEdit' => false,
89 // Simple front-end Extendify toolbar (replaces WP core admin
90 // bar for editors who prefer it). Default style is Launch-aware
91 // (simple post-Launch, full before).
92 'showSimpleToolbar' => false,
93 'showImprint' => [],
94 'showLaunchQuestions' => false,
95 'pluginGroupId' => null,
96 'requiredPlugins' => null,
97 'showProductActivation' => [],
98 'useAgentOnboarding' => false,
99 'hidePluginNotifications' => false,
100 'hideLaunchExitLink' => false,
101 'useAutoUpdate' => false,
102 'showLaunchUpdate' => false,
103 'activeTests' => [],
104 'showExtendifyCode' => false,
105 'useComingSoon' => false,
106 'extendifyCodeData' => [
107 'link' => '',
108 'title' => '',
109 'message' => '',
110 'cta-primary' => '',
111 ],
112 'customDesign' => null,
113 'strings' => [],
114 ];
115
116 // phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded
117 /**
118 * Set up and collect partner data
119 *
120 * @return void
121 */
122 public function __construct()
123 {
124 self::load();
125 }
126
127 /**
128 * @return void
129 */
130 public static function load()
131 {
132 self::$id = defined('EXTENDIFY_PARTNER_ID') ? constant('EXTENDIFY_PARTNER_ID') : null;
133 $data = self::getPartnerData();
134 self::$config['showDomainBanner'] = ($data['showDomainBanner'] ?? self::$config['showDomainBanner']);
135 self::$config['showDomainTask'] = ($data['showDomainTask'] ?? self::$config['showDomainTask']);
136 self::$config['showSecondaryDomainTask'] = ($data['showSecondaryDomainTask']
137 ?? self::$config['showSecondaryDomainTask']);
138 self::$config['showSecondaryDomainBanner'] = ($data['showSecondaryDomainBanner']
139 ?? self::$config['showSecondaryDomainBanner']);
140 self::$config['showPrimaryDomainRecommendationAgent'] = ($data['showPrimaryDomainRecommendationAgent']
141 ?? self::$config['showPrimaryDomainRecommendationAgent']);
142 self::$config['showSecondaryDomainRecommendationAgent'] = ($data['showSecondaryDomainRecommendationAgent']
143 ?? self::$config['showSecondaryDomainRecommendationAgent']);
144 self::$config['domainTLDs'] = ($data['domainTLDs'] ?? self::$config['domainTLDs']);
145 self::$config['priorityDomainTLDs'] = ($data['priorityDomainTLDs']
146 ?? self::$config['priorityDomainTLDs']);
147 self::$config['stagingSites'] = array_map('trim', ($data['stagingSites'] ?? self::$config['stagingSites']));
148 self::$config['trialDomains'] = array_map('trim', ($data['trialDomains'] ?? self::$config['trialDomains']));
149 self::$config['domainSearchURL'] = ($data['domainSearchURL'] ?? self::$config['domainSearchURL']);
150 self::$logo = isset($data['logo'][0]['thumbnails']['large']['url'])
151 ? $data['logo'][0]['thumbnails']['large']['url']
152 : self::$logo;
153 self::$config['showDraft'] = ($data['showDraft'] ?? self::$config['showDraft']);
154 self::$config['showChat'] = ($data['showChat'] ?? self::$config['showChat']);
155 self::$config['enableImageImports-1-14-6'] = ($data['enableImageImports-1-14-6']
156 ?? self::$config['enableImageImports-1-14-6']);
157 self::$config['disableLibraryAutoOpen'] = ($data['disableLibraryAutoOpen']
158 ?? self::$config['disableLibraryAutoOpen']);
159 self::$config['enableApexDomain'] = ($data['enableApexDomain'] ?? self::$config['enableApexDomain']);
160 self::$name = ($data['Name'] ?? self::$name);
161 self::$colors = [
162 'backgroundColor' => ($data['backgroundColor'] ?? null),
163 'foregroundColor' => ($data['foregroundColor'] ?? null),
164 'secondaryColor' => ($data['secondaryColor'] ?? ($data['backgroundColor'] ?? null)),
165 'secondaryColorText' => '#ffffff',
166 ];
167 self::$config['showAIPageCreation'] = ($data['showAIPageCreation'] ?? self::$config['showAIPageCreation']);
168 self::$config['mcpConfig'] = ($data['mcpConfig'] ?? self::$config['mcpConfig']);
169 self::$config['mcpWriteList'] = ($data['mcpWriteList'] ?? self::$config['mcpWriteList']);
170 self::$config['mcpReadList'] = ($data['mcpReadList'] ?? self::$config['mcpReadList']);
171 self::$config['showLaunch'] = ($data['showLaunch'] ?? self::$config['showLaunch']);
172 self::$config['showLaunchTitle'] = ($data['showLaunchTitle'] ?? self::$config['showLaunchTitle']);
173 self::$config['deactivated'] = ($data['deactivated'] ?? self::$config['deactivated']);
174 self::$config['launchRedirectWebsite'] = ($data['launchRedirectWebsite']
175 ?? self::$config['launchRedirectWebsite']);
176 self::$config['showAILogo'] = ($data['showAILogo'] ?? self::$config['showAILogo']);
177 self::$config['showProductRecommendations'] = ($data['showProductRecommendations']
178 ?? self::$config['showProductRecommendations']);
179 self::$config['productRecommendations'] = [
180 'showPartnerBranding' => ($data['productRecommendationShowPartnerBranding']
181 ?? self::$config['productRecommendations']['showPartnerBranding']),
182 'disabledProducts' => ($data['productRecommendationDisabledSlugs']
183 ?? self::$config['productRecommendations']['disabledProducts']),
184 'customProducts' => ($data['productRecommendationCustomSlugs']
185 ?? self::$config['productRecommendations']['customProducts']),
186 ];
187 self::$config['license'] = ($data['license'] ?? self::$config['license']);
188 self::$config['showImprint'] = ($data['showImprint'] ?? self::$config['showImprint']);
189 self::$config['showLaunchQuestions'] = ($data['showLaunchQuestions'] ?? self::$config['showLaunchQuestions']);
190 self::$config['showAIAgents'] = ($data['showAIAgents'] ?? self::$config['showAIAgents']);
191 self::$config['agentAbilitiesAllowlist'] = ($data['agentAbilitiesAllowlist']
192 ?? self::$config['agentAbilitiesAllowlist']);
193 self::$config['showQuickEdit'] = ($data['showQuickEdit'] ?? self::$config['showQuickEdit']);
194 self::$config['showSimpleToolbar'] = ($data['showSimpleToolbar']
195 ?? self::$config['showSimpleToolbar']);
196 self::$config['pluginGroupId'] = ($data['pluginGroup'] ?? self::$config['pluginGroupId']);
197 self::$config['requiredPlugins'] = ($data['requiredPlugins'] ?? self::$config['requiredPlugins']);
198 self::$config['showProductActivation'] = ($data['showProductActivation']
199 ?? self::$config['showProductActivation']);
200 self::$config['useAgentOnboarding'] = ($data['useAgentOnboarding'] ?? self::$config['useAgentOnboarding']);
201 self::$config['hidePluginNotifications'] = ($data['hidePluginNotifications']
202 ?? self::$config['hidePluginNotifications']);
203 self::$config['hideLaunchExitLink'] = ($data['hideLaunchExitLink'] ?? self::$config['hideLaunchExitLink']);
204 self::$config['useAutoUpdate'] = ($data['useAutoUpdate'] ?? self::$config['useAutoUpdate']);
205 self::$config['showLaunchUpdate'] = ($data['showLaunchUpdate'] ?? self::$config['showLaunchUpdate']);
206 self::$config['activeTests'] = ($data['activeTests'] ?? self::$config['activeTests']);
207 self::$config['showExtendifyCode'] = ($data['showExtendifyCode'] ?? self::$config['showExtendifyCode']);
208 self::$config['useComingSoon'] = ($data['useComingSoon'] ?? self::$config['useComingSoon']);
209 self::$config['extendifyCodeData'] = ($data['extendifyCodeData'] ?? self::$config['extendifyCodeData']);
210 self::$config['customDesign'] = ($data['customDesign'] ?? self::$config['customDesign']);
211 self::$config['strings'] = ($data['strings'] ?? self::$config['strings']);
212
213 // Add the job hook to fetch the partner data.
214 \add_action('extendify_fetch_partner_data', [self::class, 'fetchPartnerData']);
215 }
216
217 /**
218 * Retrieve partner data from the options table or from the API.
219 *
220 * @return array
221 */
222 public static function getPartnerData()
223 {
224 // Do not make a request without a partner ID (i.e. it's opt in).
225 if (!defined('EXTENDIFY_PARTNER_ID')) {
226 return [];
227 }
228
229 $partnerData = \get_option('extendify_partner_data_v2', 'empty');
230 if ($partnerData !== 'empty') {
231 // We have data, but if it's been 10 minutes, check for new data.
232 $partnerRefresh = \get_transient('extendify_partner_data_cache_check');
233 if (!$partnerRefresh && \is_user_logged_in()) {
234 \did_action('init')
235 ? self::scheduleRefresh()
236 : \add_action('init', [self::class, 'scheduleRefresh']);
237 }
238
239 return array_merge(self::$config, $partnerData);
240 }
241
242 $freshData = self::fetchPartnerData();
243 $mergedData = array_merge(self::$config, $freshData);
244 // Cache here even if empty [] to prevent multiple requests.
245 \update_option('extendify_partner_data_v2', $mergedData);
246 return $mergedData;
247 }
248
249 /**
250 * A token request may be the only visitor a site gets, so it fetches a stale config itself.
251 *
252 * @return void
253 */
254 public static function refreshIfStale()
255 {
256 if (\get_transient('extendify_partner_data_cache_check')) {
257 return;
258 }
259
260 // The 45s timeout the plugin gives its hosts is longer than a waiting MCP client allows.
261 $brief = function ($args) {
262 $args['timeout'] = 5;
263 return $args;
264 };
265 \add_filter('http_request_args', $brief, 101);
266 try {
267 $fetched = self::fetchPartnerData();
268 } finally {
269 \remove_filter('http_request_args', $brief, 101);
270 }
271
272 if ($fetched) {
273 self::load();
274 }
275 }
276
277 /**
278 * @return void
279 */
280 public static function scheduleRefresh()
281 {
282 if (\wp_next_scheduled('extendify_fetch_partner_data')) {
283 return;
284 }
285
286 \wp_schedule_single_event(time(), 'extendify_fetch_partner_data');
287 \spawn_cron();
288 }
289
290 /**
291 * Fetch or refresh the partner data
292 *
293 * @return array
294 */
295 public static function fetchPartnerData()
296 {
297 if (!defined('EXTENDIFY_PARTNER_ID')) {
298 return [];
299 }
300
301 // Set a 10 minute transient to prevent multiple requests.
302 set_transient('extendify_partner_data_cache_check', true, (10 * MINUTE_IN_SECONDS));
303
304 $url = add_query_arg(
305 [
306 'partner' => self::$id,
307 'wp_language' => \get_locale(),
308 'site_url' => \home_url(),
309 'site_id' => \get_option('extendify_site_id', ''),
310 ],
311 Constants::DASHBOARD_HOST . '/api/onboarding/partner-data/'
312 );
313
314 $response = \wp_safe_remote_get($url, ['headers' => ['Accept' => 'application/json']]);
315
316 // If there was an error, we dont update the cache.
317 if (\is_wp_error($response) || \wp_remote_retrieve_response_code($response) !== 200) {
318 return [];
319 }
320
321 $result = json_decode(\wp_remote_retrieve_body($response), true);
322
323 // If the data didn't come back as we expected it to, or if we have an error, don't update the cache.
324 if (
325 json_last_error() !== JSON_ERROR_NONE
326 || !is_array($result)
327 || !array_key_exists('data', $result)
328 || empty($result['data'])
329 ) {
330 return [];
331 }
332
333 $sanitizedData = array_merge(
334 Sanitizer::sanitizeUnknown($result['data']),
335 [
336 'consentTermsCustom' => \sanitize_text_field(htmlentities(
337 ($result['data']['consentTermsCustom'] ?? ''),
338 ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
339 )),
340 'customDesign' => self::sanitizeDesign($result['data']['customDesign'] ?? null),
341 'strings' => self::sanitizeStrings($result['data']['strings'] ?? null),
342 ]
343 );
344
345 // Merge before persisting as this data is accessed directly elsewhere.
346 $mergedData = array_merge(self::$config, $sanitizedData);
347 \update_option('extendify_partner_data_v2', $mergedData);
348
349 return $mergedData;
350 }
351
352 /**
353 * Partner copy overriding the shipped strings.
354 *
355 * Which keys exist is the flow's to know, so only shape and text are checked here.
356 *
357 * @param mixed $strings The map as the partner-data response carried it.
358 * @return array
359 */
360 public static function sanitizeStrings($strings)
361 {
362 return array_map(
363 'sanitize_text_field',
364 self::designEntries($strings, '/^[a-zA-Z][a-zA-Z0-9]*$/', 'is_string')
365 );
366 }
367
368 /**
369 * A design carries GLSL, which the text sanitizers break, so only its shape is checked.
370 *
371 * Empty members are left out rather than kept: json_encode writes an empty
372 * PHP array as [], and the page reads the design as an object.
373 *
374 * @param mixed $design The design as the partner-data response carried it.
375 * @return array|null
376 */
377 private static function sanitizeDesign($design)
378 {
379 if (!is_array($design)) {
380 return null;
381 }
382
383 $shader = ($design['shader'] ?? null);
384 $logo = ($design['logo'] ?? null);
385 $kept = array_filter([
386 'vars' => self::designEntries(
387 ($design['vars'] ?? null),
388 '/^--ext-(ui|tpl)-[a-z0-9-]+$/',
389 function ($value) {
390 // A CSS value is a string, but 0.88 is a natural way to write one.
391 return is_string($value) || is_int($value) || is_float($value);
392 }
393 ),
394 'templates' => self::designEntries(($design['templates'] ?? null), '/^[A-Za-z0-9_-]+$/', 'is_string'),
395 'shader' => is_string($shader) ? \wp_check_invalid_utf8($shader) : '',
396 'logo' => is_string($logo) ? \esc_url_raw($logo) : '',
397 ]);
398
399 return $kept ?: null;
400 }
401
402 private static function designEntries($entries, $keyPattern, callable $accepts)
403 {
404 if (!is_array($entries)) {
405 return [];
406 }
407
408 $kept = [];
409 foreach ($entries as $key => $value) {
410 if (!is_string($key) || !preg_match($keyPattern, $key) || !$accepts($value)) {
411 continue;
412 }
413
414 $kept[$key] = is_string($value) ? \wp_check_invalid_utf8($value) : $value;
415 }
416
417 return $kept;
418 }
419
420 /**
421 * Return colors mapped as css variables
422 *
423 * @return array
424 */
425 public static function cssVariableMapping()
426 {
427 $mapping = [
428 'backgroundColor' => '--ext-banner-main',
429 'foregroundColor' => '--ext-banner-text',
430 'secondaryColor' => '--ext-design-main',
431 'secondaryColorText' => '--ext-design-text',
432 ];
433
434 $cssVariables = [];
435 $adminTheme = \get_user_option('admin_color', get_current_user_id());
436 if (isset($GLOBALS['_wp_admin_css_colors'][$adminTheme])) {
437 $theme = $GLOBALS['_wp_admin_css_colors'][$adminTheme];
438 if (in_array($adminTheme, ['modern', 'blue'], true)) {
439 $cssVariables['--wp-admin-theme-main'] = $theme->colors[1];
440 $cssVariables['--wp-admin-theme-accent'] = $theme->colors[2];
441 } else {
442 $cssVariables['--wp-admin-theme-bg'] = $theme->colors[0];
443 $cssVariables['--wp-admin-theme-main'] = $theme->colors[2];
444 $cssVariables['--wp-admin-theme-accent'] = $theme->colors[3];
445 }
446 }
447
448 // Partner specific colors.
449 foreach ($mapping as $color => $variable) {
450 if (isset(self::$colors[$color])) {
451 $cssVariables[$variable] = self::$colors[$color];
452 }
453 }
454
455 return $cssVariables;
456 }
457
458 /**
459 * Retrieves the value of a setting.
460 *
461 * This method first checks if the setting exists as a static property of the class.
462 * If it does, it returns the value of that property. Otherwise, it looks for the
463 * setting in the config array and returns its value if found.
464 *
465 * @param string $settingKey The key of the setting to retrieve.
466 * @return mixed The value of the setting if found, or null if not found.
467 */
468 public static function setting($settingKey)
469 {
470 if (property_exists(self::class, $settingKey)) {
471 return self::$$settingKey;
472 }
473
474 return (self::$config[$settingKey] ?? null);
475 }
476 }
477