Current File : /home/honehdyv/readbtooom.com/wp-content/plugins/autodescription/inc/classes/detect.class.php |
<?php
/**
* @package The_SEO_Framework\Classes\Facade\Detect
* @subpackage The_SEO_Framework\Compatibility
*/
namespace The_SEO_Framework;
\defined( 'THE_SEO_FRAMEWORK_PRESENT' ) or die;
/**
* The SEO Framework plugin
* Copyright (C) 2015 - 2023 Sybre Waaijer, CyberWire B.V. (https://cyberwire.nl/)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Class The_SEO_Framework\Detect
*
* Detects other plugins and themes
*
* @since 2.8.0
*/
class Detect extends Render {
/**
* Returns list of active plugins.
* Memoizes the return value.
*
* @since 2.6.1
* @credits Jetpack for some code.
*
* @return array List of active plugins.
*/
public function active_plugins() {
// phpcs:ignore, WordPress.CodeAnalysis.AssignmentInCondition -- I know.
if ( null !== $memo = memo() ) return $memo;
$active_plugins = (array) \get_option( 'active_plugins', [] );
if ( \is_multisite() ) {
// Due to legacy code, active_sitewide_plugins stores them in the keys,
// whereas active_plugins stores them in the values. array_keys() resolves the disparity.
$network_plugins = array_keys( \get_site_option( 'active_sitewide_plugins', [] ) );
if ( $network_plugins )
$active_plugins = array_merge( $active_plugins, $network_plugins );
}
sort( $active_plugins );
return memo( $active_plugins );
}
/**
* Filterable list of conflicting plugins.
*
* @since 2.6.0
* @credits Jetpack for most code.
*
* @return array List of conflicting plugins.
*/
public function conflicting_plugins() {
$conflicting_plugins = [
'seo_tools' => [
'Yoast SEO' => 'wordpress-seo/wp-seo.php',
'Yoast SEO Premium' => 'wordpress-seo-premium/wp-seo-premium.php',
'All in One SEO Pack' => 'all-in-one-seo-pack/all_in_one_seo_pack.php',
'SEO Ultimate' => 'seo-ultimate/seo-ultimate.php',
'SEOPress' => 'wp-seopress/seopress.php',
'Rank Math' => 'seo-by-rank-math/rank-math.php',
'Smart Crawl' => 'smartcrawl-seo/wpmu-dev-seo.php',
],
'sitemaps' => [
'Google XML Sitemaps' => 'google-sitemap-generator/sitemap.php',
'XML Sitemap & Google News feeds' => 'xml-sitemap-feed/xml-sitemap.php',
'Google Sitemap by BestWebSoft' => 'google-sitemap-plugin/google-sitemap-plugin.php',
'Simple Wp Sitemap' => 'simple-wp-sitemap/simple-wp-sitemap.php', // Remove?
],
'open_graph' => [
'Facebook Open Graph Meta Tags for WordPress' => 'wonderm00ns-simple-facebook-open-graph-tags/wonderm00n-open-graph.php',
'Open Graph' => 'opengraph/opengraph.php', // Redundant.
'Open Graph Protocol Framework' => 'open-graph-protocol-framework/open-graph-protocol-framework.php', // Redundant.
'Shareaholic2' => 'shareaholic/sexy-bookmarks.php',
'WordPress Social Sharing Optimization' => 'wpsso/wpsso.php',
],
'twitter_card' => [],
];
/**
* @since 2.6.0
* @param array $conflicting_plugins The conflicting plugin list.
*/
return (array) \apply_filters_ref_array( 'the_seo_framework_conflicting_plugins', [ $conflicting_plugins ] );
}
/**
* Fetches type of conflicting plugins.
*
* @since 2.6.0
* @since 4.2.0 Now always runs the filter, even when $type is not registered.
*
* @param string $type The Key from $this->conflicting_plugins()
* @return array
*/
public function get_conflicting_plugins( $type = 'seo_tools' ) {
/**
* @since 2.6.1
* @param array $conflicting_plugins Conflicting plugins
* @param string $type The type of plugins to get.
*/
return (array) \apply_filters_ref_array(
'the_seo_framework_conflicting_plugins_type',
[
$this->conflicting_plugins()[ $type ] ?? [],
$type,
]
);
}
/**
* Detect active plugin by constant, class or function existence.
*
* Note: Class check is 3 times as slow as defined check. Function check is 2 times as slow.
*
* @since 1.3.0
* @since 2.8.0 1. Can now check for globals.
* 2. Switched detection order from FAST to SLOW.
* @since 4.0.6 Can no longer autoload classes.
*
* @param array $plugins Array of array for constants, classes and / or functions to check for plugin existence.
* @return boolean True if plugin exists or false if plugin constant, class or function not detected.
*/
public function detect_plugin( $plugins ) {
foreach ( $plugins['globals'] ?? [] as $name )
if ( isset( $GLOBALS[ $name ] ) )
return true;
// Check for constants
foreach ( $plugins['constants'] ?? [] as $name )
if ( \defined( $name ) )
return true;
// Check for functions
foreach ( $plugins['functions'] ?? [] as $name )
if ( \function_exists( $name ) )
return true;
// Check for classes
foreach ( $plugins['classes'] ?? [] as $name )
if ( class_exists( $name, false ) ) // phpcs:ignore, TSF.Performance.Functions.PHP -- we don't autoload.
return true;
// No globals, constant, function, or class found to exist
return false;
}
/**
* Detect if you can use the given constants, functions and classes.
* All inputs must be available for this method to return true.
* Memoizes the return value for the input argument--sorts the array deeply to ensure a match.
*
* @since 2.5.2
* @since 4.1.4 Fixed sorting algorithm from fribbling-me to resolving-me. Nothing changed but legibility.
* @since 4.2.0 Rewrote sorting algorithm; now, it's actually good.
* @uses $this->detect_plugin_multi()
*
* @param array[] $plugins Array of array for globals, constants, classes
* and/or functions to check for plugin existence.
* @param bool $use_cache Bypasses cache if false
*/
public function can_i_use( $plugins = [], $use_cache = true ) {
if ( ! $use_cache )
return $this->detect_plugin_multi( $plugins );
ksort( $plugins );
foreach ( $plugins as &$test )
sort( $test );
// phpcs:ignore, WordPress.PHP.DiscouragedPHPFunctions -- No objects are inserted, nor is this ever unserialized.
$key = serialize( $test );
return memo( null, $key ) ?? memo( $this->detect_plugin_multi( $plugins ), $key );
}
/**
* Detect active plugin by constant, class or function existence.
* All parameters must match and return true.
*
* @since 2.5.2
* @since 4.0.6 1. Can now check for globals.
* 2. Switched detection order from FAST to SLOW.
* 3. Can no longer autoload classes.
* This method is only used by can_i_use(), and is only effective in the Ultimate Member compat file...
* @TODO deprecate?
*
* @param array[] $plugins Array of array for constants, classes
* and / or functions to check for plugin existence.
* @return bool True if ALL functions classes and constants exists
* or false if plugin constant, class or function not detected.
*/
public function detect_plugin_multi( $plugins ) {
// Check for globals
foreach ( $plugins['globals'] ?? [] as $name )
if ( ! isset( $GLOBALS[ $name ] ) )
return false;
// Check for constants
foreach ( $plugins['constants'] ?? [] as $name )
if ( ! \defined( $name ) )
return false;
// Check for functions
foreach ( $plugins['functions'] ?? [] as $name )
if ( ! \function_exists( $name ) )
return false;
// Check for classes
foreach ( $plugins['classes'] ?? [] as $name )
if ( ! class_exists( $name, false ) ) // phpcs:ignore, TSF.Performance.Functions.PHP -- we don't autoload.
return false;
// All classes, functions and constant have been found to exist
return true;
}
/**
* Checks if the (parent) theme name is loaded.
*
* @since 2.1.0
* @since 4.2.0 No longer "loads" the theme; instead, simply compares input to active theme options.
*
* @param string|array $themes The theme names to test.
* @return bool is theme active.
*/
public function is_theme( $themes = '' ) {
$active_theme = [
strtolower( \get_option( 'stylesheet' ) ), // Parent
strtolower( \get_option( 'template' ) ), // Child
];
foreach ( (array) $themes as $theme )
if ( \in_array( strtolower( $theme ), $active_theme, true ) )
return true;
return false;
}
/**
* Determines if other SEO plugins are active.
* Memoizes the return value.
*
* @since 1.3.0
* @since 2.6.0 Uses new style detection.
* @since 3.1.0 The filter no longer short-circuits the function when it's false.
*
* @return bool SEO plugin detected.
*/
public function detect_seo_plugins() {
// phpcs:ignore, WordPress.CodeAnalysis.AssignmentInCondition -- I know.
if ( null !== $memo = memo() ) return $memo;
$active_plugins = $this->active_plugins();
if ( ! $active_plugins ) return memo( false );
foreach ( $this->get_conflicting_plugins( 'seo_tools' ) as $plugin_name => $plugin ) {
if ( \in_array( $plugin, $active_plugins, true ) ) {
/**
* @since 2.6.1
* @since 3.1.0 Added second and third parameters.
* @param bool $detected Whether the plugin should be detected.
* @param string $plugin_name The plugin name as defined in `$this->conflicting_plugins()`.
* @param string $plugin The plugin that's been detected.
*/
if ( \apply_filters_ref_array(
'the_seo_framework_seo_plugin_detected',
[
true,
$plugin_name,
$plugin,
]
) ) {
$detected = true;
break;
}
}
}
return memo( $detected ?? false );
}
/**
* Determines if other Open Graph or SEO plugins are active.
* Memoizes the return value.
*
* @since 1.3.0
* @since 2.8.0 No longer checks for old style filter.
* @since 3.1.0 The filter no longer short-circuits the function when it's false.
*
* @return bool True if OG or SEO plugin detected.
*/
public function detect_og_plugin() {
// Detect SEO plugins beforehand.
if ( $this->detect_seo_plugins() )
return true;
// phpcs:ignore, WordPress.CodeAnalysis.AssignmentInCondition -- I know.
if ( null !== $memo = memo() ) return $memo;
$active_plugins = $this->active_plugins();
if ( ! $active_plugins ) return memo( false );
foreach ( $this->get_conflicting_plugins( 'open_graph' ) as $plugin_name => $plugin ) {
if ( \in_array( $plugin, $active_plugins, true ) ) {
/**
* @since 2.6.1
* @since 3.1.0 Added second and third parameters.
* @param bool $detected Whether the plugin should be detected.
* @param string $plugin_name The plugin name as defined in `$this->conflicting_plugins()`.
* @param string $plugin The plugin that's been detected.
*/
if ( \apply_filters_ref_array(
'the_seo_framework_og_plugin_detected',
[
true,
$plugin_name,
$plugin,
]
) ) {
$detected = true;
break;
}
}
}
return memo( $detected ?? false );
}
/**
* Determines if other Twitter Card plugins are active.
* Memoizes the return value.
*
* @since 2.6.0
* @since 3.1.0 The filter no longer short-circuits the function when it's false.
*
* @return bool Twitter Card plugin detected.
*/
public function detect_twitter_card_plugin() {
// Detect SEO plugins beforehand.
if ( $this->detect_seo_plugins() )
return true;
// phpcs:ignore, WordPress.CodeAnalysis.AssignmentInCondition -- I know.
if ( null !== $memo = memo() ) return $memo;
$active_plugins = $this->active_plugins();
if ( ! $active_plugins ) return memo( false );
foreach ( $this->get_conflicting_plugins( 'twitter_card' ) as $plugin_name => $plugin ) {
if ( \in_array( $plugin, $active_plugins, true ) ) {
/**
* @since 2.6.1
* @param bool $detected Whether the plugin should be detected.
* @param string $plugin_name The plugin name as defined in `$this->conflicting_plugins()`.
* @param string $plugin The plugin that's been detected.
*/
if ( \apply_filters_ref_array(
'the_seo_framework_twittercard_plugin_detected',
[
true,
$plugin_name,
$plugin,
]
) ) {
$detected = true;
break;
}
}
}
return memo( $detected ?? false );
}
/**
* Determines if other Schema.org LD+Json plugins are active.
*
* @since 1.3.0
* @since 2.6.1 Always return false. Let other plugin authors decide its value.
* @TODO Make a list of plugins, so the users are well-informed.
*
* @return bool Whether another Schema.org plugin is active.
*/
public function has_json_ld_plugin() {
/**
* @since 2.6.5
* @param bool $detected Whether a conflicting schema plugin is detected.
*/
return (bool) \apply_filters( 'the_seo_framework_ldjson_plugin_detected', false );
}
/**
* Determines if other Sitemap plugins are active.
* Memoizes the return value.
*
* @since 2.1.0
* @since 3.1.0 The filter no longer short-circuits the function when it's false.
*
* @return bool
*/
public function detect_sitemap_plugin() {
// Detect SEO plugins beforehand.
if ( $this->detect_seo_plugins() )
return true;
// phpcs:ignore, WordPress.CodeAnalysis.AssignmentInCondition -- I know.
if ( null !== $memo = memo() ) return $memo;
$active_plugins = $this->active_plugins();
if ( ! $active_plugins ) return memo( false );
foreach ( $this->get_conflicting_plugins( 'sitemaps' ) as $plugin_name => $plugin ) {
if ( \in_array( $plugin, $active_plugins, true ) ) {
/**
* @since 2.6.1
* @param bool $detected Whether the plugin should be detected.
* @param string $plugin_name The plugin name as defined in `$this->conflicting_plugins()`.
* @param string $plugin The plugin that's been detected.
*/
if ( \apply_filters(
'the_seo_framework_sitemap_plugin_detected',
[
true,
$plugin_name,
$plugin,
]
) ) {
$detected = true;
break;
}
}
}
return memo( $detected ?? false );
}
/**
* Tells whether WP 5.5 Core Sitemaps are used.
* Memoizes the return value.
*
* @since 4.1.2
*
* @return bool
*/
public function use_core_sitemaps() {
// phpcs:ignore, WordPress.CodeAnalysis.AssignmentInCondition -- I know.
if ( null !== $memo = memo() ) return $memo;
if ( $this->get_option( 'sitemaps_output' ) )
return memo( false );
$wp_sitemaps_server = \wp_sitemaps_get_server();
return memo(
method_exists( $wp_sitemaps_server, 'sitemaps_enabled' )
&& $wp_sitemaps_server->sitemaps_enabled()
);
}
/**
* Detects presence of a page builder that renders content dynamically.
*
* Detects the following builders:
* - Divi Builder by Elegant Themes
* - Visual Composer by WPBakery
*
* @since 4.1.0
*
* @return bool
*/
public function detect_non_html_page_builder() {
return memo() ?? memo(
/**
* @since 4.1.0
* @param bool $detected Whether an active page builder that renders content dynamically is detected.
* @NOTE not to be confused with `the_seo_framework_detect_non_html_page_builder`, which tests
* the page builder status for each post individually.
*/
(bool) \apply_filters(
'the_seo_framework_shortcode_based_page_builder_active',
$this->detect_plugin( [
'constants' => [
'ET_BUILDER_VERSION',
'WPB_VC_VERSION',
],
] )
)
);
}
/**
* Detects presence of robots.txt in root folder.
* Memoizes the return value.
*
* @since 2.5.2
* @since 4.0.0 Now tries to load `wp-admin/includes/file.php` to prevent a fatal error.
*
* @return bool Whether the robots.txt file exists.
*/
public function has_robots_txt() {
// phpcs:ignore, WordPress.CodeAnalysis.AssignmentInCondition -- I know.
if ( null !== $memo = memo() ) return $memo;
// Ensure get_home_path() is declared.
if ( ! \function_exists( '\\get_home_path' ) )
require_once ABSPATH . 'wp-admin/includes/file.php';
$path = \get_home_path() . 'robots.txt';
// phpcs:ignore, TSF.Performance.Functions.PHP -- we use path, not URL.
return memo( file_exists( $path ) );
}
/**
* Detects presence of sitemap.xml in root folder.
* Memoizes the return value.
*
* @since 2.5.2
* @since 4.0.0 Now tries to load `wp-admin/includes/file.php` to prevent a fatal error.
*
* @return bool Whether the sitemap.xml file exists.
*/
public function has_sitemap_xml() {
// phpcs:ignore, WordPress.CodeAnalysis.AssignmentInCondition -- I know.
if ( null !== $memo = memo() ) return $memo;
// Ensure get_home_path() is declared.
if ( ! \function_exists( '\\get_home_path' ) )
require_once ABSPATH . 'wp-admin/includes/file.php';
$path = \get_home_path() . 'sitemap.xml';
// phpcs:ignore, TSF.Performance.Functions.PHP -- we use path, not URL.
return memo( file_exists( $path ) );
}
/**
* Determines whether the main query supports custom SEO.
*
* @since 4.0.0
* @since 4.0.2 Now tests for an existing post/term ID when on singular/term pages.
* @since 4.0.3 Can now assert empty categories again by checking for taxonomy support.
* @since 4.2.4 Added detection for AJAX, Cron, JSON, and REST queries (they're not supported as SEO-able queries).
*
* @return bool
*/
public function query_supports_seo() {
// phpcs:ignore, WordPress.CodeAnalysis.AssignmentInCondition -- I know.
if ( null !== $memo = memo() ) return $memo;
switch ( true ) :
case \is_feed():
case \wp_doing_ajax():
case \wp_doing_cron():
case \wp_is_json_request():
case \defined( 'REST_REQUEST' ) && REST_REQUEST:
$supported = false;
break;
case $this->is_singular():
$supported = $this->is_post_type_supported() && $this->get_the_real_ID();
break;
case \is_post_type_archive():
$supported = $this->is_post_type_archive_supported();
break;
case $this->is_term_meta_capable():
// When a term has no posts attached, it'll not return a post type, and it returns a 404 late in the loop.
// This is because get_post_type() tries to assert the first post in the loop here.
// Thus, we test for is_taxonomy_supported() instead.
$supported = $this->is_taxonomy_supported() && $this->get_the_real_ID();
break;
// This includes 404.
default:
$supported = true;
break;
// TODO consider this instead of the current default? (it'd make the AJAX through REST test obsolete)
// Every recognized query (aside from $this->is_singular()/is_post_type_archive for we already covered those in full)
// case \is_404():
// case $this->is_search():
// case $this->is_real_front_page():
// case $this->is_archive():
// $supported = true;
// break;
// default:
// $supported = false;
// break;
endswitch;
/**
* Override false negatives on exploit.
*
* This protects against (accidental) negative-SEO bombarding.
* Support broken queries, so we can noindex them.
*/
if ( ! $supported && $this->is_query_exploited() )
$supported = true;
/**
* @since 4.0.0
* @param bool $supported Whether the query supports SEO.
*/
return memo( (bool) \apply_filters( 'the_seo_framework_query_supports_seo', $supported ) );
}
/**
* Determines when paged/page is exploited.
* Memoizes the return value.
*
* Google is acting "smart" nowadays, and follows everything that remotely resembles a link. Therefore, unintentional
* queries can occur in WordPress. WordPress deals with this well, alas, the query parser (WP_Query::parse_query)
* doesn't rectify the mixed signals it receives. Instead, it only sanitizes it, resulting in a combobulated mess.
* Ultimately, this leads to non-existing blog archives, among other failures.
*
* Example 1: `/?p=nonnumeric` will cause an issue. We will see a non-existing blog page. `is_home` is true, but
* `page_id` leads to 0 while the database expects the blog page to be another page. So, `is_posts_page` is
* incorrectly false. This is mitigated via the canonical URL, but that MUST output, thus overriding otherwise chosen
* and expected behavior.
*
* Example 2: `/page/2/?p=nonnumeric` will cause a bigger issue. What happens is that `is_home` will again be true,
* but so will `is_paged`. `paged` will be set to `2` (as per example URL). The page ID will again be set to `0`,
* which is completely false. The canonical URL will be malformed. Even moreso, Google can ignore the canonical URL,
* so we MUST output noindex.
*
* Example 3: `/page/2/?X=nonnumeric` will also cause the same issues as in example 2. Where X can be:
* `page_id`, `attachment_id`, `year`, `monthnum`, `day`, `w`, `m`, and of course `p`.
*
* Example 4: `/?hour=nonnumeric`, the same issue as Example 1. The canonical URL is malformed, noindex is set, and
* link relationships will be active. A complete mess. `minute` and `second` are also affected the same way.
*
* Example 5: `/page/2/?p=0`, this is the trickiest. It's indicative of a paginated blog, but also the homepage. When
* the homepage is not a blog, then this query is malformed. Otherwise, however, it's a good query.
*
* @since 4.0.5
* @since 4.2.7 1. Added detection `not_home_as_page`, specifically for query variable `search`.
* 2. Improved detection for `cat` and `author`, where the value may only be numeric above 0.
* @since 4.2.8 Now blocks any publicly registered variable requested to the home-as-page.
* @global \WP_Query $wp_query
*
* @return bool Whether the query is (accidentally) exploited.
* Defaults to false when `advanced_query_protection` option is disabled.
* False when there's a query-ID found.
* False when no custom query is set (for the homepage).
* Otherwise, it performs query tests.
*/
public function is_query_exploited() {
// phpcs:ignore, WordPress.CodeAnalysis.AssignmentInCondition -- I know.
if ( null !== $memo = memo() ) return $memo;
if ( ! $this->get_option( 'advanced_query_protection' ) )
return memo( false );
// When the page ID is not 0, a real page will always be returned.
if ( $this->get_the_real_ID() )
return memo( false );
global $wp_query;
// When no special query data is registered, ignore this. Don't set cache.
if ( ! isset( $wp_query->query ) )
return false;
/**
* @since 4.0.5
* @param array $exploitables The exploitable endpoints by type.
* @since 4.2.7 Added index `not_home_as_page` with value `search`.
*/
$exploitables = \apply_filters(
'the_seo_framework_exploitable_query_endpoints',
[
'numeric' => [
'page_id',
'attachment_id',
'year',
'monthnum',
'day',
'w',
'm',
'p',
'paged', // 'page' is mitigated by WordPress.
'hour',
'minute',
'second',
'subpost_id',
],
'numeric_array' => [
'cat',
'author',
],
'requires_s' => [
'sentence',
],
// When the blog (home) is a page then these requests to any registered query variable will cause issues,
// but only when the page ID returns 0. (We already tested for `if ( $this->get_the_real_ID() )` above).
// This global's property is only populated with requested parameters that match registered `public_query_vars`.
// TODO: We only need one to pass this test. We could use array_key_first()... (PHP7.3+) -> Might be mixed.
'not_home_as_page' => array_keys( $GLOBALS['wp']->query_vars ?? [] ),
]
);
$query = $wp_query->query;
foreach ( $exploitables as $type => $qvs ) :
foreach ( $qvs as $qv ) :
// Only test isset, because falsey or empty-array is what we need to test against.
if ( ! isset( $query[ $qv ] ) ) continue;
switch ( $type ) :
case 'numeric':
if ( '0' === $query[ $qv ] || ! is_numeric( $query[ $qv ] ) )
return memo( true );
break;
case 'numeric_array':
// We can't protect non-pretty permalinks.
if ( ! $this->pretty_permalinks ) break;
// If WordPress didn't canonical_redirect() the user yet, it's exploited.
// WordPress mitigates this via a 404 query when a numeric value is found.
if ( ! preg_match( '/^[1-9][0-9]*$/', $query[ $qv ] ) )
return memo( true );
break;
case 'requires_s':
if ( ! isset( $query['s'] ) )
return memo( true );
break;
case 'not_home_as_page':
// isset($query[$qv]) is already executed. Just test if homepage ID still works.
// !$this->get_the_real_ID() is already executed. Just test if home is a page.
if ( $this->is_home_as_page() )
return memo( true );
break;
default:
break;
endswitch;
endforeach;
endforeach;
return memo( false );
}
/**
* Tests if the post type archive of said post type contains public posts.
* Memoizes the return value.
*
* @since 4.2.0
* @slow The queried result is not stored in WP Post's cache, which would allow
* direct access to all values of the post (if requested). This is because
* we're using `'fields' => 'ids'` instead of `'fields' => 'all'`.
*
* @param string $post_type The post type to test.
* @return bool True if a post is found in the archive, false otherwise.
*/
public function has_posts_in_post_type_archive( $post_type ) {
// phpcs:ignore, WordPress.CodeAnalysis.AssignmentInCondition -- I know.
if ( null !== $memo = memo( null, $post_type ) ) return $memo;
$query = new \WP_Query( [
'posts_per_page' => 1,
'post_type' => [ $post_type ],
'orderby' => 'date',
'order' => 'ASC',
'post_status' => 'publish',
'has_password' => false,
'fields' => 'ids',
'cache_results' => false,
'no_found_rows' => true,
] );
return memo( ! empty( $query->posts ), $post_type );
}
/**
* Detects if the current or inputted post type is supported and not disabled.
*
* @since 3.1.0
* @since 4.0.5 The `$post_type` fallback now uses a real query ID, instead of `$GLOBALS['post']`;
* mitigating issues with singular-archives pages (blog, shop, etc.).
*
* @param string $post_type Optional. The post type to check.
* @return bool
*/
public function is_post_type_supported( $post_type = '' ) {
$post_type = $post_type ?: $this->get_current_post_type();
/**
* @since 2.6.2
* @since 3.1.0 The first parameter is always a boolean now.
* @param bool $supported Whether the post type is supported.
* @param string $post_type_evaluated The evaluated post type.
*/
return (bool) \apply_filters_ref_array(
'the_seo_framework_supported_post_type',
[
$post_type
&& ! $this->is_post_type_disabled( $post_type )
&& \in_array( $post_type, $this->get_public_post_types(), true ),
$post_type,
]
);
}
/**
* Detects if the current or inputted post type's archive is supported and not disabled.
*
* @since 4.2.8
* @uses `tsf()->is_post_type_supported()`
*
* @param string $post_type Optional. The post type's archive to check.
* @return bool
*/
public function is_post_type_archive_supported( $post_type = '' ) {
$post_type = $post_type ?: $this->get_current_post_type();
/**
* @since 4.2.8
* @param bool $supported Whether the post type archive is supported.
* @param string $post_type_evaluated The evaluated post type.
*/
return (bool) \apply_filters_ref_array(
'the_seo_framework_supported_post_type_archive',
[
$post_type
&& $this->is_post_type_supported( $post_type )
&& \in_array( $post_type, $this->get_public_post_type_archives(), true ),
$post_type,
]
);
}
/**
* Determines if the taxonomy supports The SEO Framework.
*
* Checks if at least one taxonomy objects post type supports The SEO Framework,
* and wether the taxonomy is public and rewritable.
*
* @since 4.0.0
*
* @param string $taxonomy Optional. The taxonomy name.
* @return bool True if at least one post type in taxonomy isn't disabled.
*/
public function is_taxonomy_supported( $taxonomy = '' ) {
$taxonomy = $taxonomy ?: $this->get_current_taxonomy();
/**
* @since 3.1.0
* @since 4.0.0 Now returns only returns false when all post types in the taxonomy aren't supported.
* @param bool $post_type Whether the post type is supported
* @param string $post_type_evaluated The evaluated post type.
*/
return (bool) \apply_filters_ref_array(
'the_seo_framework_supported_taxonomy',
[
$taxonomy
&& ! $this->is_taxonomy_disabled( $taxonomy )
&& \in_array( $taxonomy, $this->get_public_taxonomies(), true ),
$taxonomy,
]
);
}
/**
* Checks (current) Post Type for having taxonomical archives.
* Memoizes the return value for the input argument.
*
* @since 2.9.3
* @since 4.0.5 The `$post_type` fallback now uses a real query ID, instead of `$GLOBALS['post']`;
* mitigating issues with singular-archives pages (blog, shop, etc.).
* @global \WP_Screen $current_screen
*
* @param string $post_type Optional. The post type to check.
* @return bool True when the post type has taxonomies.
*/
public function post_type_supports_taxonomies( $post_type = '' ) {
// phpcs:ignore, WordPress.CodeAnalysis.AssignmentInCondition -- I know.
if ( null !== $memo = memo( null, $post_type ) ) return $memo;
$post_type = $post_type ?: $this->get_current_post_type();
// Return false if no post type if found -- do not memo that, for query call might be too early.
return $post_type && memo( (bool) \get_object_taxonomies( $post_type, 'names' ), $post_type );
}
/**
* Returns a list of all supported post types with archives.
* Memoizes the return value.
*
* @since 4.2.0
* @since 4.2.8 Now filters via `tsf()->is_post_type_archive_supported()`.
*
* @return string[] Supported post types with post type archive support.
*/
public function get_supported_post_type_archives() {
return memo() ?? memo(
array_values(
array_filter(
$this->get_public_post_type_archives(),
[ $this, 'is_post_type_archive_supported' ]
)
)
);
}
/**
* Gets all post types that have PTA and could possibly support SEO.
* Memoizes the return value.
*
* @since 4.2.0
* @since 4.2.8 Added filter `the_seo_framework_public_post_type_archives`.
*
* @return string[] Public post types with post type archive support.
*/
public function get_public_post_type_archives() {
return umemo( __METHOD__ )
?? umemo(
__METHOD__,
/**
* Do not consider using this filter. Properly register your post type, noob.
*
* @since 4.2.8
* @param string[] $post_types The public post types.
*/
\apply_filters(
'the_seo_framework_public_post_type_archives',
array_values(
array_filter(
$this->get_public_post_types(),
static function( $post_type ) {
return \get_post_type_object( $post_type )->has_archive ?? false;
}
)
)
)
);
}
/**
* Returns a list of all supported post types.
*
* @since 3.1.0
*
* @return string[] All supported post types.
*/
public function get_supported_post_types() {
return memo() ?? memo(
array_values(
array_filter(
$this->get_public_post_types(),
[ $this, 'is_post_type_supported' ]
)
)
);
}
/**
* Gets all post types that could possibly support SEO.
* Memoizes the return value.
*
* @since 4.1.0
* @since 4.1.4 Now resets the index keys of the return value.
*
* @return string[] All public post types.
*/
protected function get_public_post_types() {
return umemo( __METHOD__ )
?? umemo(
__METHOD__,
/**
* Do not consider using this filter. Properly register your post type, noob.
*
* @since 4.2.0
* @param string[] $post_types The public post types.
*/
\apply_filters(
'the_seo_framework_public_post_types',
array_values(
array_filter(
array_unique(
array_merge(
$this->get_forced_supported_post_types(),
// array_keys() because get_post_types() gives a sequential array.
array_keys( (array) \get_post_types( [ 'public' => true ] ) )
)
),
'is_post_type_viewable'
)
)
)
);
}
/**
* Returns a list of builtin public post types.
*
* @since 3.1.0
* @since 4.2.0 Removed memoization.
*
* @return string[] Forced supported post types.
*/
protected function get_forced_supported_post_types() {
/**
* @since 3.1.0
* @param string[] $forced Forced supported post types
*/
return (array) \apply_filters_ref_array(
'the_seo_framework_forced_supported_post_types',
[
array_values( \get_post_types( [
'public' => true,
'_builtin' => true,
] ) ),
]
);
}
/**
* Returns a list of all supported taxonomies.
*
* @since 4.2.0
*
* @return string[] All supported taxonomies.
*/
public function get_supported_taxonomies() {
return memo() ?? memo(
array_values(
array_filter( $this->get_public_taxonomies(), [ $this, 'is_taxonomy_supported' ] )
)
);
}
/**
* Gets all taxonomies that could possibly support SEO.
* Memoizes the return value.
*
* @since 4.1.0
*
* @return string[] The taxonomies that are public.
*/
protected function get_public_taxonomies() {
return umemo( __METHOD__ )
?? umemo(
__METHOD__,
/**
* Do not consider using this filter. Properly register your taxonomy, noob.
*
* @since 4.2.0
* @param string[] $post_types The public post types.
*/
\apply_filters(
'the_seo_framework_public_taxonomies',
array_filter(
array_unique(
array_merge(
$this->get_forced_supported_taxonomies(),
// array_values() because get_taxonomies() gives a sequential array.
array_values( \get_taxonomies( [
'public' => true,
'_builtin' => false,
] ) )
)
),
'is_taxonomy_viewable'
)
)
);
}
/**
* Returns a list of builtin public taxonomies.
*
* @since 4.1.0
* @since 4.2.0 Removed memoization.
*
* @return string[] Forced supported taxonomies
*/
protected function get_forced_supported_taxonomies() {
/**
* @since 4.1.0
* @param string[] $forced Forced supported post types
*/
return (array) \apply_filters_ref_array(
'the_seo_framework_forced_supported_taxonomies',
[
array_values( \get_taxonomies( [
'public' => true,
'_builtin' => true,
] ) ),
]
);
}
/**
* Determines if the post type is disabled from SEO all optimization.
*
* @since 3.1.0
* @since 3.1.2 Now is fiterable.
* @since 4.0.5 The `$post_type` fallback now uses a real query ID, instead of `$GLOBALS['post']`;
* mitigating issues with singular-archives pages (blog, shop, etc.).
*
* @param string $post_type Optional. The post type to check.
* @return bool True if disabled, false otherwise.
*/
public function is_post_type_disabled( $post_type = '' ) {
$post_type = $post_type ?: $this->get_current_post_type();
/**
* @since 3.1.2
* @param bool $disabled
* @param string $post_type
*/
return \apply_filters_ref_array(
'the_seo_framework_post_type_disabled',
[
isset( $this->get_option( 'disabled_post_types' )[ $post_type ] ),
$post_type,
]
);
}
/**
* Checks if the taxonomy isn't disabled, and that at least one taxonomy
* objects post type supports The SEO Framework.
*
* @since 3.1.0
* @since 4.0.0 1. Now returns true if at least one post type for the taxonomy is supported.
* 2. Now uses `is_post_type_supported()` instead of `is_post_type_disabled()`.
* @since 4.1.0 1. Now also checks for the option `disabled_taxonomies`.
* 2. Now applies filters `the_seo_framework_taxonomy_disabled`.
*
* @param string $taxonomy The taxonomy name.
* @return bool True if at least one post type in taxonomy is supported.
*/
public function is_taxonomy_disabled( $taxonomy = '' ) {
$disabled = false;
// First, test pertaining option directly.
if ( $taxonomy && isset( $this->get_option( 'disabled_taxonomies' )[ $taxonomy ] ) ) {
$disabled = true;
} else {
// Then, test some() post types.
// Populate $disabled within loop, for the taxonomy might not have post types at all.
foreach ( $this->get_post_types_from_taxonomy( $taxonomy ) as $type ) {
if ( $this->is_post_type_supported( $type ) ) {
$disabled = false;
break;
}
$disabled = true;
}
}
/**
* @since 4.1.0
* @param bool $disabled
* @param string $taxonomy
*/
return \apply_filters_ref_array(
'the_seo_framework_taxonomy_disabled',
[
$disabled,
$taxonomy,
]
);
}
/**
* Determines whether a page or blog is on front.
*
* @since 2.6.0
* @since 3.1.0 Removed caching.
*
* @return bool
*/
public function has_page_on_front() {
return 'page' === \get_option( 'show_on_front' );
}
/**
* Detects if we're on a Gutenberg page.
*
* @since 3.1.0
* @since 3.2.0 1. Now detects the WP 5.0 block editor.
* 2. Method is now public.
*
* @return bool
*/
public function is_gutenberg_page() {
if ( \function_exists( '\\use_block_editor_for_post' ) )
return ! empty( $GLOBALS['post'] ) && \use_block_editor_for_post( $GLOBALS['post'] );
if ( \function_exists( '\\is_gutenberg_page' ) )
return \is_gutenberg_page();
return false;
}
/**
* Determines whether we can output sitemap or not based on options and blog status.
*
* @since 2.6.0
* @since 2.9.2 No longer checks for plain and ugly permalinks.
* @since 4.0.0 Removed caching.
*
* @return bool
*/
public function can_run_sitemap() {
return $this->get_option( 'sitemaps_output' ) && ! $this->current_blog_is_spam_or_deleted();
}
/**
* Returns the robots.txt location URL.
* Only allows root domains.
*
* @since 2.9.2
* @since 4.0.2 Now uses the preferred URL scheme.
* @global \WP_Rewrite $wp_rewrite
*
* @return string URL location of robots.txt. Unescaped.
*/
public function get_robots_txt_url() {
if ( $GLOBALS['wp_rewrite']->using_permalinks() && ! $this->is_subdirectory_installation() ) {
$home = \trailingslashit( $this->set_preferred_url_scheme( $this->get_home_host() ) );
$path = "{$home}robots.txt";
} elseif ( $this->has_robots_txt() ) {
$home = \trailingslashit( $this->set_preferred_url_scheme( \get_option( 'home' ) ) );
$path = "{$home}robots.txt";
} else {
$path = '';
}
return $path;
}
/**
* Determines if the current installation is on a subdirectory.
* Memoizes the return value.
*
* @since 2.9.0
*
* @return bool
*/
public function is_subdirectory_installation() {
return memo() ?? memo(
(bool) \strlen(
ltrim(
parse_url(
\get_option( 'home' ),
PHP_URL_PATH
) ?? '',
' \\/'
)
)
);
}
/**
* Determines whether the text has recognizable transformative syntax.
*
* It tests Yoast SEO before Rank Math because that one is more popular, thus more
* likely to yield a result.
*
* @todo test all [ 'extension', 'yoast', 'aioseo', 'rankmath', 'seopress' ]
* @since 4.2.7
* @since 4.2.8 Added SEOPress support.
*
* @param string $text The text to evaluate
* @return bool
*/
public function has_unprocessed_syntax( $text ) {
foreach ( [ 'yoast', 'rankmath', 'seopress' ] as $type )
if ( $this->{"has_{$type}_syntax"}( $text ) ) return true;
return false;
}
/**
* Determines if the input text has transformative Yoast SEO syntax.
*
* TODO rename to yoast_seo?
*
* @since 4.0.5
* @since 4.2.7 1. Added wildcard `ct_`, and `cf_` detection.
* 2. Added detection for various other types
* 2. Removed wildcard `cs_` detection.
* @see $this->has_unprocessed_syntax(), the caller.
* @link <https://yoast.com/help/list-available-snippet-variables-yoast-seo/> (This list containts false information)
* @link <https://theseoframework.com/extensions/transport/#faq/what-data-is-transformed>
*
* @param string $text The text to evaluate.
* @return bool
*/
public function has_yoast_syntax( $text ) {
// %%id%% is the shortest valid tag... ish. Let's stop at 6.
if ( \strlen( $text ) < 6 || false === strpos( $text, '%%' ) )
return false;
$tags = umemo( __METHOD__ . '/tags' );
if ( ! $tags ) {
$tags = umemo(
__METHOD__ . '/tags',
[
'simple' => implode(
'|',
[
// These are Preserved by Transport. Test first, for they are more likely in text.
'focuskw',
'page',
'pagenumber',
'pagetotal',
'primary_category',
'searchphrase',
'term404',
'wc_brand',
'wc_price',
'wc_shortdesc',
'wc_sku',
// These are transformed by Transport
'archive_title',
'author_first_name',
'author_last_name',
'caption',
'category',
'category_description',
'category_title',
'currentdate',
'currentday',
'currentmonth',
'currentyear',
'date',
'excerpt',
'excerpt_only',
'id',
'modified',
'name',
'parent_title',
'permalink',
'post_content',
'post_year',
'post_month',
'post_day',
'pt_plural',
'pt_single',
'sep',
'sitedesc',
'sitename',
'tag',
'tag_description',
'term_description',
'term_title',
'title',
'user_description',
'userid',
]
),
'wildcard_end' => implode( '|', [ 'ct_', 'cf_' ] ),
]
);
}
return preg_match( "/%%(?:{$tags['simple']})%%/", $text )
|| preg_match( "/%%(?:{$tags['wildcard_end']})[^%]+?%%/", $text );
}
/**
* Determines if the input text has transformative Rank Math syntax.
*
* @since 4.2.7
* @since 4.2.8 Actualized the variable list.
* @link <https://theseoframework.com/extensions/transport/#faq/what-data-is-transformed>
* Rank Math has no documentation on this list, but we sampled their code.
* @see $this->has_unprocessed_syntax(), the caller.
*
* @param string $text The text to evaluate.
* @return bool
*/
public function has_rankmath_syntax( $text ) {
// %id% is the shortest valid tag... ish. Let's stop at 4.
if ( \strlen( $text ) < 4 || false === strpos( $text, '%' ) )
return false;
$tags = umemo( __METHOD__ . '/tags' );
if ( ! $tags ) {
$tags = umemo(
__METHOD__ . '/tags',
[
'simple' => implode(
'|',
[
// These are Preserved by Transport. Test first, for they are more likely in text.
'currenttime', // Rank Math has two currenttime, this one is simple.
'filename',
'focuskw',
'group_desc',
'group_name',
'keywords',
'org_name',
'org_logo',
'org_url',
'page',
'pagenumber',
'pagetotal',
'post_thumbnail',
'primary_category',
'primary_taxonomy_terms',
'url',
'wc_brand',
'wc_price',
'wc_shortdesc',
'wc_sku',
'currenttime', // Rank Math has two currenttime, this one is simple.
// These are transformed by Transport
'category',
'categories',
'currentdate',
'currentday',
'currentmonth',
'currentyear',
'date',
'excerpt',
'excerpt_only',
'id',
'modified',
'name',
'parent_title',
'post_author',
'pt_plural',
'pt_single',
'seo_title',
'seo_description',
'sep',
'sitedesc',
'sitename',
'tag',
'tags',
'term',
'term_description',
'title',
'user_description',
'userid',
]
),
// Check out for ref RankMath\Replace_Variables\Replacer::set_up_replacements();
'wildcard_end' => implode(
'|',
[
'categories',
'count',
'currenttime',
'customfield',
'customterm',
'customterm_desc',
'date',
'modified',
'tags',
]
),
]
);
}
return preg_match( "/%(?:{$tags['simple']})%/", $text )
|| preg_match( "/%(?:{$tags['wildcard_end']})\([^\)]+?\)%/", $text );
}
/**
* Determines if the input text has transformative SEOPress syntax.
*
* @since 4.2.8
* @link <https://theseoframework.com/extensions/transport/#faq/what-data-is-transformed>
* SEOPress has no documentation on this list, but we sampled their code.
* @see $this->has_unprocessed_syntax(), the caller.
*
* @param string $text The text to evaluate.
* @return bool
*/
public function has_seopress_syntax( $text ) {
// %%sep%% is the shortest valid tag... ish. Let's stop at 7.
if ( \strlen( $text ) < 7 || false === strpos( $text, '%%' ) )
return false;
$tags = umemo( __METHOD__ . '/tags' );
if ( ! $tags ) {
$tags = umemo(
__METHOD__ . '/tags',
[
'simple' => implode(
'|',
[
// These are Preserved by Transport. Test first, for they are more likely in text.
'author_website',
'current_pagination',
'currenttime',
'post_thumbnail_url',
'post_url',
'target_keyword',
'wc_single_price',
'wc_single_price_exc_tax',
'wc_sku',
// These are transformed by Transport
'_category_description',
'_category_title',
'archive_title',
'author_bio',
'author_first_name',
'author_last_name',
'author_nickname',
'currentday',
'currentmonth',
'currentmonth_num',
'currentmonth_short',
'currentyear',
'date',
'excerpt',
'post_author',
'post_category',
'post_content',
'post_date',
'post_excerpt',
'post_modified_date',
'post_tag',
'post_title',
'sep',
'sitedesc',
'sitename',
'sitetitle',
'tag_description',
'tag_title',
'tagline',
'term_description',
'term_title',
'title',
'wc_single_cat',
'wc_single_short_desc',
'wc_single_tag',
]
),
// Check out for ref somewhere in SEOPress, seopress_get_dyn_variables() is one I guess.
'wildcard_end' => implode(
'|',
[
'_cf_',
'_ct_',
'_ucf_',
]
),
]
);
}
return preg_match( "/%%(?:{$tags['simple']})%%/", $text )
|| preg_match( "/%%(?:{$tags['wildcard_end']})[^%]+?%%/", $text );
}
}