PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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 / PartnerData.php

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

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