PluginProbe
Extendify / 3.2.0
Extendify v3.2.0
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.0, at app/PartnerData.php

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