license->hasCoreFeature( 'search-statistics' ) ) { return; } add_action( 'admin_init', [ $this, 'init' ] ); add_action( 'update_option_permalink_structure', [ $this, 'reset' ], 10 ); add_action( 'updated_option', [ $this, 'syncHomeUrl' ], 10, 3 ); add_action( $this->action, [ $this, 'scan' ] ); } /** * Initialize the objects. * * @since 4.3.0 * * @return void */ public function init() { if ( ! aioseo()->searchStatistics->api->auth->isConnected() ) { return; } $this->scheduleScan(); add_action( 'save_post', [ $this, 'updatePost' ], 100 ); add_action( 'delete_post', [ $this, 'updatePost' ], 100 ); add_action( 'wp_trash_post', [ $this, 'updatePost' ], 100 ); add_action( 'edit_term', [ $this, 'updateTerm' ], 100 ); add_action( 'delete_term', [ $this, 'deleteTerm' ], 100 ); add_action( 'created_term', [ $this, 'updateTerm' ], 100 ); } /** * Schedules the objects scan as a recurring action. * * @since 4.3.0 * @version 4.9.4.2 Switch to recurring action with cache-based idle state. * * @return void */ private function scheduleScan() { // If we're in idle mode (no objects to scan), unschedule and don't reschedule yet. if ( aioseo()->core->cache->get( 'as_objects_scan_idle' ) ) { aioseo()->actionScheduler->unschedule( $this->action ); return; } if ( aioseo()->actionScheduler->isScheduled( $this->action ) ) { return; } aioseo()->actionScheduler->scheduleRecurrent( $this->action, 10, MINUTE_IN_SECONDS ); } /** * Drops all object rows if the permalink structure changes. * * @since 4.3.0 * * @return void */ public function reset() { aioseo()->core->db->truncate( 'aioseo_search_statistics_objects' )->run(); as_unschedule_all_actions( $this->action ); // Clear the idle cache so that the scan restarts immediately. aioseo()->core->cache->delete( 'as_objects_scan_idle' ); $this->scheduleScan(); } /** * Syncs the homeurl when user updates the page on front. * * @since 4.3.4 * * @param string $option The option name. * @param mixed $oldValue The old option value. * @param mixed $value The new option value. * @return void */ public function syncHomeUrl( $option, $oldValue = '', $value = '' ) { if ( 'page_on_front' !== $option ) { return; } // Get the object with '/' path, which is the homepage. $wpObject = Models\WpObject::getObjectBy( 'path', '/' ); // If the new value is greater than 0, then it's a page id and we need to update the object. if ( 0 < (int) $value ) { // Sets this object to the new homepage id, so it can be updated later by post_id. Models\WpObject::update( [ 'id' => $wpObject->id ?? null, 'object_id' => $value, 'object_type' => 'post', 'object_path' => '/' ] ); // Update the post with all the needed data. $this->updatePost( $value ); return; } // Otherwise, just detach the object from any post because it's not a page anymore. Models\WpObject::update( [ 'id' => $wpObject->id ?? null, 'object_id' => null, 'object_type' => null, 'object_subtype' => null, 'object_path' => '/', ] ); } /** * The main scan function that triggers all the sub scans. * * @since 4.5.0 * @version 4.9.4.2 Use recurring action with runtime lock and idle state. * * @return void */ public function scan() { // Runtime lock: Prevent concurrent execution of this action. $lockKey = 'as_objects_scan_running'; if ( aioseo()->core->cache->get( $lockKey ) ) { return; } // Set lock with a safety timeout in case the action fails mid-execution. aioseo()->core->cache->update( $lockKey, true, 2 * MINUTE_IN_SECONDS ); $postsObjects = $this->scanForPosts(); $termsObjects = $this->scanForTerms(); $objects = array_merge( $postsObjects, $termsObjects ); if ( empty( $objects ) ) { // No more objects to scan - set idle cache. The schedule method on the next init will unschedule. aioseo()->core->cache->update( 'as_objects_scan_idle', true, DAY_IN_SECONDS ); aioseo()->core->cache->delete( $lockKey ); return; } Models\WpObject::bulkInsert( $objects ); aioseo()->core->cache->delete( $lockKey ); } /** * Checks if posts need to be updated/inserted. * * @since 4.3.0 * * @return array The list of posts to insert. */ private function scanForPosts() { $missingPosts = aioseo()->core->db->start( 'posts as p' ) ->select( 'DISTINCT p.ID, p.post_type, asso.object_id' ) ->leftJoin( 'aioseo_search_statistics_objects as asso', 'p.ID = asso.object_id' ) ->where( 'p.post_status', 'publish' ) ->whereIn( 'p.post_type', aioseo()->helpers->getPublicPostTypes( true ) ) ->whereRaw( '( asso.object_id IS NULL OR asso.updated < p.post_modified_gmt )' ) ->limit( 50 ) ->run() ->result(); if ( empty( $missingPosts ) ) { return []; } $objectsToInsert = []; foreach ( $missingPosts as $post ) { $path = aioseo()->searchStatistics->helpers->getPageSlug( get_permalink( $post->ID ) ); $wpObject = Models\WpObject::getObjectBy( 'path', $path ); $object = [ 'id' => $wpObject->id ?? null, 'object_id' => $post->ID, 'object_type' => 'post', 'object_subtype' => $post->post_type, 'object_path' => $path, ]; if ( ! empty( $object['id'] ) ) { Models\WpObject::update( $object ); } else { $objectsToInsert[] = $object; } } return $objectsToInsert; } /** * Checks if terms need to be updated/inserted. * * @since 4.5.0 * * @return array The list of terms to insert. */ private function scanForTerms() { $missingTerms = aioseo()->core->db->start( 'terms as t' ) ->select( 'DISTINCT t.term_id, tt.taxonomy, asso.object_id' ) ->join( 'term_taxonomy as tt', 't.term_id = tt.term_id' ) ->leftJoin( 'aioseo_search_statistics_objects as asso', 't.term_id = asso.object_id' ) ->leftJoin( 'aioseo_terms as at', 't.term_id = at.term_id' ) ->whereIn( 'tt.taxonomy', aioseo()->helpers->getPublicTaxonomies( true ) ) ->whereRaw( '( asso.object_id IS NULL OR asso.updated < at.updated )' ) ->limit( 50 ) ->run() ->result(); if ( empty( $missingTerms ) ) { return []; } $objectsToInsert = []; foreach ( $missingTerms as $term ) { $path = aioseo()->searchStatistics->helpers->getPageSlug( get_term_link( (int) $term->term_id, $term->taxonomy ) ); $wpObject = Models\WpObject::getObjectBy( 'path', $path ); $object = [ 'id' => $wpObject->id ?? null, 'object_id' => $term->term_id, 'object_type' => 'term', 'object_subtype' => $term->taxonomy, 'object_path' => $path ]; if ( ! empty( $object['id'] ) ) { Models\WpObject::update( $object ); } else { $objectsToInsert[] = $object; } } return $objectsToInsert; } /** * Updates or deletes the data for the given post. * * @since 4.3.0 * * @param int $postId The post ID. * @return void */ public function updatePost( $postId ) { if ( wp_is_post_autosave( $postId ) || wp_is_post_revision( $postId ) ) { return; } if ( 'publish' !== get_post_status( $postId ) ) { aioseo()->core->db->delete( 'aioseo_search_statistics_objects' ) ->where( 'object_id', $postId ) ->where( 'object_type', 'post' ) ->run(); return; } $post = get_post( $postId ); if ( ! is_a( $post, 'WP_Post' ) || ! in_array( $post->post_type, aioseo()->searchStatistics->helpers->getIncludedPostTypes(), true ) ) { return; } $path = aioseo()->searchStatistics->helpers->getPageSlug( get_permalink( $postId ) ); $wpObject = Models\WpObject::getObjectBy( 'post_id', $postId ); $object = [ 'id' => $wpObject->id ?? null, 'object_id' => $postId, 'object_type' => 'post', 'object_subtype' => $post->post_type, 'object_path' => $path, ]; if ( ! empty( $object['id'] ) ) { Models\WpObject::update( $object ); } else { Models\WpObject::bulkInsert( [ $object ] ); } // Clear cache the for the posts. aioseo()->core->cache->clearPrefix( 'aioseo_search_statistics_post_' ); } /** * Updates the data for the given term. * * @since 4.5.0 * * @param int $termId The term ID. * @return void */ public function updateTerm( $termId ) { $term = get_term( $termId ); if ( ! is_a( $term, 'WP_Term' ) ) { return; } $path = aioseo()->searchStatistics->helpers->getPageSlug( get_term_link( (int) $termId, $term->taxonomy ) ); $wpObject = Models\WpObject::getObjectBy( 'term_id', $termId ); $object = [ 'id' => $wpObject->id ?? null, 'object_id' => $termId, 'object_type' => 'term', 'object_subtype' => $term->taxonomy, 'object_path' => $path ]; if ( ! empty( $object['id'] ) ) { Models\WpObject::update( $object ); } else { Models\WpObject::bulkInsert( [ $object ] ); } // Clear cache the for the terms. aioseo()->core->cache->clearPrefix( 'aioseo_search_statistics_term_' ); } /** * Deletes the data for the given term. * * @since 4.5.0 * * @param int $termId The term ID. * @return void */ public function deleteTerm( $termId ) { aioseo()->core->db->delete( 'aioseo_search_statistics_objects' ) ->where( 'object_id', $termId ) ->where( 'object_type', 'term' ) ->run(); // Clear cache the for the terms. aioseo()->core->cache->clearPrefix( 'aioseo_search_statistics_term_' ); } }