* Help Center Script loader.
namespace Extendify\Shared;
defined('ABSPATH') || die('No direct access.');
use Extendify\PartnerData;
use Extendify\Shared\Controllers\UserSelectionController;
use Extendify\Shared\DataProvider\ResourceData;
use Extendify\Shared\Services\AdminMenuList;
use Extendify\Shared\Services\ApexDomain\ApexDomain;
use Extendify\Shared\Services\Escaper;
use Extendify\Shared\Services\PluginDependencies\SimplyBook;
use Extendify\SiteSettings;
use Extendify\Shared\Controllers\ImageGenerationController;
use Extendify\Shared\DataProvider\ProductsData;
* This class handles any file loading for the admin area.
* Adds various actions to set up the page
public function __construct()
\add_action('init', [$this, 'addExtraMetaFields']);
\add_action('admin_enqueue_scripts', [$this, 'loadGlobalScripts']);
\add_action('wp_enqueue_scripts', [$this, 'loadGlobalScripts']);
\add_action('wp_ajax_search-install-plugins', [$this, 'recordPluginsSearchTerms'], -1);
\add_action('rest_api_init', [$this, 'recordBlocksSearchTerms']);
\add_action('wp_ajax_query-themes', [$this, 'recordThemesSearchTerms'], -1);
\add_action('simplybook_activation', [SimplyBook::class, 'getIndustryCode'], 10, 0);
// phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
* Adds scripts to every page
public function loadGlobalScripts()
$version = constant('EXTENDIFY_DEVMODE') ? uniqid() : Config::$version;
* Enqueue shared JavaScript files if they exist in the asset manifest
* Ensures proper loading order: vendors -> common
// Enqueue the vendor chunk first.
if (isset(Config::$assetManifest['extendify-vendors.js'])) {
EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-vendors.js'],
// Enqueue the common chunk next, dependent on vendors.
if (isset(Config::$assetManifest['extendify-common.js'])) {
EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-common.js'],
* Loads a unique runtime generated by Webpack to manage all the other generated scripts.
* Without this runtime, the other Extendify scripts won't load.
$scriptAssetPath = EXTENDIFY_PATH . 'public/build/' . Config::$assetManifest['extendify-runtime.php'];
$scriptAsset = file_exists($scriptAssetPath) ? require $scriptAssetPath : $fallback;
foreach ($scriptAsset['dependencies'] as $style) {
\wp_enqueue_style($style);
Config::$slug . '-runtime-scripts',
EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-runtime.js'],
$scriptAsset['dependencies'],
$scriptAssetPath = EXTENDIFY_PATH . 'public/build/' . Config::$assetManifest['extendify-shared.php'];
$scriptAsset = file_exists($scriptAssetPath) ? require $scriptAssetPath : $fallback;
foreach ($scriptAsset['dependencies'] as $style) {
\wp_enqueue_style($style);
Config::$slug . '-shared-scripts',
EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-shared.js'],
$scriptAsset['dependencies'],
$siteProfile = \get_option('extendify_site_profile', []);
$siteProfile = is_string($siteProfile) ? json_decode($siteProfile, true) : $siteProfile;
$siteProfile = is_array($siteProfile) ? $siteProfile : [];
'aiDescription' => 'description',
'aiObjective' => 'objective',
'aiSiteCategory' => 'category',
'aiStructure' => 'structure',
'aiKeywords' => 'imageSearchTerms',
'logoObjectName' => 'logoObjectName',
// Converts legacy site profile to new one
foreach ($map as $old => $new) {
if (!array_key_exists($new, $siteProfile)) {
$siteProfile[$new] = $siteProfile[$old] ?? null;
$partnerData = PartnerData::getPartnerData();
$userConsent = get_user_meta(get_current_user_id(), 'extendify_ai_consent', true);
if (!function_exists('get_plugins')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
Config::$slug . '-shared-scripts',
'window.extSharedData = ' . \wp_json_encode([
'root' => \esc_url_raw(rest_url(Config::$slug . '/' . Config::$apiVersion)),
'homeUrl' => \esc_url_raw(\get_home_url()),
'adminUrl' => \esc_url_raw(\admin_url()),
'nonce' => \esc_attr(\wp_create_nonce('wp_rest')),
'devbuild' => (bool) constant('EXTENDIFY_DEVMODE'),
'assetPath' => \esc_url(EXTENDIFY_URL . 'public/assets'),
'siteId' => \esc_attr(\get_option('extendify_site_id', '')),
'siteCreatedAt' => \esc_attr(SiteSettings::getSiteCreatedAt()),
'themeSlug' => \esc_attr(\get_option('stylesheet')),
'version' => \esc_attr(Config::$version),
'siteTitle' => \esc_attr(\get_bloginfo('name')),
'siteProfile' => $siteProfile,
'wpLanguage' => \esc_attr(\get_locale()),
'wpVersion' => \esc_attr(\get_bloginfo('version')),
'isBlockTheme' => function_exists('wp_is_block_theme') ? (bool) wp_is_block_theme() : false,
'userId' => \esc_attr(\get_current_user_id()),
'partnerLogo' => \esc_attr(PartnerData::$logo),
'partnerId' => \esc_attr(PartnerData::$id),
'partnerName' => \esc_attr(PartnerData::$name),
'launchDataLegacy' => \wp_json_encode((UserSelectionController::get()->get_data() ?? [])),
'resourceData' => \wp_json_encode((new ResourceData())->getData()),
'showAIConsent' => isset($partnerData['showAIConsent']) ? (bool) $partnerData['showAIConsent'] : false,
'showChat' => (bool) (PartnerData::setting('showChat') || constant('EXTENDIFY_DEVMODE')),
'useAgentOnboarding' => (bool) (
PartnerData::setting('useAgentOnboarding') ||
constant('EXTENDIFY_DEVMODE')
'showAIPageCreation' => (bool) (
PartnerData::setting('showAIPageCreation') || constant('EXTENDIFY_DEVMODE')
'showAILogo' => (bool) PartnerData::setting('showAILogo'),
'showImprint' => array_map('esc_attr', (array) PartnerData::setting('showImprint')),
'consentTermsCustom' => \wp_kses((html_entity_decode(($partnerData['consentTermsCustom'] ?? ''))
'userGaveConsent' => $userConsent ? (bool) $userConsent : false,
'installedPlugins' => array_map('esc_attr', array_keys(\get_plugins())),
'activePlugins' => array_map('esc_attr', array_values(\get_option('active_plugins', []))),
'frontPage' => \esc_attr(\get_option('page_on_front', 0)),
'globalStylesPostID' => \esc_attr(\WP_Theme_JSON_Resolver::get_user_global_styles_post_id()),
'showLocalizedCopy' => (bool) array_key_exists('showLocalizedCopy', $partnerData),
'activity' => \wp_json_encode(\get_option('extendify_shared_activity', null)),
'showDraft' => isset($partnerData['showDraft']) ? (bool) $partnerData['showDraft'] : false,
'showLaunch' => Config::$showLaunch,
'phpVersion' => \esc_attr(PHP_VERSION),
'apexDomain' => PartnerData::setting('enableApexDomain')
? rawurlencode(ApexDomain::getApexDomain(\get_home_url()))
'launchCompletedAt' => \esc_attr(\get_option('extendify_onboarding_completed', false)),
'showSiteQuestions' => (bool) (
PartnerData::setting('showLaunchQuestions') || Config::preview('launch-questions')
'products' => ProductsData::get(),
'showAIAgents' => (bool) (PartnerData::setting('showAIAgents') || Config::preview('ai-agent')),
'pluginGroupId' => Escaper::recursiveEscAttr(PartnerData::setting('pluginGroupId')),
'adminPagesMenuList' => get_option('_transient_extendify_admin_pages_menu', []),
'globalState' => ImageGenerationController::get()->get_data(),
\wp_set_script_translations('extendify-common', 'extendify-local', EXTENDIFY_PATH . 'languages/js');
\wp_set_script_translations(
Config::$slug . '-shared-scripts',
EXTENDIFY_PATH . 'languages/js'
Config::$slug . '-shared-common-styles',
EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-shared.css'],
$cssColorVars = PartnerData::cssVariableMapping();
$cssString = implode('; ', array_map(function ($k, $v) {
}, array_keys($cssColorVars), $cssColorVars));
Config::$slug . '-shared-common-styles',
wp_strip_all_tags(":root { $cssString; }")
* Adds additional meta fields to post types
public function addExtraMetaFields()
// Add a tag to pages that were made with Launch.
register_post_meta('page', 'made_with_extendify_launch', [
register_post_meta('post', 'made_with_extendify_launch', [
* Records plugin search terms from the WordPress plugin search page
* Stores terms in the 'extendify_plugin_search_terms' option
public function recordPluginsSearchTerms()
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.NonceVerification.Missing
$searchTerm = isset($_POST['s']) ? \sanitize_text_field(\wp_unslash(urldecode($_POST['s']))) : '';
if (empty($searchTerm)) {
$searchTerms = \get_option('extendify_plugin_search_terms', []);
$searchTerms[] = $searchTerm;
$searchTerms = array_unique($searchTerms);
\update_option('extendify_plugin_search_terms', $searchTerms);
* Records block search terms from the WordPress the editor's block search
* Stores terms in the 'extendify_block_search_terms' option
public function recordBlocksSearchTerms()
// Exits early if it is not a REST API request.
if (!\wp_is_serving_rest_request()) {
// Exits early if it is not a GET request.
if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] !== 'GET') {
$restRoute = ($wp->query_vars['rest_route'] ?? '');
// Exits early if it's not the blocks search route.
if ($restRoute !== '/wp/v2/block-directory/search') {
$searchTerm = \sanitize_text_field(\wp_unslash(($wp->query_vars['term'] ?? '')));
// Exits early if the search term is empty or is not user input.
if (empty($searchTerm) || str_starts_with($searchTerm, 'block:')) {
// Get current search term from the url and merge it with existing search terms.
$searchTerms = \get_option('extendify_block_search_terms', []);
\update_option('extendify_block_search_terms', array_merge($searchTerms, [$searchTerm]));
* Updates the user meta to disable the welcome guide from the Gutenberg editor
* and close the pattern modal.
public function updateUserMeta()
$currentPreferences = get_user_meta(get_current_user_id(), 'wp_persisted_preferences', true);
if (!$currentPreferences) {
$currentPreferences = [];
$postPreferences = array_key_exists('core/edit-post', $currentPreferences)
? $currentPreferences['core/edit-post']
$corePreferences = array_key_exists('core', $currentPreferences) ? $currentPreferences['core'] : [];
$newPreferences = array_merge($currentPreferences, [
'core/edit-post' => array_merge($postPreferences, ['welcomeGuide' => false]),
'core' => array_merge($corePreferences, ['enableChoosePatternModal' => false]),
'_modified' => wp_date('Y-m-d\TH:i:s.v\Z'),
update_user_meta(get_current_user_id(), 'wp_persisted_preferences', $newPreferences);
* Records search terms used when browsing themes in the admin interface.
* This method listens for the 'query-themes' AJAX action, extracts the search term
* from the request, and stores it in the 'extendify_theme_search_terms' option.
* Duplicate terms are filtered out.
public function recordThemesSearchTerms()
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.NonceVerification.Missing
$searchTerm = \sanitize_text_field(\wp_unslash(urldecode(($_POST['request']['search'] ?? ''))));
if (empty($searchTerm)) {
$searchTerms = \get_option('extendify_theme_search_terms', []);
$searchTerms[] = $searchTerm;
$searchTerms = array_unique($searchTerms);
\update_option('extendify_theme_search_terms', $searchTerms);