sensitiveOptions->hasValue( 'licenseKey' ) ) { return; } // After completion, the scan won't reschedule until someone is in the admin. That's fine for most cases since posts being created/updated happens in the admin. add_action( 'admin_init', [ $this, 'scheduleScan' ] ); add_action( $this->action, [ $this, 'scanPosts' ] ); } /** * Schedule the recurring action to scan posts. * * @since 4.8.6 * * @return void */ public function scheduleScan() { // If we're in idle mode (no posts to scan), unschedule and don't reschedule yet. if ( aioseo()->core->cache->get( 'as_seo_analysis_post_idle' ) ) { aioseo()->actionScheduler->unschedule( $this->action ); return; } $inteval = apply_filters( 'aioseo_seo_analyzer_scan_interval', MINUTE_IN_SECONDS ); aioseo()->actionScheduler->scheduleRecurrent( $this->action, 0, $inteval ); } /** * Get the number of items to analyze per page. * * @since 4.8.6 * * @return int */ protected function getPerRun() { return (int) apply_filters( 'aioseo_seo_analyzer_scan_items_per_run', $this->perRun ); } /** * Handles the analysis for X posts and store the results. * * @since 4.8.6 * @version 4.9.4.2 Add runtime lock to prevent concurrent execution. * * @return void */ public function scanPosts() { // Runtime lock: Prevent concurrent execution of this action. $lockKey = 'as_seo_analysis_post_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 ); $postsToAnalyze = $this->getEnqueuedPosts(); if ( empty( $postsToAnalyze ) ) { // No posts to analyze - set idle cache. The schedule method on the next init will unschedule. aioseo()->core->cache->update( 'as_seo_analysis_post_idle', true, HOUR_IN_SECONDS ); aioseo()->core->cache->delete( $lockKey ); return; } foreach ( $postsToAnalyze as $postId ) { Issue::deleteAll( $postId, 'post' ); $scan = aioseo()->seoAnalysis->analyzePost( $postId ); list( $basic, $advanced ) = array_values( $scan['results'] ); $data = array_merge( $basic, $advanced ); $postType = get_post_type( $postId ); foreach ( $data as $issue ) { $model = new Issue( [ 'object_id' => $postId, 'object_type' => 'post', 'object_subtype' => $postType, 'code' => $issue->code, 'metadata' => $issue->metadata ] ); $model->save(); } // Update the scan date. $post = AioseoPost::getPost( $postId ); $post->seo_analyzer_scan_date = gmdate( 'Y-m-d H:i:s' ); $post->save(); } aioseo()->core->cache->delete( $lockKey ); } /** * Get the posts that need to be analyzed. * * @since 4.8.6 * * @return array Posts object */ private function getEnqueuedPosts() { $settings = aioseo()->dynamicOptions->seoAnalysis->all(); $publicPostTypes = aioseo()->helpers->getScannablePostTypes(); $publicPostStatuses = aioseo()->helpers->getPublicPostStatuses( true ); $postTypes = $publicPostTypes; if ( 1 !== (int) $settings['postTypes']['all'] && ! empty( $settings['postTypes']['included'] ) ) { $postTypes = array_intersect( $postTypes, $settings['postTypes']['included'] ); } $orderByCasesPostTypes = []; foreach ( $postTypes as $value ) { $count = count( $orderByCasesPostTypes ) + 1; $orderByCasesPostTypes[] = "WHEN p.post_type = '$value' THEN $count"; } $postStatuses = aioseo()->helpers->getPublicPostStatuses( true ); if ( 1 !== (int) $settings['postStatuses']['all'] && ! empty( $settings['postStatuses']['included'] ) ) { $postStatuses = array_intersect( $postStatuses, $settings['postStatuses']['included'] ); } $orderByCasesPostStatuses = []; foreach ( $postStatuses as $value ) { $count = count( $orderByCasesPostStatuses ) + 1; $orderByCasesPostStatuses[] = "WHEN p.post_status = '$value' THEN $count"; } $select = [ 'p.ID' ]; $orderBy = []; if ( ! empty( $orderByCasesPostTypes ) ) { $select[] = '(CASE ' . implode( ' ', $orderByCasesPostTypes ) . ' END) as post_type_order'; $orderBy[] = 'post_type_order'; } if ( ! empty( $orderByCasesPostStatuses ) ) { $select[] = '(CASE ' . implode( ' ', $orderByCasesPostStatuses ) . ' END) as post_status_order'; $orderBy[] = 'post_status_order'; } $query = aioseo()->core->db->start( 'posts as p' ) ->select( implode( ', ', $select ) ) ->leftJoin( 'aioseo_posts as ap', 'ap.post_id = p.ID' ) ->whereIn( 'p.post_type', $publicPostTypes ) ->whereIn( 'p.post_status', $publicPostStatuses ) ->where( 'ap.seo_analyzer_scan_date', null ); if ( ! empty( $orderBy ) ) { $query->orderBy( implode( ', ', $orderBy ) . ' ASC' ); } $posts = $query ->limit( $this->getPerRun() ) ->run() ->result(); // We just need the IDs. foreach ( $posts as &$post ) { $post = $post->ID; } return $posts; } }