viewMarkup = $viewMarkup; $this->cardMarkup = $cardMarkup; $this->options = $options; $this->cache = $cache; $this->acfViewFactory = $acfViewFactory; $this->acfCardFactory = $acfCardFactory; $this->bufferLevel = 0; } // static, as called also in AcfGroup public static function isAcfProPluginAvailable(): bool { return class_exists('acf_pro'); } public static function getThemeTextDomain(): string { return (string)wp_get_theme()->get('TextDomain'); } public static function getLabelTranslation(string $label, string $textDomain = ''): string { $textDomain = $textDomain ?: self::getThemeTextDomain(); // escape quotes to keep compatibility with the generated translation file // (quotes there escaped to prevent breaking the PHP string) $label = str_replace("'", "'", $label); $label = str_replace('"', """, $label); $translation = __($label, $textDomain); $translation = str_replace("'", "'", $translation); $translation = str_replace(""", '"', $translation); return $translation; } protected function getViewPreviewJsData(): array { $jsData = [ 'HTML' => '', 'CSS' => '', ]; global $post; if (!$this->isCPTScreen(AcfViews::NAME) || 'publish' !== $post->post_status) { return $jsData; } $acfViewData = $this->cache->getAcfViewData($post->ID); $previewPostId = $acfViewData->previewPost ?: 0; if ($previewPostId) { $postData = new Post($previewPostId, [], false, get_current_user_id()); // without minify, it's a preview $viewHTML = $this->acfViewFactory->createAndGetHtml( $postData, $post->ID, 0, false, ); } else { // $this->viewMarkup->getMarkup give TWIG, there is no sense to show it // so the HTML is empty until the preview Post ID is selected $viewHTML = ''; } // amend to allow work the '#view' alias $viewHTML = str_replace('class="acf-view ', 'id="view" class="acf-view ', $viewHTML); $jsData['HTML'] = htmlentities($viewHTML, ENT_QUOTES); $jsData['CSS'] = htmlentities($acfViewData->getCssCode(false, true), ENT_QUOTES); $jsData['HOME'] = get_site_url(); return $jsData; } protected function getCardPreviewJsData(): array { $jsData = [ 'HTML' => '', 'CSS' => '', ]; global $post; if (!$this->isCPTScreen(AcfCards::NAME) || 'publish' !== $post->post_status) { return $jsData; } $acfCardData = $this->cache->getAcfCardData($post->ID); $acfCardHtml = $this->acfCardFactory->createAndGetHtml($acfCardData, 1, false); $viewId = $this->cache->getPostIdByUniqueId($acfCardData->acfViewId, AcfViews::NAME); if (!$viewId) { return $jsData; } $acfViewData = $this->cache->getAcfViewData($viewId); // amend to allow work the '#card' alias $viewHTML = str_replace( 'class="acf-card ', 'id="card" class="acf-card ', $acfCardHtml ); $jsData['HTML'] = htmlentities($viewHTML, ENT_QUOTES); // Card CSS without minification as it's for views' purposes $jsData['CSS'] = htmlentities($acfCardData->getCssCode(false, true), ENT_QUOTES); $jsData['VIEW_CSS'] = htmlentities($acfViewData->getCssCode(), ENT_QUOTES); $jsData['HOME'] = get_site_url(); return $jsData; } protected function enqueueAdminAssets(string $currentBase, array $jsData = []): void { // add, edit pages if ('post' === $currentBase) { $jsData = array_merge_recursive($jsData, [ 'markupTextarea' => [ [ 'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_MARKUP), 'isReadOnly' => true, 'mode' => '_twig', ], [ 'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_CSS_CODE), 'isReadOnly' => false, 'mode' => '_css', ], [ 'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_JS_CODE), 'isReadOnly' => false, 'mode' => '_js', ], [ 'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_CUSTOM_MARKUP), 'isReadOnly' => false, 'mode' => '_twig', ], [ 'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_PHP_VARIABLES), 'isReadOnly' => false, 'mode' => '_php', ], [ 'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_MARKUP), 'isReadOnly' => true, 'mode' => '_twig', ], [ 'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_CSS_CODE), 'isReadOnly' => false, 'mode' => '_css', ], [ 'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_JS_CODE), 'isReadOnly' => false, 'mode' => '_js', ], [ 'idSelector' => AcfCardData::getAcfFieldName( AcfCardData::FIELD_CUSTOM_MARKUP ), 'isReadOnly' => false, 'mode' => '_twig', ], [ 'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_QUERY_PREVIEW), 'isReadOnly' => true, 'mode' => '_twig', ], ], 'fieldSelect' => [ [ 'mainSelectId' => Item::getAcfFieldName(Item::FIELD_GROUP), 'subSelectId' => Field::getAcfFieldName(Field::FIELD_KEY), 'identifierInputId' => Field::getAcfFieldName(Field::FIELD_ID), ], [ 'mainSelectId' => AcfCardData::getAcfFieldName( AcfCardData::FIELD_ORDER_BY_META_FIELD_GROUP ), 'subSelectId' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_ORDER_BY_META_FIELD_KEY), 'identifierInputId' => '', ], [ 'mainSelectId' => Field::getAcfFieldName(Field::FIELD_KEY), 'subSelectId' => RepeaterField::getAcfFieldName(RepeaterField::FIELD_KEY), 'identifierInputId' => RepeaterField::getAcfFieldName(RepeaterField::FIELD_ID), ], [ 'mainSelectId' => MetaField::getAcfFieldName(MetaField::FIELD_GROUP), 'subSelectId' => MetaField::getAcfFieldName(MetaField::FIELD_FIELD_KEY), 'identifierInputId' => '', ], [ 'mainSelectId' => TaxField::getAcfFieldName(TaxField::FIELD_TAXONOMY), 'subSelectId' => TaxField::getAcfFieldName(TaxField::FIELD_TERM), 'identifierInputId' => '', ], ], 'viewPreview' => $this->getViewPreviewJsData(), 'cardPreview' => $this->getCardPreviewJsData(), 'ajaxUrl' => admin_url('admin-ajax.php'), ]); wp_enqueue_style( AcfViews::NAME . '_cpt-item', $this->getAssetsUrl('admin/css/cpt-item.min.css'), [], $this->getVersion() ); // jquery is necessary for select2 events wp_enqueue_script( AcfViews::NAME . '_cpt-item', $this->getAssetsUrl('admin/js/cpt-item.min.js'), // make sure acf is loaded ['jquery', 'acf-input',], $this->getVersion(), [ 'in_footer' => true, // in footer, so if we need to include others, like 'ace.js' we can include in header ] ); wp_localize_script(AcfViews::NAME . '_cpt-item', 'acf_views', $jsData); } // 'edit' means 'list page' if ('edit' === $currentBase) { wp_enqueue_style( AcfViews::NAME . '_list-page', $this->getAssetsUrl('admin/css/list-page.min.css'), [], $this->getVersion() ); } // 'dashboard' for all the custom pages (but not for edit/add pages) if (0 === strpos($currentBase, 'acf_views_page_')) { wp_enqueue_style( AcfViews::NAME . '_page', $this->getAssetsUrl('admin/css/dashboard.min.css'), [], $this->getVersion() ); } // plugin-header for all the pages without exception wp_enqueue_style( AcfViews::NAME . '_common', $this->getAssetsUrl('admin/css/common.min.css'), [], $this->getVersion() ); } protected function printPluginsCSS(): string { return ''; } public function getName(): string { return __('ACF Views', 'acf-views'); } public function getSlug(): string { return $this->slug; } public function getShortSlug(): string { return $this->shortSlug; } public function getVersion(): string { // return strval(time()); return $this->version; } public function isProVersion(): bool { return $this->isProVersion; } public function getAssetsUrl(string $file): string { return plugin_dir_url(__FILE__) . 'assets/' . $file; } public function getAcfProAssetsUrl(string $file): string { return plugin_dir_url(__FILE__) . 'AcfPro/assets/' . $file; } public function isAcfPluginAvailable(bool $isProOnly = false): bool { // don't use 'is_plugin_active()' as the function available lately return static::isAcfProPluginAvailable() || (!$isProOnly && class_exists('ACF')); } public function startBuffering(): void { ob_start(); $this->bufferLevel = ob_get_level(); } public function printStylesStub(): void { echo ''; } public function showWarningAboutInactiveAcfPlugin(): void { if ($this->isAcfPluginAvailable()) { return; } $acfPluginInstallLink = get_admin_url(null, static::ACF_INSTALL_URL); $acfFree = 'https://wordpress.org/plugins/advanced-custom-fields/'; $acfPro = 'https://www.advancedcustomfields.com/pro/'; echo sprintf( '
' . '

