PluginProbe
Extendify / 3.2.3
Extendify v3.2.3
3.2.3 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 All 129 releases
extendify / app / PartnerData.php

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

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