PluginProbe
Extendify / 3.0.6
Extendify v3.0.6
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 / Agent / Admin.php

Admin.php in Extendify 3.0.6, at app/Agent/Admin.php

482 lines 18.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Admin.
5 */
6
7 namespace Extendify\Agent;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 use Extendify\Agent\Controllers\ChatHistoryController;
12 use Extendify\Agent\Controllers\TourController;
13 use Extendify\Agent\Controllers\WorkflowHistoryController;
14 use Extendify\Config;
15 use Extendify\Shared\Services\Escaper;
16 use Extendify\Agent\TagBlocks;
17 use Extendify\Agent\TagTemplateParts;
18 use Extendify\Agent\Controllers\SiteNavigationController;
19 use Extendify\PartnerData;
20 use Extendify\Shared\DataProvider\ProductsData;
21
22 /**
23 * This class handles any file loading for the admin area.
24 */
25 class Admin
26 {
27 /**
28 * Adds various actions to set up the page
29 *
30 * @return void
31 */
32 public function __construct()
33 {
34 \add_action('admin_enqueue_scripts', [$this, 'loadScriptsAndStyles']);
35 \add_action('wp_enqueue_scripts', [$this, 'loadScriptsAndStyles']);
36 ChatHistoryController::init();
37 WorkflowHistoryController::init();
38
39 // Tag blocks so we can identify them later
40 TagBlocks::init();
41 TagTemplateParts::init();
42
43 // Add the site navigation ids to the navigation blocks
44 SiteNavigationController::init();
45 }
46
47 /**
48 * Adds various JS scripts and styles
49 *
50 * @return void
51 */
52 public function loadScriptsAndStyles()
53 {
54 $version = constant('EXTENDIFY_DEVMODE') ? uniqid() : Config::$version;
55 $scriptAssetPath = EXTENDIFY_PATH . 'public/build/' . Config::$assetManifest['extendify-agent.php'];
56 $fallback = [
57 'dependencies' => [],
58 'version' => $version,
59 ];
60 $scriptAsset = file_exists($scriptAssetPath) ? require $scriptAssetPath : $fallback;
61
62 foreach ($scriptAsset['dependencies'] as $style) {
63 \wp_enqueue_style($style);
64 }
65
66 \wp_enqueue_script(
67 Config::$slug . '-agent-scripts',
68 EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-agent.js'],
69 array_merge([Config::$slug . '-shared-scripts'], $scriptAsset['dependencies']),
70 $scriptAsset['version'],
71 true
72 );
73
74 $context = [
75 'adminPage' => function_exists('get_current_screen') && ($screen = get_current_screen())
76 ? \esc_attr($screen->id)
77 : null,
78 'postId' => (int) $this->getCurrentPostId(),
79 'postTitle' => \esc_attr(\get_the_title($this->getCurrentPostId())),
80 'postType' => \esc_attr(\get_post_type($this->getCurrentPostId())),
81 'postUrl' => \esc_url(\get_permalink($this->getCurrentPostId())),
82 'isFrontPage' => (bool) \is_front_page(),
83 'postStatus' => \esc_attr(\get_post_status((int) $this->getCurrentPostId())),
84 'isBlogPage' => (bool) \is_home(),
85 'themeSlug' => \esc_attr(\wp_get_theme()->get_stylesheet()),
86 'hasThemeVariations' => (bool) $this->hasThemeVariations(),
87 'isBlockTheme' => function_exists('wp_is_block_theme') ? (bool) wp_is_block_theme() : false,
88 'wordPressVersion' => \esc_attr(\get_bloginfo('version')),
89 'usingBlockEditor' => function_exists('use_block_editor_for_post') ?
90 (bool) use_block_editor_for_post($this->getCurrentPostId()) :
91 false,
92 'isOnEditorOrFSE' => $this->isGutenbergOrFse(),
93 'activePlugins' => array_values(\get_option('active_plugins', [])),
94 // Whether the user is using the vibes experience or not.
95 'isUsingVibes' => (bool) file_exists(EXTENDIFY_PATH . 'src/Launch/_data/block-style-variations.json') &&
96 version_compare(wp_get_theme("extendable")->get('Version'), '2.0.32', '>='),
97 'siteTitle' => \esc_attr(\get_bloginfo('name')),
98 'siteDescription' => \esc_attr(\get_bloginfo('description')),
99 'themePresets' => $this->getThemePresets(),
100 ];
101 $recommendations = ProductsData::get() ?? [];
102 $pluginRecommendations = array_filter($recommendations, function ($item) {
103 return in_array('ai-agent', $item['slots'] ?? [], true) && $item['ctaType'] === 'plugin';
104 });
105 $mappedPluginRecommendations = array_values(array_map(function ($item) {
106 return [
107 'title' => $item['title'] ?? '',
108 'slug' => $item['ctaPluginSlug'] ?? $item['slug'] ?? '',
109 'description' => $item['aiDescription'] ?? $item['description'] ?? '',
110 'redirectTo' => $item['pluginSetupUrl'] ?? '', // this is a partial setup URL not full one.
111 ];
112 }, $pluginRecommendations));
113 $agentContext = [
114 'availableAdminPages' => get_option('_transient_extendify_admin_pages_menu', []),
115 'pluginRecommendations' => $mappedPluginRecommendations,
116 ];
117 $abilities = [
118 'canEditPost' => (bool) \current_user_can('edit_post', \get_queried_object_id()),
119 // TODO: this may be true for a user, while they still can't edit every post
120 // So we would need to clarify this in the instructions, and
121 // include a step that fetches the page they want to edit
122 'canEditPosts' => (bool) \current_user_can('edit_posts'),
123 'canEditThemes' => (bool) \current_user_can('edit_theme_options'),
124 'canActivatePlugins' => (bool) \current_user_can('activate_plugins'),
125 'canInstallPlugins' => (bool) \current_user_can('install_plugins'),
126 'canEditUsers' => (bool) \current_user_can('edit_users'),
127 'canEditSettings' => (bool) \current_user_can('manage_options'),
128 'canUploadMedia' => (bool) \current_user_can('upload_files'),
129 ];
130
131 $agentOnboarding = PartnerData::setting('useAgentOnboarding') ||
132 Config::preview('agent-onboarding') ||
133 constant('EXTENDIFY_DEVMODE');
134
135 \wp_add_inline_script(
136 Config::$slug . '-agent-scripts',
137 'window.extAgentData = ' . \wp_json_encode([
138 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
139 'startOnboarding' => isset($_GET['extendify-launch-success']) && $agentOnboarding,
140 'agentPosition' => $agentOnboarding && !is_admin() ? 'docked-left' : 'floating',
141 // Add context about where they are
142 'context' => $context,
143 // Context that the Agent might need when returning a response,
144 // but not for handling the workflow.
145 'agentContext' => $agentContext,
146 // List of abilities the AI can perform for this user.
147 // For example, we could check whether their theme has variations.
148 'abilities' => $abilities,
149 // List of suggestions the AI can make for this user.
150 // For example, we could check whether they need to set up a specific plugin.
151 'suggestions' => $this->getSuggestions(),
152 'chatHistory' => ChatHistoryController::getChatHistory(),
153 'workflowHistory' => WorkflowHistoryController::getWorkflowHistory(),
154 'userData' => [
155 'tourData' => \wp_json_encode(TourController::get()->get_data()),
156 ],
157 ]),
158 'before'
159 );
160
161 \wp_set_script_translations(
162 Config::$slug . '-agent-scripts',
163 'extendify-local',
164 EXTENDIFY_PATH . 'languages/js'
165 );
166
167 \wp_enqueue_style(
168 Config::$slug . '-agent-styles',
169 EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-agent.css'],
170 [],
171 Config::$version,
172 'all'
173 );
174 }
175 /**
176 * Get the current post ID based on the context.
177 *
178 * @return int
179 */
180 private function getCurrentPostId()
181 {
182 if (is_admin() && function_exists('get_current_screen')) {
183 $screen = get_current_screen();
184 if ($screen && $screen->base === 'post') {
185 global $post;
186 if ($post) {
187 return (int) $post->ID;
188 }
189 }
190 }
191 if (\is_front_page()) {
192 return (\get_option('show_on_front') === 'page') ? (int) \get_option('page_on_front') : 0;
193 }
194 if (\is_home()) {
195 return (int) \get_option('page_for_posts');
196 }
197 return (int) \get_queried_object_id();
198 }
199
200 /**
201 * Get theme presets (colors, fonts, etc.) as slug => value maps.
202 *
203 * @return array
204 */
205 private function getThemePresets()
206 {
207 if (!function_exists('wp_get_global_settings')) {
208 return ['colors' => [], 'fontSizes' => [], 'fontFamilies' => [], 'duotone' => []];
209 }
210
211 $settings = \wp_get_global_settings();
212
213 $colors = [];
214 $colorPalette = $settings['color']['palette']['theme'] ?? [];
215 foreach ($colorPalette as $item) {
216 if (isset($item['slug'], $item['color'])) {
217 $colors[$item['slug']] = $item['color'];
218 }
219 }
220
221 $duotone = [];
222 $duotonePresets = $settings['color']['duotone']['theme'] ?? [];
223 foreach ($duotonePresets as $item) {
224 if (isset($item['slug'], $item['colors']) && is_array($item['colors'])) {
225 $duotone[] = [
226 'slug' => $item['slug'],
227 'colors' => $item['colors'],
228 ];
229 }
230 }
231
232 $fontSizes = [];
233 $fontSizePresets = $settings['typography']['fontSizes']['theme'] ?? [];
234 foreach ($fontSizePresets as $item) {
235 if (isset($item['slug'], $item['size'])) {
236 $fontSizes[$item['slug']] = $item['size'];
237 }
238 }
239
240 $fontFamilies = [];
241 $fontFamilyPresets = $settings['typography']['fontFamilies']['theme'] ?? [];
242 foreach ($fontFamilyPresets as $item) {
243 if (isset($item['slug'], $item['fontFamily'])) {
244 $fontFamilies[$item['slug']] = $item['fontFamily'];
245 }
246 }
247
248 $colorPairs = [];
249 if (function_exists('wp_get_global_styles')) {
250 $colorPairs = self::extractColorPairs();
251 }
252
253 return [
254 'colors' => $colors,
255 'duotone' => $duotone,
256 'fontSizes' => $fontSizes,
257 'fontFamilies' => $fontFamilies,
258 'colorPairs' => $colorPairs,
259 ];
260 }
261
262 private static function extractColorSlug(string $value)
263 {
264 if (preg_match('/var\(--wp--preset--color--([^)]+)\)/', $value, $m)) {
265 return $m[1];
266 }
267 return null;
268 }
269
270 private static function extractColorPairs()
271 {
272 $styles = \wp_get_global_styles();
273 $settings = \wp_get_global_settings();
274 $custom = $settings['custom'] ?? [];
275
276 $pairs = [];
277 $seen = [];
278
279 $bodyTextSlug = self::extractColorSlug($styles['color']['text'] ?? '');
280 $bodyBgSlug = self::extractColorSlug($styles['color']['background'] ?? '');
281
282 $candidates = [];
283
284 if ($bodyTextSlug && $bodyBgSlug) {
285 $candidates[] = ['text' => $bodyTextSlug, 'bg' => $bodyBgSlug];
286 }
287
288 $btnTextSlug = self::extractColorSlug($styles['elements']['button']['color']['text'] ?? '')
289 ?? self::extractColorSlug($custom['elements']['button']['color']['text'] ?? '');
290 $btnBgSlug = self::extractColorSlug($styles['elements']['button']['color']['background'] ?? '')
291 ?? self::extractColorSlug($custom['elements']['button']['color']['background'] ?? '');
292 if ($btnTextSlug && $btnBgSlug) {
293 $candidates[] = ['text' => $btnTextSlug, 'bg' => $btnBgSlug];
294 }
295
296 $btnHoverTextSlug = self::extractColorSlug($styles['elements']['button'][':hover']['color']['text'] ?? '')
297 ?? self::extractColorSlug($custom['elements']['button'][':hover']['color']['text'] ?? '');
298 $btnHoverBgSlug = self::extractColorSlug($styles['elements']['button'][':hover']['color']['background'] ?? '')
299 ?? self::extractColorSlug($custom['elements']['button'][':hover']['color']['background'] ?? '');
300 if ($btnHoverTextSlug && $btnHoverBgSlug) {
301 $candidates[] = ['text' => $btnHoverTextSlug, 'bg' => $btnHoverBgSlug];
302 }
303
304 $linkTextSlug = self::extractColorSlug($styles['elements']['link']['color']['text'] ?? '')
305 ?? self::extractColorSlug($custom['elements']['link']['color']['text'] ?? '');
306 if ($linkTextSlug && $bodyBgSlug) {
307 $candidates[] = ['text' => $linkTextSlug, 'bg' => $bodyBgSlug];
308 }
309
310 $headingTextSlug = self::extractColorSlug($styles['elements']['heading']['color']['text'] ?? '')
311 ?? self::extractColorSlug($custom['elements']['heading']['color']['text'] ?? '');
312 if ($headingTextSlug && $bodyBgSlug) {
313 $candidates[] = ['text' => $headingTextSlug, 'bg' => $bodyBgSlug];
314 }
315
316 if ($bodyTextSlug) {
317 $candidates[] = ['text' => $bodyTextSlug, 'bg' => 'tertiary'];
318 }
319 if ($headingTextSlug && $headingTextSlug !== $bodyTextSlug) {
320 $candidates[] = ['text' => $headingTextSlug, 'bg' => 'tertiary'];
321 }
322
323 foreach ($candidates as $pair) {
324 $key = $pair['text'] . '|' . $pair['bg'];
325 if (isset($seen[$key])) {
326 continue;
327 }
328 $seen[$key] = true;
329 $pairs[] = $pair;
330 }
331
332 return $pairs;
333 }
334
335 /**
336 * Scan the style dirs to locate if they have variations.
337 * Ported from here:
338 * https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/class-wp-theme-json-resolver.php#L810
339 *
340 * @return bool
341 */
342 private function hasThemeVariations()
343 {
344 $base_directory = get_stylesheet_directory() . '/styles';
345 $template_directory = get_template_directory() . '/styles';
346
347 if (is_dir($base_directory) && glob($base_directory . '/*.json', GLOB_NOSORT)) {
348 return true;
349 }
350
351 // Only check parent if it's different from child
352 if (
353 $template_directory !== $base_directory &&
354 is_dir($template_directory) &&
355 glob($template_directory . '/*.json', GLOB_NOSORT)
356 ) {
357 return true;
358 }
359
360 return false;
361 }
362
363 /**
364 * Get suggestions for the user.
365 *
366 * @return array
367 */
368 private function getSuggestions()
369 {
370 return [
371 [
372 'id' => 'publish-this-page',
373 'icon' => 'published',
374 'message' => __('Publish this page', 'extendify-local'),
375 'workflowId' => 'update-post-status',
376 'source' => 'plugin',
377 'available' => ['context' => ['postStatus' => 'draft']],
378 ],
379 [
380 'id' => 'unpublish-this-page',
381 'icon' => 'drafts',
382 'message' => __('Unpublish this page', 'extendify-local'),
383 'workflowId' => 'update-post-status',
384 'source' => 'plugin',
385 'available' => ['context' => ['postStatus' => 'publish', 'isFrontPage' => false]],
386 ],
387 [
388 'id' => 'change-site-title',
389 'icon' => 'edit',
390 'message' => __('Change website title', 'extendify-local'),
391 'workflowId' => 'edit-wp-setting',
392 'source' => 'plugin',
393 'available' => ['abilities' => ['canEditSettings']],
394 ],
395 [
396 'id' => 'change-theme-colors',
397 'icon' => 'styles',
398 'message' => __('Change website colors', 'extendify-local'),
399 'workflowId' => 'change-theme-variation',
400 'source' => 'plugin',
401 'available' => ['context' => ['hasThemeVariations']],
402 ],
403 [
404 'id' => 'change-theme-fonts',
405 'icon' => 'typography',
406 'message' => __('Change website fonts', 'extendify-local'),
407 'workflowId' => 'change-theme-fonts-variation',
408 'source' => 'plugin',
409 'available' => ['context' => ['hasThemeVariations']],
410 ],
411 [
412 'id' => 'change-site-style',
413 'icon' => 'styles',
414 'message' => __('Change website style', 'extendify-local'),
415 'workflowId' => 'change-site-vibes',
416 'source' => 'plugin',
417 'available' => ['abilities' => ['canEditThemes'], 'context' => ['isUsingVibes']],
418 ],
419 [
420 'id' => 'edit-text-on-this-page',
421 'icon' => 'edit',
422 'message' => __('Edit text on this page', 'extendify-local'),
423 'workflowId' => 'edit-post-strings',
424 'source' => 'plugin',
425 'available' => ['abilities' => ['canEditPost'], 'context' => ['postId', 'usingBlockEditor']],
426 ],
427 [
428 'id' => 'change-website-animation',
429 'icon' => 'swatch',
430 'message' => __('Change website animation', 'extendify-local'),
431 'workflowId' => 'change-animation',
432 'source' => 'plugin',
433 'available' => ['abilities' => ['canEditSettings']],
434 ],
435 [
436 'id' => 'change-website-logo',
437 'icon' => 'siteLogo',
438 'message' => __('Change website logo', 'extendify-local'),
439 'workflowId' => 'update-logo',
440 'source' => 'plugin',
441 'available' => ['abilities' => ['canEditSettings', 'canUploadMedia']],
442 ],
443 [
444 'id' => 'change-website-browser-icon',
445 'icon' => 'siteLogo',
446 'message' => __('Change website browser icon', 'extendify-local'),
447 'workflowId' => 'update-site-icon',
448 'source' => 'plugin',
449 'available' => ['abilities' => ['canEditSettings', 'canUploadMedia']],
450 ],
451 ];
452 }
453
454 /**
455 * Check if the user is in the Gutenberg or FSE editor.
456 *
457 * @return false|bool
458 */
459 public function isGutenbergOrFse()
460 {
461 if (!is_admin() || !function_exists('get_current_screen')) {
462 return false;
463 }
464
465 $screen = get_current_screen();
466 if (!$screen) {
467 return false;
468 }
469
470 $is_fse =
471 in_array($screen->id, ['site-editor', 'appearance_page_gutenberg-edit-site'], true) ||
472 (isset($GLOBALS['pagenow']) && $GLOBALS['pagenow'] === 'site-editor.php');
473
474 $is_gutenberg =
475 $screen->base === 'post' &&
476 method_exists($screen, 'is_block_editor') &&
477 $screen->is_block_editor();
478
479 return $is_fse || $is_gutenberg;
480 }
481 }
482