%s %s (%s %s %s) %s

' . '
', __('"ACF Views" requires', 'acf-views'), $acfPluginInstallLink, __('Advanced Custom Fields', 'acf-views'), $acfFree, __('free', 'acf-views'), __('or', 'acf-views'), $acfPro, __('pro', 'acf-views'), __('to be installed and activated.', 'acf-views'), ); } public function showWarningAboutOpcacheIssue(): void { if (!function_exists('ini_get') || '0' !== ini_get('opcache.save_comments')) { return; } $readMoreLink = sprintf( '%s', self::CONFLICTS_URL, __('Read more', 'acf-views') ); printf( '

%s
%s %s

', __('Compatibility issue detected! "ACF Views" plugin requires "PHPDoc" comments in code.', 'acf-views'), __( 'Please change the "opcache.save_comments" option in your php.ini file to the default value of "1" on your hosting.', 'acf-views' ), $readMoreLink ); } public function enqueueAdminScripts(): void { $currentScreen = get_current_screen(); if (!$currentScreen || (!in_array($currentScreen->id, [AcfViews::NAME, AcfCards::NAME,], true) && !in_array($currentScreen->post_type, [AcfViews::NAME, AcfCards::NAME], true))) { return; } $this->enqueueAdminAssets($currentScreen->base); } public function printCustomAssets(): void { $allJsCode = ''; $allCssCode = $this->printPluginsCSS(); $views = $this->acfViewFactory->getRenderedViews(); foreach ($views as $view) { $cssCode = $view->getCssCode(); // 'minify' JS $jsCode = str_replace(["\t", "\n", "\r"], '', $view->jsCode); $jsCode = trim($jsCode); // no escaping, it's a CSS code, so e.g '.a > .b' shouldn't be escaped $allCssCode .= $cssCode ? sprintf("\n/*view-%s*/\n%s", $view->getSource(), $cssCode) : ''; $allJsCode .= $jsCode ? sprintf("\n/*view-%s*/\n%s", $view->getSource(), $jsCode) : ''; } $cards = $this->acfCardFactory->getRenderedCards(); foreach ($cards as $card) { $cssCode = $card->getCssCode(); // 'minify' JS $jsCode = str_replace(["\t", "\n", "\r"], '', $card->jsCode); $jsCode = trim($jsCode); // no escaping, it's a CSS code, so e.g '.a > .b' shouldn't be escaped $allCssCode .= $cssCode ? sprintf("\n/*card-%s*/\n%s", $card->getSource(), $cssCode) : ''; $allJsCode .= $jsCode ? sprintf("\n/*card-%s*/\n%s", $card->getSource(), $jsCode) : ''; } if (!$allCssCode && !$allJsCode) { // do not close the buffer, if it's not ours // (then ours will be closed automatically with the end of script execution) if (ob_get_level() === $this->bufferLevel) { echo ob_get_clean(); } return; } // close previous buffers. Some plugins may not close, if detect that ob_get_level() is another than was // e.g. 'lightbox-photoswipe' while (ob_get_level() > $this->bufferLevel) { echo ob_get_clean(); } $pageContent = ob_get_clean(); $cssTag = $allCssCode ? sprintf("", $allCssCode) : ''; $pageContent = str_replace('', $cssTag, $pageContent); echo $pageContent; if ($allJsCode) { printf("", $allJsCode); } } public function enqueueGoogleMapsJS(): void { if (!function_exists('acf_get_setting') || !$this->acfViewFactory->getMaps()) { return; } $apiData = apply_filters('acf/fields/google_map/api', []); $key = $apiData['key'] ?? ''; $key = !$key ? acf_get_setting('google_api_key') : $key; if (!$key) { return; } wp_enqueue_script( AcfViews::NAME . '_maps', $this->getAssetsUrl('front/js/maps.min.js'), [], $this->getVersion(), [ 'in_footer' => true, ] ); wp_localize_script( AcfViews::NAME . '_maps', 'acfViewsMaps', $this->acfViewFactory->getMaps() ); wp_enqueue_script( AcfViews::NAME . '_google-maps', sprintf('https://maps.googleapis.com/maps/api/js?key=%s&callback=acfViewsGoogleMaps', $key), [ // setup deps, to make sure loaded only after plugin's maps.min.js AcfViews::NAME . '_maps', ], null, [ 'in_footer' => true, ] ); } public function isCPTScreen(string $cptName, array $targetBase = ['post', 'add',]): bool { $currentScreen = get_current_screen(); $isTargetPost = in_array($currentScreen->id, [$cptName,], true) || in_array($currentScreen->post_type, [$cptName], true); // base = edit (list management), post (editing), add (adding) return $isTargetPost && in_array($currentScreen->base, $targetBase, true); } public function deactivateOtherInstances(string $activatedPlugin): void { if (!in_array($activatedPlugin, ['acf-views/acf-views.php', 'acf-views-pro/acf-views-pro.php'], true)) { return; } $pluginToDeactivate = 'acf-views/acf-views.php'; $deactivatedNoticeId = 1; // If we just activated the free version, deactivate the pro version. if ($activatedPlugin === $pluginToDeactivate) { $pluginToDeactivate = 'acf-views-pro/acf-views-pro.php'; $deactivatedNoticeId = 2; } if (is_multisite() && is_network_admin()) { $activePlugins = (array)get_site_option('active_sitewide_plugins', []); $activePlugins = array_keys($activePlugins); } else { $activePlugins = (array)get_option('active_plugins', []); } foreach ($activePlugins as $pluginBasename) { if ($pluginToDeactivate !== $pluginBasename) { continue; } $this->options->setTransient( Options::TRANSIENT_DEACTIVATED_OTHER_INSTANCES, $deactivatedNoticeId, 1 * HOUR_IN_SECONDS ); deactivate_plugins($pluginBasename); return; } } // notice when either Basic or Pro was automatically deactivated public function showPluginDeactivatedNotice(): void { $deactivatedNoticeId = (int)$this->options->getTransient(Options::TRANSIENT_DEACTIVATED_OTHER_INSTANCES); // not set = false = 0 if (!in_array($deactivatedNoticeId, [1, 2,], true)) { return; } $message = sprintf( '%s "%s".', __( "'ACF Views' and 'ACF Views Pro' should not be active at the same time. We've automatically deactivated", 'acf-views' ), 1 === $deactivatedNoticeId ? __('ACF Views', 'acf-views') : __('ACF Views Pro', 'acf-views') ); $this->options->deleteTransient(Options::TRANSIENT_DEACTIVATED_OTHER_INSTANCES); echo sprintf( '
' . '

