Commit realizado el 12:13:52 08-04-2024

This commit is contained in:
Pagina Web Monito
2024-04-08 12:13:55 -04:00
commit 0c33094de9
7815 changed files with 1365694 additions and 0 deletions

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

View File

@@ -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'],
];
}
}

View File

@@ -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' );
}
}

View File

@@ -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() );
}
}

View File

@@ -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' ) .
'&nbsp;<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;
}
}

View File

@@ -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();
}
}

View File

@@ -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 );
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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,
];
}
}

View File

@@ -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 );
}
}

View File

@@ -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 );
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 );
}
}
}

View File

@@ -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;
}
}

View File

@@ -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 );
}
}

View File

@@ -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';
}
}

View File

@@ -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'] );
}
}

View File

@@ -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
}
}

View File

@@ -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'
);
}
}

View File

@@ -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 );
}
}
}

View File

@@ -0,0 +1 @@
<?php // Silence is golden.

View File

@@ -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 ) );
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@@ -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
}

View File

@@ -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>&nbsp;</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>

View File

@@ -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' );

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 ? '&#9650;' : '&#9660;' ) . '</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;' ] );
} ?>

View File

@@ -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' ); ?>

View File

@@ -0,0 +1 @@
<?php // Silence is golden.

View File

@@ -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>' .
'&nbsp;&nbsp;<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>' .
'&nbsp;&nbsp;<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 );

View File

@@ -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 );
}
}

View File

@@ -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' );
}
}

View File

@@ -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' );
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 ) ) );
}
}

View File

@@ -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' );
}
}