File: /home/swifizcd/julnohub.com.ng/wp-content/plugins/extendify/app/Agent/Admin.php
<?php
/**
* Admin.
*/
namespace Extendify\Agent;
defined('ABSPATH') || die('No direct access.');
use Extendify\Agent\Controllers\ChatHistoryController;
use Extendify\Agent\Controllers\TourController;
use Extendify\Agent\Controllers\WorkflowHistoryController;
use Extendify\Config;
use Extendify\Shared\Services\Escaper;
use Extendify\Agent\TagBlocks;
use Extendify\Agent\TagTemplateParts;
use Extendify\Agent\Controllers\SiteNavigationController;
use Extendify\PartnerData;
use Extendify\Shared\DataProvider\ProductsData;
/**
* This class handles any file loading for the admin area.
*/
class Admin
{
/**
* Adds various actions to set up the page
*
* @return void
*/
public function __construct()
{
\add_action('admin_enqueue_scripts', [$this, 'loadScriptsAndStyles']);
\add_action('wp_enqueue_scripts', [$this, 'loadScriptsAndStyles']);
ChatHistoryController::init();
WorkflowHistoryController::init();
// Tag blocks so we can identify them later
TagBlocks::init();
TagTemplateParts::init();
// Add the site navigation ids to the navigation blocks
SiteNavigationController::init();
}
/**
* Adds various JS scripts and styles
*
* @return void
*/
public function loadScriptsAndStyles()
{
$version = constant('EXTENDIFY_DEVMODE') ? uniqid() : Config::$version;
$scriptAssetPath = EXTENDIFY_PATH . 'public/build/' . Config::$assetManifest['extendify-agent.php'];
$fallback = [
'dependencies' => [],
'version' => $version,
];
$scriptAsset = file_exists($scriptAssetPath) ? require $scriptAssetPath : $fallback;
foreach ($scriptAsset['dependencies'] as $style) {
\wp_enqueue_style($style);
}
\wp_enqueue_script(
Config::$slug . '-agent-scripts',
EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-agent.js'],
array_merge([Config::$slug . '-shared-scripts'], $scriptAsset['dependencies']),
$scriptAsset['version'],
true
);
$context = [
'adminPage' => function_exists('get_current_screen') && ($screen = get_current_screen())
? \esc_attr($screen->id)
: null,
'postId' => (int) $this->getCurrentPostId(),
'postTitle' => \esc_attr(\get_the_title($this->getCurrentPostId())),
'postType' => \esc_attr(\get_post_type($this->getCurrentPostId())),
'postUrl' => \esc_url(\get_permalink($this->getCurrentPostId())),
'isFrontPage' => (bool) \is_front_page(),
'postStatus' => \esc_attr(\get_post_status((int) $this->getCurrentPostId())),
'isBlogPage' => (bool) \is_home(),
'themeSlug' => \esc_attr(\wp_get_theme()->get_stylesheet()),
'hasThemeVariations' => (bool) $this->hasThemeVariations(),
'isBlockTheme' => function_exists('wp_is_block_theme') ? (bool) wp_is_block_theme() : false,
'wordPressVersion' => \esc_attr(\get_bloginfo('version')),
'usingBlockEditor' => function_exists('use_block_editor_for_post') ?
(bool) use_block_editor_for_post($this->getCurrentPostId()) :
false,
'isOnEditorOrFSE' => $this->isGutenbergOrFse(),
'activePlugins' => array_values(\get_option('active_plugins', [])),
// Whether the user is using the vibes experience or not.
'isUsingVibes' => (bool) file_exists(EXTENDIFY_PATH . 'src/Launch/_data/block-style-variations.json') &&
version_compare(wp_get_theme("extendable")->get('Version'), '2.0.32', '>='),
'siteTitle' => \esc_attr(\get_bloginfo('name')),
'siteDescription' => \esc_attr(\get_bloginfo('description')),
'themePresets' => $this->getThemePresets(),
];
$recommendations = ProductsData::get() ?? [];
$pluginRecommendations = array_filter($recommendations, function ($item) {
return in_array('ai-agent', $item['slots'] ?? [], true) && $item['ctaType'] === 'plugin';
});
$mappedPluginRecommendations = array_values(array_map(function ($item) {
return [
'title' => $item['title'] ?? '',
'slug' => $item['ctaPluginSlug'] ?? $item['slug'] ?? '',
'description' => $item['aiDescription'] ?? $item['description'] ?? '',
'redirectTo' => $item['pluginSetupUrl'] ?? '', // this is a partial setup URL not full one.
];
}, $pluginRecommendations));
$agentContext = [
'availableAdminPages' => get_option('_transient_extendify_admin_pages_menu', []),
'pluginRecommendations' => $mappedPluginRecommendations,
];
$abilities = [
'canEditPost' => (bool) \current_user_can('edit_post', \get_queried_object_id()),
// TODO: this may be true for a user, while they still can't edit every post
// So we would need to clarify this in the instructions, and
// include a step that fetches the page they want to edit
'canEditPosts' => (bool) \current_user_can('edit_posts'),
'canEditThemes' => (bool) \current_user_can('edit_theme_options'),
'canActivatePlugins' => (bool) \current_user_can('activate_plugins'),
'canInstallPlugins' => (bool) \current_user_can('install_plugins'),
'canEditUsers' => (bool) \current_user_can('edit_users'),
'canEditSettings' => (bool) \current_user_can('manage_options'),
'canUploadMedia' => (bool) \current_user_can('upload_files'),
];
$agentOnboarding = PartnerData::setting('useAgentOnboarding') ||
Config::preview('agent-onboarding') ||
constant('EXTENDIFY_DEVMODE');
\wp_add_inline_script(
Config::$slug . '-agent-scripts',
'window.extAgentData = ' . \wp_json_encode([
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
'startOnboarding' => isset($_GET['extendify-launch-success']) && $agentOnboarding,
'agentPosition' => $agentOnboarding && !is_admin() ? 'docked-left' : 'floating',
// Add context about where they are
'context' => $context,
// Context that the Agent might need when returning a response,
// but not for handling the workflow.
'agentContext' => $agentContext,
// List of abilities the AI can perform for this user.
// For example, we could check whether their theme has variations.
'abilities' => $abilities,
// List of suggestions the AI can make for this user.
// For example, we could check whether they need to set up a specific plugin.
'suggestions' => $this->getSuggestions(),
'chatHistory' => ChatHistoryController::getChatHistory(),
'workflowHistory' => WorkflowHistoryController::getWorkflowHistory(),
'userData' => [
'tourData' => \wp_json_encode(TourController::get()->get_data()),
],
]),
'before'
);
\wp_set_script_translations(
Config::$slug . '-agent-scripts',
'extendify-local',
EXTENDIFY_PATH . 'languages/js'
);
\wp_enqueue_style(
Config::$slug . '-agent-styles',
EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-agent.css'],
[],
Config::$version,
'all'
);
}
/**
* Get the current post ID based on the context.
*
* @return int
*/
private function getCurrentPostId()
{
if (is_admin() && function_exists('get_current_screen')) {
$screen = get_current_screen();
if ($screen && $screen->base === 'post') {
global $post;
if ($post) {
return (int) $post->ID;
}
}
}
if (\is_front_page()) {
return (\get_option('show_on_front') === 'page') ? (int) \get_option('page_on_front') : 0;
}
if (\is_home()) {
return (int) \get_option('page_for_posts');
}
return (int) \get_queried_object_id();
}
/**
* Get theme presets (colors, fonts, etc.) as slug => value maps.
*
* @return array
*/
private function getThemePresets()
{
if (!function_exists('wp_get_global_settings')) {
return ['colors' => [], 'fontSizes' => [], 'fontFamilies' => [], 'duotone' => []];
}
$settings = \wp_get_global_settings();
$colors = [];
$colorPalette = $settings['color']['palette']['theme'] ?? [];
foreach ($colorPalette as $item) {
if (isset($item['slug'], $item['color'])) {
$colors[$item['slug']] = $item['color'];
}
}
$duotone = [];
$duotonePresets = $settings['color']['duotone']['theme'] ?? [];
foreach ($duotonePresets as $item) {
if (isset($item['slug'], $item['colors']) && is_array($item['colors'])) {
$duotone[] = [
'slug' => $item['slug'],
'colors' => $item['colors'],
];
}
}
$fontSizes = [];
$fontSizePresets = $settings['typography']['fontSizes']['theme'] ?? [];
foreach ($fontSizePresets as $item) {
if (isset($item['slug'], $item['size'])) {
$fontSizes[$item['slug']] = $item['size'];
}
}
$fontFamilies = [];
$fontFamilyPresets = $settings['typography']['fontFamilies']['theme'] ?? [];
foreach ($fontFamilyPresets as $item) {
if (isset($item['slug'], $item['fontFamily'])) {
$fontFamilies[$item['slug']] = $item['fontFamily'];
}
}
$colorPairs = [];
if (function_exists('wp_get_global_styles')) {
$colorPairs = self::extractColorPairs();
}
return [
'colors' => $colors,
'duotone' => $duotone,
'fontSizes' => $fontSizes,
'fontFamilies' => $fontFamilies,
'colorPairs' => $colorPairs,
];
}
private static function extractColorSlug(string $value)
{
if (preg_match('/var\(--wp--preset--color--([^)]+)\)/', $value, $m)) {
return $m[1];
}
return null;
}
private static function extractColorPairs()
{
$styles = \wp_get_global_styles();
$settings = \wp_get_global_settings();
$custom = $settings['custom'] ?? [];
$pairs = [];
$seen = [];
$bodyTextSlug = self::extractColorSlug($styles['color']['text'] ?? '');
$bodyBgSlug = self::extractColorSlug($styles['color']['background'] ?? '');
$candidates = [];
if ($bodyTextSlug && $bodyBgSlug) {
$candidates[] = ['text' => $bodyTextSlug, 'bg' => $bodyBgSlug];
}
$btnTextSlug = self::extractColorSlug($styles['elements']['button']['color']['text'] ?? '')
?? self::extractColorSlug($custom['elements']['button']['color']['text'] ?? '');
$btnBgSlug = self::extractColorSlug($styles['elements']['button']['color']['background'] ?? '')
?? self::extractColorSlug($custom['elements']['button']['color']['background'] ?? '');
if ($btnTextSlug && $btnBgSlug) {
$candidates[] = ['text' => $btnTextSlug, 'bg' => $btnBgSlug];
}
$btnHoverTextSlug = self::extractColorSlug($styles['elements']['button'][':hover']['color']['text'] ?? '')
?? self::extractColorSlug($custom['elements']['button'][':hover']['color']['text'] ?? '');
$btnHoverBgSlug = self::extractColorSlug($styles['elements']['button'][':hover']['color']['background'] ?? '')
?? self::extractColorSlug($custom['elements']['button'][':hover']['color']['background'] ?? '');
if ($btnHoverTextSlug && $btnHoverBgSlug) {
$candidates[] = ['text' => $btnHoverTextSlug, 'bg' => $btnHoverBgSlug];
}
$linkTextSlug = self::extractColorSlug($styles['elements']['link']['color']['text'] ?? '')
?? self::extractColorSlug($custom['elements']['link']['color']['text'] ?? '');
if ($linkTextSlug && $bodyBgSlug) {
$candidates[] = ['text' => $linkTextSlug, 'bg' => $bodyBgSlug];
}
$headingTextSlug = self::extractColorSlug($styles['elements']['heading']['color']['text'] ?? '')
?? self::extractColorSlug($custom['elements']['heading']['color']['text'] ?? '');
if ($headingTextSlug && $bodyBgSlug) {
$candidates[] = ['text' => $headingTextSlug, 'bg' => $bodyBgSlug];
}
if ($bodyTextSlug) {
$candidates[] = ['text' => $bodyTextSlug, 'bg' => 'tertiary'];
}
if ($headingTextSlug && $headingTextSlug !== $bodyTextSlug) {
$candidates[] = ['text' => $headingTextSlug, 'bg' => 'tertiary'];
}
foreach ($candidates as $pair) {
$key = $pair['text'] . '|' . $pair['bg'];
if (isset($seen[$key])) {
continue;
}
$seen[$key] = true;
$pairs[] = $pair;
}
return $pairs;
}
/**
* Scan the style dirs to locate if they have variations.
* Ported from here:
* https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/class-wp-theme-json-resolver.php#L810
*
* @return bool
*/
private function hasThemeVariations()
{
$base_directory = get_stylesheet_directory() . '/styles';
$template_directory = get_template_directory() . '/styles';
if (is_dir($base_directory) && glob($base_directory . '/*.json', GLOB_NOSORT)) {
return true;
}
// Only check parent if it's different from child
if (
$template_directory !== $base_directory &&
is_dir($template_directory) &&
glob($template_directory . '/*.json', GLOB_NOSORT)
) {
return true;
}
return false;
}
/**
* Get suggestions for the user.
*
* @return array
*/
private function getSuggestions()
{
return [
[
'id' => 'what-tours-are-available',
'icon' => 'video',
'message' => __('What tours are available?', 'extendify-local'),
'workflowId' => 'list-tours',
'source' => 'plugin',
'available' => [],
],
[
'id' => 'publish-this-page',
'icon' => 'published',
'message' => __('Publish this page', 'extendify-local'),
'workflowId' => 'update-post-status',
'source' => 'plugin',
'available' => ['context' => ['postStatus' => 'draft']],
],
[
'id' => 'unpublish-this-page',
'icon' => 'drafts',
'message' => __('Unpublish this page', 'extendify-local'),
'workflowId' => 'update-post-status',
'source' => 'plugin',
'available' => ['context' => ['postStatus' => 'publish']],
],
[
'id' => 'change-site-title',
'icon' => 'edit',
'message' => __('Change website title', 'extendify-local'),
'workflowId' => 'edit-wp-setting',
'source' => 'plugin',
'available' => ['abilities' => ['canEditSettings']],
],
[
'id' => 'change-theme-colors',
'icon' => 'styles',
'message' => __('Change website colors', 'extendify-local'),
'workflowId' => 'change-theme-variation',
'source' => 'plugin',
'available' => ['context' => ['hasThemeVariations']],
],
[
'id' => 'change-theme-fonts',
'icon' => 'typography',
'message' => __('Change website fonts', 'extendify-local'),
'workflowId' => 'change-theme-fonts-variation',
'source' => 'plugin',
'available' => ['context' => ['hasThemeVariations']],
],
[
'id' => 'change-site-style',
'icon' => 'styles',
'message' => __('Change website style', 'extendify-local'),
'workflowId' => 'change-site-vibes',
'source' => 'plugin',
'available' => ['abilities' => ['canEditThemes'], 'context' => ['isUsingVibes']],
],
[
'id' => 'edit-text-on-this-page',
'icon' => 'edit',
'message' => __('Edit text on this page', 'extendify-local'),
'workflowId' => 'edit-post-strings',
'source' => 'plugin',
'available' => ['abilities' => ['canEditPost'], 'context' => ['postId', 'usingBlockEditor']],
],
[
'id' => 'how-can-i-create-a-post',
'icon' => 'help',
'message' => __('How can I create a post?', 'extendify-local'),
'workflowId' => 'answer-general-questions',
'source' => 'plugin',
'available' => ['abilities' => ['canEditPost']],
],
[
'id' => 'how-can-i-create-a-page',
'icon' => 'help',
'message' => __('How can I create a page?', 'extendify-local'),
'workflowId' => 'create-page',
'source' => 'plugin',
'available' => ['abilities' => ['canEditPost']],
],
[
'id' => 'how-can-i-install-a-plugin',
'icon' => 'help',
'message' => __('How can I install a plugin?', 'extendify-local'),
'workflowId' => 'recommend-plugins',
'source' => 'plugin',
'available' => ['abilities' => ['canActivatePlugins']],
],
[
'id' => 'how-can-i-change-my-theme',
'icon' => 'help',
'message' => __('How can I change my theme?', 'extendify-local'),
'workflowId' => 'answer-general-questions',
'source' => 'plugin',
'available' => ['abilities' => ['canEditThemes']],
],
[
'id' => 'how-can-i-change-the-site-footer',
'icon' => 'help',
'message' => __('How can I change the website footer?', 'extendify-local'),
'workflowId' => 'answer-general-questions',
'source' => 'plugin',
'available' => ['abilities' => ['canEditThemes']],
],
[
'id' => 'how-can-i-change-the-site-header',
'icon' => 'help',
'message' => __('How can I change the website header?', 'extendify-local'),
'workflowId' => 'answer-general-questions',
'source' => 'plugin',
'available' => ['abilities' => ['canEditThemes']],
],
[
'id' => 'how-can-i-upload-an-image',
'icon' => 'help',
'message' => __('How can I upload an image?', 'extendify-local'),
'workflowId' => 'answer-general-questions',
'source' => 'plugin',
'available' => ['abilities' => ['canUploadMedia']],
],
[
'id' => 'how-can-i-change-the-site-icon',
'icon' => 'help',
'message' => __('How can I change the website icon?', 'extendify-local'),
'workflowId' => 'answer-general-questions',
'source' => 'plugin',
'available' => ['abilities' => ['canUploadMedia']],
],
[
'id' => 'how-can-i-change-my-site-title',
'icon' => 'help',
'message' => __('How can I change my website title?', 'extendify-local'),
'workflowId' => 'edit-wp-setting',
'source' => 'plugin',
'available' => ['abilities' => ['canEditSettings']],
],
[
'id' => 'how-can-i-change-my-site-tagline',
'icon' => 'help',
'message' => __('How can I change my website tagline?', 'extendify-local'),
'workflowId' => 'edit-wp-setting',
'source' => 'plugin',
'available' => ['abilities' => ['canEditSettings']],
],
[
'id' => 'how-can-i-change-my-site-language',
'icon' => 'help',
'message' => __('How can I change my website language?', 'extendify-local'),
'workflowId' => 'answer-general-questions',
'source' => 'plugin',
'available' => ['abilities' => ['canEditSettings']],
],
[
'id' => 'change-website-animation',
'icon' => 'swatch',
'message' => __('Change website animation', 'extendify-local'),
'workflowId' => 'change-animation',
'source' => 'plugin',
'available' => ['abilities' => ['canEditSettings']],
],
[
'id' => 'change-website-logo',
'icon' => 'siteLogo',
'message' => __('Change website logo', 'extendify-local'),
'workflowId' => 'update-logo',
'source' => 'plugin',
'available' => ['abilities' => ['canEditSettings', 'canUploadMedia']],
],
[
'id' => 'change-website-browser-icon',
'icon' => 'siteLogo',
'message' => __('Change website browser icon', 'extendify-local'),
'workflowId' => 'update-site-icon',
'source' => 'plugin',
'available' => ['abilities' => ['canEditSettings', 'canUploadMedia']],
],
];
}
/**
* Check if the user is in the Gutenberg or FSE editor.
*
* @return false|bool
*/
public function isGutenbergOrFse()
{
if (!is_admin() || !function_exists('get_current_screen')) {
return false;
}
$screen = get_current_screen();
if (!$screen) {
return false;
}
$is_fse =
in_array($screen->id, ['site-editor', 'appearance_page_gutenberg-edit-site'], true) ||
(isset($GLOBALS['pagenow']) && $GLOBALS['pagenow'] === 'site-editor.php');
$is_gutenberg =
$screen->base === 'post' &&
method_exists($screen, 'is_block_editor') &&
$screen->is_block_editor();
return $is_fse || $is_gutenberg;
}
}