관리-도구
편집 파일: PostSettings.php
<?php namespace AIOSEO\Plugin\Common\Admin; // Exit if accessed directly. if ( ! defined( 'ABSPATH' ) ) { exit; } use AIOSEO\Plugin\Common\Models; /** * Abstract class that Pro and Lite both extend. * * @since 4.0.0 */ class PostSettings { /** * The integrations instance. * * @since 4.4.3 * * @var array[object] */ public $integrations; /** * Initialize the admin. * * @since 4.0.0 * * @return void */ public function __construct() { if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } // Clear the Post Type Overview cache. add_action( 'save_post', [ $this, 'clearPostTypeOverviewCache' ], 100 ); add_action( 'delete_post', [ $this, 'clearPostTypeOverviewCache' ], 100 ); add_action( 'wp_trash_post', [ $this, 'clearPostTypeOverviewCache' ], 100 ); if ( wp_doing_ajax() || wp_doing_cron() || ! is_admin() ) { return; } // Load Vue APP. add_action( 'admin_enqueue_scripts', [ $this, 'enqueuePostSettingsAssets' ] ); // Add metabox. add_action( 'add_meta_boxes', [ $this, 'addPostSettingsMetabox' ] ); // Add metabox to terms on init hook. add_action( 'init', [ $this, 'init' ], 1000 ); // Save metabox. add_action( 'save_post', [ $this, 'saveSettingsMetabox' ] ); add_action( 'edit_attachment', [ $this, 'saveSettingsMetabox' ] ); add_action( 'add_attachment', [ $this, 'saveSettingsMetabox' ] ); // Filter the sql clauses to show posts filtered by our params. add_filter( 'posts_clauses', [ $this, 'changeClausesToFilterPosts' ], 10, 2 ); } /** * Enqueues the JS/CSS for the on page/posts settings. * * @since 4.0.0 * * @return void */ public function enqueuePostSettingsAssets() { if ( aioseo()->helpers->isScreenBase( 'event-espresso' ) || aioseo()->helpers->isScreenBase( 'post' ) || aioseo()->helpers->isScreenBase( 'term' ) || aioseo()->helpers->isScreenBase( 'edit-tags' ) || aioseo()->helpers->isScreenBase( 'site-editor' ) ) { $page = null; if ( aioseo()->helpers->isScreenBase( 'event-espresso' ) || aioseo()->helpers->isScreenBase( 'post' ) ) { $page = 'post'; } aioseo()->core->assets->load( 'src/vue/standalone/post-settings/main.js', [], aioseo()->helpers->getVueData( $page ) ); aioseo()->core->assets->load( 'src/vue/standalone/link-format/main.js', [], aioseo()->helpers->getVueData( $page ) ); } $screen = aioseo()->helpers->getCurrentScreen(); if ( empty( $screen->id ) ) { return; } if ( 'attachment' === $screen->id ) { wp_enqueue_media(); } } /** * Check whether or not we can add the metabox. * * @since 4.1.7 * * @param string $postType The post type to check. * @return boolean Whether or not can add the Metabox. */ public function canAddPostSettingsMetabox( $postType ) { $dynamicOptions = aioseo()->dynamicOptions->noConflict(); $pageAnalysisSettingsCapability = aioseo()->access->hasCapability( 'aioseo_page_analysis' ); $generalSettingsCapability = aioseo()->access->hasCapability( 'aioseo_page_general_settings' ); $socialSettingsCapability = aioseo()->access->hasCapability( 'aioseo_page_social_settings' ); $schemaSettingsCapability = aioseo()->access->hasCapability( 'aioseo_page_schema_settings' ); $linkAssistantCapability = aioseo()->access->hasCapability( 'aioseo_page_link_assistant_settings' ); $redirectsCapability = aioseo()->access->hasCapability( 'aioseo_page_redirects_manage' ); $advancedSettingsCapability = aioseo()->access->hasCapability( 'aioseo_page_advanced_settings' ); $seoRevisionsSettingsCapability = aioseo()->access->hasCapability( 'aioseo_page_seo_revisions_settings' ); if ( $dynamicOptions->searchAppearance->postTypes->has( $postType ) && $dynamicOptions->searchAppearance->postTypes->$postType->advanced->showMetaBox && ! ( empty( $pageAnalysisSettingsCapability ) && empty( $generalSettingsCapability ) && empty( $socialSettingsCapability ) && empty( $schemaSettingsCapability ) && empty( $linkAssistantCapability ) && empty( $redirectsCapability ) && empty( $advancedSettingsCapability ) && empty( $seoRevisionsSettingsCapability ) ) ) { return true; } return false; } /** * Adds a meta box to page/posts screens. * * @since 4.0.0 * * @return void */ public function addPostSettingsMetabox() { $screen = aioseo()->helpers->getCurrentScreen(); if ( empty( $screen->post_type ) ) { return; } $postType = $screen->post_type; if ( $this->canAddPostSettingsMetabox( $postType ) ) { // Translators: 1 - The plugin short name ("AIOSEO"). $aioseoMetaboxTitle = sprintf( esc_html__( '%1$s Settings', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME ); add_meta_box( 'aioseo-settings', $aioseoMetaboxTitle, [ $this, 'postSettingsMetabox' ], [ $postType ], 'normal', apply_filters( 'aioseo_post_metabox_priority', 'high' ) ); } } /** * Render the on page/posts settings metabox with Vue App wrapper. * * @since 4.0.0 * * @return void */ public function postSettingsMetabox() { $this->postSettingsHiddenField(); ?> <div id="aioseo-post-settings-metabox"> <?php aioseo()->templates->getTemplate( 'parts/loader.php' ); ?> </div> <?php } /** * Adds the hidden field where all the metabox data goes. * * @since 4.0.17 * * @return void */ public function postSettingsHiddenField() { static $fieldExists = false; if ( $fieldExists ) { return; } $fieldExists = true; ?> <div id="aioseo-post-settings-field"> <input type="hidden" name="aioseo-post-settings" id="aioseo-post-settings" value=""/> <?php wp_nonce_field( 'aioseoPostSettingsNonce', 'PostSettingsNonce' ); ?> </div> <?php } /** * Handles metabox saving. * * @since 4.0.3 * * @param int $postId Post ID. * @return void */ public function saveSettingsMetabox( $postId ) { if ( ! aioseo()->helpers->isValidPost( $postId, [ 'all' ] ) ) { return; } // Security check. if ( ! isset( $_POST['PostSettingsNonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['PostSettingsNonce'] ) ), 'aioseoPostSettingsNonce' ) ) { return; } // If we don't have our post settings input, we can safely skip. if ( ! isset( $_POST['aioseo-post-settings'] ) ) { return; } // Check user permissions. if ( ! current_user_can( 'edit_post', $postId ) ) { return; } $currentPost = json_decode( sanitize_text_field( wp_unslash( ( $_POST['aioseo-post-settings'] ) ) ), true ); // If there is no data, there likely was an error, e.g. if the hidden field wasn't populated on load and the user saved the post without making changes in the metabox. // In that case we should return to prevent a complete reset of the data. if ( empty( $currentPost ) ) { return; } Models\Post::savePost( $postId, $currentPost ); } /** * Clear the Post Type Overview cache from our cache table. * * @since 4.2.0 * * @param int $postId The Post ID being updated/deleted. * @return void */ public function clearPostTypeOverviewCache( $postId ) { $postType = get_post_type( $postId ); if ( empty( $postType ) ) { return; } aioseo()->core->cache->delete( $postType . '_overview_data' ); } /** * Get a list of post types with an overview showing how many posts are good, okay and so on. * * @since 4.2.0 * * @return array The list of post types with the overview. */ public function getPostTypesOverview() { $overviewData = []; $eligiblePostTypes = aioseo()->helpers->getTruSeoEligiblePostTypes(); foreach ( aioseo()->helpers->getPublicPostTypes( true ) as $postType ) { if ( ! in_array( $postType, $eligiblePostTypes, true ) ) { continue; } $overviewData[ $postType ] = $this->getPostTypeOverview( $postType ); } return $overviewData; } /** * Get how many posts are good, okay, needs improvement or are missing the focus keyphrase for the given post type. * * @since 4.2.0 * * @param string $postType The post type name. * @return array The overview data for the given post type. */ public function getPostTypeOverview( $postType ) { $overviewData = aioseo()->core->cache->get( $postType . '_overview_data' ); if ( null !== $overviewData ) { return $overviewData; } $eligiblePostTypes = aioseo()->helpers->getTruSeoEligiblePostTypes(); if ( ! in_array( $postType, $eligiblePostTypes, true ) ) { return [ 'total' => 0, 'withoutFocusKeyphrase' => 0, 'needsImprovement' => 0, 'okay' => 0, 'good' => 0 ]; } $specialPageIds = aioseo()->helpers->getSpecialPageIds(); $implodedPageIdPlaceholders = array_fill( 0, count( $specialPageIds ), '%d' ); $implodedPageIdPlaceholders = implode( ', ', $implodedPageIdPlaceholders ); global $wpdb; $overviewData = $wpdb->get_row( // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( "SELECT COUNT(*) as total, COALESCE( SUM(CASE WHEN ap.keyphrases = '' OR ap.keyphrases IS NULL OR ap.keyphrases LIKE %s THEN 1 ELSE 0 END), 0) as withoutFocusKeyphrase, COALESCE( SUM(CASE WHEN ap.seo_score < 50 AND NOT (ap.keyphrases = '' OR ap.keyphrases IS NULL OR ap.keyphrases LIKE %s) THEN 1 ELSE 0 END), 0) as needsImprovement, COALESCE( SUM(CASE WHEN ap.seo_score BETWEEN 50 AND 79 AND NOT (ap.keyphrases = '' OR ap.keyphrases IS NULL OR ap.keyphrases LIKE %s) THEN 1 ELSE 0 END), 0) as okay, COALESCE( SUM(CASE WHEN ap.seo_score >= 80 AND NOT (ap.keyphrases = '' OR ap.keyphrases IS NULL OR ap.keyphrases LIKE %s) THEN 1 ELSE 0 END), 0) as good FROM {$wpdb->posts} as p LEFT JOIN {$wpdb->prefix}aioseo_posts as ap ON ap.post_id = p.ID WHERE p.post_status = 'publish' AND p.post_type = %s AND p.ID NOT IN ( $implodedPageIdPlaceholders )", // phpcs:enable '{"focus":{"keyphrase":""%', '{"focus":{"keyphrase":""%', '{"focus":{"keyphrase":""%', '{"focus":{"keyphrase":""%', $postType, ...array_values( $specialPageIds ) ), ARRAY_A ); // Ensure sure all the values are integers. foreach ( $overviewData as $key => $value ) { $overviewData[ $key ] = (int) $value; } // Give me the raw SQL of the query. aioseo()->core->cache->update( $postType . '_overview_data', $overviewData, HOUR_IN_SECONDS ); return $overviewData; } /** * Change the JOIN and WHERE clause to filter just the posts we need to show depending on the query string. * * @since 4.2.0 * * @param array $clauses Associative array of the clauses for the query. * @param \WP_Query $query The WP_Query instance (passed by reference). * @return array The clauses array updated. */ public function changeClausesToFilterPosts( $clauses, $query = null ) { if ( ! is_admin() || ! $query->is_main_query() ) { return $clauses; } $filter = filter_input( INPUT_GET, 'aioseo-filter' ); if ( empty( $filter ) ) { return $clauses; } $whereClause = ''; $noKeyphrasesClause = "(aioseo_p.keyphrases = '' OR aioseo_p.keyphrases IS NULL OR aioseo_p.keyphrases LIKE '{\"focus\":{\"keyphrase\":\"\"%')"; switch ( $filter ) { case 'withoutFocusKeyphrase': $whereClause = " AND $noKeyphrasesClause "; break; case 'needsImprovement': $whereClause = " AND aioseo_p.seo_score < 50 AND NOT $noKeyphrasesClause "; break; case 'okay': $whereClause = " AND aioseo_p.seo_score BETWEEN 50 AND 80 AND NOT $noKeyphrasesClause "; break; case 'good': $whereClause = " AND aioseo_p.seo_score > 80 AND NOT $noKeyphrasesClause "; break; } $prefix = aioseo()->core->db->prefix; $postsTable = aioseo()->core->db->db->posts; $clauses['join'] .= " LEFT JOIN {$prefix}aioseo_posts AS aioseo_p ON ({$postsTable}.ID = aioseo_p.post_id) "; $clauses['where'] .= $whereClause; add_action( 'wp', [ $this, 'filterPostsAfterChangingClauses' ] ); return $clauses; } /** * Filter the posts array to remove the ones that are not eligible for page analysis. * Hooked into `wp` action hook. * * @since 4.7.1 * * @return void */ public function filterPostsAfterChangingClauses() { remove_action( 'wp', [ $this, 'filterPostsAfterChangingClauses' ] ); // phpcs:disable Squiz.NamingConventions.ValidVariableName global $wp_query; if ( ! empty( $wp_query->posts ) && is_array( $wp_query->posts ) ) { $wp_query->posts = array_filter( $wp_query->posts, function ( $post ) { return aioseo()->helpers->isTruSeoEligible( $post->ID ); } ); // Update `post_count` for pagination. if ( isset( $wp_query->post_count ) ) { $wp_query->post_count = count( $wp_query->posts ); } } // phpcs:enable Squiz.NamingConventions.ValidVariableName } }