| 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\Config; |
| 14 |
use Extendify\Constants; |
| 15 |
use Extendify\Shared\Services\Escaper; |
| 16 |
use Extendify\Shared\Services\HttpClient; |
| 17 |
use Extendify\Agent\TagBlocks; |
| 18 |
use Extendify\Agent\TagTemplateParts; |
| 19 |
use Extendify\Agent\WooProductImages; |
| 20 |
use Extendify\Agent\AbilitiesDiscovery; |
| 21 |
use Extendify\Agent\Controllers\SiteNavigationController; |
| 22 |
use Extendify\PartnerData; |
| 23 |
use Extendify\Shared\DataProvider\ProductsData; |
| 24 |
|
| 25 |
/** |
| 26 |
* This class handles any file loading for the admin area. |
| 27 |
*/ |
| 28 |
class Admin |
| 29 |
{ |
| 30 |
/** |
| 31 |
* Adds various actions to set up the page |
| 32 |
* |
| 33 |
* @return void |
| 34 |
*/ |
| 35 |
public function __construct() |
| 36 |
{ |
| 37 |
\add_action('admin_enqueue_scripts', [$this, 'loadScriptsAndStyles']); |
| 38 |
\add_action('wp_enqueue_scripts', [$this, 'loadScriptsAndStyles']); |
| 39 |
ChatHistoryController::init(); |
| 40 |
|
| 41 |
// Tag blocks so we can identify them later |
| 42 |
TagBlocks::init(); |
| 43 |
TagTemplateParts::init(); |
| 44 |
|
| 45 |
// Add the site navigation ids to the navigation blocks |
| 46 |
SiteNavigationController::init(); |
| 47 |
|
| 48 |
Skeleton::init(); |
| 49 |
|
| 50 |
WooProductImages::init(); |
| 51 |
|
| 52 |
\add_action('extendify_agent_suggestions_refresh', [$this, 'refreshSuggestions']); |
| 53 |
} |
| 54 |
|
| 55 |
/** |
| 56 |
* Where the agent panel mounts — docked-left only on the onboarding front end. |
| 57 |
* |
| 58 |
* @return string |
| 59 |
*/ |
| 60 |
public static function agentPosition() |
| 61 |
{ |
| 62 |
$agentOnboarding = PartnerData::setting('useAgentOnboarding') || |
| 63 |
Config::preview('agent-onboarding') || |
| 64 |
constant('EXTENDIFY_DEVMODE'); |
| 65 |
|
| 66 |
return $agentOnboarding && !is_admin() ? 'docked-left' : 'floating'; |
| 67 |
} |
| 68 |
|
| 69 |
/** |
| 70 |
* Adds various JS scripts and styles |
| 71 |
* |
| 72 |
* @return void |
| 73 |
*/ |
| 74 |
public function loadScriptsAndStyles() |
| 75 |
{ |
| 76 |
// The Customizer preview iframe is a front-end render, so this fires |
| 77 |
// there too — but the Agent only belongs on the live, top-level page. |
| 78 |
if (is_customize_preview()) { |
| 79 |
return; |
| 80 |
} |
| 81 |
|
| 82 |
$version = constant('EXTENDIFY_DEVMODE') ? uniqid() : Config::$version; |
| 83 |
$scriptAssetPath = EXTENDIFY_PATH . 'public/build/' . Config::$assetManifest['extendify-agent.php']; |
| 84 |
$fallback = [ |
| 85 |
'dependencies' => [], |
| 86 |
'version' => $version, |
| 87 |
]; |
| 88 |
$scriptAsset = file_exists($scriptAssetPath) ? require $scriptAssetPath : $fallback; |
| 89 |
|
| 90 |
foreach ($scriptAsset['dependencies'] as $style) { |
| 91 |
\wp_enqueue_style($style); |
| 92 |
} |
| 93 |
|
| 94 |
\wp_enqueue_script( |
| 95 |
Config::$slug . '-agent-scripts', |
| 96 |
EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-agent.js'], |
| 97 |
array_merge([Config::$slug . '-shared-scripts'], $scriptAsset['dependencies']), |
| 98 |
$scriptAsset['version'], |
| 99 |
true |
| 100 |
); |
| 101 |
|
| 102 |
$context = [ |
| 103 |
'adminPage' => function_exists('get_current_screen') && ($screen = get_current_screen()) |
| 104 |
? \esc_attr($screen->id) |
| 105 |
: null, |
| 106 |
'postId' => (int) $this->getCurrentPostId(), |
| 107 |
'postTitle' => \esc_attr(\get_the_title($this->getCurrentPostId())), |
| 108 |
'postType' => \esc_attr(\get_post_type($this->getCurrentPostId())), |
| 109 |
'postUrl' => \esc_url(\get_permalink($this->getCurrentPostId())), |
| 110 |
'isFrontPage' => (bool) \is_front_page(), |
| 111 |
'postStatus' => \esc_attr(\get_post_status((int) $this->getCurrentPostId())), |
| 112 |
'isBlogPage' => (bool) \is_home(), |
| 113 |
'themeSlug' => \esc_attr(\wp_get_theme()->get_stylesheet()), |
| 114 |
'hasThemeVariations' => (bool) $this->hasThemeVariations(), |
| 115 |
'isBlockTheme' => function_exists('wp_is_block_theme') ? (bool) wp_is_block_theme() : false, |
| 116 |
'wordPressVersion' => \esc_attr(\get_bloginfo('version')), |
| 117 |
'usingBlockEditor' => function_exists('use_block_editor_for_post') ? |
| 118 |
(bool) use_block_editor_for_post($this->getCurrentPostId()) : |
| 119 |
false, |
| 120 |
'isOnEditorOrFSE' => $this->isGutenbergOrFse(), |
| 121 |
'activePlugins' => array_values(\get_option('active_plugins', [])), |
| 122 |
// Whether the user is using the vibes experience or not. |
| 123 |
'isUsingVibes' => (bool) file_exists(EXTENDIFY_PATH . 'src/Launch/_data/block-style-variations.json') && |
| 124 |
version_compare(wp_get_theme("extendable")->get('Version'), '2.0.32', '>='), |
| 125 |
'siteTitle' => \esc_attr(\get_bloginfo('name')), |
| 126 |
'siteDescription' => \esc_attr(\get_bloginfo('description')), |
| 127 |
'themePresets' => $this->getThemePresets(), |
| 128 |
'presetSlugs' => $this->getPresetSlugs(), |
| 129 |
]; |
| 130 |
$recommendations = ProductsData::get() ?? []; |
| 131 |
$pluginRecommendations = array_filter($recommendations, function ($item) { |
| 132 |
return in_array('ai-agent', $item['slots'] ?? [], true) && $item['ctaType'] === 'plugin'; |
| 133 |
}); |
| 134 |
$mappedPluginRecommendations = array_values(array_map(function ($item) { |
| 135 |
return [ |
| 136 |
'title' => $item['title'] ?? '', |
| 137 |
'slug' => $item['ctaPluginSlug'] ?? $item['slug'] ?? '', |
| 138 |
'description' => $item['aiDescription'] ?? $item['description'] ?? '', |
| 139 |
'redirectTo' => $item['pluginSetupUrl'] ?? '', // this is a partial setup URL not full one. |
| 140 |
]; |
| 141 |
}, $pluginRecommendations)); |
| 142 |
$agentContext = [ |
| 143 |
'availableAdminPages' => get_option('_transient_extendify_admin_pages_menu', []), |
| 144 |
'pluginRecommendations' => $mappedPluginRecommendations, |
| 145 |
]; |
| 146 |
$abilities = [ |
| 147 |
'canEditPost' => (bool) \current_user_can('edit_post', \get_queried_object_id()), |
| 148 |
// TODO: this may be true for a user, while they still can't edit every post |
| 149 |
// So we would need to clarify this in the instructions, and |
| 150 |
// include a step that fetches the page they want to edit |
| 151 |
'canEditPosts' => (bool) \current_user_can('edit_posts'), |
| 152 |
'canEditThemes' => (bool) \current_user_can('edit_theme_options'), |
| 153 |
'canActivatePlugins' => (bool) \current_user_can('activate_plugins'), |
| 154 |
'canInstallPlugins' => (bool) \current_user_can('install_plugins'), |
| 155 |
'canEditUsers' => (bool) \current_user_can('edit_users'), |
| 156 |
'canEditSettings' => (bool) \current_user_can('manage_options'), |
| 157 |
'canUploadMedia' => (bool) \current_user_can('upload_files'), |
| 158 |
]; |
| 159 |
|
| 160 |
$agentOnboarding = PartnerData::setting('useAgentOnboarding') || |
| 161 |
Config::preview('agent-onboarding') || |
| 162 |
constant('EXTENDIFY_DEVMODE'); |
| 163 |
|
| 164 |
\wp_add_inline_script( |
| 165 |
Config::$slug . '-agent-scripts', |
| 166 |
'window.extAgentData = ' . \wp_json_encode([ |
| 167 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 168 |
'startOnboarding' => isset($_GET['extendify-launch-success']) && $agentOnboarding, |
| 169 |
'agentPosition' => self::agentPosition(), |
| 170 |
// Add context about where they are |
| 171 |
'context' => $context, |
| 172 |
// Material a workflow may want; each declares the keys it reads. |
| 173 |
'agentContext' => $agentContext, |
| 174 |
// List of abilities the AI can perform for this user. |
| 175 |
// For example, we could check whether their theme has variations. |
| 176 |
'abilities' => $abilities, |
| 177 |
// Registered WordPress Abilities (6.9+) this user may run. |
| 178 |
'wpAbilities' => AbilitiesDiscovery::discover(), |
| 179 |
// List of suggestions the AI can make for this user. |
| 180 |
// For example, we could check whether they need to set up a specific plugin. |
| 181 |
'suggestions' => $this->getSuggestions(), |
| 182 |
'domainsSuggestionSettings' => [ |
| 183 |
'showPrimary' => (bool) PartnerData::setting('showPrimaryDomainRecommendationAgent'), |
| 184 |
'showSecondary' => (bool) PartnerData::setting('showSecondaryDomainRecommendationAgent'), |
| 185 |
'stagingSites' => PartnerData::setting('stagingSites'), |
| 186 |
'searchUrl' => PartnerData::setting('domainSearchURL'), |
| 187 |
], |
| 188 |
'chatHistory' => ChatHistoryController::getChatHistory(), |
| 189 |
'userData' => [ |
| 190 |
'tourData' => \wp_json_encode(TourController::get()->get_data()), |
| 191 |
'domainsRecommendationsActivities' => \wp_json_encode( |
| 192 |
\get_option('extendify_domains_recommendations_activities', null) |
| 193 |
), |
| 194 |
], |
| 195 |
]), |
| 196 |
'before' |
| 197 |
); |
| 198 |
|
| 199 |
\wp_set_script_translations( |
| 200 |
Config::$slug . '-agent-scripts', |
| 201 |
'extendify-local', |
| 202 |
EXTENDIFY_PATH . 'languages/js' |
| 203 |
); |
| 204 |
|
| 205 |
\wp_enqueue_style( |
| 206 |
Config::$slug . '-agent-styles', |
| 207 |
EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-agent.css'], |
| 208 |
[], |
| 209 |
Config::$version, |
| 210 |
'all' |
| 211 |
); |
| 212 |
} |
| 213 |
/** |
| 214 |
* Get the current post ID based on the context. |
| 215 |
* |
| 216 |
* @return int |
| 217 |
*/ |
| 218 |
private function getCurrentPostId() |
| 219 |
{ |
| 220 |
if (is_admin() && function_exists('get_current_screen')) { |
| 221 |
$screen = get_current_screen(); |
| 222 |
if ($screen && $screen->base === 'post') { |
| 223 |
global $post; |
| 224 |
if ($post) { |
| 225 |
return (int) $post->ID; |
| 226 |
} |
| 227 |
} |
| 228 |
} |
| 229 |
if (\is_front_page()) { |
| 230 |
return (\get_option('show_on_front') === 'page') ? (int) \get_option('page_on_front') : 0; |
| 231 |
} |
| 232 |
if (\is_home()) { |
| 233 |
return (int) \get_option('page_for_posts'); |
| 234 |
} |
| 235 |
return (int) \get_queried_object_id(); |
| 236 |
} |
| 237 |
|
| 238 |
/** |
| 239 |
* Get theme presets (colors, fonts, etc.) as slug => value maps. |
| 240 |
* |
| 241 |
* @return array |
| 242 |
*/ |
| 243 |
private function getThemePresets() |
| 244 |
{ |
| 245 |
if (!function_exists('wp_get_global_settings')) { |
| 246 |
return ['colors' => [], 'fontSizes' => [], 'fontFamilies' => [], 'duotone' => []]; |
| 247 |
} |
| 248 |
|
| 249 |
$settings = \wp_get_global_settings(); |
| 250 |
|
| 251 |
$colors = []; |
| 252 |
$colorPalette = $settings['color']['palette']['theme'] ?? []; |
| 253 |
foreach ($colorPalette as $item) { |
| 254 |
if (isset($item['slug'], $item['color'])) { |
| 255 |
$colors[$item['slug']] = $item['color']; |
| 256 |
} |
| 257 |
} |
| 258 |
|
| 259 |
$duotone = []; |
| 260 |
$duotonePresets = $settings['color']['duotone']['theme'] ?? []; |
| 261 |
foreach ($duotonePresets as $item) { |
| 262 |
if (isset($item['slug'], $item['colors']) && is_array($item['colors'])) { |
| 263 |
$duotone[] = [ |
| 264 |
'slug' => $item['slug'], |
| 265 |
'colors' => $item['colors'], |
| 266 |
]; |
| 267 |
} |
| 268 |
} |
| 269 |
|
| 270 |
$fontSizes = []; |
| 271 |
$fontSizePresets = $settings['typography']['fontSizes']['theme'] ?? []; |
| 272 |
foreach ($fontSizePresets as $item) { |
| 273 |
if (isset($item['slug'], $item['size'])) { |
| 274 |
$fontSizes[$item['slug']] = $item['size']; |
| 275 |
} |
| 276 |
} |
| 277 |
|
| 278 |
$fontFamilies = []; |
| 279 |
$fontFamilyPresets = $settings['typography']['fontFamilies']['theme'] ?? []; |
| 280 |
foreach ($fontFamilyPresets as $item) { |
| 281 |
if (isset($item['slug'], $item['fontFamily'])) { |
| 282 |
$fontFamilies[$item['slug']] = $item['fontFamily']; |
| 283 |
} |
| 284 |
} |
| 285 |
|
| 286 |
$colorPairs = []; |
| 287 |
if (function_exists('wp_get_global_styles')) { |
| 288 |
$colorPairs = self::extractColorPairs(); |
| 289 |
} |
| 290 |
|
| 291 |
return [ |
| 292 |
'colors' => $colors, |
| 293 |
'duotone' => $duotone, |
| 294 |
'fontSizes' => $fontSizes, |
| 295 |
'fontFamilies' => $fontFamilies, |
| 296 |
'colorPairs' => $colorPairs, |
| 297 |
]; |
| 298 |
} |
| 299 |
|
| 300 |
/** |
| 301 |
* Preset slugs per design-token family across every origin. Feeds the Agent's |
| 302 |
* block-attribute schema so the model picks named tokens, not raw CSS. |
| 303 |
* |
| 304 |
* @return array<string,string[]> |
| 305 |
*/ |
| 306 |
private function getPresetSlugs() |
| 307 |
{ |
| 308 |
if (!function_exists('wp_get_global_settings')) { |
| 309 |
return []; |
| 310 |
} |
| 311 |
|
| 312 |
$color = \wp_get_global_settings(['color']); |
| 313 |
$typography = \wp_get_global_settings(['typography']); |
| 314 |
|
| 315 |
return [ |
| 316 |
'color' => $this->originSlugs($color, 'palette', 'defaultPalette'), |
| 317 |
'gradient' => $this->originSlugs($color, 'gradients', 'defaultGradients'), |
| 318 |
'fontSize' => $this->originSlugs($typography, 'fontSizes', 'defaultFontSizes'), |
| 319 |
'fontFamily' => $this->originSlugs($typography, 'fontFamilies', 'defaultFontFamilies'), |
| 320 |
]; |
| 321 |
} |
| 322 |
|
| 323 |
/** |
| 324 |
* Preset slugs from a settings node's origin buckets; drops the `default` |
| 325 |
* origin when the theme opted the family out. |
| 326 |
* |
| 327 |
* @return string[] |
| 328 |
*/ |
| 329 |
private function originSlugs($node, $listKey, $defaultKey) |
| 330 |
{ |
| 331 |
$list = $node[$listKey] ?? []; |
| 332 |
|
| 333 |
$origins = ['custom', 'theme']; |
| 334 |
if (($node[$defaultKey] ?? true) !== false) { |
| 335 |
$origins[] = 'default'; |
| 336 |
} |
| 337 |
|
| 338 |
$slugs = []; |
| 339 |
foreach ($origins as $origin) { |
| 340 |
foreach ($list[$origin] ?? [] as $item) { |
| 341 |
if (isset($item['slug'])) { |
| 342 |
$slugs[] = (string) $item['slug']; |
| 343 |
} |
| 344 |
} |
| 345 |
} |
| 346 |
|
| 347 |
return array_values(array_unique($slugs)); |
| 348 |
} |
| 349 |
|
| 350 |
private static function extractColorSlug(string $value) |
| 351 |
{ |
| 352 |
if (preg_match('/var\(--wp--preset--color--([^)]+)\)/', $value, $m)) { |
| 353 |
return $m[1]; |
| 354 |
} |
| 355 |
return null; |
| 356 |
} |
| 357 |
|
| 358 |
private static function extractColorPairs() |
| 359 |
{ |
| 360 |
$styles = \wp_get_global_styles(); |
| 361 |
$settings = \wp_get_global_settings(); |
| 362 |
$custom = $settings['custom'] ?? []; |
| 363 |
|
| 364 |
$pairs = []; |
| 365 |
$seen = []; |
| 366 |
|
| 367 |
$bodyTextSlug = self::extractColorSlug($styles['color']['text'] ?? ''); |
| 368 |
$bodyBgSlug = self::extractColorSlug($styles['color']['background'] ?? ''); |
| 369 |
|
| 370 |
$candidates = []; |
| 371 |
|
| 372 |
if ($bodyTextSlug && $bodyBgSlug) { |
| 373 |
$candidates[] = ['text' => $bodyTextSlug, 'bg' => $bodyBgSlug]; |
| 374 |
} |
| 375 |
|
| 376 |
$btnTextSlug = self::extractColorSlug($styles['elements']['button']['color']['text'] ?? '') |
| 377 |
?? self::extractColorSlug($custom['elements']['button']['color']['text'] ?? ''); |
| 378 |
$btnBgSlug = self::extractColorSlug($styles['elements']['button']['color']['background'] ?? '') |
| 379 |
?? self::extractColorSlug($custom['elements']['button']['color']['background'] ?? ''); |
| 380 |
if ($btnTextSlug && $btnBgSlug) { |
| 381 |
$candidates[] = ['text' => $btnTextSlug, 'bg' => $btnBgSlug]; |
| 382 |
} |
| 383 |
|
| 384 |
$btnHoverTextSlug = self::extractColorSlug($styles['elements']['button'][':hover']['color']['text'] ?? '') |
| 385 |
?? self::extractColorSlug($custom['elements']['button'][':hover']['color']['text'] ?? ''); |
| 386 |
$btnHoverBgSlug = self::extractColorSlug($styles['elements']['button'][':hover']['color']['background'] ?? '') |
| 387 |
?? self::extractColorSlug($custom['elements']['button'][':hover']['color']['background'] ?? ''); |
| 388 |
if ($btnHoverTextSlug && $btnHoverBgSlug) { |
| 389 |
$candidates[] = ['text' => $btnHoverTextSlug, 'bg' => $btnHoverBgSlug]; |
| 390 |
} |
| 391 |
|
| 392 |
$linkTextSlug = self::extractColorSlug($styles['elements']['link']['color']['text'] ?? '') |
| 393 |
?? self::extractColorSlug($custom['elements']['link']['color']['text'] ?? ''); |
| 394 |
if ($linkTextSlug && $bodyBgSlug) { |
| 395 |
$candidates[] = ['text' => $linkTextSlug, 'bg' => $bodyBgSlug]; |
| 396 |
} |
| 397 |
|
| 398 |
$headingTextSlug = self::extractColorSlug($styles['elements']['heading']['color']['text'] ?? '') |
| 399 |
?? self::extractColorSlug($custom['elements']['heading']['color']['text'] ?? ''); |
| 400 |
if ($headingTextSlug && $bodyBgSlug) { |
| 401 |
$candidates[] = ['text' => $headingTextSlug, 'bg' => $bodyBgSlug]; |
| 402 |
} |
| 403 |
|
| 404 |
if ($bodyTextSlug) { |
| 405 |
$candidates[] = ['text' => $bodyTextSlug, 'bg' => 'tertiary']; |
| 406 |
} |
| 407 |
if ($headingTextSlug && $headingTextSlug !== $bodyTextSlug) { |
| 408 |
$candidates[] = ['text' => $headingTextSlug, 'bg' => 'tertiary']; |
| 409 |
} |
| 410 |
|
| 411 |
foreach ($candidates as $pair) { |
| 412 |
$key = $pair['text'] . '|' . $pair['bg']; |
| 413 |
if (isset($seen[$key])) { |
| 414 |
continue; |
| 415 |
} |
| 416 |
$seen[$key] = true; |
| 417 |
$pairs[] = $pair; |
| 418 |
} |
| 419 |
|
| 420 |
return $pairs; |
| 421 |
} |
| 422 |
|
| 423 |
/** |
| 424 |
* Scan the style dirs to locate if they have variations. |
| 425 |
* Ported from here: |
| 426 |
* https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/class-wp-theme-json-resolver.php#L810 |
| 427 |
* |
| 428 |
* @return bool |
| 429 |
*/ |
| 430 |
private function hasThemeVariations() |
| 431 |
{ |
| 432 |
$base_directory = get_stylesheet_directory() . '/styles'; |
| 433 |
$template_directory = get_template_directory() . '/styles'; |
| 434 |
|
| 435 |
if (is_dir($base_directory) && glob($base_directory . '/*.json', GLOB_NOSORT)) { |
| 436 |
return true; |
| 437 |
} |
| 438 |
|
| 439 |
// Only check parent if it's different from child |
| 440 |
if ( |
| 441 |
$template_directory !== $base_directory && |
| 442 |
is_dir($template_directory) && |
| 443 |
glob($template_directory . '/*.json', GLOB_NOSORT) |
| 444 |
) { |
| 445 |
return true; |
| 446 |
} |
| 447 |
|
| 448 |
return false; |
| 449 |
} |
| 450 |
|
| 451 |
/** |
| 452 |
* Get suggestions for the user. |
| 453 |
* |
| 454 |
* @return array |
| 455 |
*/ |
| 456 |
private function getSuggestions() |
| 457 |
{ |
| 458 |
$locale = \get_locale(); |
| 459 |
$cached = \get_option('extendify_agent_suggestions_' . $locale); |
| 460 |
|
| 461 |
if (!is_array($cached) || !isset($cached['fetchedAt'])) { |
| 462 |
return $this->refreshSuggestions($locale) ?? []; |
| 463 |
} |
| 464 |
|
| 465 |
$age = time() - $cached['fetchedAt']; |
| 466 |
if ($age > DAY_IN_SECONDS) { |
| 467 |
if (!\wp_next_scheduled('extendify_agent_suggestions_refresh', [$locale])) { |
| 468 |
\wp_schedule_single_event(time(), 'extendify_agent_suggestions_refresh', [$locale]); |
| 469 |
if (\is_admin()) { |
| 470 |
\spawn_cron(); |
| 471 |
} |
| 472 |
} |
| 473 |
} |
| 474 |
|
| 475 |
return $cached['data'] ?? []; |
| 476 |
} |
| 477 |
|
| 478 |
/** |
| 479 |
* Fetch suggestions from the API and persist them. |
| 480 |
* Called synchronously on cold start and via wp-cron when cache is stale. |
| 481 |
* |
| 482 |
* @param string $locale - Locale to fetch (cron may run in a different site locale). |
| 483 |
* @return array|null |
| 484 |
*/ |
| 485 |
public function refreshSuggestions($locale) |
| 486 |
{ |
| 487 |
// When the refresh runs via wp-cron, the active locale may differ from the cached entry's locale. |
| 488 |
// Switch so HttpClient sends the matching wp_language and the response lands in the right cache key. |
| 489 |
$needSwitch = $locale !== \get_locale(); |
| 490 |
if ($needSwitch) { |
| 491 |
\switch_to_locale($locale); |
| 492 |
} |
| 493 |
|
| 494 |
$response = HttpClient::post( |
| 495 |
Constants::AI_HOST . '/api/agent/suggestions', |
| 496 |
[], |
| 497 |
null, |
| 498 |
true |
| 499 |
); |
| 500 |
|
| 501 |
if ($needSwitch) { |
| 502 |
\restore_previous_locale(); |
| 503 |
} |
| 504 |
|
| 505 |
$optionKey = 'extendify_agent_suggestions_' . $locale; |
| 506 |
|
| 507 |
if ($response['code'] !== 200) { |
| 508 |
// Back off: stamp the cache as fetched ~23h ago so we retry in ~1h instead of every request. |
| 509 |
$cached = \get_option($optionKey); |
| 510 |
\update_option( |
| 511 |
$optionKey, |
| 512 |
[ |
| 513 |
'data' => is_array($cached) ? ($cached['data'] ?? []) : [], |
| 514 |
'fetchedAt' => time() - (DAY_IN_SECONDS - HOUR_IN_SECONDS), |
| 515 |
], |
| 516 |
false |
| 517 |
); |
| 518 |
return null; |
| 519 |
} |
| 520 |
|
| 521 |
$suggestions = $response['response']['suggestions'] ?? []; |
| 522 |
\update_option( |
| 523 |
$optionKey, |
| 524 |
['data' => $suggestions, 'fetchedAt' => time()], |
| 525 |
false |
| 526 |
); |
| 527 |
return $suggestions; |
| 528 |
} |
| 529 |
|
| 530 |
/** |
| 531 |
* Check if the user is in the Gutenberg or FSE editor. |
| 532 |
* |
| 533 |
* @return false|bool |
| 534 |
*/ |
| 535 |
public function isGutenbergOrFse() |
| 536 |
{ |
| 537 |
if (!is_admin() || !function_exists('get_current_screen')) { |
| 538 |
return false; |
| 539 |
} |
| 540 |
|
| 541 |
$screen = get_current_screen(); |
| 542 |
if (!$screen) { |
| 543 |
return false; |
| 544 |
} |
| 545 |
|
| 546 |
$is_fse = |
| 547 |
in_array($screen->id, ['site-editor', 'appearance_page_gutenberg-edit-site'], true) || |
| 548 |
(isset($GLOBALS['pagenow']) && $GLOBALS['pagenow'] === 'site-editor.php'); |
| 549 |
|
| 550 |
$is_gutenberg = |
| 551 |
$screen->base === 'post' && |
| 552 |
method_exists($screen, 'is_block_editor') && |
| 553 |
$screen->is_block_editor(); |
| 554 |
|
| 555 |
return $is_fse || $is_gutenberg; |
| 556 |
} |
| 557 |
} |
| 558 |
|