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
← All changes | app/PartnerData.php +159 -8 3.1.1 → 3.2.2 View file →
@@ -59,16 +59,21 @@
59 59 'showSecondaryDomainRecommendationAgent' => false,
60 60 'domainTLDs' => ['com', 'net'],
61 61 'priorityDomainTLDs' => [],
62 62 'stagingSites' => ['wordpress'],
63 + 'trialDomains' => [],
63 64 'domainSearchURL' => '',
64 65 'showDraft' => false,
65 66 'showChat' => false,
66 67 'showAIPageCreation' => false,
68 + 'mcpConfig' => [],
69 + 'mcpWriteList' => [],
70 + 'mcpReadList' => [],
67 71 'enableImageImports-1-14-6' => false,
68 72 'disableLibraryAutoOpen' => false,
69 73 'enableApexDomain' => false,
70 74 'showLaunch' => false,
75 + 'showLaunchTitle' => false,
71 76 'deactivated' => true,
72 77 'launchRedirectWebsite' => false,
73 78 'showAILogo' => false,
74 79 'showProductRecommendations' => false,
@@ -78,8 +83,9 @@
78 83 'customProducts' => [],
79 84 ],
80 85 'license' => 'active',
81 86 'showAIAgents' => false,
87 + 'agentAbilitiesAllowlist' => [],
82 88 'showQuickEdit' => false,
83 89 // Simple front-end Extendify toolbar (replaces WP core admin
84 90 // bar for editors who prefer it). Default style is Launch-aware
85 91 // (simple post-Launch, full before).
@@ -92,9 +98,20 @@
92 98 'useAgentOnboarding' => false,
93 99 'hidePluginNotifications' => false,
94 100 'hideLaunchExitLink' => false,
95 101 'useAutoUpdate' => false,
102 + 'showLaunchUpdate' => false,
96 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' => [],
97 114 ];
98 115
99 116 // phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded
100 117 /**
@@ -103,8 +120,16 @@
103 120 * @return void
104 121 */
105 122 public function __construct()
106 123 {
124 + self::load();
125 + }
126 +
127 + /**
128 + * @return void
129 + */
130 + public static function load()
131 + {
107 132 self::$id = defined('EXTENDIFY_PARTNER_ID') ? constant('EXTENDIFY_PARTNER_ID') : null;
108 133 $data = self::getPartnerData();
109 134 self::$config['showDomainBanner'] = ($data['showDomainBanner'] ?? self::$config['showDomainBanner']);
110 135 self::$config['showDomainTask'] = ($data['showDomainTask'] ?? self::$config['showDomainTask']);
@@ -119,8 +144,9 @@
119 144 self::$config['domainTLDs'] = ($data['domainTLDs'] ?? self::$config['domainTLDs']);
120 145 self::$config['priorityDomainTLDs'] = ($data['priorityDomainTLDs']
121 146 ?? self::$config['priorityDomainTLDs']);
122 147 self::$config['stagingSites'] = array_map('trim', ($data['stagingSites'] ?? self::$config['stagingSites']));
148 + self::$config['trialDomains'] = array_map('trim', ($data['trialDomains'] ?? self::$config['trialDomains']));
123 149 self::$config['domainSearchURL'] = ($data['domainSearchURL'] ?? self::$config['domainSearchURL']);
124 150 self::$logo = isset($data['logo'][0]['thumbnails']['large']['url'])
125 151 ? $data['logo'][0]['thumbnails']['large']['url']
126 152 : self::$logo;
@@ -138,9 +164,13 @@
138 164 'secondaryColor' => ($data['secondaryColor'] ?? ($data['backgroundColor'] ?? null)),
139 165 'secondaryColorText' => '#ffffff',
140 166 ];
141 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']);
142 171 self::$config['showLaunch'] = ($data['showLaunch'] ?? self::$config['showLaunch']);
172 + self::$config['showLaunchTitle'] = ($data['showLaunchTitle'] ?? self::$config['showLaunchTitle']);
143 173 self::$config['deactivated'] = ($data['deactivated'] ?? self::$config['deactivated']);
144 174 self::$config['launchRedirectWebsite'] = ($data['launchRedirectWebsite']
145 175 ?? self::$config['launchRedirectWebsite']);
146 176 self::$config['showAILogo'] = ($data['showAILogo'] ?? self::$config['showAILogo']);
@@ -157,8 +187,10 @@
157 187 self::$config['license'] = ($data['license'] ?? self::$config['license']);
158 188 self::$config['showImprint'] = ($data['showImprint'] ?? self::$config['showImprint']);
159 189 self::$config['showLaunchQuestions'] = ($data['showLaunchQuestions'] ?? self::$config['showLaunchQuestions']);
160 190 self::$config['showAIAgents'] = ($data['showAIAgents'] ?? self::$config['showAIAgents']);
191 + self::$config['agentAbilitiesAllowlist'] = ($data['agentAbilitiesAllowlist']
192 + ?? self::$config['agentAbilitiesAllowlist']);
161 193 self::$config['showQuickEdit'] = ($data['showQuickEdit'] ?? self::$config['showQuickEdit']);
162 194 self::$config['showSimpleToolbar'] = ($data['showSimpleToolbar']
163 195 ?? self::$config['showSimpleToolbar']);
164 196 self::$config['pluginGroupId'] = ($data['pluginGroup'] ?? self::$config['pluginGroupId']);
@@ -169,9 +201,15 @@
169 201 self::$config['hidePluginNotifications'] = ($data['hidePluginNotifications']
170 202 ?? self::$config['hidePluginNotifications']);
171 203 self::$config['hideLaunchExitLink'] = ($data['hideLaunchExitLink'] ?? self::$config['hideLaunchExitLink']);
172 204 self::$config['useAutoUpdate'] = ($data['useAutoUpdate'] ?? self::$config['useAutoUpdate']);
205 + self::$config['showLaunchUpdate'] = ($data['showLaunchUpdate'] ?? self::$config['showLaunchUpdate']);
173 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']);
174 212
175 213 // Add the job hook to fetch the partner data.
176 214 \add_action('extendify_fetch_partner_data', [self::class, 'fetchPartnerData']);
177 215 }
@@ -191,15 +229,12 @@
191 229 $partnerData = \get_option('extendify_partner_data_v2', 'empty');
192 230 if ($partnerData !== 'empty') {
193 231 // We have data, but if it's been 10 minutes, check for new data.
194 232 $partnerRefresh = \get_transient('extendify_partner_data_cache_check');
195 - if (!$partnerRefresh && \is_admin()) {
196 - \add_action('init', function () {
197 - if (!\wp_next_scheduled('extendify_fetch_partner_data')) {
198 - \wp_schedule_single_event(time(), 'extendify_fetch_partner_data');
199 - \spawn_cron();
200 - }
201 - });
233 + if (!$partnerRefresh && \is_user_logged_in()) {
234 + \did_action('init')
235 + ? self::scheduleRefresh()
236 + : \add_action('init', [self::class, 'scheduleRefresh']);
202 237 }
203 238
204 239 return array_merge(self::$config, $partnerData);
205 240 }
@@ -211,8 +246,49 @@
211 246 return $mergedData;
212 247 }
213 248
214 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 + /**
215 291 * Fetch or refresh the partner data
216 292 *
217 293 * @return array
218 294 */
@@ -255,9 +331,16 @@
255 331 }
256 332
257 333 $sanitizedData = array_merge(
258 334 Sanitizer::sanitizeUnknown($result['data']),
259 - ['consentTermsCustom' => \sanitize_text_field(htmlentities(($result['data']['consentTermsCustom'] ?? '')))]
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 + ]
260 343 );
261 344
262 345 // Merge before persisting as this data is accessed directly elsewhere.
263 346 $mergedData = array_merge(self::$config, $sanitizedData);
@@ -263,8 +346,76 @@
263 346 $mergedData = array_merge(self::$config, $sanitizedData);
264 347 \update_option('extendify_partner_data_v2', $mergedData);
265 348
266 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;
267 418 }
268 419
269 420 /**
270 421 * Return colors mapped as css variables