license->hasCoreFeature( 'search-statistics', 'seo-statistics' ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Feature not available.' ], 400 ); } $seoStatistics = aioseo()->searchStatistics->getSeoStatisticsData( $request->get_params() ); $success = false === $seoStatistics['data'] ? false : true; $statusCode = false === $seoStatistics['data'] ? 400 : 200; $responseData = [ 'success' => $success, 'data' => $seoStatistics['data'], 'range' => aioseo()->searchStatistics->stats->getDateRange() ]; // Include error message if available if ( ! empty( $seoStatistics['error'] ) ) { $responseData['message'] = $seoStatistics['error']; } return new \WP_REST_Response( $responseData, $statusCode ); } catch ( \Exception $e ) { return new \WP_REST_Response( [ 'success' => false, 'message' => esc_html( $e->getMessage() ) ], 400 ); } } /** * Returns pages by the given keyword. * * @since 4.3.0 * * @param \WP_REST_Request $request The REST Request * @return \WP_REST_Response The response. */ public static function getPagesByKeywords( $request ) { if ( ! aioseo()->license->hasCoreFeature( 'search-statistics', 'keyword-rankings-pages' ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Feature not available.' ], 400 ); } $body = $request->get_json_params(); if ( empty( $body['startDate'] ) || empty( $body['endDate'] ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Invalid date range.' ], 400 ); } $limit = intval( $body['limit'] ?? 0 ); $offset = intval( $body['offset'] ?? 0 ); $keywords = ! empty( $body['keywords'] ) ? $body['keywords'] : []; if ( empty( $keywords ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'No keywords were given.' ], 400 ); } sort( $keywords ); aioseo()->searchStatistics->stats->setDateRange( $body['startDate'], $body['endDate'] ); $cacheArgs = [ aioseo()->searchStatistics->api->auth->getAuthedSite(), $body['startDate'], $body['endDate'], implode( ',', $keywords ), $limit, $offset ]; $cacheHash = sha1( implode( '|', $cacheArgs ) ); $data = aioseo()->core->cache->get( "aioseo_search_statistics_page_keywords_{$cacheHash}" ); if ( null === $data ) { $args = [ 'start' => $body['startDate'], 'end' => $body['endDate'], 'keywords' => $keywords ]; $api = new Api\Request( 'google-search-console/statistics/keyword/pages/', $args, 'POST' ); $response = $api->request(); if ( is_wp_error( $response ) || ! empty( $response['error'] ) || empty( $response['data'] ) ) { aioseo()->core->cache->update( "aioseo_search_statistics_page_keywords_{$cacheHash}", false, 60 ); return new \WP_REST_Response( [ 'success' => false, 'data' => false, ], 400 ); } $data = $response['data']; aioseo()->core->cache->update( "aioseo_search_statistics_page_keywords_{$cacheHash}", $data, MONTH_IN_SECONDS ); } $success = false === $data ? false : true; if ( $success ) { $data = aioseo()->searchStatistics->stats->posts->addPostData( $data, 'keywords' ); if ( 1 < $limit ) { $data = array_map( function ( $pages ) use ( $offset, $limit ) { return [ 'paginated' => [ 'rows' => array_slice( $pages, $offset, $limit ), 'totals' => [ 'total' => count( $pages ), 'pages' => $pages = ( 0 === count( $pages ) ? 1 : ceil( count( $pages ) / $limit ) ), 'page' => min( $pages, 0 === $offset ? 1 : ( $offset / $limit ) + 1 ) ] ] ]; }, $data ); } } return new \WP_REST_Response( [ 'success' => $success, 'data' => $data ], false === $data ? 400 : 200 ); } /** * Get Keywords data. * * @since 4.3.0 * * @param \WP_REST_Request $request The REST Request * @return \WP_REST_Response The response. */ public static function getKeywords( $request ) { try { if ( ! aioseo()->license->hasCoreFeature( 'search-statistics', 'keyword-rankings' ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Feature not available.' ], 400 ); } $keywords = aioseo()->searchStatistics->getKeywordsData( $request->get_params() ); $success = false === $keywords['data'] ? false : true; $statusCode = false === $keywords['data'] ? 400 : 200; return new \WP_REST_Response( [ 'success' => $success, 'data' => $keywords['data'], 'range' => $keywords['range'] ], $statusCode ); } catch ( \Exception $e ) { return new \WP_REST_Response( [ 'success' => false, 'message' => esc_html( $e->getMessage() ) ], 400 ); } } /** * Get Content Rankings data. * * @since 4.3.6 * * @param \WP_REST_Request $request The REST Request * @return \WP_REST_Response The response. */ public static function getContentRankings( $request ) { if ( ! aioseo()->license->hasCoreFeature( 'search-statistics', 'content-rankings' ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Feature not available.' ], 400 ); } $content = aioseo()->searchStatistics->getContentRankingsData( $request->get_params() ); $success = false === $content['data'] ? false : true; $statusCode = false === $content['data'] ? 400 : 200; return new \WP_REST_Response( [ 'success' => $success, 'data' => $content['data'], 'range' => $content['range'] ], $statusCode ); } /** * Get Page Speed data. * * @since 4.3.0 * * @param \WP_REST_Request $request The REST Request * @return \WP_REST_Response The response. */ public static function getPageSpeed( $request ) { if ( ! aioseo()->license->hasCoreFeature( 'search-statistics', 'post-detail-page-speed' ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Feature not available.' ], 400 ); } $postId = $request->get_param( 'postId' ); if ( empty( $postId ) || ! is_numeric( $postId ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Invalid post id.' ], 400 ); } $url = get_permalink( $postId ); if ( empty( $url ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Invalid post id.' ], 400 ); } $force = boolval( $request->get_param( 'force' ) ); $data = aioseo()->searchStatistics->pageSpeed->getResults( $url, $force ); return new \WP_REST_Response( [ 'success' => true, 'data' => $data ], 200 ); } /** * Get SEO Analysis. * * @since 4.3.0 * * @param \WP_REST_Request $request The REST Request * @return \WP_REST_Response The response. */ public static function getSeoAnalysis( $request ) { $postId = $request->get_param( 'postId' ); if ( empty( $postId ) || ! is_numeric( $postId ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Invalid post id.' ], 400 ); } $url = get_permalink( $postId ); if ( empty( $url ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Invalid post id.' ], 400 ); } $cacheKey = 'search_statistics_seo_analysis_' . $postId; $cache = aioseo()->core->cache->get( $cacheKey ); if ( null !== $cache ) { return new \WP_REST_Response( $cache, 200 ); } $token = aioseo()->sensitiveOptions->get( 'siteAnalysisConnectToken' ); $apiUrl = defined( 'AIOSEO_ANALYZE_URL' ) ? AIOSEO_ANALYZE_URL : 'https://analyze.aioseo.com'; $response = aioseo()->helpers->wpRemotePost( $apiUrl . '/v1/analyze/', [ 'timeout' => 60, 'headers' => [ 'X-AIOSEO-Key' => $token, 'Content-Type' => 'application/json' ], 'body' => wp_json_encode( [ 'url' => $url ] ), ] ); $responseCode = wp_remote_retrieve_response_code( $response ); $responseBody = json_decode( wp_remote_retrieve_body( $response ) ); if ( 200 !== $responseCode || empty( $responseBody->success ) || ! empty( $responseBody->error ) ) { return new \WP_REST_Response( [ 'success' => false, 'response' => $responseBody ], 400 ); } aioseo()->core->cache->update( $cacheKey, $responseBody, WEEK_IN_SECONDS ); return new \WP_REST_Response( [ 'success' => true, 'data' => $responseBody ], 200 ); } /** * Get Inspection Result for the given paths. * * @since 4.5.0 * * @param \WP_REST_Request $request The REST Request * @return \WP_REST_Response The response. */ public static function getInspectionResult( $request ) { $paths = $request->get_param( 'paths' ); if ( empty( $paths ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Missing path list.' ], 400 ); } $data = aioseo()->searchStatistics->urlInspection->fetchInspectionResults( $paths, (bool) $request->get_param( 'force' ) ); return new \WP_REST_Response( [ 'success' => true, 'quotaExceeded' => $data['quotaExceeded'], 'data' => $data['pathsWithResult'] ], 200 ); } /** * Get Post details. * * @since 4.3.0 * * @param \WP_REST_Request $request The REST Request * @return \WP_REST_Response The response. */ public static function getPostDetail( $request ) { if ( ! aioseo()->license->hasCoreFeature( 'search-statistics', 'post-detail' ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Feature not available.' ], 400 ); } $startDate = $request->get_param( 'startDate' ); $endDate = $request->get_param( 'endDate' ); $postId = $request->get_param( 'postId' ); if ( empty( $startDate ) || empty( $endDate ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Invalid date range.' ], 400 ); } if ( empty( $postId ) || ! is_numeric( $postId ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Invalid post id.' ], 400 ); } aioseo()->searchStatistics->stats->setDateRange( $startDate, $endDate ); $wpPost = aioseo()->helpers->getPost( $postId ); $postTypeObject = get_post_type_object( get_post_type( $postId ) ); $post = Models\Post::getPost( $wpPost->ID ); $keyphrases = Models\Post::getKeyphrasesDefaults( $post->keyphrases ); $permalink = get_permalink( $postId ); $page = aioseo()->searchStatistics->helpers->getPageSlug( $permalink ); aioseo()->helpers->setWpQueryPost( $wpPost ); $seoMeta = [ 'title' => aioseo()->meta->title->getTitle(), 'description' => aioseo()->meta->description->getDescription(), 'schema' => implode( ', ', self::getSchemaGraphs( $wpPost ) ), 'canonicalUrl' => aioseo()->helpers->canonicalUrl(), 'robots' => aioseo()->meta->robots->meta(), 'additionalKeywords' => wp_list_pluck( $keyphrases->additional, 'keyphrase' ), ]; aioseo()->helpers->restoreWpQuery(); $wpObject = SearchStatisticsModels\WpObject::getObjectBy( 'path', $page ); return new \WP_REST_Response( [ 'success' => true, 'data' => [ 'postTitle' => aioseo()->helpers->decodeHtmlEntities( $wpPost->post_title ), 'permalink' => $permalink, 'page' => $page, 'editLink' => get_edit_post_link( $postId, '' ), 'postType' => aioseo()->helpers->getPostType( $postTypeObject ), 'seoScores' => [ 'headline' => (int) aioseo()->standalone->headlineAnalyzer->getResult( get_the_title( $postId ) )['score'], 'seoAnalysis' => false, // Will be loaded later. 'truSeo' => (int) $post->seo_score, ], 'inspectionResult' => $wpObject->inspection_result ?? null, 'focusKeyword' => $keyphrases->focus->keyphrase, 'seoMeta' => $seoMeta, 'suggestedChanges' => aioseo()->searchStatistics->helpers->getSuggestedChanges( $post ), 'linkAssistant' => (object) aioseo()->searchStatistics->helpers->getLinkAssistantData( $postId ), 'redirects' => (object) aioseo()->searchStatistics->helpers->getRedirectsData( $postId ), ] ], 200 ); } /** * Returns the statistics for the post detail page. * * @since 4.3.0 * * @param \WP_REST_Request $request The REST Request * @return \WP_REST_Response The response. */ public static function getPostDetailSeoStatistics( $request ) { try { if ( ! aioseo()->license->hasCoreFeature( 'search-statistics', 'post-detail-seo-statistics' ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Feature not available.' ], 400 ); } $postDetail = aioseo()->searchStatistics->getPostDetailSeoStatisticsData( $request->get_params() ); $success = false === $postDetail['data'] ? false : true; $statusCode = false === $postDetail['data'] ? 400 : 200; return new \WP_REST_Response( [ 'success' => $success, 'data' => $postDetail['data'], 'range' => aioseo()->searchStatistics->stats->getDateRange() ], $statusCode ); } catch ( \Exception $e ) { return new \WP_REST_Response( [ 'success' => false, 'message' => esc_html( $e->getMessage() ) ], 400 ); } } /** * Returns the keywords for the post detail page. * * @since 4.3.0 * * @param \WP_REST_Request $request The REST Request * @return \WP_REST_Response The response. */ public static function getPostDetailKeywords( $request ) { if ( ! aioseo()->license->hasCoreFeature( 'search-statistics', 'post-detail-keywords' ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Feature not available.' ], 400 ); } $params = $request->get_params(); $startDate = ! empty( $params['startDate'] ) ? $params['startDate'] : ''; $endDate = ! empty( $params['endDate'] ) ? $params['endDate'] : ''; $limit = ! empty( $params['limit'] ) ? $params['limit'] : aioseo()->settings->tablePagination['searchStatisticsPostDetailKeywords']; $offset = ! empty( $params['offset'] ) ? $params['offset'] : 0; $filter = ! empty( $params['filter'] ) ? $params['filter'] : 'all'; $searchTerm = ! empty( $params['searchTerm'] ) ? sanitize_text_field( $params['searchTerm'] ) : ''; $orderDir = ! empty( $params['orderDir'] ) ? strtoupper( $params['orderDir'] ) : 'DESC'; $orderBy = ! empty( $params['orderBy'] ) ? aioseo()->helpers->toCamelCase( $params['orderBy'] ) : 'clicks'; $postId = ! empty( $params['postId'] ) ? $params['postId'] : 0; // If we're on the Top Losing/Top Winning pages, then we need to override the default ORDER BY/ORDER DIR. if ( 'all' !== $filter ) { if ( 'topLosing' === $filter ) { $orderBy = 'decay'; $orderDir = 'ASC'; } elseif ( 'topWinning' === $filter ) { $orderBy = 'decay'; $orderDir = 'DESC'; } } if ( empty( $startDate ) || empty( $endDate ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Invalid date range.' ], 400 ); } if ( empty( $postId ) || ! is_numeric( $postId ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Invalid post id.' ], 400 ); } aioseo()->searchStatistics->stats->setDateRange( $startDate, $endDate ); $permalink = get_permalink( $postId ); $page = aioseo()->searchStatistics->helpers->getPageSlug( $permalink ); $baseUrl = untrailingslashit( aioseo()->searchStatistics->api->auth->getAuthedSite() ); $pageUrl = $baseUrl . $page; $cacheArgs = [ aioseo()->searchStatistics->api->auth->getAuthedSite(), $startDate, $endDate, $limit, $offset, $filter, $searchTerm, $orderDir, $orderBy, $postId ]; $cacheHash = sha1( implode( ',', $cacheArgs ) ); $cachedData = aioseo()->core->cache->get( "aioseo_search_statistics_page_kws_{$cacheHash}" ); if ( null !== $cachedData ) { $success = false === $cachedData ? false : true; $statusCode = false === $cachedData ? 400 : 200; return new \WP_REST_Response( [ 'success' => $success, 'data' => $cachedData, 'range' => aioseo()->searchStatistics->stats->getDateRange() ], $statusCode ); } $args = [ 'start' => $startDate, 'end' => $endDate, 'page' => $pageUrl, 'pagination' => [ 'limit' => $limit, 'offset' => $offset, 'filter' => $filter, 'searchTerm' => $searchTerm, 'orderDir' => $orderDir, 'orderBy' => $orderBy ] ]; $api = new Api\Request( 'google-search-console/statistics/page/keywords/', $args, 'POST' ); $response = $api->request(); if ( is_wp_error( $response ) || ! empty( $response['error'] ) || empty( $response['data'] ) ) { aioseo()->core->cache->update( "aioseo_search_statistics_page_kws_{$cacheHash}", false, 60 ); return new \WP_REST_Response( [ 'success' => false, 'data' => false, 'range' => aioseo()->searchStatistics->stats->getDateRange() ], 400 ); } $data = $response['data']; // Add localized filters to paginated data. $data['paginated']['filters'] = aioseo()->searchStatistics->stats->keywords->getFilters( $filter, $searchTerm ); aioseo()->core->cache->update( "aioseo_search_statistics_page_kws_{$cacheHash}", $data, MONTH_IN_SECONDS ); return new \WP_REST_Response( [ 'success' => true, 'data' => $data ], 200 ); } /** * Returns the focus keyword trend for the post detail page. * * @since 4.3.0 * * @param \WP_REST_Request $request The REST Request * @return \WP_REST_Response The response. */ public static function getPostDetailFocusKeywordTrend( $request ) { if ( ! aioseo()->license->hasCoreFeature( 'search-statistics', 'post-detail-focus-keyword-trend' ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Feature not available.' ], 400 ); } $params = $request->get_params(); $startDate = ! empty( $params['startDate'] ) ? $params['startDate'] : ''; $endDate = ! empty( $params['endDate'] ) ? $params['endDate'] : ''; $postId = ! empty( $params['postId'] ) ? $params['postId'] : 0; $focusKeyword = ! empty( $params['focusKeyword'] ) ? $params['focusKeyword'] : ''; if ( empty( $startDate ) || empty( $endDate ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Invalid date range.' ], 400 ); } if ( empty( $postId ) || ! is_numeric( $postId ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Invalid post id.' ], 400 ); } if ( empty( $focusKeyword ) ) { return new \WP_REST_Response( [ 'success' => false, 'message' => 'Invalid focus keyword.' ], 400 ); } aioseo()->searchStatistics->stats->setDateRange( $startDate, $endDate ); $cacheArgs = [ aioseo()->searchStatistics->api->auth->getAuthedSite(), $startDate, $endDate, $postId, $focusKeyword ]; $cacheHash = sha1( implode( ',', $cacheArgs ) ); $cachedData = aioseo()->core->cache->get( "aioseo_search_statistics_page_focus_kw_{$cacheHash}" ); if ( null !== $cachedData ) { $success = false === $cachedData ? false : true; $statusCode = false === $cachedData ? 400 : 200; return new \WP_REST_Response( [ 'success' => $success, 'data' => $cachedData ], $statusCode ); } $permalink = get_permalink( $postId ); $page = aioseo()->searchStatistics->helpers->getPageSlug( $permalink ); $baseUrl = untrailingslashit( aioseo()->searchStatistics->api->auth->getAuthedSite() ); $pageUrl = $baseUrl . $page; $args = [ 'start' => $startDate, 'end' => $endDate, 'page' => $pageUrl, 'focusKeyword' => $focusKeyword ]; $api = new Api\Request( 'google-search-console/statistics/page/focus-keyword/', $args, 'POST' ); $response = $api->request(); if ( is_wp_error( $response ) || ! empty( $response['error'] ) || empty( $response['data'] ) ) { aioseo()->core->cache->update( "aioseo_search_statistics_page_focus_kw_{$cacheHash}", false, 60 ); return new \WP_REST_Response( [ 'success' => false, 'data' => false ], 400 ); } aioseo()->core->cache->update( "aioseo_search_statistics_page_focus_kw_{$cacheHash}", $response['data'], MONTH_IN_SECONDS ); return new \WP_REST_Response( [ 'success' => true, 'data' => $response['data'] ], 200 ); } /** * Returns a list of schema graphs for the current post. * * @since 4.3.0 * * @param \WP_Post $post The post to get the schema graphs. * @return array[string] List of schema graph names. */ private static function getSchemaGraphs( $post ) { $schemaGraphs = []; $defaultGraph = aioseo()->schema->getDefaultPostGraph(); if ( $defaultGraph ) { $schemaGraphs[] = $defaultGraph; } $userDefinedGraphs = []; $metaData = aioseo()->meta->metaData->getMetaData( $post ); if ( ! is_a( $post, 'WP_Post' ) || empty( $metaData->post_id ) ) { return $schemaGraphs; } $graphs = $metaData->schema->graphs; foreach ( $graphs as $graphData ) { $graphData = (object) $graphData; if ( empty( $graphData->id ) || empty( $graphData->graphName ) || empty( $graphData->properties ) ) { continue; } // If the graph has a subtype, this is the place where we need to replace the main graph name with the one of the subtype. if ( ! empty( $graphData->properties->type ) ) { $graphData->graphName = $graphData->properties->type; } $userDefinedGraphs[] = $graphData->graphName; } $customGraphs = []; foreach ( $metaData->schema->customGraphs as $customGraphData ) { $customGraphData = (object) $customGraphData; if ( empty( $customGraphData->schema ) ) { continue; } $customSchema = json_decode( $customGraphData->schema, true ); if ( ! empty( $customSchema ) ) { if ( isset( $customSchema['@graph'] ) && is_array( $customSchema['@graph'] ) ) { foreach ( $customSchema['@graph'] as $graph ) { if ( ! empty( $graph['@type'] ) ) { $userDefinedGraphs[] = $graph['@type'] . ' ' . __( '(Custom)', 'aioseo-pro' ); } } } else { if ( ! empty( $customSchema['@type'] ) ) { $userDefinedGraphs[] = $customSchema['@type'] . ' ' . __( '(Custom)', 'aioseo-pro' ); } } } } $blockGraphs = []; foreach ( $metaData->schema->blockGraphs as $blockGraphData ) { // If the type isn't set for whatever reason, then bail. if ( empty( $blockGraphData->type ) ) { continue; } $type = strtolower( $blockGraphData->type ); switch ( $type ) { case 'aioseo/faq': $blockGraphs[] = 'FAQPage' . ' ' . __( '(Block)', 'aioseo-pro' ); break; default: break; } } $schemaGraphs = array_merge( $schemaGraphs, $userDefinedGraphs, $customGraphs, $blockGraphs ); sort( $schemaGraphs ); return $schemaGraphs; } }