You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

755 lines
22 KiB
PHP

<?php
/**
* The Analytics Module
*
* @since 2.0.0
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics;
use stdClass;
use WP_Error;
use WP_REST_Request;
use RankMath\Traits\Cache;
use RankMath\Traits\Hooker;
use RankMath\Analytics\Stats;
defined( 'ABSPATH' ) || exit;
/**
* Posts class.
*/
class Posts {
use Hooker, Cache;
/**
* Main instance
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Posts
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Posts ) ) {
$instance = new Posts();
$instance->setup();
}
return $instance;
}
/**
* Constructor.
*/
public function setup() {
$this->filter( 'rank_math/analytics/single/report', 'add_badges', 10, 1 );
$this->filter( 'rank_math/analytics/single/report', 'add_backlinks', 10, 1 );
$this->filter( 'rank_math/analytics/single/report', 'add_ranking_keywords', 10, 1 );
$this->filter( 'rank_math/analytics/single/report', 'get_graph_data_for_post', 10, 1 );
$this->filter( 'rank_math/analytics/post_data', 'sort_new_data', 10, 2 );
$this->filter( 'rank_math/analytics/get_objects_by_score_args', 'get_objects_by_score_args', 10, 2 );
$this->filter( 'rank_math/analytics/get_posts_rows_by_objects', 'get_posts_rows_by_objects', 10, 2 );
}
/**
* Get posts by objects.
*
* @param boolean $result Check.
* @param WP_REST_Request $request Full details about the request.
* @return $args for order and orderby.
*/
public function get_objects_by_score_args( $result, WP_REST_Request $request ) {
$orderby = $request->get_param( 'orderby' );
$is_valid_order_param = in_array( $orderby, [ 'title', 'seo_score' ], true );
$orderby = $is_valid_order_param ? $orderby : 'created';
$order = strtoupper( $request->get_param( 'order' ) );
$args['orderBy'] = $orderby;
$args['order'] = $order;
return $args;
}
/**
* Change user perference.
*
* @param array $data array.
* @param WP_REST_Request $request post object.
* @return array $data sorted array.
*/
public function sort_new_data( $data, WP_REST_Request $request ) {
$id = $request->get_param( 'id' );
$orderby = $request->get_param( 'orderby' );
$order = strtoupper( $request->get_param( 'order' ) );
if ( 'query' !== $orderby ) {
$data['rankingKeywords'] = $this->ranking_keyword_array_sort( $data['rankingKeywords'], $order, $orderby );
}
if ( 'query' === $orderby ) {
if ( 'DESC' === $order ) {
uasort(
$data['rankingKeywords'],
function( $a, $b ) use ( $orderby ) {
return strtolower( $a[ $orderby ] ) < strtolower( $b[ $orderby ] );
}
);
}
if ( 'ASC' === $order ) {
uasort(
$data['rankingKeywords'],
function( $a, $b ) use ( $orderby ) {
return strtolower( $a[ $orderby ] ) > strtolower( $b[ $orderby ] );
}
);
}
}
return $data;
}
/**
* Sort array for ranking keyword by order and orderby
*
* @param array $arr array.
*
* @param Variable $arr_order is order direction.
*
* @param Variable $arr_orderby is key for sort.
*/
public function ranking_keyword_array_sort( $arr, $arr_order, $arr_orderby ) {
if ( 'DESC' === $arr_order ) {
uasort(
$arr,
function( $a, $b ) use ( $arr_orderby ) {
return $a[ $arr_orderby ]['total'] < $b[ $arr_orderby ]['total'];
}
);
}
if ( 'ASC' === $arr_order ) {
uasort(
$arr,
function( $a, $b ) use ( $arr_orderby ) {
return $a[ $arr_orderby ]['total'] > $b[ $arr_orderby ]['total'];
}
);
}
return $arr;
}
/**
* Get posts by objects.
*
* @param boolean $result Check.
* @param WP_REST_Request $request Full details about the request.
* @return array Posts rows.
*/
public function get_posts_rows_by_objects( $result, WP_REST_Request $request ) {
$orderby = $request->get_param( 'orderby' );
$order = strtoupper( $request->get_param( 'order' ) );
$objects = Stats::get()->get_objects_by_score( $request );
$objects = Links::get_links_by_objects( $objects );
$pages = isset( $objects['rows'] ) ? \array_keys( $objects['rows'] ) : [];
$pageviews = Pageviews::get_pageviews( [ 'pages' => $pages ] );
$pageviews = Stats::get()->set_page_as_key( $pageviews['rows'] );
$console = Stats::get()->get_analytics_data(
[
'orderBy' => 'diffImpressions',
'pageview' => true,
'offset' => 0, // Here offset should always zero.
'perpage' => ! empty( $objects['rowsFound'] ) ? $objects['rowsFound'] : 0,
'sub_where' => " AND page IN ('" . join( "', '", $pages ) . "')",
]
);
$new_rows = [];
if ( ! empty( $objects['rows'] ) ) {
foreach ( $objects['rows'] as $object ) {
$page = $object['page'];
if ( isset( $pageviews[ $page ] ) ) {
$object['pageviews'] = [
'total' => (int) $pageviews[ $page ]['pageviews'],
'difference' => (int) $pageviews[ $page ]['difference'],
];
}
if ( isset( $console[ $page ] ) ) {
$object = \array_merge( $console[ $page ], $object );
}
if ( ! isset( $object['links'] ) ) {
$object['links'] = new stdClass();
}
$new_rows[ $page ] = $object;
}
}
$history = $this->get_graph_data_for_pages( $pages );
$new_rows = Stats::get()->set_page_position_graph( $new_rows, $history );
if ( in_array( $orderby, [ 'position', 'clicks', 'pageviews', 'impressions' ], true ) ) {
$new_rows = $this->analytics_array_sort( $new_rows, $order, $orderby );
}
if ( empty( $new_rows ) ) {
$new_rows['response'] = 'No Data';
}
return [
'rows' => $new_rows,
'rowsFound' => ! empty( $objects['rowsFound'] ) ? $objects['rowsFound'] : 0,
];
}
/**
* Sort array by order and orderby
*
* @param array $arr array.
*
* @param Variable $arr_order is order direction.
*
* @param Variable $arr_orderby is key for sort.
*
* @return $arr sorted array
*/
public function analytics_array_sort( $arr, $arr_order, $arr_orderby ) {
if ( 'DESC' === $arr_order ) {
uasort(
$arr,
function( $a, $b ) use ( $arr_orderby ) {
if ( false === array_key_exists( $arr_orderby, $a ) ) {
$a[ $arr_orderby ] = [ 'total' => '0' ];
}
if ( false === array_key_exists( $arr_orderby, $b ) ) {
$b[ $arr_orderby ] = [ 'total' => '0' ];
}
return $a[ $arr_orderby ]['total'] < $b[ $arr_orderby ]['total'];
}
);
}
if ( 'ASC' === $arr_order ) {
uasort(
$arr,
function( $a, $b ) use ( $arr_orderby ) {
if ( false === array_key_exists( $arr_orderby, $a ) ) {
$a[ $arr_orderby ] = [ 'total' => '0' ];
}
if ( false === array_key_exists( $arr_orderby, $b ) ) {
$b[ $arr_orderby ] = [ 'total' => '0' ];
}
return $a[ $arr_orderby ]['total'] > $b[ $arr_orderby ]['total'];
}
);
}
return $arr;
}
/**
* Get ranking keywords data and append it to existing post data.
*
* @param object $post Post object.
* @return object
*/
public function add_ranking_keywords( $post ) {
$page = $post->page;
$data = Stats::get()->get_analytics_data(
[
'dimension' => 'query',
'offset' => 0,
'perpage' => 20,
'orderBy' => 'impressions',
'sub_where' => "AND page = '{$page}'",
]
);
$history = Keywords::get()->get_graph_data_for_keywords( \array_keys( $data ) );
$post->rankingKeywords = Stats::get()->set_query_position( $data, $history ); // phpcs:ignore
return $post;
}
/**
* Append backlinks data into existing post data.
*
* @param object $post Post object.
* @return object
*/
public function add_backlinks( $post ) {
$post->backlinks = [
'total' => 0,
'previous' => 0,
'difference' => 0,
];
return $post;
}
/**
* Append badges data into existing post data.
*
* @param object $post Post object.
* @return object
*/
public function add_badges( $post ) {
$post->badges = [
'clicks' => $this->get_position_for_badges( 'clicks', $post->page ),
'traffic' => $this->get_position_for_badges( 'traffic', $post->page ),
'keywords' => $this->get_position_for_badges( 'query', $post->page ),
'impressions' => $this->get_position_for_badges( 'impressions', $post->page ),
];
return $post;
}
/**
* Get position for badges.
*
* @param string $column Column name.
* @param string $page Page url.
* @return integer
*/
public function get_position_for_badges( $column, $page ) {
$start = date( 'Y-m-d H:i:s', strtotime( '-30 days ', Stats::get()->end ) );
if ( 'traffic' === $column ) {
$rows = DB::traffic()
->select( 'page' )
->selectSum( 'pageviews', 'pageviews' )
->whereBetween( 'created', [ $start, Stats::get()->end_date ] )
->groupBy( 'page' )
->orderBy( 'pageviews', 'DESC' )
->limit( 5 );
} else {
$rows = DB::analytics()
->select( 'page' )
->whereBetween( 'created', [ $start, Stats::get()->end_date ] )
->groupBy( 'page' )
->orderBy( $column, 'DESC' )
->limit( 5 );
}
if ( 'impressions' === $column || 'click' === $column ) {
$rows->selectSum( $column, $column );
}
if ( 'query' === $column ) {
$rows->selectCount( 'DISTINCT(query)', 'keywords' );
}
$rows = $rows->get( ARRAY_A );
foreach ( $rows as $index => $row ) {
if ( $page === $row['page'] ) {
return $index + 1;
}
}
return 99;
}
/**
* Append analytics graph data into existing post data.
*
* @param object $post Post object.
* @return object
*/
public function get_graph_data_for_post( $post ) {
global $wpdb;
// Step1. Get splitted date intervals for graph within selected date range.
$data = new stdClass();
$page = $post->page;
$intervals = Stats::get()->get_intervals();
$sql_daterange = Stats::get()->get_sql_date_intervals( $intervals );
// Step2. Get analytics data summary for each splitted date intervals.
$query = $wpdb->prepare(
"SELECT DATE_FORMAT( created, '%%Y-%%m-%%d') as date, SUM( clicks ) as clicks, SUM(impressions) as impressions, ROUND( AVG(ctr), 2 ) as ctr, {$sql_daterange}
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s AND page LIKE '%{$page}'
GROUP BY range_group",
Stats::get()->start_date,
Stats::get()->end_date
);
$metrics = $wpdb->get_results( $query );
// Step3. Get position data summary for each splitted date intervals.
$query = $wpdb->prepare(
"SELECT page, MAX(CONCAT(t.uid, ':', t.range_group)) as range_group FROM
(SELECT page, MAX(CONCAT(page, ':', DATE(created), ':', LPAD((100 - position), 3, '0'))) as uid, {$sql_daterange}
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s AND page LIKE '%{$page}'
GROUP BY range_group, DATE(created)
ORDER BY DATE(created) DESC) AS t
GROUP BY t.range_group",
Stats::get()->start_date,
Stats::get()->end_date
);
$positions = $wpdb->get_results( $query );
$positions = Stats::get()->extract_data_from_mixed( $positions, 'range_group', ':', [ 'range_group', 'position', 'date' ] );
// Step4. Get keywords count for each splitted date intervals.
$query = $wpdb->prepare(
"SELECT DATE_FORMAT( created, '%%Y-%%m-%%d') as date, COUNT(DISTINCT(query)) as keywords, {$sql_daterange}
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s AND page LIKE '%{$page}'
GROUP BY range_group",
Stats::get()->start_date,
Stats::get()->end_date
);
$keywords = $wpdb->get_results( $query );
// phpcs:enable
// Step5. Filter graph data.
$metrics = Stats::get()->filter_graph_rows( $metrics );
$positions = Stats::get()->filter_graph_rows( $positions );
$keywords = Stats::get()->filter_graph_rows( $keywords );
// Step6. Convert types.
$metrics = array_map( [ Stats::get(), 'normalize_graph_rows' ], $metrics );
$positions = array_map( [ Stats::get(), 'normalize_graph_rows' ], $positions );
$keywords = array_map( [ Stats::get(), 'normalize_graph_rows' ], $keywords );
// Step7. Merge all analytics data.
$data = Stats::get()->get_date_array(
$intervals['dates'],
[
'clicks' => [],
'impressions' => [],
'position' => [],
'ctr' => [],
'keywords' => [],
'pageviews' => [],
]
);
$data = Stats::get()->get_merge_data_graph( $metrics, $data, $intervals['map'] );
$data = Stats::get()->get_merge_data_graph( $positions, $data, $intervals['map'] );
$data = Stats::get()->get_merge_data_graph( $keywords, $data, $intervals['map'] );
// Step8. Get traffic data in case analytics is connected for each splitted data intervals.
if ( \RankMath\Google\Analytics::is_analytics_connected() ) {
$query = $wpdb->prepare(
"SELECT DATE_FORMAT( created, '%%Y-%%m-%%d') as date, SUM( pageviews ) as pageviews, {$sql_daterange}
FROM {$wpdb->prefix}rank_math_analytics_ga
WHERE created BETWEEN %s AND %s AND page LIKE '%{$page}'
GROUP BY range_group",
Stats::get()->start_date,
Stats::get()->end_date
);
$traffic = $wpdb->get_results( $query );
// Filter graph data.
$traffic = Stats::get()->filter_graph_rows( $traffic );
// Convert types.
$traffic = array_map( [ Stats::get(), 'normalize_graph_rows' ], $traffic );
$data = Stats::get()->get_merge_data_graph( $traffic, $data, $intervals['map'] );
}
$data = Stats::get()->get_graph_data_flat( $data );
// Step9. Append graph data into existing post data.
$post->graph = array_values( $data );
return $post;
}
/**
* Get posts rows.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return array Posts rows.
*/
public function get_posts_rows( WP_REST_Request $request ) {
$per_page = 25;
$cache_args = $request->get_params();
$cache_args['per_page'] = $per_page;
$cache_group = 'rank_math_rest_posts_rows';
$cache_key = $this->generate_hash( $cache_args );
$data = $this->get_cache( $cache_key, $cache_group );
if ( ! empty( $data ) ) {
return $data;
}
// Pagination.
$offset = ( $request->get_param( 'page' ) - 1 ) * $per_page;
$orderby = $request->get_param( 'orderby' );
$post_type = sanitize_key( $request->get_param( 'postType' ) );
$order = $request->get_param( 'order' );
$order = in_array( $order, [ 'asc', 'desc' ], true ) ? $order : 'desc';
$order = strtoupper( $order );
$post_type_clause = $post_type ? " AND o.object_subtype = '{$post_type}'" : '';
if ( 'pageviews' === $orderby ) {
// Get posts order by pageviews.
$t_data = Pageviews::get_pageviews_with_object(
[
'order' => $order,
'limit' => "LIMIT {$offset}, {$per_page}",
'sub_where' => $post_type_clause,
]
);
$pageviews = Stats::get()->set_page_as_key( $t_data['rows'] );
$pages = \array_keys( $pageviews );
$pages = array_map( 'esc_sql', $pages );
$console = Stats::get()->get_analytics_data(
[
'offset' => 0, // Should set as 0.
'perpage' => $per_page,
'objects' => false,
'sub_where' => " AND page IN ('" . join( "', '", $pages ) . "')",
]
);
$data['rowsFound'] = $this->rows_found();
foreach ( $pageviews as $page => &$pageview ) {
$pageview['pageviews'] = [
'total' => (int) $pageview['pageviews'],
'difference' => (int) $pageview['difference'],
];
if ( isset( $console[ $page ] ) ) {
unset( $console[ $page ]['pageviews'] );
$pageview = \array_merge( $pageview, $console[ $page ] );
}
}
$history = $this->get_graph_data_for_pages( $pages );
$pageviews = Stats::get()->set_page_position_graph( $pageviews, $history );
$data['rows'] = $pageviews;
} else {
// Get posts order by impressions.
$t_data = DB::objects()
->select( [ 'page', 'title', 'object_id' ] )
->where( 'is_indexable', 1 );
if ( 'title' === $orderby ) {
$t_data->orderBy( $orderby, $order )
->limit( $per_page, $offset );
}
$t_data = $t_data->get( ARRAY_A );
$pages = Stats::get()->set_page_as_key( $t_data );
$params = \array_keys( $pages );
$params = array_map( 'esc_sql', $params );
$args = [
'dimension' => 'page',
'offset' => 0,
'perpage' => 20000,
'sub_where' => " AND page IN ('" . join( "', '", $params ) . "')",
];
if ( 'title' !== $orderby ) {
$args['orderBy'] = $orderby;
$args['order'] = $order;
}
$rows = Stats::get()->get_analytics_data( $args );
if ( 'title' !== $orderby ) {
foreach ( $pages as $page => $row ) {
if ( ! isset( $rows[ $page ] ) ) {
$rows[ $page ] = $row;
} else {
$rows[ $page ] = \array_merge( $rows[ $page ], $row );
}
}
$history = $this->get_graph_data_for_pages( $params );
$data['rows'] = Stats::get()->set_page_position_graph( $rows, $history );
$data['rowsFound'] = count( $pages );
// Filter array by $offset, $perpage value.
$data['rows'] = array_slice( $data['rows'], $offset, $per_page, true );
} else {
foreach ( $pages as $page => &$row ) {
if ( isset( $rows[ $page ] ) ) {
$row = \array_merge( $row, $rows[ $page ] );
}
}
$history = $this->get_graph_data_for_pages( $params );
$data['rows'] = Stats::get()->set_page_position_graph( $pages, $history );
$data['rowsFound'] = $this->rows_found();
}
// Get fetched page info again.
$pages = Stats::get()->set_page_as_key( $data['rows'] );
$params = \array_keys( $pages );
$params = array_map( 'esc_sql', $params );
// Get pageviews info.
$pageviews = Pageviews::get_pageviews_with_object(
[
'limit' => "LIMIT 0, {$per_page}",
'sub_where' => " AND o.page IN ('" . join( "', '", $params ) . "')" . $post_type_clause,
]
);
$pageviews = Stats::get()->set_page_as_key( $pageviews['rows'] );
// Merge pageview info into main data.
foreach ( $data['rows'] as $page => &$row ) {
if ( isset( $pageviews[ $page ] ) ) {
$pageview = [
'pageviews' => [
'total' => (int) $pageviews[ $page ]['pageviews'],
'difference' => (int) $pageviews[ $page ]['difference'],
],
];
$row = \array_merge( $row, $pageview );
}
}
}
if ( empty( $data ) ) {
$data['response'] = 'No Data';
} else {
$this->set_cache( $cache_key, $data, $cache_group, DAY_IN_SECONDS );
}
return $data;
}
/**
* Get top 5 winning posts.
*
* @return array
*/
public function get_winning_posts() {
global $wpdb;
$cache_key = Stats::get()->get_cache_key( 'winning_posts', Stats::get()->days . 'days' );
$cache = get_transient( $cache_key );
if ( false !== $cache ) {
return $cache;
}
$rows = Stats::get()->get_analytics_data(
[
'order' => 'ASC',
'objects' => true,
'pageview' => true,
'offset' => 0,
'perpage' => 5,
'type' => 'win',
]
);
$history = $this->get_graph_data_for_pages( \array_keys( $rows ) );
$rows = Stats::get()->set_page_position_graph( $rows, $history );
if ( empty( $rows ) ) {
$rows['response'] = 'No Data';
}
set_transient( $cache_key, $rows, DAY_IN_SECONDS );
return $rows;
}
/**
* Get top 5 losing posts.
*
* @return object
*/
public function get_losing_posts() {
global $wpdb;
$cache_key = Stats::get()->get_cache_key( 'losing_posts', Stats::get()->days . 'days' );
$cache = get_transient( $cache_key );
if ( false !== $cache ) {
return $cache;
}
$rows = Stats::get()->get_analytics_data(
[
'objects' => true,
'pageview' => true,
'offset' => 0,
'perpage' => 5,
'type' => 'lose',
]
);
$history = $this->get_graph_data_for_pages( \array_keys( $rows ) );
$rows = Stats::get()->set_page_position_graph( $rows, $history );
if ( empty( $rows ) ) {
$rows['response'] = 'No Data';
}
set_transient( $cache_key, $rows, DAY_IN_SECONDS );
return $rows;
}
/**
* Get graph data for pages.
*
* @param array $pages Pages to get data for.
*
* @return array
*/
public function get_graph_data_for_pages( $pages ) {
global $wpdb;
$intervals = Stats::get()->get_intervals();
$sql_daterange = Stats::get()->get_sql_date_intervals( $intervals );
$pages = \array_map( 'esc_sql', $pages );
$pages = '(\'' . join( '\', \'', $pages ) . '\')';
$query = $wpdb->prepare(
"SELECT page, date, MAX(CONCAT(t.uid, ':', t.range_group)) as range_group FROM
( SELECT page, DATE_FORMAT( created,'%%Y-%%m-%%d') as date, MAX( CONCAT( page, ':', DATE( created ), ':', LPAD( ( 100 - position ), 3, '0' ) ) ) as uid, {$sql_daterange}
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE page IN {$pages} AND created BETWEEN %s AND %s
GROUP BY page, range_group, DATE(created)
ORDER BY page ASC, DATE(created) DESC) AS t
GROUP BY t.page, t.range_group
ORDER BY date ASC",
Stats::get()->start_date,
Stats::get()->end_date
);
$data = $wpdb->get_results( $query );
$data = Stats::get()->extract_data_from_mixed( $data, 'range_group', ':', [ 'range_group', 'position' ] );
$data = Stats::get()->filter_graph_rows( $data );
return array_map( [ Stats::get(), 'normalize_graph_rows' ], $data );
}
/**
* Count indexable pages.
*
* @return mixed
*/
private function rows_found() {
return DB::objects()
->selectCount( 'page' )
->where( 'is_indexable', 1 )
->getVar();
}
}