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
← All changes | app/PartnerData.php +184 -8 3.0.6 → 3.2.3 View file →
@@ -54,19 +54,26 @@
54 54 'showDomainBanner' => false,
55 55 'showDomainTask' => false,
56 56 'showSecondaryDomainBanner' => false,
57 57 'showSecondaryDomainTask' => false,
58 + 'showPrimaryDomainRecommendationAgent' => false,
59 + 'showSecondaryDomainRecommendationAgent' => false,
58 60 'domainTLDs' => ['com', 'net'],
59 61 'priorityDomainTLDs' => [],
60 62 'stagingSites' => ['wordpress'],
63 + 'trialDomains' => [],
61 64 'domainSearchURL' => '',
62 65 'showDraft' => false,
63 66 'showChat' => false,
64 67 'showAIPageCreation' => false,
68 + 'mcpConfig' => [],
69 + 'mcpWriteList' => [],
70 + 'mcpReadList' => [],
65 71 'enableImageImports-1-14-6' => false,
66 72 'disableLibraryAutoOpen' => false,
67 73 'enableApexDomain' => false,
68 74 'showLaunch' => false,
75 + 'showLaunchTitle' => false,
69 76 'deactivated' => true,
70 77 'launchRedirectWebsite' => false,
71 78 'showAILogo' => false,
72 79 'showProductRecommendations' => false,
@@ -76,15 +83,36 @@
76 83 'customProducts' => [],
77 84 ],
78 85 'license' => 'active',
79 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,
80 93 'showImprint' => [],
81 94 'showLaunchQuestions' => false,
82 95 'pluginGroupId' => null,
83 96 'requiredPlugins' => null,
97 + 'showProductActivation' => [],
84 98 'useAgentOnboarding' => false,
85 99 'hidePluginNotifications' => false,
86 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' => [],
87 115 ];
88 116
89 117 // phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded
90 118 /**
@@ -93,8 +121,16 @@
93 121 * @return void
94 122 */
95 123 public function __construct()
96 124 {
125 + self::load();
126 + }
127 +
128 + /**
129 + * @return void
130 + */
131 + public static function load()
132 + {
97 133 self::$id = defined('EXTENDIFY_PARTNER_ID') ? constant('EXTENDIFY_PARTNER_ID') : null;
98 134 $data = self::getPartnerData();
99 135 self::$config['showDomainBanner'] = ($data['showDomainBanner'] ?? self::$config['showDomainBanner']);
100 136 self::$config['showDomainTask'] = ($data['showDomainTask'] ?? self::$config['showDomainTask']);
@@ -101,12 +137,17 @@
101 137 self::$config['showSecondaryDomainTask'] = ($data['showSecondaryDomainTask']
102 138 ?? self::$config['showSecondaryDomainTask']);
103 139 self::$config['showSecondaryDomainBanner'] = ($data['showSecondaryDomainBanner']
104 140 ?? self::$config['showSecondaryDomainBanner']);
141 + self::$config['showPrimaryDomainRecommendationAgent'] = ($data['showPrimaryDomainRecommendationAgent']
142 + ?? self::$config['showPrimaryDomainRecommendationAgent']);
143 + self::$config['showSecondaryDomainRecommendationAgent'] = ($data['showSecondaryDomainRecommendationAgent']
144 + ?? self::$config['showSecondaryDomainRecommendationAgent']);
105 145 self::$config['domainTLDs'] = ($data['domainTLDs'] ?? self::$config['domainTLDs']);
106 146 self::$config['priorityDomainTLDs'] = ($data['priorityDomainTLDs']
107 147 ?? self::$config['priorityDomainTLDs']);
108 148 self::$config['stagingSites'] = array_map('trim', ($data['stagingSites'] ?? self::$config['stagingSites']));
149 + self::$config['trialDomains'] = array_map('trim', ($data['trialDomains'] ?? self::$config['trialDomains']));
109 150 self::$config['domainSearchURL'] = ($data['domainSearchURL'] ?? self::$config['domainSearchURL']);
110 151 self::$logo = isset($data['logo'][0]['thumbnails']['large']['url'])
111 152 ? $data['logo'][0]['thumbnails']['large']['url']
112 153 : self::$logo;
@@ -124,9 +165,13 @@
124 165 'secondaryColor' => ($data['secondaryColor'] ?? ($data['backgroundColor'] ?? null)),
125 166 'secondaryColorText' => '#ffffff',
126 167 ];
127 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']);
128 172 self::$config['showLaunch'] = ($data['showLaunch'] ?? self::$config['showLaunch']);
173 + self::$config['showLaunchTitle'] = ($data['showLaunchTitle'] ?? self::$config['showLaunchTitle']);
129 174 self::$config['deactivated'] = ($data['deactivated'] ?? self::$config['deactivated']);
130 175 self::$config['launchRedirectWebsite'] = ($data['launchRedirectWebsite']
131 176 ?? self::$config['launchRedirectWebsite']);
132 177 self::$config['showAILogo'] = ($data['showAILogo'] ?? self::$config['showAILogo']);
@@ -143,14 +188,31 @@
143 188 self::$config['license'] = ($data['license'] ?? self::$config['license']);
144 189 self::$config['showImprint'] = ($data['showImprint'] ?? self::$config['showImprint']);
145 190 self::$config['showLaunchQuestions'] = ($data['showLaunchQuestions'] ?? self::$config['showLaunchQuestions']);
146 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']);
147 197 self::$config['pluginGroupId'] = ($data['pluginGroup'] ?? self::$config['pluginGroupId']);
148 198 self::$config['requiredPlugins'] = ($data['requiredPlugins'] ?? self::$config['requiredPlugins']);
199 + self::$config['showProductActivation'] = ($data['showProductActivation']
200 + ?? self::$config['showProductActivation']);
149 201 self::$config['useAgentOnboarding'] = ($data['useAgentOnboarding'] ?? self::$config['useAgentOnboarding']);
150 202 self::$config['hidePluginNotifications'] = ($data['hidePluginNotifications']
151 203 ?? self::$config['hidePluginNotifications']);
152 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']);
153 215
154 216 // Add the job hook to fetch the partner data.
155 217 \add_action('extendify_fetch_partner_data', [self::class, 'fetchPartnerData']);
156 218 }
@@ -170,15 +232,12 @@
170 232 $partnerData = \get_option('extendify_partner_data_v2', 'empty');
171 233 if ($partnerData !== 'empty') {
172 234 // We have data, but if it's been 10 minutes, check for new data.
173 235 $partnerRefresh = \get_transient('extendify_partner_data_cache_check');
174 - if (!$partnerRefresh && \is_admin()) {
175 - \add_action('init', function () {
176 - if (!\wp_next_scheduled('extendify_fetch_partner_data')) {
177 - \wp_schedule_single_event(time(), 'extendify_fetch_partner_data');
178 - \spawn_cron();
179 - }
180 - });
236 + if (!$partnerRefresh && \is_user_logged_in()) {
237 + \did_action('init')
238 + ? self::scheduleRefresh()
239 + : \add_action('init', [self::class, 'scheduleRefresh']);
181 240 }
182 241
183 242 return array_merge(self::$config, $partnerData);
184 243 }
@@ -190,8 +249,49 @@
190 249 return $mergedData;
191 250 }
192 251
193 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 + /**
194 294 * Fetch or refresh the partner data
195 295 *
196 296 * @return array
197 297 */
@@ -208,8 +308,9 @@
208 308 [
209 309 'partner' => self::$id,
210 310 'wp_language' => \get_locale(),
211 311 'site_url' => \home_url(),
312 + 'site_id' => \get_option('extendify_site_id', ''),
212 313 ],
213 314 Constants::DASHBOARD_HOST . '/api/onboarding/partner-data/'
214 315 );
215 316
@@ -233,9 +334,16 @@
233 334 }
234 335
235 336 $sanitizedData = array_merge(
236 337 Sanitizer::sanitizeUnknown($result['data']),
237 - ['consentTermsCustom' => \sanitize_text_field(htmlentities(($result['data']['consentTermsCustom'] ?? '')))]
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 + ]
238 346 );
239 347
240 348 // Merge before persisting as this data is accessed directly elsewhere.
241 349 $mergedData = array_merge(self::$config, $sanitizedData);
@@ -241,8 +349,76 @@
241 349 $mergedData = array_merge(self::$config, $sanitizedData);
242 350 \update_option('extendify_partner_data_v2', $mergedData);
243 351
244 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;
245 421 }
246 422
247 423 /**
248 424 * Return colors mapped as css variables