acf = $acf;
$this->viewMarkup = $viewMarkup;
$this->cardMarkup = $cardMarkup;
$this->queryBuilder = $queryBuilder;
$this->options = $options;
$this->cache = $cache;
$this->displayingView = [];
}
protected static function getErrorMarkup(string $shortcode, array $args, string $error): string
{
$attrs = [];
foreach ($args as $name => $value) {
$attrs[] = sprintf('%s="%s"', $name, $value);
}
return sprintf(
"
Shortcode error: %s %s
",
$error,
sprintf('(%s %s)', $shortcode, implode(' ', $attrs))
);
}
// static, as called also in AcfGroup
public static function isAcfProPluginAvailable(): bool
{
return class_exists('acf_pro');
}
protected function getAcfView(Post $dataPost, int $viewId, int $pageId): AcfView
{
$viewGroup = $this->cache->getAcfViewData($viewId);
// don't use the 'AcfViewData->markup' field, as user can override it (and it shouldn't be supported)
$viewMarkup = $this->viewMarkup->getMarkup($viewGroup, $pageId);
return new AcfView($this->acf, $viewGroup, $dataPost, $pageId, $viewMarkup);
}
protected function getAcfCard(AcfCardData $acfCardData): AcfCard
{
return new AcfCard($acfCardData, $this->queryBuilder, $this->cardMarkup);
}
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) {
$acfView = $this->getAcfView(new Post($previewPostId), $post->ID, 0);
// without minify, it's a preview
$acfView->insertFields(false);
$viewHTML = $acfView->getHTML();
} else {
$viewHTML = $this->viewMarkup->getMarkup($acfViewData, 0);
}
// 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);
$acfCard = $this->getAcfCard($acfCardData);
$acfCard->queryPostsAndInsertData(1, false);
$acfViewData = $this->cache->getAcfViewData($acfCardData->acfViewId);
// amend to allow work the '#card' alias
$viewHTML = str_replace(
'class="acf-card ',
'id="card" class="acf-card ',
$acfCard->getHTML()
);
$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(array $jsData = []): void
{
$jsData = array_merge_recursive($jsData, [
'markupTextarea' => [
[
'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_MARKUP),
'isReadOnly' => true,
'mode' => 'htmlmixed',
],
[
'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_CSS_CODE),
'isReadOnly' => false,
'mode' => 'css',
],
[
'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_JS_CODE),
'isReadOnly' => false,
'mode' => 'javascript',
],
[
'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_CUSTOM_MARKUP),
'isReadOnly' => false,
'mode' => 'htmlmixed',
],
[
'idSelector' => AcfViewData::getAcfFieldName(AcfViewData::FIELD_PHP_VARIABLES),
'isReadOnly' => false,
'mode' => 'php',
],
[
'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_MARKUP),
'isReadOnly' => true,
'mode' => 'htmlmixed',
],
[
'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_CSS_CODE),
'isReadOnly' => false,
'mode' => 'css',
],
[
'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_JS_CODE),
'isReadOnly' => false,
'mode' => 'javascript',
],
[
'idSelector' => AcfCardData::getAcfFieldName(
AcfCardData::FIELD_CUSTOM_MARKUP
),
'isReadOnly' => false,
'mode' => 'htmlmixed',
],
[
'idSelector' => AcfCardData::getAcfFieldName(AcfCardData::FIELD_QUERY_PREVIEW),
'isReadOnly' => true,
'mode' => 'javascript',
],
],
'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(),
]);
wp_enqueue_style(AcfViews::NAME, $this->getAssetsUrl('admin.css'), [], $this->getVersion());
// jquery is necessary for select2 events
wp_enqueue_script(AcfViews::NAME, $this->getAssetsUrl('admin.js'), ['jquery',], $this->getVersion());
wp_localize_script(AcfViews::NAME, 'acf_views', $jsData);
}
protected function printPluginsCSS(): string
{
return '';
}
public function isShortcodeAvailableForUser(array $userRoles, array $shortcodeArgs): bool
{
$userWithRoles = (string)($shortcodeArgs['user-with-roles'] ?? '');
$userWithRoles = trim($userWithRoles);
$userWithRoles = $userWithRoles ?
explode(',', $userWithRoles) :
[];
$userWithoutRoles = (string)($shortcodeArgs['user-without-roles'] ?? '');
$userWithoutRoles = trim($userWithoutRoles);
$userWithoutRoles = $userWithoutRoles ?
explode(',', $userWithoutRoles) :
[];
if (!$userWithRoles &&
!$userWithoutRoles) {
return true;
}
$userHasAllowedRoles = !!array_intersect($userWithRoles, $userRoles);
$userHasDeniedRoles = !!array_intersect($userWithoutRoles, $userRoles);
if (($userWithRoles && !$userHasAllowedRoles) ||
($userWithoutRoles && $userHasDeniedRoles)) {
return false;
}
return true;
}
public function getName(): string
{
return $this->name;
}
public function getSlug(): string
{
return $this->slug;
}
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/' . $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();
}
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(
'' .
'
"ACF Views" plugin requires Advanced Custom Fields plugin to be enabled on your website
(either Free or Pro version).' .
' Please enable "Advanced Custom Fields" plugin or "ACF Views" plugin won\'t work properly.
' .
'
',
$acfPluginInstallLink,
$acfFree,
$acfPro
);
}
public function showWarningAboutOpcacheIssue(): void
{
if (!function_exists('ini_get') ||
'0' !== ini_get('opcache.save_comments')) {
return;
}
$readMoreLink = sprintf('Read more', self::CONFLICTS_URL);
printf('Compatibility issue detected! "ACF Views" plugin requires "PHPDoc" comments in code.
Please change the "opcache.save_comments" option in your php.ini file to the default value of "1" on your hosting. %s
', $readMoreLink);
}
public function acfCardsShortcode($attrs): string
{
$attrs = $attrs ?
(array)$attrs :
[];
if (!$this->isShortcodeAvailableForUser(wp_get_current_user()->roles, $attrs)) {
return '';
}
$cardId = (int)($attrs['card-id'] ?? 0);
$acfCardPost = $cardId ?
get_post($cardId) :
null;
if (!$acfCardPost ||
!in_array($acfCardPost->post_type, [AcfCards::NAME,], true) ||
'publish' !== $acfCardPost->post_status
) {
return self::getErrorMarkup(self::SHORTCODE_CARDS, $attrs, 'card-id attribute is missing or wrong');
}
$acfCardData = $this->cache->getAcfCardData($cardId);
$acfCard = $this->getAcfCard($acfCardData);
$acfCard->queryPostsAndInsertData(1);
return $acfCard->getHTML();
}
public function acfViewsShortcode($attrs): string
{
$attrs = $attrs ?
(array)$attrs :
[];
if (!$this->isShortcodeAvailableForUser(wp_get_current_user()->roles, $attrs)) {
return '';
}
// equals to 0 on WooCommerce Shop Page, but in this case pageID can't be gotten with built-in WP functions
$currentPageId = get_queried_object_id();
$viewId = (int)($attrs['view-id'] ?? 0);
$acfViewPost = $viewId ?
get_post($viewId) :
null;
if (!$acfViewPost ||
!in_array($acfViewPost->post_type, [AcfViews::NAME,], true) ||
'publish' !== $acfViewPost->post_status
) {
return self::getErrorMarkup(self::SHORTCODE, $attrs, 'view-id attribute is missing or wrong');
}
global $post;
// a. dataPostId from the shortcode argument
$dataPostId = (string)($attrs['object-id'] ?? 0);
if (in_array($dataPostId, ['$user$', 'options',], true)) {
$dataPostId = '$user$' === $dataPostId ?
'user_' . get_current_user_id() :
$dataPostId;
} else {
$dataPostId = (int)$dataPostId;
// b. dataPostId from the current loop (WordPress posts, WooCommerce products...)
$dataPostId = $dataPostId ?: ($post->ID ?? 0);
// c. dataPostId from the current page
$dataPostId = $dataPostId ?: $currentPageId;
// validate the ID
$dataPostId = get_post($dataPostId) ?
$dataPostId :
0;
}
if (!$dataPostId) {
return self::getErrorMarkup(self::SHORTCODE, $attrs, 'object-id argument contains the wrong value');
}
// recursionKey must consist from both. It's allowed to use the same View for a post_object field, but with another id
$recursionKey = $viewId . '-' . $dataPostId;
/*
* In case with post_object and relationship fields can be a recursion
* e.g. There is a post_object field. PostA contains link to PostB. PostB contains link to postA. View displays PostA...
* In this case just return empty string, without any error message (so user can display PostB in PostA without issues)
*/
if (isset($this->displayingView[$recursionKey])) {
return '';
}
$this->displayingView[$recursionKey] = true;
$acfView = $this->getAcfView(new Post($dataPostId), $viewId, $currentPageId);
$acfView->insertFields();
$html = $acfView->getHTML();
unset($this->displayingView[$recursionKey]);
return $html;
}
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))) {
$this->enqueueAdminAssets();
}
}
public function printCustomAssets(): void
{
$allJsCode = '';
$allCssCode = $this->printPluginsCSS();
$views = $this->viewMarkup->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->cardMarkup->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) :
'';
}
$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->viewMarkup->isWithGoogleMap()) {
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('maps.min.js'),
[],
$this->getVersion(),
true
);
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,
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(
'"ACF Views" and "ACF Views Pro" should not be active at the same time. We\'ve automatically deactivated "%s".',
1 === $deactivatedNoticeId ?
'ACF Views' :
'ACF Views Pro'
);
$this->options->deleteTransient(Options::TRANSIENT_DEACTIVATED_OTHER_INSTANCES);
echo sprintf(
'',
$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(
'Upgrade to Pro to unlock. %s',
Plugin::PRO_VERSION_URL,
$field['instructions']
);
}
}
if ($isAcfProField) {
$field['instructions'] = sprintf(
'(ACF Pro version is required for this feature) %s',
'https://www.advancedcustomfields.com/pro/',
$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(
'Thank you for creating with WordPress and ACF Views.',
'https://wordpress.org/',
self::BASIC_VERSION_URL
);
$content .= " " . sprintf(
"Take 2 minute survey to improve the ACF Views plugin.",
self::SURVEY_URL
);
return sprintf(
'',
$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, 'printCustomAssets']);
add_action('wp_footer', [$this, 'enqueueGoogleMapsJS']);
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_shortcode(Plugin::SHORTCODE, [$this, 'acfViewsShortcode']);
add_shortcode(Plugin::SHORTCODE_CARDS, [$this, 'acfCardsShortcode']);
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']);
}
}