%s

' . '
', $message ); } public function amendProFieldLabelAndInstruction(array $field): array { $isProField = !$this->isProVersion() && key_exists('a-pro', $field); $isAcfProField = !$this->isAcfPluginAvailable(true) && key_exists('a-acf-pro', $field); if (!$isProField && !$isAcfProField) { return $field; } $type = $field['type'] ?? ''; $field['label'] = $field['label'] ?? ''; $field['instructions'] = $field['instructions'] ?? ''; if ($isProField) { if ('tab' === $type) { $field['label'] = $field['label'] . ' (Pro)'; } else { $field['instructions'] = sprintf( '%s %s %s', Plugin::PRO_VERSION_URL, __('Upgrade to Pro', 'acf-views'), __('to unlock.', 'acf-views'), $field['instructions'] ); } } if ($isAcfProField) { $field['instructions'] = sprintf( '(%s %s) %s', 'https://www.advancedcustomfields.com/pro/', __('ACF Pro', 'acf-views'), __('version is required for this feature', 'acf-views'), $field['instructions'] ); } return $field; } public function addClassToAdminProFieldClasses(array $wrapper, array $field): array { $isProField = !$this->isProVersion() && key_exists('a-pro', $field); $isAcfProField = !$this->isAcfPluginAvailable(true) && key_exists('a-acf-pro', $field); if (!$isProField && !$isAcfProField) { return $wrapper; } if (!key_exists('class', $wrapper)) { $wrapper['class'] = ''; } $wrapper['class'] .= ' acf-views-pro'; return $wrapper; } public function makeEnqueueJSAsync(string $tag, string $handle): string { if (!in_array($handle, [ AcfViews::NAME . '_maps', AcfViews::NAME . '_google-maps' ], true)) { return $tag; } // defer, not async as order should be kept (google-maps will call a callback from maps' js) return str_replace(' src', ' defer src', $tag); } public function getAdminUrl( string $page = '', string $cptName = AcfViews::NAME, string $base = 'edit.php' ): string { $pageArg = $page ? '&page=' . $page : ''; // don't use just '/wp-admin/x' as some websites can have custom admin url, like 'wp.org/wordpress/wp-admin' $pageUrl = get_admin_url(null, $base . '?post_type='); return $pageUrl . $cptName . $pageArg; } public function printSurveyLink(string $html): string { if (!$this->isCPTScreen(AcfViews::NAME, ['post', 'add', 'edit',]) && !$this->isCPTScreen(AcfCards::NAME, ['post', 'add', 'edit',])) { return $html; } $content = sprintf( '%s %s %s %s.', __('Thank you for creating with', 'acf-views'), 'https://wordpress.org/', __('WordPress', 'acf-views'), __('and', 'acf-views'), self::BASIC_VERSION_URL, __('ACF Views', 'acf-views') ); $content .= " " . sprintf( "%s %s %s", __('Take', 'acf-views'), self::SURVEY_URL, __('2 minute survey', 'acf-views'), __('to improve the ACF Views plugin.', 'acf-views') ); return sprintf( '%s', $content ); } public function setHooks(): void { add_action('admin_notices', [$this, 'showWarningAboutInactiveAcfPlugin']); add_action('admin_notices', [$this, 'showWarningAboutOpcacheIssue']); add_action('admin_enqueue_scripts', [$this, 'enqueueAdminScripts']); add_action('wp_footer', [$this, 'enqueueGoogleMapsJS']); // printCustomAssets() contains ob_get_clean, so must be executed after all other scripts add_action('wp_footer', [$this, 'printCustomAssets'], 9999); add_action('activated_plugin', [$this, 'deactivateOtherInstances']); add_action('pre_current_active_plugins', [$this, 'showPluginDeactivatedNotice']); add_action('wp_head', [$this, 'printStylesStub']); // don't use 'get_header', as it doesn't work in blocks theme add_action('template_redirect', [$this, 'startBuffering']); add_filter('acf/prepare_field', [$this, 'amendProFieldLabelAndInstruction']); add_filter('acf/field_wrapper_attributes', [$this, 'addClassToAdminProFieldClasses'], 10, 2); add_filter('script_loader_tag', [$this, 'makeEnqueueJSAsync'], 10, 2); add_filter('admin_footer_text', [$this, 'printSurveyLink']); } }