core->db->start( 'aioseo_search_statistics_objects' ) ->where( 'id', $data['id'] ) ->run() ->model( 'AIOSEO\\Plugin\\Pro\\Models\\SearchStatistics\\WpObject' ); if ( ! $wpObject->exists() ) { return; } try { $wpObject->set( self::sanitizeAll( array_merge( json_decode( wp_json_encode( $wpObject ), true ), $data ) ) ); $wpObject->save(); } catch ( \Exception $e ) { // Do nothing. It only exists because the set() method above throws an exception if it fails. } } /** * Gets the objects from the database. * * @since 4.8.2 * * @param array $args The arguments. * @return array The object rows. */ public static function getObjects( $args = [] ) { // phpcs:disable Generic.Files.LineLength.MaxExceeded $args = array_merge( [ 'filter' => 'all', 'searchTerm' => '', 'additionalFilters' => [], 'paths' => [], 'count' => true, ], $args ); $searchTerm = sanitize_text_field( $args['searchTerm'] ); $orderDir = ! empty( $args['orderDir'] ) ? $args['orderDir'] : 'DESC'; $limit = ! empty( $args['limit'] ) ? intval( $args['limit'] ) : aioseo()->settings->tablePagination['searchStatisticsIndexStatus']; $offset = ! empty( $args['offset'] ) ? intval( $args['offset'] ) : 0; $additionalFilters = ! empty( $args['additionalFilters'] ) ? $args['additionalFilters'] : []; $paths = ! empty( $args['paths'] ) && is_array( $args['paths'] ) ? array_filter( $args['paths'] ) : []; $postType = $additionalFilters['postType'] ?? ''; $status = $additionalFilters['status'] ?? ''; $robotsTxtState = $additionalFilters['robotsTxtState'] ?? ''; $pageFetchState = $additionalFilters['pageFetchState'] ?? ''; $crawledAs = $additionalFilters['crawledAs'] ?? ''; switch ( $args['orderBy'] ?? '' ) { case 'title': $orderBy = 'wp.post_title'; break; case 'lastCrawlTime': $orderBy = 'aio.last_crawl_time'; break; default: $orderBy = 'aio.created'; } /** * Fetching objects from our table is probably the best way since it's constantly being updated {@see Objects::scanForPosts()}, * and it already has some of the necessary data (e.g. the "inspection_result" column). */ $query = aioseo()->core->db->start( 'aioseo_search_statistics_objects as aio' ) ->select( 'aio.id, aio.object_id, aio.object_type, aio.object_subtype, aio.object_path, aio.inspection_result, aio.inspection_result_date, aio.verdict, aio.robots_txt_state, aio.indexing_state, aio.page_fetch_state, aio.coverage_state, aio.crawled_as, aio.last_crawl_time, wp.post_title, wp.post_type' ) ->join( 'posts as wp', 'aio.object_id = wp.ID', 'INNER' ) ->where( 'aio.object_type', 'post' ) ->whereIn( 'aio.object_subtype', aioseo()->helpers->getPublicPostTypes( true ) ) ->orderBy( "$orderBy $orderDir" ) ->limit( $limit, $offset ); $totalQuery = aioseo()->core->db->noConflict()->start( 'aioseo_search_statistics_objects as aio' ) ->join( 'posts as wp', 'aio.object_id = wp.ID AND aio.object_type = "post"', 'INNER' ) ->where( 'aio.object_type', 'post' ) ->whereIn( 'aio.object_subtype', aioseo()->helpers->getPublicPostTypes( true ) ); if ( $paths ) { $sanitizedPaths = array_map( 'sanitize_text_field', array_unique( $paths ) ); $query->whereIn( 'object_path_hash', array_map( 'sha1', $sanitizedPaths ) ); $totalQuery->whereIn( 'object_path_hash', array_map( 'sha1', $sanitizedPaths ) ); } if ( $searchTerm ) { $query->whereLike( 'wp.post_title', '%' . $searchTerm . '%', true ); $totalQuery->whereLike( 'wp.post_title', '%' . $searchTerm . '%', true ); } if ( $postType ) { $query->where( 'aio.object_subtype', $postType ); $totalQuery->where( 'aio.object_subtype', $postType ); } // {@see \AIOSEO\Plugin\Common\SearchStatistics\IndexStatus::getUiOptions()} for all possible status. if ( $status ) { if ( 'submitted' === $status ) { $query->where( 'aio.verdict', 'PASS' ); $totalQuery->where( 'aio.verdict', 'PASS' ); } if ( 'crawled' === $status || 'discovered' === $status ) { $query->whereRaw( 'LOWER(aio.coverage_state) LIKE \'%' . $status . '%\'' ); $totalQuery->whereRaw( 'LOWER(aio.coverage_state) LIKE \'%' . $status . '%\'' ); } if ( 'empty' === $status ) { $query->whereRaw( '( aio.coverage_state IS NULL OR aio.coverage_state = "" )' ); $totalQuery->whereRaw( '( aio.coverage_state IS NULL OR aio.coverage_state = "" )' ); } // This is supposed to cover all other possible statuses. if ( 'unknown|excluded|invalid|error' === $status ) { $query->whereRaw( "aio.coverage_state IS NOT NULL AND aio.coverage_state != '' AND aio.verdict != 'PASS' AND LOWER(aio.coverage_state) NOT LIKE '%crawled%' AND LOWER(aio.coverage_state) NOT LIKE '%discovered%'" ); $totalQuery->whereRaw( "aio.coverage_state IS NOT NULL AND aio.coverage_state != '' AND aio.verdict != 'PASS' AND LOWER(aio.coverage_state) NOT LIKE '%crawled%' AND LOWER(aio.coverage_state) NOT LIKE '%discovered%'" ); } } if ( $robotsTxtState ) { $query->where( 'aio.robots_txt_state', $robotsTxtState ); $totalQuery->where( 'aio.robots_txt_state', $robotsTxtState ); } if ( $pageFetchState ) { if ( false !== strpos( $pageFetchState, ',' ) ) { // It might be SOFT_404,BLOCKED_ROBOTS_TXT,NOT_FOUND... $pageFetchState = explode( ',', $pageFetchState ); $query->whereIn( 'aio.page_fetch_state', $pageFetchState ); $totalQuery->whereIn( 'aio.page_fetch_state', $pageFetchState ); } else { $query->where( 'aio.page_fetch_state', $pageFetchState ); $totalQuery->where( 'aio.page_fetch_state', $pageFetchState ); } } if ( $crawledAs ) { $query->where( 'aio.crawled_as', $crawledAs ); $totalQuery->where( 'aio.crawled_as', $crawledAs ); } $total = $args['count'] ? $totalQuery->count() : null; $rows = $query->run()->result(); return [ 'rows' => array_values( $rows ), 'totals' => ! is_null( $total ) ? [ 'total' => $total, 'pages' => $pages = ( 0 === $total ? 1 : ceil( $total / $limit ) ), 'page' => min( $pages, 0 === $offset ? 1 : ( $offset / $limit ) + 1 ) ] : [] ]; } /** * Gets a row by its path. * * @since 4.5.0 * * @param string $path The path. * @return WpObject|null The object or null if not found. */ public static function getObject( $path ) { return aioseo()->core->db->start( 'aioseo_search_statistics_objects' ) ->where( 'object_path_hash', sha1( sanitize_text_field( $path ) ) ) ->run() ->model( 'AIOSEO\\Plugin\\Pro\\Models\\SearchStatistics\\WpObject' ); } /** * Gets a row by the given field => value. * * @since 4.5.3 * * @param string $field The field to look for. * @param string $value The value to look for. * @return WpObject|null The object or null if not found. */ public static function getObjectBy( $field, $value ) { $wpObject = aioseo()->core->db->start( 'aioseo_search_statistics_objects' ); switch ( $field ) { case 'path': $wpObject = $wpObject->where( 'object_path_hash', sha1( sanitize_text_field( $value ) ) ); break; case 'post_id': $wpObject = $wpObject->where( 'object_id', $value ); $wpObject = $wpObject->where( 'object_type', 'post' ); break; case 'term_id': $wpObject = $wpObject->where( 'object_id', $value ); $wpObject = $wpObject->where( 'object_type', 'term' ); break; default: $wpObject = $wpObject->where( $field, $value ); } $wpObject = $wpObject->run()->model( 'AIOSEO\\Plugin\\Pro\\Models\\SearchStatistics\\WpObject' ); return $wpObject; } /** * Checks if the inspection result is valid. * * @since 4.6.1 * @version 4.8.4 Changed to use the static method. * * @param object $row The row to check. * @return bool Whether the inspection result is valid. */ public static function isInspectionResultValid( $row ) { // If there is no inspection result. if ( empty( $row->inspection_result ) ) { return false; } // If the inspection result is older than 30 days. if ( empty( $row->inspection_result_date ) || strtotime( $row->inspection_result_date ) < strtotime( '-1 month' ) ) { return false; } // If this object is not indexed and the inspection result is older than 1 day. if ( empty( $row->verdict ) || ( 'PASS' !== $row->verdict && strtotime( $row->inspection_result_date ) < strtotime( '-1 day' ) ) ) { return false; } return true; } /** * Bulk inserts a set of rows. * * @since 4.3.0 * * @param array $rows The rows to insert. * @return void */ public static function bulkInsert( $rows ) { $columns = [ 'object_id', 'object_type', 'object_subtype', 'object_path', 'object_path_hash', 'inspection_result', 'inspection_result_date', 'verdict', 'robots_txt_state', 'indexing_state', 'page_fetch_state', 'coverage_state', 'crawled_as', 'last_crawl_time', 'created', 'updated' ]; $currentDate = gmdate( 'Y-m-d H:i:s' ); $insertRows = []; foreach ( $rows as $row ) { $row = json_decode( wp_json_encode( $row ), true ); if ( empty( $row['object_path'] ) ) { continue; } $insertRows[] = array_merge( array_values( self::sanitizeAll( $row ) ), [ $currentDate, $currentDate ] ); } $onDuplicate = [ 'object_id' => 'VALUES(`object_id`)', 'object_type' => 'VALUES(`object_type`)', 'object_subtype' => 'VALUES(`object_subtype`)', 'inspection_result_date' => "'0000-00-00 00:00:00'", 'updated' => 'VALUES(`updated`)' ]; aioseo()->core->db->bulkInsert( 'aioseo_search_statistics_objects', $columns, $insertRows, [ 'onDuplicate' => $onDuplicate ] ); } /** * Parses an object row. * * @since {row} * * @param object $row The row to format. * @return array The formatted row. */ public static function parseObject( $row ) { $parsed = []; $parsed['id'] = intval( $row->id ); $parsed['objectTitle'] = aioseo()->helpers->decodeHtmlEntities( $row->post_title ); $parsed['objectId'] = intval( $row->object_id ); $parsed['editLink'] = get_edit_post_link( $parsed['objectId'], 'url' ); $parsed['permalink'] = get_permalink( $parsed['objectId'] ); $inspectionResult = json_decode( (string) $row->inspection_result, true ); $indexStatusResult = $inspectionResult['indexStatusResult'] ?? []; $postTypeLabels = aioseo()->helpers->getPostTypeLabels( $row->object_subtype ); $parsed['postTypeLabels'] = [ 'singular' => $postTypeLabels->singular_name ?? '' ]; $parsed['path'] = $row->object_path; $parsed['verdict'] = $row->verdict; $parsed['robotsTxtState'] = $row->robots_txt_state; $parsed['indexingState'] = $row->indexing_state; $parsed['pageFetchState'] = $row->page_fetch_state; $parsed['coverageState'] = $row->coverage_state; $parsed['crawledAs'] = $row->crawled_as; $parsed['lastCrawlTime'] = aioseo()->helpers->dateToWpFormat( $row->last_crawl_time ); $parsed['userCanonical'] = ! empty( $indexStatusResult['userCanonical'] ) ? $indexStatusResult['userCanonical'] : null; $parsed['googleCanonical'] = ! empty( $indexStatusResult['googleCanonical'] ) ? $indexStatusResult['googleCanonical'] : null; $parsed['sitemap'] = ! empty( $indexStatusResult['sitemap'] ) ? $indexStatusResult['sitemap'] : []; $parsed['referringUrls'] = ! empty( $indexStatusResult['referringUrls'] ) ? array_map( [ aioseo()->helpers, 'decodeUrl' ], $indexStatusResult['referringUrls'] ) : []; $parsed['richResultsResult'] = $inspectionResult['richResultsResult'] ?? null; $parsed['inspectionResultLink'] = $inspectionResult['inspectionResultLink'] ?? null; $parsed['isInspectionValid'] = self::isInspectionResultValid( $row ); if ( $parsed['permalink'] ) { $parsed['richResultsTestLink'] = add_query_arg( [ 'url' => $parsed['permalink'] ], 'https://search.google.com/test/rich-results' ); } return $parsed; } /** * Sanitize all the Model field values. * * @since 4.8.2 * * @param array $fields All the field values. * @return array The sanitized field values. */ public static function sanitizeAll( $fields ) { $sanitized = []; $inspectionResult = $fields['inspection_result'] ?? []; $isr = $inspectionResult['indexStatusResult'] ?? []; $sanitized['object_id'] = ! empty( $fields['object_id'] ) ? (int) $fields['object_id'] : null; $sanitized['object_type'] = ! empty( $fields['object_type'] ) ? sanitize_text_field( $fields['object_type'] ) : null; $sanitized['object_subtype'] = ! empty( $fields['object_subtype'] ) ? sanitize_text_field( $fields['object_subtype'] ) : null; $sanitized['object_path'] = ! empty( $fields['object_path'] ) ? sanitize_text_field( $fields['object_path'] ) : null; $sanitized['object_path_hash'] = ! empty( $sanitized['object_path'] ) ? sha1( $sanitized['object_path'] ) : null; $sanitized['inspection_result'] = ! empty( $inspectionResult ) ? $inspectionResult : null; $sanitized['inspection_result_date'] = ! empty( $fields['inspection_result_date'] ) ? $fields['inspection_result_date'] : null; $sanitized['verdict'] = ! empty( $isr['verdict'] ) ? sanitize_text_field( $isr['verdict'] ) : null; $sanitized['robots_txt_state'] = ! empty( $isr['robotsTxtState'] ) ? sanitize_text_field( $isr['robotsTxtState'] ) : null; $sanitized['indexing_state'] = ! empty( $isr['indexingState'] ) ? sanitize_text_field( $isr['indexingState'] ) : null; $sanitized['page_fetch_state'] = ! empty( $isr['pageFetchState'] ) ? sanitize_text_field( $isr['pageFetchState'] ) : null; $sanitized['coverage_state'] = ! empty( $isr['coverageState'] ) ? sanitize_text_field( $isr['coverageState'] ) : null; $sanitized['crawled_as'] = ! empty( $isr['crawledAs'] ) ? sanitize_text_field( $isr['crawledAs'] ) : null; $sanitized['last_crawl_time'] = ! empty( $isr['lastCrawlTime'] ) ? date( 'Y-m-d H:i:s', strtotime( $isr['lastCrawlTime'] ) ) : null; return $sanitized; } }