Commit realizado el 12:13:52 08-04-2024
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 70 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,573 @@
|
||||
<?php
|
||||
/**
|
||||
* The Analytics AJAX
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use RankMath\Helper;
|
||||
use RankMath\Google\Api;
|
||||
use RankMath\Helpers\Str;
|
||||
use RankMath\Helpers\Param;
|
||||
use RankMath\Google\Analytics;
|
||||
use RankMath\Google\Authentication;
|
||||
use RankMath\Sitemap\Sitemap;
|
||||
use RankMath\Google\Console as Google_Analytics;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* AJAX class.
|
||||
*/
|
||||
class AJAX {
|
||||
|
||||
use \RankMath\Traits\Ajax;
|
||||
|
||||
/**
|
||||
* The Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->ajax( 'query_analytics', 'query_analytics' );
|
||||
$this->ajax( 'add_site_console', 'add_site_console' );
|
||||
$this->ajax( 'disconnect_google', 'disconnect_google' );
|
||||
$this->ajax( 'verify_site_console', 'verify_site_console' );
|
||||
$this->ajax( 'google_check_all_services', 'check_all_services' );
|
||||
|
||||
// Google Data Management Services.
|
||||
$this->ajax( 'analytics_delete_cache', 'delete_cache' );
|
||||
$this->ajax( 'analytic_start_fetching', 'analytic_start_fetching' );
|
||||
$this->ajax( 'analytic_cancel_fetching', 'analytic_cancel_fetching' );
|
||||
|
||||
// Save Linked Google Account info Services.
|
||||
$this->ajax( 'check_console_request', 'check_console_request' );
|
||||
$this->ajax( 'check_analytics_request', 'check_analytics_request' );
|
||||
$this->ajax( 'save_analytic_profile', 'save_analytic_profile' );
|
||||
$this->ajax( 'save_analytic_options', 'save_analytic_options' );
|
||||
|
||||
// Create new GA4 property.
|
||||
$this->ajax( 'create_ga4_property', 'create_ga4_property' );
|
||||
$this->ajax( 'get_ga4_data_streams', 'get_ga4_data_streams' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new GA4 property.
|
||||
*/
|
||||
public function create_ga4_property() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
$account_id = Param::post( 'accountID', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
|
||||
|
||||
$timezone = get_option( 'timezone_string' );
|
||||
$offset = get_option( 'gmt_offset' );
|
||||
|
||||
if ( empty( $timezone ) && 0 !== $offset && floor( $offset ) === $offset ) {
|
||||
$offset_st = $offset > 0 ? "-$offset" : '+' . absint( $offset );
|
||||
$timezone = 'Etc/GMT' . $offset_st;
|
||||
}
|
||||
|
||||
$args = [
|
||||
'displayName' => get_bloginfo( 'sitename' ) . ' - GA4',
|
||||
'parent' => "accounts/{$account_id}",
|
||||
'timeZone' => empty( $timezone ) ? 'UTC' : $timezone,
|
||||
];
|
||||
|
||||
$response = Api::get()->http_post(
|
||||
'https://analyticsadmin.googleapis.com/v1alpha/properties',
|
||||
$args
|
||||
);
|
||||
|
||||
if ( ! empty( $response['error'] ) ) {
|
||||
$this->error( $response['error']['message'] );
|
||||
}
|
||||
|
||||
$property_id = str_replace( 'properties/', '', $response['name'] );
|
||||
$property_name = esc_html( $response['displayName'] );
|
||||
$all_accounts = get_option( 'rank_math_analytics_all_services' );
|
||||
if ( isset( $all_accounts['accounts'][ $account_id ] ) ) {
|
||||
$all_accounts['accounts'][ $account_id ]['properties'][ $property_id ] = [
|
||||
'name' => $property_name,
|
||||
'id' => $property_id,
|
||||
'account_id' => $account_id,
|
||||
'type' => 'GA4',
|
||||
];
|
||||
|
||||
update_option( 'rank_math_analytics_all_services', $all_accounts );
|
||||
}
|
||||
|
||||
$this->success(
|
||||
[
|
||||
'id' => $property_id,
|
||||
'name' => $property_name,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of Web data streams.
|
||||
*/
|
||||
public function get_ga4_data_streams() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
$property_id = Param::post( 'propertyID', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
|
||||
|
||||
$response = Api::get()->http_get(
|
||||
"https://analyticsadmin.googleapis.com/v1alpha/properties/{$property_id}/dataStreams"
|
||||
);
|
||||
|
||||
if ( ! empty( $response['error'] ) ) {
|
||||
$this->error( $response['error']['message'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $response['dataStreams'] ) ) {
|
||||
$streams = [];
|
||||
foreach ( $response['dataStreams'] as $data_stream ) {
|
||||
$streams[] = [
|
||||
'id' => str_replace( "properties/{$property_id}/dataStreams/", '', $data_stream['name'] ),
|
||||
'name' => $data_stream['displayName'],
|
||||
'measurementId' => $data_stream['webStreamData']['measurementId'],
|
||||
];
|
||||
}
|
||||
|
||||
$this->success( [ 'streams' => $streams ] );
|
||||
}
|
||||
|
||||
$stream = $this->create_ga4_data_stream( "properties/{$property_id}" );
|
||||
if ( ! is_array( $stream ) ) {
|
||||
$this->error( $stream );
|
||||
}
|
||||
|
||||
$this->success( [ 'streams' => [ $stream ] ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the Google Search Console request.
|
||||
*/
|
||||
public function check_console_request() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
|
||||
$success = Api::get()->get_search_analytics();
|
||||
|
||||
if ( is_wp_error( $success ) ) {
|
||||
$this->error( esc_html__( 'Data import will not work for this service as sufficient permissions are not given.', 'rank-math' ) );
|
||||
}
|
||||
|
||||
$this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the Google Analytics request.
|
||||
*/
|
||||
public function check_analytics_request() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
|
||||
$success = Analytics::get_analytics( [], true );
|
||||
|
||||
if ( is_wp_error( $success ) ) {
|
||||
$this->error( esc_html__( 'Data import will not work for this service as sufficient permissions are not given.', 'rank-math' ) );
|
||||
}
|
||||
|
||||
$this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save analytic profile.
|
||||
*/
|
||||
public function save_analytic_profile() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
|
||||
$profile = Param::post( 'profile' );
|
||||
$country = Param::post( 'country', 'all' );
|
||||
$days = Param::get( 'days', 90, FILTER_VALIDATE_INT );
|
||||
$enable_index_status = Param::post( 'enableIndexStatus', false, FILTER_VALIDATE_BOOLEAN );
|
||||
|
||||
$success = Api::get()->get_search_analytics(
|
||||
[
|
||||
'country' => $country,
|
||||
'profile' => $profile,
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $success ) ) {
|
||||
$this->error( esc_html__( 'Data import will not work for this service as sufficient permissions are not given.', 'rank-math' ) );
|
||||
}
|
||||
|
||||
$prev = get_option( 'rank_math_google_analytic_profile', [] );
|
||||
$value = [
|
||||
'country' => $country,
|
||||
'profile' => $profile,
|
||||
'enable_index_status' => $enable_index_status,
|
||||
];
|
||||
update_option( 'rank_math_google_analytic_profile', $value );
|
||||
|
||||
// Remove other stored sites from option for privacy.
|
||||
$all_accounts = get_option( 'rank_math_analytics_all_services', [] );
|
||||
$all_accounts['sites'] = [ $profile => $profile ];
|
||||
|
||||
update_option( 'rank_math_analytics_all_services', $all_accounts );
|
||||
|
||||
// Purge Cache.
|
||||
if ( ! empty( array_diff( $prev, $value ) ) ) {
|
||||
DB::purge_cache();
|
||||
}
|
||||
|
||||
// Start fetching console data.
|
||||
Workflow\Workflow::do_workflow(
|
||||
'console',
|
||||
$days,
|
||||
$prev,
|
||||
$value
|
||||
);
|
||||
|
||||
$this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save analytic profile.
|
||||
*/
|
||||
public function save_analytic_options() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
|
||||
$value = [
|
||||
'account_id' => Param::post( 'accountID', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ),
|
||||
'property_id' => Param::post( 'propertyID', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ),
|
||||
'view_id' => Param::post( 'viewID', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ),
|
||||
'measurement_id' => Param::post( 'measurementID', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ),
|
||||
'stream_name' => Param::post( 'streamName', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ),
|
||||
'country' => Param::post( 'country', 'all', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_BACKTICK ),
|
||||
'install_code' => Param::post( 'installCode', false, FILTER_VALIDATE_BOOLEAN ),
|
||||
'anonymize_ip' => Param::post( 'anonymizeIP', false, FILTER_VALIDATE_BOOLEAN ),
|
||||
'local_ga_js' => Param::post( 'localGAJS', false, FILTER_VALIDATE_BOOLEAN ),
|
||||
'exclude_loggedin' => Param::post( 'excludeLoggedin', false, FILTER_VALIDATE_BOOLEAN ),
|
||||
];
|
||||
|
||||
// Test Google Analytics (GA) connection request.
|
||||
if ( ! empty( $value['view_id'] ) || ! empty( $value['country'] ) || ! empty( $value['property_id'] ) ) {
|
||||
$request = Analytics::get_analytics(
|
||||
[
|
||||
'view_id' => $value['view_id'],
|
||||
'country' => $value['country'],
|
||||
'property_id' => $value['property_id'],
|
||||
],
|
||||
true
|
||||
);
|
||||
|
||||
if ( is_wp_error( $request ) ) {
|
||||
$this->error( esc_html__( 'Data import will not work for this service as sufficient permissions are not given.', 'rank-math' ) );
|
||||
}
|
||||
}
|
||||
|
||||
$days = Param::get( 'days', 90, FILTER_VALIDATE_INT );
|
||||
|
||||
$prev = get_option( 'rank_math_google_analytic_options' );
|
||||
// Preserve adsense info.
|
||||
if ( isset( $prev['adsense_id'] ) ) {
|
||||
$value['adsense_id'] = $prev['adsense_id'];
|
||||
}
|
||||
update_option( 'rank_math_google_analytic_options', $value );
|
||||
|
||||
// Remove other stored accounts from option for privacy.
|
||||
$all_accounts = get_option( 'rank_math_analytics_all_services', [] );
|
||||
if ( isset( $all_accounts['accounts'][ $value['account_id'] ] ) ) {
|
||||
$account = $all_accounts['accounts'][ $value['account_id'] ];
|
||||
|
||||
if ( isset( $account['properties'][ $value['property_id'] ] ) ) {
|
||||
$property = $account['properties'][ $value['property_id'] ];
|
||||
$account['properties'] = [ $value['property_id'] => $property ];
|
||||
}
|
||||
|
||||
$all_accounts['accounts'] = [ $value['account_id'] => $account ];
|
||||
}
|
||||
update_option( 'rank_math_analytics_all_services', $all_accounts );
|
||||
|
||||
// Start fetching analytics data.
|
||||
Workflow\Workflow::do_workflow(
|
||||
'analytics',
|
||||
$days,
|
||||
$prev,
|
||||
$value
|
||||
);
|
||||
|
||||
$this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect google.
|
||||
*/
|
||||
public function disconnect_google() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
Api::get()->revoke_token();
|
||||
Workflow\Workflow::kill_workflows();
|
||||
|
||||
$this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel fetching data.
|
||||
*/
|
||||
public function analytic_cancel_fetching() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
Workflow\Workflow::kill_workflows();
|
||||
|
||||
$this->success( esc_html__( 'Data fetching cancelled.', 'rank-math' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Start data fetching for console, analytics, adsense.
|
||||
*/
|
||||
public function analytic_start_fetching() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
|
||||
if ( ! Authentication::is_authorized() ) {
|
||||
$this->error( esc_html__( 'Google oAuth is not authorized.', 'rank-math' ) );
|
||||
}
|
||||
|
||||
$days = Param::get( 'days', 90, FILTER_VALIDATE_INT );
|
||||
$days = $days * 2;
|
||||
$rows = DB::objects()
|
||||
->selectCount( 'id' )
|
||||
->getVar();
|
||||
|
||||
if ( empty( $rows ) ) {
|
||||
delete_option( 'rank_math_analytics_installed' );
|
||||
}
|
||||
delete_option( 'rank_math_analytics_last_single_action_schedule_time' );
|
||||
// Start fetching data.
|
||||
foreach ( [ 'console', 'analytics', 'adsense' ] as $action ) {
|
||||
Workflow\Workflow::do_workflow(
|
||||
$action,
|
||||
$days,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
$this->success( esc_html__( 'Data fetching started in the background.', 'rank-math' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cache.
|
||||
*/
|
||||
public function delete_cache() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
|
||||
$days = Param::get( 'days', false, FILTER_VALIDATE_INT );
|
||||
if ( ! $days ) {
|
||||
$this->error( esc_html__( 'Not a valid settings founds to delete cache.', 'rank-math' ) );
|
||||
}
|
||||
|
||||
// Delete fetched console data within specified date range.
|
||||
DB::delete_by_days( $days );
|
||||
|
||||
// Cancel data fetch action.
|
||||
Workflow\Workflow::kill_workflows();
|
||||
delete_transient( 'rank_math_analytics_data_info' );
|
||||
$db_info = DB::info();
|
||||
$db_info['message'] = sprintf( '<div class="rank-math-console-db-info"><span class="dashicons dashicons-calendar-alt"></span> Cached Days: <strong>%s</strong></div>', $db_info['days'] ) .
|
||||
sprintf( '<div class="rank-math-console-db-info"><span class="dashicons dashicons-editor-ul"></span> Data Rows: <strong>%s</strong></div>', Str::human_number( $db_info['rows'] ) ) .
|
||||
sprintf( '<div class="rank-math-console-db-info"><span class="dashicons dashicons-editor-code"></span> Size: <strong>%s</strong></div>', size_format( $db_info['size'] ) );
|
||||
|
||||
$this->success( $db_info );
|
||||
}
|
||||
|
||||
/**
|
||||
* Search objects info by title or page and return.
|
||||
*/
|
||||
public function query_analytics() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
|
||||
$query = Param::get( 'query', '', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_BACKTICK );
|
||||
|
||||
$data = DB::objects()
|
||||
->whereLike( 'title', $query )
|
||||
->orWhereLike( 'page', $query )
|
||||
->limit( 10 )
|
||||
->get();
|
||||
|
||||
$this->send( [ 'data' => $data ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all google services.
|
||||
*/
|
||||
public function check_all_services() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
|
||||
$result = [
|
||||
'isVerified' => false,
|
||||
'inSearchConsole' => false,
|
||||
'hasSitemap' => false,
|
||||
'hasAnalytics' => false,
|
||||
'hasAnalyticsProperty' => false,
|
||||
];
|
||||
|
||||
$result['homeUrl'] = Google_Analytics::get_site_url();
|
||||
$result['sites'] = Api::get()->get_sites();
|
||||
$result['inSearchConsole'] = $this->is_site_in_search_console();
|
||||
|
||||
if ( $result['inSearchConsole'] ) {
|
||||
$result['isVerified'] = Helper::is_localhost() ? true : Api::get()->is_site_verified( Google_Analytics::get_site_url() );
|
||||
$result['hasSitemap'] = $this->has_sitemap_submitted();
|
||||
}
|
||||
|
||||
$result['accounts'] = Api::get()->get_analytics_accounts();
|
||||
|
||||
if ( ! empty( $result['accounts'] ) ) {
|
||||
$result['hasAnalytics'] = true;
|
||||
$result['hasAnalyticsProperty'] = $this->is_site_in_analytics( $result['accounts'] );
|
||||
}
|
||||
|
||||
$result = apply_filters( 'rank_math/analytics/check_all_services', $result );
|
||||
|
||||
update_option( 'rank_math_analytics_all_services', $result );
|
||||
|
||||
$this->success( $result );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add site to search console
|
||||
*/
|
||||
public function add_site_console() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
|
||||
$home_url = Google_Analytics::get_site_url();
|
||||
Api::get()->add_site( $home_url );
|
||||
Api::get()->verify_site( $home_url );
|
||||
|
||||
$this->success( [ 'sites' => Api::get()->get_sites() ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify site console.
|
||||
*/
|
||||
public function verify_site_console() {
|
||||
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
|
||||
$this->has_cap_ajax( 'analytics' );
|
||||
|
||||
$home_url = Google_Analytics::get_site_url();
|
||||
Api::get()->verify_site( $home_url );
|
||||
|
||||
$this->success( [ 'verified' => true ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Is site in search console.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function is_site_in_search_console() {
|
||||
// Early Bail!!
|
||||
if ( Helper::is_localhost() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$sites = Api::get()->get_sites();
|
||||
$home_url = Google_Analytics::get_site_url();
|
||||
|
||||
foreach ( $sites as $site ) {
|
||||
if ( trailingslashit( $site ) === $home_url ) {
|
||||
$profile = get_option( 'rank_math_google_analytic_profile' );
|
||||
if ( empty( $profile ) ) {
|
||||
update_option(
|
||||
'rank_math_google_analytic_profile',
|
||||
[
|
||||
'country' => 'all',
|
||||
'profile' => $home_url,
|
||||
]
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is site in analytics.
|
||||
*
|
||||
* @param array $accounts Analytics accounts.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function is_site_in_analytics( $accounts ) {
|
||||
$home_url = Google_Analytics::get_site_url();
|
||||
|
||||
foreach ( $accounts as $account ) {
|
||||
foreach ( $account['properties'] as $property ) {
|
||||
if ( ! empty( $property['url'] ) && trailingslashit( $property['url'] ) === $home_url ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has sitemap in search console.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function has_sitemap_submitted() {
|
||||
$home_url = Google_Analytics::get_site_url();
|
||||
$sitemaps = Api::get()->get_sitemaps( $home_url );
|
||||
|
||||
if ( ! \is_array( $sitemaps ) || empty( $sitemaps ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ( $sitemaps as $sitemap ) {
|
||||
if ( $sitemap['path'] === $home_url . Sitemap::get_sitemap_index_slug() . '.xml' ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new data stream.
|
||||
*
|
||||
* @param string $property_id GA4 property ID.
|
||||
*/
|
||||
private function create_ga4_data_stream( $property_id ) {
|
||||
$args = [
|
||||
'type' => 'WEB_DATA_STREAM',
|
||||
'displayName' => 'Website',
|
||||
'webStreamData' => [
|
||||
'defaultUri' => home_url(),
|
||||
],
|
||||
];
|
||||
|
||||
$stream = Api::get()->http_post(
|
||||
"https://analyticsadmin.googleapis.com/v1alpha/{$property_id}/dataStreams",
|
||||
$args
|
||||
);
|
||||
|
||||
if ( ! empty( $stream['error'] ) ) {
|
||||
return $stream['error']['message'];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => str_replace( "properties/{$property_id}/dataStreams/", '', $stream['name'] ),
|
||||
'name' => $stream['displayName'],
|
||||
'measurementId' => $stream['webStreamData']['measurementId'],
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
/**
|
||||
* Methods for frontend and backend in admin-only module
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Analytics
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use RankMath\Helper;
|
||||
use RankMath\Helpers\Str;
|
||||
use RankMath\Helpers\DB as DB_Helper;
|
||||
use RankMath\Traits\Hooker;
|
||||
use RankMath\Google\Console;
|
||||
use RankMath\Google\Authentication;
|
||||
use RankMath\Analytics\Workflow\Jobs;
|
||||
use RankMath\Analytics\Workflow\Workflow;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Analytics class.
|
||||
*/
|
||||
class Analytics_Common {
|
||||
|
||||
use Hooker;
|
||||
|
||||
/**
|
||||
* The Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( Helper::is_heartbeat() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( Helper::has_cap( 'analytics' ) ) {
|
||||
$this->action( 'rank_math/admin_bar/items', 'admin_bar_items', 11 );
|
||||
}
|
||||
|
||||
// Show Analytics block in the Dashboard widget only if account is connected or user has permissions.
|
||||
if ( Helper::has_cap( 'analytics' ) && Authentication::is_authorized() ) {
|
||||
$this->action( 'rank_math/dashboard/widget', 'dashboard_widget' );
|
||||
}
|
||||
|
||||
new GTag();
|
||||
new Analytics_Stats();
|
||||
$this->action( 'plugins_loaded', 'maybe_init_email_reports', 15 );
|
||||
$this->action( 'init', 'maybe_enable_email_reports', 20 );
|
||||
$this->action( 'cmb2_save_options-page_fields_rank-math-options-general_options', 'maybe_update_report_schedule', 20, 3 );
|
||||
|
||||
Jobs::get();
|
||||
Workflow::get();
|
||||
|
||||
$this->action( 'rest_api_init', 'init_rest_api' );
|
||||
$this->filter( 'rank_math/webmaster/google_verify', 'add_site_verification' );
|
||||
|
||||
$this->filter( 'rank_math/tools/analytics_clear_caches', 'analytics_clear_caches' );
|
||||
$this->filter( 'rank_math/tools/analytics_reindex_posts', 'analytics_reindex_posts' );
|
||||
$this->filter( 'rank_math/tools/analytics_fix_collations', 'analytics_fix_collations' );
|
||||
$this->filter( 'wp_helpers_notifications_render', 'replace_notice_link', 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add stats widget into admin dashboard.
|
||||
*/
|
||||
public function dashboard_widget() {
|
||||
?>
|
||||
<h3>
|
||||
<?php esc_html_e( 'Analytics', 'rank-math' ); ?>
|
||||
<span><?php esc_html_e( 'Last 30 Days', 'rank-math' ); ?></span>
|
||||
<a href="<?php echo esc_url( Helper::get_admin_url( 'analytics' ) ); ?>" class="rank-math-view-report" title="<?php esc_html_e( 'View Report', 'rank-math' ); ?>">
|
||||
<i class="dashicons dashicons-chart-bar"></i>
|
||||
</a>
|
||||
</h3>
|
||||
<div class="rank-math-dashboard-block items-4">
|
||||
<?php
|
||||
$items = $this->get_dashboard_widget_items();
|
||||
foreach ( $items as $label => $item ) {
|
||||
if ( ! $item['value'] ) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<div>
|
||||
<h4>
|
||||
<?php echo esc_html( $item['label'] ); ?>
|
||||
<span class="rank-math-tooltip">
|
||||
<em class="dashicons-before dashicons-editor-help"></em>
|
||||
<span>
|
||||
<?php echo esc_html( $item['desc'] ); ?>
|
||||
</span>
|
||||
</span>
|
||||
</h4>
|
||||
<?php $this->get_analytic_block( $item['data'], ! empty( $item['revert'] ) ); ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Return site verification code.
|
||||
*
|
||||
* @param string $content If any code from setting.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function add_site_verification( $content ) {
|
||||
$code = get_transient( 'rank_math_google_site_verification' );
|
||||
|
||||
return ! empty( $code ) ? $code : $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the REST API endpoints.
|
||||
*/
|
||||
public function init_rest_api() {
|
||||
$controllers = [
|
||||
new Rest(),
|
||||
];
|
||||
|
||||
foreach ( $controllers as $controller ) {
|
||||
$controller->register_routes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add admin bar item.
|
||||
*
|
||||
* @param Admin_Bar_Menu $menu Menu class instance.
|
||||
*/
|
||||
public function admin_bar_items( $menu ) {
|
||||
$dot_color = '#ed5e5e';
|
||||
if ( Console::is_console_connected() ) {
|
||||
$dot_color = '#11ac84';
|
||||
}
|
||||
|
||||
$menu->add_sub_menu(
|
||||
'analytics',
|
||||
[
|
||||
'title' => esc_html__( 'Analytics', 'rank-math' ) . '<span class="rm-menu-new update-plugins" style="background: ' . $dot_color . ';margin-left: 5px;min-width: 10px;height: 10px;margin-bottom: -1px;display: inline-block;border-radius: 5px;"><span class="plugin-count"></span></span>',
|
||||
'href' => Helper::get_admin_url( 'analytics' ),
|
||||
'meta' => [ 'title' => esc_html__( 'Review analytics and sitemaps', 'rank-math' ) ],
|
||||
'priority' => 20,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge cache.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function analytics_clear_caches() {
|
||||
DB::purge_cache();
|
||||
return __( 'Analytics cache cleared.', 'rank-math' );
|
||||
}
|
||||
|
||||
/**
|
||||
* ReIndex posts.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function analytics_reindex_posts() {
|
||||
// Clear all objects data.
|
||||
DB::objects()
|
||||
->truncate();
|
||||
|
||||
// Clear all metadata related to object.
|
||||
DB::table( 'postmeta' )
|
||||
->where( 'meta_key', 'rank_math_analytic_object_id' )
|
||||
->delete();
|
||||
|
||||
// Start reindexing posts.
|
||||
( new \RankMath\Analytics\Workflow\Objects() )->flat_posts();
|
||||
|
||||
return __( 'Post re-index in progress.', 'rank-math' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix table & column collations.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function analytics_fix_collations() {
|
||||
$tables = [
|
||||
'rank_math_analytics_ga',
|
||||
'rank_math_analytics_gsc',
|
||||
'rank_math_analytics_keyword_manager',
|
||||
'rank_math_analytics_inspections',
|
||||
];
|
||||
|
||||
$objects_coll = DB_Helper::get_table_collation( 'rank_math_analytics_objects' );
|
||||
$changed = 0;
|
||||
foreach ( $tables as $table ) {
|
||||
$changed += (int) DB_Helper::check_collation( $table, 'all', $objects_coll );
|
||||
}
|
||||
|
||||
return $changed ? sprintf(
|
||||
/* translators: %1$d: number of changes, %2$s: new collation. */
|
||||
_n( '%1$d collation changed to %2$s.', '%1$d collations changed to %2$s.', $changed, 'rank-math' ),
|
||||
$changed,
|
||||
'`' . $objects_coll . '`'
|
||||
) : __( 'No collation mismatch to fix.', 'rank-math' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Init Email Reports class if the option is enabled.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function maybe_init_email_reports() {
|
||||
if ( Helper::get_settings( 'general.console_email_reports' ) ) {
|
||||
new Email_Reports();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the email reports option if the `enable_email_reports` param is set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function maybe_enable_email_reports() {
|
||||
if ( ! Helper::has_cap( 'analytics' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'enable_email_reports' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! empty( $_GET['enable_email_reports'] ) ) {
|
||||
$all_opts = rank_math()->settings->all_raw();
|
||||
$general = $all_opts['general'];
|
||||
|
||||
$general['console_email_reports'] = 'on';
|
||||
|
||||
Helper::update_all_settings( $general, null, null );
|
||||
rank_math()->settings->reset();
|
||||
$this->schedule_email_reporting();
|
||||
|
||||
Helper::remove_notification( 'rank_math_analytics_new_email_reports' );
|
||||
Helper::redirect( remove_query_arg( 'enable_email_reports' ) );
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/remove/change scheduled action when the report on/off or the frequency options are changed.
|
||||
*
|
||||
* @param int $object_id The ID of the current object.
|
||||
* @param array $updated Array of field IDs that were updated.
|
||||
* Will only include field IDs that had values change.
|
||||
* @param object $cmb CMB object.
|
||||
*/
|
||||
public function maybe_update_report_schedule( $object_id, $updated, $cmb ) {
|
||||
// Early bail if our options are not changed.
|
||||
if ( ! in_array( 'console_email_reports', $updated, true ) && ! in_array( 'console_email_frequency', $updated, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
as_unschedule_all_actions( 'rank_math/analytics/email_report_event', [], 'rank-math' );
|
||||
$values = $cmb->get_sanitized_values( $_POST ); // phpcs:ignore
|
||||
if ( 'off' === $values['console_email_reports'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$frequency = isset( $values['console_email_frequency'] ) ? $values['console_email_frequency'] : 'monthly';
|
||||
$this->schedule_email_reporting( $frequency );
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace link inside notice dynamically to avoid issues with the nonce.
|
||||
*
|
||||
* @param string $output Notice output.
|
||||
* @param string $message Notice message.
|
||||
* @param array $options Notice options.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function replace_notice_link( $output, $message, $options ) {
|
||||
$url = wp_nonce_url( Helper::get_admin_url( 'options-general&enable_email_reports=1#setting-panel-analytics' ), 'enable_email_reports' );
|
||||
$output = str_replace( '###ENABLE_EMAIL_REPORTS###', $url, $output );
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Dashboard Widget items.
|
||||
*/
|
||||
private function get_dashboard_widget_items() {
|
||||
// Get stats info within last 30 days.
|
||||
Stats::get()->set_date_range( '-30 days' );
|
||||
$data = Stats::get()->get_widget();
|
||||
$analytics = get_option( 'rank_math_google_analytic_options' );
|
||||
$is_connected = ! empty( $analytics ) && ! empty( $analytics['view_id'] );
|
||||
return [
|
||||
'search-traffic' => [
|
||||
'label' => __( 'Search Traffic', 'rank-math' ),
|
||||
'desc' => __( 'This is the number of pageviews carried out by visitors from Search Engines.', 'rank-math' ),
|
||||
'value' => $is_connected && defined( 'RANK_MATH_PRO_FILE' ),
|
||||
'data' => isset( $data->pageviews ) ? $data->pageviews : '',
|
||||
],
|
||||
'total-impressions' => [
|
||||
'label' => __( 'Total Impressions', 'rank-math' ),
|
||||
'desc' => __( 'How many times your site showed up in the search results.', 'rank-math' ),
|
||||
'value' => true,
|
||||
'data' => $data->impressions,
|
||||
],
|
||||
'total-clicks' => [
|
||||
'label' => __( 'Total Clicks', 'rank-math' ),
|
||||
'desc' => __( 'This is the number of pageviews carried out by visitors from Google.', 'rank-math' ),
|
||||
'value' => ! $is_connected || ( $is_connected && ! defined( 'RANK_MATH_PRO_FILE' ) ),
|
||||
'data' => $data->clicks,
|
||||
],
|
||||
'total-keywords' => [
|
||||
'label' => __( 'Total Keywords', 'rank-math' ),
|
||||
'desc' => __( 'Total number of keywords your site ranking below 100 position.', 'rank-math' ),
|
||||
'value' => true,
|
||||
'data' => $data->keywords,
|
||||
],
|
||||
'average-position' => [
|
||||
'label' => __( 'Average Position', 'rank-math' ),
|
||||
'desc' => __( 'Average position of all the ranking keywords below 100 position.', 'rank-math' ),
|
||||
'value' => true,
|
||||
'revert' => true,
|
||||
'data' => $data->position,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytic block
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @param boolean $revert Flag whether to revert difference icon or not.
|
||||
*/
|
||||
private function get_analytic_block( $item, $revert = false ) {
|
||||
$total = isset( $item['total'] ) ? abs( $item['total'] ) : 0;
|
||||
$difference = isset( $item['difference'] ) ? abs( $item['difference'] ) : 0;
|
||||
$is_negative = isset( $item['difference'] ) && abs( $item['difference'] ) !== $item['difference'];
|
||||
$diff_class = 'up';
|
||||
if ( ( ! $revert && $is_negative ) || ( $revert && ! $is_negative && $item['difference'] > 0 ) ) {
|
||||
$diff_class = 'down';
|
||||
}
|
||||
?>
|
||||
<div class="rank-math-item-numbers">
|
||||
<strong class="text-large" title="<?php echo esc_html( Str::human_number( $total ) ); ?>"><?php echo esc_html( Str::human_number( $total ) ); ?></strong>
|
||||
<span class="rank-math-item-difference <?php echo esc_attr( $diff_class ); ?>" title="<?php echo esc_html( Str::human_number( $difference ) ); ?>"><?php echo esc_html( Str::human_number( $difference ) ); ?></span>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule Email Reporting.
|
||||
*
|
||||
* @param string $frequency The frequency in which the action should run.
|
||||
* @return void
|
||||
*/
|
||||
private function schedule_email_reporting( $frequency = 'monthly' ) {
|
||||
$interval_days = Email_Reports::get_period_from_frequency( $frequency );
|
||||
$midnight = strtotime( 'tomorrow midnight' );
|
||||
as_schedule_recurring_action( $midnight, $interval_days * DAY_IN_SECONDS, 'rank_math/analytics/email_report_event', [], 'rank-math' );
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* Show Analytics stats on frontend.
|
||||
*
|
||||
* @since 1.0.86
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Analytics
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use RankMath\Helper;
|
||||
use RankMath\KB;
|
||||
use RankMath\Traits\Hooker;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Analytics_Stats class.
|
||||
*/
|
||||
class Analytics_Stats {
|
||||
|
||||
use Hooker;
|
||||
|
||||
/**
|
||||
* The Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( ! Helper::can_add_frontend_stats() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->action( 'wp_enqueue_scripts', 'enqueue' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue Styles and Scripts
|
||||
*/
|
||||
public function enqueue() {
|
||||
if ( ! is_singular() || is_admin() || is_preview() || Helper::is_divi_frontend_editor() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uri = untrailingslashit( plugin_dir_url( __FILE__ ) );
|
||||
wp_enqueue_style( 'rank-math-analytics-stats', $uri . '/assets/css/admin-bar.css', null, rank_math()->version );
|
||||
wp_enqueue_script( 'rank-math-analytics-stats', $uri . '/assets/js/admin-bar.js', [ 'jquery', 'wp-api-fetch', 'wp-element', 'wp-components', 'lodash' ], rank_math()->version, true );
|
||||
|
||||
Helper::add_json( 'isAnalyticsConnected', \RankMath\Google\Analytics::is_analytics_connected() );
|
||||
Helper::add_json( 'hideFrontendStats', get_user_meta( get_current_user_id(), 'rank_math_hide_frontend_stats', true ) );
|
||||
|
||||
Helper::add_json( 'links', KB::get_links() );
|
||||
}
|
||||
}
|
@@ -0,0 +1,539 @@
|
||||
<?php
|
||||
/**
|
||||
* The Analytics Module
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use RankMath\KB;
|
||||
use RankMath\Helper;
|
||||
use RankMath\Helpers\Arr;
|
||||
use RankMath\Helpers\Param;
|
||||
use RankMath\Google\Api;
|
||||
use RankMath\Module\Base;
|
||||
use RankMath\Admin\Page;
|
||||
use RankMath\Google\Console;
|
||||
use RankMath\Google\Authentication;
|
||||
use RankMath\Analytics\Workflow\Jobs;
|
||||
use RankMath\Analytics\Workflow\OAuth;
|
||||
use RankMath\Analytics\Workflow\Workflow;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Analytics class.
|
||||
*/
|
||||
class Analytics extends Base {
|
||||
|
||||
/**
|
||||
* Module ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $id = '';
|
||||
|
||||
/**
|
||||
* Module directory.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $directory = '';
|
||||
|
||||
/**
|
||||
* Module help.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $help = [];
|
||||
|
||||
/**
|
||||
* Module page.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
public $page;
|
||||
|
||||
/**
|
||||
* The Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( Helper::is_heartbeat() || ! Helper::has_cap( 'analytics' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$directory = dirname( __FILE__ );
|
||||
$this->config(
|
||||
[
|
||||
'id' => 'analytics',
|
||||
'directory' => $directory,
|
||||
'help' => [
|
||||
'title' => esc_html__( 'Analytics', 'rank-math' ),
|
||||
'view' => $directory . '/views/help.php',
|
||||
],
|
||||
]
|
||||
);
|
||||
parent::__construct();
|
||||
|
||||
new AJAX();
|
||||
Api::get();
|
||||
Watcher::get();
|
||||
Stats::get();
|
||||
Jobs::get();
|
||||
Workflow::get();
|
||||
|
||||
$this->action( 'admin_notices', 'render_notice' );
|
||||
$this->action( 'rank_math/admin/enqueue_scripts', 'enqueue' );
|
||||
$this->action( 'admin_enqueue_scripts', 'options_panel_messages' );
|
||||
$this->action( 'wp_helpers_notification_dismissed', 'analytic_first_fetch_dismiss' );
|
||||
|
||||
if ( is_admin() ) {
|
||||
$this->filter( 'rank_math/database/tools', 'add_tools' );
|
||||
$this->filter( 'rank_math/settings/general', 'add_settings' );
|
||||
$this->action( 'admin_init', 'refresh_token_missing', 25 );
|
||||
$this->action( 'admin_init', 'cancel_fetch', 5 );
|
||||
|
||||
new OAuth();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel Fetching of Google.
|
||||
*/
|
||||
public function cancel_fetch() {
|
||||
$cancel = Param::get( 'cancel-fetch', false );
|
||||
if (
|
||||
empty( $cancel ) ||
|
||||
! Param::get( '_wpnonce' ) ||
|
||||
! wp_verify_nonce( Param::get( '_wpnonce' ), 'rank_math_cancel_fetch' ) ||
|
||||
! Helper::has_cap( 'analytics' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
Workflow::kill_workflows();
|
||||
}
|
||||
|
||||
/**
|
||||
* If refresh token missing add notice.
|
||||
*/
|
||||
public function refresh_token_missing() {
|
||||
// Bail if the user is not authenticated at all yet.
|
||||
if ( ! Helper::is_site_connected() || ! Authentication::is_authorized() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tokens = Authentication::tokens();
|
||||
if ( ! empty( $tokens['refresh_token'] ) ) {
|
||||
Helper::remove_notification( 'reconnect' );
|
||||
return;
|
||||
}
|
||||
|
||||
// Show admin notification.
|
||||
Helper::add_notification(
|
||||
sprintf(
|
||||
/* translators: Auth URL */
|
||||
'<i class="rm-icon rm-icon-rank-math"></i>' . __( 'It seems like the connection with your Google account & Rank Math needs to be made again. <a href="%s" class="rank-math-reconnect-google">Please click here.</a>', 'rank-math' ),
|
||||
esc_url( Authentication::get_auth_url() )
|
||||
),
|
||||
[
|
||||
'type' => 'error',
|
||||
'classes' => 'rank-math-error rank-math-notice',
|
||||
'id' => 'reconnect',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide fetch notice.
|
||||
*
|
||||
* @param string $notification_id Notification id.
|
||||
*/
|
||||
public function analytic_first_fetch_dismiss( $notification_id ) {
|
||||
if ( 'rank_math_analytics_first_fetch' !== $notification_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
update_option( 'rank_math_analytics_first_fetch', 'hidden' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin init.
|
||||
*/
|
||||
public function render_notice() {
|
||||
$this->remove_action( 'admin_notices', 'render_notice' );
|
||||
if ( 'fetching' === get_option( 'rank_math_analytics_first_fetch' ) ) {
|
||||
$actions = as_get_scheduled_actions(
|
||||
[
|
||||
'order' => 'DESC',
|
||||
'hook' => 'rank_math/analytics/clear_cache',
|
||||
'status' => \ActionScheduler_Store::STATUS_PENDING,
|
||||
]
|
||||
);
|
||||
|
||||
if ( empty( $actions ) ) {
|
||||
update_option( 'rank_math_analytics_first_fetch', 'hidden' );
|
||||
return;
|
||||
}
|
||||
|
||||
$action = current( $actions );
|
||||
$schedule = $action->get_schedule();
|
||||
$next_timestamp = $schedule->get_date()->getTimestamp();
|
||||
|
||||
// Calculate extra time needed for the inspections.
|
||||
$objects_count = DB::objects()->selectCount( 'id' )->getVar();
|
||||
$daily_api_limit = \RankMath\Analytics\Workflow\Inspections::API_LIMIT;
|
||||
$time_gap = \RankMath\Analytics\Workflow\Inspections::REQUEST_GAP_SECONDS;
|
||||
$extra_time = $objects_count * $time_gap;
|
||||
if ( $objects_count > $daily_api_limit ) {
|
||||
$extra_time += DAY_IN_SECONDS * floor( $objects_count / $daily_api_limit );
|
||||
}
|
||||
|
||||
// phpcs:disable
|
||||
$notification = new \RankMath\Admin\Notifications\Notification(
|
||||
/* translators: delete counter */
|
||||
sprintf(
|
||||
'<svg style="vertical-align: middle; margin-right: 5px" viewBox="0 0 462.03 462.03" xmlns="http://www.w3.org/2000/svg" width="20"><g><path d="m462 234.84-76.17 3.43 13.43 21-127 81.18-126-52.93-146.26 60.97 10.14 24.34 136.1-56.71 128.57 54 138.69-88.61 13.43 21z"></path><path d="m54.1 312.78 92.18-38.41 4.49 1.89v-54.58h-96.67zm210.9-223.57v235.05l7.26 3 89.43-57.05v-181zm-105.44 190.79 96.67 40.62v-165.19h-96.67z"></path></g></svg>' .
|
||||
esc_html__( 'Rank Math is importing latest data from connected Google Services, %1$s remaining.', 'rank-math' ) .
|
||||
' <a href="%2$s">' . esc_html__( 'Cancel Fetch', 'rank-math' ) . '</a>',
|
||||
$this->human_interval( $next_timestamp - gmdate( 'U' ) + $extra_time ),
|
||||
esc_url( wp_nonce_url( add_query_arg( 'cancel-fetch', 1 ), 'rank_math_cancel_fetch' ) )
|
||||
),
|
||||
[
|
||||
'type' => 'info',
|
||||
'id' => 'rank_math_analytics_first_fetch',
|
||||
'classes' => 'rank-math-notice',
|
||||
]
|
||||
);
|
||||
|
||||
echo $notification;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an interval of seconds into a two part human friendly string.
|
||||
*
|
||||
* The WordPress human_time_diff() function only calculates the time difference to one degree, meaning
|
||||
* even if an action is 1 day and 11 hours away, it will display "1 day". This function goes one step
|
||||
* further to display two degrees of accuracy.
|
||||
*
|
||||
* Inspired by the Crontrol::interval() function by Edward Dale: https://wordpress.org/plugins/wp-crontrol/
|
||||
*
|
||||
* @param int $interval A interval in seconds.
|
||||
* @param int $periods_to_include Depth of time periods to include, e.g. for an interval of 70, and $periods_to_include of 2, both minutes and seconds would be included. With a value of 1, only minutes would be included.
|
||||
* @return string A human friendly string representation of the interval.
|
||||
*/
|
||||
private function human_interval( $interval, $periods_to_include = 2 ) {
|
||||
$time_periods = [
|
||||
[
|
||||
'seconds' => YEAR_IN_SECONDS,
|
||||
/* translators: %s: amount of time */
|
||||
'names' => _n_noop( '%s year', '%s years', 'rank-math' ),
|
||||
],
|
||||
[
|
||||
'seconds' => MONTH_IN_SECONDS,
|
||||
/* translators: %s: amount of time */
|
||||
'names' => _n_noop( '%s month', '%s months', 'rank-math' ),
|
||||
],
|
||||
[
|
||||
'seconds' => WEEK_IN_SECONDS,
|
||||
/* translators: %s: amount of time */
|
||||
'names' => _n_noop( '%s week', '%s weeks', 'rank-math' ),
|
||||
],
|
||||
[
|
||||
'seconds' => DAY_IN_SECONDS,
|
||||
/* translators: %s: amount of time */
|
||||
'names' => _n_noop( '%s day', '%s days', 'rank-math' ),
|
||||
],
|
||||
[
|
||||
'seconds' => HOUR_IN_SECONDS,
|
||||
/* translators: %s: amount of time */
|
||||
'names' => _n_noop( '%s hour', '%s hours', 'rank-math' ),
|
||||
],
|
||||
[
|
||||
'seconds' => MINUTE_IN_SECONDS,
|
||||
/* translators: %s: amount of time */
|
||||
'names' => _n_noop( '%s minute', '%s minutes', 'rank-math' ),
|
||||
],
|
||||
[
|
||||
'seconds' => 1,
|
||||
/* translators: %s: amount of time */
|
||||
'names' => _n_noop( '%s second', '%s seconds', 'rank-math' ),
|
||||
],
|
||||
];
|
||||
|
||||
if ( $interval <= 0 ) {
|
||||
return __( 'Now!', 'rank-math' );
|
||||
}
|
||||
|
||||
$output = '';
|
||||
|
||||
for ( $time_period_index = 0, $periods_included = 0, $seconds_remaining = $interval; $time_period_index < count( $time_periods ) && $seconds_remaining > 0 && $periods_included < $periods_to_include; $time_period_index++ ) { // phpcs:ignore
|
||||
|
||||
$periods_in_interval = floor( $seconds_remaining / $time_periods[ $time_period_index ]['seconds'] );
|
||||
|
||||
if ( $periods_in_interval > 0 ) {
|
||||
if ( ! empty( $output ) ) {
|
||||
$output .= ' ';
|
||||
}
|
||||
$output .= sprintf( _n( $time_periods[ $time_period_index ]['names'][0], $time_periods[ $time_period_index ]['names'][1], $periods_in_interval, 'rank-math' ), $periods_in_interval ); // phpcs:ignore
|
||||
$seconds_remaining -= $periods_in_interval * $time_periods[ $time_period_index ]['seconds'];
|
||||
$periods_included++;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add l18n for the Settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function options_panel_messages() {
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( 'rank-math_page_rank-math-options-general' !== $screen->id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Helper::add_json( 'confirmAction', esc_html__( 'Are you sure you want to do this?', 'rank-math' ) );
|
||||
Helper::add_json( 'confirmClearImportedData', esc_html__( 'You are about to delete all the previously imported data.', 'rank-math' ) );
|
||||
Helper::add_json( 'confirmClear90DaysCache', esc_html__( 'You are about to delete your 90 days cache.', 'rank-math' ) );
|
||||
Helper::add_json( 'confirmDisconnect', esc_html__( 'Are you sure you want to disconnect Google services from your site?', 'rank-math' ) );
|
||||
Helper::add_json( 'feedbackCacheDeleted', esc_html__( 'Cache deleted.', 'rank-math' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts for the metabox.
|
||||
*/
|
||||
public function enqueue() {
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( 'rank-math_page_rank-math-analytics' !== $screen->id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uri = untrailingslashit( plugin_dir_url( __FILE__ ) );
|
||||
|
||||
wp_enqueue_style(
|
||||
'rank-math-analytics',
|
||||
$uri . '/assets/css/stats.css',
|
||||
[],
|
||||
rank_math()->version
|
||||
);
|
||||
|
||||
wp_register_script(
|
||||
'rank-math-analytics',
|
||||
$uri . '/assets/js/stats.js',
|
||||
[
|
||||
'lodash',
|
||||
'wp-components',
|
||||
'wp-element',
|
||||
'wp-i18n',
|
||||
'wp-date',
|
||||
'wp-api-fetch',
|
||||
'wp-html-entities',
|
||||
],
|
||||
rank_math()->version,
|
||||
true
|
||||
);
|
||||
|
||||
wp_set_script_translations( 'rank-math-analytics', 'rank-math', plugin_dir_path(__FILE__) . 'languages/' );
|
||||
|
||||
$this->action( 'admin_footer', 'dequeue_cmb2' );
|
||||
|
||||
$preference = apply_filters(
|
||||
'rank_math/analytics/user_preference',
|
||||
[
|
||||
'topPosts' => [
|
||||
'seo_score' => false,
|
||||
'schemas_in_use' => false,
|
||||
'impressions' => true,
|
||||
'pageviews' => true,
|
||||
'clicks' => false,
|
||||
'position' => true,
|
||||
'positionHistory' => true,
|
||||
],
|
||||
'siteAnalytics' => [
|
||||
'seo_score' => true,
|
||||
'schemas_in_use' => true,
|
||||
'impressions' => false,
|
||||
'pageviews' => true,
|
||||
'links' => true,
|
||||
'clicks' => false,
|
||||
'position' => false,
|
||||
'positionHistory' => false,
|
||||
],
|
||||
'performance' => [
|
||||
'seo_score' => true,
|
||||
'schemas_in_use' => true,
|
||||
'impressions' => true,
|
||||
'pageviews' => true,
|
||||
'ctr' => false,
|
||||
'clicks' => true,
|
||||
'position' => true,
|
||||
'positionHistory' => true,
|
||||
],
|
||||
'keywords' => [
|
||||
'impressions' => true,
|
||||
'ctr' => false,
|
||||
'clicks' => true,
|
||||
'position' => true,
|
||||
'positionHistory' => true,
|
||||
],
|
||||
'topKeywords' => [
|
||||
'impressions' => true,
|
||||
'ctr' => true,
|
||||
'clicks' => true,
|
||||
'position' => true,
|
||||
'positionHistory' => true,
|
||||
],
|
||||
'trackKeywords' => [
|
||||
'impressions' => true,
|
||||
'ctr' => false,
|
||||
'clicks' => true,
|
||||
'position' => true,
|
||||
'positionHistory' => true,
|
||||
],
|
||||
'rankingKeywords' => [
|
||||
'impressions' => true,
|
||||
'ctr' => false,
|
||||
'clicks' => true,
|
||||
'position' => true,
|
||||
'positionHistory' => true,
|
||||
],
|
||||
'indexing' => [
|
||||
'index_verdict' => true,
|
||||
'indexing_state' => true,
|
||||
'mobile_usability_verdict' => true,
|
||||
'rich_results_items' => true,
|
||||
'page_fetch_state' => false,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if ( metadata_exists( 'user', $user_id, 'rank_math_analytics_table_columns' ) ) {
|
||||
$preference = wp_parse_args(
|
||||
get_user_meta( $user_id, 'rank_math_analytics_table_columns', true ),
|
||||
$preference
|
||||
);
|
||||
}
|
||||
|
||||
Helper::add_json( 'userColumnPreference', $preference );
|
||||
|
||||
// Last Updated.
|
||||
$updated = get_option( 'rank_math_analytics_last_updated', false );
|
||||
$updated = $updated ? date_i18n( get_option( 'date_format' ), $updated ) : '';
|
||||
Helper::add_json( 'lastUpdated', $updated );
|
||||
|
||||
Helper::add_json( 'singleImage', rank_math()->plugin_url() . 'includes/modules/analytics/assets/img/single-post-report.jpg' );
|
||||
|
||||
// Index Status tab.
|
||||
$enable_index_status = Helper::can_add_index_status();
|
||||
Helper::add_json( 'enableIndexStatus', $enable_index_status );
|
||||
Helper::add_json( 'viewedIndexStatus', get_option( 'rank_math_viewed_index_status', false ) );
|
||||
|
||||
if ( $enable_index_status ) {
|
||||
update_option( 'rank_math_viewed_index_status', true );
|
||||
}
|
||||
|
||||
Helper::add_json( 'isRtl', is_rtl() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Dequeue cmb2.
|
||||
*/
|
||||
public function dequeue_cmb2() {
|
||||
wp_dequeue_script( 'cmb2-scripts' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register admin page.
|
||||
*/
|
||||
public function register_admin_page() {
|
||||
$dot_color = '#ed5e5e';
|
||||
if ( Console::is_console_connected() ) {
|
||||
$dot_color = '#11ac84';
|
||||
}
|
||||
|
||||
$this->page = new Page(
|
||||
'rank-math-analytics',
|
||||
esc_html__( 'Analytics', 'rank-math' ) . '<span class="rm-menu-new update-plugins" style="background: ' . $dot_color . '; margin-left: 5px;min-width: 10px;height: 10px;margin-top: 5px;"><span class="plugin-count"></span></span>',
|
||||
[
|
||||
'position' => 5,
|
||||
'parent' => 'rank-math',
|
||||
'capability' => 'rank_math_analytics',
|
||||
'render' => $this->directory . '/views/dashboard.php',
|
||||
'classes' => [ 'rank-math-page' ],
|
||||
'assets' => [
|
||||
'styles' => [
|
||||
'rank-math-common' => '',
|
||||
'rank-math-analytics' => '',
|
||||
],
|
||||
'scripts' => [
|
||||
'rank-math-analytics' => '',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add module settings into general optional panel.
|
||||
*
|
||||
* @param array $tabs Array of option panel tabs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function add_settings( $tabs ) {
|
||||
Arr::insert(
|
||||
$tabs,
|
||||
[
|
||||
'analytics' => [
|
||||
'icon' => 'rm-icon rm-icon-search-console',
|
||||
'title' => esc_html__( 'Analytics', 'rank-math' ),
|
||||
/* translators: Link to kb article */
|
||||
'desc' => sprintf( esc_html__( 'See your Google Search Console, Analytics and AdSense data without leaving your WP dashboard. %s.', 'rank-math' ), '<a href="' . KB::get( 'analytics-settings', 'Options Panel Analytics Tab' ) . '" target="_blank">' . esc_html__( 'Learn more', 'rank-math' ) . '</a>' ),
|
||||
'file' => $this->directory . '/views/options.php',
|
||||
],
|
||||
],
|
||||
9
|
||||
);
|
||||
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add database tools.
|
||||
*
|
||||
* @param array $tools Array of tools.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function add_tools( $tools ) {
|
||||
Arr::insert(
|
||||
$tools,
|
||||
[
|
||||
'analytics_clear_caches' => [
|
||||
'title' => __( 'Purge Analytics Cache', 'rank-math' ),
|
||||
'description' => __( 'Clear analytics cache to re-calculate all the stats again.', 'rank-math' ),
|
||||
'button_text' => __( 'Clear Cache', 'rank-math' ),
|
||||
],
|
||||
'analytics_reindex_posts' => [
|
||||
'title' => __( 'Rebuild Index for Analytics', 'rank-math' ),
|
||||
'description' => __( 'Missing some posts/pages in the Analytics data? Clear the index and build a new one for more accurate stats.', 'rank-math' ),
|
||||
'button_text' => __( 'Rebuild Index', 'rank-math' ),
|
||||
],
|
||||
],
|
||||
3
|
||||
);
|
||||
|
||||
return $tools;
|
||||
}
|
||||
}
|
@@ -0,0 +1,494 @@
|
||||
<?php
|
||||
/**
|
||||
* The Analytics module database operations
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use RankMath\Helper;
|
||||
use RankMath\Google\Api;
|
||||
use RankMath\Google\Console;
|
||||
use RankMath\Helpers\Str;
|
||||
use RankMath\Helpers\DB as DB_Helper;
|
||||
use RankMath\Admin\Database\Database;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* DB class.
|
||||
*/
|
||||
class DB {
|
||||
|
||||
/**
|
||||
* Get any table.
|
||||
*
|
||||
* @param string $table_name Table name.
|
||||
*
|
||||
* @return \RankMath\Admin\Database\Query_Builder
|
||||
*/
|
||||
public static function table( $table_name ) {
|
||||
return Database::table( $table_name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get console data table.
|
||||
*
|
||||
* @return \RankMath\Admin\Database\Query_Builder
|
||||
*/
|
||||
public static function analytics() {
|
||||
return Database::table( 'rank_math_analytics_gsc' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get objects table.
|
||||
*
|
||||
* @return \RankMath\Admin\Database\Query_Builder
|
||||
*/
|
||||
public static function objects() {
|
||||
return Database::table( 'rank_math_analytics_objects' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inspections table.
|
||||
*
|
||||
* @return \RankMath\Admin\Database\Query_Builder
|
||||
*/
|
||||
public static function inspections() {
|
||||
return Database::table( 'rank_math_analytics_inspections' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a record.
|
||||
*
|
||||
* @param int $days Decide whether to delete all or delete 90 days data.
|
||||
*/
|
||||
public static function delete_by_days( $days ) {
|
||||
// Delete console data.
|
||||
if ( Console::is_console_connected() ) {
|
||||
if ( -1 === $days ) {
|
||||
self::analytics()->truncate();
|
||||
} else {
|
||||
$start = date_i18n( 'Y-m-d H:i:s', strtotime( '-1 days' ) );
|
||||
$end = date_i18n( 'Y-m-d H:i:s', strtotime( '-' . $days . ' days' ) );
|
||||
|
||||
self::analytics()->whereBetween( 'created', [ $end, $start ] )->delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Delete analytics, adsense data.
|
||||
do_action( 'rank_math/analytics/delete_by_days', $days );
|
||||
self::purge_cache();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete record for comparison.
|
||||
*/
|
||||
public static function delete_data_log() {
|
||||
$days = Helper::get_settings( 'general.console_caching_control', 90 );
|
||||
|
||||
// Delete old console data more than 2 times ago of specified number of days to keep the data.
|
||||
$start = date_i18n( 'Y-m-d H:i:s', strtotime( '-' . ( $days * 2 ) . ' days' ) );
|
||||
|
||||
self::analytics()->where( 'created', '<', $start )->delete();
|
||||
|
||||
// Delete old analytics and adsense data.
|
||||
do_action( 'rank_math/analytics/delete_data_log', $start );
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge SC transient
|
||||
*/
|
||||
public static function purge_cache() {
|
||||
$table = Database::table( 'options' );
|
||||
$table->whereLike( 'option_name', 'top_keywords' )->delete();
|
||||
$table->whereLike( 'option_name', 'posts_summary' )->delete();
|
||||
$table->whereLike( 'option_name', 'top_keywords_graph' )->delete();
|
||||
$table->whereLike( 'option_name', 'dashboard_stats_widget' )->delete();
|
||||
$table->whereLike( 'option_name', 'rank_math_analytics_data_info' )->delete();
|
||||
|
||||
do_action( 'rank_math/analytics/purge_cache', $table );
|
||||
|
||||
wp_cache_flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get search console table info.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function info() {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! Api::get()->is_console_connected() ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$key = 'rank_math_analytics_data_info';
|
||||
$data = get_transient( $key );
|
||||
if ( false !== $data ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$days = self::analytics()
|
||||
->selectCount( 'DISTINCT(created)', 'days' )
|
||||
->getVar();
|
||||
|
||||
$rows = self::analytics()
|
||||
->selectCount( 'id' )
|
||||
->getVar();
|
||||
|
||||
$size = $wpdb->get_var( "SELECT SUM((data_length + index_length)) AS size FROM information_schema.TABLES WHERE table_schema='" . $wpdb->dbname . "' AND (table_name='" . $wpdb->prefix . "rank_math_analytics_gsc')" ); // phpcs:ignore
|
||||
$data = compact( 'days', 'rows', 'size' );
|
||||
|
||||
$data = apply_filters( 'rank_math/analytics/analytics_tables_info', $data );
|
||||
|
||||
set_transient( $key, $data, DAY_IN_SECONDS );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has data pulled.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function has_data() {
|
||||
static $rank_math_gsc_has_data;
|
||||
if ( isset( $rank_math_gsc_has_data ) ) {
|
||||
return $rank_math_gsc_has_data;
|
||||
}
|
||||
|
||||
$id = self::objects()
|
||||
->select( 'id' )
|
||||
->limit( 1 )
|
||||
->getVar();
|
||||
|
||||
$rank_math_gsc_has_data = $id > 0 ? true : false;
|
||||
return $rank_math_gsc_has_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if console data exists at specified date.
|
||||
*
|
||||
* @param string $date Date to check data existence.
|
||||
* @param string $action Action name to filter data type.
|
||||
* @return boolean
|
||||
*/
|
||||
public static function date_exists( $date, $action = 'console' ) {
|
||||
$tables['console'] = DB_Helper::check_table_exists( 'rank_math_analytics_gsc' ) ? 'rank_math_analytics_gsc' : '';
|
||||
|
||||
/**
|
||||
* Filter: 'rank_math/analytics/date_exists_tables' - Allow developers to add more tables to check.
|
||||
*/
|
||||
$tables = apply_filters( 'rank_math/analytics/date_exists_tables', $tables, $date, $action );
|
||||
|
||||
if ( empty( $tables[ $action ] ) ) {
|
||||
return true; // Should return true to avoid further data fetch action.
|
||||
}
|
||||
|
||||
$table = self::table( $tables[ $action ] );
|
||||
|
||||
$id = $table
|
||||
->select( 'id' )
|
||||
->where( 'DATE(created)', $date )
|
||||
->getVar();
|
||||
|
||||
return $id > 0 ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new record into objects table.
|
||||
*
|
||||
* @param array $args Values to insert.
|
||||
*
|
||||
* @return bool|int
|
||||
*/
|
||||
public static function add_object( $args = [] ) {
|
||||
if ( empty( $args ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset( $args['id'] );
|
||||
|
||||
$args = wp_parse_args(
|
||||
$args,
|
||||
[
|
||||
'created' => current_time( 'mysql' ),
|
||||
'page' => '',
|
||||
'object_type' => 'post',
|
||||
'object_subtype' => 'post',
|
||||
'object_id' => 0,
|
||||
'primary_key' => '',
|
||||
'seo_score' => 0,
|
||||
'page_score' => 0,
|
||||
'is_indexable' => false,
|
||||
'schemas_in_use' => '',
|
||||
]
|
||||
);
|
||||
|
||||
return self::objects()->insert( $args, [ '%s', '%s', '%s', '%s', '%d', '%s', '%d', '%d', '%d', '%s' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new record in the inspections table.
|
||||
*
|
||||
* @param array $args Values to insert.
|
||||
*
|
||||
* @return bool|int
|
||||
*/
|
||||
public static function store_inspection( $args = [] ) {
|
||||
if ( empty( $args ) || empty( $args['page'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset( $args['id'] );
|
||||
|
||||
$defaults = self::get_inspection_defaults();
|
||||
|
||||
// Only keep $args items that are in $defaults.
|
||||
$args = array_intersect_key( $args, $defaults );
|
||||
|
||||
// Apply defaults.
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
// We only have strings: placeholders will be '%s'.
|
||||
$format = array_fill( 0, count( $args ), '%s' );
|
||||
|
||||
// Check if we have an existing record, based on 'page'.
|
||||
$id = self::inspections()
|
||||
->select( 'id' )
|
||||
->where( 'page', $args['page'] )
|
||||
->getVar();
|
||||
|
||||
if ( $id ) {
|
||||
return self::inspections()
|
||||
->set( $args )
|
||||
->where( 'id', $id )
|
||||
->update();
|
||||
}
|
||||
|
||||
return self::inspections()->insert( $args, $format );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inspection defaults.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_inspection_defaults() {
|
||||
$defaults = [
|
||||
'created' => current_time( 'mysql' ),
|
||||
'page' => '',
|
||||
'index_verdict' => 'VERDICT_UNSPECIFIED',
|
||||
'indexing_state' => 'INDEXING_STATE_UNSPECIFIED',
|
||||
'coverage_state' => '',
|
||||
'page_fetch_state' => 'PAGE_FETCH_STATE_UNSPECIFIED',
|
||||
'robots_txt_state' => 'ROBOTS_TXT_STATE_UNSPECIFIED',
|
||||
'mobile_usability_verdict' => 'VERDICT_UNSPECIFIED',
|
||||
'mobile_usability_issues' => '',
|
||||
'rich_results_verdict' => 'VERDICT_UNSPECIFIED',
|
||||
'rich_results_items' => '',
|
||||
'last_crawl_time' => '',
|
||||
'crawled_as' => 'CRAWLING_USER_AGENT_UNSPECIFIED',
|
||||
'google_canonical' => '',
|
||||
'user_canonical' => '',
|
||||
'sitemap' => '',
|
||||
'referring_urls' => '',
|
||||
'raw_api_response' => '',
|
||||
];
|
||||
|
||||
return apply_filters( 'rank_math/analytics/inspection_defaults', $defaults );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/Update a record into/from objects table.
|
||||
*
|
||||
* @param array $args Values to update.
|
||||
*
|
||||
* @return bool|int
|
||||
*/
|
||||
public static function update_object( $args = [] ) {
|
||||
if ( empty( $args ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If object exists, try to update.
|
||||
$old_id = absint( $args['id'] );
|
||||
if ( ! empty( $old_id ) ) {
|
||||
unset( $args['id'] );
|
||||
|
||||
$updated = self::objects()->set( $args )
|
||||
->where( 'id', $old_id )
|
||||
->where( 'object_id', absint( $args['object_id'] ) )
|
||||
->update();
|
||||
|
||||
if ( ! empty( $updated ) ) {
|
||||
return $old_id;
|
||||
}
|
||||
}
|
||||
|
||||
// In case of new object or failed to update, try to add.
|
||||
return self::add_object( $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add console records.
|
||||
*
|
||||
* @param string $date Date of creation.
|
||||
* @param array $rows Data rows to insert.
|
||||
*/
|
||||
public static function add_query_page_bulk( $date, $rows ) {
|
||||
$chunks = array_chunk( $rows, 50 );
|
||||
|
||||
foreach ( $chunks as $chunk ) {
|
||||
self::bulk_insert_query_page_data( $date . ' 00:00:00', $chunk );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk inserts records into a console table using WPDB. All rows must contain the same keys.
|
||||
*
|
||||
* @param string $date Date.
|
||||
* @param array $rows Rows to insert.
|
||||
*/
|
||||
public static function bulk_insert_query_page_data( $date, $rows ) {
|
||||
global $wpdb;
|
||||
|
||||
$data = [];
|
||||
$placeholders = [];
|
||||
$columns = [
|
||||
'created',
|
||||
'query',
|
||||
'page',
|
||||
'clicks',
|
||||
'impressions',
|
||||
'position',
|
||||
'ctr',
|
||||
];
|
||||
$columns = '`' . implode( '`, `', $columns ) . '`';
|
||||
$placeholder = [
|
||||
'%s',
|
||||
'%s',
|
||||
'%s',
|
||||
'%d',
|
||||
'%d',
|
||||
'%d',
|
||||
'%d',
|
||||
];
|
||||
|
||||
// Start building SQL, initialise data and placeholder arrays.
|
||||
$sql = "INSERT INTO `{$wpdb->prefix}rank_math_analytics_gsc` ( $columns ) VALUES\n";
|
||||
|
||||
// Build placeholders for each row, and add values to data array.
|
||||
foreach ( $rows as $row ) {
|
||||
if (
|
||||
$row['position'] > self::get_position_filter() ||
|
||||
Str::contains( '?', $row['page'] )
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data[] = $date;
|
||||
$data[] = $row['query'];
|
||||
$data[] = str_replace( Helper::get_home_url(), '', self::remove_hash( urldecode( $row['page'] ) ) );
|
||||
$data[] = $row['clicks'];
|
||||
$data[] = $row['impressions'];
|
||||
$data[] = $row['position'];
|
||||
$data[] = $row['ctr'];
|
||||
|
||||
$placeholders[] = '(' . implode( ', ', $placeholder ) . ')';
|
||||
}
|
||||
|
||||
// Don't run insert with empty dataset, return 0 since no rows affected.
|
||||
if ( empty( $data ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Stitch all rows together.
|
||||
$sql .= implode( ",\n", $placeholders );
|
||||
|
||||
// Run the query. Returns number of affected rows.
|
||||
return $wpdb->query( $wpdb->prepare( $sql, $data ) ); // phpcs:ignore
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove hash part from Url.
|
||||
*
|
||||
* @param string $url Url to process.
|
||||
* @return string
|
||||
*/
|
||||
public static function remove_hash( $url ) {
|
||||
if ( ! Str::contains( '#', $url ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$url = \explode( '#', $url );
|
||||
return $url[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get position filter.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function get_position_filter() {
|
||||
$number = apply_filters( 'rank_math/analytics/position_limit', false );
|
||||
if ( false === $number ) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all inspections.
|
||||
*
|
||||
* @param array $params REST Parameters.
|
||||
* @param int $per_page Limit.
|
||||
*/
|
||||
public static function get_inspections( $params, $per_page ) {
|
||||
$page = ! empty( $params['page'] ) ? absint( $params['page'] ) : 1;
|
||||
$per_page = absint( $per_page );
|
||||
$offset = ( $page - 1 ) * $per_page;
|
||||
|
||||
$inspections = self::inspections()->table;
|
||||
$objects = self::objects()->table;
|
||||
|
||||
$query = self::inspections()
|
||||
->select( [ "$inspections.*", "$objects.title", "$objects.object_id" ] )
|
||||
->leftJoin( $objects, "$inspections.page", "$objects.page" )
|
||||
->where( "$objects.page", '!=', '' )
|
||||
->orderBy( 'id', 'DESC' )
|
||||
->limit( $per_page, $offset );
|
||||
|
||||
do_action_ref_array( 'rank_math/analytics/get_inspections_query', [ &$query, $params ] );
|
||||
|
||||
$results = $query->get();
|
||||
|
||||
return apply_filters( 'rank_math/analytics/get_inspections_results', $results );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inspections count.
|
||||
*
|
||||
* @param array $params REST Parameters.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function get_inspections_count( $params ) {
|
||||
$pages = self::objects()->select( 'page' )->get( ARRAY_A );
|
||||
$pages = array_unique( wp_list_pluck( $pages, 'page' ) );
|
||||
$query = self::inspections()->selectCount( 'id', 'total' )->whereIn( 'page', $pages );
|
||||
|
||||
do_action_ref_array( 'rank_math/analytics/get_inspections_count_query', [ &$query, $params ] );
|
||||
|
||||
return $query->getVar();
|
||||
}
|
||||
}
|
@@ -0,0 +1,648 @@
|
||||
<?php
|
||||
/**
|
||||
* Analytics Email Reports.
|
||||
*
|
||||
* @since 1.0.68
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use RankMath\KB;
|
||||
use RankMath\Helper;
|
||||
use RankMath\Traits\Hooker;
|
||||
use RankMath\Google\Console;
|
||||
use RankMath\Admin\Admin_Helper;
|
||||
|
||||
use RankMath\Helpers\Param;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Email_Reports class.
|
||||
*/
|
||||
class Email_Reports {
|
||||
|
||||
use Hooker;
|
||||
|
||||
/**
|
||||
* Email content variables.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $variables = [];
|
||||
|
||||
/**
|
||||
* Path to the views directory.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $views_path = '';
|
||||
|
||||
/**
|
||||
* URL to the assets directory.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $assets_url = '';
|
||||
|
||||
/**
|
||||
* Charts Account.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $charts_account = 'rankmath';
|
||||
|
||||
/**
|
||||
* Charts Key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $charts_key = '10042B42-9193-428A-ABA7-5753F3370F84';
|
||||
|
||||
/**
|
||||
* Graph data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $graph_data = [];
|
||||
|
||||
/**
|
||||
* Debug mode.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private $debug = false;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( ! Console::is_console_connected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$directory = dirname( __FILE__ );
|
||||
$this->views_path = $directory . '/views/email-reports/';
|
||||
|
||||
$url = plugin_dir_url( __FILE__ );
|
||||
$this->assets_url = $this->do_filter( 'analytics/email_report_assets_url', $url . 'assets/' );
|
||||
|
||||
$this->hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add filter & action hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function hooks() {
|
||||
$this->action( 'rank_math/analytics/email_report_event', 'email_report' );
|
||||
$this->action( 'template_redirect', 'maybe_debug' );
|
||||
|
||||
$this->action( 'rank_math/analytics/email_report_html', 'replace_variables' );
|
||||
$this->action( 'rank_math/analytics/email_report_html', 'strip_comments' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Analytics report or error message.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function email_report() {
|
||||
$this->setup_variables();
|
||||
$this->send_report();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect variables to be used in the Report template.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setup_variables() {
|
||||
$stats = $this->get_stats();
|
||||
$date = $this->get_date();
|
||||
|
||||
// Translators: placeholder is "rankmath.com" as a link.
|
||||
$footer_text = sprintf( esc_html__( 'This email was sent to you as a registered member of %s.', 'rank-math' ), '<a href="###SITE_URL###">###SITE_URL_SIMPLE###</a>' );
|
||||
$footer_text .= ' ';
|
||||
|
||||
// Translators: placeholder is "click here" as a link.
|
||||
$footer_text .= sprintf( esc_html__( 'To update your email preferences, %s.', 'rank-math' ), '<a href="###SETTINGS_URL###">' . esc_html__( 'click here', 'rank-math' ) . '</a>' );
|
||||
|
||||
$footer_text .= '###ADDRESS###';
|
||||
|
||||
$this->variables = [
|
||||
'site_url' => get_home_url(),
|
||||
'site_url_simple' => explode( '://', get_home_url() )[1],
|
||||
'settings_url' => Helper::get_admin_url( 'options-general#setting-panel-analytics' ),
|
||||
'report_url' => Helper::get_admin_url( 'analytics' ),
|
||||
'assets_url' => $this->assets_url,
|
||||
'address' => '<br/> [rank_math_contact_info show="address"]',
|
||||
'logo_link' => KB::get( 'email-reports-logo', 'Email Report Logo' ),
|
||||
|
||||
'period_days' => $date['period'],
|
||||
'start_date' => $date['start'],
|
||||
'end_date' => $date['end'],
|
||||
|
||||
'stats_clicks' => $stats['clicks'],
|
||||
'stats_clicks_diff' => $stats['clicks_diff'],
|
||||
'stats_traffic' => $stats['traffic'],
|
||||
'stats_traffic_diff' => $stats['traffic_diff'],
|
||||
'stats_impressions' => $stats['impressions'],
|
||||
'stats_impressions_diff' => $stats['impressions_diff'],
|
||||
'stats_keywords' => $stats['keywords'],
|
||||
'stats_keywords_diff' => $stats['keywords_diff'],
|
||||
'stats_position' => $stats['position'],
|
||||
'stats_position_diff' => $stats['position_diff'],
|
||||
'stats_top_3_positions' => $stats['top_3_positions'],
|
||||
'stats_top_3_positions_diff' => $stats['top_3_positions_diff'],
|
||||
'stats_top_10_positions' => $stats['top_10_positions'],
|
||||
'stats_top_10_positions_diff' => $stats['top_10_positions_diff'],
|
||||
'stats_top_50_positions' => $stats['top_50_positions'],
|
||||
'stats_top_50_positions_diff' => $stats['top_50_positions_diff'],
|
||||
'stats_invalid_data' => $stats['invalid_data'],
|
||||
'footer_html' => $footer_text,
|
||||
];
|
||||
|
||||
$this->variables = $this->do_filter( 'analytics/email_report_variables', $this->variables );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_date() {
|
||||
$period = self::get_period_from_frequency();
|
||||
|
||||
// Shift 3 days prior.
|
||||
$subtract = DAY_IN_SECONDS * 3;
|
||||
$start = strtotime( '-' . $period . ' days' ) - $subtract;
|
||||
$end = strtotime( $this->do_filter( 'analytics/report_end_date', 'today' ) ) - $subtract;
|
||||
|
||||
$start = date_i18n( 'd M Y', $start );
|
||||
$end = date_i18n( 'd M Y', $end );
|
||||
return compact( 'start', 'end', 'period' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Analytics stats.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stats() {
|
||||
$period = self::get_period_from_frequency();
|
||||
$stats = Stats::get();
|
||||
$stats->set_date_range( "-{$period} days" );
|
||||
|
||||
// Basic stats.
|
||||
$data = (array) $stats->get_analytics_summary();
|
||||
|
||||
$analytics = get_option( 'rank_math_google_analytic_options' );
|
||||
$is_analytics_connected = ! empty( $analytics ) && ! empty( $analytics['view_id'] );
|
||||
|
||||
$out = [];
|
||||
|
||||
$out['impressions'] = $data['impressions']['total'];
|
||||
$out['impressions_diff'] = $data['impressions']['difference'];
|
||||
|
||||
$out['traffic'] = 0;
|
||||
$out['traffic_diff'] = 0;
|
||||
if ( $is_analytics_connected && defined( 'RANK_MATH_PRO_FILE' ) && isset( $data['pageviews'] ) ) {
|
||||
$out['traffic'] = $data['pageviews']['total'];
|
||||
$out['traffic_diff'] = $data['pageviews']['difference'];
|
||||
}
|
||||
|
||||
$out['clicks'] = 0;
|
||||
$out['clicks_diff'] = 0;
|
||||
if ( ! $is_analytics_connected || ( $is_analytics_connected && ! defined( 'RANK_MATH_PRO_FILE' ) ) ) {
|
||||
$out['clicks'] = $data['clicks']['total'];
|
||||
$out['clicks_diff'] = $data['clicks']['difference'];
|
||||
}
|
||||
|
||||
$out['keywords'] = $data['keywords']['total'];
|
||||
$out['keywords_diff'] = $data['keywords']['difference'];
|
||||
|
||||
$out['position'] = $data['position']['total'];
|
||||
$out['position_diff'] = $data['position']['difference'];
|
||||
|
||||
// Keyword stats.
|
||||
$kw_data = (array) $stats->get_top_keywords();
|
||||
|
||||
$out['top_3_positions'] = $kw_data['top3']['total'];
|
||||
$out['top_3_positions_diff'] = $kw_data['top3']['difference'];
|
||||
|
||||
$out['top_10_positions'] = $kw_data['top10']['total'];
|
||||
$out['top_10_positions_diff'] = $kw_data['top10']['difference'];
|
||||
|
||||
$out['top_50_positions'] = $kw_data['top50']['total'];
|
||||
$out['top_50_positions_diff'] = $kw_data['top50']['difference'];
|
||||
|
||||
$out['invalid_data'] = false;
|
||||
if ( ! count( array_filter( $out ) ) ) {
|
||||
$out['invalid_data'] = true;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date period (days) from the frequency option.
|
||||
*
|
||||
* @param string $frequency Frequency string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_period_from_frequency( $frequency = null ) {
|
||||
$periods = [
|
||||
'monthly' => 30,
|
||||
];
|
||||
|
||||
$periods = apply_filters( 'rank_math/analytics/email_report_periods', $periods );
|
||||
|
||||
if ( empty( $frequency ) ) {
|
||||
$frequency = self::get_setting( 'frequency', 'monthly' );
|
||||
}
|
||||
|
||||
if ( isset( $periods[ $frequency ] ) ) {
|
||||
return absint( $periods[ $frequency ] );
|
||||
}
|
||||
|
||||
return absint( reset( $periods ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send report data.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function send_report() {
|
||||
$account = Admin_Helper::get_registration_data();
|
||||
$report_email = [
|
||||
'to' => $account['email'],
|
||||
'subject' => sprintf(
|
||||
// Translators: placeholder is the site URL.
|
||||
__( 'Rank Math [SEO Report] - %s', 'rank-math' ),
|
||||
explode( '://', get_home_url() )[1]
|
||||
),
|
||||
'message' => $this->get_template( 'report' ),
|
||||
'headers' => 'Content-Type: text/html; charset=UTF-8',
|
||||
];
|
||||
|
||||
/**
|
||||
* Filter: rank_math/analytics/email_report_parameters
|
||||
* Filters the report email parameters.
|
||||
*/
|
||||
$report_email = $this->do_filter( 'analytics/email_report_parameters', $report_email );
|
||||
|
||||
wp_mail(
|
||||
$report_email['to'],
|
||||
$report_email['subject'],
|
||||
$report_email['message'],
|
||||
$report_email['headers']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full HTML template for email.
|
||||
*
|
||||
* @param string $template Template name.
|
||||
* @return string
|
||||
*/
|
||||
private function get_template( $template ) {
|
||||
$file = $this->locate_template( $template );
|
||||
|
||||
/**
|
||||
* Filter template file.
|
||||
*/
|
||||
$file = $this->do_filter( 'analytics/email_report_template', $file, $template );
|
||||
|
||||
if ( ! file_exists( $file ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
include_once $file;
|
||||
$content = ob_get_clean();
|
||||
|
||||
/**
|
||||
* Filter template HTML.
|
||||
*/
|
||||
return $this->do_filter( 'analytics/email_report_html', $content );
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate and include template part.
|
||||
*
|
||||
* @param string $part Template part.
|
||||
* @param array $args Template arguments.
|
||||
* @return mixed
|
||||
*/
|
||||
private function template_part( $part, $args = [] ) {
|
||||
$file = $this->locate_template( $part );
|
||||
|
||||
/**
|
||||
* Filter template part.
|
||||
*/
|
||||
$file = $this->do_filter( 'analytics/email_report_template_part', $file, $part, $args );
|
||||
|
||||
if ( ! file_exists( $file ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
extract( $args, EXTR_SKIP ); // phpcs:ignore
|
||||
include $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace variables in content.
|
||||
*
|
||||
* @param string $content Email content.
|
||||
* @param string $recursion Recursion count, to account for double-encoded variables.
|
||||
* @return string
|
||||
*/
|
||||
public function replace_variables( $content, $recursion = 1 ) {
|
||||
foreach ( $this->variables as $key => $value ) {
|
||||
if ( ! is_scalar( $value ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Variables must be uppercase.
|
||||
$key = mb_strtoupper( $key );
|
||||
|
||||
$content = str_replace( "###$key###", $value, $content );
|
||||
}
|
||||
|
||||
if ( $recursion ) {
|
||||
$recursion--;
|
||||
$content = $this->replace_variables( $content, $recursion );
|
||||
}
|
||||
|
||||
return do_shortcode( $content );
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip HTML & CSS comments.
|
||||
*
|
||||
* @param string $content Email content.
|
||||
* @return string
|
||||
*/
|
||||
public function strip_comments( $content ) {
|
||||
$content = preg_replace( '[(<!--(.*)-->|/\*(.*)\*/)]isU', '', $content );
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init debug mode if requested and allowed.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function maybe_debug() {
|
||||
if ( 1 !== absint( Param::get( 'rank_math_analytics_report_preview' ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! Helper::has_cap( 'analytics' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$send = boolval( Param::get( 'send' ) );
|
||||
$values = boolval( Param::get( 'values', '1' ) );
|
||||
|
||||
$this->debug( $send, $values );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send or output the report email.
|
||||
*
|
||||
* @param boolean $send Send email or output to browser.
|
||||
* @param boolean $values Replace variables with actual values.
|
||||
* @return void
|
||||
*/
|
||||
private function debug( $send = false, $values = true ) {
|
||||
$this->debug = true;
|
||||
|
||||
if ( $values ) {
|
||||
$this->setup_variables();
|
||||
}
|
||||
|
||||
if ( $send ) {
|
||||
// Send it now.
|
||||
$this->send_report();
|
||||
$url = remove_query_arg(
|
||||
[
|
||||
'rank_math_analytics_report_preview',
|
||||
'send',
|
||||
'values',
|
||||
]
|
||||
);
|
||||
Helper::redirect( $url );
|
||||
exit;
|
||||
}
|
||||
|
||||
// Output it to the browser.
|
||||
echo $this->get_template( 'report' ); // phpcs:ignore
|
||||
die();
|
||||
}
|
||||
|
||||
/**
|
||||
* Variable getter, whenever the value is needed in PHP.
|
||||
*
|
||||
* @param string $name Variable name.
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_variable( $name ) {
|
||||
if ( isset( $this->variables[ $name ] ) ) {
|
||||
return $this->variables[ $name ];
|
||||
}
|
||||
|
||||
return "###$name###";
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting getter.
|
||||
*
|
||||
* @param string $option Option name.
|
||||
* @param mixed $default Default value.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_setting( $option, $default = false ) {
|
||||
return Helper::get_settings( 'general.console_email_' . $option, $default );
|
||||
}
|
||||
|
||||
/**
|
||||
* Output image inside the email template.
|
||||
*
|
||||
* @param string $url Image URL.
|
||||
* @param string $width Image width.
|
||||
* @param string $height Image height.
|
||||
* @param string $alt ALT text.
|
||||
* @param array $attr Additional attributes.
|
||||
* @return void
|
||||
*/
|
||||
public function image( $url, $width = 0, $height = 0, $alt = '', $attr = [] ) {
|
||||
$atts = $attr;
|
||||
$atts['border'] = '0';
|
||||
|
||||
if ( ! isset( $atts['src'] ) ) {
|
||||
$atts['src'] = $url;
|
||||
}
|
||||
|
||||
if ( ! isset( $atts['width'] ) && $width ) {
|
||||
$atts['width'] = $width;
|
||||
}
|
||||
|
||||
if ( ! isset( $atts['height'] ) && $height ) {
|
||||
$atts['height'] = $height;
|
||||
}
|
||||
|
||||
if ( ! isset( $atts['alt'] ) ) {
|
||||
$atts['alt'] = $alt;
|
||||
}
|
||||
|
||||
if ( ! isset( $atts['style'] ) ) {
|
||||
$atts['style'] = 'border: 0; outline: none; text-decoration: none; display: inline-block;';
|
||||
}
|
||||
|
||||
if ( substr( $atts['src'], 0, 4 ) !== 'http' && substr( $atts['src'], 0, 3 ) !== '###' ) {
|
||||
$atts['src'] = $this->assets_url . 'img/' . $atts['src'];
|
||||
}
|
||||
|
||||
$atts = $this->do_filter( 'analytics/email_report_image_atts', $atts, $url, $width, $height, $alt, $attr );
|
||||
|
||||
$attributes = '';
|
||||
foreach ( $atts as $name => $value ) {
|
||||
if ( ! empty( $value ) ) {
|
||||
$value = ( 'src' === $name ) ? esc_url_raw( $value ) : esc_attr( $value );
|
||||
$attributes .= ' ' . $name . '="' . $value . '"';
|
||||
}
|
||||
}
|
||||
|
||||
$image = "<img $attributes>";
|
||||
$image = $this->do_filter( 'analytics/email_report_image_html', $image, $url, $width, $height, $alt, $attr );
|
||||
|
||||
echo $image; // phpcs:ignore
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets template path.
|
||||
*
|
||||
* @param string $template_name Template name.
|
||||
* @param bool $return_full_path Return the full path or not.
|
||||
* @return string
|
||||
*/
|
||||
public function locate_template( $template_name, $return_full_path = true ) {
|
||||
$default_paths = [ $this->views_path ];
|
||||
$template_paths = $this->do_filter( 'analytics/email_report_template_paths', $default_paths );
|
||||
|
||||
$paths = array_reverse( $template_paths );
|
||||
$located = '';
|
||||
$path_partial = '';
|
||||
foreach ( $paths as $path ) {
|
||||
if ( file_exists( $full_path = trailingslashit( $path ) . $template_name . '.php' ) ) { // phpcs:ignore
|
||||
$located = $full_path;
|
||||
$path_partial = $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $return_full_path ? $located : $path_partial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all graph data into memory.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function load_graph_data() {
|
||||
$period = self::get_period_from_frequency();
|
||||
$stats = Stats::get();
|
||||
$stats->set_date_range( "-{$period} days" );
|
||||
$this->graph_data = (array) $stats->get_analytics_summary_graph();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data points for graph.
|
||||
*
|
||||
* @param string $chart Chart to get data for.
|
||||
* @return array
|
||||
*/
|
||||
public function get_graph_data( $chart ) {
|
||||
if ( empty( $this->graph_data ) ) {
|
||||
$this->load_graph_data();
|
||||
}
|
||||
|
||||
$data = [];
|
||||
$group = 'merged';
|
||||
$prop = $chart;
|
||||
if ( 'traffic' === $chart ) {
|
||||
$group = 'traffic';
|
||||
$prop = 'pageviews';
|
||||
}
|
||||
|
||||
if ( empty( $this->graph_data[ $group ] ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
foreach ( (array) $this->graph_data[ $group ] as $range_data ) {
|
||||
$range_data = (array) $range_data;
|
||||
if ( isset( $range_data[ $prop ] ) ) {
|
||||
$data[] = $range_data[ $prop ];
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Charts API sign request.
|
||||
*
|
||||
* @param string $query Query.
|
||||
* @param string $code Code.
|
||||
* @return string
|
||||
*/
|
||||
private function charts_api_sign( $query, $code ) {
|
||||
return hash_hmac( 'sha256', $query, $code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate URL for the Charts API image.
|
||||
*
|
||||
* @param array $graph_data Graph data points.
|
||||
* @param int $width Image height.
|
||||
* @param int $height Image width.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function charts_api_url( $graph_data, $width = 192, $height = 102 ) {
|
||||
$params = [
|
||||
'chco' => '80ace7',
|
||||
'chds' => 'a',
|
||||
'chf' => 'bg,s,f7f9fb',
|
||||
'chls' => 4,
|
||||
'chm' => 'B,e2eeff,0,0,0',
|
||||
'chs' => "{$width}x{$height}",
|
||||
'cht' => 'ls',
|
||||
'chd' => 'a:' . join( ',', $graph_data ),
|
||||
'icac' => $this->charts_account,
|
||||
];
|
||||
|
||||
$query_string = urldecode( http_build_query( $params ) );
|
||||
$signature = $this->charts_api_sign( $query_string, $this->charts_key );
|
||||
|
||||
return 'https://charts.rankmath.com/chart?' . $query_string . '&ichm=' . $signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if fields should be hidden.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function are_fields_hidden() {
|
||||
return apply_filters( 'rank_math/analytics/hide_email_report_options', false );
|
||||
}
|
||||
}
|
@@ -0,0 +1,413 @@
|
||||
<?php
|
||||
/**
|
||||
* The GTag
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*
|
||||
* @copyright 2019 Google LLC
|
||||
* The following code is a derivative work of the code from the Site Kit Plugin(https://sitekit.withgoogle.com), which is licensed under Apache License 2.0.
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use RankMath\Helper;
|
||||
use RankMath\Traits\Hooker;
|
||||
use RankMath\Helpers\Str;
|
||||
use AMP_Theme_Support;
|
||||
use AMP_Options_Manager;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* GTag class.
|
||||
*/
|
||||
class GTag {
|
||||
|
||||
use Hooker;
|
||||
|
||||
/**
|
||||
* Primary "standard" AMP website mode.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AMP_MODE_PRIMARY = 'primary';
|
||||
|
||||
/**
|
||||
* Secondary AMP website mode.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AMP_MODE_SECONDARY = 'secondary';
|
||||
|
||||
/**
|
||||
* Options.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $options = null;
|
||||
|
||||
/**
|
||||
* Internal flag set after gtag amp print for the first time.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $did_amp_gtag = false;
|
||||
|
||||
/**
|
||||
* The Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->action( 'template_redirect', 'add_analytics_tag' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add analytics tag.
|
||||
*/
|
||||
public function add_analytics_tag() {
|
||||
// Early Bail!!
|
||||
$use_snippet = $this->get( 'install_code' );
|
||||
if ( ! $use_snippet ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$property_id = $this->get( 'property_id' );
|
||||
if ( ! $property_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->action( 'wp_head', 'print_tracking_opt_out', 0 ); // For non-AMP and AMP.
|
||||
$this->action( 'web_stories_story_head', 'print_tracking_opt_out', 0 ); // For Web Stories plugin.
|
||||
|
||||
if ( $this->is_amp() ) {
|
||||
$this->action( 'amp_print_analytics', 'print_amp_gtag' ); // For all AMP modes.
|
||||
$this->action( 'wp_footer', 'print_amp_gtag', 20 ); // For AMP Standard and Transitional.
|
||||
$this->action( 'amp_post_template_footer', 'print_amp_gtag', 20 ); // For AMP Reader.
|
||||
$this->action( 'web_stories_print_analytics', 'print_amp_gtag' ); // For Web Stories plugin.
|
||||
|
||||
// Load amp-analytics component for AMP Reader.
|
||||
$this->filter( 'amp_post_template_data', 'amp_analytics_component_data' );
|
||||
} else {
|
||||
// For non-AMP. If current WordPress verion is 5.7 or above, use core function introducted from WordPress 5.7 and add async loading to gtag script.
|
||||
if ( version_compare( get_bloginfo( 'version' ), '5.7', '<' ) ) {
|
||||
$this->action( 'wp_enqueue_scripts', 'enqueue_gtag_js' );
|
||||
} else {
|
||||
$this->action( 'wp_head', 'add_gtag_js' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print gtag <amp-analytics> tag.
|
||||
*/
|
||||
public function print_amp_gtag() {
|
||||
if ( $this->did_amp_gtag ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->did_amp_gtag = true;
|
||||
|
||||
$property_id = $this->get( 'property_id' );
|
||||
$gtag_options = [
|
||||
'vars' => [
|
||||
'gtag_id' => $property_id,
|
||||
'config' => [
|
||||
$property_id => [
|
||||
'groups' => 'default',
|
||||
'linker' => [
|
||||
'domains' => [ $this->get_home_domain() ],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'optoutElementId' => '__gaOptOutExtension',
|
||||
];
|
||||
?>
|
||||
<amp-analytics type="gtag" data-credentials="include">
|
||||
<script type="application/json">
|
||||
<?php echo wp_json_encode( $gtag_options ); ?>
|
||||
</script>
|
||||
</amp-analytics>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads AMP analytics script if opted in.
|
||||
*
|
||||
* @param array $data AMP template data.
|
||||
* @return array Filtered $data.
|
||||
*/
|
||||
public function amp_analytics_component_data( $data ) {
|
||||
if ( isset( $data['amp_component_scripts']['amp-analytics'] ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$data['amp_component_scripts']['amp-analytics'] = 'https://cdn.ampproject.org/v0/amp-analytics-0.1.js';
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print gtag snippet for non-amp. Used only for WordPress 5.7 or above.
|
||||
*/
|
||||
public function add_gtag_js() {
|
||||
if ( $this->is_tracking_disabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$gtag_script_info = $this->get_gtag_info();
|
||||
|
||||
wp_print_script_tag(
|
||||
[
|
||||
'id' => 'google_gtagjs',
|
||||
'src' => $gtag_script_info['url'],
|
||||
'async' => true,
|
||||
]
|
||||
);
|
||||
|
||||
wp_print_inline_script_tag(
|
||||
$gtag_script_info['inline'],
|
||||
[
|
||||
'id' => 'google_gtagjs-inline',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print gtag snippet for non-amp. Used for below WordPress 5.7.
|
||||
*/
|
||||
public function enqueue_gtag_js() {
|
||||
if ( $this->is_tracking_disabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$gtag_script_info = $this->get_gtag_info();
|
||||
|
||||
wp_enqueue_script( // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
|
||||
'google_gtagjs',
|
||||
$gtag_script_info['url'],
|
||||
false,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
wp_add_inline_script(
|
||||
'google_gtagjs',
|
||||
$gtag_script_info['inline']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current AMP mode.
|
||||
*
|
||||
* @return bool|string 'primary' if in standard mode,
|
||||
* 'secondary' if in transitional or reader modes
|
||||
* false if AMP not active, or unknown mode
|
||||
*/
|
||||
public function get_amp_mode() {
|
||||
if ( ! class_exists( 'AMP_Theme_Support' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$exposes_support_mode = defined( 'AMP_Theme_Support::STANDARD_MODE_SLUG' )
|
||||
&& defined( 'AMP_Theme_Support::TRANSITIONAL_MODE_SLUG' )
|
||||
&& defined( 'AMP_Theme_Support::READER_MODE_SLUG' );
|
||||
|
||||
if ( defined( 'AMP__VERSION' ) ) {
|
||||
$amp_plugin_version = AMP__VERSION;
|
||||
if ( strpos( $amp_plugin_version, '-' ) !== false ) {
|
||||
$amp_plugin_version = explode( '-', $amp_plugin_version )[0];
|
||||
}
|
||||
|
||||
$amp_plugin_version_2_or_higher = version_compare( $amp_plugin_version, '2.0.0', '>=' );
|
||||
} else {
|
||||
$amp_plugin_version_2_or_higher = false;
|
||||
}
|
||||
|
||||
if ( $amp_plugin_version_2_or_higher ) {
|
||||
$exposes_support_mode = class_exists( 'AMP_Options_Manager' )
|
||||
&& method_exists( 'AMP_Options_Manager', 'get_option' )
|
||||
&& $exposes_support_mode;
|
||||
} else {
|
||||
$exposes_support_mode = class_exists( 'AMP_Theme_Support' )
|
||||
&& method_exists( 'AMP_Theme_Support', 'get_support_mode' )
|
||||
&& $exposes_support_mode;
|
||||
}
|
||||
|
||||
if ( $exposes_support_mode ) {
|
||||
// If recent version, we can properly detect the mode.
|
||||
if ( $amp_plugin_version_2_or_higher ) {
|
||||
$mode = AMP_Options_Manager::get_option( 'theme_support' );
|
||||
} else {
|
||||
$mode = AMP_Theme_Support::get_support_mode();
|
||||
}
|
||||
|
||||
if ( AMP_Theme_Support::STANDARD_MODE_SLUG === $mode ) {
|
||||
return self::AMP_MODE_PRIMARY;
|
||||
}
|
||||
|
||||
if ( in_array( $mode, [ AMP_Theme_Support::TRANSITIONAL_MODE_SLUG, AMP_Theme_Support::READER_MODE_SLUG ], true ) ) {
|
||||
return self::AMP_MODE_SECONDARY;
|
||||
}
|
||||
} elseif ( function_exists( 'amp_is_canonical' ) ) {
|
||||
// On older versions, if it is not primary AMP, it is definitely secondary AMP (transitional or reader mode).
|
||||
if ( amp_is_canonical() ) {
|
||||
return self::AMP_MODE_PRIMARY;
|
||||
}
|
||||
|
||||
return self::AMP_MODE_SECONDARY;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is AMP url.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_amp() {
|
||||
if ( is_singular( 'web-story' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return function_exists( 'is_amp_endpoint' ) && is_amp_endpoint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is tracking disabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_tracking_disabled() {
|
||||
if ( ! $this->get( 'exclude_loggedin' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$logged_in = is_user_logged_in();
|
||||
$filter_match = false;
|
||||
if ( $logged_in ) {
|
||||
if ( ! function_exists( 'get_editable_roles' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/user.php';
|
||||
}
|
||||
|
||||
$all_roles = array_keys( get_editable_roles() );
|
||||
$all_roles = array_combine( $all_roles, $all_roles ); // Copy values to keys for easier filtering.
|
||||
$user_roles = array_flip( get_userdata( get_current_user_id() )->roles );
|
||||
$filter_match = count( array_intersect_key( (array) $this->do_filter( 'analytics/gtag_exclude_loggedin_roles', $all_roles ), $user_roles ) );
|
||||
}
|
||||
|
||||
return $filter_match;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the hostname of the home URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_home_domain() {
|
||||
return wp_parse_url( home_url(), PHP_URL_HOST );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get option
|
||||
*
|
||||
* @param string $id Option to get.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function get( $id ) {
|
||||
if ( is_null( $this->options ) ) {
|
||||
$this->options = $this->normalize_it( get_option( 'rank_math_google_analytic_options', [] ) );
|
||||
}
|
||||
|
||||
$value = isset( $this->options[ $id ] ) ? $this->options[ $id ] : false;
|
||||
if ( $value && 'property_id' === $id && ! Str::starts_with( 'UA-', $value ) ) {
|
||||
$value = $this->get( 'measurement_id' );
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gtag script info
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function get_gtag_info() {
|
||||
// Get Google Analytics Property ID.
|
||||
$property_id = $this->get( 'property_id' );
|
||||
|
||||
// Get main gtag script Url.
|
||||
$url = 'https://www.googletagmanager.com/gtag/js?id=' . esc_attr( $property_id );
|
||||
|
||||
$gtag_opt = [];
|
||||
if ( $this->get_amp_mode() ) {
|
||||
$gtag_opt['linker'] = [
|
||||
'domains' => [ $this->get_home_domain() ],
|
||||
];
|
||||
}
|
||||
|
||||
$gtag_inline_linker_script = '';
|
||||
if ( ! empty( $gtag_opt['linker'] ) ) {
|
||||
$gtag_inline_linker_script = 'gtag(\'set\', \'linker\', ' . wp_json_encode( $gtag_opt['linker'] ) . ' );';
|
||||
}
|
||||
unset( $gtag_opt['linker'] );
|
||||
|
||||
// Get Google Analytics Property ID.
|
||||
$gtag_config = [];
|
||||
$gtag_config = $this->do_filter( 'analytics/gtag_config', $gtag_config );
|
||||
|
||||
// Construct inline scripts.
|
||||
$gtag_inline_script = 'window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}';
|
||||
$gtag_inline_script .= $gtag_inline_linker_script;
|
||||
$gtag_inline_script .= 'gtag(\'js\', new Date());';
|
||||
$gtag_inline_script .= 'gtag(\'config\', \'' . esc_attr( $property_id ) . '\', {' . join( ', ', $gtag_config ) . '} );';
|
||||
|
||||
$gtag = $this->do_filter(
|
||||
'analytics/gtag',
|
||||
[
|
||||
'url' => $url,
|
||||
'inline' => $gtag_inline_script,
|
||||
]
|
||||
);
|
||||
|
||||
return $gtag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize option data
|
||||
*
|
||||
* @param mixed $options Array to normalize.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function normalize_it( $options ) {
|
||||
foreach ( (array) $options as $key => $value ) {
|
||||
$options[ $key ] = is_array( $value ) ? $this->normalize_it( $value ) : Helper::normalize_data( $value );
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the user tracking opt-out code
|
||||
*
|
||||
* This script opts out of all Google Analytics tracking, for all measurement IDs, regardless of implementation.
|
||||
*
|
||||
* @link https://developers.google.com/analytics/devguides/collection/analyticsjs/user-opt-out
|
||||
*/
|
||||
public function print_tracking_opt_out() {
|
||||
if ( ! $this->is_tracking_disabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $this->is_amp() ) :
|
||||
?>
|
||||
<script type="application/ld+json" id="__gaOptOutExtension"></script>
|
||||
<?php else : ?>
|
||||
<script type="text/javascript">window['ga-disable-<?php echo esc_js( $this->get( 'property_id' ) ); ?>'] = true;</script>
|
||||
<?php
|
||||
endif;
|
||||
}
|
||||
}
|
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
/**
|
||||
* The Analytics Module
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use WP_REST_Request;
|
||||
use RankMath\Analytics\Stats;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Keywords class.
|
||||
*/
|
||||
class Keywords extends Posts {
|
||||
|
||||
/**
|
||||
* Get most recent day's keywords.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_recent_keywords() {
|
||||
global $wpdb;
|
||||
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT query
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE DATE(created) = (SELECT MAX(DATE(created)) FROM {$wpdb->prefix}rank_math_analytics_gsc WHERE created BETWEEN %s AND %s)
|
||||
GROUP BY query",
|
||||
Stats::get()->start_date,
|
||||
Stats::get()->end_date
|
||||
);
|
||||
$data = $wpdb->get_results( $query ); // phpcs:ignore
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get keywords data.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_keywords_rows( WP_REST_Request $request ) {
|
||||
// Get most recent day's keywords only.
|
||||
$keywords = $this->get_recent_keywords();
|
||||
$keywords = wp_list_pluck( $keywords, 'query' );
|
||||
$keywords = array_map( 'esc_sql', $keywords );
|
||||
$keywords = array_map( 'mb_strtolower', $keywords );
|
||||
$per_page = 25;
|
||||
|
||||
$cache_args = $request->get_params();
|
||||
$cache_args['per_page'] = $per_page;
|
||||
|
||||
$cache_group = 'rank_math_rest_keywords_rows';
|
||||
$cache_key = $this->generate_hash( $cache_args );
|
||||
$rows = $this->get_cache( $cache_key, $cache_group );
|
||||
if ( empty( $rows ) ) {
|
||||
$rows = $this->get_analytics_data(
|
||||
[
|
||||
'dimension' => 'query',
|
||||
'objects' => false,
|
||||
'pageview' => false,
|
||||
'orderBy' => ! empty( $request->get_param( 'orderby' ) ) ? $request->get_param( 'orderby' ) : 'impressions',
|
||||
'order' => in_array( $request->get_param( 'order' ), [ 'asc', 'desc' ], true ) ? strtoupper( $request->get_param( 'order' ) ) : 'DESC',
|
||||
'offset' => ( $request->get_param( 'page' ) - 1 ) * $per_page,
|
||||
'perpage' => $per_page,
|
||||
'sub_where' => " AND query IN ('" . join( "', '", $keywords ) . "')",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$rows = apply_filters( 'rank_math/analytics/keywords', $rows );
|
||||
if ( empty( $rows ) ) {
|
||||
$rows['response'] = 'No Data';
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top keywords overview filtered by keyword position range.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function get_top_keywords() {
|
||||
global $wpdb;
|
||||
|
||||
$cache_key = $this->get_cache_key( 'top_keywords', $this->days . 'days' );
|
||||
$cache = get_transient( $cache_key );
|
||||
|
||||
if ( false !== $cache ) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
// Get current keywords count filtered by position range.
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT(t1.query) AS total,
|
||||
CASE
|
||||
WHEN t1.position BETWEEN 1 AND 3 THEN 'top3'
|
||||
WHEN t1.position BETWEEN 4 AND 10 THEN 'top10'
|
||||
WHEN t1.position BETWEEN 11 AND 50 THEN 'top50'
|
||||
WHEN t1.position BETWEEN 51 AND 100 THEN 'top100'
|
||||
ELSE 'none'
|
||||
END AS position_type
|
||||
FROM (SELECT query, ROUND( AVG(position), 0 ) AS position
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE created BETWEEN %s AND %s AND DATE(created) = (SELECT MAX(DATE(created)) FROM {$wpdb->prefix}rank_math_analytics_gsc WHERE created BETWEEN %s AND %s)
|
||||
GROUP BY query
|
||||
ORDER BY position) as t1
|
||||
GROUP BY position_type",
|
||||
$this->start_date,
|
||||
$this->end_date,
|
||||
$this->start_date,
|
||||
$this->end_date
|
||||
);
|
||||
$data = $wpdb->get_results( $query ); // phpcs:ignore
|
||||
|
||||
// Get compare keywords count filtered by position range.
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT(t1.query) AS total,
|
||||
CASE
|
||||
WHEN t1.position BETWEEN 1 AND 3 THEN 'top3'
|
||||
WHEN t1.position BETWEEN 4 AND 10 THEN 'top10'
|
||||
WHEN t1.position BETWEEN 11 AND 50 THEN 'top50'
|
||||
WHEN t1.position BETWEEN 51 AND 100 THEN 'top100'
|
||||
ELSE 'none'
|
||||
END AS position_type
|
||||
FROM (SELECT query, ROUND( AVG(position), 0 ) AS position
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE created BETWEEN %s AND %s AND DATE(created) = (SELECT MAX(DATE(created)) FROM {$wpdb->prefix}rank_math_analytics_gsc WHERE created BETWEEN %s AND %s)
|
||||
GROUP BY query
|
||||
ORDER BY position) as t1
|
||||
GROUP BY position_type",
|
||||
$this->compare_start_date,
|
||||
$this->compare_end_date,
|
||||
$this->compare_start_date,
|
||||
$this->compare_end_date
|
||||
);
|
||||
$compare = $wpdb->get_results( $query ); // phpcs:ignore
|
||||
|
||||
$positions = [
|
||||
'top3' => [
|
||||
'total' => 0,
|
||||
'difference' => 0,
|
||||
],
|
||||
'top10' => [
|
||||
'total' => 0,
|
||||
'difference' => 0,
|
||||
],
|
||||
'top50' => [
|
||||
'total' => 0,
|
||||
'difference' => 0,
|
||||
],
|
||||
'top100' => [
|
||||
'total' => 0,
|
||||
'difference' => 0,
|
||||
],
|
||||
'ctr' => 0,
|
||||
'ctrDifference' => 0,
|
||||
];
|
||||
|
||||
// Calculate total and difference for each position range.
|
||||
$positions = $this->get_top_position_total( $positions, $data, 'total' );
|
||||
$positions = $this->get_top_position_total( $positions, $compare, 'difference' );
|
||||
|
||||
// Get CTR.
|
||||
$positions['ctr'] = DB::analytics()
|
||||
->selectAvg( 'ctr', 'ctr' )
|
||||
->whereBetween( 'created', [ $this->start_date, $this->end_date ] )
|
||||
->getVar();
|
||||
|
||||
// Get compare CTR.
|
||||
$positions['ctrDifference'] = DB::analytics()
|
||||
->selectAvg( 'ctr', 'ctr' )
|
||||
->whereBetween( 'created', [ $this->compare_start_date, $this->compare_end_date ] )
|
||||
->getVar();
|
||||
|
||||
// Calculate current CTR and CTR difference.
|
||||
$positions['ctr'] = empty( $positions['ctr'] ) ? 0 : $positions['ctr'];
|
||||
$positions['ctrDifference'] = empty( $positions['ctrDifference'] ) ? 0 : $positions['ctrDifference'];
|
||||
$positions['ctrDifference'] = $positions['ctr'] - $positions['ctrDifference'];
|
||||
|
||||
set_transient( $cache_key, $positions, DAY_IN_SECONDS );
|
||||
|
||||
return $positions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get position graph
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_top_position_graph() {
|
||||
global $wpdb;
|
||||
|
||||
$cache_key = $this->get_cache_key( 'top_keywords_graph', $this->days . 'days' );
|
||||
$cache = get_transient( $cache_key );
|
||||
|
||||
if ( false !== $cache ) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
// Step1. Get splitted date intervals for graph within selected date range.
|
||||
$intervals = $this->get_intervals();
|
||||
$sql_daterange = $this->get_sql_date_intervals( $intervals );
|
||||
|
||||
// Step2. Get most recent days for each splitted date intervals.
|
||||
// phpcs:disable
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT MAX(DATE(created)) as date, {$sql_daterange}
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE created BETWEEN %s AND %s
|
||||
GROUP BY range_group",
|
||||
$this->start_date,
|
||||
$this->end_date
|
||||
);
|
||||
$position_dates = $wpdb->get_results( $query, ARRAY_A );
|
||||
// phpcs:enable
|
||||
|
||||
if ( count( $position_dates ) === 0 ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$dates = [];
|
||||
foreach ( $position_dates as $row ) {
|
||||
array_push( $dates, $row['date'] );
|
||||
}
|
||||
$dates = '(\'' . join( '\', \'', $dates ) . '\')';
|
||||
|
||||
// Step3. Get keywords count filtered by position range group for each date.
|
||||
// phpcs:disable
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT(t.query) AS total, t.date,
|
||||
CASE
|
||||
WHEN t.position BETWEEN 1 AND 3 THEN 'top3'
|
||||
WHEN t.position BETWEEN 4 AND 10 THEN 'top10'
|
||||
WHEN t.position BETWEEN 11 AND 50 THEN 'top50'
|
||||
WHEN t.position BETWEEN 51 AND 100 THEN 'top100'
|
||||
ELSE 'none'
|
||||
END AS position_type
|
||||
FROM (
|
||||
SELECT query, ROUND( AVG(position), 0 ) AS position, Date(created) as date
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE created BETWEEN %s AND %s AND DATE(created) IN {$dates}
|
||||
GROUP BY DATE(created), query) AS t
|
||||
GROUP BY t.date, position_type",
|
||||
$this->start_date,
|
||||
$this->end_date
|
||||
);
|
||||
$position_data = $wpdb->get_results( $query );
|
||||
// phpcs:enable
|
||||
|
||||
// Construct return data.
|
||||
$data = $this->get_date_array(
|
||||
$intervals['dates'],
|
||||
[
|
||||
'top3' => 0,
|
||||
'top10' => 0,
|
||||
'top50' => 0,
|
||||
'top100' => 0,
|
||||
]
|
||||
);
|
||||
|
||||
foreach ( $position_data as $row ) {
|
||||
if ( ! isset( $intervals['map'][ $row->date ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$date = $intervals['map'][ $row->date ];
|
||||
|
||||
if ( ! isset( $data[ $date ][ $row->position_type ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $row->position_type;
|
||||
|
||||
$data[ $date ][ $key ] = $row->total;
|
||||
}
|
||||
|
||||
$data = array_values( $data );
|
||||
set_transient( $cache_key, $data, DAY_IN_SECONDS );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top position total.
|
||||
*
|
||||
* @param array $positions Position array.
|
||||
* @param array $rows Data to process.
|
||||
* @param string $where What data to get total.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_top_position_total( $positions, $rows, $where ) {
|
||||
foreach ( $rows as $row ) {
|
||||
$positions[ $row->position_type ][ $where ] = $row->total;
|
||||
}
|
||||
|
||||
if ( 'difference' === $where ) {
|
||||
$positions['top3']['difference'] = $positions['top3']['total'] - $positions['top3']['difference'];
|
||||
$positions['top10']['difference'] = $positions['top10']['total'] - $positions['top10']['difference'];
|
||||
$positions['top50']['difference'] = $positions['top50']['total'] - $positions['top50']['difference'];
|
||||
$positions['top100']['difference'] = $positions['top100']['total'] - $positions['top100']['difference'];
|
||||
}
|
||||
|
||||
return $positions;
|
||||
}
|
||||
}
|
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/**
|
||||
* The Analytics Module
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Objects class.
|
||||
*/
|
||||
class Objects extends Summary {
|
||||
|
||||
/**
|
||||
* Get objects for pages.
|
||||
*
|
||||
* @param array $pages Array of urls.
|
||||
* @return array
|
||||
*/
|
||||
public function get_objects( $pages ) {
|
||||
if ( empty( $pages ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$pages = DB::objects()
|
||||
->whereIn( 'page', \array_unique( $pages ) )
|
||||
->where( 'is_indexable', 1 )
|
||||
->get( ARRAY_A );
|
||||
|
||||
return $this->set_page_as_key( $pages );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get objects by seo score range filter.
|
||||
*
|
||||
* @param WP_REST_Request $request Filters.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_objects_by_score( $request ) {
|
||||
global $wpdb;
|
||||
|
||||
$orderby = in_array( $request->get_param( 'orderby' ), [ 'title', 'seo_score', 'created' ], true ) ? $request->get_param( 'orderby' ) : 'created';
|
||||
$order = in_array( $request->get_param( 'order' ), [ 'asc', 'desc' ], true ) ? strtoupper( $request->get_param( 'order' ) ) : 'DESC';
|
||||
$post_type = sanitize_key( $request->get_param( 'postType' ) );
|
||||
// Construct filters from request parameters.
|
||||
$filters = [
|
||||
'good' => $request->get_param( 'good' ),
|
||||
'ok' => $request->get_param( 'ok' ),
|
||||
'bad' => $request->get_param( 'bad' ),
|
||||
'noData' => $request->get_param( 'noData' ),
|
||||
];
|
||||
$field_name = 'seo_score';
|
||||
$per_page = $request->get_param( 'per_page' ) ? sanitize_text_field( $request->get_param( 'per_page' ) ) : 25;
|
||||
$offset = ( sanitize_text_field( $request->get_param( 'page' ) ) - 1 ) * $per_page;
|
||||
|
||||
// Construct SQL condition based on filter parameters.
|
||||
$conditions = [];
|
||||
if ( $filters['good'] ) {
|
||||
$conditions[] = "{$field_name} BETWEEN 81 AND 100";
|
||||
}
|
||||
|
||||
if ( $filters['ok'] ) {
|
||||
$conditions[] = "{$field_name} BETWEEN 51 AND 80";
|
||||
}
|
||||
|
||||
if ( $filters['bad'] ) {
|
||||
$conditions[] = "{$field_name} BETWEEN 1 AND 50";
|
||||
}
|
||||
|
||||
if ( $filters['noData'] ) {
|
||||
$conditions[] = "{$field_name} = 0";
|
||||
}
|
||||
|
||||
$subwhere = '';
|
||||
if ( count( $conditions ) > 0 ) {
|
||||
$subwhere = implode( ' OR ', $conditions );
|
||||
$subwhere = " AND ({$subwhere})";
|
||||
}
|
||||
|
||||
if ( $post_type ) {
|
||||
$subwhere = $subwhere . ' AND object_subtype = "' . $post_type . '"';
|
||||
}
|
||||
|
||||
// Get filtered objects data limited by page param.
|
||||
// phpcs:disable
|
||||
$pages = $wpdb->get_results(
|
||||
"SELECT * FROM {$wpdb->prefix}rank_math_analytics_objects
|
||||
WHERE is_indexable = 1
|
||||
{$subwhere}
|
||||
ORDER BY {$orderby} {$order}
|
||||
LIMIT {$offset} , {$per_page}",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
// Get total filtered objects count.
|
||||
$total_rows = $wpdb->get_var(
|
||||
"SELECT count(*) FROM {$wpdb->prefix}rank_math_analytics_objects
|
||||
WHERE is_indexable = 1
|
||||
{$subwhere}
|
||||
ORDER BY created DESC"
|
||||
);
|
||||
// phpcs:enable
|
||||
|
||||
return [
|
||||
'rows' => $this->set_page_as_key( $pages ),
|
||||
'rowsFound' => $total_rows,
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/**
|
||||
* The Analytics Module
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use stdClass;
|
||||
use WP_Error;
|
||||
use WP_REST_Request;
|
||||
use RankMath\Helper;
|
||||
use RankMath\Analytics\DB;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Posts class.
|
||||
*/
|
||||
class Posts extends Objects {
|
||||
|
||||
/**
|
||||
* Get post data.
|
||||
*
|
||||
* @param WP_REST_Request $request post object.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function get_post( $request ) {
|
||||
$id = $request->get_param( 'id' );
|
||||
$post = DB::objects()
|
||||
->where( 'object_id', $id )
|
||||
->one();
|
||||
|
||||
if ( is_null( $post ) ) {
|
||||
return [ 'errorMessage' => esc_html__( 'Sorry, no post found for given id.', 'rank-math' ) ];
|
||||
}
|
||||
|
||||
$post->admin_url = admin_url();
|
||||
$post->home_url = home_url();
|
||||
|
||||
return apply_filters( 'rank_math/analytics/post_data', (array) $post, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get posts by objects.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_posts_rows_by_objects( WP_REST_Request $request ) {
|
||||
$pre = apply_filters( 'rank_math/analytics/get_posts_rows_by_objects', false, $request );
|
||||
if ( false !== $pre ) {
|
||||
return $pre;
|
||||
}
|
||||
|
||||
$cache_group = 'rank_math_posts_rows_by_objects';
|
||||
$cache_key = $this->generate_hash( $request );
|
||||
$data = $this->get_cache( $cache_key, $cache_group );
|
||||
if ( false !== $data ) {
|
||||
return rest_ensure_response( $data );
|
||||
}
|
||||
|
||||
// Pagination.
|
||||
$per_page = 25;
|
||||
$offset = ( $request->get_param( 'page' ) - 1 ) * $per_page;
|
||||
|
||||
// Get objects filtered by seo score range and it's analytics data.
|
||||
$objects = $this->get_objects_by_score( $request );
|
||||
$pages = \array_keys( $objects['rows'] );
|
||||
$console = $this->get_analytics_data(
|
||||
[
|
||||
'offset' => 0, // Here offset should always zero.
|
||||
'perpage' => $objects['rowsFound'],
|
||||
'sub_where' => " AND page IN ('" . join( "', '", $pages ) . "')",
|
||||
]
|
||||
);
|
||||
|
||||
// Construct return data.
|
||||
$new_rows = [];
|
||||
foreach ( $objects['rows'] as $object ) {
|
||||
$page = $object['page'];
|
||||
|
||||
if ( isset( $console[ $page ] ) ) {
|
||||
$object = \array_merge( $console[ $page ], $object );
|
||||
}
|
||||
|
||||
if ( ! isset( $object['links'] ) ) {
|
||||
$object['links'] = new stdClass();
|
||||
}
|
||||
|
||||
$new_rows[ $page ] = $object;
|
||||
}
|
||||
|
||||
$count = count( $new_rows );
|
||||
|
||||
if ( $offset + 25 <= $count ) {
|
||||
$new_rows = array_slice( $new_rows, $offset, 25 );
|
||||
|
||||
} else {
|
||||
$rest = $count - $offset;
|
||||
$new_rows = array_slice( $new_rows, $offset, $rest );
|
||||
}
|
||||
if ( empty( $new_rows ) ) {
|
||||
$new_rows['response'] = 'No Data';
|
||||
}
|
||||
|
||||
$output = [
|
||||
'rows' => $new_rows,
|
||||
'rowsFound' => $objects['rowsFound'],
|
||||
];
|
||||
|
||||
$this->set_cache( $cache_key, $output, $cache_group, DAY_IN_SECONDS );
|
||||
|
||||
return rest_ensure_response( $output );
|
||||
}
|
||||
}
|
@@ -0,0 +1,993 @@
|
||||
<?php
|
||||
/**
|
||||
* The Analytics Module
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use RankMath\Helper;
|
||||
use RankMath\Traits\Hooker;
|
||||
use RankMath\Helpers\Param;
|
||||
use RankMathPro\Analytics\Pageviews;
|
||||
use RankMath\Google\Console as Google_Analytics;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Stats class.
|
||||
*/
|
||||
class Stats extends Keywords {
|
||||
|
||||
use Hooker;
|
||||
|
||||
/**
|
||||
* Start timestamp.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $start = 0;
|
||||
|
||||
/**
|
||||
* End timestamp.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $end = 0;
|
||||
|
||||
/**
|
||||
* Start date.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $start_date = '';
|
||||
|
||||
/**
|
||||
* End date.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $end_date = '';
|
||||
|
||||
/**
|
||||
* Compare Start date.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $compare_start_date = '';
|
||||
|
||||
/**
|
||||
* Compare End date.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $compare_end_date = '';
|
||||
|
||||
/**
|
||||
* Number of days.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $days = 0;
|
||||
|
||||
/**
|
||||
* Main instance
|
||||
*
|
||||
* Ensure only one instance is loaded or can be loaded.
|
||||
*
|
||||
* @return Stats
|
||||
*/
|
||||
public static function get() {
|
||||
static $instance;
|
||||
|
||||
if ( is_null( $instance ) && ! ( $instance instanceof Stats ) ) {
|
||||
$instance = new Stats();
|
||||
$instance->set_date_range();
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set date range.
|
||||
*
|
||||
* @param string $range Range of days.
|
||||
*/
|
||||
public function set_date_range( $range = false ) {
|
||||
// Shift 3 days prior.
|
||||
$subtract = DAY_IN_SECONDS * 3;
|
||||
$end = strtotime( $this->do_filter( 'analytics/end_date', 'today' ) ) - $subtract;
|
||||
$start = strtotime( false !== $range ? $range : $this->get_date_from_cookie( 'date_range', '-30 days' ), $end ) - $subtract;
|
||||
|
||||
// Timestamp.
|
||||
$this->end = Helper::get_midnight( $end );
|
||||
$this->start = Helper::get_midnight( $start );
|
||||
|
||||
// Period.
|
||||
$this->end_date = Helper::get_date( 'Y-m-d 23:59:59', $end, false, true );
|
||||
$this->start_date = Helper::get_date( 'Y-m-d 00:00:00', $start, false, true );
|
||||
|
||||
// Compare date.
|
||||
$this->days = ceil( abs( $end - $start ) / DAY_IN_SECONDS );
|
||||
$this->compare_end_date = $start - DAY_IN_SECONDS;
|
||||
$this->compare_start_date = $this->compare_end_date - ( $this->days * DAY_IN_SECONDS );
|
||||
$this->compare_end_date = Helper::get_date( 'Y-m-d 23:59:59', $this->compare_end_date, false, true );
|
||||
$this->compare_start_date = Helper::get_date( 'Y-m-d 00:00:00', $this->compare_start_date, false, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date intervals for graph.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_intervals() {
|
||||
$range = $this->get_date_from_cookie( 'date_range', '-30 days' );
|
||||
$interval = [
|
||||
'-7 days' => '0 days',
|
||||
'-15 days' => '-3 days',
|
||||
'-30 days' => '-6 days',
|
||||
'-3 months' => '-6 days',
|
||||
'-6 months' => '-30 days',
|
||||
'-1 year' => '-30 days',
|
||||
];
|
||||
|
||||
$ticks = [
|
||||
'-7 days' => 7,
|
||||
'-15 days' => 5,
|
||||
'-30 days' => 5,
|
||||
'-3 months' => 13,
|
||||
'-6 months' => 6,
|
||||
'-1 year' => 12,
|
||||
];
|
||||
|
||||
$addition = [
|
||||
'-7 days' => 0,
|
||||
'-15 days' => DAY_IN_SECONDS,
|
||||
'-30 days' => DAY_IN_SECONDS,
|
||||
'-3 months' => -DAY_IN_SECONDS / 6,
|
||||
'-6 months' => DAY_IN_SECONDS / 2,
|
||||
'-1 year' => 0,
|
||||
];
|
||||
|
||||
$ticks = $ticks[ $range ];
|
||||
$interval = $interval[ $range ];
|
||||
$addition = $addition[ $range ];
|
||||
|
||||
$map = [];
|
||||
$dates = [];
|
||||
|
||||
$end = $this->end;
|
||||
$start = strtotime( $interval, $end );
|
||||
|
||||
for ( $i = 0; $i < $ticks; $i++ ) {
|
||||
$end_date = Helper::get_date( 'Y-m-d', $end, false, true );
|
||||
$start_date = Helper::get_date( 'Y-m-d', $start, false, true );
|
||||
|
||||
$dates[ $end_date ] = [
|
||||
'start' => $start_date,
|
||||
'end' => $end_date,
|
||||
'formatted_date' => Helper::get_date( 'd M, Y', $end ),
|
||||
'formatted_period' => Helper::get_date( 'd M', $start ) . ' - ' . Helper::get_date( 'd M, Y', $end ),
|
||||
];
|
||||
|
||||
$map[ $start_date ] = $end_date;
|
||||
for ( $j = 1; $j < 32; $j++ ) {
|
||||
$date = Helper::get_date( 'Y-m-d', strtotime( $j . ' days', $start ), false, true );
|
||||
if ( $start_date === $end_date ) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $date === $end_date ) {
|
||||
break;
|
||||
}
|
||||
|
||||
$map[ $date ] = $end_date;
|
||||
}
|
||||
$map[ $end_date ] = $end_date;
|
||||
|
||||
$end = \strtotime( '-1 days', $start );
|
||||
$start = \strtotime( $interval, $end + $addition );
|
||||
}
|
||||
return [
|
||||
'map' => $map,
|
||||
'dates' => \array_reverse( $dates ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date intervals for SQL query.
|
||||
*
|
||||
* @param array $intervals Date Intervals.
|
||||
* @param string $column Column name to check.
|
||||
* @param string $newcolumn Column name to return.
|
||||
* @return string
|
||||
*/
|
||||
public function get_sql_date_intervals( $intervals, $column = 'created', $newcolumn = 'range_group' ) {
|
||||
$sql_parts = [];
|
||||
array_push( $sql_parts, 'CASE' );
|
||||
|
||||
$index = 1;
|
||||
foreach ( $intervals['dates'] as $date_range ) {
|
||||
$start_date = $date_range['start'] . ' 00:00:00';
|
||||
$end_date = $date_range['end'] . ' 23:59:59';
|
||||
|
||||
array_push( $sql_parts, sprintf( "WHEN %s BETWEEN '%s' AND '%s' THEN 'range%d'", $column, $start_date, $end_date, $index ) );
|
||||
|
||||
$index ++;
|
||||
}
|
||||
|
||||
array_push( $sql_parts, "ELSE 'none'" );
|
||||
array_push( $sql_parts, sprintf( "END AS '%s'", $newcolumn ) );
|
||||
|
||||
return implode( ' ', $sql_parts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date array
|
||||
*
|
||||
* @param array $dates Dates.
|
||||
* @param array $default Default value.
|
||||
* @return array
|
||||
*/
|
||||
public function get_date_array( $dates, $default ) {
|
||||
$data = [];
|
||||
foreach ( $dates as $date => $d ) {
|
||||
$data[ $date ] = $default;
|
||||
$data[ $date ]['date'] = $date;
|
||||
$data[ $date ]['dateFormatted'] = $d['start'] === $d['end'] ? $d['formatted_date'] : $d['formatted_period'];
|
||||
$data[ $date ]['formattedDate'] = $d['formatted_date'];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert data to proper type.
|
||||
*
|
||||
* @param array $row Row to normalize.
|
||||
* @return array
|
||||
*/
|
||||
public function normalize_graph_rows( $row ) {
|
||||
foreach ( $row as $col => $val ) {
|
||||
if ( in_array( $col, [ 'query', 'page', 'date', 'created', 'dateFormatted' ], true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( in_array( $col, [ 'ctr', 'position', 'earnings' ], true ) ) {
|
||||
$row->$col = round( $row->$col, 0 );
|
||||
continue;
|
||||
}
|
||||
|
||||
$row->$col = absint( $row->$col );
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove uncessary graph rows.
|
||||
*
|
||||
* @param array $rows Rows to filter.
|
||||
* @return array
|
||||
*/
|
||||
public function filter_graph_rows( $rows ) {
|
||||
foreach ( $rows as $key => $row ) {
|
||||
if ( isset( $row->range_group ) && 'none' === $row->range_group ) {
|
||||
unset( $rows[ $key ] );
|
||||
}
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract proper data.
|
||||
*
|
||||
* @param array $rows Data rows.
|
||||
* @param string $column Column name contains mixed data.
|
||||
* @param string $sep Separator for mixed data.
|
||||
* @param array $keys Column array to extract.
|
||||
* @return array
|
||||
*/
|
||||
public function extract_data_from_mixed( $rows, $column, $sep, $keys ) {
|
||||
foreach ( $rows as $index => &$row ) {
|
||||
if ( ! isset( $row->$column ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mixed = explode( $sep, $row->$column );
|
||||
$mixed_count = count( $mixed );
|
||||
if ( ! $mixed_count ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( $keys as $key_idx => $key ) {
|
||||
if ( 'position' === $key ) {
|
||||
// Should subtract the position value from 100. The position data was inverted before call this function.
|
||||
$value = 100 - (int) $mixed[ $mixed_count - $key_idx - 1 ];
|
||||
} else {
|
||||
$value = $mixed[ $mixed_count - $key_idx - 1 ];
|
||||
}
|
||||
$row->$key = $value;
|
||||
}
|
||||
|
||||
unset( $row->$column );
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two metrics array into one
|
||||
*
|
||||
* @param array $metrics_rows1 Metrics Rows to merge.
|
||||
* @param array $metrics_rows2 Metrics Rows to merge.
|
||||
* @param boolean $has_traffic Flag to include/exclude traffic data.
|
||||
* @return array
|
||||
*/
|
||||
public function get_merged_metrics( $metrics_rows1, $metrics_rows2, $has_traffic = false ) {
|
||||
$data = [];
|
||||
|
||||
// Construct base data array.
|
||||
$base_array = [
|
||||
'position' => 0,
|
||||
'diffPosition' => 0,
|
||||
'clicks' => 0,
|
||||
'diffClicks' => 0,
|
||||
'impressions' => 0,
|
||||
'diffImpressions' => 0,
|
||||
'ctr' => 0,
|
||||
'diffCtr' => 0,
|
||||
];
|
||||
|
||||
if ( $has_traffic ) {
|
||||
$base_array['pageviews'] = 0;
|
||||
$base_array['difference'] = 0;
|
||||
}
|
||||
|
||||
// Merge first array and second array into base array.
|
||||
foreach ( $metrics_rows1 as $key => $row ) {
|
||||
if ( isset( $metrics_rows2[ $key ] ) ) {
|
||||
if ( is_object( $row ) ) {
|
||||
$data[ $key ] = (object) array_merge( $base_array, (array) $row, (array) $metrics_rows2[ $key ] );
|
||||
} else {
|
||||
$data[ $key ] = array_merge( $base_array, $row, $metrics_rows2[ $key ] );
|
||||
}
|
||||
unset( $metrics_rows2[ $key ] );
|
||||
} else {
|
||||
$data[ $key ] = array_merge( $base_array, $row );
|
||||
}
|
||||
}
|
||||
|
||||
// Merge remaining items from second array into base array.
|
||||
foreach ( $metrics_rows2 as $key => $row ) {
|
||||
if ( is_object( $row ) ) {
|
||||
$metrics_rows2[ $key ] = (object) array_merge( $base_array, (array) $row );
|
||||
} else {
|
||||
$metrics_rows2[ $key ] = array_merge( $base_array, $row );
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge( $data, $metrics_rows2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge data graph by date.
|
||||
*
|
||||
* @param array $rows Rows to merge.
|
||||
* @param array $data Data array.
|
||||
* @param array $map Interval map.
|
||||
* @return array
|
||||
*/
|
||||
public function get_merge_data_graph( $rows, $data, $map ) {
|
||||
foreach ( $rows as $row ) {
|
||||
if ( ! isset( $map[ $row->date ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$date = $map[ $row->date ];
|
||||
foreach ( $row as $key => $value ) {
|
||||
if ( 'date' === $key || 'created' === $key ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// trick to invert Position Graph YAxis.
|
||||
if ( 'position' === $key ) {
|
||||
$value = 0 - $value;
|
||||
}
|
||||
$data[ $date ][ $key ][] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat graph data.
|
||||
*
|
||||
* @param array $rows Graph data.
|
||||
* @return array
|
||||
*/
|
||||
public function get_graph_data_flat( $rows ) {
|
||||
foreach ( $rows as &$row ) {
|
||||
if ( isset( $row['clicks'] ) ) {
|
||||
$row['clicks'] = \array_sum( $row['clicks'] );
|
||||
}
|
||||
|
||||
if ( isset( $row['impressions'] ) ) {
|
||||
$row['impressions'] = \array_sum( $row['impressions'] );
|
||||
}
|
||||
|
||||
if ( isset( $row['earnings'] ) ) {
|
||||
$row['earnings'] = \array_sum( $row['earnings'] );
|
||||
}
|
||||
|
||||
if ( isset( $row['pageviews'] ) ) {
|
||||
$row['pageviews'] = \array_sum( $row['pageviews'] );
|
||||
}
|
||||
|
||||
if ( isset( $row['ctr'] ) ) {
|
||||
$row['ctr'] = empty( $row['ctr'] ) ? 0 : ceil( array_sum( $row['ctr'] ) / count( $row['ctr'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $row['position'] ) ) {
|
||||
if ( empty( $row['position'] ) ) {
|
||||
unset( $row['position'] );
|
||||
} else {
|
||||
$row['position'] = ceil( array_sum( $row['position'] ) / count( $row['position'] ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $row['keywords'] ) ) {
|
||||
$row['keywords'] = empty( $row['keywords'] ) ? 0 : ceil( array_sum( $row['keywords'] ) / count( $row['keywords'] ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filter data.
|
||||
*
|
||||
* @param string $filter Filter key.
|
||||
* @param string $default Filter default value.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_date_from_cookie( $filter, $default ) {
|
||||
$cookie_key = 'rank_math_analytics_' . $filter;
|
||||
$new_value = sanitize_title( Param::post( $filter ) );
|
||||
if ( $new_value ) {
|
||||
setcookie( $cookie_key, $new_value, time() + ( HOUR_IN_SECONDS * 30 ), COOKIEPATH, COOKIE_DOMAIN, false, true );
|
||||
return $new_value;
|
||||
}
|
||||
|
||||
if ( ! empty( $_COOKIE[ $cookie_key ] ) ) {
|
||||
return $_COOKIE[ $cookie_key ];
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics data.
|
||||
*
|
||||
* @param array $args Array of arguments.
|
||||
* @return array
|
||||
*/
|
||||
public function get_analytics_data( $args = [] ) {
|
||||
global $wpdb;
|
||||
|
||||
$args = wp_parse_args(
|
||||
$args,
|
||||
[
|
||||
'dimension' => 'page',
|
||||
'order' => 'DESC',
|
||||
'orderBy' => 'diffPosition',
|
||||
'objects' => false,
|
||||
'pageview' => false,
|
||||
'where' => '',
|
||||
'sub_where' => '',
|
||||
'pages' => [],
|
||||
'type' => '',
|
||||
'offset' => 0,
|
||||
'perpage' => 5,
|
||||
]
|
||||
);
|
||||
|
||||
$dimension = $args['dimension'];
|
||||
$type = $args['type'];
|
||||
$offset = $args['offset'];
|
||||
$perpage = $args['perpage'];
|
||||
$order_by_field = $args['orderBy'];
|
||||
$sub_where = $args['sub_where'];
|
||||
|
||||
$order_position_fields = [ 'position', 'diffPosition' ];
|
||||
$order_metrics_fields = [ 'clicks', 'diffClicks', 'impressions', 'diffImpressions', 'ctr', 'diffCtr' ];
|
||||
|
||||
if ( in_array( $order_by_field, $order_position_fields, true ) ) {
|
||||
// In case order by position related fields, get position data first.
|
||||
$positions = $this->get_position_data_by_dimension( $args );
|
||||
|
||||
// Filter position data by condition.
|
||||
$positions = $this->filter_analytics_data( $positions, $args );
|
||||
|
||||
// Get dimension list from above result.
|
||||
$dimensions = wp_list_pluck( $positions, $dimension );
|
||||
$dimensions = array_map( 'esc_sql', $dimensions );
|
||||
|
||||
// Get metrics data based on above dimension list.
|
||||
$metrics = $this->get_metrics_data_by_dimension(
|
||||
[
|
||||
'dimension' => $dimension,
|
||||
'sub_where' => ' AND ' . $dimension . " IN ('" . join( "', '", $dimensions ) . "')",
|
||||
]
|
||||
);
|
||||
|
||||
// Merge above two data into one.
|
||||
$rows = $this->get_merged_metrics( $positions, $metrics, true );
|
||||
|
||||
} elseif ( in_array( $order_by_field, $order_metrics_fields, true ) ) {
|
||||
// In case order by fields which are not related with position, get metrics data first.
|
||||
$metrics = $this->get_metrics_data_by_dimension( $args );
|
||||
|
||||
// Filter metrics data by condition.
|
||||
$metrics = $this->filter_analytics_data( $metrics, $args );
|
||||
|
||||
// Get dimension list from above result.
|
||||
$dimensions = wp_list_pluck( $metrics, $dimension );
|
||||
$dimensions = array_map( 'esc_sql', $dimensions );
|
||||
|
||||
// Get position data based on above dimension list.
|
||||
$positions = $this->get_position_data_by_dimension(
|
||||
[
|
||||
'dimension' => $dimension,
|
||||
'sub_where' => ' AND ' . $dimension . " IN ('" . join( "', '", $dimensions ) . "') " . $sub_where,
|
||||
]
|
||||
);
|
||||
|
||||
// Merge above two data into one.
|
||||
$rows = $this->get_merged_metrics( $metrics, $positions, true );
|
||||
} else {
|
||||
// Get position data and other metrics data separately.
|
||||
$positions = $this->get_position_data_by_dimension( $args );
|
||||
$metrics = $this->get_metrics_data_by_dimension( $args );
|
||||
|
||||
// Merge above two data into one.
|
||||
$rows = $this->get_merged_metrics( $positions, $metrics, true );
|
||||
|
||||
// Filter array by condition.
|
||||
$rows = $this->filter_analytics_data( $rows, $args );
|
||||
}
|
||||
|
||||
$page_urls = \array_merge( \array_keys( $rows ), $args['pages'] );
|
||||
|
||||
$pageviews = [];
|
||||
if ( \class_exists( 'RankMathPro\Analytics\Pageviews' ) && $args['pageview'] && ! empty( $page_urls ) ) {
|
||||
$pageviews = Pageviews::get_pageviews( [ 'pages' => $page_urls ] );
|
||||
$pageviews = $pageviews['rows'];
|
||||
}
|
||||
|
||||
if ( $args['objects'] ) {
|
||||
$objects = $this->get_objects( $page_urls );
|
||||
}
|
||||
foreach ( $rows as $page => $row ) {
|
||||
$rows[ $page ]['pageviews'] = [
|
||||
'total' => 0,
|
||||
'difference' => 0,
|
||||
];
|
||||
|
||||
$rows[ $page ]['clicks'] = [
|
||||
'total' => (int) $rows[ $page ]['clicks'],
|
||||
'difference' => (int) $rows[ $page ]['diffClicks'],
|
||||
];
|
||||
|
||||
$rows[ $page ]['impressions'] = [
|
||||
'total' => (int) $rows[ $page ]['impressions'],
|
||||
'difference' => (int) $rows[ $page ]['diffImpressions'],
|
||||
];
|
||||
|
||||
$rows[ $page ]['position'] = [
|
||||
'total' => (float) $rows[ $page ]['position'],
|
||||
'difference' => (float) $rows[ $page ]['diffPosition'],
|
||||
];
|
||||
|
||||
$rows[ $page ]['ctr'] = [
|
||||
'total' => (float) $rows[ $page ]['ctr'],
|
||||
'difference' => (float) $rows[ $page ]['diffCtr'],
|
||||
];
|
||||
|
||||
unset(
|
||||
$rows[ $page ]['diffClicks'],
|
||||
$rows[ $page ]['diffImpressions'],
|
||||
$rows[ $page ]['diffPosition'],
|
||||
$rows[ $page ]['diffCtr'],
|
||||
$rows[ $page ]['difference']
|
||||
);
|
||||
}
|
||||
|
||||
if ( $args['pageview'] && ! empty( $pageviews ) ) {
|
||||
foreach ( $pageviews as $pageview ) {
|
||||
$page = $pageview['page'];
|
||||
if ( ! isset( $rows[ $page ] ) ) {
|
||||
$rows[ $page ] = [];
|
||||
}
|
||||
|
||||
$rows[ $page ]['pageviews'] = [
|
||||
'total' => (int) $pageview['pageviews'],
|
||||
'difference' => (int) $pageview['difference'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ( $args['objects'] && ! empty( $objects ) ) {
|
||||
foreach ( $objects as $object ) {
|
||||
$page = $object['page'];
|
||||
if ( ! isset( $rows[ $page ] ) ) {
|
||||
$rows[ $page ] = [];
|
||||
}
|
||||
$rows[ $page ] = array_merge( $rows[ $page ], $object );
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get position data.
|
||||
*
|
||||
* @param array $args Argument array.
|
||||
* @return array
|
||||
*/
|
||||
public function get_position_data_by_dimension( $args = [] ) {
|
||||
global $wpdb;
|
||||
|
||||
$args = wp_parse_args(
|
||||
$args,
|
||||
[
|
||||
'dimension' => 'page',
|
||||
'where' => '',
|
||||
'sub_where' => '',
|
||||
]
|
||||
);
|
||||
|
||||
$dimension = $args['dimension'];
|
||||
$where = $args['where'];
|
||||
$sub_where = $args['sub_where'];
|
||||
|
||||
if ( 'page' === $dimension ) {
|
||||
// In case dimension is set as 'page', position data for each page will be top position of last ranked date.
|
||||
// That is, among all the position value from the last date of the page, the top position(smallest position value) value will be the result.
|
||||
|
||||
// Get current position data.
|
||||
// phpcs:disable
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT {$dimension}, MAX(CONCAT({$dimension}, ':', DATE(created), ':', LPAD((100 - position), 3, '0'))) as uid
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE created BETWEEN %s AND %s {$sub_where}
|
||||
GROUP BY {$dimension}",
|
||||
$this->start_date,
|
||||
$this->end_date
|
||||
);
|
||||
$positions = $wpdb->get_results( $query );
|
||||
|
||||
// Get old position data.
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT {$dimension}, MAX(CONCAT({$dimension}, ':', DATE(created), ':', LPAD((100 - position), 3, '0'))) as uid
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE created BETWEEN %s AND %s
|
||||
GROUP BY {$dimension}",
|
||||
$this->compare_start_date,
|
||||
$this->compare_end_date
|
||||
);
|
||||
$old_positions = $wpdb->get_results( $query );
|
||||
// phpcs:enable
|
||||
|
||||
// Extract proper position data.
|
||||
$positions = $this->extract_data_from_mixed( $positions, 'uid', ':', [ 'position', 'date' ] );
|
||||
$old_positions = $this->extract_data_from_mixed( $old_positions, 'uid', ':', [ 'position', 'date' ] );
|
||||
|
||||
// Set 'page' as key.
|
||||
$positions = $this->set_dimension_as_key( $positions, $dimension );
|
||||
$old_positions = $this->set_dimension_as_key( $old_positions, $dimension );
|
||||
|
||||
// Calculate position difference, merge old into current position data array.
|
||||
foreach ( $positions as $page => &$row ) {
|
||||
$row = (array) $row; // force to convert as array.
|
||||
if ( ! isset( $old_positions[ $page ] ) ) {
|
||||
$old_position_value = 100; // Should set as 100 here to get correct position difference.
|
||||
} else {
|
||||
$old_position_value = $old_positions[ $page ]->position;
|
||||
}
|
||||
|
||||
$row['diffPosition'] = $row['position'] - $old_position_value;
|
||||
}
|
||||
} else {
|
||||
// In case dimension is not 'page', position data for each dimension will be most recent position value.
|
||||
|
||||
// Step1. Get most recent row id for each dimension for current data.
|
||||
// phpcs:disable
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT t1.id as id
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc t1
|
||||
INNER JOIN (
|
||||
SELECT query, MAX(created) as latest_created
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE created BETWEEN %s AND %s {$sub_where} GROUP BY {$dimension}
|
||||
) t2 ON t1.query = t2.query AND t1.created = t2.latest_created",
|
||||
$this->start_date,
|
||||
$this->end_date
|
||||
);
|
||||
$ids = $wpdb->get_results( $query );
|
||||
// phpcs:enable
|
||||
|
||||
// Step2. Get id list from above result.
|
||||
$ids = wp_list_pluck( $ids, 'id' );
|
||||
$ids_where = " AND id IN ('" . join( "', '", $ids ) . "')";
|
||||
|
||||
// Step3. Get most recent row id for each dimension for compare data.
|
||||
// phpcs:disable
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT t1.id as id
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc t1
|
||||
INNER JOIN (
|
||||
SELECT query, MAX(created) as latest_created
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE created BETWEEN %s AND %s {$sub_where} GROUP BY {$dimension}
|
||||
) t2 ON t1.query = t2.query AND t1.created = t2.latest_created",
|
||||
$this->compare_start_date,
|
||||
$this->compare_end_date
|
||||
);
|
||||
$old_ids = $wpdb->get_results( $query );
|
||||
// phpcs:enable
|
||||
|
||||
// Step4. Get id list from above result.
|
||||
$old_ids = wp_list_pluck( $old_ids, 'id' );
|
||||
$old_ids_where = " AND id IN ('" . join( "', '", $old_ids ) . "')";
|
||||
|
||||
// Step5. Get position and difference data based on above id list.
|
||||
// phpcs:disable
|
||||
$positions = $wpdb->get_results(
|
||||
"SELECT
|
||||
t1.{$dimension} as {$dimension}, ROUND( t1.position, 0 ) as position,
|
||||
COALESCE( ROUND( t1.position - COALESCE( t2.position, 100 ), 0 ), 0 ) as diffPosition
|
||||
FROM
|
||||
( SELECT a.{$dimension}, a.position FROM {$wpdb->prefix}rank_math_analytics_gsc AS a WHERE 1 = 1{$ids_where}) AS t1
|
||||
LEFT JOIN
|
||||
( SELECT a.{$dimension}, a.position FROM {$wpdb->prefix}rank_math_analytics_gsc AS a WHERE 1 = 1{$old_ids_where}) AS t2
|
||||
ON t1.{$dimension} = t2.{$dimension}
|
||||
{$where}",
|
||||
ARRAY_A
|
||||
);
|
||||
// phpcs:enable
|
||||
|
||||
$positions = $this->set_dimension_as_key( $positions, $dimension );
|
||||
}
|
||||
|
||||
return $positions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metrics data.
|
||||
*
|
||||
* @param array $args Argument array.
|
||||
* @return array
|
||||
*/
|
||||
public function get_metrics_data_by_dimension( $args = [] ) {
|
||||
global $wpdb;
|
||||
Helper::enable_big_selects_for_queries();
|
||||
$args = wp_parse_args(
|
||||
$args,
|
||||
[
|
||||
'dimension' => 'page',
|
||||
'sub_where' => '',
|
||||
]
|
||||
);
|
||||
|
||||
$dimension = $args['dimension'];
|
||||
$sub_where = $args['sub_where'];
|
||||
|
||||
// Get metrics data like impressions, click, ctr, etc.
|
||||
// phpcs:disable
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
t1.{$dimension} as {$dimension}, t1.clicks, t1.impressions, t1.ctr,
|
||||
COALESCE( t1.clicks - t2.clicks, 0 ) as diffClicks,
|
||||
COALESCE( t1.impressions - t2.impressions, 0 ) as diffImpressions,
|
||||
COALESCE( t1.ctr - t2.ctr, 0 ) as diffCtr
|
||||
FROM
|
||||
( SELECT {$dimension}, SUM( clicks ) as clicks, SUM(impressions) as impressions, AVG(ctr) as ctr
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE 1 = 1 AND created BETWEEN %s AND %s {$sub_where}
|
||||
GROUP BY {$dimension}) as t1
|
||||
LEFT JOIN
|
||||
( SELECT {$dimension}, SUM( clicks ) as clicks, SUM(impressions) as impressions, AVG(ctr) as ctr
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE 1 = 1 AND created BETWEEN %s AND %s {$sub_where}
|
||||
GROUP BY {$dimension}) as t2
|
||||
ON t1.{$dimension} = t2.{$dimension}",
|
||||
$this->start_date,
|
||||
$this->end_date,
|
||||
$this->compare_start_date,
|
||||
$this->compare_end_date
|
||||
);
|
||||
$metrics = $wpdb->get_results( $query, ARRAY_A );
|
||||
// phpcs:enable
|
||||
|
||||
$metrics = $this->set_dimension_as_key( $metrics, $dimension );
|
||||
|
||||
return $metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter analytics data.
|
||||
*
|
||||
* @param array $data Data to process.
|
||||
* @param array $args Argument array.
|
||||
* @return array
|
||||
*/
|
||||
public function filter_analytics_data( $data, $args ) {
|
||||
$dimension = $args['dimension'];
|
||||
$offset = $args['offset'];
|
||||
$perpage = $args['perpage'];
|
||||
$order_by_field = $args['orderBy'];
|
||||
|
||||
/**
|
||||
* Short-circuit to filter the data.
|
||||
*/
|
||||
$pre = $this->do_filter( 'analytics/pre_filter_data', null, $data, $args );
|
||||
if ( is_array( $pre ) ) {
|
||||
return $pre;
|
||||
}
|
||||
|
||||
// Sort array by $args['order'], $order_by_field value.
|
||||
if ( ! empty( $args['order'] ) ) {
|
||||
$sort_base_arr = array_column( $data, $order_by_field, $dimension );
|
||||
array_multisort( $sort_base_arr, 'ASC' === $args['order'] ? SORT_ASC : SORT_DESC, $data );
|
||||
}
|
||||
|
||||
// Filter array by $offset, $perpage value.
|
||||
$data = array_slice( $data, $offset, $perpage, true );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set page as key.
|
||||
*
|
||||
* @param array $data Rows to process.
|
||||
* @return array
|
||||
*/
|
||||
public function set_page_as_key( $data ) {
|
||||
$rows = [];
|
||||
foreach ( $data as $row ) {
|
||||
$page = $this->get_relative_url( $row['page'] );
|
||||
if ( ! empty( $row['object_id'] ) && empty( $row['schemas_in_use'] ) ) {
|
||||
$row['schemas_in_use'] = Helper::get_default_schema_type( $row['object_id'], true, true );
|
||||
}
|
||||
$rows[ $page ] = $row;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set dimension parameter as key.
|
||||
*
|
||||
* @param array $data Rows to process.
|
||||
* @param string $dimension Dimension to set as key.
|
||||
* @return array
|
||||
*/
|
||||
public function set_dimension_as_key( $data, $dimension = 'query' ) {
|
||||
$rows = [];
|
||||
foreach ( $data as $row ) {
|
||||
if ( is_object( $row ) ) {
|
||||
$value = $row->$dimension;
|
||||
} else {
|
||||
$value = $row[ $dimension ];
|
||||
}
|
||||
$key = 'page' === $dimension ? $this->get_relative_url( $value ) : strtolower( $value );
|
||||
$rows[ $key ] = $row;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set query position history.
|
||||
*
|
||||
* @param array $data Rows to process.
|
||||
* @param array $history Rows to process.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function set_query_position( $data, $history ) {
|
||||
foreach ( $history as $row ) {
|
||||
$key = strtolower( $row->query );
|
||||
|
||||
$data[ $key ]['query'] = isset( $data[ $key ]['query'] ) ? $data[ $key ]['query'] : $key;
|
||||
$data[ $key ]['graph'] = isset( $data[ $key ]['graph'] ) ? $data[ $key ]['graph'] : [];
|
||||
|
||||
if ( ! isset( $row->formatted_date ) ) {
|
||||
$formatted_date = Helper::get_date( 'd M, Y', strtotime( $row->date ) );
|
||||
$row->formatted_date = $formatted_date;
|
||||
}
|
||||
|
||||
$data[ $row->query ]['graph'][] = $row;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set page position history.
|
||||
*
|
||||
* @param array $data Rows to process.
|
||||
* @param array $history Rows to process.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function set_page_position_graph( $data, $history ) {
|
||||
foreach ( $history as $row ) {
|
||||
$data[ $row->page ]['graph'] = isset( $data[ $row->page ]['graph'] ) ? $data[ $row->page ]['graph'] : [];
|
||||
|
||||
if ( ! isset( $row->formatted_date ) ) {
|
||||
$formatted_date = Helper::get_date( 'd M, Y', strtotime( $row->date ) );
|
||||
$row->formatted_date = $formatted_date;
|
||||
}
|
||||
$data[ $row->page ]['graph'][] = $row;
|
||||
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Cache Keys.
|
||||
*
|
||||
* @param string $what What for you need the key.
|
||||
* @param mixed $args more salt to add into key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_cache_key( $what, $args = [] ) {
|
||||
$key = 'rank_math_' . $what;
|
||||
|
||||
if ( ! empty( $args ) ) {
|
||||
$key .= '_' . join( '_', (array) $args );
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relative url.
|
||||
*
|
||||
* @param string $url Url to make relative.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_relative_url( $url ) {
|
||||
$home_url = Google_Analytics::get_site_url();
|
||||
|
||||
// On multisite and sub-directory setup replace the home url.
|
||||
if ( is_multisite() && ! is_subdomain_install() ) {
|
||||
$url = \str_replace( $home_url, '/', $url );
|
||||
} else {
|
||||
$domain = strtolower( wp_parse_url( $home_url, PHP_URL_HOST ) );
|
||||
$domain = str_replace( [ 'www.', '.' ], [ '', '\.' ], $domain );
|
||||
$regex = "/http[s]?:\/\/(www\.)?$domain/mU";
|
||||
$url = strtolower( trim( $url ) );
|
||||
$url = preg_replace( $regex, '', $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Google API and get_permalink sends URL Encoded strings so we need
|
||||
* to urldecode in order to get them to match with whats saved in DB.
|
||||
*/
|
||||
$url = urldecode( $url );
|
||||
return \str_replace( $home_url, '', $url );
|
||||
}
|
||||
}
|
@@ -0,0 +1,432 @@
|
||||
<?php
|
||||
/**
|
||||
* The Analytics Module
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use RankMath\Traits\Cache;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Summary class.
|
||||
*/
|
||||
class Summary {
|
||||
|
||||
use Cache;
|
||||
|
||||
/**
|
||||
* Start date.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $start_date;
|
||||
|
||||
/**
|
||||
* End date.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $end_date;
|
||||
|
||||
/**
|
||||
* Compare start date.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $compare_start_date;
|
||||
|
||||
/**
|
||||
* Compare end date.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $compare_end_date;
|
||||
|
||||
/**
|
||||
* Days.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $days;
|
||||
|
||||
/**
|
||||
* Get Widget.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function get_widget() {
|
||||
global $wpdb;
|
||||
|
||||
$cache_key = Stats::get()->get_cache_key( 'dashboard_stats_widget' );
|
||||
$cache = get_transient( $cache_key );
|
||||
|
||||
if ( false !== $cache ) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
$stats = DB::analytics()
|
||||
->selectSum( 'impressions', 'impressions' )
|
||||
->selectSum( 'clicks', 'clicks' )
|
||||
->selectAvg( 'position', 'position' )
|
||||
->whereBetween( 'created', [ Stats::get()->start_date, Stats::get()->end_date ] )
|
||||
->one();
|
||||
|
||||
$old_stats = DB::analytics()
|
||||
->selectSum( 'impressions', 'impressions' )
|
||||
->selectSum( 'clicks', 'clicks' )
|
||||
->selectAvg( 'position', 'position' )
|
||||
->whereBetween( 'created', [ Stats::get()->compare_start_date, Stats::get()->compare_end_date ] )
|
||||
->one();
|
||||
|
||||
if ( is_null( $stats ) ) {
|
||||
$stats = (object) [
|
||||
'clicks' => 0,
|
||||
'impressions' => 0,
|
||||
'position' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
if ( is_null( $old_stats ) ) {
|
||||
$old_stats = $stats;
|
||||
}
|
||||
|
||||
$stats->clicks = [
|
||||
'total' => (int) $stats->clicks,
|
||||
'previous' => (int) $old_stats->clicks,
|
||||
'difference' => $stats->clicks - $old_stats->clicks,
|
||||
];
|
||||
|
||||
$stats->impressions = [
|
||||
'total' => (int) $stats->impressions,
|
||||
'previous' => (int) $old_stats->impressions,
|
||||
'difference' => $stats->impressions - $old_stats->impressions,
|
||||
];
|
||||
|
||||
$stats->position = [
|
||||
'total' => (float) \number_format( $stats->position, 2 ),
|
||||
'previous' => (float) \number_format( $old_stats->position, 2 ),
|
||||
'difference' => (float) \number_format( $stats->position - $old_stats->position, 2 ),
|
||||
];
|
||||
|
||||
$stats->keywords = $this->get_keywords_summary();
|
||||
|
||||
$stats = apply_filters( 'rank_math/analytics/get_widget', $stats );
|
||||
|
||||
set_transient( $cache_key, $stats, DAY_IN_SECONDS * Stats::get()->days );
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Optimization stats.
|
||||
*
|
||||
* @param string $post_type Selected Post Type.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function get_optimization_summary( $post_type = '' ) {
|
||||
global $wpdb;
|
||||
|
||||
$cache_group = 'rank_math_optimization_summary';
|
||||
$cache_key = $this->generate_hash( $post_type );
|
||||
$cache = $this->get_cache( $cache_key, $cache_group );
|
||||
if ( false !== $cache ) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
$stats = (object) [
|
||||
'good' => 0,
|
||||
'ok' => 0,
|
||||
'bad' => 0,
|
||||
'noData' => 0,
|
||||
'total' => 0,
|
||||
'average' => 0,
|
||||
];
|
||||
|
||||
$object_type_sql = $post_type ? ' AND object_subtype = "' . $post_type . '"' : '';
|
||||
$data = $wpdb->get_results(
|
||||
"SELECT COUNT(object_id) AS count,
|
||||
CASE
|
||||
WHEN seo_score BETWEEN 81 AND 100 THEN 'good'
|
||||
WHEN seo_score BETWEEN 51 AND 80 THEN 'ok'
|
||||
WHEN seo_score BETWEEN 1 AND 50 THEN 'bad'
|
||||
WHEN seo_score = 0 THEN 'noData'
|
||||
ELSE 'none'
|
||||
END AS type
|
||||
FROM {$wpdb->prefix}rank_math_analytics_objects
|
||||
WHERE is_indexable = 1
|
||||
{$object_type_sql}
|
||||
GROUP BY type"
|
||||
);
|
||||
|
||||
$total = 0;
|
||||
foreach ( $data as $row ) {
|
||||
$total += (int) $row->count;
|
||||
$stats->{$row->type} = (int) $row->count;
|
||||
}
|
||||
$stats->total = $total;
|
||||
$stats->average = 0;
|
||||
|
||||
// Average.
|
||||
$query = DB::objects()
|
||||
->selectCount( 'object_id', 'total' )
|
||||
->where( 'is_indexable', 1 )
|
||||
->selectSum( 'seo_score', 'score' );
|
||||
if ( $object_type_sql ) {
|
||||
$query->where( 'object_subtype', $post_type );
|
||||
}
|
||||
|
||||
$average = $query->one();
|
||||
$average->total += property_exists( $stats, 'noData' ) ? $stats->noData : 0; // phpcs:ignore
|
||||
if ( $average->total > 0 ) {
|
||||
$stats->average = \round( $average->score / $average->total, 2 );
|
||||
}
|
||||
|
||||
$this->set_cache( $cache_key, $stats, $cache_group, DAY_IN_SECONDS );
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics summary.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function get_analytics_summary() {
|
||||
$args = [
|
||||
'start_date' => $this->start_date,
|
||||
'end_date' => $this->end_date,
|
||||
'compare_start_date' => $this->compare_start_date,
|
||||
'compare_end_date' => $this->compare_end_date,
|
||||
];
|
||||
|
||||
$cache_group = 'rank_math_analytics_summary';
|
||||
$cache_key = $this->generate_hash( $args );
|
||||
$cache = $this->get_cache( $cache_key, $cache_group );
|
||||
if ( false !== $cache ) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
$stats = DB::analytics()
|
||||
->selectCount( 'DISTINCT(page)', 'posts' )
|
||||
->selectSum( 'impressions', 'impressions' )
|
||||
->selectSum( 'clicks', 'clicks' )
|
||||
->selectAvg( 'position', 'position' )
|
||||
->whereBetween( 'created', [ $this->start_date, $this->end_date ] )
|
||||
->one();
|
||||
// Check validation.
|
||||
$stats->clicks = empty( $stats->clicks ) ? 0 : $stats->clicks;
|
||||
$stats->impressions = empty( $stats->impressions ) ? 0 : $stats->impressions;
|
||||
$stats->position = empty( $stats->position ) ? 0 : $stats->position;
|
||||
|
||||
$old_stats = DB::analytics()
|
||||
->selectCount( 'DISTINCT(page)', 'posts' )
|
||||
->selectSum( 'impressions', 'impressions' )
|
||||
->selectSum( 'clicks', 'clicks' )
|
||||
->selectAvg( 'position', 'position' )
|
||||
->whereBetween( 'created', [ $this->compare_start_date, $this->compare_end_date ] )
|
||||
->one();
|
||||
|
||||
// Check validation.
|
||||
$old_stats->clicks = empty( $old_stats->clicks ) ? 0 : $old_stats->clicks;
|
||||
$old_stats->impressions = empty( $old_stats->impressions ) ? 0 : $old_stats->impressions;
|
||||
$old_stats->position = empty( $old_stats->position ) ? 0 : $old_stats->position;
|
||||
|
||||
$total_ctr = 0 !== $stats->impressions ? round( ( $stats->clicks / $stats->impressions ) * 100, 2 ) : 0;
|
||||
$previous_ctr = 0 !== $old_stats->impressions ? round( ( $old_stats->clicks / $old_stats->impressions ) * 100, 2 ) : 0;
|
||||
$stats->ctr = [
|
||||
'total' => $total_ctr,
|
||||
'previous' => $previous_ctr,
|
||||
'difference' => $total_ctr - $previous_ctr,
|
||||
];
|
||||
|
||||
$stats->clicks = [
|
||||
'total' => (int) $stats->clicks,
|
||||
'previous' => (int) $old_stats->clicks,
|
||||
'difference' => $stats->clicks - $old_stats->clicks,
|
||||
];
|
||||
|
||||
$stats->impressions = [
|
||||
'total' => (int) $stats->impressions,
|
||||
'previous' => (int) $old_stats->impressions,
|
||||
'difference' => $stats->impressions - $old_stats->impressions,
|
||||
];
|
||||
|
||||
$stats->position = [
|
||||
'total' => (float) \number_format( $stats->position, 2 ),
|
||||
'previous' => (float) \number_format( $old_stats->position, 2 ),
|
||||
'difference' => (float) \number_format( $stats->position - $old_stats->position, 2 ),
|
||||
];
|
||||
$stats->keywords = $this->get_keywords_summary();
|
||||
$stats->graph = $this->get_analytics_summary_graph();
|
||||
|
||||
$stats = apply_filters( 'rank_math/analytics/summary', $stats );
|
||||
|
||||
$stats = array_filter( (array) $stats );
|
||||
|
||||
$this->set_cache( $cache_key, $stats, $cache_group, DAY_IN_SECONDS );
|
||||
|
||||
return $stats;
|
||||
}
|
||||
/**
|
||||
* Get posts summary.
|
||||
*
|
||||
* @param string $post_type Selected Post Type.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function get_posts_summary( $post_type = '' ) {
|
||||
$cache_key = $this->get_cache_key( 'posts_summary', $this->days . 'days' );
|
||||
$cache = ! $post_type ? get_transient( $cache_key ) : false;
|
||||
|
||||
if ( false !== $cache ) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$query = DB::analytics()
|
||||
->selectCount( 'DISTINCT(' . $wpdb->prefix . 'rank_math_analytics_gsc.page)', 'posts' )
|
||||
->selectSum( 'impressions', 'impressions' )
|
||||
->selectSum( 'clicks', 'clicks' )
|
||||
->selectAvg( 'ctr', 'ctr' )
|
||||
->whereBetween( $wpdb->prefix . 'rank_math_analytics_gsc.created', [ $this->start_date, $this->end_date ] );
|
||||
$summary = $query->one();
|
||||
$summary = apply_filters( 'rank_math/analytics/posts_summary', $summary, $post_type, $query );
|
||||
$summary = wp_parse_args(
|
||||
array_filter( (array) $summary ),
|
||||
[
|
||||
'ctr' => 0,
|
||||
'posts' => 0,
|
||||
'clicks' => 0,
|
||||
'pageviews' => 0,
|
||||
'impressions' => 0,
|
||||
]
|
||||
);
|
||||
|
||||
set_transient( $cache_key, $summary, DAY_IN_SECONDS );
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get keywords summary.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_keywords_summary() {
|
||||
global $wpdb;
|
||||
|
||||
// Get Total Keywords Counts.
|
||||
$keywords_count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT(query))
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE created BETWEEN %s AND %s",
|
||||
$this->start_date,
|
||||
$this->end_date
|
||||
)
|
||||
);
|
||||
|
||||
$old_keywords_count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT(query))
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE created BETWEEN %s AND %s",
|
||||
$this->compare_start_date,
|
||||
$this->compare_end_date
|
||||
)
|
||||
);
|
||||
|
||||
$keywords = [
|
||||
'total' => (int) $keywords_count,
|
||||
'previous' => (int) $old_keywords_count,
|
||||
'difference' => (int) $keywords_count - (int) $old_keywords_count,
|
||||
];
|
||||
|
||||
return $keywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics graph data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_analytics_summary_graph() {
|
||||
global $wpdb;
|
||||
|
||||
$data = new \stdClass();
|
||||
|
||||
// Step1. Get splitted date intervals for graph within selected date range.
|
||||
$intervals = $this->get_intervals();
|
||||
$sql_daterange = $this->get_sql_date_intervals( $intervals );
|
||||
|
||||
// Step2. Get current analytics data by splitted date intervals.
|
||||
// phpcs:disable
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT DATE_FORMAT( created, '%%Y-%%m-%%d') as date, SUM(clicks) as clicks, SUM(impressions) as impressions, AVG(position) as position, AVG(ctr) as ctr, {$sql_daterange}
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE created BETWEEN %s AND %s
|
||||
GROUP BY range_group",
|
||||
$this->start_date,
|
||||
$this->end_date
|
||||
);
|
||||
$analytics = $wpdb->get_results( $query );
|
||||
$analytics = $this->set_dimension_as_key( $analytics, 'range_group' );
|
||||
// phpcs:enable
|
||||
|
||||
// Step2. Get current keyword data by splitted date intervals. Keyword count should be calculated as total count of most recent date for each splitted date intervals.
|
||||
// phpcs:disable
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT t.range_group, MAX(CONCAT(t.range_group, ':', t.date, ':', t.keywords )) as mixed FROM
|
||||
(SELECT COUNT(DISTINCT(query)) as keywords, Date(created) as date, {$sql_daterange}
|
||||
FROM {$wpdb->prefix}rank_math_analytics_gsc
|
||||
WHERE created BETWEEN %s AND %s
|
||||
GROUP BY range_group, Date(created)) AS t
|
||||
GROUP BY t.range_group",
|
||||
$this->start_date,
|
||||
$this->end_date
|
||||
);
|
||||
$keywords = $wpdb->get_results( $query );
|
||||
// phpcs:enable
|
||||
|
||||
$keywords = $this->extract_data_from_mixed( $keywords, 'mixed', ':', [ 'keywords', 'date' ] );
|
||||
$keywords = $this->set_dimension_as_key( $keywords, 'range_group' );
|
||||
|
||||
// merge metrics data.
|
||||
$data->analytics = [];
|
||||
$data->analytics = $this->get_merged_metrics( $analytics, $keywords, true );
|
||||
|
||||
$data->merged = $this->get_date_array(
|
||||
$intervals['dates'],
|
||||
[
|
||||
'clicks' => [],
|
||||
'impressions' => [],
|
||||
'position' => [],
|
||||
'ctr' => [],
|
||||
'keywords' => [],
|
||||
'pageviews' => [],
|
||||
]
|
||||
);
|
||||
|
||||
// Convert types.
|
||||
$data->analytics = array_map( [ $this, 'normalize_graph_rows' ], $data->analytics );
|
||||
|
||||
// Merge for performance.
|
||||
$data->merged = $this->get_merge_data_graph( $data->analytics, $data->merged, $intervals['map'] );
|
||||
|
||||
// For developers.
|
||||
$data = apply_filters( 'rank_math/analytics/analytics_summary_graph', $data, $intervals );
|
||||
|
||||
$data->merged = $this->get_graph_data_flat( $data->merged );
|
||||
$data->merged = array_values( $data->merged );
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
/**
|
||||
* Get URL Inspection data.
|
||||
*
|
||||
* @since 1.0.84
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use Exception;
|
||||
use RankMath\Helpers\DB as DB_Helper;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Url_Inspection class.
|
||||
*/
|
||||
class Url_Inspection {
|
||||
|
||||
/**
|
||||
* Holds the singleton instance of this class.
|
||||
*
|
||||
* @var Url_Inspection
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* Singleton
|
||||
*/
|
||||
public static function get() {
|
||||
if ( is_null( self::$instance ) ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a new inspection for an object ID.
|
||||
*
|
||||
* @param string $page URL to inspect (relative).
|
||||
* @param string $reschedule What to do if the job already exists: reschedule for new time, or skip and keep old time.
|
||||
* @param int $delay Number of seconds to delay the inspection from now.
|
||||
*/
|
||||
public function schedule_inspection( $page, $reschedule = true, $delay = 0 ) {
|
||||
$delay = absint( $delay );
|
||||
if ( $reschedule ) {
|
||||
as_unschedule_action( 'rank_math/analytics/get_inspections_data', [ $page ], 'rank-math' );
|
||||
} elseif ( as_has_scheduled_action( 'rank_math/analytics/get_inspections_data', [ $page ], 'rank-math' ) ) {
|
||||
// Already scheduled and reschedule = false.
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 0 === $delay ) {
|
||||
as_enqueue_async_action( 'rank_math/analytics/get_inspections_data', [ $page ], 'rank-math' );
|
||||
return;
|
||||
}
|
||||
|
||||
$time = time() + $delay;
|
||||
as_schedule_single_action( $time, 'rank_math/analytics/get_inspections_data', [ $page ], 'rank-math' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the inspection data for a URL, store it, and return it.
|
||||
*
|
||||
* @param string $page URL to inspect.
|
||||
*/
|
||||
public function inspect( $page ) {
|
||||
$inspection = \RankMath\Google\Url_Inspection::get()->get_inspection_data( $page );
|
||||
|
||||
if ( empty( $inspection ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
DB::store_inspection( $inspection );
|
||||
|
||||
return wp_parse_args( $inspection, DB::get_inspection_defaults() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest inspection results for each page.
|
||||
*
|
||||
* @param array $params Parameters.
|
||||
* @param int $per_page Number of items per page.
|
||||
*/
|
||||
public function get_inspections( $params, $per_page ) {
|
||||
// Early Bail!!
|
||||
if ( ! DB_Helper::check_table_exists( 'rank_math_analytics_inspections' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
return DB::get_inspections( $params, $per_page );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the "Enable Index Status Tab" option is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_enabled() {
|
||||
$profile = get_option( 'rank_math_google_analytic_profile', [] );
|
||||
if ( empty( $profile ) || ! is_array( $profile ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$enable_index_status = true;
|
||||
if ( isset( $profile['enable_index_status'] ) ) {
|
||||
$enable_index_status = $profile['enable_index_status'];
|
||||
}
|
||||
|
||||
return $enable_index_status;
|
||||
}
|
||||
}
|
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
/**
|
||||
* The Analytics Module
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use RankMath\Helper;
|
||||
use RankMath\Traits\Hooker;
|
||||
use RankMath\Google\Authentication;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Watcher class.
|
||||
*/
|
||||
class Watcher {
|
||||
|
||||
use Hooker;
|
||||
|
||||
/**
|
||||
* Main instance
|
||||
*
|
||||
* Ensure only one instance is loaded or can be loaded.
|
||||
*
|
||||
* @return Watcher
|
||||
*/
|
||||
public static function get() {
|
||||
static $instance;
|
||||
|
||||
if ( is_null( $instance ) && ! ( $instance instanceof Watcher ) ) {
|
||||
$instance = new Watcher();
|
||||
$instance->hooks();
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
public function hooks() {
|
||||
if ( Authentication::is_authorized() ) {
|
||||
$this->action( 'save_post', 'update_post_info', 99 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update post info for analytics.
|
||||
*
|
||||
* @param int $post_id Post id.
|
||||
*/
|
||||
public function update_post_info( $post_id ) {
|
||||
$status = get_post_status( $post_id );
|
||||
$post_type = get_post_type( $post_id );
|
||||
if (
|
||||
'publish' !== $status ||
|
||||
wp_is_post_autosave( $post_id ) ||
|
||||
wp_is_post_revision( $post_id ) ||
|
||||
! Helper::is_post_type_accessible( $post_type )
|
||||
) {
|
||||
DB::objects()
|
||||
->where( 'object_type', 'post' )
|
||||
->where( 'object_id', $post_id )
|
||||
->delete();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get primary focus keyword.
|
||||
$primary_keyword = get_post_meta( $post_id, 'rank_math_focus_keyword', true );
|
||||
if ( $primary_keyword ) {
|
||||
$primary_keyword = explode( ',', $primary_keyword );
|
||||
$primary_keyword = trim( $primary_keyword[0] );
|
||||
}
|
||||
|
||||
$page = str_replace( Helper::get_home_url(), '', urldecode( get_permalink( $post_id ) ) );
|
||||
|
||||
// Set argument for object row.
|
||||
$object_args = [
|
||||
'id' => get_post_meta( $post_id, 'rank_math_analytic_object_id', true ),
|
||||
'created' => get_the_modified_date( 'Y-m-d H:i:s', $post_id ),
|
||||
'title' => get_the_title( $post_id ),
|
||||
'page' => $page,
|
||||
'object_type' => 'post',
|
||||
'object_subtype' => $post_type,
|
||||
'object_id' => $post_id,
|
||||
'primary_key' => $primary_keyword,
|
||||
'seo_score' => $primary_keyword ? get_post_meta( $post_id, 'rank_math_seo_score', true ) : 0,
|
||||
'schemas_in_use' => \RankMath\Schema\DB::get_schema_types( $post_id, true, false ),
|
||||
'is_indexable' => Helper::is_post_indexable( $post_id ),
|
||||
'pagespeed_refreshed' => 'NULL',
|
||||
];
|
||||
|
||||
// Get translated object info in case multi-language plugin is installed.
|
||||
$translated_objects = apply_filters( 'rank_math/analytics/get_translated_objects', $post_id );
|
||||
if ( false !== $translated_objects && is_array( $translated_objects ) ) {
|
||||
// Remove current object info from objects table.
|
||||
DB::objects()
|
||||
->where( 'object_id', $post_id )
|
||||
->delete();
|
||||
|
||||
foreach ( $translated_objects as $obj ) {
|
||||
$object_args['title'] = $obj['title'];
|
||||
$object_args['page'] = $obj['url'];
|
||||
|
||||
DB::add_object( $object_args );
|
||||
}
|
||||
|
||||
// Here we don't need to add `rank_math_analytic_object_id` post meta, because we always remove old translated objects info and add new one, in case of multi-lanauge.
|
||||
return;
|
||||
}
|
||||
|
||||
// Update post from objects table.
|
||||
$id = DB::update_object( $object_args );
|
||||
|
||||
if ( $id > 0 ) {
|
||||
update_post_meta( $post_id, 'rank_math_analytic_object_id', $id );
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
/**
|
||||
* Google Analytics.
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Google;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use WP_Error;
|
||||
use RankMath\Google\Api;
|
||||
use RankMath\Helpers\Str;
|
||||
use RankMath\Analytics\Workflow\Base;
|
||||
|
||||
/**
|
||||
* Analytics class.
|
||||
*/
|
||||
class Analytics extends Request {
|
||||
|
||||
/**
|
||||
* Get analytics accounts.
|
||||
*/
|
||||
public function get_analytics_accounts() {
|
||||
$accounts = [];
|
||||
$v3_response = $this->http_get( 'https://www.googleapis.com/analytics/v3/management/accountSummaries' );
|
||||
$v3_data = true;
|
||||
if ( ! $this->is_success() || isset( $v3_response->error ) ) {
|
||||
$v3_data = false;
|
||||
}
|
||||
if ( false !== $v3_data ) {
|
||||
foreach ( $v3_response['items'] as $account ) {
|
||||
if ( 'analytics#accountSummary' !== $account['kind'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$properties = [];
|
||||
$account_id = $account['id'];
|
||||
|
||||
foreach ( $account['webProperties'] as $property ) {
|
||||
$property_id = $property['id'];
|
||||
|
||||
$properties[ $property_id ] = [
|
||||
'name' => $property['name'],
|
||||
'id' => $property['id'],
|
||||
'url' => $property['websiteUrl'],
|
||||
'account_id' => $account_id,
|
||||
];
|
||||
|
||||
foreach ( $property['profiles'] as $profile ) {
|
||||
unset( $profile['kind'] );
|
||||
$properties[ $property_id ]['profiles'][ $profile['id'] ] = $profile;
|
||||
}
|
||||
}
|
||||
|
||||
$accounts[ $account_id ] = [
|
||||
'name' => $account['name'],
|
||||
'properties' => $properties,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->add_ga4_accounts( $accounts );
|
||||
}
|
||||
/**
|
||||
* Get GA4 accounts info.
|
||||
*
|
||||
* @param array $accounts GA3 accounts info or empty array.
|
||||
*
|
||||
* @return array $accounts with added ga4 accounts
|
||||
*/
|
||||
public function add_ga4_accounts( $accounts ) {
|
||||
|
||||
$v4_response = $this->http_get( 'https://analyticsadmin.googleapis.com/v1alpha/accountSummaries?pageSize=200' );
|
||||
if ( ! $this->is_success() || isset( $v4_response->error ) ) {
|
||||
return $accounts;
|
||||
}
|
||||
foreach ( $v4_response['accountSummaries'] as $account ) {
|
||||
if ( empty( $account['propertySummaries'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$properties = [];
|
||||
$account_id = str_replace( 'accounts/', '', $account['account'] );
|
||||
|
||||
foreach ( $account['propertySummaries'] as $property ) {
|
||||
$property_id = str_replace( 'properties/', '', $property['property'] );
|
||||
|
||||
$accounts[ $account_id ]['properties'][ $property_id ] = [
|
||||
'name' => $property['displayName'],
|
||||
'id' => $property_id,
|
||||
'account_id' => $account_id,
|
||||
'type' => 'GA4',
|
||||
];
|
||||
}
|
||||
}
|
||||
return $accounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if google analytics is connected.
|
||||
*
|
||||
* @return boolean Returns True if the google analytics is connected, otherwise False.
|
||||
*/
|
||||
public static function is_analytics_connected() {
|
||||
$account = wp_parse_args(
|
||||
get_option( 'rank_math_google_analytic_options' ),
|
||||
[ 'view_id' => '' ]
|
||||
);
|
||||
|
||||
return ! empty( $account['view_id'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Query analytics data from google client api.
|
||||
*
|
||||
* @param array $options Analytics options.
|
||||
* @param boolean $days Whether to include dates.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_analytics( $options = [], $days = false ) {
|
||||
// Check view ID.
|
||||
$view_id = isset( $options['view_id'] ) ? $options['view_id'] : self::get_view_id();
|
||||
if ( ! $view_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stored = get_option(
|
||||
'rank_math_google_analytic_options',
|
||||
[
|
||||
'account_id' => '',
|
||||
'property_id' => '',
|
||||
'view_id' => '',
|
||||
'measurement_id' => '',
|
||||
'stream_name' => '',
|
||||
'country' => '',
|
||||
'install_code' => '',
|
||||
'anonymize_ip' => '',
|
||||
'local_ga_js' => '',
|
||||
'exclude_loggedin' => '',
|
||||
]
|
||||
);
|
||||
|
||||
// Check property ID.
|
||||
$property_id = isset( $options['property_id'] ) ? $options['property_id'] : $stored['property_id'];
|
||||
if ( ! $property_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check dates.
|
||||
$dates = Base::get_dates();
|
||||
$start_date = isset( $options['start_date'] ) ? $options['start_date'] : $dates['start_date'];
|
||||
$end_date = isset( $options['end_date'] ) ? $options['end_date'] : $dates['end_date'];
|
||||
if ( ! $start_date || ! $end_date ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Request params.
|
||||
$row_limit = isset( $options['row_limit'] ) ? $options['row_limit'] : Api::get()->get_row_limit();
|
||||
$country = isset( $options['country'] ) ? $options['country'] : '';
|
||||
if ( ! empty( $stored['country'] ) && 'all' !== $stored['country'] ) {
|
||||
$country = $stored['country'];
|
||||
}
|
||||
|
||||
// Check the property for old Google Analytics.
|
||||
if ( Str::starts_with( 'UA-', $property_id ) ) {
|
||||
$args = [
|
||||
'viewId' => $view_id,
|
||||
'pageSize' => $row_limit,
|
||||
'dateRanges' => [
|
||||
[
|
||||
'startDate' => $start_date,
|
||||
'endDate' => $end_date,
|
||||
],
|
||||
],
|
||||
'dimensionFilterClauses' => [
|
||||
[
|
||||
'filters' => [
|
||||
[
|
||||
'dimensionName' => 'ga:medium',
|
||||
'operator' => 'EXACT',
|
||||
'expressions' => 'organic',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Include only dates.
|
||||
if ( true === $days ) {
|
||||
$args = wp_parse_args(
|
||||
[
|
||||
'dimensions' => [
|
||||
[ 'name' => 'ga:date' ],
|
||||
],
|
||||
],
|
||||
$args
|
||||
);
|
||||
} else {
|
||||
$args = wp_parse_args(
|
||||
[
|
||||
'metrics' => [
|
||||
[ 'expression' => 'ga:pageviews' ],
|
||||
[ 'expression' => 'ga:users' ],
|
||||
],
|
||||
'dimensions' => [
|
||||
[ 'name' => 'ga:date' ],
|
||||
[ 'name' => 'ga:pagePath' ],
|
||||
[ 'name' => 'ga:hostname' ],
|
||||
],
|
||||
'orderBys' => [
|
||||
[
|
||||
'fieldName' => 'ga:pageviews',
|
||||
'sortOrder' => 'DESCENDING',
|
||||
],
|
||||
],
|
||||
],
|
||||
$args
|
||||
);
|
||||
|
||||
// Add country.
|
||||
if ( ! $country ) {
|
||||
$args['dimensionFilterClauses'][0]['filters'][] = [
|
||||
'dimensionName' => 'ga:countryIsoCode',
|
||||
'operator' => 'EXACT',
|
||||
'expressions' => $country,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$response = Api::get()->http_post(
|
||||
'https://analyticsreporting.googleapis.com/v4/reports:batchGet',
|
||||
[
|
||||
'reportRequests' => [ $args ],
|
||||
]
|
||||
);
|
||||
|
||||
Api::get()->log_failed_request( $response, 'analytics', $start_date, func_get_args() );
|
||||
|
||||
if ( ! Api::get()->is_success() ) {
|
||||
return new WP_Error( 'request_failed', __( 'The Google Analytics request failed.', 'rank-math' ) );
|
||||
}
|
||||
|
||||
if ( ! isset( $response['reports'], $response['reports'][0]['data']['rows'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response['reports'][0]['data']['rows'];
|
||||
}
|
||||
|
||||
// Request for GA4 API.
|
||||
$args = [
|
||||
'dateRanges' => [
|
||||
[
|
||||
'startDate' => $start_date,
|
||||
'endDate' => $end_date,
|
||||
],
|
||||
],
|
||||
'dimensionFilter' => [
|
||||
'andGroup' => [
|
||||
'expressions' => [
|
||||
[
|
||||
'filter' => [
|
||||
'fieldName' => 'streamId',
|
||||
'stringFilter' => [
|
||||
'matchType' => 'EXACT',
|
||||
'value' => $view_id,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'filter' => [
|
||||
'fieldName' => 'sessionMedium',
|
||||
'stringFilter' => [
|
||||
'matchType' => 'EXACT',
|
||||
'value' => 'organic',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Include only dates.
|
||||
if ( true === $days ) {
|
||||
$args = wp_parse_args(
|
||||
[
|
||||
'dimensions' => [
|
||||
[ 'name' => 'date' ],
|
||||
],
|
||||
],
|
||||
$args
|
||||
);
|
||||
} else {
|
||||
$args = wp_parse_args(
|
||||
[
|
||||
'dimensions' => [
|
||||
[ 'name' => 'hostname' ],
|
||||
[ 'name' => 'pagePath' ],
|
||||
[ 'name' => 'countryId' ],
|
||||
[ 'name' => 'sessionMedium' ],
|
||||
],
|
||||
'metrics' => [
|
||||
[ 'name' => 'screenPageViews' ],
|
||||
[ 'name' => 'totalUsers' ],
|
||||
],
|
||||
],
|
||||
$args
|
||||
);
|
||||
|
||||
// Include country.
|
||||
if ( $country ) {
|
||||
$args['dimensionFilter']['andGroup']['expressions'][] = [
|
||||
'filter' => [
|
||||
'fieldName' => 'countryId',
|
||||
'stringFilter' => [
|
||||
'matchType' => 'EXACT',
|
||||
'value' => $country,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$workflow = 'analytics';
|
||||
Api::get()->set_workflow( $workflow );
|
||||
$response = Api::get()->http_post(
|
||||
'https://analyticsdata.googleapis.com/v1beta/properties/' . $property_id . ':runReport',
|
||||
$args
|
||||
);
|
||||
|
||||
Api::get()->log_failed_request( $response, $workflow, $start_date, func_get_args() );
|
||||
|
||||
if ( ! Api::get()->is_success() ) {
|
||||
return new WP_Error( 'request_failed', __( 'The Google Analytics Console request failed.', 'rank-math' ) );
|
||||
}
|
||||
|
||||
if ( ! isset( $response['rows'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response['rows'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get view id.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_view_id() {
|
||||
static $rank_math_view_id;
|
||||
|
||||
if ( is_null( $rank_math_view_id ) ) {
|
||||
$options = get_option( 'rank_math_google_analytic_options' );
|
||||
$rank_math_view_id = ! empty( $options['view_id'] ) ? $options['view_id'] : false;
|
||||
}
|
||||
|
||||
return $rank_math_view_id;
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal Google API wrapper.
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Google;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Api
|
||||
*/
|
||||
class Api extends Console {
|
||||
|
||||
/**
|
||||
* Access token.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $token = [];
|
||||
|
||||
/**
|
||||
* Main instance
|
||||
*
|
||||
* Ensure only one instance is loaded or can be loaded.
|
||||
*
|
||||
* @return Api
|
||||
*/
|
||||
public static function get() {
|
||||
static $instance;
|
||||
|
||||
if ( is_null( $instance ) && ! ( $instance instanceof Api ) ) {
|
||||
$instance = new Api();
|
||||
$instance->setup();
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup token.
|
||||
*/
|
||||
private function setup() {
|
||||
if ( ! Authentication::is_authorized() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tokens = Authentication::tokens();
|
||||
$this->token = $tokens['access_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get row limit.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_row_limit() {
|
||||
return apply_filters( 'rank_math/analytics/row_limit', 10000 );
|
||||
}
|
||||
}
|
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
/**
|
||||
* Google Authentication wrapper.
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Google;
|
||||
|
||||
use RankMath\Helpers\Str;
|
||||
use RankMath\Data_Encryption;
|
||||
use RankMath\Helpers\Param;
|
||||
use RankMath\Helpers\Security;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Authentication class.
|
||||
*/
|
||||
class Authentication {
|
||||
|
||||
/**
|
||||
* API version.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $api_version = '2.1';
|
||||
|
||||
/**
|
||||
* Get or update token data.
|
||||
*
|
||||
* @param bool|array $data Data to save.
|
||||
* @return bool|array
|
||||
*/
|
||||
public static function tokens( $data = null ) {
|
||||
$key = 'rank_math_google_oauth_tokens';
|
||||
$encrypt_keys = [
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
];
|
||||
|
||||
// Clear data.
|
||||
if ( false === $data ) {
|
||||
delete_option( $key );
|
||||
return false;
|
||||
}
|
||||
|
||||
$saved = get_option( $key, [] );
|
||||
foreach ( $encrypt_keys as $enc_key ) {
|
||||
if ( isset( $saved[ $enc_key ] ) ) {
|
||||
$saved[ $enc_key ] = Data_Encryption::deep_decrypt( $saved[ $enc_key ] );
|
||||
}
|
||||
}
|
||||
|
||||
// Getter.
|
||||
if ( is_null( $data ) ) {
|
||||
return wp_parse_args( $saved, [] );
|
||||
}
|
||||
|
||||
// Setter.
|
||||
foreach ( $encrypt_keys as $enc_key ) {
|
||||
if ( isset( $saved[ $enc_key ] ) ) {
|
||||
$saved[ $enc_key ] = Data_Encryption::deep_encrypt( $saved[ $enc_key ] );
|
||||
}
|
||||
if ( isset( $data[ $enc_key ] ) ) {
|
||||
$data[ $enc_key ] = Data_Encryption::deep_encrypt( $data[ $enc_key ] );
|
||||
}
|
||||
}
|
||||
|
||||
$data = wp_parse_args( $data, $saved );
|
||||
update_option( $key, $data );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is google authorized.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_authorized() {
|
||||
$tokens = self::tokens();
|
||||
|
||||
return isset( $tokens['access_token'] ) && isset( $tokens['refresh_token'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token is expired.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_token_expired() {
|
||||
$tokens = self::tokens();
|
||||
|
||||
return $tokens['expire'] && time() > $tokens['expire'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get oauth url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_auth_url() {
|
||||
$page = self::get_page_slug();
|
||||
|
||||
return Security::add_query_arg_raw(
|
||||
[
|
||||
'version' => defined( 'RANK_MATH_PRO_VERSION' ) ? 'pro' : 'free',
|
||||
'api_version' => static::$api_version,
|
||||
'redirect_uri' => rawurlencode( admin_url( 'admin.php?page=' . $page ) ),
|
||||
'security' => wp_create_nonce( 'rank_math_oauth_token' ),
|
||||
],
|
||||
self::get_auth_app_url()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Google custom app.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_auth_app_url() {
|
||||
return apply_filters( 'rank_math/analytics/app_url', 'https://oauth.rankmath.com' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page slug according to request.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_page_slug() {
|
||||
$page = Param::get( 'page' );
|
||||
if ( ! empty( $page ) ) {
|
||||
switch ( $page ) {
|
||||
case 'rank-math-wizard':
|
||||
return 'rank-math-wizard&step=analytics';
|
||||
|
||||
case 'rank-math-analytics':
|
||||
return 'rank-math-analytics';
|
||||
|
||||
default:
|
||||
return 'rank-math-options-general#setting-panel-analytics';
|
||||
}
|
||||
}
|
||||
|
||||
$page = wp_get_referer();
|
||||
if ( ! empty( $page ) && Str::contains( 'wizard', $page ) ) {
|
||||
return 'rank-math-wizard&step=analytics';
|
||||
}
|
||||
|
||||
return 'rank-math-options-general#setting-panel-analytics';
|
||||
}
|
||||
}
|
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
/**
|
||||
* Google Search Console.
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Google;
|
||||
|
||||
use RankMath\Helpers\Str;
|
||||
use RankMath\Analytics\Workflow\Base;
|
||||
use RankMath\Sitemap\Sitemap;
|
||||
use WP_Error;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Console class.
|
||||
*/
|
||||
class Console extends Analytics {
|
||||
|
||||
/**
|
||||
* Add site.
|
||||
*
|
||||
* @param string $url Site url to add.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function add_site( $url ) {
|
||||
$this->http_put( 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode( $url ) );
|
||||
return $this->is_success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get site verification token.
|
||||
*
|
||||
* @param string $url Site url to add.
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function get_site_verification_token( $url ) {
|
||||
$args = [
|
||||
'site' => [
|
||||
'type' => 'SITE',
|
||||
'identifier' => $url,
|
||||
],
|
||||
'verificationMethod' => 'META',
|
||||
];
|
||||
|
||||
$response = $this->http_post( 'https://www.googleapis.com/siteVerification/v1/token', $args );
|
||||
if ( ! $this->is_success() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \RankMath\CMB2::sanitize_webmaster_tags( $response['token'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify site token.
|
||||
*
|
||||
* @param string $url Site url to add.
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function verify_site( $url ) {
|
||||
$token = $this->get_site_verification_token( $url );
|
||||
if ( ! $token ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save in transient.
|
||||
set_transient( 'rank_math_google_site_verification', $token, DAY_IN_SECONDS * 2 );
|
||||
|
||||
// Call Google site verification.
|
||||
$args = [
|
||||
'site' => [
|
||||
'type' => 'SITE',
|
||||
'identifier' => $url,
|
||||
],
|
||||
];
|
||||
|
||||
$this->http_post( 'https://www.googleapis.com/siteVerification/v1/webResource?verificationMethod=META', $args );
|
||||
|
||||
// Sync sitemap.
|
||||
as_enqueue_async_action( 'rank_math/analytics/sync_sitemaps', [], 'rank-math' );
|
||||
|
||||
return $this->is_success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sites.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_sites() {
|
||||
static $rank_math_google_sites;
|
||||
if ( ! \is_null( $rank_math_google_sites ) ) {
|
||||
return $rank_math_google_sites;
|
||||
}
|
||||
|
||||
$rank_math_google_sites = [];
|
||||
$response = $this->http_get( 'https://www.googleapis.com/webmasters/v3/sites' );
|
||||
if ( ! $this->is_success() || empty( $response['siteEntry'] ) ) {
|
||||
return $rank_math_google_sites;
|
||||
}
|
||||
|
||||
foreach ( $response['siteEntry'] as $site ) {
|
||||
$rank_math_google_sites[ $site['siteUrl'] ] = $site['siteUrl'];
|
||||
}
|
||||
|
||||
return $rank_math_google_sites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch sitemaps.
|
||||
*
|
||||
* @param string $url Site to get sitemaps for.
|
||||
* @param boolean $with_index With index data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_sitemaps( $url, $with_index = false ) {
|
||||
$with_index = $with_index ? '?sitemapIndex=' . rawurlencode( $url . Sitemap::get_sitemap_index_slug() . '.xml' ) : '';
|
||||
$response = $this->http_get( 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode( $url ) . '/sitemaps' . $with_index );
|
||||
|
||||
if ( ! $this->is_success() || empty( $response['sitemap'] ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $response['sitemap'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit sitemap to search console.
|
||||
*
|
||||
* @param string $url Site to add sitemap for.
|
||||
* @param string $sitemap Sitemap url.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function add_sitemap( $url, $sitemap ) {
|
||||
return $this->http_put( 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode( $url ) . '/sitemaps/' . rawurlencode( $sitemap ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete sitemap from search console.
|
||||
*
|
||||
* @param string $url Site to delete sitemap for.
|
||||
* @param string $sitemap Sitemap url.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function delete_sitemap( $url, $sitemap ) {
|
||||
return $this->http_delete( 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode( $url ) . '/sitemaps/' . rawurlencode( $sitemap ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Query analytics data from google client api.
|
||||
*
|
||||
* @param array $args Query arguments.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_search_analytics( $args = [] ) {
|
||||
$dates = Base::get_dates();
|
||||
|
||||
$start_date = isset( $args['start_date'] ) ? $args['start_date'] : $dates['start_date'];
|
||||
$end_date = isset( $args['end_date'] ) ? $args['end_date'] : $dates['end_date'];
|
||||
$dimensions = isset( $args['dimensions'] ) ? $args['dimensions'] : 'date';
|
||||
$row_limit = isset( $args['row_limit'] ) ? $args['row_limit'] : Api::get()->get_row_limit();
|
||||
|
||||
$params = [
|
||||
'startDate' => $start_date,
|
||||
'endDate' => $end_date,
|
||||
'rowLimit' => $row_limit,
|
||||
'dimensions' => \is_array( $dimensions ) ? $dimensions : [ $dimensions ],
|
||||
];
|
||||
|
||||
$stored = get_option(
|
||||
'rank_math_google_analytic_profile',
|
||||
[
|
||||
'country' => '',
|
||||
'profile' => '',
|
||||
'enable_index_status' => '',
|
||||
]
|
||||
);
|
||||
$country = isset( $args['country'] ) ? $args['country'] : $stored['country'];
|
||||
$profile = isset( $args['profile'] ) ? $args['profile'] : $stored['profile'];
|
||||
|
||||
if ( 'all' !== $country ) {
|
||||
$params['dimensionFilterGroups'] = [
|
||||
[
|
||||
'filters' => [
|
||||
[
|
||||
'dimension' => 'country',
|
||||
'operator' => 'equals',
|
||||
'expression' => $country,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ( empty( $profile ) ) {
|
||||
$profile = trailingslashit( strtolower( home_url() ) );
|
||||
}
|
||||
|
||||
$workflow = 'console';
|
||||
$this->set_workflow( $workflow );
|
||||
$response = $this->http_post(
|
||||
'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode( $profile ) . '/searchAnalytics/query',
|
||||
$params
|
||||
);
|
||||
|
||||
$this->log_failed_request( $response, $workflow, $start_date, func_get_args() );
|
||||
|
||||
if ( ! $this->is_success() ) {
|
||||
return new WP_Error( 'request_failed', __( 'The Google Search Console request failed.', 'rank-math' ) );
|
||||
}
|
||||
|
||||
if ( ! isset( $response['rows'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response['rows'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is site verified.
|
||||
*
|
||||
* @param string $url Site to verify.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_site_verified( $url ) {
|
||||
$response = $this->http_get( 'https://www.googleapis.com/siteVerification/v1/webResource/' . rawurlencode( $url ) );
|
||||
if ( ! $this->is_success() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isset( $response['owners'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync sitemaps with google search console.
|
||||
*/
|
||||
public function sync_sitemaps() {
|
||||
$site_url = self::get_site_url();
|
||||
$data = $this->get_sitemap_to_sync();
|
||||
|
||||
// Submit it.
|
||||
if ( ! $data['sitemaps_in_list'] ) {
|
||||
$this->add_sitemap( $site_url, $data['local_sitemap'] );
|
||||
}
|
||||
|
||||
if ( empty( $data['delete_sitemaps'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete it.
|
||||
foreach ( $data['delete_sitemaps'] as $sitemap ) {
|
||||
$this->delete_sitemap( $site_url, $sitemap );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sitemaps to sync.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_sitemap_to_sync() {
|
||||
$delete_sitemaps = [];
|
||||
$sitemaps_in_list = false;
|
||||
$site_url = self::get_site_url();
|
||||
$sitemaps = $this->get_sitemaps( $site_url );
|
||||
$local_sitemap = trailingslashit( $site_url ) . Sitemap::get_sitemap_index_slug() . '.xml';
|
||||
|
||||
// Early Bail if there are no sitemaps.
|
||||
if ( empty( $sitemaps ) ) {
|
||||
return compact( 'delete_sitemaps', 'sitemaps_in_list', 'local_sitemap' );
|
||||
}
|
||||
|
||||
foreach ( $sitemaps as $sitemap ) {
|
||||
if ( $sitemap['path'] === $local_sitemap ) {
|
||||
$sitemaps_in_list = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$delete_sitemaps[] = $sitemap['path'];
|
||||
}
|
||||
|
||||
return compact( 'delete_sitemaps', 'sitemaps_in_list', 'local_sitemap' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get site url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_site_url() {
|
||||
static $rank_math_site_url;
|
||||
|
||||
if ( is_null( $rank_math_site_url ) ) {
|
||||
$default = trailingslashit( strtolower( home_url() ) );
|
||||
$rank_math_site_url = get_option( 'rank_math_google_analytic_profile', [ 'profile' => $default ] );
|
||||
$rank_math_site_url = empty( $rank_math_site_url['profile'] ) ? $default : $rank_math_site_url['profile'];
|
||||
|
||||
if ( Str::contains( 'sc-domain:', $rank_math_site_url ) ) {
|
||||
$rank_math_site_url = str_replace( 'sc-domain:', '', $rank_math_site_url );
|
||||
$rank_math_site_url = ( is_ssl() ? 'https://' : 'http://' ) . $rank_math_site_url;
|
||||
}
|
||||
}
|
||||
|
||||
return $rank_math_site_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if console is connected.
|
||||
*
|
||||
* @return boolean Returns True if the console is connected, otherwise False.
|
||||
*/
|
||||
public static function is_console_connected() {
|
||||
$profile = wp_parse_args(
|
||||
get_option( 'rank_math_google_analytic_profile' ),
|
||||
[
|
||||
'profile' => '',
|
||||
'country' => 'all',
|
||||
]
|
||||
);
|
||||
|
||||
return ! empty( $profile['profile'] );
|
||||
}
|
||||
}
|
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* Google Permissions.
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Google;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Permissions class.
|
||||
*/
|
||||
class Permissions {
|
||||
|
||||
const OPTION_NAME = 'rank_math_analytics_permissions';
|
||||
|
||||
/**
|
||||
* Permission info.
|
||||
*/
|
||||
public static function fetch() {
|
||||
$tokens = Authentication::tokens();
|
||||
if ( empty( $tokens['access_token'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' . $tokens['access_token'];
|
||||
$response = wp_remote_get( $url );
|
||||
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = wp_remote_retrieve_body( $response );
|
||||
if ( empty( $response ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = \json_decode( $response, true );
|
||||
$scopes = $response['scope'];
|
||||
$scopes = explode( ' ', $scopes );
|
||||
$scopes = str_replace( 'https://www.googleapis.com/auth/', '', $scopes );
|
||||
|
||||
update_option( self::OPTION_NAME, $scopes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permissions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get() {
|
||||
return get_option( self::OPTION_NAME, [] );
|
||||
}
|
||||
|
||||
/**
|
||||
* If user give permission or not.
|
||||
*
|
||||
* @param string $permission Permission name.
|
||||
* @return boolean
|
||||
*/
|
||||
public static function has( $permission ) {
|
||||
$permissions = self::get();
|
||||
|
||||
return in_array( $permission, $permissions, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* If user give permission or not.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function has_console() {
|
||||
return self::has( 'webmasters' );
|
||||
}
|
||||
|
||||
/**
|
||||
* If user give permission or not.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function has_analytics() {
|
||||
return self::has( 'analytics.readonly' ) ||
|
||||
self::has( 'analytics.provision' ) ||
|
||||
self::has( 'analytics.edit' );
|
||||
}
|
||||
|
||||
/**
|
||||
* If user give permission or not.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function has_adsense() {
|
||||
return self::has( 'adsense.readonly' );
|
||||
}
|
||||
|
||||
/**
|
||||
* If user give permission or not.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_status() {
|
||||
return [
|
||||
esc_html__( 'Search Console', 'rank-math' ) => self::get_status_text( self::has_console() ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Status text
|
||||
*
|
||||
* @param boolean $check Truthness.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_status_text( $check ) {
|
||||
return $check ? esc_html__( 'Given', 'rank-math' ) : esc_html__( 'Not Given', 'rank-math' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Print warning
|
||||
*/
|
||||
public static function print_warning() {
|
||||
?>
|
||||
<p class="warning">
|
||||
<strong class="warning">
|
||||
<?php esc_html_e( 'Warning:', 'rank-math' ); ?>
|
||||
</strong>
|
||||
<?php
|
||||
/* translators: %s is the reconnect link. */
|
||||
printf( wp_kses_post( __( 'You have not given the permission to fetch this data. Please <a href="%s">reconnect</a> with all required permissions.', 'rank-math' ) ), wp_nonce_url( admin_url( 'admin.php?reconnect=google' ), 'rank_math_reconnect_google' ) );
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
}
|
@@ -0,0 +1,489 @@
|
||||
<?php
|
||||
/**
|
||||
* Google API Request.
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Google;
|
||||
|
||||
use RankMath\Helper;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Request
|
||||
*/
|
||||
class Request {
|
||||
|
||||
/**
|
||||
* Workflow.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $workflow = '';
|
||||
|
||||
/**
|
||||
* Was the last request successful.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_success = false;
|
||||
|
||||
/**
|
||||
* Last error.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $last_error = '';
|
||||
|
||||
/**
|
||||
* Last response.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $last_response = [];
|
||||
|
||||
/**
|
||||
* Last response header code.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $last_code = 0;
|
||||
|
||||
/**
|
||||
* Is refresh token notice added.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_notice_added = false;
|
||||
|
||||
/**
|
||||
* Access token.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $token = '';
|
||||
|
||||
/**
|
||||
* Set workflow
|
||||
*
|
||||
* @param string $workflow Workflow name.
|
||||
*/
|
||||
public function set_workflow( $workflow = '' ) {
|
||||
$this->workflow = $workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Was the last request successful?
|
||||
*
|
||||
* @return bool True for success, false for failure
|
||||
*/
|
||||
public function is_success() {
|
||||
return $this->is_success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last error returned by either the network transport, or by the API.
|
||||
* If something didn't work, this should contain the string describing the problem.
|
||||
*
|
||||
* @return array|false describing the error
|
||||
*/
|
||||
public function get_error() {
|
||||
return $this->last_error ? $this->last_error : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array containing the HTTP headers and the body of the API response.
|
||||
*
|
||||
* @return array Assoc array with keys 'headers' and 'body'
|
||||
*/
|
||||
public function get_response() {
|
||||
return $this->last_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP GET request - for retrieving data.
|
||||
*
|
||||
* @param string $url URL to do request.
|
||||
* @param array $args Assoc array of arguments (usually your data).
|
||||
* @param int $timeout Timeout limit for request in seconds.
|
||||
*
|
||||
* @return WP_Error|array|false Assoc array of API response, decoded from JSON.
|
||||
*/
|
||||
public function http_get( $url, $args = [], $timeout = 10 ) {
|
||||
return $this->make_request( 'GET', $url, $args, $timeout );
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP POST request - for creating and updating items.
|
||||
*
|
||||
* @param string $url URL to do request.
|
||||
* @param array $args Assoc array of arguments (usually your data).
|
||||
* @param int $timeout Timeout limit for request in seconds.
|
||||
*
|
||||
* @return WP_Error|array|false Assoc array of API response, decoded from JSON.
|
||||
*/
|
||||
public function http_post( $url, $args = [], $timeout = 10 ) {
|
||||
return $this->make_request( 'POST', $url, $args, $timeout );
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP PUT request - for creating new items.
|
||||
*
|
||||
* @param string $url URL to do request.
|
||||
* @param array $args Assoc array of arguments (usually your data).
|
||||
* @param int $timeout Timeout limit for request in seconds.
|
||||
*
|
||||
* @return WP_Error|array|false Assoc array of API response, decoded from JSON.
|
||||
*/
|
||||
public function http_put( $url, $args = [], $timeout = 10 ) {
|
||||
return $this->make_request( 'PUT', $url, $args, $timeout );
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP DELETE request - for deleting data.
|
||||
*
|
||||
* @param string $url URL to do request.
|
||||
* @param array $args Assoc array of arguments (usually your data).
|
||||
* @param int $timeout Timeout limit for request in seconds.
|
||||
*
|
||||
* @return WP_Error|array|false Assoc array of API response, decoded from JSON.
|
||||
*/
|
||||
public function http_delete( $url, $args = [], $timeout = 10 ) {
|
||||
return $this->make_request( 'DELETE', $url, $args, $timeout );
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the underlying HTTP request. Not very exciting.
|
||||
*
|
||||
* @param string $http_verb The HTTP verb to use: get, post, put, patch, delete.
|
||||
* @param string $url URL to do request.
|
||||
* @param array $args Assoc array of parameters to be passed.
|
||||
* @param int $timeout Timeout limit for request in seconds.
|
||||
*
|
||||
* @return array|false Assoc array of decoded result.
|
||||
*/
|
||||
private function make_request( $http_verb, $url, $args = [], $timeout = 10 ) {
|
||||
// Early Bail!!
|
||||
if ( ! Authentication::is_authorized() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->refresh_token() || ! is_scalar( $this->token ) ) {
|
||||
if ( ! $this->is_notice_added ) {
|
||||
$this->is_notice_added = true;
|
||||
$this->is_success = false;
|
||||
$this->last_error = sprintf(
|
||||
/* translators: reconnect link */
|
||||
wp_kses_post( __( 'There is a problem with the Google auth token. Please <a href="%1$s" class="button button-link rank-math-reconnect-google">reconnect your app</a>', 'rank-math' ) ),
|
||||
wp_nonce_url( admin_url( 'admin.php?reconnect=google' ), 'rank_math_reconnect_google' )
|
||||
);
|
||||
$this->log_response( $http_verb, $url, $args, '', '', '', date( 'Y-m-d H:i:s' ) . ': Google auth token has been expired or is invalid' );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'timeout' => $timeout,
|
||||
'method' => $http_verb,
|
||||
];
|
||||
|
||||
$params['headers'] = [ 'Authorization' => 'Bearer ' . $this->token ];
|
||||
|
||||
if ( 'DELETE' === $http_verb || 'PUT' === $http_verb ) {
|
||||
$params['headers']['Content-Length'] = '0';
|
||||
} elseif ( 'POST' === $http_verb && ! empty( $args ) && is_array( $args ) ) {
|
||||
$json = wp_json_encode( $args );
|
||||
$params['body'] = $json;
|
||||
$params['headers']['Content-Type'] = 'application/json';
|
||||
$params['headers']['Content-Length'] = strlen( $json );
|
||||
}
|
||||
|
||||
$this->reset();
|
||||
sleep( 1 );
|
||||
$response = wp_remote_request( $url, $params );
|
||||
$formatted_response = $this->format_response( $response );
|
||||
$this->determine_success( $response, $formatted_response );
|
||||
|
||||
$this->log_response( $http_verb, $url, $args, $response, $formatted_response, $params );
|
||||
|
||||
// Error handaling.
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
if ( 200 !== $code ) {
|
||||
// Remove workflow actions.
|
||||
if ( $this->workflow ) {
|
||||
as_unschedule_all_actions( 'rank_math/analytics/get_' . $this->workflow . '_data' );
|
||||
}
|
||||
}
|
||||
|
||||
do_action(
|
||||
'rank_math/analytics/handle_' . $this->workflow . '_response',
|
||||
[
|
||||
'formatted_response' => $formatted_response,
|
||||
'response' => $response,
|
||||
'http_verb' => $http_verb,
|
||||
'url' => $url,
|
||||
'args' => $args,
|
||||
'code' => $code,
|
||||
]
|
||||
);
|
||||
|
||||
return $formatted_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the response in analytics_debug.log file.
|
||||
*
|
||||
* @param string $http_verb The HTTP verb to use: get, post, put, patch, delete.
|
||||
* @param string $url URL to do request.
|
||||
* @param array $args Assoc array of parameters to be passed.
|
||||
* @param string $response make_request response.
|
||||
* @param string $formatted_response Formated response.
|
||||
* @param array $params Parameters.
|
||||
* @param string $text Text to append at the end of the response.
|
||||
*/
|
||||
private function log_response( $http_verb = '', $url = '', $args = [], $response = [], $formatted_response = '', $params = [], $text = '' ) {
|
||||
do_action( 'rank_math/analytics/log', $http_verb, $url, $args, $response, $formatted_response, $params );
|
||||
|
||||
if ( ! apply_filters( 'rank_math/analytics/log_response', false ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uploads = wp_upload_dir();
|
||||
$file = $uploads['basedir'] . '/rank-math/analytics-debug.log';
|
||||
|
||||
$wp_filesystem = Helper::get_filesystem();
|
||||
|
||||
// Create log file if it doesn't exist.
|
||||
$wp_filesystem->touch( $file );
|
||||
|
||||
// Not writable? Bail.
|
||||
if ( ! $wp_filesystem->is_writable( $file ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = '********************************' . PHP_EOL;
|
||||
$message .= date( 'Y-m-d h:i:s' ) . PHP_EOL;
|
||||
|
||||
$tokens = Authentication::tokens();
|
||||
if ( ! empty( $tokens ) && is_array( $tokens ) && isset( $tokens['expire'] ) ) {
|
||||
$message .= 'Expiry: ' . date( 'Y-m-d h:i:s', $tokens['expire'] ) . PHP_EOL;
|
||||
$message .= 'Expiry Readable: ' . human_time_diff( $tokens['expire'] ) . PHP_EOL;
|
||||
}
|
||||
|
||||
$message .= $text . PHP_EOL;
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$message .= '<span class="fail">FAIL</span>' . PHP_EOL;
|
||||
$message .= 'WP_Error: ' . $response->get_error_message() . PHP_EOL;
|
||||
} elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||
$message .= '<span class="fail">FAIL</span>' . PHP_EOL;
|
||||
} elseif ( isset( $formatted_response['error_description'] ) ) {
|
||||
$message .= '<span class="fail">FAIL</span>' . PHP_EOL;
|
||||
$message .= 'Bad Request' === $formatted_response['error_description'] ?
|
||||
esc_html__( 'Bad request. Please check the code.', 'rank-math' ) : $formatted_response['error_description'];
|
||||
} else {
|
||||
$message .= '<span class="pass">PASS</span>' . PHP_EOL;
|
||||
}
|
||||
$message .= 'REQUEST: ' . $http_verb . ' > ' . $url . PHP_EOL;
|
||||
$message .= 'REQUEST_PARAMETERS: ' . wp_json_encode( $params ) . PHP_EOL;
|
||||
$message .= 'REQUEST_API_ARGUMENTS: ' . wp_json_encode( $args ) . PHP_EOL;
|
||||
$message .= 'RESPONSE_CODE: ' . wp_remote_retrieve_response_code( $response ) . PHP_EOL;
|
||||
$message .= 'RESPONSE_CODE_MESSAGE: ' . wp_remote_retrieve_body( $response ) . PHP_EOL;
|
||||
$message .= 'RESPONSE_FORMATTED: ' . wp_json_encode( $formatted_response ) . PHP_EOL;
|
||||
$message .= 'ORIGINAL_RESPONSE: ' . wp_json_encode( $response ) . PHP_EOL;
|
||||
$message .= '================================' . PHP_EOL;
|
||||
$message .= $wp_filesystem->get_contents( $file );
|
||||
|
||||
$wp_filesystem->put_contents( $file, $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the response and format any error messages for debugging
|
||||
*
|
||||
* @param array $response The response from the curl request.
|
||||
*
|
||||
* @return array|false The JSON decoded into an array
|
||||
*/
|
||||
private function format_response( $response ) {
|
||||
$this->last_response = $response;
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! empty( $response['body'] ) ) {
|
||||
return json_decode( $response['body'], true );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the response was successful or a failure. If it failed, store the error.
|
||||
*
|
||||
* @param object $response The response from the curl request.
|
||||
* @param array|false $formatted_response The response body payload from the curl request.
|
||||
*/
|
||||
private function determine_success( $response, $formatted_response ) {
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$this->last_error = 'WP_Error: ' . $response->get_error_message();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->last_code = wp_remote_retrieve_response_code( $response );
|
||||
if ( in_array( $this->last_code, [ 200, 204 ], true ) ) {
|
||||
$this->is_success = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $formatted_response['error_description'] ) ) {
|
||||
$this->last_error = 'Bad Request' === $formatted_response['error_description'] ?
|
||||
esc_html__( 'Bad request. Please check the code.', 'rank-math' ) : $formatted_response['error_description'];
|
||||
return;
|
||||
}
|
||||
|
||||
$this->last_error = esc_html__( 'Unknown error, call get_response() to find out what happened.', 'rank-math' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset request.
|
||||
*/
|
||||
private function reset() {
|
||||
$this->last_code = 0;
|
||||
$this->last_error = '';
|
||||
$this->is_success = false;
|
||||
$this->last_response = [
|
||||
'body' => null,
|
||||
'headers' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh access token when user login.
|
||||
*/
|
||||
public function refresh_token() {
|
||||
// Bail if the user is not authenticated at all yet.
|
||||
if ( ! Authentication::is_authorized() || ! Authentication::is_token_expired() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$response = $this->get_refresh_token();
|
||||
if ( ! $response ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( false === $response['success'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tokens = Authentication::tokens();
|
||||
|
||||
// Save new token.
|
||||
$this->token = $response['access_token'];
|
||||
$tokens['expire'] = $response['expire'];
|
||||
$tokens['access_token'] = $response['access_token'];
|
||||
Authentication::tokens( $tokens );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the new refresh token.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function get_refresh_token() {
|
||||
$tokens = Authentication::tokens();
|
||||
if ( empty( $tokens['refresh_token'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = wp_remote_get(
|
||||
add_query_arg(
|
||||
[
|
||||
'code' => $tokens['refresh_token'],
|
||||
'format' => 'json',
|
||||
],
|
||||
Authentication::get_auth_app_url() . '/refresh.php'
|
||||
)
|
||||
);
|
||||
|
||||
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
if ( empty( $response ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke an OAuth2 token.
|
||||
*
|
||||
* @return boolean Whether the token was revoked successfully.
|
||||
*/
|
||||
public function revoke_token() {
|
||||
Authentication::tokens( false );
|
||||
delete_option( 'rank_math_google_analytic_profile' );
|
||||
delete_option( 'rank_math_google_analytic_options' );
|
||||
delete_option( 'rankmath_google_api_failed_attempts_data' );
|
||||
delete_option( 'rankmath_google_api_reconnect' );
|
||||
|
||||
return $this->is_success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log every failed API call.
|
||||
* And kill all next scheduled event if failed count is more then three.
|
||||
*
|
||||
* @param array $response Response from api.
|
||||
* @param string $action Action performing.
|
||||
* @param string $start_date Start date fetching for (or page URI for inspections).
|
||||
* @param array $args Array of arguments.
|
||||
*/
|
||||
public function log_failed_request( $response, $action, $start_date, $args ) {
|
||||
if ( $this->is_success() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$option_key = 'rankmath_google_api_failed_attempts_data';
|
||||
$reconnect_google_option_key = 'rankmath_google_api_reconnect';
|
||||
if ( empty( $response['error'] ) || ! is_array( $response['error'] ) ) {
|
||||
delete_option( $option_key );
|
||||
delete_option( $reconnect_google_option_key );
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit maximum 10 failed attempt data to log.
|
||||
$failed_attempts = get_option( $option_key, [] );
|
||||
$failed_attempts = ( ! empty( $failed_attempts ) && is_array( $failed_attempts ) ) ? array_slice( $failed_attempts, -9, 9 ) : [];
|
||||
$failed_attempts[] = [
|
||||
'action' => $action,
|
||||
'args' => $args,
|
||||
'error' => $response['error'],
|
||||
];
|
||||
|
||||
update_option( $option_key, $failed_attempts, false );
|
||||
|
||||
// Number of allowed attempt.
|
||||
if ( 3 < count( $failed_attempts ) ) {
|
||||
update_option( $reconnect_google_option_key, 'search_analytics_query' );
|
||||
return;
|
||||
}
|
||||
|
||||
as_schedule_single_action(
|
||||
time() + 60,
|
||||
"rank_math/analytics/get_{$action}_data",
|
||||
[ $start_date ],
|
||||
'rank-math'
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
/**
|
||||
* Google URL Inspection API.
|
||||
*
|
||||
* @since 1.0.84
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Google;
|
||||
|
||||
use RankMath\Helper;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Analytics class.
|
||||
*/
|
||||
class Url_Inspection extends Request {
|
||||
/**
|
||||
* URL Inspection API base URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $api_url = 'https://searchconsole.googleapis.com/v1/urlInspection/index:inspect';
|
||||
|
||||
/**
|
||||
* Access token.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $token = [];
|
||||
|
||||
/**
|
||||
* Main instance
|
||||
*
|
||||
* Ensure only one instance is loaded or can be loaded.
|
||||
*
|
||||
* @return Url_Inspection
|
||||
*/
|
||||
public static function get() {
|
||||
static $instance;
|
||||
|
||||
if ( is_null( $instance ) && ! ( $instance instanceof Url_Inspection ) ) {
|
||||
$instance = new Url_Inspection();
|
||||
$instance->setup();
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup token.
|
||||
*/
|
||||
public function setup() {
|
||||
if ( ! Authentication::is_authorized() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tokens = Authentication::tokens();
|
||||
$this->token = $tokens['access_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send URL to the API and return the response, or false on failure.
|
||||
*
|
||||
* @param string $page URL to inspect (relative).
|
||||
*/
|
||||
public function get_api_results( $page ) {
|
||||
$lang_arr = \explode( '_', get_locale() );
|
||||
$lang_code = empty( $lang_arr[1] ) ? $lang_arr[0] : $lang_arr[0] . '-' . $lang_arr[1];
|
||||
|
||||
$args = [
|
||||
'inspectionUrl' => untrailingslashit( Helper::get_home_url() ) . $page,
|
||||
'siteUrl' => Console::get_site_url(),
|
||||
'languageCode' => $lang_code,
|
||||
];
|
||||
|
||||
set_time_limit( 90 );
|
||||
|
||||
$workflow = 'inspections';
|
||||
$this->set_workflow( $workflow );
|
||||
|
||||
$response = $this->http_post( $this->api_url, $args, 60 );
|
||||
|
||||
$this->log_failed_request( $response, $workflow, $page, func_get_args() );
|
||||
|
||||
if ( ! $this->is_success() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inspection data.
|
||||
*
|
||||
* @param string $page URL to inspect.
|
||||
*/
|
||||
public function get_inspection_data( $page ) {
|
||||
$inspection = $this->get_api_results( $page );
|
||||
if ( empty( $inspection ) || empty( $inspection['inspectionResult'] ) ) {
|
||||
return;
|
||||
}
|
||||
$inspection = $this->normalize_inspection_data( $inspection );
|
||||
|
||||
$inspection['page'] = $page;
|
||||
|
||||
return $inspection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize inspection data.
|
||||
*
|
||||
* @param array $inspection Inspection data.
|
||||
*/
|
||||
private function normalize_inspection_data( $inspection ) {
|
||||
$incoming = $inspection['inspectionResult'];
|
||||
$normalized = [];
|
||||
|
||||
$map_properties = [
|
||||
'indexStatusResult.verdict' => 'index_verdict',
|
||||
'indexStatusResult.coverageState' => 'coverage_state',
|
||||
'indexStatusResult.indexingState' => 'indexing_state',
|
||||
'indexStatusResult.pageFetchState' => 'page_fetch_state',
|
||||
'indexStatusResult.robotsTxtState' => 'robots_txt_state',
|
||||
'mobileUsabilityResult.verdict' => 'mobile_usability_verdict',
|
||||
'mobileUsabilityResult.issues' => 'mobile_usability_issues',
|
||||
'richResultsResult.verdict' => 'rich_results_verdict',
|
||||
'indexStatusResult.crawledAs' => 'crawled_as',
|
||||
'indexStatusResult.googleCanonical' => 'google_canonical',
|
||||
'indexStatusResult.userCanonical' => 'user_canonical',
|
||||
'indexStatusResult.sitemap' => 'sitemap',
|
||||
'indexStatusResult.referringUrls' => 'referring_urls',
|
||||
];
|
||||
|
||||
$this->assign_inspection_values( $incoming, $map_properties, $normalized );
|
||||
|
||||
$normalized = apply_filters( 'rank_math/analytics/url_inspection_map_properties', $normalized, $incoming );
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign inspection field value to the data array.
|
||||
*
|
||||
* @param array $raw_data Raw data.
|
||||
* @param string $field Field name.
|
||||
* @param string $assign_to Field name to assign to.
|
||||
* @param array $data Data array.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function assign_inspection_value( $raw_data, $field, $assign_to, &$data ) {
|
||||
$data[ $assign_to ] = $this->get_result_field( $raw_data, $field );
|
||||
|
||||
if ( is_array( $data[ $assign_to ] ) ) {
|
||||
$data[ $assign_to ] = wp_json_encode( $data[ $assign_to ] );
|
||||
} elseif ( preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/', $data[ $assign_to ], $matches ) ) {
|
||||
// If it's a date, convert to MySQL format.
|
||||
$data[ $assign_to ] = date( 'Y-m-d H:i:s', strtotime( $matches[0] ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Date is stored as TIMESTAMP, so the timezone is converted automatically.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a field from the inspection result.
|
||||
*
|
||||
* @param array $raw_data Incoming data.
|
||||
* @param string $field Field name.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function get_result_field( $raw_data, $field ) {
|
||||
if ( false !== strpos( $field, '.' ) ) {
|
||||
$fields = explode( '.', $field );
|
||||
|
||||
if ( ! isset( $raw_data[ $fields[0] ] ) || ! isset( $raw_data[ $fields[0] ][ $fields[1] ] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $raw_data[ $fields[0] ][ $fields[1] ];
|
||||
}
|
||||
|
||||
if ( ! isset( $raw_data[ $field ] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $raw_data[ $field ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign inspection field values to the data array.
|
||||
*
|
||||
* @param array $raw_data Raw data.
|
||||
* @param array $fields Map properties.
|
||||
* @param array $data Data array.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function assign_inspection_values( $raw_data, $fields, &$data ) {
|
||||
foreach ( $fields as $field => $assign_to ) {
|
||||
$this->assign_inspection_value( $raw_data, $field, $assign_to, $data );
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
/**
|
||||
* The Global functionality of the plugin.
|
||||
*
|
||||
* Defines the functionality loaded on admin.
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Rest
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics;
|
||||
|
||||
use WP_Error;
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Controller;
|
||||
use RankMath\Helper;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Rest class.
|
||||
*/
|
||||
class Rest extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = \RankMath\Rest\Rest_Helper::BASE . '/an';
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the routes for the objects of the controller.
|
||||
*/
|
||||
public function register_routes() {
|
||||
$routes = [
|
||||
'dashboard' => [
|
||||
'callback' => [ $this, 'get_dashboard' ],
|
||||
],
|
||||
'keywordsOverview' => [
|
||||
'callback' => [ $this, 'get_keywords_overview' ],
|
||||
],
|
||||
'postsSummary' => [
|
||||
'callback' => [ Stats::get(), 'get_posts_summary' ],
|
||||
],
|
||||
'postsRowsByObjects' => [
|
||||
'callback' => [ Stats::get(), 'get_posts_rows_by_objects' ],
|
||||
],
|
||||
'post/(?P<id>\d+)' => [
|
||||
'callback' => [ $this, 'get_post' ],
|
||||
],
|
||||
'keywordsSummary' => [
|
||||
'callback' => [ Stats::get(), 'get_analytics_summary' ],
|
||||
],
|
||||
'analyticsSummary' => [
|
||||
'callback' => [ $this, 'get_analytics_summary' ],
|
||||
],
|
||||
'keywordsRows' => [
|
||||
'callback' => [ Stats::get(), 'get_keywords_rows' ],
|
||||
],
|
||||
'userPreferences' => [
|
||||
'callback' => [ $this, 'update_user_preferences' ],
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
],
|
||||
'inspectionResults' => [
|
||||
'callback' => [ $this, 'get_inspection_results' ],
|
||||
],
|
||||
'removeFrontendStats' => [
|
||||
'callback' => [ $this, 'remove_frontend_stats' ],
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ( $routes as $route => $args ) {
|
||||
$this->register_route( $route, $args );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a route.
|
||||
*
|
||||
* @param string $route Route.
|
||||
* @param array $args Arguments.
|
||||
*/
|
||||
private function register_route( $route, $args ) {
|
||||
$route_defaults = [
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'permission_callback' => [ $this, 'has_permission' ],
|
||||
];
|
||||
|
||||
$route_args = wp_parse_args( $args, $route_defaults );
|
||||
|
||||
register_rest_route( $this->namespace, '/' . $route, $route_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current user can manage analytics.
|
||||
*
|
||||
* @return true
|
||||
*/
|
||||
public function has_permission() {
|
||||
return current_user_can( 'rank_math_analytics' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user perferences.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return boolean|WP_Error True on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function update_user_preferences( WP_REST_Request $request ) {
|
||||
$pref = $request->get_param( 'preferences' );
|
||||
if ( empty( $pref ) ) {
|
||||
return new WP_Error(
|
||||
'param_value_empty',
|
||||
esc_html__( 'Sorry, no preference found.', 'rank-math' )
|
||||
);
|
||||
}
|
||||
|
||||
update_user_meta(
|
||||
get_current_user_id(),
|
||||
'rank_math_analytics_table_columns',
|
||||
$pref
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post data.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_post( WP_REST_Request $request ) {
|
||||
$id = $request->get_param( 'id' );
|
||||
if ( empty( $id ) ) {
|
||||
return new WP_Error(
|
||||
'param_value_empty',
|
||||
esc_html__( 'Sorry, no post id found.', 'rank-math' )
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response( Stats::get()->get_post( $request ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dashboard data.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_dashboard( WP_REST_Request $request ) { // phpcs:ignore
|
||||
return rest_ensure_response(
|
||||
[
|
||||
'stats' => Stats::get()->get_analytics_summary(),
|
||||
'optimization' => Stats::get()->get_optimization_summary(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics summary.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_analytics_summary( WP_REST_Request $request ) { // phpcs:ignore
|
||||
$post_type = sanitize_key( $request->get_param( 'postType' ) );
|
||||
return rest_ensure_response(
|
||||
[
|
||||
'summary' => Stats::get()->get_posts_summary( $post_type ),
|
||||
'optimization' => Stats::get()->get_optimization_summary( $post_type ),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get keywords overview.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_keywords_overview( WP_REST_Request $request ) { // phpcs:ignore
|
||||
return rest_ensure_response(
|
||||
apply_filters(
|
||||
'rank_math/analytics/keywords_overview',
|
||||
[
|
||||
'topKeywords' => Stats::get()->get_top_keywords(),
|
||||
'positionGraph' => Stats::get()->get_top_position_graph(),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inspection results: latest result for each post.
|
||||
*
|
||||
* @param WP_REST_Request $request Rest request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_inspection_results( WP_REST_Request $request ) {
|
||||
$per_page = 25;
|
||||
$rows = Url_Inspection::get()->get_inspections( $request->get_params(), $per_page );
|
||||
|
||||
if ( empty( $rows ) ) {
|
||||
return [
|
||||
'rows' => [ 'response' => 'No Data' ],
|
||||
'rowsFound' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
[
|
||||
'rows' => $rows,
|
||||
'rowsFound' => DB::get_inspections_count( $request->get_params() ),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove frontend stats.
|
||||
*
|
||||
* @param WP_REST_Request $request Rest request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function remove_frontend_stats( WP_REST_Request $request ) {
|
||||
if ( (bool) $request->get_param( 'toggleBar' ) ) {
|
||||
$hide_bar = (bool) $request->get_param( 'hide' );
|
||||
$user_id = get_current_user_id();
|
||||
if ( $hide_bar ) {
|
||||
return update_user_meta( $user_id, 'rank_math_hide_frontend_stats', true );
|
||||
}
|
||||
|
||||
return delete_user_meta( $user_id, 'rank_math_hide_frontend_stats' );
|
||||
}
|
||||
|
||||
$all_opts = rank_math()->settings->all_raw();
|
||||
$general = $all_opts['general'];
|
||||
$general['analytics_stats'] = 'off';
|
||||
|
||||
Helper::update_all_settings( $general, null, null );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should update pagespeed record.
|
||||
*
|
||||
* @param int $id Database row id.
|
||||
* @return bool
|
||||
*/
|
||||
private function should_update_pagespeed( $id ) {
|
||||
$record = DB::objects()->where( 'id', $id )->one();
|
||||
|
||||
return \time() > ( \strtotime( $record->pagespeed_refreshed ) + ( DAY_IN_SECONDS * 7 ) );
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* Dashboard page template.
|
||||
*
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Admin
|
||||
*/
|
||||
|
||||
use RankMath\Helper;
|
||||
use RankMath\Google\Authentication;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
// Header.
|
||||
rank_math()->admin->display_admin_header();
|
||||
$path = rank_math()->admin_dir() . 'wizard/views/'; // phpcs:ignore
|
||||
?>
|
||||
<div class="wrap rank-math-wrap analytics">
|
||||
|
||||
<span class="wp-header-end"></span>
|
||||
|
||||
<?php
|
||||
if ( ! Helper::is_site_connected() ) {
|
||||
require_once $path . 'rank-math-connect.php';
|
||||
} elseif ( ! Authentication::is_authorized() ) {
|
||||
require_once $path . 'google-connect.php';
|
||||
} else {
|
||||
echo '<div class="rank-math-analytics" id="rank-math-analytics"></div>';
|
||||
}
|
||||
?>
|
||||
|
||||
</div>
|
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* Analytics Report header template.
|
||||
*
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Admin
|
||||
*/
|
||||
|
||||
use RankMath\KB;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
?>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="cta">
|
||||
<tbody>
|
||||
<tr class="top">
|
||||
<td align="left">
|
||||
<a href="<?php KB::the( 'seo-email-reporting', 'Email Report CTA' ); ?>"><?php $this->image( 'rank-math-pro.jpg', 540, 422, __( 'Rank Math PRO', 'rank-math' ) ); ?></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* Analytics Report email template footer.
|
||||
*
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<tr class="footer">
|
||||
<td class="wrapper">
|
||||
<p class="first">
|
||||
###FOOTER_HTML###
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END FOOTER -->
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
|
||||
</table>
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Analytics Report header template.
|
||||
*
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Admin
|
||||
*/
|
||||
|
||||
use RankMath\Helper;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
?>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="report-info">
|
||||
<tr>
|
||||
<td>
|
||||
<h1><?php esc_html_e( 'SEO Report of Your Website', 'rank-math' ); ?></h1>
|
||||
<h2 class="report-date">###START_DATE### - ###END_DATE###</h2>
|
||||
<a href="###SITE_URL###" target="_blank" class="site-url">###SITE_URL_SIMPLE###</a>
|
||||
</td>
|
||||
<td class="full-report-link">
|
||||
<a href="###REPORT_URL###" target="_blank" class="full-report-link">
|
||||
<?php esc_html_e( 'FULL REPORT', 'rank-math' ); ?>
|
||||
<?php $this->image( 'report-icon-external.png', 12, 12, __( 'External Link Icon', 'rank-math' ) ); ?>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php if ( $this->get_variable( 'stats_invalid_data' ) ) { ?>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="report-error">
|
||||
<tr>
|
||||
<td>
|
||||
<h2><?php esc_html_e( 'Uh-oh', 'rank-math' ); ?></h2>
|
||||
<p><em><?php esc_html_e( 'It seems that there are no stats to show right now.', 'rank-math' ); ?></em></p>
|
||||
<?php // Translators: placeholders are anchor opening and closing tags. ?>
|
||||
<p><?php printf( esc_html__( 'If you can see the site data in your Search Console and Analytics accounts, but not here, then %1$s try reconnecting your account %2$s and make sure that the correct properties are selected in the %1$s Analytics Settings%2$s.', 'rank-math' ), '<a href="' . Helper::get_admin_url( 'options-general#setting-panel-analytics' ) . '">', '</a>' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Analytics Report header template.
|
||||
*
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
?><!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title><?php esc_html_e( 'SEO Report of Your Website', 'rank-math' ); ?></title>
|
||||
|
||||
<?php $this->template_part( 'style' ); ?>
|
||||
</head>
|
||||
<body class="">
|
||||
<span class="preheader"><?php esc_html_e( 'SEO Report of Your Website', 'rank-math' ); ?></span>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<table role="presentation" class="main" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<!-- START HEADER -->
|
||||
<tr>
|
||||
<td class="header">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="logo">
|
||||
<a href="###LOGO_LINK###" target="_blank">
|
||||
<?php $this->image( 'report-logo.png', 0, 26, __( 'Rank Math', 'rank-math' ) ); ?>
|
||||
</a>
|
||||
</td>
|
||||
<td class="period-days">
|
||||
<?php // Translators: don't translate the variable names between the #hashes#. ?>
|
||||
<?php esc_html_e( 'Last ###PERIOD_DAYS### Days', 'rank-math' ); ?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END HEADER -->
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* Analytics Report email template.
|
||||
*
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
$this->template_part( 'header' );
|
||||
|
||||
?>
|
||||
|
||||
<?php $this->template_part( 'header-after' ); ?>
|
||||
|
||||
<?php $this->template_part( 'sections/summary' ); ?>
|
||||
|
||||
<?php $this->template_part( 'sections/positions' ); ?>
|
||||
|
||||
<?php $this->template_part( 'cta' ); ?>
|
||||
|
||||
<?php
|
||||
$this->template_part( 'footer' );
|
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Analytics Report summary table template.
|
||||
*
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
?>
|
||||
|
||||
<?php if ( $this->get_variable( 'stats_invalid_data' ) ) { ?>
|
||||
<?php return; ?>
|
||||
<?php } ?>
|
||||
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="stats-2">
|
||||
<tr>
|
||||
<td class="col-1">
|
||||
<h3><?php esc_html_e( 'Top 3 Positions', 'rank-math' ); ?></h3>
|
||||
<?php
|
||||
$this->template_part(
|
||||
'stat',
|
||||
[
|
||||
'value' => $this->get_variable( 'stats_top_3_positions' ),
|
||||
'diff' => $this->get_variable( 'stats_top_3_positions_diff' ),
|
||||
]
|
||||
);
|
||||
?>
|
||||
</td>
|
||||
<td class="col-2">
|
||||
<h3><?php esc_html_e( '4-10 Positions', 'rank-math' ); ?></h3>
|
||||
<?php
|
||||
$this->template_part(
|
||||
'stat',
|
||||
[
|
||||
'value' => $this->get_variable( 'stats_top_10_positions' ),
|
||||
'diff' => $this->get_variable( 'stats_top_10_positions_diff' ),
|
||||
]
|
||||
);
|
||||
?>
|
||||
</td>
|
||||
<td class="col-3">
|
||||
<h3><?php esc_html_e( '11-50 Positions', 'rank-math' ); ?></h3>
|
||||
<?php
|
||||
$this->template_part(
|
||||
'stat',
|
||||
[
|
||||
'value' => $this->get_variable( 'stats_top_50_positions' ),
|
||||
'diff' => $this->get_variable( 'stats_top_50_positions_diff' ),
|
||||
]
|
||||
);
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* Analytics Report summary table template.
|
||||
*
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
?>
|
||||
|
||||
<?php if ( $this->get_variable( 'stats_invalid_data' ) ) { ?>
|
||||
<?php return; ?>
|
||||
<?php } ?>
|
||||
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="stats">
|
||||
<tr>
|
||||
<td class="col-1">
|
||||
<h3><?php esc_html_e( 'Total Impressions', 'rank-math' ); ?></h3>
|
||||
<?php
|
||||
$this->template_part(
|
||||
'stat',
|
||||
[
|
||||
'value' => $this->get_variable( 'stats_impressions' ),
|
||||
'diff' => $this->get_variable( 'stats_impressions_diff' ),
|
||||
'graph' => true,
|
||||
'graph_data' => $this->get_graph_data( 'impressions' ),
|
||||
]
|
||||
);
|
||||
?>
|
||||
</td>
|
||||
<td class="col-2">
|
||||
<h3><?php esc_html_e( 'Total Clicks', 'rank-math' ); ?></h3>
|
||||
<?php
|
||||
$this->template_part(
|
||||
'stat',
|
||||
[
|
||||
'value' => $this->get_variable( 'stats_clicks' ),
|
||||
'diff' => $this->get_variable( 'stats_clicks_diff' ),
|
||||
'graph' => true,
|
||||
'graph_data' => $this->get_graph_data( 'clicks' ),
|
||||
]
|
||||
);
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="col-1">
|
||||
<h3><?php esc_html_e( 'Total Keywords', 'rank-math' ); ?></h3>
|
||||
<?php
|
||||
$this->template_part(
|
||||
'stat',
|
||||
[
|
||||
'value' => $this->get_variable( 'stats_keywords' ),
|
||||
'diff' => $this->get_variable( 'stats_keywords_diff' ),
|
||||
'graph' => true,
|
||||
'graph_data' => $this->get_graph_data( 'keywords' ),
|
||||
]
|
||||
);
|
||||
?>
|
||||
</td>
|
||||
<td class="col-2">
|
||||
<h3><?php esc_html_e( 'Average Position', 'rank-math' ); ?></h3>
|
||||
<?php
|
||||
$this->template_part(
|
||||
'stat',
|
||||
[
|
||||
'value' => $this->get_variable( 'stats_position' ),
|
||||
'diff' => $this->get_variable( 'stats_position_diff' ),
|
||||
'graph' => true,
|
||||
'graph_data' => $this->get_graph_data( 'position' ),
|
||||
'graph_modifier' => -100,
|
||||
'human_number' => false,
|
||||
'invert' => true,
|
||||
]
|
||||
);
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
/**
|
||||
* Analytics Report header template.
|
||||
*
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Admin
|
||||
*/
|
||||
|
||||
use RankMath\Helpers\Str;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
$diff_class = $diff > 0 ? 'positive' : 'negative';
|
||||
|
||||
if ( ! empty( $invert ) ) {
|
||||
$diff_class = $diff < 0 ? 'positive' : 'negative';
|
||||
}
|
||||
|
||||
$diff_sign = '<span class="diff-sign">' . ( 'positive' === $diff_class ? '▲' : '▼' ) . '</span>';
|
||||
|
||||
if ( 0.0 === floatval( $diff ) ) {
|
||||
$diff_class = 'no-diff';
|
||||
$diff_sign = '';
|
||||
}
|
||||
|
||||
$stat_value = $value;
|
||||
$stat_diff = abs( $diff );
|
||||
|
||||
// Human number is 'true' by default.
|
||||
if ( ! isset( $human_number ) || $human_number ) {
|
||||
$stat_value = Str::human_number( $stat_value );
|
||||
$stat_diff = Str::human_number( $stat_diff );
|
||||
}
|
||||
|
||||
?>
|
||||
<span class="stat-value">
|
||||
<?php echo esc_html( $stat_value ); ?>
|
||||
</span>
|
||||
<span class="stat-diff <?php echo sanitize_html_class( $diff_class ); ?>">
|
||||
<?php echo $diff_sign . ' ' . esc_html( $stat_diff ); // phpcs:ignore ?>
|
||||
</span>
|
||||
|
||||
<?php
|
||||
if ( ! empty( $graph ) && ! empty( $graph_data ) ) {
|
||||
|
||||
$show_graph = false;
|
||||
// Check data points.
|
||||
foreach ( $graph_data as $key => $value ) {
|
||||
if ( ! empty( $value ) ) {
|
||||
$show_graph = true;
|
||||
}
|
||||
|
||||
// Adjust values.
|
||||
if ( ! empty( $graph_modifier ) ) {
|
||||
$graph_data[ $key ] = abs( $graph_data[ $key ] + $graph_modifier );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $show_graph ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// `img` tag size.
|
||||
// Actual image size is 3x this.
|
||||
$width = 64;
|
||||
$height = 34;
|
||||
|
||||
$this->image( $this->charts_api_url( $graph_data, $width * 3, $height * 3 ), $width, $height, __( 'Data Chart', 'rank-math' ), [ 'style' => 'float: right;margin-top: -7px;' ] );
|
||||
} ?>
|
@@ -0,0 +1,496 @@
|
||||
<?php
|
||||
/**
|
||||
* Analytics Report email styling.
|
||||
*
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
?>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
/* All the styling goes here */
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f7f9fb;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
font-size: 15px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
.body, td {
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;
|
||||
}
|
||||
|
||||
.body {
|
||||
background-color: #F0F4F8;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block. */
|
||||
.container {
|
||||
display: block;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 90%;
|
||||
padding: 50px 0;
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
color: #1a1e22;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 30px 30px 60px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #724BB7;
|
||||
background: linear-gradient(90deg, #724BB7 0%, #4098D7 100%);
|
||||
border-radius: 8px 8px 0 0;
|
||||
height: 76px;
|
||||
vertical-align: middle;
|
||||
padding: 0 30px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
td.logo {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td.logo img {
|
||||
width: auto;
|
||||
height: 26px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.period-days {
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer .wrapper {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span {
|
||||
color: #999ba7;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer td {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.footer p.first {
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e5e5e7;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.footer .rank-math-contact-address {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.footer p:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.footer address {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #000000;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #22a8e6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h2.report-date {
|
||||
margin: 25px 0 4px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.site-url {
|
||||
color: #595d6f;
|
||||
text-decoration: none;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.full-report-link {
|
||||
vertical-align: bottom;
|
||||
text-align: right;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.full-report-link a {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.full-report-link img {
|
||||
vertical-align: -1px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
table.report-error {
|
||||
border: 2px solid #f1d400;
|
||||
background: #fffdec;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
table.report-error td {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
table.stats {
|
||||
border-collapse: separate;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
table.stats td {
|
||||
width: 50%;
|
||||
padding: 20px 20px;
|
||||
background: #f7f9fb;
|
||||
border: 10px solid #fff;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
table.stats td.col-2 {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
table.stats td.col-1 {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #565a6b;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #000000;
|
||||
font-size: 25px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-diff {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-diff.positive {
|
||||
color: #339e75;
|
||||
}
|
||||
|
||||
span.stat-diff.negative {
|
||||
color: #e2454f;
|
||||
}
|
||||
|
||||
.stat-diff.no-diff {
|
||||
color: #999ba7;
|
||||
}
|
||||
|
||||
.diff-sign {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.stats-2 {
|
||||
margin: 50px 0 24px;
|
||||
}
|
||||
|
||||
.stats-2 td.col-1, .stats-2 td.col-2 {
|
||||
border-right: 3px solid #f7f9fb;
|
||||
}
|
||||
|
||||
.stats-2 td.col-2, .stats-2 td.col-3 {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.cta {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn > tbody > tr > td {
|
||||
padding-bottom: 48px;
|
||||
text-align: center;
|
||||
padding-top: 34px;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
border: none;
|
||||
border-radius: 31px;
|
||||
box-sizing: border-box;
|
||||
color: #59403b;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
padding: 18px 44px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
background: rgb(47,166,129);
|
||||
background: linear-gradient( 0deg, #f7d070 0%, #f7dc6f 100%);
|
||||
letter-spacing: 0.7px;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #3498db;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #F0F4F8;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.rankmath-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php $this->template_part( 'pro-style' ); ?>
|
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
/**
|
||||
* Search console options.
|
||||
*
|
||||
* @package Rank_Math
|
||||
*/
|
||||
|
||||
use RankMath\KB;
|
||||
use RankMath\Helper;
|
||||
use RankMath\Analytics\DB;
|
||||
use RankMath\Helpers\Str;
|
||||
use RankMath\Google\Authentication;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
// phpcs:disable
|
||||
$actions = \as_get_scheduled_actions(
|
||||
[
|
||||
'hook' => 'rank_math/analytics/clear_cache',
|
||||
'status' => \ActionScheduler_Store::STATUS_PENDING,
|
||||
]
|
||||
);
|
||||
$db_info = DB::info();
|
||||
$is_queue_empty = empty( $actions );
|
||||
$disable = ( ! Authentication::is_authorized() || ! $is_queue_empty ) ? true : false;
|
||||
|
||||
if ( ! empty( $db_info ) ) {
|
||||
$db_info = [
|
||||
/* translators: number of days */
|
||||
'<div class="rank-math-console-db-info"><i class="rm-icon rm-icon-calendar"></i> ' . sprintf( esc_html__( 'Storage Days: %s', 'rank-math' ), '<strong>' . $db_info['days'] . '</strong>' ) . '</div>',
|
||||
/* translators: number of rows */
|
||||
'<div class="rank-math-console-db-info"><i class="rm-icon rm-icon-faq"></i> ' . sprintf( esc_html__( 'Data Rows: %s', 'rank-math' ), '<strong>' . Str::human_number( $db_info['rows'] ) . '</strong>' ) . '</div>',
|
||||
/* translators: database size */
|
||||
'<div class="rank-math-console-db-info"><i class="rm-icon rm-icon-database"></i> ' . sprintf( esc_html__( 'Size: %s', 'rank-math' ), '<strong>' . size_format( $db_info['size'] ) . '</strong>' ) . '</div>',
|
||||
];
|
||||
}
|
||||
|
||||
$actions = as_get_scheduled_actions(
|
||||
[
|
||||
'order' => 'DESC',
|
||||
'hook' => 'rank_math/analytics/data_fetch',
|
||||
'status' => \ActionScheduler_Store::STATUS_PENDING,
|
||||
]
|
||||
);
|
||||
if ( Authentication::is_authorized() && ! empty( $actions ) ) {
|
||||
$action = current( $actions );
|
||||
$schedule = $action->get_schedule();
|
||||
$next_date = $schedule->get_date();
|
||||
if ( $next_date ) {
|
||||
$cmb->add_field(
|
||||
[
|
||||
'id' => 'console_data_empty',
|
||||
'type' => 'raw',
|
||||
/* translators: date */
|
||||
'content' => sprintf(
|
||||
'<span class="next-fetch">' . __( 'Next data fetch on %s', 'rank-math' ),
|
||||
date_i18n( 'd M, Y H:m:i', $next_date->getTimestamp() ) . '</span>'
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
// phpcs:enable
|
||||
|
||||
$cmb->add_field(
|
||||
[
|
||||
'id' => 'search_console_ui',
|
||||
'type' => 'raw',
|
||||
'file' => rank_math()->admin_dir() . '/wizard/views/search-console-ui.php',
|
||||
]
|
||||
);
|
||||
|
||||
if ( ! Authentication::is_authorized() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$is_fetching = 'fetching' === get_option( 'rank_math_analytics_first_fetch' );
|
||||
$buttons = '<br>' .
|
||||
'<button class="button button-small console-cache-delete" data-days="-1">' . esc_html__( 'Delete data', 'rank-math' ) . '</button>' .
|
||||
' <button class="button button-small console-cache-update-manually"' . ( $disable ? ' disabled="disabled"' : '' ) . '>' . ( $is_queue_empty ? esc_html__( 'Update data manually', 'rank-math' ) : esc_html__( 'Fetching in Progress', 'rank-math' ) ) . '</button>' .
|
||||
' <button class="button button-link-delete button-small cancel-fetch"' . disabled( $is_fetching, false, false ) . '>' . esc_html__( 'Cancel Fetching', 'rank-math' ) . '</button>';
|
||||
|
||||
$buttons .= '<br>' . join( '', $db_info );
|
||||
|
||||
// Translators: placeholder is a link to rankmath.com, with "free version" as the anchor text.
|
||||
$description = sprintf( __( 'Enter the number of days to keep Analytics data in your database. The maximum allowed days are 90 in the %s. Though, 2x data will be stored in the DB for calculating the difference properly.', 'rank-math' ), '<a href="' . KB::get( 'pro', 'Analytics DB Option' ) . '" target="_blank" rel="noopener noreferrer">' . __( 'free version', 'rank-math' ) . '</a>' );
|
||||
$description = apply_filters_deprecated( 'rank_math/analytics/options/cahce_control/description', [ $description ], '1.0.61.1', 'rank_math/analytics/options/cache_control/description' );
|
||||
$description = apply_filters( 'rank_math/analytics/options/cache_control/description', $description );
|
||||
|
||||
$cmb->add_field(
|
||||
[
|
||||
'id' => 'console_caching_control',
|
||||
'type' => 'text',
|
||||
'name' => __( 'Analytics Database', 'rank-math' ),
|
||||
// translators: Anchor text 'free version', linking to pricing page.
|
||||
'description' => $description,
|
||||
'default' => 90,
|
||||
'sanitization_cb' => function( $value ) {
|
||||
$max = apply_filters( 'rank_math/analytics/max_days_allowed', 90 );
|
||||
$value = absint( $value );
|
||||
if ( $value > $max ) {
|
||||
$value = $max;
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
'after_field' => $buttons,
|
||||
]
|
||||
);
|
||||
|
||||
$cmb->add_field(
|
||||
[
|
||||
'id' => 'analytics_stats',
|
||||
'type' => 'toggle',
|
||||
'name' => __( 'Frontend Stats Bar', 'rank-math' ),
|
||||
'description' => esc_html__( 'Enable this option to show Analytics Stats on the front just after the admin bar.', 'rank-math' ),
|
||||
'default' => 'on',
|
||||
]
|
||||
);
|
||||
|
||||
if ( RankMath\Analytics\Email_Reports::are_fields_hidden() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$preview_url = home_url( '?rank_math_analytics_report_preview=1' );
|
||||
$title = esc_html__( 'Email Reports', 'rank-math' );
|
||||
// Translators: Placeholders are the opening and closing tag for the link.
|
||||
$description = sprintf( esc_html__( 'Receive periodic SEO Performance reports via email. Once enabled and options are saved, you can see %1$s the preview here%2$s.', 'rank-math' ), '<a href="' . esc_url_raw( $preview_url ) . '" target="_blank">', '</a>' );
|
||||
$cmb->add_field(
|
||||
[
|
||||
'id' => 'email_reports_title',
|
||||
'type' => 'raw',
|
||||
'content' => sprintf( '<div class="cmb-form cmb-row nopb"><header class="email-reports-title"><h3>%1$s</h3><p class="description">%2$s</p></header></div>', $title, $description ),
|
||||
]
|
||||
);
|
||||
|
||||
$cmb->add_field(
|
||||
[
|
||||
'id' => 'console_email_reports',
|
||||
'type' => 'toggle',
|
||||
'name' => __( 'Email Reports', 'rank-math' ),
|
||||
'description' => __( 'Turn on email reports.', 'rank-math' ),
|
||||
'default' => Helper::get_settings( 'general.console_email_reports' ) ? 'on' : 'off',
|
||||
'classes' => 'nob',
|
||||
]
|
||||
);
|
||||
|
||||
$is_pro_active = defined( 'RANK_MATH_PRO_FILE' );
|
||||
$pro_badge = '<span class="rank-math-pro-badge"><a href="' . KB::get( 'seo-email-reporting', 'Email Frequency Toggle' ) . '" target="_blank" rel="noopener noreferrer">' . __( 'PRO', 'rank-math' ) . '</a></span>';
|
||||
$args = [
|
||||
'id' => 'console_email_frequency',
|
||||
'type' => 'select',
|
||||
'name' => esc_html__( 'Email Frequency', 'rank-math' ) . ( ! $is_pro_active ? $pro_badge : '' ),
|
||||
'desc' => wp_kses_post( __( 'Email report frequency.', 'rank-math' ) ),
|
||||
'default' => 'monthly',
|
||||
'options' => [
|
||||
'monthly' => esc_html__( 'Every 30 days', 'rank-math' ),
|
||||
],
|
||||
'dep' => [ [ 'console_email_reports', 'on' ] ],
|
||||
'attributes' => ! $is_pro_active ? [ 'disabled' => 'disabled' ] : [],
|
||||
'before_row' => ! $is_pro_active ? '<div class="cmb-redirector-element" data-url="' . KB::get( 'seo-email-reporting', 'Email Frequency Toggle' ) . '">' : '',
|
||||
'after_row' => ! $is_pro_active ? '</div>' : '',
|
||||
];
|
||||
|
||||
$cmb->add_field( $args );
|
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
/**
|
||||
* Workflow Base.
|
||||
*
|
||||
* @since 1.0.54
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics\Workflow;
|
||||
|
||||
use RankMath\Helper;
|
||||
use function has_filter;
|
||||
use RankMath\Analytics\DB;
|
||||
use RankMath\Traits\Hooker;
|
||||
use function as_schedule_single_action;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Base class.
|
||||
*/
|
||||
abstract class Base {
|
||||
|
||||
use Hooker;
|
||||
|
||||
/**
|
||||
* Start fetching process.
|
||||
*
|
||||
* @param integer $days Number of days to fetch from past.
|
||||
* @param string $action Action to perform.
|
||||
* @return integer
|
||||
*/
|
||||
public function create_jobs( $days = 90, $action = 'console' ) {
|
||||
$count = $this->add_data_pull( $days + 3, $action );
|
||||
$time_gap = $this->get_schedule_gap();
|
||||
|
||||
Workflow::add_clear_cache( time() + ( $time_gap * ( $count + 1 ) ) );
|
||||
|
||||
update_option( 'rank_math_analytics_first_fetch', 'fetching' );
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data pull jobs.
|
||||
*
|
||||
* @param integer $days Number of days to fetch from past.
|
||||
* @param string $action Action to perform.
|
||||
* @return integer
|
||||
*/
|
||||
private function add_data_pull( $days, $action = 'console' ) {
|
||||
$count = 1;
|
||||
$start = Helper::get_midnight( time() + DAY_IN_SECONDS );
|
||||
$interval = $this->get_data_interval();
|
||||
$time_gap = $this->get_schedule_gap();
|
||||
|
||||
$hook = "get_{$action}_data";
|
||||
if ( 1 === $interval ) {
|
||||
for ( $current = 1; $current <= $days; $current++ ) {
|
||||
$date = date_i18n( 'Y-m-d', $start - ( DAY_IN_SECONDS * $current ) );
|
||||
if ( ! DB::date_exists( $date, $action ) ) {
|
||||
$count++;
|
||||
as_schedule_single_action(
|
||||
time() + ( $time_gap * $count ),
|
||||
'rank_math/analytics/' . $hook,
|
||||
[ $date ],
|
||||
'rank-math'
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for ( $current = 1; $current <= $days; $current = $current + $interval ) {
|
||||
for ( $j = 0; $j < $interval; $j++ ) {
|
||||
$date = date_i18n( 'Y-m-d', $start - ( DAY_IN_SECONDS * ( $current + $j ) ) );
|
||||
if ( ! DB::date_exists( $date, $action ) ) {
|
||||
$count++;
|
||||
as_schedule_single_action(
|
||||
time() + ( $time_gap * $count ),
|
||||
'rank_math/analytics/' . $hook,
|
||||
[ $date ],
|
||||
'rank-math'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data interval.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function get_data_interval() {
|
||||
$is_custom = has_filter( 'rank_math/analytics/app_url' );
|
||||
|
||||
return $is_custom ? $this->do_filter( 'analytics/data_interval', 7 ) : 7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schedule gap.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function get_schedule_gap() {
|
||||
return $this->do_filter( 'analytics/schedule_gap', 30 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if google profile is updated.
|
||||
*
|
||||
* @param string $param Google profile param name.
|
||||
* @param string $prev Previous profile data.
|
||||
* @param string $new New posted profile data.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_profile_updated( $param, $prev, $new ) {
|
||||
if (
|
||||
! is_null( $prev ) &&
|
||||
! is_null( $new ) &&
|
||||
isset( $prev[ $param ] ) &&
|
||||
isset( $new[ $param ] ) &&
|
||||
$prev[ $param ] === $new[ $param ]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to get the dates.
|
||||
*
|
||||
* @param int $days Number of days.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_dates( $days = 90 ) {
|
||||
$end = Helper::get_midnight( strtotime( '-1 day', time() ) );
|
||||
$start = strtotime( '-' . $days . ' day', $end );
|
||||
|
||||
return [
|
||||
'start_date' => date_i18n( 'Y-m-d', $start ),
|
||||
'end_date' => date_i18n( 'Y-m-d', $end ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule single action
|
||||
*
|
||||
* @param int $days Number of days.
|
||||
* @param string $action Name of the action hook.
|
||||
* @param array $args Arguments to pass to callbacks when the hook triggers.
|
||||
* @param string $group The group to assign this job to.
|
||||
* @param boolean $unique Whether the action should be unique.
|
||||
*/
|
||||
public function schedule_single_action( $days = 90, $action = '', $args = [], $group = 'rank-math', $unique = false ) {
|
||||
$timestamp = get_option( 'rank_math_analytics_last_single_action_schedule_time', time() );
|
||||
$time_gap = $this->get_schedule_gap();
|
||||
|
||||
$dates = self::get_dates( $days );
|
||||
|
||||
// Get the analytics dates in which analytics data is actually available.
|
||||
$days = apply_filters(
|
||||
'rank_math/analytics/get_' . $action . '_days',
|
||||
[
|
||||
'start_date' => $dates['start_date'],
|
||||
'end_date' => $dates['end_date'],
|
||||
]
|
||||
);
|
||||
|
||||
// No days then don't schedule the action.
|
||||
if ( empty( $days ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $days as $day ) {
|
||||
|
||||
// Next schedule time.
|
||||
$timestamp = $timestamp + $time_gap;
|
||||
|
||||
$args = wp_parse_args(
|
||||
[
|
||||
'start_date' => $day['start_date'],
|
||||
'end_date' => $day['end_date'],
|
||||
],
|
||||
$args
|
||||
);
|
||||
|
||||
as_schedule_single_action(
|
||||
$timestamp,
|
||||
'rank_math/analytics/get_' . $action . '_data',
|
||||
$args,
|
||||
$group,
|
||||
$unique
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
Workflow::add_clear_cache( $timestamp );
|
||||
|
||||
// Update timestamp.
|
||||
update_option( 'rank_math_analytics_last_single_action_schedule_time', $timestamp );
|
||||
}
|
||||
}
|
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
/**
|
||||
* Google Search Console.
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Analytics
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics\Workflow;
|
||||
|
||||
use Exception;
|
||||
use RankMath\Helpers\DB;
|
||||
use RankMath\Google\Console as GoogleConsole;
|
||||
use function as_unschedule_all_actions;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Console class.
|
||||
*/
|
||||
class Console extends Base {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
$this->create_tables();
|
||||
|
||||
// If console is not connected, ignore all no need to proceed.
|
||||
if ( ! GoogleConsole::is_console_connected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->action( 'rank_math/analytics/workflow/console', 'kill_jobs', 5, 0 );
|
||||
$this->action( 'rank_math/analytics/workflow/create_tables', 'create_tables' );
|
||||
$this->action( 'rank_math/analytics/workflow/console', 'create_tables', 6, 0 );
|
||||
$this->action( 'rank_math/analytics/workflow/console', 'create_data_jobs', 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unschedule all console data fetch action.
|
||||
*
|
||||
* Stop processing queue items, clear cronjob and delete all batches.
|
||||
*/
|
||||
public function kill_jobs() {
|
||||
as_unschedule_all_actions( 'rank_math/analytics/get_console_data' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tables.
|
||||
*/
|
||||
public function create_tables() {
|
||||
global $wpdb;
|
||||
|
||||
$collate = $wpdb->get_charset_collate();
|
||||
$table = 'rank_math_analytics_gsc';
|
||||
|
||||
// Early Bail!!
|
||||
if ( DB::check_table_exists( $table ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema = "CREATE TABLE {$wpdb->prefix}{$table} (
|
||||
id bigint(20) unsigned NOT NULL auto_increment,
|
||||
created timestamp NOT NULL,
|
||||
query varchar(1000) NOT NULL,
|
||||
page varchar(500) NOT NULL,
|
||||
clicks mediumint(6) NOT NULL,
|
||||
impressions mediumint(6) NOT NULL,
|
||||
position double NOT NULL,
|
||||
ctr double NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY analytics_query (query(190)),
|
||||
KEY analytics_page (page(190)),
|
||||
KEY clicks (clicks),
|
||||
KEY rank_position (position)
|
||||
) $collate;";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
try {
|
||||
dbDelta( $schema );
|
||||
} catch ( Exception $e ) { // phpcs:ignore
|
||||
// Will log.
|
||||
}
|
||||
|
||||
// Make sure that collations match the objects table.
|
||||
$objects_coll = DB::get_table_collation( 'rank_math_analytics_objects' );
|
||||
DB::check_collation( $table, 'all', $objects_coll );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create jobs to fetch data.
|
||||
*
|
||||
* @param integer $days Number of days to fetch from past.
|
||||
* @param string $prev Previous saved value.
|
||||
* @param string $new New posted value.
|
||||
*/
|
||||
public function create_data_jobs( $days, $prev, $new ) {
|
||||
// Early bail if saved & new profile are same.
|
||||
if ( ! $this->is_profile_updated( 'profile', $prev, $new ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
update_option( 'rank_math_analytics_first_fetch', 'fetching' );
|
||||
|
||||
// Fetch now.
|
||||
$this->schedule_single_action( $days, 'console' );
|
||||
}
|
||||
}
|
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* Google Search Console.
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Analytics
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics\Workflow;
|
||||
|
||||
use Exception;
|
||||
use RankMath\Helpers\DB;
|
||||
use RankMath\Traits\Hooker;
|
||||
use RankMath\Analytics\DB as AnalyticsDB;
|
||||
use RankMath\Analytics\Url_Inspection;
|
||||
use RankMath\Google\Console;
|
||||
|
||||
use function as_unschedule_all_actions;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Inspections class.
|
||||
*/
|
||||
class Inspections {
|
||||
|
||||
use Hooker;
|
||||
|
||||
/**
|
||||
* API Limit.
|
||||
* 600 requests per minute, 2000 per day.
|
||||
* We can ignore the per-minute limit, since we will use a few seconds delay after each request.
|
||||
*/
|
||||
const API_LIMIT = 2000;
|
||||
|
||||
/**
|
||||
* Interval between requests.
|
||||
*/
|
||||
const REQUEST_GAP_SECONDS = 7;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
$this->create_tables();
|
||||
|
||||
// If console is not connected, ignore all, no need to proceed.
|
||||
if ( ! Console::is_console_connected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->action( 'rank_math/analytics/workflow/create_tables', 'create_tables' );
|
||||
$this->action( 'rank_math/analytics/workflow/inspections', 'create_tables', 6, 0 );
|
||||
$this->action( 'rank_math/analytics/workflow/inspections', 'create_data_jobs', 10, 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unschedule all inspections data fetch action.
|
||||
*
|
||||
* Stop processing queue items, clear cronjob and delete all batches.
|
||||
*/
|
||||
public static function kill_jobs() {
|
||||
as_unschedule_all_actions( 'rank_math/analytics/get_inspections_data' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tables.
|
||||
*/
|
||||
public function create_tables() {
|
||||
global $wpdb;
|
||||
|
||||
$collate = $wpdb->get_charset_collate();
|
||||
$table = 'rank_math_analytics_inspections';
|
||||
|
||||
// Early Bail!!
|
||||
if ( DB::check_table_exists( $table ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema = "CREATE TABLE {$wpdb->prefix}{$table} (
|
||||
id bigint(20) unsigned NOT NULL auto_increment,
|
||||
page varchar(500) NOT NULL,
|
||||
created timestamp NOT NULL,
|
||||
index_verdict varchar(64) NOT NULL, /* PASS, PARTIAL, FAIL, NEUTRAL, VERDICT_UNSPECIFIED */
|
||||
indexing_state varchar(64) NOT NULL, /* INDEXING_ALLOWED, BLOCKED_BY_META_TAG, BLOCKED_BY_HTTP_HEADER, BLOCKED_BY_ROBOTS_TXT, INDEXING_STATE_UNSPECIFIED */
|
||||
coverage_state text NOT NULL, /* String, e.g. 'Submitted and indexed'. */
|
||||
page_fetch_state varchar(64) NOT NULL, /* SUCCESSFUL, SOFT_404, BLOCKED_ROBOTS_TXT, NOT_FOUND, ACCESS_DENIED, SERVER_ERROR, REDIRECT_ERROR, ACCESS_FORBIDDEN, BLOCKED_4XX, INTERNAL_CRAWL_ERROR, INVALID_URL, PAGE_FETCH_STATE_UNSPECIFIED */
|
||||
robots_txt_state varchar(64) NOT NULL, /* ALLOWED, DISALLOWED, ROBOTS_TXT_STATE_UNSPECIFIED */
|
||||
mobile_usability_verdict varchar(64) NOT NULL, /* PASS, PARTIAL, FAIL, NEUTRAL, VERDICT_UNSPECIFIED */
|
||||
mobile_usability_issues longtext NOT NULL, /* JSON */
|
||||
rich_results_verdict varchar(64) NOT NULL, /* PASS, PARTIAL, FAIL, NEUTRAL, VERDICT_UNSPECIFIED */
|
||||
rich_results_items longtext NOT NULL, /* JSON */
|
||||
last_crawl_time timestamp NOT NULL,
|
||||
crawled_as varchar(64) NOT NULL, /* DESKTOP, MOBILE, CRAWLING_USER_AGENT_UNSPECIFIED */
|
||||
google_canonical text NOT NULL, /* Google-chosen canonical URL. */
|
||||
user_canonical text NOT NULL, /* Canonical URL declared on-page. */
|
||||
sitemap text NOT NULL, /* Sitemap URL. */
|
||||
referring_urls longtext NOT NULL, /* JSON */
|
||||
raw_api_response longtext NOT NULL, /* JSON */
|
||||
PRIMARY KEY (id),
|
||||
KEY analytics_object_page (page(190)),
|
||||
KEY created (created),
|
||||
KEY index_verdict (index_verdict),
|
||||
KEY page_fetch_state (page_fetch_state),
|
||||
KEY robots_txt_state (robots_txt_state),
|
||||
KEY mobile_usability_verdict (mobile_usability_verdict),
|
||||
KEY rich_results_verdict (rich_results_verdict)
|
||||
) $collate;";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
try {
|
||||
dbDelta( $schema );
|
||||
} catch ( Exception $e ) { // phpcs:ignore
|
||||
// Will log.
|
||||
}
|
||||
|
||||
// Make sure that collations match the objects table.
|
||||
$objects_coll = DB::get_table_collation( 'rank_math_analytics_objects' );
|
||||
DB::check_collation( $table, 'all', $objects_coll );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create jobs to fetch data.
|
||||
*/
|
||||
public function create_data_jobs() {
|
||||
// If there are jobs left from the previous queue, don't create new jobs.
|
||||
if ( as_has_scheduled_action( 'rank_math/analytics/get_inspections_data' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the option is disabled, don't create jobs.
|
||||
if ( ! Url_Inspection::is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$inspections_table = AnalyticsDB::inspections()->table;
|
||||
$objects_table = AnalyticsDB::objects()->table;
|
||||
|
||||
$objects = AnalyticsDB::objects()
|
||||
->select( [ "$objects_table.id", "$objects_table.page", "$inspections_table.created" ] )
|
||||
->leftJoin( $inspections_table, "$inspections_table.page", "$objects_table.page" )
|
||||
->where( "$objects_table.is_indexable", 1 )
|
||||
->orderBy( "$inspections_table.created", 'ASC' )
|
||||
->get();
|
||||
|
||||
$count = 0;
|
||||
foreach ( $objects as $object ) {
|
||||
$count++;
|
||||
$time = time() + ( $count * self::REQUEST_GAP_SECONDS );
|
||||
if ( $count > self::API_LIMIT ) {
|
||||
$delay_days = floor( $count / self::API_LIMIT );
|
||||
$time = strtotime( "+{$delay_days} days", $time );
|
||||
}
|
||||
|
||||
as_schedule_single_action( $time, 'rank_math/analytics/get_inspections_data', [ $object->page ], 'rank-math' );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
/**
|
||||
* Jobs.
|
||||
*
|
||||
* @since 1.0.54
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics\Workflow;
|
||||
|
||||
use Exception;
|
||||
use RankMath\Helper;
|
||||
use RankMath\Google\Api;
|
||||
use RankMath\Google\Console;
|
||||
use RankMath\Google\Url_Inspection;
|
||||
use RankMath\Analytics\DB;
|
||||
use RankMath\Traits\Cache;
|
||||
use RankMath\Traits\Hooker;
|
||||
use RankMath\Analytics\Stats;
|
||||
use RankMath\Analytics\Watcher;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Jobs class.
|
||||
*/
|
||||
class Jobs {
|
||||
|
||||
use Hooker, Cache;
|
||||
|
||||
/**
|
||||
* Main instance
|
||||
*
|
||||
* Ensure only one instance is loaded or can be loaded.
|
||||
*
|
||||
* @return Jobs
|
||||
*/
|
||||
public static function get() {
|
||||
static $instance;
|
||||
|
||||
if ( is_null( $instance ) && ! ( $instance instanceof Jobs ) ) {
|
||||
$instance = new Jobs();
|
||||
$instance->hooks();
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks.
|
||||
*/
|
||||
public function hooks() {
|
||||
$this->action( 'rank_math/analytics/flat_posts', 'do_flat_posts' );
|
||||
$this->action( 'rank_math/analytics/flat_posts_completed', 'flat_posts_completed' );
|
||||
add_action( 'rank_math/analytics/sync_sitemaps', [ Api::get(), 'sync_sitemaps' ] );
|
||||
|
||||
if ( Console::is_console_connected() ) {
|
||||
$this->action( 'rank_math/analytics/clear_cache', 'clear_cache', 99 );
|
||||
|
||||
// Fetch missing google data action.
|
||||
$this->action( 'rank_math/analytics/data_fetch', 'data_fetch' );
|
||||
|
||||
// Console data fetch.
|
||||
$this->filter( 'rank_math/analytics/get_console_days', 'get_console_days' );
|
||||
$this->action( 'rank_math/analytics/get_console_data', 'get_console_data' );
|
||||
$this->action( 'rank_math/analytics/handle_console_response', 'handle_console_response' );
|
||||
|
||||
// Inspections data fetch.
|
||||
$this->action( 'rank_math/analytics/get_inspections_data', 'get_inspections_data' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch missing console data.
|
||||
*/
|
||||
public function data_fetch() {
|
||||
$this->check_for_missing_dates( 'console' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform post check.
|
||||
*/
|
||||
public function flat_posts_completed() {
|
||||
$rows = DB::objects()
|
||||
->selectCount( 'id' )
|
||||
->getVar();
|
||||
|
||||
Workflow::kill_workflows();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/update posts info from objects table.
|
||||
*
|
||||
* @param array $ids Posts ids to process.
|
||||
*/
|
||||
public function do_flat_posts( $ids ) {
|
||||
Inspections::kill_jobs();
|
||||
|
||||
foreach ( $ids as $id ) {
|
||||
Watcher::get()->update_post_info( $id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache.
|
||||
*/
|
||||
public function clear_cache() {
|
||||
global $wpdb;
|
||||
|
||||
// Delete all useless data from console data table.
|
||||
$wpdb->get_results( "DELETE FROM {$wpdb->prefix}rank_math_analytics_gsc WHERE page NOT IN ( SELECT page from {$wpdb->prefix}rank_math_analytics_objects )" );
|
||||
|
||||
// Delete useless data from inspections table too.
|
||||
$wpdb->get_results( "DELETE FROM {$wpdb->prefix}rank_math_analytics_inspections WHERE page NOT IN ( SELECT page from {$wpdb->prefix}rank_math_analytics_objects )" );
|
||||
|
||||
delete_transient( 'rank_math_analytics_data_info' );
|
||||
DB::purge_cache();
|
||||
DB::delete_data_log();
|
||||
$this->calculate_stats();
|
||||
|
||||
update_option( 'rank_math_analytics_last_updated', time() );
|
||||
|
||||
Workflow::do_workflow( 'inspections' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the console start and end dates.
|
||||
*
|
||||
* @param array $args Args containing start and end date.
|
||||
*/
|
||||
public function get_console_days( $args = [] ) {
|
||||
set_time_limit( 300 );
|
||||
|
||||
$rows = Api::get()->get_search_analytics(
|
||||
[
|
||||
'start_date' => $args['start_date'],
|
||||
'end_date' => $args['end_date'],
|
||||
'dimensions' => [ 'date' ],
|
||||
]
|
||||
);
|
||||
|
||||
if ( empty( $rows ) || is_wp_error( $rows ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$empty_dates = get_option( 'rank_math_console_empty_dates', [] );
|
||||
|
||||
$dates = [];
|
||||
|
||||
foreach ( $rows as $row ) {
|
||||
|
||||
// Have at least few impressions.
|
||||
if ( $row['impressions'] ) {
|
||||
$date = $row['keys'][0];
|
||||
|
||||
if ( ! DB::date_exists( $date, 'console' ) && ! in_array( $date, $empty_dates, true ) ) {
|
||||
$dates[] = [
|
||||
'start_date' => $date,
|
||||
'end_date' => $date,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get console data.
|
||||
*
|
||||
* @param string $date Date to fetch data for.
|
||||
*/
|
||||
public function get_console_data( $date ) {
|
||||
set_time_limit( 300 );
|
||||
|
||||
$rows = Api::get()->get_search_analytics(
|
||||
[
|
||||
'start_date' => $date,
|
||||
'end_date' => $date,
|
||||
'dimensions' => [ 'query', 'page' ],
|
||||
]
|
||||
);
|
||||
|
||||
if ( empty( $rows ) || is_wp_error( $rows ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = \array_map( [ $this, 'normalize_query_page_data' ], $rows );
|
||||
|
||||
try {
|
||||
DB::add_query_page_bulk( $date, $rows );
|
||||
|
||||
// Clear the cache here.
|
||||
$this->cache_flush_group( 'rank_math_rest_keywords_rows' );
|
||||
$this->cache_flush_group( 'rank_math_posts_rows_by_objects' );
|
||||
$this->cache_flush_group( 'rank_math_analytics_summary' );
|
||||
|
||||
return $rows;
|
||||
} catch ( Exception $e ) {} // phpcs:ignore
|
||||
}
|
||||
|
||||
/**
|
||||
* Handlle console response.
|
||||
*
|
||||
* @param array $data API request and response data.
|
||||
*/
|
||||
public function handle_console_response( $data = [] ) {
|
||||
if ( 200 !== $data['code'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $data['formatted_response']['rows'] ) && ! empty( $data['formatted_response']['rows'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $data['args']['startDate'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dates = get_option( 'rank_math_console_empty_dates', [] );
|
||||
if ( ! $dates ) {
|
||||
$dates = [];
|
||||
}
|
||||
|
||||
$dates[] = $data['args']['startDate'];
|
||||
$dates[] = $data['args']['endDate'];
|
||||
|
||||
$dates = array_unique( $dates );
|
||||
|
||||
update_option( 'rank_math_console_empty_dates', $dates );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inspection results from the API and store them in the database.
|
||||
*
|
||||
* @param string $page URI to fetch data for.
|
||||
*/
|
||||
public function get_inspections_data( $page ) {
|
||||
// If the option is disabled, don't fetch data.
|
||||
if ( ! \RankMath\Analytics\Url_Inspection::is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$inspection = Url_Inspection::get()->get_inspection_data( $page );
|
||||
if ( empty( $inspection ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
DB::store_inspection( $inspection );
|
||||
} catch ( Exception $e ) {} // phpcs:ignore
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for missing dates.
|
||||
*
|
||||
* @param string $action Action to perform.
|
||||
*/
|
||||
public function check_for_missing_dates( $action ) {
|
||||
$days = Helper::get_settings( 'general.console_caching_control', 90 );
|
||||
|
||||
Workflow::do_workflow(
|
||||
$action,
|
||||
$days,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate stats.
|
||||
*/
|
||||
private function calculate_stats() {
|
||||
$ranges = [
|
||||
'-7 days',
|
||||
'-15 days',
|
||||
'-30 days',
|
||||
'-3 months',
|
||||
'-6 months',
|
||||
'-1 year',
|
||||
];
|
||||
|
||||
foreach ( $ranges as $range ) {
|
||||
Stats::get()->set_date_range( $range );
|
||||
Stats::get()->get_top_keywords();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize console data.
|
||||
*
|
||||
* @param array $row Single row item.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function normalize_query_page_data( $row ) {
|
||||
$row = $this->normalize_data( $row );
|
||||
$row['query'] = $row['keys'][0];
|
||||
$row['page'] = $row['keys'][1];
|
||||
|
||||
unset( $row['keys'] );
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize console data.
|
||||
*
|
||||
* @param array $row Single row item.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function normalize_data( $row ) {
|
||||
$row['ctr'] = round( $row['ctr'] * 100, 2 );
|
||||
$row['position'] = round( $row['position'], 2 );
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* Authentication workflow.
|
||||
*
|
||||
* @since 1.0.55
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Analytics
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics\Workflow;
|
||||
|
||||
use RankMath\Helper;
|
||||
use RankMath\Google\Api;
|
||||
use RankMath\Traits\Hooker;
|
||||
use RankMath\Helpers\Str;
|
||||
use RankMath\Helpers\Param;
|
||||
use RankMath\Helpers\Security;
|
||||
use RankMath\Analytics\DB;
|
||||
use RankMath\Google\Permissions;
|
||||
use RankMath\Google\Authentication;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* OAuth class.
|
||||
*/
|
||||
class OAuth {
|
||||
|
||||
use Hooker;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->action( 'admin_init', 'process_oauth' );
|
||||
$this->action( 'admin_init', 'reconnect_google' );
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth reply back
|
||||
*/
|
||||
public function process_oauth() {
|
||||
$process_oauth = Param::get( 'process_oauth', 0, FILTER_VALIDATE_INT );
|
||||
$access_token = Param::get( 'access_token', '', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
|
||||
$security = Param::get( 'rankmath_security', '', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
|
||||
|
||||
// Early Bail!!
|
||||
if ( empty( $security ) || ( $process_oauth < 1 && empty( $access_token ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! wp_verify_nonce( $security, 'rank_math_oauth_token' ) ) {
|
||||
wp_nonce_ays( 'rank_math_oauth_token' );
|
||||
die();
|
||||
}
|
||||
|
||||
$redirect = false;
|
||||
// Backward compatibility.
|
||||
if ( ! empty( $process_oauth ) ) {
|
||||
$redirect = $this->get_tokens_from_server();
|
||||
}
|
||||
|
||||
// New version.
|
||||
if ( ! empty( $access_token ) ) {
|
||||
$redirect = $this->get_tokens_from_url();
|
||||
}
|
||||
|
||||
// Remove possible admin notice if we have new access token.
|
||||
delete_option( 'rankmath_google_api_failed_attempts_data' );
|
||||
delete_option( 'rankmath_google_api_reconnect' );
|
||||
|
||||
Permissions::fetch();
|
||||
|
||||
if ( ! empty( $redirect ) ) {
|
||||
Helper::redirect( $redirect );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect Google.
|
||||
*/
|
||||
public function reconnect_google() {
|
||||
if ( ! isset( $_GET['reconnect'] ) || 'google' !== $_GET['reconnect'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'rank_math_reconnect_google' ) ) {
|
||||
wp_nonce_ays( 'rank_math_reconnect_google' );
|
||||
die();
|
||||
}
|
||||
|
||||
if ( ! Helper::has_cap( 'analytics' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = DB::objects()
|
||||
->selectCount( 'id' )
|
||||
->getVar();
|
||||
|
||||
if ( empty( $rows ) ) {
|
||||
delete_option( 'rank_math_analytics_installed' );
|
||||
}
|
||||
|
||||
Api::get()->revoke_token();
|
||||
Workflow::kill_workflows();
|
||||
|
||||
wp_redirect( Authentication::get_auth_url() ); // phpcs:ignore
|
||||
die();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token from url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_tokens_from_url() {
|
||||
$data = [
|
||||
'access_token' => urldecode( Param::get( 'access_token', '', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ) ),
|
||||
'refresh_token' => urldecode( Param::get( 'refresh_token', '', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ) ),
|
||||
'expire' => urldecode( Param::get( 'expire', 0, FILTER_VALIDATE_INT ) ),
|
||||
];
|
||||
|
||||
Authentication::tokens( $data );
|
||||
|
||||
$current_request = remove_query_arg(
|
||||
[
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'expire',
|
||||
'security',
|
||||
]
|
||||
);
|
||||
|
||||
return $current_request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token from rankmath server.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_tokens_from_server() {
|
||||
// Bail if the user is not authenticated at all yet.
|
||||
$id = Param::get( 'process_oauth', 0, FILTER_VALIDATE_INT );
|
||||
if ( $id < 1 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = wp_remote_get( Authentication::get_auth_app_url() . '/get.php?id=' . $id );
|
||||
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = wp_remote_retrieve_body( $response );
|
||||
if ( empty( $response ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = \json_decode( $response, true );
|
||||
unset( $response['id'] );
|
||||
|
||||
// Save new token.
|
||||
Authentication::tokens( $response );
|
||||
|
||||
$redirect = Security::remove_query_arg_raw( [ 'process_oauth', 'security' ] );
|
||||
if ( Str::contains( 'rank-math-options-general', $redirect ) ) {
|
||||
$redirect .= '#setting-panel-analytics';
|
||||
}
|
||||
|
||||
Helper::remove_notification( 'rank_math_analytics_reauthenticate' );
|
||||
|
||||
return $redirect;
|
||||
}
|
||||
}
|
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
/**
|
||||
* Install objects.
|
||||
*
|
||||
* @since 1.0.49
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics\Workflow;
|
||||
|
||||
use Exception;
|
||||
use RankMath\Helper;
|
||||
use RankMath\Helpers\DB;
|
||||
use RankMath\Traits\Hooker;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Objects class.
|
||||
*/
|
||||
class Objects extends Base {
|
||||
|
||||
use Hooker;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$done = \boolval( get_option( 'rank_math_analytics_installed' ) );
|
||||
if ( $done ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->create_tables();
|
||||
$this->create_data_job();
|
||||
$this->flat_posts();
|
||||
|
||||
update_option( 'rank_math_analytics_installed', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tables.
|
||||
*/
|
||||
public function create_tables() {
|
||||
global $wpdb;
|
||||
|
||||
$collate = $wpdb->get_charset_collate();
|
||||
$table = 'rank_math_analytics_objects';
|
||||
|
||||
// Early Bail!!
|
||||
if ( DB::check_table_exists( $table ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema = "CREATE TABLE {$wpdb->prefix}{$table} (
|
||||
id bigint(20) unsigned NOT NULL auto_increment,
|
||||
created timestamp NOT NULL,
|
||||
title text NOT NULL,
|
||||
page varchar(500) NOT NULL,
|
||||
object_type varchar(100) NOT NULL,
|
||||
object_subtype varchar(100) NOT NULL,
|
||||
object_id bigint(20) unsigned NOT NULL,
|
||||
primary_key varchar(255) NOT NULL,
|
||||
seo_score tinyint NOT NULL default 0,
|
||||
page_score tinyint NOT NULL default 0,
|
||||
is_indexable tinyint(1) NOT NULL default 1,
|
||||
schemas_in_use varchar(500),
|
||||
desktop_interactive double default 0,
|
||||
desktop_pagescore double default 0,
|
||||
mobile_interactive double default 0,
|
||||
mobile_pagescore double default 0,
|
||||
pagespeed_refreshed timestamp,
|
||||
PRIMARY KEY (id),
|
||||
KEY analytics_object_page (page(190))
|
||||
) $collate;";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
try {
|
||||
dbDelta( $schema );
|
||||
} catch ( Exception $e ) { // phpcs:ignore
|
||||
// Will log.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create jobs to fetch data.
|
||||
*/
|
||||
public function create_data_job() {
|
||||
// Clear old schedule.
|
||||
wp_clear_scheduled_hook( 'rank_math/analytics/get_analytics' );
|
||||
|
||||
// Add action for scheduler.
|
||||
$task_name = 'rank_math/analytics/data_fetch';
|
||||
$fetch_gap = apply_filters( 'rank_math/analytics/fetch_gap', 7, 'objects' );
|
||||
|
||||
// Schedule new action only when there is no existing action.
|
||||
if ( false === as_next_scheduled_action( $task_name ) ) {
|
||||
$schedule_in_minute = wp_rand( 3, defined( 'RANK_MATH_PRO_FILE' ) ? 1380 : 4320 );
|
||||
$time_to_schedule = ( strtotime( 'tomorrow' ) + ( $schedule_in_minute * MINUTE_IN_SECONDS ) );
|
||||
as_schedule_recurring_action(
|
||||
$time_to_schedule,
|
||||
DAY_IN_SECONDS * $fetch_gap,
|
||||
$task_name,
|
||||
[],
|
||||
'rank-math'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat posts
|
||||
*/
|
||||
public function flat_posts() {
|
||||
$post_types = $this->do_filter( 'analytics/post_types', Helper::get_accessible_post_types() );
|
||||
unset( $post_types['attachment'] );
|
||||
|
||||
$ids = get_posts(
|
||||
[
|
||||
'post_type' => array_keys( $post_types ),
|
||||
'post_status' => 'publish',
|
||||
'fields' => 'ids',
|
||||
'posts_per_page' => -1,
|
||||
]
|
||||
);
|
||||
|
||||
$counter = 0;
|
||||
$chunks = \array_chunk( $ids, 50 );
|
||||
foreach ( $chunks as $chunk ) {
|
||||
$counter++;
|
||||
as_schedule_single_action(
|
||||
time() + ( 60 * ( $counter / 2 ) ),
|
||||
'rank_math/analytics/flat_posts',
|
||||
[ $chunk ],
|
||||
'rank-math'
|
||||
);
|
||||
}
|
||||
|
||||
// Check for posts.
|
||||
as_schedule_single_action(
|
||||
time() + ( 60 * ( ( $counter + 1 ) / 2 ) ),
|
||||
'rank_math/analytics/flat_posts_completed',
|
||||
[],
|
||||
'rank-math'
|
||||
);
|
||||
|
||||
// Clear cache.
|
||||
Workflow::add_clear_cache( time() + ( 60 * ( ( $counter + 2 ) / 2 ) ) );
|
||||
}
|
||||
}
|
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
/**
|
||||
* Workflow.
|
||||
*
|
||||
* @since 1.0.54
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\modules
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Analytics\Workflow;
|
||||
|
||||
use RankMath\Traits\Hooker;
|
||||
use function as_enqueue_async_action;
|
||||
use function as_unschedule_all_actions;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Workflow class.
|
||||
*/
|
||||
class Workflow {
|
||||
|
||||
use Hooker;
|
||||
|
||||
/**
|
||||
* Main instance
|
||||
*
|
||||
* Ensure only one instance is loaded or can be loaded.
|
||||
*
|
||||
* @return Workflow
|
||||
*/
|
||||
public static function get() {
|
||||
static $instance;
|
||||
|
||||
if ( is_null( $instance ) && ! ( $instance instanceof Workflow ) ) {
|
||||
$instance = new Workflow();
|
||||
$instance->hooks();
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks.
|
||||
*/
|
||||
public function hooks() {
|
||||
// Common.
|
||||
$this->action( 'rank_math/analytics/workflow', 'maybe_first_install', 5, 0 );
|
||||
$this->action( 'rank_math/analytics/workflow', 'start_workflow', 10, 4 );
|
||||
$this->action( 'rank_math/analytics/workflow/create_tables', 'create_tables_only', 5 );
|
||||
|
||||
// Console.
|
||||
$this->action( 'rank_math/analytics/workflow/console', 'init_console_workflow', 5, 0 );
|
||||
|
||||
// Inspections.
|
||||
$this->action( 'rank_math/analytics/workflow/inspections', 'init_inspections_workflow', 5, 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe first install.
|
||||
*/
|
||||
public function maybe_first_install() {
|
||||
new Objects();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init Console workflow
|
||||
*/
|
||||
public function init_console_workflow() {
|
||||
new Console();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init Inspections workflow.
|
||||
*/
|
||||
public function init_inspections_workflow() {
|
||||
new Inspections();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tables only.
|
||||
*/
|
||||
public function create_tables_only() {
|
||||
( new Objects() )->create_tables();
|
||||
( new Inspections() )->create_tables();
|
||||
new Console();
|
||||
}
|
||||
|
||||
/**
|
||||
* Service workflow
|
||||
*
|
||||
* @param string $action Action to perform.
|
||||
* @param integer $days Number of days to fetch from past.
|
||||
* @param string $prev Previous saved value.
|
||||
* @param string $new New posted value.
|
||||
*/
|
||||
public function start_workflow( $action, $days = 0, $prev = null, $new = null ) {
|
||||
do_action(
|
||||
'rank_math/analytics/workflow/' . $action,
|
||||
$days,
|
||||
$prev,
|
||||
$new
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Service workflow
|
||||
*
|
||||
* @param string $action Action to perform.
|
||||
* @param integer $days Number of days to fetch from past.
|
||||
* @param string $prev Previous saved value.
|
||||
* @param string $new New posted value.
|
||||
*/
|
||||
public static function do_workflow( $action, $days = 0, $prev = null, $new = null ) {
|
||||
as_enqueue_async_action(
|
||||
'rank_math/analytics/workflow',
|
||||
[
|
||||
$action,
|
||||
$days,
|
||||
$prev,
|
||||
$new,
|
||||
],
|
||||
'rank-math'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill all workflows
|
||||
*
|
||||
* Stop processing queue items, clear cronjob and delete all batches.
|
||||
*/
|
||||
public static function kill_workflows() {
|
||||
as_unschedule_all_actions( 'rank_math/analytics/workflow' );
|
||||
as_unschedule_all_actions( 'rank_math/analytics/clear_cache' );
|
||||
as_unschedule_all_actions( 'rank_math/analytics/get_console_data' );
|
||||
as_unschedule_all_actions( 'rank_math/analytics/get_analytics_data' );
|
||||
as_unschedule_all_actions( 'rank_math/analytics/get_adsense_data' );
|
||||
as_unschedule_all_actions( 'rank_math/analytics/get_inspections_data' );
|
||||
|
||||
do_action( 'rank_math/analytics/clear_cache' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add clear cache job.
|
||||
*
|
||||
* @param int $time Timestamp to add job for.
|
||||
*/
|
||||
public static function add_clear_cache( $time ) {
|
||||
as_unschedule_all_actions( 'rank_math/analytics/clear_cache' );
|
||||
as_schedule_single_action(
|
||||
$time,
|
||||
'rank_math/analytics/clear_cache',
|
||||
[],
|
||||
'rank-math'
|
||||
);
|
||||
|
||||
delete_option( 'rank_math_analytics_last_single_action_schedule_time' );
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user