acf = $acf;
$this->viewMarkup = $viewMarkup;
$this->cardMarkup = $cardMarkup;
$this->queryBuilder = $queryBuilder;
$this->options = $options;
$this->cache = $cache;
$this->displayingView = [];
}
protected static function getErrorMarkup(string $error): string
{
return "
{$error}
";
}
// 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);
$acfView->insertFields();
// without minify, it's a preview
$viewHTML = $acfView->getHTML(false);
} 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);
$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(false)
);
$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);
}
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 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_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']);
}
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;
}
// fixme add a read more link (to our docs)
echo "Detected compatibility issue. \"ACF Views\" plugin relies on PHPDoc comments in code, but they're unavailable in this hosting configuration.
Please ask your hosting support back the 'opcache.save_comments' option in the 'php.ini' file to the default '1'.
";
}
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;
$shortCodeName = ':' . self::SHORTCODE_CARDS . ':';
if (!$acfCardPost ||
!in_array($acfCardPost->post_type, [AcfCards::NAME,], true) ||
'publish' !== $acfCardPost->post_status
) {
return self::getErrorMarkup($shortCodeName . ' 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;
$shortCodeName = ':' . self::SHORTCODE . ':';
if (!$acfViewPost ||
!in_array($acfViewPost->post_type, [AcfViews::NAME,], true) ||
'publish' !== $acfViewPost->post_status
) {
return self::getErrorMarkup($shortCodeName . ' view-id attribute is missing or wrong');
}
global $post;
// a. dataPostId from the shortcode argument
$dataPostId = (string)($attrs['object-id'] ?? 0);
$dataPostId = 'options' !== $dataPostId ?
(int)$dataPostId :
$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;
$dataPostId = ('options' === $dataPostId || get_post($dataPostId)) ?
$dataPostId :
0;
if (!$dataPostId) {
return self::getErrorMarkup($shortCodeName . ' object-id argument is not a valid post id');
}
// 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
{
$code = '';
$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
$code .= $cssCode ?
sprintf("", $view->getSource(), $cssCode) :
'';
$code .= $jsCode ?
sprintf("", $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
$code .= $cssCode ?
sprintf("", $card->getSource(), $cssCode) :
'';
$code .= $jsCode ?
sprintf("", $card->getSource(), $jsCode) :
'';
}
echo $code;
}
public function enqueueGoogleMapsJS(): void
{
if (!function_exists('acf_get_setting')) {
return;
}
$isPageWithMaps = false;
$views = $this->viewMarkup->getRenderedViews();
foreach ($views as $view) {
if (!$view->isHasMapField()) {
continue;
}
$isPageWithMaps = true;
break;
}
if (!$isPageWithMaps) {
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
);
}
}