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
@@ -0,0 +1,5 @@
/*!
* Plugin: Rank Math - 404 Monitor
* URL: https://rankmath.com/wordpress/plugin/seo-suite/
* Name: 404-monitor.css
*/.metabox-prefs legend+label{display:none}
@@ -0,0 +1 @@
!function(){"use strict";var t,e={n:function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,{a:n}),n},d:function(t,n){for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},o:function(t,e){return Object.prototype.hasOwnProperty.call(t,e)}},n=jQuery;(t=e.n(n)())((function(){var e=this;e.wrap=t(".rank-math-404-monitor-wrap"),e.wrap.on("click",".rank-math-404-delete",(function(e){e.preventDefault();var n=t(this),r=n.attr("href").replace("admin.php","admin-ajax.php").replace("action=delete","action=rank_math_delete_log").replace("page=","math=");t.ajax({url:r,type:"GET",success:function(e){e&&e.success&&n.closest("tr").fadeOut(800,(function(){t(this).remove()}))}})})),e.wrap.on("click",".rank-math-clear-logs",(function(e){if(e.preventDefault(),!confirm(rankMath.logConfirmClear))return!1;t(this).closest("form").append('<input type="hidden" name="action" value="clear_log">').submit()})),t("#doaction, #doaction2").on("click",(function(){"redirect"===t("#bulk-action-selector-top").val()&&t(this).closest("form").attr("action",rankMath.redirectionsUri)}))}))}();
@@ -0,0 +1,11 @@
// compileCompressed: $1.css
/*!
* Plugin: Rank Math - 404 Monitor
* URL: https://rankmath.com/wordpress/plugin/seo-suite/
* Name: 404-monitor.css
*/
.metabox-prefs legend + label {
display: none;
}
@@ -0,0 +1,241 @@
<?php
/**
* The admin-side code for the 404 Monitor module.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Monitor
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Monitor;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Module\Base;
use RankMath\Admin\Page;
use RankMath\Helpers\Arr;
use RankMath\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* Admin class.
*/
class Admin extends Base {
/**
* Module directory.
*
* @var string
*/
public $directory;
/**
* WP_List_Table class name.
*
* @var string
*/
public $table;
/**
* Screen options.
*
* @var array
*/
public $screen_options = [];
/**
* Page object.
*
* @var Page
*/
public $page;
/**
* The Constructor.
*/
public function __construct() {
$directory = dirname( __FILE__ );
$this->config(
[
'id' => '404-monitor',
'directory' => $directory,
'table' => 'RankMath\Monitor\Table',
'screen_options' => [
'id' => 'rank_math_404_monitor_per_page',
'default' => 100,
],
]
);
parent::__construct();
if ( $this->page->is_current_page() ) {
$this->action( 'init', 'init' );
}
if ( Helper::has_cap( '404_monitor' ) ) {
$this->filter( 'rank_math/settings/general', 'add_settings' );
}
}
/**
* Initialize.
*/
public function init() {
$action = Helper::get_request_action();
if ( false === $action || ! in_array( $action, [ 'delete', 'clear_log' ], true ) ) {
return;
}
if ( ! check_admin_referer( 'bulk-events' ) ) {
check_admin_referer( '404_delete_log', 'security' );
}
$action = 'do_' . $action;
$this->$action();
}
/**
* Delete selected log.
*/
protected function do_delete() {
$log = Param::request( 'log', '', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
if ( empty( $log ) ) {
return;
}
$count = DB::delete_log( $log );
if ( $count > 0 ) {
Helper::add_notification(
/* translators: delete counter */
sprintf( esc_html__( '%d log(s) deleted.', 'rank-math' ), $count ),
[ 'type' => 'success' ]
);
}
}
/**
* Clears all 404 logs, by truncating the log table.
* Fired with the `$this->$action();` line inside the `init()` method.
*/
protected function do_clear_log() {
$count = DB::get_count();
DB::clear_logs();
Helper::add_notification(
/* translators: delete counter */
sprintf( esc_html__( 'Log cleared - %d items deleted.', 'rank-math' ), $count ),
[ 'type' => 'success' ]
);
}
/**
* Register the 404 Monitor admin page.
*/
public function register_admin_page() {
$dir = $this->directory . '/views/';
$uri = untrailingslashit( plugin_dir_url( __FILE__ ) );
$this->page = new Page(
'rank-math-404-monitor',
esc_html__( '404 Monitor', 'rank-math' ),
[
'position' => 30,
'parent' => 'rank-math',
'capability' => 'rank_math_404_monitor',
'render' => $dir . 'main.php',
'help' => [
'404-overview' => [
'title' => esc_html__( 'Overview', 'rank-math' ),
'view' => $dir . 'help-tab-overview.php',
],
'404-screen-content' => [
'title' => esc_html__( 'Screen Content', 'rank-math' ),
'view' => $dir . 'help-tab-screen-content.php',
],
'404-actions' => [
'title' => esc_html__( 'Available Actions', 'rank-math' ),
'view' => $dir . 'help-tab-actions.php',
],
'404-bulk' => [
'title' => esc_html__( 'Bulk Actions', 'rank-math' ),
'view' => $dir . 'help-tab-bulk.php',
],
],
'assets' => [
'styles' => [
'rank-math-common' => '',
'rank-math-404-monitor' => $uri . '/assets/css/404-monitor.css',
],
'scripts' => [ 'rank-math-404-monitor' => $uri . '/assets/js/404-monitor.js' ],
],
]
);
if ( $this->page->is_current_page() ) {
Helper::add_json( 'logConfirmClear', esc_html__( 'Are you sure you wish to delete all 404 error logs?', 'rank-math' ) );
Helper::add_json( 'redirectionsUri', Helper::get_admin_url( 'redirections' ) );
}
}
/**
* Add module settings tab in the General Settings.
*
* @param array $tabs Array of option panel tabs.
*
* @return array
*/
public function add_settings( $tabs ) {
Arr::insert(
$tabs,
[
'404-monitor' => [
'icon' => 'rm-icon rm-icon-404',
'title' => esc_html__( '404 Monitor', 'rank-math' ),
/* translators: 1. Link to KB article 2. Link to redirection setting scree */
'desc' => sprintf( esc_html__( 'Monitor broken pages that ruin user-experience and affect SEO. %s.', 'rank-math' ), '<a href="' . KB::get( '404-monitor-settings', 'Options Panel 404 Monitor Tab' ) . '" target="_blank">' . esc_html__( 'Learn more', 'rank-math' ) . '</a>' ),
'file' => $this->directory . '/views/options.php',
],
],
7
);
return $tabs;
}
/**
* Output page title actions.
*
* @return void
*/
public function page_title_actions() {
$actions = [
'settings' => [
'class' => 'page-title-action',
'href' => Helper::get_admin_url( 'options-general#setting-panel-404-monitor' ),
'label' => __( 'Settings', 'rank-math' ),
],
'learn_more' => [
'class' => 'page-title-action',
'href' => KB::get( '404-monitor', '404 Page Learn More Button' ),
'label' => __( 'Learn More', 'rank-math' ),
],
];
/**
* Filters the title actions available on the 404 Monitor page.
*
* @param array $actions Multidimensional array of actions to show.
*/
$actions = $this->do_filter( '404_monitor/page_title_actions', $actions );
foreach ( $actions as $action_name => $action ) {
?>
<a class="<?php echo esc_attr( $action['class'] ); ?> rank-math-404-monitor-<?php echo esc_attr( $action_name ); ?>" href="<?php echo esc_attr( $action['href'] ); ?>" target="_blank"><?php echo esc_attr( $action['label'] ); ?></a>
<?php
}
}
}
@@ -0,0 +1,173 @@
<?php
/**
* The database operations for the 404 Monitor module.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Monitor
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Monitor;
use RankMath\Helper;
use RankMath\Admin\Database\Database;
defined( 'ABSPATH' ) || exit;
/**
* DB class.
*/
class DB {
/**
* Get query builder.
*
* @return Query_Builder
*/
private static function table() {
return Database::table( 'rank_math_404_logs' );
}
/**
* Get error log items.
*
* @param array $args Array of arguments.
*
* @return array
*/
public static function get_logs( $args ) {
$args = wp_parse_args(
$args,
[
'orderby' => 'id',
'order' => 'DESC',
'limit' => 10,
'paged' => 1,
'search' => '',
'ids' => [],
'uri' => '',
]
);
$args = apply_filters( 'rank_math/404_monitor/get_logs_args', $args );
$table = self::table()->found_rows()->page( $args['paged'] - 1, $args['limit'] );
if ( ! empty( $args['search'] ) ) {
$table->whereLike( 'uri', rawurlencode( $args['search'] ) );
}
if ( ! empty( $args['ids'] ) ) {
$table->whereIn( 'id', (array) $args['ids'] );
}
if ( ! empty( $args['uri'] ) ) {
$table->where( 'uri', $args['uri'] );
}
if ( ! empty( $args['orderby'] ) && in_array( $args['orderby'], [ 'id', 'uri', 'accessed', 'times_accessed' ], true ) ) {
$table->orderBy( $args['orderby'], $args['order'] );
}
return [
'logs' => $table->get( ARRAY_A ),
'count' => $table->get_found_rows(),
];
}
/**
* Add a record.
*
* @param array $args Values to insert.
*/
public static function add( $args ) {
$args = wp_parse_args(
$args,
[
'uri' => '',
'accessed' => current_time( 'mysql' ),
'times_accessed' => '1',
'referer' => '',
'user_agent' => '',
]
);
// Maybe delete logs if record exceed defined limit.
$limit = absint( Helper::get_settings( 'general.404_monitor_limit' ) );
if ( $limit && self::get_count() >= $limit ) {
self::clear_logs();
}
return self::table()->insert( $args, [ '%s', '%s', '%d', '%s', '%s', '%s' ] );
}
/**
* Update a record.
*
* @param array $args Values to update.
*/
public static function update( $args ) {
$row = self::table()->where( 'uri', $args['uri'] )->one( ARRAY_A );
if ( $row ) {
return self::update_counter( $row );
}
return self::add( $args );
}
/**
* Delete a record.
*
* @param array $ids Array of IDs to delete.
*
* @return int Number of records deleted.
*/
public static function delete_log( $ids ) {
return self::table()->whereIn( 'id', (array) $ids )->delete();
}
/**
* Get total number of log items (number of rows in the DB table).
*
* @return int
*/
public static function get_count() {
return self::table()->selectCount()->getVar();
}
/**
* Clear logs completely.
*
* @return int
*/
public static function clear_logs() {
return self::table()->truncate();
}
/**
* Get stats for dashboard widget.
*
* @return array
*/
public static function get_stats() {
return self::table()->selectCount( '*', 'total' )->selectSum( 'times_accessed', 'hits' )->one();
}
/**
* Update if URL is matched and hit.
*
* @param object $row Record to update.
*
* @return int|false The number of rows updated, or false on error.
*/
private static function update_counter( $row ) {
$update_data = [
'accessed' => current_time( 'mysql' ),
'times_accessed' => absint( $row['times_accessed'] ) + 1,
];
return self::table()->set( $update_data )->where( 'id', absint( $row['id'] ) )->update();
}
}
@@ -0,0 +1,266 @@
<?php
/**
* The 404 Monitor Module.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Monitor
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Monitor;
use RankMath\Helper;
use RankMath\Helpers\Str;
use RankMath\Helpers\Param;
use RankMath\Traits\Ajax;
use RankMath\Traits\Hooker;
use donatj\UserAgent\UserAgentParser;
defined( 'ABSPATH' ) || exit;
/**
* Monitor class.
*/
class Monitor {
use Hooker, Ajax;
/**
* Admin object.
*
* @var Admin
*/
public $admin;
/**
* The Constructor.
*/
public function __construct() {
if ( is_admin() ) {
$this->admin = new Admin();
}
if ( Helper::is_ajax() ) {
$this->ajax( 'delete_log', 'delete_log' );
}
if ( Helper::has_cap( '404_monitor' ) && Helper::is_rest() ) {
$this->action( 'rank_math/dashboard/widget', 'dashboard_widget', 11 );
}
$this->action( $this->get_hook(), 'capture_404' );
if ( Helper::has_cap( '404_monitor' ) ) {
$this->action( 'rank_math/admin_bar/items', 'admin_bar_items', 11 );
}
}
/**
* Add stats in the admin dashboard widget.
*/
public function dashboard_widget() {
$data = DB::get_stats();
?>
<h3>
<?php esc_html_e( '404 Monitor', 'rank-math' ); ?>
<a href="<?php echo esc_url( Helper::get_admin_url( '404-monitor' ) ); ?>" 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">
<div>
<h4>
<?php esc_html_e( 'Log Count', 'rank-math' ); ?>
<span class="rank-math-tooltip"><em class="dashicons-before dashicons-editor-help"></em><span><?php esc_html_e( 'Total number of 404 pages opened by the users.', 'rank-math' ); ?></span></span>
</h4>
<strong class="text-large"><?php echo esc_html( Str::human_number( $data->total ) ); ?></strong>
</div>
<div>
<h4>
<?php esc_html_e( 'URL Hits', 'rank-math' ); ?>
<span class="rank-math-tooltip"><em class="dashicons-before dashicons-editor-help"></em><span><?php esc_html_e( 'Total number visits received on all the 404 pages.', 'rank-math' ); ?></span></span>
</h4>
<strong class="text-large"><?php echo esc_html( Str::human_number( $data->hits ) ); ?></strong>
</div>
</div>
<?php
}
/**
* Add admin bar item.
*
* @param Admin_Bar_Menu $menu Menu class instance.
*/
public function admin_bar_items( $menu ) {
$menu->add_sub_menu(
'404-monitor',
[
'title' => esc_html__( '404 Monitor', 'rank-math' ),
'href' => Helper::get_admin_url( '404-monitor' ),
'meta' => [ 'title' => esc_html__( 'Review 404 errors on your site', 'rank-math' ) ],
'priority' => 50,
]
);
}
/**
* Delete a log item.
*/
public function delete_log() {
check_ajax_referer( '404_delete_log', 'security' );
$this->has_cap_ajax( '404_monitor' );
$id = Param::request( 'log' );
if ( ! $id ) {
$this->error( esc_html__( 'No valid id found.', 'rank-math' ) );
}
DB::delete_log( $id );
$this->success( esc_html__( 'Log item successfully deleted.', 'rank-math' ) );
}
/**
* Log the request details when is_404() is true and WP's response code is *not* 410 or 451.
*/
public function capture_404() {
if ( ! is_404() || in_array( http_response_code(), [ 410, 451 ], true ) ) {
return;
}
$uri = untrailingslashit( Helper::get_current_page_url( Helper::get_settings( 'general.404_monitor_ignore_query_parameters' ) ) );
$uri = str_replace( Helper::get_home_url( '/' ), '', $uri );
// Check if excluded.
if ( $this->is_url_excluded( $uri ) ) {
return;
}
// Mode = simple.
if ( 'simple' === Helper::get_settings( 'general.404_monitor_mode' ) ) {
DB::update( [ 'uri' => $uri ] );
return;
}
// Mode = advanced.
DB::add(
[
'uri' => $uri,
'referer' => Param::server( 'HTTP_REFERER', '' ),
'user_agent' => $this->get_user_agent(),
]
);
}
/**
* Check if given URL is excluded.
*
* @param string $uri The URL to check for exclusion.
*
* @return boolean
*/
private function is_url_excluded( $uri ) {
$excludes = Helper::get_settings( 'general.404_monitor_exclude' );
if ( ! is_array( $excludes ) ) {
return false;
}
foreach ( $excludes as $rule ) {
$rule['exclude'] = empty( $rule['exclude'] ) ? '' : $this->sanitize_exclude_pattern( $rule['exclude'], $rule['comparison'] );
if ( ! empty( $rule['exclude'] ) && Str::comparison( $rule['exclude'], $uri, $rule['comparison'] ) ) {
return true;
}
}
return false;
}
/**
* Check if regex pattern has delimiters or not, and add them if not.
*
* @param string $pattern The pattern to check.
* @param string $comparison The comparison type.
*
* @return string
*/
private function sanitize_exclude_pattern( $pattern, $comparison ) {
if ( 'regex' !== $comparison ) {
return $pattern;
}
if ( preg_match( '[^(?:([^a-zA-Z0-9\\\\]).*\\1|\\(.*\\)|\\{.*\\}|\\[.*\\]|<.*>)[imsxADSUXJu]*$]', $pattern ) ) {
return $pattern;
}
return '[' . addslashes( $pattern ) . ']';
}
/**
* Get user-agent header.
*
* @return string
*/
private function get_user_agent() {
$u_agent = Param::server( 'HTTP_USER_AGENT' );
if ( empty( $u_agent ) ) {
return '';
}
$parsed = $this->parse_user_agent( $u_agent );
$nice_ua = '';
if ( ! empty( $parsed['browser'] ) ) {
$nice_ua .= $parsed['browser'];
}
if ( ! empty( $parsed['version'] ) ) {
$nice_ua .= ' ' . $parsed['version'];
}
return $nice_ua . ' | ' . $u_agent;
}
/**
* Parses a user-agent string into its parts.
*
* @link https://github.com/donatj/PhpUserAgent
*
* @param string $u_agent User agent string to parse or null. Uses $_SERVER['HTTP_USER_AGENT'] on NULL.
*
* @return string[] an array with browser, version and platform keys
*/
private function parse_user_agent( $u_agent ) {
if ( ! $u_agent ) {
return [
'platform' => null,
'browser' => null,
'version' => null,
];
}
$parser = new UserAgentParser();
$agent = $parser->parse( $u_agent );
return [
'platform' => $agent->platform(),
'browser' => $agent->browser(),
'version' => $agent->browserVersion(),
];
}
/**
* Function to get the hook name depending on the theme.
*
* @return string WP hook.
*/
private function get_hook() {
if ( defined( 'CT_VERSION' ) ) {
return 'oxygen_enqueue_frontend_scripts';
}
if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
return 'wp_head';
}
return 'get_header';
}
}
@@ -0,0 +1,285 @@
<?php
/**
* The WP List Table class for the 404 Monitor module.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Monitor
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Monitor;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Redirections\DB as RedirectionsDB;
use RankMath\Redirections\Cache as RedirectionsCache;
use RankMath\Admin\List_Table;
use RankMath\Monitor\Admin;
defined( 'ABSPATH' ) || exit;
/**
* Table class.
*/
class Table extends List_Table {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
parent::__construct(
[
'screen' => Admin::get_screen(),
'singular' => 'event',
'plural' => 'events',
'no_items' => esc_html__( 'The 404 error log is empty.', 'rank-math' ),
]
);
}
/**
* Prepares the list of items for displaying.
*/
public function prepare_items() {
global $per_page;
$per_page = $this->get_items_per_page( 'rank_math_404_monitor_per_page', 100 );
$search = $this->get_search();
$data = DB::get_logs(
[
'limit' => $per_page,
'order' => $this->get_order(),
'orderby' => $this->get_orderby( 'accessed' ),
'paged' => $this->get_pagenum(),
'search' => $search ? $search : '',
]
);
$this->items = $data['logs'];
foreach ( $this->items as $i => $item ) {
$this->items[ $i ]['uri_decoded'] = urldecode( $item['uri'] );
}
$this->set_pagination_args(
[
'total_items' => $data['count'],
'per_page' => $per_page,
]
);
}
/**
* Extra controls to be displayed between bulk actions and pagination.
*
* @param string $which Where to show nav.
*/
protected function extra_tablenav( $which ) {
if ( empty( $this->items ) ) {
return;
}
?>
<div class="alignleft actions">
<input type="button" class="button button-link-delete action rank-math-clear-logs" value="<?php esc_attr_e( 'Clear Log', 'rank-math' ); ?>">
</div>
<?php
}
/**
* Handles the checkbox column output.
*
* @param object $item The current item.
*/
public function column_cb( $item ) {
$out = sprintf( '<input type="checkbox" name="log[]" value="%s" />', $item['id'] );
return $this->do_filter( '404_monitor/list_table_column', $out, $item, 'cb' );
}
/**
* Handle the URI column.
*
* @param object $item The current item.
*/
protected function column_uri( $item ) {
$link = '<a href="' . esc_url( home_url( $item['uri'] ) ) . '" target="_blank" title="' . esc_attr__( 'View', 'rank-math' ) . '">' . esc_html( $item['uri_decoded'] ) . '</a>';
$out = $link . $this->column_actions( $item );
return $this->do_filter( '404_monitor/list_table_column', $out, $item, 'uri' );
}
/**
* Handle the referer column.
*
* @param object $item The current item.
*/
protected function column_referer( $item ) {
$out = '<a href="' . esc_url( $item['referer'] ) . '" target="_blank">' . esc_html( $item['referer'] ) . '</a>';
return $this->do_filter( '404_monitor/list_table_column', $out, $item, 'referer' );
}
/**
* Handles the default column output.
*
* @param object $item The current item.
* @param string $column_name The current column name.
*/
public function column_default( $item, $column_name ) {
$out = '';
if ( in_array( $column_name, [ 'times_accessed', 'accessed', 'user_agent' ], true ) ) {
$out = esc_html( $item[ $column_name ] );
}
return $this->do_filter( '404_monitor/list_table_column', $out, $item, $column_name );
}
/**
* Generate row actions div.
*
* @param object $item The current item.
*/
public function column_actions( $item ) {
$actions = [];
$actions['view'] = sprintf(
'<a href="%s" target="_blank">' . esc_html__( 'View', 'rank-math' ) . '</a>',
esc_url( home_url( $item['uri'] ) )
);
if ( Helper::get_module( 'redirections' ) ) {
$this->add_redirection_actions( $item, $actions );
}
$actions['delete'] = sprintf(
'<a href="%s" class="rank-math-404-delete">' . esc_html__( 'Delete', 'rank-math' ) . '</a>',
Helper::get_admin_url(
'404-monitor',
[
'action' => 'delete',
'log' => $item['id'],
'security' => wp_create_nonce( '404_delete_log' ),
]
)
);
return $this->row_actions( $actions );
}
/**
* Add redirection actions.
*
* @param object $item The current item.
* @param array $actions Array of actions.
*/
private function add_redirection_actions( $item, &$actions ) {
$redirection = RedirectionsCache::get_by_url( $item['uri_decoded'] );
if ( ! $redirection ) {
$redirection = RedirectionsDB::match_redirections( $item['uri_decoded'] );
}
if ( $redirection ) {
$redirection_array = (array) $redirection;
$url = esc_url(
Helper::get_admin_url(
'redirections',
[
'redirection' => isset( $redirection_array['redirection_id'] ) ? $redirection_array['redirection_id'] : $redirection_array['id'],
'security' => wp_create_nonce( 'redirection_list_action' ),
'action' => 'edit',
]
)
);
$actions['view_redirection'] = sprintf( '<a href="%s" target="_blank">' . esc_html__( 'View Redirection', 'rank-math' ) . '</a>', $url );
return;
}
$url = esc_url(
Helper::get_admin_url(
'redirections',
[
'url' => $item['uri_decoded'],
]
)
);
$actions['redirect'] = sprintf(
'<a href="%1$s" class="rank-math-404-redirect-btn">%2$s</a>',
$url,
esc_html__( 'Redirect', 'rank-math' )
);
}
/**
* Get the list of columns.
*
* @return array
*/
public function get_columns() {
$columns = [
'cb' => '<input type="checkbox" />',
'uri' => esc_html__( 'URI', 'rank-math' ),
'referer' => esc_html__( 'Referer', 'rank-math' ),
'user_agent' => esc_html__( 'User-Agent', 'rank-math' ),
'times_accessed' => esc_html__( 'Hits', 'rank-math' ),
'accessed' => esc_html__( 'Access Time', 'rank-math' ),
];
$columns = $this->filter_columns( $columns );
return $this->do_filter( '404_monitor/list_table_columns', $columns );
}
/**
* Filter columns.
*
* @param array $columns Original columns.
*
* @return array
*/
private function filter_columns( $columns ) {
if ( 'simple' === Helper::get_settings( 'general.404_monitor_mode' ) ) {
unset( $columns['referer'], $columns['user_agent'] );
return $columns;
}
unset( $columns['times_accessed'] );
return $columns;
}
/**
* Get the list of sortable columns.
*
* @return array
*/
public function get_sortable_columns() {
$sortable = [
'uri' => [ 'uri', false ],
'times_accessed' => [ 'times_accessed', false ],
'accessed' => [ 'accessed', false ],
];
return $this->do_filter( '404_monitor/list_table_sortable_columns', $sortable );
}
/**
* Get an associative array ( option_name => option_title ) with the list
* of bulk actions available on this table.
*
* @return array
*/
public function get_bulk_actions() {
$actions = [
'redirect' => esc_html__( 'Redirect', 'rank-math' ),
'delete' => esc_html__( 'Delete', 'rank-math' ),
];
if ( ! Helper::get_module( 'redirections' ) ) {
unset( $actions['redirect'] );
}
return $actions;
}
}
@@ -0,0 +1 @@
<?php // Silence is golden.
@@ -0,0 +1,19 @@
<?php
/**
* 404 Monitor inline help: "Available Actions" tab.
*
* @package RankMath
* @subpackage RankMath\Monitor
*/
defined( 'ABSPATH' ) || exit;
?>
<p>
<?php esc_html_e( 'Hovering over a row in the list will display action links that allow you to manage the item. You can perform the following actions:', 'rank-math' ); ?>
</p>
<ul>
<li><?php echo wp_kses_post( __( '<strong>View Details</strong> shows details about the 404 requests.', 'rank-math' ) ); ?></li>
<li><?php echo wp_kses_post( __( '<strong>Redirect</strong> takes you to the Redirections manager to redirect the 404 URL.', 'rank-math' ) ); ?></li>
<li><?php echo wp_kses_post( __( '<strong>Delete</strong> permanently removes the item from the list.', 'rank-math' ) ); ?></li>
</ul>
@@ -0,0 +1,14 @@
<?php
/**
* 404 Monitor inline help: "Bulk Actions" tab.
*
* @package RankMath
* @subpackage RankMath\Monitor
*/
defined( 'ABSPATH' ) || exit;
?>
<p>
<?php esc_html_e( 'You can also redirect or delete multiple items at once. Selecting multiple items to redirect allows you to redirect them to a single URL.', 'rank-math' ); ?>
</p>
@@ -0,0 +1,39 @@
<?php
/**
* 404 Monitor inline help: "Overview" tab.
*
* @package RankMath
* @subpackage RankMath\Monitor
*/
use RankMath\KB;
defined( 'ABSPATH' ) || exit;
?>
<p>
<?php esc_html_e( 'With the 404 monitor you can see where users and search engines are unable to find your content.', 'rank-math' ); ?>
</p>
<p>
<?php esc_html_e( 'Knowledge Base Articles:', 'rank-math' ); ?>
</p>
<ul>
<li>
<a href="<?php echo KB::get( '404-monitor', '404 Monitor Help Toggle' ); ?>" target="_blank">
<?php esc_html_e( '404 Monitor', 'rank-math' ); ?>
</a>
</li>
<li>
<a href="<?php echo KB::get( '404-monitor-settings', '404 Monitor Help Toggle' ); ?>" target="_blank">
<?php esc_html_e( '404 Monitor Settings', 'rank-math' ); ?>
</a>
</li>
<li>
<a href="<?php echo KB::get( 'fix-404', '404 Monitor Help Toggle Fix link' ); ?>" target="_blank">
<?php esc_html_e( 'Fix 404 Errors', 'rank-math' ); ?>
</a>
</li>
</ul>
@@ -0,0 +1,20 @@
<?php
/**
* 404 Monitor inline help: "Screen Content" tab.
*
* @package RankMath
* @subpackage RankMath\Monitor
*/
defined( 'ABSPATH' ) || exit;
?>
<p>
<?php esc_html_e( 'You can customize the display of this screen\'s contents in a number of ways:', 'rank-math' ); ?>
</p>
<ul>
<li><?php esc_html_e( 'You can hide/display columns based on your needs.', 'rank-math' ); ?></li>
<li><?php esc_html_e( 'You can decide how many items to list per screen using the Screen Options tab.', 'rank-math' ); ?></li>
<li><?php esc_html_e( 'You can search items using the search form at the top.', 'rank-math' ); ?></li>
<li><?php esc_html_e( 'You can reorder the list by clicking on the column headings. ', 'rank-math' ); ?></li>
</ul>
@@ -0,0 +1 @@
<?php // Silence is golden.
@@ -0,0 +1,33 @@
<?php
/**
* Main template for 404 monitor
*
* @package RankMath
* @subpackage RankMath\Monitor
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
$monitor = Helper::get_module( '404-monitor' )->admin;
$monitor->table->prepare_items();
?>
<div class="wrap rank-math-404-monitor-wrap">
<h2>
<?php echo esc_html( get_admin_page_title() ); ?>
<?php $monitor->page_title_actions(); ?>
</h2>
<?php \do_action( 'rank_math/404_monitor/before_list_table', $monitor ); ?>
<form method="get">
<input type="hidden" name="page" value="rank-math-404-monitor">
<?php $monitor->table->search_box( esc_html__( 'Search', 'rank-math' ), 's' ); ?>
</form>
<form method="post">
<?php $monitor->table->display(); ?>
</form>
</div>
@@ -0,0 +1,89 @@
<?php
/**
* 404 Monitor general settings.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Monitor
* @author Rank Math <support@rankmath.com>
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
$cmb->add_field(
[
'id' => '404_advanced_monitor',
'type' => 'notice',
'what' => 'error',
'content' => esc_html__( 'If you have hundreds of 404 errors, your error log might increase quickly. Only choose this option if you have a very few 404s and are unable to replicate the 404 error on a particular URL from your end.', 'rank-math' ),
'dep' => [ [ '404_monitor_mode', 'advanced' ] ],
]
);
$cmb->add_field(
[
'id' => '404_monitor_mode',
'type' => 'radio_inline',
'name' => esc_html__( 'Mode', 'rank-math' ),
'desc' => esc_html__( 'The Simple mode only logs URI and access time, while the Advanced mode creates detailed logs including additional information such as the Referer URL.', 'rank-math' ),
'options' => [
'simple' => esc_html__( 'Simple', 'rank-math' ),
'advanced' => esc_html__( 'Advanced', 'rank-math' ),
],
'default' => 'simple',
]
);
$cmb->add_field(
[
'id' => '404_monitor_limit',
'type' => 'text',
'name' => esc_html__( 'Log Limit', 'rank-math' ),
'desc' => esc_html__( 'Sets the max number of rows in a log. Set to 0 to disable the limit.', 'rank-math' ),
'default' => '100',
'attributes' => [ 'type' => 'number' ],
]
);
$monitor_exclude = $cmb->add_field(
[
'id' => '404_monitor_exclude',
'type' => 'group',
'name' => esc_html__( 'Exclude Paths', 'rank-math' ),
'desc' => esc_html__( 'Enter URIs or keywords you wish to prevent from getting logged by the 404 monitor.', 'rank-math' ),
'options' => [
'add_button' => esc_html__( 'Add another', 'rank-math' ),
'remove_button' => esc_html__( 'Remove', 'rank-math' ),
],
'classes' => 'cmb-group-text-only',
]
);
$cmb->add_group_field(
$monitor_exclude,
[
'id' => 'exclude',
'type' => 'text',
]
);
$cmb->add_group_field(
$monitor_exclude,
[
'id' => 'comparison',
'type' => 'select',
'options' => Helper::choices_comparison_types(),
]
);
$cmb->add_field(
[
'id' => '404_monitor_ignore_query_parameters',
'type' => 'toggle',
'name' => esc_html__( 'Ignore Query Parameters', 'rank-math' ),
'desc' => esc_html__( 'Turn ON to ignore all query parameters (the part after a question mark in a URL) when logging 404 errors.', 'rank-math' ),
'default' => 'off',
]
);
File diff suppressed because one or more lines are too long
@@ -0,0 +1,131 @@
<?php
/**
* The ACF Module
*
* @since 1.0.33
* @package RankMath
* @subpackage RankMath\ACF
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ACF;
use RankMath\Helper;
use RankMath\Admin\Admin_Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* ACF class.
*/
class ACF {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
if ( ! Admin_Helper::is_post_edit() && ! Admin_Helper::is_term_edit() ) {
return;
}
$this->action( 'rank_math/admin/editor_scripts', 'enqueue' );
}
/**
* Enqueue styles and scripts for the metabox on the post editor & term editor screens.
*/
public function enqueue() {
if ( Helper::is_elementor_editor() ) {
return;
}
if ( ! Admin_Helper::is_post_edit() && ! Admin_Helper::is_term_edit() ) {
return;
}
wp_enqueue_script( 'rank-math-acf-post-analysis', rank_math()->plugin_url() . 'includes/modules/acf/assets/js/acf.js', [ 'wp-hooks', 'rank-math-analyzer' ], rank_math()->version, true );
Helper::add_json( 'acf', $this->get_config() );
}
/**
* Get ACF module config data.
*
* @return array The config data.
*/
private function get_config() {
/**
* Filter the ACF config data.
*
* @param array $config Config data array.
*/
return $this->do_filter(
'acf/config',
[
'pluginName' => 'rank-math-acf',
'headlines' => [],
'names' => [],
'refreshRate' => $this->get_refresh_rate(),
'blacklistTypes' => $this->get_blacklist_type(),
]
);
}
/**
* Retrieves the default blacklist - the field types we won't include in the SEO analysis.
*
* @return array The blacklisted field types.
*/
private function get_blacklist_type() {
/**
* Filter the blacklisted ACF field types.
*
* @param array $blacklist The blacklisted field types.
*/
return $this->do_filter(
'acf/blacklist/types',
[
'number',
'password',
'file',
'select',
'checkbox',
'radio',
'true_false',
'post_object',
'page_link',
'relationship',
'user',
'date_picker',
'color_picker',
'message',
'tab',
'repeater',
'flexible_content',
'group',
]
);
}
/**
* Get refresh rate to be used.
*
* @return int The number of milliseconds between runs.
*/
private function get_refresh_rate() {
/**
* Filter the refresh rate for changes to ACF fields.
*
* @param int $refresh_rate Refresh rates in milliseconds.
*/
$refresh_rate = $this->do_filter( 'acf/refresh_rate', 1000 );
$refresh_rate = intval( $refresh_rate, 10 );
return max( 200, $refresh_rate );
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,573 @@
<?php
/**
* The Analytics AJAX
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use RankMath\Helper;
use RankMath\Google\Api;
use RankMath\Helpers\Str;
use RankMath\Helpers\Param;
use RankMath\Google\Analytics;
use RankMath\Google\Authentication;
use RankMath\Sitemap\Sitemap;
use RankMath\Google\Console as Google_Analytics;
defined( 'ABSPATH' ) || exit;
/**
* AJAX class.
*/
class AJAX {
use \RankMath\Traits\Ajax;
/**
* The Constructor
*/
public function __construct() {
$this->ajax( 'query_analytics', 'query_analytics' );
$this->ajax( 'add_site_console', 'add_site_console' );
$this->ajax( 'disconnect_google', 'disconnect_google' );
$this->ajax( 'verify_site_console', 'verify_site_console' );
$this->ajax( 'google_check_all_services', 'check_all_services' );
// Google Data Management Services.
$this->ajax( 'analytics_delete_cache', 'delete_cache' );
$this->ajax( 'analytic_start_fetching', 'analytic_start_fetching' );
$this->ajax( 'analytic_cancel_fetching', 'analytic_cancel_fetching' );
// Save Linked Google Account info Services.
$this->ajax( 'check_console_request', 'check_console_request' );
$this->ajax( 'check_analytics_request', 'check_analytics_request' );
$this->ajax( 'save_analytic_profile', 'save_analytic_profile' );
$this->ajax( 'save_analytic_options', 'save_analytic_options' );
// Create new GA4 property.
$this->ajax( 'create_ga4_property', 'create_ga4_property' );
$this->ajax( 'get_ga4_data_streams', 'get_ga4_data_streams' );
}
/**
* Create a new GA4 property.
*/
public function create_ga4_property() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
$account_id = Param::post( 'accountID', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
$timezone = get_option( 'timezone_string' );
$offset = get_option( 'gmt_offset' );
if ( empty( $timezone ) && 0 !== $offset && floor( $offset ) === $offset ) {
$offset_st = $offset > 0 ? "-$offset" : '+' . absint( $offset );
$timezone = 'Etc/GMT' . $offset_st;
}
$args = [
'displayName' => get_bloginfo( 'sitename' ) . ' - GA4',
'parent' => "accounts/{$account_id}",
'timeZone' => empty( $timezone ) ? 'UTC' : $timezone,
];
$response = Api::get()->http_post(
'https://analyticsadmin.googleapis.com/v1alpha/properties',
$args
);
if ( ! empty( $response['error'] ) ) {
$this->error( $response['error']['message'] );
}
$property_id = str_replace( 'properties/', '', $response['name'] );
$property_name = esc_html( $response['displayName'] );
$all_accounts = get_option( 'rank_math_analytics_all_services' );
if ( isset( $all_accounts['accounts'][ $account_id ] ) ) {
$all_accounts['accounts'][ $account_id ]['properties'][ $property_id ] = [
'name' => $property_name,
'id' => $property_id,
'account_id' => $account_id,
'type' => 'GA4',
];
update_option( 'rank_math_analytics_all_services', $all_accounts );
}
$this->success(
[
'id' => $property_id,
'name' => $property_name,
]
);
}
/**
* Get the list of Web data streams.
*/
public function get_ga4_data_streams() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
$property_id = Param::post( 'propertyID', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
$response = Api::get()->http_get(
"https://analyticsadmin.googleapis.com/v1alpha/properties/{$property_id}/dataStreams"
);
if ( ! empty( $response['error'] ) ) {
$this->error( $response['error']['message'] );
}
if ( ! empty( $response['dataStreams'] ) ) {
$streams = [];
foreach ( $response['dataStreams'] as $data_stream ) {
$streams[] = [
'id' => str_replace( "properties/{$property_id}/dataStreams/", '', $data_stream['name'] ),
'name' => $data_stream['displayName'],
'measurementId' => $data_stream['webStreamData']['measurementId'],
];
}
$this->success( [ 'streams' => $streams ] );
}
$stream = $this->create_ga4_data_stream( "properties/{$property_id}" );
if ( ! is_array( $stream ) ) {
$this->error( $stream );
}
$this->success( [ 'streams' => [ $stream ] ] );
}
/**
* Check the Google Search Console request.
*/
public function check_console_request() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
$success = Api::get()->get_search_analytics();
if ( is_wp_error( $success ) ) {
$this->error( esc_html__( 'Data import will not work for this service as sufficient permissions are not given.', 'rank-math' ) );
}
$this->success();
}
/**
* Check the Google Analytics request.
*/
public function check_analytics_request() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
$success = Analytics::get_analytics( [], true );
if ( is_wp_error( $success ) ) {
$this->error( esc_html__( 'Data import will not work for this service as sufficient permissions are not given.', 'rank-math' ) );
}
$this->success();
}
/**
* Save analytic profile.
*/
public function save_analytic_profile() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
$profile = Param::post( 'profile' );
$country = Param::post( 'country', 'all' );
$days = Param::get( 'days', 90, FILTER_VALIDATE_INT );
$enable_index_status = Param::post( 'enableIndexStatus', false, FILTER_VALIDATE_BOOLEAN );
$success = Api::get()->get_search_analytics(
[
'country' => $country,
'profile' => $profile,
]
);
if ( is_wp_error( $success ) ) {
$this->error( esc_html__( 'Data import will not work for this service as sufficient permissions are not given.', 'rank-math' ) );
}
$prev = get_option( 'rank_math_google_analytic_profile', [] );
$value = [
'country' => $country,
'profile' => $profile,
'enable_index_status' => $enable_index_status,
];
update_option( 'rank_math_google_analytic_profile', $value );
// Remove other stored sites from option for privacy.
$all_accounts = get_option( 'rank_math_analytics_all_services', [] );
$all_accounts['sites'] = [ $profile => $profile ];
update_option( 'rank_math_analytics_all_services', $all_accounts );
// Purge Cache.
if ( ! empty( array_diff( $prev, $value ) ) ) {
DB::purge_cache();
}
// Start fetching console data.
Workflow\Workflow::do_workflow(
'console',
$days,
$prev,
$value
);
$this->success();
}
/**
* Save analytic profile.
*/
public function save_analytic_options() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
$value = [
'account_id' => Param::post( 'accountID', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ),
'property_id' => Param::post( 'propertyID', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ),
'view_id' => Param::post( 'viewID', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ),
'measurement_id' => Param::post( 'measurementID', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ),
'stream_name' => Param::post( 'streamName', false, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ),
'country' => Param::post( 'country', 'all', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_BACKTICK ),
'install_code' => Param::post( 'installCode', false, FILTER_VALIDATE_BOOLEAN ),
'anonymize_ip' => Param::post( 'anonymizeIP', false, FILTER_VALIDATE_BOOLEAN ),
'local_ga_js' => Param::post( 'localGAJS', false, FILTER_VALIDATE_BOOLEAN ),
'exclude_loggedin' => Param::post( 'excludeLoggedin', false, FILTER_VALIDATE_BOOLEAN ),
];
// Test Google Analytics (GA) connection request.
if ( ! empty( $value['view_id'] ) || ! empty( $value['country'] ) || ! empty( $value['property_id'] ) ) {
$request = Analytics::get_analytics(
[
'view_id' => $value['view_id'],
'country' => $value['country'],
'property_id' => $value['property_id'],
],
true
);
if ( is_wp_error( $request ) ) {
$this->error( esc_html__( 'Data import will not work for this service as sufficient permissions are not given.', 'rank-math' ) );
}
}
$days = Param::get( 'days', 90, FILTER_VALIDATE_INT );
$prev = get_option( 'rank_math_google_analytic_options' );
// Preserve adsense info.
if ( isset( $prev['adsense_id'] ) ) {
$value['adsense_id'] = $prev['adsense_id'];
}
update_option( 'rank_math_google_analytic_options', $value );
// Remove other stored accounts from option for privacy.
$all_accounts = get_option( 'rank_math_analytics_all_services', [] );
if ( isset( $all_accounts['accounts'][ $value['account_id'] ] ) ) {
$account = $all_accounts['accounts'][ $value['account_id'] ];
if ( isset( $account['properties'][ $value['property_id'] ] ) ) {
$property = $account['properties'][ $value['property_id'] ];
$account['properties'] = [ $value['property_id'] => $property ];
}
$all_accounts['accounts'] = [ $value['account_id'] => $account ];
}
update_option( 'rank_math_analytics_all_services', $all_accounts );
// Start fetching analytics data.
Workflow\Workflow::do_workflow(
'analytics',
$days,
$prev,
$value
);
$this->success();
}
/**
* Disconnect google.
*/
public function disconnect_google() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
Api::get()->revoke_token();
Workflow\Workflow::kill_workflows();
$this->success();
}
/**
* Cancel fetching data.
*/
public function analytic_cancel_fetching() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
Workflow\Workflow::kill_workflows();
$this->success( esc_html__( 'Data fetching cancelled.', 'rank-math' ) );
}
/**
* Start data fetching for console, analytics, adsense.
*/
public function analytic_start_fetching() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
if ( ! Authentication::is_authorized() ) {
$this->error( esc_html__( 'Google oAuth is not authorized.', 'rank-math' ) );
}
$days = Param::get( 'days', 90, FILTER_VALIDATE_INT );
$days = $days * 2;
$rows = DB::objects()
->selectCount( 'id' )
->getVar();
if ( empty( $rows ) ) {
delete_option( 'rank_math_analytics_installed' );
}
delete_option( 'rank_math_analytics_last_single_action_schedule_time' );
// Start fetching data.
foreach ( [ 'console', 'analytics', 'adsense' ] as $action ) {
Workflow\Workflow::do_workflow(
$action,
$days,
null,
null
);
}
$this->success( esc_html__( 'Data fetching started in the background.', 'rank-math' ) );
}
/**
* Delete cache.
*/
public function delete_cache() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
$days = Param::get( 'days', false, FILTER_VALIDATE_INT );
if ( ! $days ) {
$this->error( esc_html__( 'Not a valid settings founds to delete cache.', 'rank-math' ) );
}
// Delete fetched console data within specified date range.
DB::delete_by_days( $days );
// Cancel data fetch action.
Workflow\Workflow::kill_workflows();
delete_transient( 'rank_math_analytics_data_info' );
$db_info = DB::info();
$db_info['message'] = sprintf( '<div class="rank-math-console-db-info"><span class="dashicons dashicons-calendar-alt"></span> Cached Days: <strong>%s</strong></div>', $db_info['days'] ) .
sprintf( '<div class="rank-math-console-db-info"><span class="dashicons dashicons-editor-ul"></span> Data Rows: <strong>%s</strong></div>', Str::human_number( $db_info['rows'] ) ) .
sprintf( '<div class="rank-math-console-db-info"><span class="dashicons dashicons-editor-code"></span> Size: <strong>%s</strong></div>', size_format( $db_info['size'] ) );
$this->success( $db_info );
}
/**
* Search objects info by title or page and return.
*/
public function query_analytics() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
$query = Param::get( 'query', '', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_BACKTICK );
$data = DB::objects()
->whereLike( 'title', $query )
->orWhereLike( 'page', $query )
->limit( 10 )
->get();
$this->send( [ 'data' => $data ] );
}
/**
* Check all google services.
*/
public function check_all_services() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
$result = [
'isVerified' => false,
'inSearchConsole' => false,
'hasSitemap' => false,
'hasAnalytics' => false,
'hasAnalyticsProperty' => false,
];
$result['homeUrl'] = Google_Analytics::get_site_url();
$result['sites'] = Api::get()->get_sites();
$result['inSearchConsole'] = $this->is_site_in_search_console();
if ( $result['inSearchConsole'] ) {
$result['isVerified'] = Helper::is_localhost() ? true : Api::get()->is_site_verified( Google_Analytics::get_site_url() );
$result['hasSitemap'] = $this->has_sitemap_submitted();
}
$result['accounts'] = Api::get()->get_analytics_accounts();
if ( ! empty( $result['accounts'] ) ) {
$result['hasAnalytics'] = true;
$result['hasAnalyticsProperty'] = $this->is_site_in_analytics( $result['accounts'] );
}
$result = apply_filters( 'rank_math/analytics/check_all_services', $result );
update_option( 'rank_math_analytics_all_services', $result );
$this->success( $result );
}
/**
* Add site to search console
*/
public function add_site_console() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
$home_url = Google_Analytics::get_site_url();
Api::get()->add_site( $home_url );
Api::get()->verify_site( $home_url );
$this->success( [ 'sites' => Api::get()->get_sites() ] );
}
/**
* Verify site console.
*/
public function verify_site_console() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
$home_url = Google_Analytics::get_site_url();
Api::get()->verify_site( $home_url );
$this->success( [ 'verified' => true ] );
}
/**
* Is site in search console.
*
* @return boolean
*/
private function is_site_in_search_console() {
// Early Bail!!
if ( Helper::is_localhost() ) {
return true;
}
$sites = Api::get()->get_sites();
$home_url = Google_Analytics::get_site_url();
foreach ( $sites as $site ) {
if ( trailingslashit( $site ) === $home_url ) {
$profile = get_option( 'rank_math_google_analytic_profile' );
if ( empty( $profile ) ) {
update_option(
'rank_math_google_analytic_profile',
[
'country' => 'all',
'profile' => $home_url,
]
);
}
return true;
}
}
return false;
}
/**
* Is site in analytics.
*
* @param array $accounts Analytics accounts.
*
* @return boolean
*/
private function is_site_in_analytics( $accounts ) {
$home_url = Google_Analytics::get_site_url();
foreach ( $accounts as $account ) {
foreach ( $account['properties'] as $property ) {
if ( ! empty( $property['url'] ) && trailingslashit( $property['url'] ) === $home_url ) {
return true;
}
}
}
return false;
}
/**
* Has sitemap in search console.
*
* @return boolean
*/
private function has_sitemap_submitted() {
$home_url = Google_Analytics::get_site_url();
$sitemaps = Api::get()->get_sitemaps( $home_url );
if ( ! \is_array( $sitemaps ) || empty( $sitemaps ) ) {
return false;
}
foreach ( $sitemaps as $sitemap ) {
if ( $sitemap['path'] === $home_url . Sitemap::get_sitemap_index_slug() . '.xml' ) {
return true;
}
}
return false;
}
/**
* Create a new data stream.
*
* @param string $property_id GA4 property ID.
*/
private function create_ga4_data_stream( $property_id ) {
$args = [
'type' => 'WEB_DATA_STREAM',
'displayName' => 'Website',
'webStreamData' => [
'defaultUri' => home_url(),
],
];
$stream = Api::get()->http_post(
"https://analyticsadmin.googleapis.com/v1alpha/{$property_id}/dataStreams",
$args
);
if ( ! empty( $stream['error'] ) ) {
return $stream['error']['message'];
}
return [
'id' => str_replace( "properties/{$property_id}/dataStreams/", '', $stream['name'] ),
'name' => $stream['displayName'],
'measurementId' => $stream['webStreamData']['measurementId'],
];
}
}
@@ -0,0 +1,366 @@
<?php
/**
* Methods for frontend and backend in admin-only module
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\Analytics
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use RankMath\Helper;
use RankMath\Helpers\Str;
use RankMath\Helpers\DB as DB_Helper;
use RankMath\Traits\Hooker;
use RankMath\Google\Console;
use RankMath\Google\Authentication;
use RankMath\Analytics\Workflow\Jobs;
use RankMath\Analytics\Workflow\Workflow;
defined( 'ABSPATH' ) || exit;
/**
* Analytics class.
*/
class Analytics_Common {
use Hooker;
/**
* The Constructor
*/
public function __construct() {
if ( Helper::is_heartbeat() ) {
return;
}
if ( Helper::has_cap( 'analytics' ) ) {
$this->action( 'rank_math/admin_bar/items', 'admin_bar_items', 11 );
}
// Show Analytics block in the Dashboard widget only if account is connected or user has permissions.
if ( Helper::has_cap( 'analytics' ) && Authentication::is_authorized() ) {
$this->action( 'rank_math/dashboard/widget', 'dashboard_widget' );
}
new GTag();
new Analytics_Stats();
$this->action( 'plugins_loaded', 'maybe_init_email_reports', 15 );
$this->action( 'init', 'maybe_enable_email_reports', 20 );
$this->action( 'cmb2_save_options-page_fields_rank-math-options-general_options', 'maybe_update_report_schedule', 20, 3 );
Jobs::get();
Workflow::get();
$this->action( 'rest_api_init', 'init_rest_api' );
$this->filter( 'rank_math/webmaster/google_verify', 'add_site_verification' );
$this->filter( 'rank_math/tools/analytics_clear_caches', 'analytics_clear_caches' );
$this->filter( 'rank_math/tools/analytics_reindex_posts', 'analytics_reindex_posts' );
$this->filter( 'rank_math/tools/analytics_fix_collations', 'analytics_fix_collations' );
$this->filter( 'wp_helpers_notifications_render', 'replace_notice_link', 10, 3 );
}
/**
* Add stats widget into admin dashboard.
*/
public function dashboard_widget() {
?>
<h3>
<?php esc_html_e( 'Analytics', 'rank-math' ); ?>
<span><?php esc_html_e( 'Last 30 Days', 'rank-math' ); ?></span>
<a href="<?php echo esc_url( Helper::get_admin_url( 'analytics' ) ); ?>" class="rank-math-view-report" title="<?php esc_html_e( 'View Report', 'rank-math' ); ?>">
<i class="dashicons dashicons-chart-bar"></i>
</a>
</h3>
<div class="rank-math-dashboard-block items-4">
<?php
$items = $this->get_dashboard_widget_items();
foreach ( $items as $label => $item ) {
if ( ! $item['value'] ) {
continue;
}
?>
<div>
<h4>
<?php echo esc_html( $item['label'] ); ?>
<span class="rank-math-tooltip">
<em class="dashicons-before dashicons-editor-help"></em>
<span>
<?php echo esc_html( $item['desc'] ); ?>
</span>
</span>
</h4>
<?php $this->get_analytic_block( $item['data'], ! empty( $item['revert'] ) ); ?>
</div>
<?php } ?>
</div>
<?php
}
/**
* Return site verification code.
*
* @param string $content If any code from setting.
*
* @return string
*/
public function add_site_verification( $content ) {
$code = get_transient( 'rank_math_google_site_verification' );
return ! empty( $code ) ? $code : $content;
}
/**
* Load the REST API endpoints.
*/
public function init_rest_api() {
$controllers = [
new Rest(),
];
foreach ( $controllers as $controller ) {
$controller->register_routes();
}
}
/**
* Add admin bar item.
*
* @param Admin_Bar_Menu $menu Menu class instance.
*/
public function admin_bar_items( $menu ) {
$dot_color = '#ed5e5e';
if ( Console::is_console_connected() ) {
$dot_color = '#11ac84';
}
$menu->add_sub_menu(
'analytics',
[
'title' => esc_html__( 'Analytics', 'rank-math' ) . '<span class="rm-menu-new update-plugins" style="background: ' . $dot_color . ';margin-left: 5px;min-width: 10px;height: 10px;margin-bottom: -1px;display: inline-block;border-radius: 5px;"><span class="plugin-count"></span></span>',
'href' => Helper::get_admin_url( 'analytics' ),
'meta' => [ 'title' => esc_html__( 'Review analytics and sitemaps', 'rank-math' ) ],
'priority' => 20,
]
);
}
/**
* Purge cache.
*
* @return string
*/
public function analytics_clear_caches() {
DB::purge_cache();
return __( 'Analytics cache cleared.', 'rank-math' );
}
/**
* ReIndex posts.
*
* @return string
*/
public function analytics_reindex_posts() {
// Clear all objects data.
DB::objects()
->truncate();
// Clear all metadata related to object.
DB::table( 'postmeta' )
->where( 'meta_key', 'rank_math_analytic_object_id' )
->delete();
// Start reindexing posts.
( new \RankMath\Analytics\Workflow\Objects() )->flat_posts();
return __( 'Post re-index in progress.', 'rank-math' );
}
/**
* Fix table & column collations.
*
* @return string
*/
public function analytics_fix_collations() {
$tables = [
'rank_math_analytics_ga',
'rank_math_analytics_gsc',
'rank_math_analytics_keyword_manager',
'rank_math_analytics_inspections',
];
$objects_coll = DB_Helper::get_table_collation( 'rank_math_analytics_objects' );
$changed = 0;
foreach ( $tables as $table ) {
$changed += (int) DB_Helper::check_collation( $table, 'all', $objects_coll );
}
return $changed ? sprintf(
/* translators: %1$d: number of changes, %2$s: new collation. */
_n( '%1$d collation changed to %2$s.', '%1$d collations changed to %2$s.', $changed, 'rank-math' ),
$changed,
'`' . $objects_coll . '`'
) : __( 'No collation mismatch to fix.', 'rank-math' );
}
/**
* Init Email Reports class if the option is enabled.
*
* @return void
*/
public function maybe_init_email_reports() {
if ( Helper::get_settings( 'general.console_email_reports' ) ) {
new Email_Reports();
}
}
/**
* Enable the email reports option if the `enable_email_reports` param is set.
*
* @return void
*/
public function maybe_enable_email_reports() {
if ( ! Helper::has_cap( 'analytics' ) ) {
return;
}
if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'enable_email_reports' ) ) {
return;
}
if ( ! empty( $_GET['enable_email_reports'] ) ) {
$all_opts = rank_math()->settings->all_raw();
$general = $all_opts['general'];
$general['console_email_reports'] = 'on';
Helper::update_all_settings( $general, null, null );
rank_math()->settings->reset();
$this->schedule_email_reporting();
Helper::remove_notification( 'rank_math_analytics_new_email_reports' );
Helper::redirect( remove_query_arg( 'enable_email_reports' ) );
die();
}
}
/**
* Add/remove/change scheduled action when the report on/off or the frequency options are changed.
*
* @param int $object_id The ID of the current object.
* @param array $updated Array of field IDs that were updated.
* Will only include field IDs that had values change.
* @param object $cmb CMB object.
*/
public function maybe_update_report_schedule( $object_id, $updated, $cmb ) {
// Early bail if our options are not changed.
if ( ! in_array( 'console_email_reports', $updated, true ) && ! in_array( 'console_email_frequency', $updated, true ) ) {
return;
}
as_unschedule_all_actions( 'rank_math/analytics/email_report_event', [], 'rank-math' );
$values = $cmb->get_sanitized_values( $_POST ); // phpcs:ignore
if ( 'off' === $values['console_email_reports'] ) {
return;
}
$frequency = isset( $values['console_email_frequency'] ) ? $values['console_email_frequency'] : 'monthly';
$this->schedule_email_reporting( $frequency );
}
/**
* Replace link inside notice dynamically to avoid issues with the nonce.
*
* @param string $output Notice output.
* @param string $message Notice message.
* @param array $options Notice options.
*
* @return string
*/
public function replace_notice_link( $output, $message, $options ) {
$url = wp_nonce_url( Helper::get_admin_url( 'options-general&enable_email_reports=1#setting-panel-analytics' ), 'enable_email_reports' );
$output = str_replace( '###ENABLE_EMAIL_REPORTS###', $url, $output );
return $output;
}
/**
* Get Dashboard Widget items.
*/
private function get_dashboard_widget_items() {
// Get stats info within last 30 days.
Stats::get()->set_date_range( '-30 days' );
$data = Stats::get()->get_widget();
$analytics = get_option( 'rank_math_google_analytic_options' );
$is_connected = ! empty( $analytics ) && ! empty( $analytics['view_id'] );
return [
'search-traffic' => [
'label' => __( 'Search Traffic', 'rank-math' ),
'desc' => __( 'This is the number of pageviews carried out by visitors from Search Engines.', 'rank-math' ),
'value' => $is_connected && defined( 'RANK_MATH_PRO_FILE' ),
'data' => isset( $data->pageviews ) ? $data->pageviews : '',
],
'total-impressions' => [
'label' => __( 'Total Impressions', 'rank-math' ),
'desc' => __( 'How many times your site showed up in the search results.', 'rank-math' ),
'value' => true,
'data' => $data->impressions,
],
'total-clicks' => [
'label' => __( 'Total Clicks', 'rank-math' ),
'desc' => __( 'This is the number of pageviews carried out by visitors from Google.', 'rank-math' ),
'value' => ! $is_connected || ( $is_connected && ! defined( 'RANK_MATH_PRO_FILE' ) ),
'data' => $data->clicks,
],
'total-keywords' => [
'label' => __( 'Total Keywords', 'rank-math' ),
'desc' => __( 'Total number of keywords your site ranking below 100 position.', 'rank-math' ),
'value' => true,
'data' => $data->keywords,
],
'average-position' => [
'label' => __( 'Average Position', 'rank-math' ),
'desc' => __( 'Average position of all the ranking keywords below 100 position.', 'rank-math' ),
'value' => true,
'revert' => true,
'data' => $data->position,
],
];
}
/**
* Get analytic block
*
* @param object $item Item.
* @param boolean $revert Flag whether to revert difference icon or not.
*/
private function get_analytic_block( $item, $revert = false ) {
$total = isset( $item['total'] ) ? abs( $item['total'] ) : 0;
$difference = isset( $item['difference'] ) ? abs( $item['difference'] ) : 0;
$is_negative = isset( $item['difference'] ) && abs( $item['difference'] ) !== $item['difference'];
$diff_class = 'up';
if ( ( ! $revert && $is_negative ) || ( $revert && ! $is_negative && $item['difference'] > 0 ) ) {
$diff_class = 'down';
}
?>
<div class="rank-math-item-numbers">
<strong class="text-large" title="<?php echo esc_html( Str::human_number( $total ) ); ?>"><?php echo esc_html( Str::human_number( $total ) ); ?></strong>
<span class="rank-math-item-difference <?php echo esc_attr( $diff_class ); ?>" title="<?php echo esc_html( Str::human_number( $difference ) ); ?>"><?php echo esc_html( Str::human_number( $difference ) ); ?></span>
</div>
<?php
}
/**
* Schedule Email Reporting.
*
* @param string $frequency The frequency in which the action should run.
* @return void
*/
private function schedule_email_reporting( $frequency = 'monthly' ) {
$interval_days = Email_Reports::get_period_from_frequency( $frequency );
$midnight = strtotime( 'tomorrow midnight' );
as_schedule_recurring_action( $midnight, $interval_days * DAY_IN_SECONDS, 'rank_math/analytics/email_report_event', [], 'rank-math' );
}
}
@@ -0,0 +1,54 @@
<?php
/**
* Show Analytics stats on frontend.
*
* @since 1.0.86
* @package RankMath
* @subpackage RankMath\Analytics
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use RankMath\Helper;
use RankMath\KB;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Analytics_Stats class.
*/
class Analytics_Stats {
use Hooker;
/**
* The Constructor
*/
public function __construct() {
if ( ! Helper::can_add_frontend_stats() ) {
return;
}
$this->action( 'wp_enqueue_scripts', 'enqueue' );
}
/**
* Enqueue Styles and Scripts
*/
public function enqueue() {
if ( ! is_singular() || is_admin() || is_preview() || Helper::is_divi_frontend_editor() ) {
return;
}
$uri = untrailingslashit( plugin_dir_url( __FILE__ ) );
wp_enqueue_style( 'rank-math-analytics-stats', $uri . '/assets/css/admin-bar.css', null, rank_math()->version );
wp_enqueue_script( 'rank-math-analytics-stats', $uri . '/assets/js/admin-bar.js', [ 'jquery', 'wp-api-fetch', 'wp-element', 'wp-components', 'lodash' ], rank_math()->version, true );
Helper::add_json( 'isAnalyticsConnected', \RankMath\Google\Analytics::is_analytics_connected() );
Helper::add_json( 'hideFrontendStats', get_user_meta( get_current_user_id(), 'rank_math_hide_frontend_stats', true ) );
Helper::add_json( 'links', KB::get_links() );
}
}
@@ -0,0 +1,539 @@
<?php
/**
* The Analytics Module
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Helpers\Arr;
use RankMath\Helpers\Param;
use RankMath\Google\Api;
use RankMath\Module\Base;
use RankMath\Admin\Page;
use RankMath\Google\Console;
use RankMath\Google\Authentication;
use RankMath\Analytics\Workflow\Jobs;
use RankMath\Analytics\Workflow\OAuth;
use RankMath\Analytics\Workflow\Workflow;
defined( 'ABSPATH' ) || exit;
/**
* Analytics class.
*/
class Analytics extends Base {
/**
* Module ID.
*
* @var string
*/
public $id = '';
/**
* Module directory.
*
* @var string
*/
public $directory = '';
/**
* Module help.
*
* @var array
*/
public $help = [];
/**
* Module page.
*
* @var object
*/
public $page;
/**
* The Constructor
*/
public function __construct() {
if ( Helper::is_heartbeat() || ! Helper::has_cap( 'analytics' ) ) {
return;
}
$directory = dirname( __FILE__ );
$this->config(
[
'id' => 'analytics',
'directory' => $directory,
'help' => [
'title' => esc_html__( 'Analytics', 'rank-math' ),
'view' => $directory . '/views/help.php',
],
]
);
parent::__construct();
new AJAX();
Api::get();
Watcher::get();
Stats::get();
Jobs::get();
Workflow::get();
$this->action( 'admin_notices', 'render_notice' );
$this->action( 'rank_math/admin/enqueue_scripts', 'enqueue' );
$this->action( 'admin_enqueue_scripts', 'options_panel_messages' );
$this->action( 'wp_helpers_notification_dismissed', 'analytic_first_fetch_dismiss' );
if ( is_admin() ) {
$this->filter( 'rank_math/database/tools', 'add_tools' );
$this->filter( 'rank_math/settings/general', 'add_settings' );
$this->action( 'admin_init', 'refresh_token_missing', 25 );
$this->action( 'admin_init', 'cancel_fetch', 5 );
new OAuth();
}
}
/**
* Cancel Fetching of Google.
*/
public function cancel_fetch() {
$cancel = Param::get( 'cancel-fetch', false );
if (
empty( $cancel ) ||
! Param::get( '_wpnonce' ) ||
! wp_verify_nonce( Param::get( '_wpnonce' ), 'rank_math_cancel_fetch' ) ||
! Helper::has_cap( 'analytics' )
) {
return;
}
Workflow::kill_workflows();
}
/**
* If refresh token missing add notice.
*/
public function refresh_token_missing() {
// Bail if the user is not authenticated at all yet.
if ( ! Helper::is_site_connected() || ! Authentication::is_authorized() ) {
return;
}
$tokens = Authentication::tokens();
if ( ! empty( $tokens['refresh_token'] ) ) {
Helper::remove_notification( 'reconnect' );
return;
}
// Show admin notification.
Helper::add_notification(
sprintf(
/* translators: Auth URL */
'<i class="rm-icon rm-icon-rank-math"></i>' . __( 'It seems like the connection with your Google account & Rank Math needs to be made again. <a href="%s" class="rank-math-reconnect-google">Please click here.</a>', 'rank-math' ),
esc_url( Authentication::get_auth_url() )
),
[
'type' => 'error',
'classes' => 'rank-math-error rank-math-notice',
'id' => 'reconnect',
]
);
}
/**
* Hide fetch notice.
*
* @param string $notification_id Notification id.
*/
public function analytic_first_fetch_dismiss( $notification_id ) {
if ( 'rank_math_analytics_first_fetch' !== $notification_id ) {
return;
}
update_option( 'rank_math_analytics_first_fetch', 'hidden' );
}
/**
* Admin init.
*/
public function render_notice() {
$this->remove_action( 'admin_notices', 'render_notice' );
if ( 'fetching' === get_option( 'rank_math_analytics_first_fetch' ) ) {
$actions = as_get_scheduled_actions(
[
'order' => 'DESC',
'hook' => 'rank_math/analytics/clear_cache',
'status' => \ActionScheduler_Store::STATUS_PENDING,
]
);
if ( empty( $actions ) ) {
update_option( 'rank_math_analytics_first_fetch', 'hidden' );
return;
}
$action = current( $actions );
$schedule = $action->get_schedule();
$next_timestamp = $schedule->get_date()->getTimestamp();
// Calculate extra time needed for the inspections.
$objects_count = DB::objects()->selectCount( 'id' )->getVar();
$daily_api_limit = \RankMath\Analytics\Workflow\Inspections::API_LIMIT;
$time_gap = \RankMath\Analytics\Workflow\Inspections::REQUEST_GAP_SECONDS;
$extra_time = $objects_count * $time_gap;
if ( $objects_count > $daily_api_limit ) {
$extra_time += DAY_IN_SECONDS * floor( $objects_count / $daily_api_limit );
}
// phpcs:disable
$notification = new \RankMath\Admin\Notifications\Notification(
/* translators: delete counter */
sprintf(
'<svg style="vertical-align: middle; margin-right: 5px" viewBox="0 0 462.03 462.03" xmlns="http://www.w3.org/2000/svg" width="20"><g><path d="m462 234.84-76.17 3.43 13.43 21-127 81.18-126-52.93-146.26 60.97 10.14 24.34 136.1-56.71 128.57 54 138.69-88.61 13.43 21z"></path><path d="m54.1 312.78 92.18-38.41 4.49 1.89v-54.58h-96.67zm210.9-223.57v235.05l7.26 3 89.43-57.05v-181zm-105.44 190.79 96.67 40.62v-165.19h-96.67z"></path></g></svg>' .
esc_html__( 'Rank Math is importing latest data from connected Google Services, %1$s remaining.', 'rank-math' ) .
'&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;
}
}
@@ -0,0 +1,494 @@
<?php
/**
* The Analytics module database operations
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use RankMath\Helper;
use RankMath\Google\Api;
use RankMath\Google\Console;
use RankMath\Helpers\Str;
use RankMath\Helpers\DB as DB_Helper;
use RankMath\Admin\Database\Database;
defined( 'ABSPATH' ) || exit;
/**
* DB class.
*/
class DB {
/**
* Get any table.
*
* @param string $table_name Table name.
*
* @return \RankMath\Admin\Database\Query_Builder
*/
public static function table( $table_name ) {
return Database::table( $table_name );
}
/**
* Get console data table.
*
* @return \RankMath\Admin\Database\Query_Builder
*/
public static function analytics() {
return Database::table( 'rank_math_analytics_gsc' );
}
/**
* Get objects table.
*
* @return \RankMath\Admin\Database\Query_Builder
*/
public static function objects() {
return Database::table( 'rank_math_analytics_objects' );
}
/**
* Get inspections table.
*
* @return \RankMath\Admin\Database\Query_Builder
*/
public static function inspections() {
return Database::table( 'rank_math_analytics_inspections' );
}
/**
* Delete a record.
*
* @param int $days Decide whether to delete all or delete 90 days data.
*/
public static function delete_by_days( $days ) {
// Delete console data.
if ( Console::is_console_connected() ) {
if ( -1 === $days ) {
self::analytics()->truncate();
} else {
$start = date_i18n( 'Y-m-d H:i:s', strtotime( '-1 days' ) );
$end = date_i18n( 'Y-m-d H:i:s', strtotime( '-' . $days . ' days' ) );
self::analytics()->whereBetween( 'created', [ $end, $start ] )->delete();
}
}
// Delete analytics, adsense data.
do_action( 'rank_math/analytics/delete_by_days', $days );
self::purge_cache();
return true;
}
/**
* Delete record for comparison.
*/
public static function delete_data_log() {
$days = Helper::get_settings( 'general.console_caching_control', 90 );
// Delete old console data more than 2 times ago of specified number of days to keep the data.
$start = date_i18n( 'Y-m-d H:i:s', strtotime( '-' . ( $days * 2 ) . ' days' ) );
self::analytics()->where( 'created', '<', $start )->delete();
// Delete old analytics and adsense data.
do_action( 'rank_math/analytics/delete_data_log', $start );
}
/**
* Purge SC transient
*/
public static function purge_cache() {
$table = Database::table( 'options' );
$table->whereLike( 'option_name', 'top_keywords' )->delete();
$table->whereLike( 'option_name', 'posts_summary' )->delete();
$table->whereLike( 'option_name', 'top_keywords_graph' )->delete();
$table->whereLike( 'option_name', 'dashboard_stats_widget' )->delete();
$table->whereLike( 'option_name', 'rank_math_analytics_data_info' )->delete();
do_action( 'rank_math/analytics/purge_cache', $table );
wp_cache_flush();
}
/**
* Get search console table info.
*
* @return array
*/
public static function info() {
global $wpdb;
if ( ! Api::get()->is_console_connected() ) {
return [];
}
$key = 'rank_math_analytics_data_info';
$data = get_transient( $key );
if ( false !== $data ) {
return $data;
}
$days = self::analytics()
->selectCount( 'DISTINCT(created)', 'days' )
->getVar();
$rows = self::analytics()
->selectCount( 'id' )
->getVar();
$size = $wpdb->get_var( "SELECT SUM((data_length + index_length)) AS size FROM information_schema.TABLES WHERE table_schema='" . $wpdb->dbname . "' AND (table_name='" . $wpdb->prefix . "rank_math_analytics_gsc')" ); // phpcs:ignore
$data = compact( 'days', 'rows', 'size' );
$data = apply_filters( 'rank_math/analytics/analytics_tables_info', $data );
set_transient( $key, $data, DAY_IN_SECONDS );
return $data;
}
/**
* Has data pulled.
*
* @return boolean
*/
public static function has_data() {
static $rank_math_gsc_has_data;
if ( isset( $rank_math_gsc_has_data ) ) {
return $rank_math_gsc_has_data;
}
$id = self::objects()
->select( 'id' )
->limit( 1 )
->getVar();
$rank_math_gsc_has_data = $id > 0 ? true : false;
return $rank_math_gsc_has_data;
}
/**
* Check if console data exists at specified date.
*
* @param string $date Date to check data existence.
* @param string $action Action name to filter data type.
* @return boolean
*/
public static function date_exists( $date, $action = 'console' ) {
$tables['console'] = DB_Helper::check_table_exists( 'rank_math_analytics_gsc' ) ? 'rank_math_analytics_gsc' : '';
/**
* Filter: 'rank_math/analytics/date_exists_tables' - Allow developers to add more tables to check.
*/
$tables = apply_filters( 'rank_math/analytics/date_exists_tables', $tables, $date, $action );
if ( empty( $tables[ $action ] ) ) {
return true; // Should return true to avoid further data fetch action.
}
$table = self::table( $tables[ $action ] );
$id = $table
->select( 'id' )
->where( 'DATE(created)', $date )
->getVar();
return $id > 0 ? true : false;
}
/**
* Add a new record into objects table.
*
* @param array $args Values to insert.
*
* @return bool|int
*/
public static function add_object( $args = [] ) {
if ( empty( $args ) ) {
return false;
}
unset( $args['id'] );
$args = wp_parse_args(
$args,
[
'created' => current_time( 'mysql' ),
'page' => '',
'object_type' => 'post',
'object_subtype' => 'post',
'object_id' => 0,
'primary_key' => '',
'seo_score' => 0,
'page_score' => 0,
'is_indexable' => false,
'schemas_in_use' => '',
]
);
return self::objects()->insert( $args, [ '%s', '%s', '%s', '%s', '%d', '%s', '%d', '%d', '%d', '%s' ] );
}
/**
* Add new record in the inspections table.
*
* @param array $args Values to insert.
*
* @return bool|int
*/
public static function store_inspection( $args = [] ) {
if ( empty( $args ) || empty( $args['page'] ) ) {
return false;
}
unset( $args['id'] );
$defaults = self::get_inspection_defaults();
// Only keep $args items that are in $defaults.
$args = array_intersect_key( $args, $defaults );
// Apply defaults.
$args = wp_parse_args( $args, $defaults );
// We only have strings: placeholders will be '%s'.
$format = array_fill( 0, count( $args ), '%s' );
// Check if we have an existing record, based on 'page'.
$id = self::inspections()
->select( 'id' )
->where( 'page', $args['page'] )
->getVar();
if ( $id ) {
return self::inspections()
->set( $args )
->where( 'id', $id )
->update();
}
return self::inspections()->insert( $args, $format );
}
/**
* Get inspection defaults.
*
* @return array
*/
public static function get_inspection_defaults() {
$defaults = [
'created' => current_time( 'mysql' ),
'page' => '',
'index_verdict' => 'VERDICT_UNSPECIFIED',
'indexing_state' => 'INDEXING_STATE_UNSPECIFIED',
'coverage_state' => '',
'page_fetch_state' => 'PAGE_FETCH_STATE_UNSPECIFIED',
'robots_txt_state' => 'ROBOTS_TXT_STATE_UNSPECIFIED',
'mobile_usability_verdict' => 'VERDICT_UNSPECIFIED',
'mobile_usability_issues' => '',
'rich_results_verdict' => 'VERDICT_UNSPECIFIED',
'rich_results_items' => '',
'last_crawl_time' => '',
'crawled_as' => 'CRAWLING_USER_AGENT_UNSPECIFIED',
'google_canonical' => '',
'user_canonical' => '',
'sitemap' => '',
'referring_urls' => '',
'raw_api_response' => '',
];
return apply_filters( 'rank_math/analytics/inspection_defaults', $defaults );
}
/**
* Add/Update a record into/from objects table.
*
* @param array $args Values to update.
*
* @return bool|int
*/
public static function update_object( $args = [] ) {
if ( empty( $args ) ) {
return false;
}
// If object exists, try to update.
$old_id = absint( $args['id'] );
if ( ! empty( $old_id ) ) {
unset( $args['id'] );
$updated = self::objects()->set( $args )
->where( 'id', $old_id )
->where( 'object_id', absint( $args['object_id'] ) )
->update();
if ( ! empty( $updated ) ) {
return $old_id;
}
}
// In case of new object or failed to update, try to add.
return self::add_object( $args );
}
/**
* Add console records.
*
* @param string $date Date of creation.
* @param array $rows Data rows to insert.
*/
public static function add_query_page_bulk( $date, $rows ) {
$chunks = array_chunk( $rows, 50 );
foreach ( $chunks as $chunk ) {
self::bulk_insert_query_page_data( $date . ' 00:00:00', $chunk );
}
}
/**
* Bulk inserts records into a console table using WPDB. All rows must contain the same keys.
*
* @param string $date Date.
* @param array $rows Rows to insert.
*/
public static function bulk_insert_query_page_data( $date, $rows ) {
global $wpdb;
$data = [];
$placeholders = [];
$columns = [
'created',
'query',
'page',
'clicks',
'impressions',
'position',
'ctr',
];
$columns = '`' . implode( '`, `', $columns ) . '`';
$placeholder = [
'%s',
'%s',
'%s',
'%d',
'%d',
'%d',
'%d',
];
// Start building SQL, initialise data and placeholder arrays.
$sql = "INSERT INTO `{$wpdb->prefix}rank_math_analytics_gsc` ( $columns ) VALUES\n";
// Build placeholders for each row, and add values to data array.
foreach ( $rows as $row ) {
if (
$row['position'] > self::get_position_filter() ||
Str::contains( '?', $row['page'] )
) {
continue;
}
$data[] = $date;
$data[] = $row['query'];
$data[] = str_replace( Helper::get_home_url(), '', self::remove_hash( urldecode( $row['page'] ) ) );
$data[] = $row['clicks'];
$data[] = $row['impressions'];
$data[] = $row['position'];
$data[] = $row['ctr'];
$placeholders[] = '(' . implode( ', ', $placeholder ) . ')';
}
// Don't run insert with empty dataset, return 0 since no rows affected.
if ( empty( $data ) ) {
return 0;
}
// Stitch all rows together.
$sql .= implode( ",\n", $placeholders );
// Run the query. Returns number of affected rows.
return $wpdb->query( $wpdb->prepare( $sql, $data ) ); // phpcs:ignore
}
/**
* Remove hash part from Url.
*
* @param string $url Url to process.
* @return string
*/
public static function remove_hash( $url ) {
if ( ! Str::contains( '#', $url ) ) {
return $url;
}
$url = \explode( '#', $url );
return $url[0];
}
/**
* Get position filter.
*
* @return int
*/
private static function get_position_filter() {
$number = apply_filters( 'rank_math/analytics/position_limit', false );
if ( false === $number ) {
return 100;
}
return $number;
}
/**
* Get all inspections.
*
* @param array $params REST Parameters.
* @param int $per_page Limit.
*/
public static function get_inspections( $params, $per_page ) {
$page = ! empty( $params['page'] ) ? absint( $params['page'] ) : 1;
$per_page = absint( $per_page );
$offset = ( $page - 1 ) * $per_page;
$inspections = self::inspections()->table;
$objects = self::objects()->table;
$query = self::inspections()
->select( [ "$inspections.*", "$objects.title", "$objects.object_id" ] )
->leftJoin( $objects, "$inspections.page", "$objects.page" )
->where( "$objects.page", '!=', '' )
->orderBy( 'id', 'DESC' )
->limit( $per_page, $offset );
do_action_ref_array( 'rank_math/analytics/get_inspections_query', [ &$query, $params ] );
$results = $query->get();
return apply_filters( 'rank_math/analytics/get_inspections_results', $results );
}
/**
* Get inspections count.
*
* @param array $params REST Parameters.
*
* @return int
*/
public static function get_inspections_count( $params ) {
$pages = self::objects()->select( 'page' )->get( ARRAY_A );
$pages = array_unique( wp_list_pluck( $pages, 'page' ) );
$query = self::inspections()->selectCount( 'id', 'total' )->whereIn( 'page', $pages );
do_action_ref_array( 'rank_math/analytics/get_inspections_count_query', [ &$query, $params ] );
return $query->getVar();
}
}
@@ -0,0 +1,648 @@
<?php
/**
* Analytics Email Reports.
*
* @since 1.0.68
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Google\Console;
use RankMath\Admin\Admin_Helper;
use RankMath\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* Email_Reports class.
*/
class Email_Reports {
use Hooker;
/**
* Email content variables.
*
* @var array
*/
private $variables = [];
/**
* Path to the views directory.
*
* @var array
*/
private $views_path = '';
/**
* URL to the assets directory.
*
* @var array
*/
private $assets_url = '';
/**
* Charts Account.
*
* @var string
*/
private $charts_account = 'rankmath';
/**
* Charts Key.
*
* @var string
*/
private $charts_key = '10042B42-9193-428A-ABA7-5753F3370F84';
/**
* Graph data.
*
* @var array
*/
private $graph_data = [];
/**
* Debug mode.
*
* @var boolean
*/
private $debug = false;
/**
* The constructor.
*/
public function __construct() {
if ( ! Console::is_console_connected() ) {
return;
}
$directory = dirname( __FILE__ );
$this->views_path = $directory . '/views/email-reports/';
$url = plugin_dir_url( __FILE__ );
$this->assets_url = $this->do_filter( 'analytics/email_report_assets_url', $url . 'assets/' );
$this->hooks();
}
/**
* Add filter & action hooks.
*
* @return void
*/
public function hooks() {
$this->action( 'rank_math/analytics/email_report_event', 'email_report' );
$this->action( 'template_redirect', 'maybe_debug' );
$this->action( 'rank_math/analytics/email_report_html', 'replace_variables' );
$this->action( 'rank_math/analytics/email_report_html', 'strip_comments' );
}
/**
* Send Analytics report or error message.
*
* @return void
*/
public function email_report() {
$this->setup_variables();
$this->send_report();
}
/**
* Collect variables to be used in the Report template.
*
* @return void
*/
public function setup_variables() {
$stats = $this->get_stats();
$date = $this->get_date();
// Translators: placeholder is "rankmath.com" as a link.
$footer_text = sprintf( esc_html__( 'This email was sent to you as a registered member of %s.', 'rank-math' ), '<a href="###SITE_URL###">###SITE_URL_SIMPLE###</a>' );
$footer_text .= ' ';
// Translators: placeholder is "click here" as a link.
$footer_text .= sprintf( esc_html__( 'To update your email preferences, %s.', 'rank-math' ), '<a href="###SETTINGS_URL###">' . esc_html__( 'click here', 'rank-math' ) . '</a>' );
$footer_text .= '###ADDRESS###';
$this->variables = [
'site_url' => get_home_url(),
'site_url_simple' => explode( '://', get_home_url() )[1],
'settings_url' => Helper::get_admin_url( 'options-general#setting-panel-analytics' ),
'report_url' => Helper::get_admin_url( 'analytics' ),
'assets_url' => $this->assets_url,
'address' => '<br/> [rank_math_contact_info show="address"]',
'logo_link' => KB::get( 'email-reports-logo', 'Email Report Logo' ),
'period_days' => $date['period'],
'start_date' => $date['start'],
'end_date' => $date['end'],
'stats_clicks' => $stats['clicks'],
'stats_clicks_diff' => $stats['clicks_diff'],
'stats_traffic' => $stats['traffic'],
'stats_traffic_diff' => $stats['traffic_diff'],
'stats_impressions' => $stats['impressions'],
'stats_impressions_diff' => $stats['impressions_diff'],
'stats_keywords' => $stats['keywords'],
'stats_keywords_diff' => $stats['keywords_diff'],
'stats_position' => $stats['position'],
'stats_position_diff' => $stats['position_diff'],
'stats_top_3_positions' => $stats['top_3_positions'],
'stats_top_3_positions_diff' => $stats['top_3_positions_diff'],
'stats_top_10_positions' => $stats['top_10_positions'],
'stats_top_10_positions_diff' => $stats['top_10_positions_diff'],
'stats_top_50_positions' => $stats['top_50_positions'],
'stats_top_50_positions_diff' => $stats['top_50_positions_diff'],
'stats_invalid_data' => $stats['invalid_data'],
'footer_html' => $footer_text,
];
$this->variables = $this->do_filter( 'analytics/email_report_variables', $this->variables );
}
/**
* Get date data.
*
* @return array
*/
public function get_date() {
$period = self::get_period_from_frequency();
// Shift 3 days prior.
$subtract = DAY_IN_SECONDS * 3;
$start = strtotime( '-' . $period . ' days' ) - $subtract;
$end = strtotime( $this->do_filter( 'analytics/report_end_date', 'today' ) ) - $subtract;
$start = date_i18n( 'd M Y', $start );
$end = date_i18n( 'd M Y', $end );
return compact( 'start', 'end', 'period' );
}
/**
* Get Analytics stats.
*
* @return array
*/
public function get_stats() {
$period = self::get_period_from_frequency();
$stats = Stats::get();
$stats->set_date_range( "-{$period} days" );
// Basic stats.
$data = (array) $stats->get_analytics_summary();
$analytics = get_option( 'rank_math_google_analytic_options' );
$is_analytics_connected = ! empty( $analytics ) && ! empty( $analytics['view_id'] );
$out = [];
$out['impressions'] = $data['impressions']['total'];
$out['impressions_diff'] = $data['impressions']['difference'];
$out['traffic'] = 0;
$out['traffic_diff'] = 0;
if ( $is_analytics_connected && defined( 'RANK_MATH_PRO_FILE' ) && isset( $data['pageviews'] ) ) {
$out['traffic'] = $data['pageviews']['total'];
$out['traffic_diff'] = $data['pageviews']['difference'];
}
$out['clicks'] = 0;
$out['clicks_diff'] = 0;
if ( ! $is_analytics_connected || ( $is_analytics_connected && ! defined( 'RANK_MATH_PRO_FILE' ) ) ) {
$out['clicks'] = $data['clicks']['total'];
$out['clicks_diff'] = $data['clicks']['difference'];
}
$out['keywords'] = $data['keywords']['total'];
$out['keywords_diff'] = $data['keywords']['difference'];
$out['position'] = $data['position']['total'];
$out['position_diff'] = $data['position']['difference'];
// Keyword stats.
$kw_data = (array) $stats->get_top_keywords();
$out['top_3_positions'] = $kw_data['top3']['total'];
$out['top_3_positions_diff'] = $kw_data['top3']['difference'];
$out['top_10_positions'] = $kw_data['top10']['total'];
$out['top_10_positions_diff'] = $kw_data['top10']['difference'];
$out['top_50_positions'] = $kw_data['top50']['total'];
$out['top_50_positions_diff'] = $kw_data['top50']['difference'];
$out['invalid_data'] = false;
if ( ! count( array_filter( $out ) ) ) {
$out['invalid_data'] = true;
}
return $out;
}
/**
* Get date period (days) from the frequency option.
*
* @param string $frequency Frequency string.
*
* @return string
*/
public static function get_period_from_frequency( $frequency = null ) {
$periods = [
'monthly' => 30,
];
$periods = apply_filters( 'rank_math/analytics/email_report_periods', $periods );
if ( empty( $frequency ) ) {
$frequency = self::get_setting( 'frequency', 'monthly' );
}
if ( isset( $periods[ $frequency ] ) ) {
return absint( $periods[ $frequency ] );
}
return absint( reset( $periods ) );
}
/**
* Send report data.
*
* @return void
*/
public function send_report() {
$account = Admin_Helper::get_registration_data();
$report_email = [
'to' => $account['email'],
'subject' => sprintf(
// Translators: placeholder is the site URL.
__( 'Rank Math [SEO Report] - %s', 'rank-math' ),
explode( '://', get_home_url() )[1]
),
'message' => $this->get_template( 'report' ),
'headers' => 'Content-Type: text/html; charset=UTF-8',
];
/**
* Filter: rank_math/analytics/email_report_parameters
* Filters the report email parameters.
*/
$report_email = $this->do_filter( 'analytics/email_report_parameters', $report_email );
wp_mail(
$report_email['to'],
$report_email['subject'],
$report_email['message'],
$report_email['headers']
);
}
/**
* Get full HTML template for email.
*
* @param string $template Template name.
* @return string
*/
private function get_template( $template ) {
$file = $this->locate_template( $template );
/**
* Filter template file.
*/
$file = $this->do_filter( 'analytics/email_report_template', $file, $template );
if ( ! file_exists( $file ) ) {
return '';
}
ob_start();
include_once $file;
$content = ob_get_clean();
/**
* Filter template HTML.
*/
return $this->do_filter( 'analytics/email_report_html', $content );
}
/**
* Locate and include template part.
*
* @param string $part Template part.
* @param array $args Template arguments.
* @return mixed
*/
private function template_part( $part, $args = [] ) {
$file = $this->locate_template( $part );
/**
* Filter template part.
*/
$file = $this->do_filter( 'analytics/email_report_template_part', $file, $part, $args );
if ( ! file_exists( $file ) ) {
return '';
}
extract( $args, EXTR_SKIP ); // phpcs:ignore
include $file;
}
/**
* Replace variables in content.
*
* @param string $content Email content.
* @param string $recursion Recursion count, to account for double-encoded variables.
* @return string
*/
public function replace_variables( $content, $recursion = 1 ) {
foreach ( $this->variables as $key => $value ) {
if ( ! is_scalar( $value ) ) {
continue;
}
// Variables must be uppercase.
$key = mb_strtoupper( $key );
$content = str_replace( "###$key###", $value, $content );
}
if ( $recursion ) {
$recursion--;
$content = $this->replace_variables( $content, $recursion );
}
return do_shortcode( $content );
}
/**
* Strip HTML & CSS comments.
*
* @param string $content Email content.
* @return string
*/
public function strip_comments( $content ) {
$content = preg_replace( '[(<!--(.*)-->|/\*(.*)\*/)]isU', '', $content );
return $content;
}
/**
* Init debug mode if requested and allowed.
*
* @return void
*/
public function maybe_debug() {
if ( 1 !== absint( Param::get( 'rank_math_analytics_report_preview' ) ) ) {
return;
}
if ( ! Helper::has_cap( 'analytics' ) ) {
return;
}
$send = boolval( Param::get( 'send' ) );
$values = boolval( Param::get( 'values', '1' ) );
$this->debug( $send, $values );
}
/**
* Send or output the report email.
*
* @param boolean $send Send email or output to browser.
* @param boolean $values Replace variables with actual values.
* @return void
*/
private function debug( $send = false, $values = true ) {
$this->debug = true;
if ( $values ) {
$this->setup_variables();
}
if ( $send ) {
// Send it now.
$this->send_report();
$url = remove_query_arg(
[
'rank_math_analytics_report_preview',
'send',
'values',
]
);
Helper::redirect( $url );
exit;
}
// Output it to the browser.
echo $this->get_template( 'report' ); // phpcs:ignore
die();
}
/**
* Variable getter, whenever the value is needed in PHP.
*
* @param string $name Variable name.
* @return mixed
*/
public function get_variable( $name ) {
if ( isset( $this->variables[ $name ] ) ) {
return $this->variables[ $name ];
}
return "###$name###";
}
/**
* Setting getter.
*
* @param string $option Option name.
* @param mixed $default Default value.
* @return mixed
*/
public static function get_setting( $option, $default = false ) {
return Helper::get_settings( 'general.console_email_' . $option, $default );
}
/**
* Output image inside the email template.
*
* @param string $url Image URL.
* @param string $width Image width.
* @param string $height Image height.
* @param string $alt ALT text.
* @param array $attr Additional attributes.
* @return void
*/
public function image( $url, $width = 0, $height = 0, $alt = '', $attr = [] ) {
$atts = $attr;
$atts['border'] = '0';
if ( ! isset( $atts['src'] ) ) {
$atts['src'] = $url;
}
if ( ! isset( $atts['width'] ) && $width ) {
$atts['width'] = $width;
}
if ( ! isset( $atts['height'] ) && $height ) {
$atts['height'] = $height;
}
if ( ! isset( $atts['alt'] ) ) {
$atts['alt'] = $alt;
}
if ( ! isset( $atts['style'] ) ) {
$atts['style'] = 'border: 0; outline: none; text-decoration: none; display: inline-block;';
}
if ( substr( $atts['src'], 0, 4 ) !== 'http' && substr( $atts['src'], 0, 3 ) !== '###' ) {
$atts['src'] = $this->assets_url . 'img/' . $atts['src'];
}
$atts = $this->do_filter( 'analytics/email_report_image_atts', $atts, $url, $width, $height, $alt, $attr );
$attributes = '';
foreach ( $atts as $name => $value ) {
if ( ! empty( $value ) ) {
$value = ( 'src' === $name ) ? esc_url_raw( $value ) : esc_attr( $value );
$attributes .= ' ' . $name . '="' . $value . '"';
}
}
$image = "<img $attributes>";
$image = $this->do_filter( 'analytics/email_report_image_html', $image, $url, $width, $height, $alt, $attr );
echo $image; // phpcs:ignore
}
/**
* Gets template path.
*
* @param string $template_name Template name.
* @param bool $return_full_path Return the full path or not.
* @return string
*/
public function locate_template( $template_name, $return_full_path = true ) {
$default_paths = [ $this->views_path ];
$template_paths = $this->do_filter( 'analytics/email_report_template_paths', $default_paths );
$paths = array_reverse( $template_paths );
$located = '';
$path_partial = '';
foreach ( $paths as $path ) {
if ( file_exists( $full_path = trailingslashit( $path ) . $template_name . '.php' ) ) { // phpcs:ignore
$located = $full_path;
$path_partial = $path;
break;
}
}
return $return_full_path ? $located : $path_partial;
}
/**
* Load all graph data into memory.
*
* @return void
*/
private function load_graph_data() {
$period = self::get_period_from_frequency();
$stats = Stats::get();
$stats->set_date_range( "-{$period} days" );
$this->graph_data = (array) $stats->get_analytics_summary_graph();
}
/**
* Get data points for graph.
*
* @param string $chart Chart to get data for.
* @return array
*/
public function get_graph_data( $chart ) {
if ( empty( $this->graph_data ) ) {
$this->load_graph_data();
}
$data = [];
$group = 'merged';
$prop = $chart;
if ( 'traffic' === $chart ) {
$group = 'traffic';
$prop = 'pageviews';
}
if ( empty( $this->graph_data[ $group ] ) ) {
return $data;
}
foreach ( (array) $this->graph_data[ $group ] as $range_data ) {
$range_data = (array) $range_data;
if ( isset( $range_data[ $prop ] ) ) {
$data[] = $range_data[ $prop ];
}
}
return $data;
}
/**
* Charts API sign request.
*
* @param string $query Query.
* @param string $code Code.
* @return string
*/
private function charts_api_sign( $query, $code ) {
return hash_hmac( 'sha256', $query, $code );
}
/**
* Generate URL for the Charts API image.
*
* @param array $graph_data Graph data points.
* @param int $width Image height.
* @param int $height Image width.
*
* @return string
*/
private function charts_api_url( $graph_data, $width = 192, $height = 102 ) {
$params = [
'chco' => '80ace7',
'chds' => 'a',
'chf' => 'bg,s,f7f9fb',
'chls' => 4,
'chm' => 'B,e2eeff,0,0,0',
'chs' => "{$width}x{$height}",
'cht' => 'ls',
'chd' => 'a:' . join( ',', $graph_data ),
'icac' => $this->charts_account,
];
$query_string = urldecode( http_build_query( $params ) );
$signature = $this->charts_api_sign( $query_string, $this->charts_key );
return 'https://charts.rankmath.com/chart?' . $query_string . '&ichm=' . $signature;
}
/**
* Check if fields should be hidden.
*
* @return bool
*/
public static function are_fields_hidden() {
return apply_filters( 'rank_math/analytics/hide_email_report_options', false );
}
}
@@ -0,0 +1,413 @@
<?php
/**
* The GTag
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*
* @copyright 2019 Google LLC
* The following code is a derivative work of the code from the Site Kit Plugin(https://sitekit.withgoogle.com), which is licensed under Apache License 2.0.
*/
namespace RankMath\Analytics;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Helpers\Str;
use AMP_Theme_Support;
use AMP_Options_Manager;
defined( 'ABSPATH' ) || exit;
/**
* GTag class.
*/
class GTag {
use Hooker;
/**
* Primary "standard" AMP website mode.
*
* @var string
*/
const AMP_MODE_PRIMARY = 'primary';
/**
* Secondary AMP website mode.
*
* @var string
*/
const AMP_MODE_SECONDARY = 'secondary';
/**
* Options.
*
* @var array
*/
private $options = null;
/**
* Internal flag set after gtag amp print for the first time.
*
* @var bool
*/
private $did_amp_gtag = false;
/**
* The Constructor
*/
public function __construct() {
$this->action( 'template_redirect', 'add_analytics_tag' );
}
/**
* Add analytics tag.
*/
public function add_analytics_tag() {
// Early Bail!!
$use_snippet = $this->get( 'install_code' );
if ( ! $use_snippet ) {
return;
}
$property_id = $this->get( 'property_id' );
if ( ! $property_id ) {
return;
}
$this->action( 'wp_head', 'print_tracking_opt_out', 0 ); // For non-AMP and AMP.
$this->action( 'web_stories_story_head', 'print_tracking_opt_out', 0 ); // For Web Stories plugin.
if ( $this->is_amp() ) {
$this->action( 'amp_print_analytics', 'print_amp_gtag' ); // For all AMP modes.
$this->action( 'wp_footer', 'print_amp_gtag', 20 ); // For AMP Standard and Transitional.
$this->action( 'amp_post_template_footer', 'print_amp_gtag', 20 ); // For AMP Reader.
$this->action( 'web_stories_print_analytics', 'print_amp_gtag' ); // For Web Stories plugin.
// Load amp-analytics component for AMP Reader.
$this->filter( 'amp_post_template_data', 'amp_analytics_component_data' );
} else {
// For non-AMP. If current WordPress verion is 5.7 or above, use core function introducted from WordPress 5.7 and add async loading to gtag script.
if ( version_compare( get_bloginfo( 'version' ), '5.7', '<' ) ) {
$this->action( 'wp_enqueue_scripts', 'enqueue_gtag_js' );
} else {
$this->action( 'wp_head', 'add_gtag_js' );
}
}
}
/**
* Print gtag <amp-analytics> tag.
*/
public function print_amp_gtag() {
if ( $this->did_amp_gtag ) {
return;
}
$this->did_amp_gtag = true;
$property_id = $this->get( 'property_id' );
$gtag_options = [
'vars' => [
'gtag_id' => $property_id,
'config' => [
$property_id => [
'groups' => 'default',
'linker' => [
'domains' => [ $this->get_home_domain() ],
],
],
],
],
'optoutElementId' => '__gaOptOutExtension',
];
?>
<amp-analytics type="gtag" data-credentials="include">
<script type="application/json">
<?php echo wp_json_encode( $gtag_options ); ?>
</script>
</amp-analytics>
<?php
}
/**
* Loads AMP analytics script if opted in.
*
* @param array $data AMP template data.
* @return array Filtered $data.
*/
public function amp_analytics_component_data( $data ) {
if ( isset( $data['amp_component_scripts']['amp-analytics'] ) ) {
return $data;
}
$data['amp_component_scripts']['amp-analytics'] = 'https://cdn.ampproject.org/v0/amp-analytics-0.1.js';
return $data;
}
/**
* Print gtag snippet for non-amp. Used only for WordPress 5.7 or above.
*/
public function add_gtag_js() {
if ( $this->is_tracking_disabled() ) {
return;
}
$gtag_script_info = $this->get_gtag_info();
wp_print_script_tag(
[
'id' => 'google_gtagjs',
'src' => $gtag_script_info['url'],
'async' => true,
]
);
wp_print_inline_script_tag(
$gtag_script_info['inline'],
[
'id' => 'google_gtagjs-inline',
]
);
}
/**
* Print gtag snippet for non-amp. Used for below WordPress 5.7.
*/
public function enqueue_gtag_js() {
if ( $this->is_tracking_disabled() ) {
return;
}
$gtag_script_info = $this->get_gtag_info();
wp_enqueue_script( // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
'google_gtagjs',
$gtag_script_info['url'],
false,
null,
false
);
wp_add_inline_script(
'google_gtagjs',
$gtag_script_info['inline']
);
}
/**
* Gets the current AMP mode.
*
* @return bool|string 'primary' if in standard mode,
* 'secondary' if in transitional or reader modes
* false if AMP not active, or unknown mode
*/
public function get_amp_mode() {
if ( ! class_exists( 'AMP_Theme_Support' ) ) {
return false;
}
$exposes_support_mode = defined( 'AMP_Theme_Support::STANDARD_MODE_SLUG' )
&& defined( 'AMP_Theme_Support::TRANSITIONAL_MODE_SLUG' )
&& defined( 'AMP_Theme_Support::READER_MODE_SLUG' );
if ( defined( 'AMP__VERSION' ) ) {
$amp_plugin_version = AMP__VERSION;
if ( strpos( $amp_plugin_version, '-' ) !== false ) {
$amp_plugin_version = explode( '-', $amp_plugin_version )[0];
}
$amp_plugin_version_2_or_higher = version_compare( $amp_plugin_version, '2.0.0', '>=' );
} else {
$amp_plugin_version_2_or_higher = false;
}
if ( $amp_plugin_version_2_or_higher ) {
$exposes_support_mode = class_exists( 'AMP_Options_Manager' )
&& method_exists( 'AMP_Options_Manager', 'get_option' )
&& $exposes_support_mode;
} else {
$exposes_support_mode = class_exists( 'AMP_Theme_Support' )
&& method_exists( 'AMP_Theme_Support', 'get_support_mode' )
&& $exposes_support_mode;
}
if ( $exposes_support_mode ) {
// If recent version, we can properly detect the mode.
if ( $amp_plugin_version_2_or_higher ) {
$mode = AMP_Options_Manager::get_option( 'theme_support' );
} else {
$mode = AMP_Theme_Support::get_support_mode();
}
if ( AMP_Theme_Support::STANDARD_MODE_SLUG === $mode ) {
return self::AMP_MODE_PRIMARY;
}
if ( in_array( $mode, [ AMP_Theme_Support::TRANSITIONAL_MODE_SLUG, AMP_Theme_Support::READER_MODE_SLUG ], true ) ) {
return self::AMP_MODE_SECONDARY;
}
} elseif ( function_exists( 'amp_is_canonical' ) ) {
// On older versions, if it is not primary AMP, it is definitely secondary AMP (transitional or reader mode).
if ( amp_is_canonical() ) {
return self::AMP_MODE_PRIMARY;
}
return self::AMP_MODE_SECONDARY;
}
return false;
}
/**
* Is AMP url.
*
* @return bool
*/
protected function is_amp() {
if ( is_singular( 'web-story' ) ) {
return true;
}
return function_exists( 'is_amp_endpoint' ) && is_amp_endpoint();
}
/**
* Is tracking disabled.
*
* @return bool
*/
protected function is_tracking_disabled() {
if ( ! $this->get( 'exclude_loggedin' ) ) {
return false;
}
$logged_in = is_user_logged_in();
$filter_match = false;
if ( $logged_in ) {
if ( ! function_exists( 'get_editable_roles' ) ) {
require_once ABSPATH . 'wp-admin/includes/user.php';
}
$all_roles = array_keys( get_editable_roles() );
$all_roles = array_combine( $all_roles, $all_roles ); // Copy values to keys for easier filtering.
$user_roles = array_flip( get_userdata( get_current_user_id() )->roles );
$filter_match = count( array_intersect_key( (array) $this->do_filter( 'analytics/gtag_exclude_loggedin_roles', $all_roles ), $user_roles ) );
}
return $filter_match;
}
/**
* Gets the hostname of the home URL.
*
* @return string
*/
private function get_home_domain() {
return wp_parse_url( home_url(), PHP_URL_HOST );
}
/**
* Get option
*
* @param string $id Option to get.
*
* @return mixed
*/
protected function get( $id ) {
if ( is_null( $this->options ) ) {
$this->options = $this->normalize_it( get_option( 'rank_math_google_analytic_options', [] ) );
}
$value = isset( $this->options[ $id ] ) ? $this->options[ $id ] : false;
if ( $value && 'property_id' === $id && ! Str::starts_with( 'UA-', $value ) ) {
$value = $this->get( 'measurement_id' );
}
return $value;
}
/**
* Get gtag script info
*
* @return mixed
*/
protected function get_gtag_info() {
// Get Google Analytics Property ID.
$property_id = $this->get( 'property_id' );
// Get main gtag script Url.
$url = 'https://www.googletagmanager.com/gtag/js?id=' . esc_attr( $property_id );
$gtag_opt = [];
if ( $this->get_amp_mode() ) {
$gtag_opt['linker'] = [
'domains' => [ $this->get_home_domain() ],
];
}
$gtag_inline_linker_script = '';
if ( ! empty( $gtag_opt['linker'] ) ) {
$gtag_inline_linker_script = 'gtag(\'set\', \'linker\', ' . wp_json_encode( $gtag_opt['linker'] ) . ' );';
}
unset( $gtag_opt['linker'] );
// Get Google Analytics Property ID.
$gtag_config = [];
$gtag_config = $this->do_filter( 'analytics/gtag_config', $gtag_config );
// Construct inline scripts.
$gtag_inline_script = 'window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}';
$gtag_inline_script .= $gtag_inline_linker_script;
$gtag_inline_script .= 'gtag(\'js\', new Date());';
$gtag_inline_script .= 'gtag(\'config\', \'' . esc_attr( $property_id ) . '\', {' . join( ', ', $gtag_config ) . '} );';
$gtag = $this->do_filter(
'analytics/gtag',
[
'url' => $url,
'inline' => $gtag_inline_script,
]
);
return $gtag;
}
/**
* Normalize option data
*
* @param mixed $options Array to normalize.
* @return mixed
*/
protected function normalize_it( $options ) {
foreach ( (array) $options as $key => $value ) {
$options[ $key ] = is_array( $value ) ? $this->normalize_it( $value ) : Helper::normalize_data( $value );
}
return $options;
}
/**
* Print the user tracking opt-out code
*
* This script opts out of all Google Analytics tracking, for all measurement IDs, regardless of implementation.
*
* @link https://developers.google.com/analytics/devguides/collection/analyticsjs/user-opt-out
*/
public function print_tracking_opt_out() {
if ( ! $this->is_tracking_disabled() ) {
return;
}
if ( $this->is_amp() ) :
?>
<script type="application/ld+json" id="__gaOptOutExtension"></script>
<?php else : ?>
<script type="text/javascript">window['ga-disable-<?php echo esc_js( $this->get( 'property_id' ) ); ?>'] = true;</script>
<?php
endif;
}
}
@@ -0,0 +1,316 @@
<?php
/**
* The Analytics Module
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use WP_REST_Request;
use RankMath\Analytics\Stats;
defined( 'ABSPATH' ) || exit;
/**
* Keywords class.
*/
class Keywords extends Posts {
/**
* Get most recent day's keywords.
*
* @return array
*/
public function get_recent_keywords() {
global $wpdb;
$query = $wpdb->prepare(
"SELECT query
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE DATE(created) = (SELECT MAX(DATE(created)) FROM {$wpdb->prefix}rank_math_analytics_gsc WHERE created BETWEEN %s AND %s)
GROUP BY query",
Stats::get()->start_date,
Stats::get()->end_date
);
$data = $wpdb->get_results( $query ); // phpcs:ignore
return $data;
}
/**
* Get keywords data.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_keywords_rows( WP_REST_Request $request ) {
// Get most recent day's keywords only.
$keywords = $this->get_recent_keywords();
$keywords = wp_list_pluck( $keywords, 'query' );
$keywords = array_map( 'esc_sql', $keywords );
$keywords = array_map( 'mb_strtolower', $keywords );
$per_page = 25;
$cache_args = $request->get_params();
$cache_args['per_page'] = $per_page;
$cache_group = 'rank_math_rest_keywords_rows';
$cache_key = $this->generate_hash( $cache_args );
$rows = $this->get_cache( $cache_key, $cache_group );
if ( empty( $rows ) ) {
$rows = $this->get_analytics_data(
[
'dimension' => 'query',
'objects' => false,
'pageview' => false,
'orderBy' => ! empty( $request->get_param( 'orderby' ) ) ? $request->get_param( 'orderby' ) : 'impressions',
'order' => in_array( $request->get_param( 'order' ), [ 'asc', 'desc' ], true ) ? strtoupper( $request->get_param( 'order' ) ) : 'DESC',
'offset' => ( $request->get_param( 'page' ) - 1 ) * $per_page,
'perpage' => $per_page,
'sub_where' => " AND query IN ('" . join( "', '", $keywords ) . "')",
]
);
}
$rows = apply_filters( 'rank_math/analytics/keywords', $rows );
if ( empty( $rows ) ) {
$rows['response'] = 'No Data';
}
return $rows;
}
/**
* Get top keywords overview filtered by keyword position range.
*
* @return object
*/
public function get_top_keywords() {
global $wpdb;
$cache_key = $this->get_cache_key( 'top_keywords', $this->days . 'days' );
$cache = get_transient( $cache_key );
if ( false !== $cache ) {
return $cache;
}
// Get current keywords count filtered by position range.
$query = $wpdb->prepare(
"SELECT COUNT(t1.query) AS total,
CASE
WHEN t1.position BETWEEN 1 AND 3 THEN 'top3'
WHEN t1.position BETWEEN 4 AND 10 THEN 'top10'
WHEN t1.position BETWEEN 11 AND 50 THEN 'top50'
WHEN t1.position BETWEEN 51 AND 100 THEN 'top100'
ELSE 'none'
END AS position_type
FROM (SELECT query, ROUND( AVG(position), 0 ) AS position
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s AND DATE(created) = (SELECT MAX(DATE(created)) FROM {$wpdb->prefix}rank_math_analytics_gsc WHERE created BETWEEN %s AND %s)
GROUP BY query
ORDER BY position) as t1
GROUP BY position_type",
$this->start_date,
$this->end_date,
$this->start_date,
$this->end_date
);
$data = $wpdb->get_results( $query ); // phpcs:ignore
// Get compare keywords count filtered by position range.
$query = $wpdb->prepare(
"SELECT COUNT(t1.query) AS total,
CASE
WHEN t1.position BETWEEN 1 AND 3 THEN 'top3'
WHEN t1.position BETWEEN 4 AND 10 THEN 'top10'
WHEN t1.position BETWEEN 11 AND 50 THEN 'top50'
WHEN t1.position BETWEEN 51 AND 100 THEN 'top100'
ELSE 'none'
END AS position_type
FROM (SELECT query, ROUND( AVG(position), 0 ) AS position
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s AND DATE(created) = (SELECT MAX(DATE(created)) FROM {$wpdb->prefix}rank_math_analytics_gsc WHERE created BETWEEN %s AND %s)
GROUP BY query
ORDER BY position) as t1
GROUP BY position_type",
$this->compare_start_date,
$this->compare_end_date,
$this->compare_start_date,
$this->compare_end_date
);
$compare = $wpdb->get_results( $query ); // phpcs:ignore
$positions = [
'top3' => [
'total' => 0,
'difference' => 0,
],
'top10' => [
'total' => 0,
'difference' => 0,
],
'top50' => [
'total' => 0,
'difference' => 0,
],
'top100' => [
'total' => 0,
'difference' => 0,
],
'ctr' => 0,
'ctrDifference' => 0,
];
// Calculate total and difference for each position range.
$positions = $this->get_top_position_total( $positions, $data, 'total' );
$positions = $this->get_top_position_total( $positions, $compare, 'difference' );
// Get CTR.
$positions['ctr'] = DB::analytics()
->selectAvg( 'ctr', 'ctr' )
->whereBetween( 'created', [ $this->start_date, $this->end_date ] )
->getVar();
// Get compare CTR.
$positions['ctrDifference'] = DB::analytics()
->selectAvg( 'ctr', 'ctr' )
->whereBetween( 'created', [ $this->compare_start_date, $this->compare_end_date ] )
->getVar();
// Calculate current CTR and CTR difference.
$positions['ctr'] = empty( $positions['ctr'] ) ? 0 : $positions['ctr'];
$positions['ctrDifference'] = empty( $positions['ctrDifference'] ) ? 0 : $positions['ctrDifference'];
$positions['ctrDifference'] = $positions['ctr'] - $positions['ctrDifference'];
set_transient( $cache_key, $positions, DAY_IN_SECONDS );
return $positions;
}
/**
* Get position graph
*
* @return array
*/
public function get_top_position_graph() {
global $wpdb;
$cache_key = $this->get_cache_key( 'top_keywords_graph', $this->days . 'days' );
$cache = get_transient( $cache_key );
if ( false !== $cache ) {
return $cache;
}
// Step1. Get splitted date intervals for graph within selected date range.
$intervals = $this->get_intervals();
$sql_daterange = $this->get_sql_date_intervals( $intervals );
// Step2. Get most recent days for each splitted date intervals.
// phpcs:disable
$query = $wpdb->prepare(
"SELECT MAX(DATE(created)) as date, {$sql_daterange}
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s
GROUP BY range_group",
$this->start_date,
$this->end_date
);
$position_dates = $wpdb->get_results( $query, ARRAY_A );
// phpcs:enable
if ( count( $position_dates ) === 0 ) {
return [];
}
$dates = [];
foreach ( $position_dates as $row ) {
array_push( $dates, $row['date'] );
}
$dates = '(\'' . join( '\', \'', $dates ) . '\')';
// Step3. Get keywords count filtered by position range group for each date.
// phpcs:disable
$query = $wpdb->prepare(
"SELECT COUNT(t.query) AS total, t.date,
CASE
WHEN t.position BETWEEN 1 AND 3 THEN 'top3'
WHEN t.position BETWEEN 4 AND 10 THEN 'top10'
WHEN t.position BETWEEN 11 AND 50 THEN 'top50'
WHEN t.position BETWEEN 51 AND 100 THEN 'top100'
ELSE 'none'
END AS position_type
FROM (
SELECT query, ROUND( AVG(position), 0 ) AS position, Date(created) as date
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s AND DATE(created) IN {$dates}
GROUP BY DATE(created), query) AS t
GROUP BY t.date, position_type",
$this->start_date,
$this->end_date
);
$position_data = $wpdb->get_results( $query );
// phpcs:enable
// Construct return data.
$data = $this->get_date_array(
$intervals['dates'],
[
'top3' => 0,
'top10' => 0,
'top50' => 0,
'top100' => 0,
]
);
foreach ( $position_data as $row ) {
if ( ! isset( $intervals['map'][ $row->date ] ) ) {
continue;
}
$date = $intervals['map'][ $row->date ];
if ( ! isset( $data[ $date ][ $row->position_type ] ) ) {
continue;
}
$key = $row->position_type;
$data[ $date ][ $key ] = $row->total;
}
$data = array_values( $data );
set_transient( $cache_key, $data, DAY_IN_SECONDS );
return $data;
}
/**
* Get top position total.
*
* @param array $positions Position array.
* @param array $rows Data to process.
* @param string $where What data to get total.
*
* @return array
*/
private function get_top_position_total( $positions, $rows, $where ) {
foreach ( $rows as $row ) {
$positions[ $row->position_type ][ $where ] = $row->total;
}
if ( 'difference' === $where ) {
$positions['top3']['difference'] = $positions['top3']['total'] - $positions['top3']['difference'];
$positions['top10']['difference'] = $positions['top10']['total'] - $positions['top10']['difference'];
$positions['top50']['difference'] = $positions['top50']['total'] - $positions['top50']['difference'];
$positions['top100']['difference'] = $positions['top100']['total'] - $positions['top100']['difference'];
}
return $positions;
}
}
@@ -0,0 +1,116 @@
<?php
/**
* The Analytics Module
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
defined( 'ABSPATH' ) || exit;
/**
* Objects class.
*/
class Objects extends Summary {
/**
* Get objects for pages.
*
* @param array $pages Array of urls.
* @return array
*/
public function get_objects( $pages ) {
if ( empty( $pages ) ) {
return [];
}
$pages = DB::objects()
->whereIn( 'page', \array_unique( $pages ) )
->where( 'is_indexable', 1 )
->get( ARRAY_A );
return $this->set_page_as_key( $pages );
}
/**
* Get objects by seo score range filter.
*
* @param WP_REST_Request $request Filters.
*
* @return array
*/
public function get_objects_by_score( $request ) {
global $wpdb;
$orderby = in_array( $request->get_param( 'orderby' ), [ 'title', 'seo_score', 'created' ], true ) ? $request->get_param( 'orderby' ) : 'created';
$order = in_array( $request->get_param( 'order' ), [ 'asc', 'desc' ], true ) ? strtoupper( $request->get_param( 'order' ) ) : 'DESC';
$post_type = sanitize_key( $request->get_param( 'postType' ) );
// Construct filters from request parameters.
$filters = [
'good' => $request->get_param( 'good' ),
'ok' => $request->get_param( 'ok' ),
'bad' => $request->get_param( 'bad' ),
'noData' => $request->get_param( 'noData' ),
];
$field_name = 'seo_score';
$per_page = $request->get_param( 'per_page' ) ? sanitize_text_field( $request->get_param( 'per_page' ) ) : 25;
$offset = ( sanitize_text_field( $request->get_param( 'page' ) ) - 1 ) * $per_page;
// Construct SQL condition based on filter parameters.
$conditions = [];
if ( $filters['good'] ) {
$conditions[] = "{$field_name} BETWEEN 81 AND 100";
}
if ( $filters['ok'] ) {
$conditions[] = "{$field_name} BETWEEN 51 AND 80";
}
if ( $filters['bad'] ) {
$conditions[] = "{$field_name} BETWEEN 1 AND 50";
}
if ( $filters['noData'] ) {
$conditions[] = "{$field_name} = 0";
}
$subwhere = '';
if ( count( $conditions ) > 0 ) {
$subwhere = implode( ' OR ', $conditions );
$subwhere = " AND ({$subwhere})";
}
if ( $post_type ) {
$subwhere = $subwhere . ' AND object_subtype = "' . $post_type . '"';
}
// Get filtered objects data limited by page param.
// phpcs:disable
$pages = $wpdb->get_results(
"SELECT * FROM {$wpdb->prefix}rank_math_analytics_objects
WHERE is_indexable = 1
{$subwhere}
ORDER BY {$orderby} {$order}
LIMIT {$offset} , {$per_page}",
ARRAY_A
);
// Get total filtered objects count.
$total_rows = $wpdb->get_var(
"SELECT count(*) FROM {$wpdb->prefix}rank_math_analytics_objects
WHERE is_indexable = 1
{$subwhere}
ORDER BY created DESC"
);
// phpcs:enable
return [
'rows' => $this->set_page_as_key( $pages ),
'rowsFound' => $total_rows,
];
}
}
@@ -0,0 +1,122 @@
<?php
/**
* The Analytics Module
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use stdClass;
use WP_Error;
use WP_REST_Request;
use RankMath\Helper;
use RankMath\Analytics\DB;
defined( 'ABSPATH' ) || exit;
/**
* Posts class.
*/
class Posts extends Objects {
/**
* Get post data.
*
* @param WP_REST_Request $request post object.
*
* @return object
*/
public function get_post( $request ) {
$id = $request->get_param( 'id' );
$post = DB::objects()
->where( 'object_id', $id )
->one();
if ( is_null( $post ) ) {
return [ 'errorMessage' => esc_html__( 'Sorry, no post found for given id.', 'rank-math' ) ];
}
$post->admin_url = admin_url();
$post->home_url = home_url();
return apply_filters( 'rank_math/analytics/post_data', (array) $post, $request );
}
/**
* Get posts by objects.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_posts_rows_by_objects( WP_REST_Request $request ) {
$pre = apply_filters( 'rank_math/analytics/get_posts_rows_by_objects', false, $request );
if ( false !== $pre ) {
return $pre;
}
$cache_group = 'rank_math_posts_rows_by_objects';
$cache_key = $this->generate_hash( $request );
$data = $this->get_cache( $cache_key, $cache_group );
if ( false !== $data ) {
return rest_ensure_response( $data );
}
// Pagination.
$per_page = 25;
$offset = ( $request->get_param( 'page' ) - 1 ) * $per_page;
// Get objects filtered by seo score range and it's analytics data.
$objects = $this->get_objects_by_score( $request );
$pages = \array_keys( $objects['rows'] );
$console = $this->get_analytics_data(
[
'offset' => 0, // Here offset should always zero.
'perpage' => $objects['rowsFound'],
'sub_where' => " AND page IN ('" . join( "', '", $pages ) . "')",
]
);
// Construct return data.
$new_rows = [];
foreach ( $objects['rows'] as $object ) {
$page = $object['page'];
if ( isset( $console[ $page ] ) ) {
$object = \array_merge( $console[ $page ], $object );
}
if ( ! isset( $object['links'] ) ) {
$object['links'] = new stdClass();
}
$new_rows[ $page ] = $object;
}
$count = count( $new_rows );
if ( $offset + 25 <= $count ) {
$new_rows = array_slice( $new_rows, $offset, 25 );
} else {
$rest = $count - $offset;
$new_rows = array_slice( $new_rows, $offset, $rest );
}
if ( empty( $new_rows ) ) {
$new_rows['response'] = 'No Data';
}
$output = [
'rows' => $new_rows,
'rowsFound' => $objects['rowsFound'],
];
$this->set_cache( $cache_key, $output, $cache_group, DAY_IN_SECONDS );
return rest_ensure_response( $output );
}
}
@@ -0,0 +1,993 @@
<?php
/**
* The Analytics Module
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Helpers\Param;
use RankMathPro\Analytics\Pageviews;
use RankMath\Google\Console as Google_Analytics;
defined( 'ABSPATH' ) || exit;
/**
* Stats class.
*/
class Stats extends Keywords {
use Hooker;
/**
* Start timestamp.
*
* @var int
*/
public $start = 0;
/**
* End timestamp.
*
* @var int
*/
public $end = 0;
/**
* Start date.
*
* @var string
*/
public $start_date = '';
/**
* End date.
*
* @var string
*/
public $end_date = '';
/**
* Compare Start date.
*
* @var string
*/
public $compare_start_date = '';
/**
* Compare End date.
*
* @var string
*/
public $compare_end_date = '';
/**
* Number of days.
*
* @var int
*/
public $days = 0;
/**
* Main instance
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Stats
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Stats ) ) {
$instance = new Stats();
$instance->set_date_range();
}
return $instance;
}
/**
* Set date range.
*
* @param string $range Range of days.
*/
public function set_date_range( $range = false ) {
// Shift 3 days prior.
$subtract = DAY_IN_SECONDS * 3;
$end = strtotime( $this->do_filter( 'analytics/end_date', 'today' ) ) - $subtract;
$start = strtotime( false !== $range ? $range : $this->get_date_from_cookie( 'date_range', '-30 days' ), $end ) - $subtract;
// Timestamp.
$this->end = Helper::get_midnight( $end );
$this->start = Helper::get_midnight( $start );
// Period.
$this->end_date = Helper::get_date( 'Y-m-d 23:59:59', $end, false, true );
$this->start_date = Helper::get_date( 'Y-m-d 00:00:00', $start, false, true );
// Compare date.
$this->days = ceil( abs( $end - $start ) / DAY_IN_SECONDS );
$this->compare_end_date = $start - DAY_IN_SECONDS;
$this->compare_start_date = $this->compare_end_date - ( $this->days * DAY_IN_SECONDS );
$this->compare_end_date = Helper::get_date( 'Y-m-d 23:59:59', $this->compare_end_date, false, true );
$this->compare_start_date = Helper::get_date( 'Y-m-d 00:00:00', $this->compare_start_date, false, true );
}
/**
* Get date intervals for graph.
*
* @return array
*/
public function get_intervals() {
$range = $this->get_date_from_cookie( 'date_range', '-30 days' );
$interval = [
'-7 days' => '0 days',
'-15 days' => '-3 days',
'-30 days' => '-6 days',
'-3 months' => '-6 days',
'-6 months' => '-30 days',
'-1 year' => '-30 days',
];
$ticks = [
'-7 days' => 7,
'-15 days' => 5,
'-30 days' => 5,
'-3 months' => 13,
'-6 months' => 6,
'-1 year' => 12,
];
$addition = [
'-7 days' => 0,
'-15 days' => DAY_IN_SECONDS,
'-30 days' => DAY_IN_SECONDS,
'-3 months' => -DAY_IN_SECONDS / 6,
'-6 months' => DAY_IN_SECONDS / 2,
'-1 year' => 0,
];
$ticks = $ticks[ $range ];
$interval = $interval[ $range ];
$addition = $addition[ $range ];
$map = [];
$dates = [];
$end = $this->end;
$start = strtotime( $interval, $end );
for ( $i = 0; $i < $ticks; $i++ ) {
$end_date = Helper::get_date( 'Y-m-d', $end, false, true );
$start_date = Helper::get_date( 'Y-m-d', $start, false, true );
$dates[ $end_date ] = [
'start' => $start_date,
'end' => $end_date,
'formatted_date' => Helper::get_date( 'd M, Y', $end ),
'formatted_period' => Helper::get_date( 'd M', $start ) . ' - ' . Helper::get_date( 'd M, Y', $end ),
];
$map[ $start_date ] = $end_date;
for ( $j = 1; $j < 32; $j++ ) {
$date = Helper::get_date( 'Y-m-d', strtotime( $j . ' days', $start ), false, true );
if ( $start_date === $end_date ) {
break;
}
if ( $date === $end_date ) {
break;
}
$map[ $date ] = $end_date;
}
$map[ $end_date ] = $end_date;
$end = \strtotime( '-1 days', $start );
$start = \strtotime( $interval, $end + $addition );
}
return [
'map' => $map,
'dates' => \array_reverse( $dates ),
];
}
/**
* Get date intervals for SQL query.
*
* @param array $intervals Date Intervals.
* @param string $column Column name to check.
* @param string $newcolumn Column name to return.
* @return string
*/
public function get_sql_date_intervals( $intervals, $column = 'created', $newcolumn = 'range_group' ) {
$sql_parts = [];
array_push( $sql_parts, 'CASE' );
$index = 1;
foreach ( $intervals['dates'] as $date_range ) {
$start_date = $date_range['start'] . ' 00:00:00';
$end_date = $date_range['end'] . ' 23:59:59';
array_push( $sql_parts, sprintf( "WHEN %s BETWEEN '%s' AND '%s' THEN 'range%d'", $column, $start_date, $end_date, $index ) );
$index ++;
}
array_push( $sql_parts, "ELSE 'none'" );
array_push( $sql_parts, sprintf( "END AS '%s'", $newcolumn ) );
return implode( ' ', $sql_parts );
}
/**
* Get date array
*
* @param array $dates Dates.
* @param array $default Default value.
* @return array
*/
public function get_date_array( $dates, $default ) {
$data = [];
foreach ( $dates as $date => $d ) {
$data[ $date ] = $default;
$data[ $date ]['date'] = $date;
$data[ $date ]['dateFormatted'] = $d['start'] === $d['end'] ? $d['formatted_date'] : $d['formatted_period'];
$data[ $date ]['formattedDate'] = $d['formatted_date'];
}
return $data;
}
/**
* Convert data to proper type.
*
* @param array $row Row to normalize.
* @return array
*/
public function normalize_graph_rows( $row ) {
foreach ( $row as $col => $val ) {
if ( in_array( $col, [ 'query', 'page', 'date', 'created', 'dateFormatted' ], true ) ) {
continue;
}
if ( in_array( $col, [ 'ctr', 'position', 'earnings' ], true ) ) {
$row->$col = round( $row->$col, 0 );
continue;
}
$row->$col = absint( $row->$col );
}
return $row;
}
/**
* Remove uncessary graph rows.
*
* @param array $rows Rows to filter.
* @return array
*/
public function filter_graph_rows( $rows ) {
foreach ( $rows as $key => $row ) {
if ( isset( $row->range_group ) && 'none' === $row->range_group ) {
unset( $rows[ $key ] );
}
}
return $rows;
}
/**
* Extract proper data.
*
* @param array $rows Data rows.
* @param string $column Column name contains mixed data.
* @param string $sep Separator for mixed data.
* @param array $keys Column array to extract.
* @return array
*/
public function extract_data_from_mixed( $rows, $column, $sep, $keys ) {
foreach ( $rows as $index => &$row ) {
if ( ! isset( $row->$column ) ) {
continue;
}
$mixed = explode( $sep, $row->$column );
$mixed_count = count( $mixed );
if ( ! $mixed_count ) {
continue;
}
foreach ( $keys as $key_idx => $key ) {
if ( 'position' === $key ) {
// Should subtract the position value from 100. The position data was inverted before call this function.
$value = 100 - (int) $mixed[ $mixed_count - $key_idx - 1 ];
} else {
$value = $mixed[ $mixed_count - $key_idx - 1 ];
}
$row->$key = $value;
}
unset( $row->$column );
}
return $rows;
}
/**
* Merge two metrics array into one
*
* @param array $metrics_rows1 Metrics Rows to merge.
* @param array $metrics_rows2 Metrics Rows to merge.
* @param boolean $has_traffic Flag to include/exclude traffic data.
* @return array
*/
public function get_merged_metrics( $metrics_rows1, $metrics_rows2, $has_traffic = false ) {
$data = [];
// Construct base data array.
$base_array = [
'position' => 0,
'diffPosition' => 0,
'clicks' => 0,
'diffClicks' => 0,
'impressions' => 0,
'diffImpressions' => 0,
'ctr' => 0,
'diffCtr' => 0,
];
if ( $has_traffic ) {
$base_array['pageviews'] = 0;
$base_array['difference'] = 0;
}
// Merge first array and second array into base array.
foreach ( $metrics_rows1 as $key => $row ) {
if ( isset( $metrics_rows2[ $key ] ) ) {
if ( is_object( $row ) ) {
$data[ $key ] = (object) array_merge( $base_array, (array) $row, (array) $metrics_rows2[ $key ] );
} else {
$data[ $key ] = array_merge( $base_array, $row, $metrics_rows2[ $key ] );
}
unset( $metrics_rows2[ $key ] );
} else {
$data[ $key ] = array_merge( $base_array, $row );
}
}
// Merge remaining items from second array into base array.
foreach ( $metrics_rows2 as $key => $row ) {
if ( is_object( $row ) ) {
$metrics_rows2[ $key ] = (object) array_merge( $base_array, (array) $row );
} else {
$metrics_rows2[ $key ] = array_merge( $base_array, $row );
}
}
return array_merge( $data, $metrics_rows2 );
}
/**
* Merge data graph by date.
*
* @param array $rows Rows to merge.
* @param array $data Data array.
* @param array $map Interval map.
* @return array
*/
public function get_merge_data_graph( $rows, $data, $map ) {
foreach ( $rows as $row ) {
if ( ! isset( $map[ $row->date ] ) ) {
continue;
}
$date = $map[ $row->date ];
foreach ( $row as $key => $value ) {
if ( 'date' === $key || 'created' === $key ) {
continue;
}
// trick to invert Position Graph YAxis.
if ( 'position' === $key ) {
$value = 0 - $value;
}
$data[ $date ][ $key ][] = $value;
}
}
return $data;
}
/**
* Flat graph data.
*
* @param array $rows Graph data.
* @return array
*/
public function get_graph_data_flat( $rows ) {
foreach ( $rows as &$row ) {
if ( isset( $row['clicks'] ) ) {
$row['clicks'] = \array_sum( $row['clicks'] );
}
if ( isset( $row['impressions'] ) ) {
$row['impressions'] = \array_sum( $row['impressions'] );
}
if ( isset( $row['earnings'] ) ) {
$row['earnings'] = \array_sum( $row['earnings'] );
}
if ( isset( $row['pageviews'] ) ) {
$row['pageviews'] = \array_sum( $row['pageviews'] );
}
if ( isset( $row['ctr'] ) ) {
$row['ctr'] = empty( $row['ctr'] ) ? 0 : ceil( array_sum( $row['ctr'] ) / count( $row['ctr'] ) );
}
if ( isset( $row['position'] ) ) {
if ( empty( $row['position'] ) ) {
unset( $row['position'] );
} else {
$row['position'] = ceil( array_sum( $row['position'] ) / count( $row['position'] ) );
}
}
if ( isset( $row['keywords'] ) ) {
$row['keywords'] = empty( $row['keywords'] ) ? 0 : ceil( array_sum( $row['keywords'] ) / count( $row['keywords'] ) );
}
}
return $rows;
}
/**
* Get filter data.
*
* @param string $filter Filter key.
* @param string $default Filter default value.
*
* @return mixed
*/
public function get_date_from_cookie( $filter, $default ) {
$cookie_key = 'rank_math_analytics_' . $filter;
$new_value = sanitize_title( Param::post( $filter ) );
if ( $new_value ) {
setcookie( $cookie_key, $new_value, time() + ( HOUR_IN_SECONDS * 30 ), COOKIEPATH, COOKIE_DOMAIN, false, true );
return $new_value;
}
if ( ! empty( $_COOKIE[ $cookie_key ] ) ) {
return $_COOKIE[ $cookie_key ];
}
return $default;
}
/**
* Get analytics data.
*
* @param array $args Array of arguments.
* @return array
*/
public function get_analytics_data( $args = [] ) {
global $wpdb;
$args = wp_parse_args(
$args,
[
'dimension' => 'page',
'order' => 'DESC',
'orderBy' => 'diffPosition',
'objects' => false,
'pageview' => false,
'where' => '',
'sub_where' => '',
'pages' => [],
'type' => '',
'offset' => 0,
'perpage' => 5,
]
);
$dimension = $args['dimension'];
$type = $args['type'];
$offset = $args['offset'];
$perpage = $args['perpage'];
$order_by_field = $args['orderBy'];
$sub_where = $args['sub_where'];
$order_position_fields = [ 'position', 'diffPosition' ];
$order_metrics_fields = [ 'clicks', 'diffClicks', 'impressions', 'diffImpressions', 'ctr', 'diffCtr' ];
if ( in_array( $order_by_field, $order_position_fields, true ) ) {
// In case order by position related fields, get position data first.
$positions = $this->get_position_data_by_dimension( $args );
// Filter position data by condition.
$positions = $this->filter_analytics_data( $positions, $args );
// Get dimension list from above result.
$dimensions = wp_list_pluck( $positions, $dimension );
$dimensions = array_map( 'esc_sql', $dimensions );
// Get metrics data based on above dimension list.
$metrics = $this->get_metrics_data_by_dimension(
[
'dimension' => $dimension,
'sub_where' => ' AND ' . $dimension . " IN ('" . join( "', '", $dimensions ) . "')",
]
);
// Merge above two data into one.
$rows = $this->get_merged_metrics( $positions, $metrics, true );
} elseif ( in_array( $order_by_field, $order_metrics_fields, true ) ) {
// In case order by fields which are not related with position, get metrics data first.
$metrics = $this->get_metrics_data_by_dimension( $args );
// Filter metrics data by condition.
$metrics = $this->filter_analytics_data( $metrics, $args );
// Get dimension list from above result.
$dimensions = wp_list_pluck( $metrics, $dimension );
$dimensions = array_map( 'esc_sql', $dimensions );
// Get position data based on above dimension list.
$positions = $this->get_position_data_by_dimension(
[
'dimension' => $dimension,
'sub_where' => ' AND ' . $dimension . " IN ('" . join( "', '", $dimensions ) . "') " . $sub_where,
]
);
// Merge above two data into one.
$rows = $this->get_merged_metrics( $metrics, $positions, true );
} else {
// Get position data and other metrics data separately.
$positions = $this->get_position_data_by_dimension( $args );
$metrics = $this->get_metrics_data_by_dimension( $args );
// Merge above two data into one.
$rows = $this->get_merged_metrics( $positions, $metrics, true );
// Filter array by condition.
$rows = $this->filter_analytics_data( $rows, $args );
}
$page_urls = \array_merge( \array_keys( $rows ), $args['pages'] );
$pageviews = [];
if ( \class_exists( 'RankMathPro\Analytics\Pageviews' ) && $args['pageview'] && ! empty( $page_urls ) ) {
$pageviews = Pageviews::get_pageviews( [ 'pages' => $page_urls ] );
$pageviews = $pageviews['rows'];
}
if ( $args['objects'] ) {
$objects = $this->get_objects( $page_urls );
}
foreach ( $rows as $page => $row ) {
$rows[ $page ]['pageviews'] = [
'total' => 0,
'difference' => 0,
];
$rows[ $page ]['clicks'] = [
'total' => (int) $rows[ $page ]['clicks'],
'difference' => (int) $rows[ $page ]['diffClicks'],
];
$rows[ $page ]['impressions'] = [
'total' => (int) $rows[ $page ]['impressions'],
'difference' => (int) $rows[ $page ]['diffImpressions'],
];
$rows[ $page ]['position'] = [
'total' => (float) $rows[ $page ]['position'],
'difference' => (float) $rows[ $page ]['diffPosition'],
];
$rows[ $page ]['ctr'] = [
'total' => (float) $rows[ $page ]['ctr'],
'difference' => (float) $rows[ $page ]['diffCtr'],
];
unset(
$rows[ $page ]['diffClicks'],
$rows[ $page ]['diffImpressions'],
$rows[ $page ]['diffPosition'],
$rows[ $page ]['diffCtr'],
$rows[ $page ]['difference']
);
}
if ( $args['pageview'] && ! empty( $pageviews ) ) {
foreach ( $pageviews as $pageview ) {
$page = $pageview['page'];
if ( ! isset( $rows[ $page ] ) ) {
$rows[ $page ] = [];
}
$rows[ $page ]['pageviews'] = [
'total' => (int) $pageview['pageviews'],
'difference' => (int) $pageview['difference'],
];
}
}
if ( $args['objects'] && ! empty( $objects ) ) {
foreach ( $objects as $object ) {
$page = $object['page'];
if ( ! isset( $rows[ $page ] ) ) {
$rows[ $page ] = [];
}
$rows[ $page ] = array_merge( $rows[ $page ], $object );
}
}
return $rows;
}
/**
* Get position data.
*
* @param array $args Argument array.
* @return array
*/
public function get_position_data_by_dimension( $args = [] ) {
global $wpdb;
$args = wp_parse_args(
$args,
[
'dimension' => 'page',
'where' => '',
'sub_where' => '',
]
);
$dimension = $args['dimension'];
$where = $args['where'];
$sub_where = $args['sub_where'];
if ( 'page' === $dimension ) {
// In case dimension is set as 'page', position data for each page will be top position of last ranked date.
// That is, among all the position value from the last date of the page, the top position(smallest position value) value will be the result.
// Get current position data.
// phpcs:disable
$query = $wpdb->prepare(
"SELECT {$dimension}, MAX(CONCAT({$dimension}, ':', DATE(created), ':', LPAD((100 - position), 3, '0'))) as uid
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s {$sub_where}
GROUP BY {$dimension}",
$this->start_date,
$this->end_date
);
$positions = $wpdb->get_results( $query );
// Get old position data.
$query = $wpdb->prepare(
"SELECT {$dimension}, MAX(CONCAT({$dimension}, ':', DATE(created), ':', LPAD((100 - position), 3, '0'))) as uid
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s
GROUP BY {$dimension}",
$this->compare_start_date,
$this->compare_end_date
);
$old_positions = $wpdb->get_results( $query );
// phpcs:enable
// Extract proper position data.
$positions = $this->extract_data_from_mixed( $positions, 'uid', ':', [ 'position', 'date' ] );
$old_positions = $this->extract_data_from_mixed( $old_positions, 'uid', ':', [ 'position', 'date' ] );
// Set 'page' as key.
$positions = $this->set_dimension_as_key( $positions, $dimension );
$old_positions = $this->set_dimension_as_key( $old_positions, $dimension );
// Calculate position difference, merge old into current position data array.
foreach ( $positions as $page => &$row ) {
$row = (array) $row; // force to convert as array.
if ( ! isset( $old_positions[ $page ] ) ) {
$old_position_value = 100; // Should set as 100 here to get correct position difference.
} else {
$old_position_value = $old_positions[ $page ]->position;
}
$row['diffPosition'] = $row['position'] - $old_position_value;
}
} else {
// In case dimension is not 'page', position data for each dimension will be most recent position value.
// Step1. Get most recent row id for each dimension for current data.
// phpcs:disable
$query = $wpdb->prepare(
"SELECT t1.id as id
FROM {$wpdb->prefix}rank_math_analytics_gsc t1
INNER JOIN (
SELECT query, MAX(created) as latest_created
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s {$sub_where} GROUP BY {$dimension}
) t2 ON t1.query = t2.query AND t1.created = t2.latest_created",
$this->start_date,
$this->end_date
);
$ids = $wpdb->get_results( $query );
// phpcs:enable
// Step2. Get id list from above result.
$ids = wp_list_pluck( $ids, 'id' );
$ids_where = " AND id IN ('" . join( "', '", $ids ) . "')";
// Step3. Get most recent row id for each dimension for compare data.
// phpcs:disable
$query = $wpdb->prepare(
"SELECT t1.id as id
FROM {$wpdb->prefix}rank_math_analytics_gsc t1
INNER JOIN (
SELECT query, MAX(created) as latest_created
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s {$sub_where} GROUP BY {$dimension}
) t2 ON t1.query = t2.query AND t1.created = t2.latest_created",
$this->compare_start_date,
$this->compare_end_date
);
$old_ids = $wpdb->get_results( $query );
// phpcs:enable
// Step4. Get id list from above result.
$old_ids = wp_list_pluck( $old_ids, 'id' );
$old_ids_where = " AND id IN ('" . join( "', '", $old_ids ) . "')";
// Step5. Get position and difference data based on above id list.
// phpcs:disable
$positions = $wpdb->get_results(
"SELECT
t1.{$dimension} as {$dimension}, ROUND( t1.position, 0 ) as position,
COALESCE( ROUND( t1.position - COALESCE( t2.position, 100 ), 0 ), 0 ) as diffPosition
FROM
( SELECT a.{$dimension}, a.position FROM {$wpdb->prefix}rank_math_analytics_gsc AS a WHERE 1 = 1{$ids_where}) AS t1
LEFT JOIN
( SELECT a.{$dimension}, a.position FROM {$wpdb->prefix}rank_math_analytics_gsc AS a WHERE 1 = 1{$old_ids_where}) AS t2
ON t1.{$dimension} = t2.{$dimension}
{$where}",
ARRAY_A
);
// phpcs:enable
$positions = $this->set_dimension_as_key( $positions, $dimension );
}
return $positions;
}
/**
* Get metrics data.
*
* @param array $args Argument array.
* @return array
*/
public function get_metrics_data_by_dimension( $args = [] ) {
global $wpdb;
Helper::enable_big_selects_for_queries();
$args = wp_parse_args(
$args,
[
'dimension' => 'page',
'sub_where' => '',
]
);
$dimension = $args['dimension'];
$sub_where = $args['sub_where'];
// Get metrics data like impressions, click, ctr, etc.
// phpcs:disable
$query = $wpdb->prepare(
"SELECT
t1.{$dimension} as {$dimension}, t1.clicks, t1.impressions, t1.ctr,
COALESCE( t1.clicks - t2.clicks, 0 ) as diffClicks,
COALESCE( t1.impressions - t2.impressions, 0 ) as diffImpressions,
COALESCE( t1.ctr - t2.ctr, 0 ) as diffCtr
FROM
( SELECT {$dimension}, SUM( clicks ) as clicks, SUM(impressions) as impressions, AVG(ctr) as ctr
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE 1 = 1 AND created BETWEEN %s AND %s {$sub_where}
GROUP BY {$dimension}) as t1
LEFT JOIN
( SELECT {$dimension}, SUM( clicks ) as clicks, SUM(impressions) as impressions, AVG(ctr) as ctr
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE 1 = 1 AND created BETWEEN %s AND %s {$sub_where}
GROUP BY {$dimension}) as t2
ON t1.{$dimension} = t2.{$dimension}",
$this->start_date,
$this->end_date,
$this->compare_start_date,
$this->compare_end_date
);
$metrics = $wpdb->get_results( $query, ARRAY_A );
// phpcs:enable
$metrics = $this->set_dimension_as_key( $metrics, $dimension );
return $metrics;
}
/**
* Filter analytics data.
*
* @param array $data Data to process.
* @param array $args Argument array.
* @return array
*/
public function filter_analytics_data( $data, $args ) {
$dimension = $args['dimension'];
$offset = $args['offset'];
$perpage = $args['perpage'];
$order_by_field = $args['orderBy'];
/**
* Short-circuit to filter the data.
*/
$pre = $this->do_filter( 'analytics/pre_filter_data', null, $data, $args );
if ( is_array( $pre ) ) {
return $pre;
}
// Sort array by $args['order'], $order_by_field value.
if ( ! empty( $args['order'] ) ) {
$sort_base_arr = array_column( $data, $order_by_field, $dimension );
array_multisort( $sort_base_arr, 'ASC' === $args['order'] ? SORT_ASC : SORT_DESC, $data );
}
// Filter array by $offset, $perpage value.
$data = array_slice( $data, $offset, $perpage, true );
return $data;
}
/**
* Set page as key.
*
* @param array $data Rows to process.
* @return array
*/
public function set_page_as_key( $data ) {
$rows = [];
foreach ( $data as $row ) {
$page = $this->get_relative_url( $row['page'] );
if ( ! empty( $row['object_id'] ) && empty( $row['schemas_in_use'] ) ) {
$row['schemas_in_use'] = Helper::get_default_schema_type( $row['object_id'], true, true );
}
$rows[ $page ] = $row;
}
return $rows;
}
/**
* Set dimension parameter as key.
*
* @param array $data Rows to process.
* @param string $dimension Dimension to set as key.
* @return array
*/
public function set_dimension_as_key( $data, $dimension = 'query' ) {
$rows = [];
foreach ( $data as $row ) {
if ( is_object( $row ) ) {
$value = $row->$dimension;
} else {
$value = $row[ $dimension ];
}
$key = 'page' === $dimension ? $this->get_relative_url( $value ) : strtolower( $value );
$rows[ $key ] = $row;
}
return $rows;
}
/**
* Set query position history.
*
* @param array $data Rows to process.
* @param array $history Rows to process.
*
* @return array
*/
public function set_query_position( $data, $history ) {
foreach ( $history as $row ) {
$key = strtolower( $row->query );
$data[ $key ]['query'] = isset( $data[ $key ]['query'] ) ? $data[ $key ]['query'] : $key;
$data[ $key ]['graph'] = isset( $data[ $key ]['graph'] ) ? $data[ $key ]['graph'] : [];
if ( ! isset( $row->formatted_date ) ) {
$formatted_date = Helper::get_date( 'd M, Y', strtotime( $row->date ) );
$row->formatted_date = $formatted_date;
}
$data[ $row->query ]['graph'][] = $row;
}
return $data;
}
/**
* Set page position history.
*
* @param array $data Rows to process.
* @param array $history Rows to process.
*
* @return array
*/
public function set_page_position_graph( $data, $history ) {
foreach ( $history as $row ) {
$data[ $row->page ]['graph'] = isset( $data[ $row->page ]['graph'] ) ? $data[ $row->page ]['graph'] : [];
if ( ! isset( $row->formatted_date ) ) {
$formatted_date = Helper::get_date( 'd M, Y', strtotime( $row->date ) );
$row->formatted_date = $formatted_date;
}
$data[ $row->page ]['graph'][] = $row;
}
return $data;
}
/**
* Generate Cache Keys.
*
* @param string $what What for you need the key.
* @param mixed $args more salt to add into key.
*
* @return string
*/
public function get_cache_key( $what, $args = [] ) {
$key = 'rank_math_' . $what;
if ( ! empty( $args ) ) {
$key .= '_' . join( '_', (array) $args );
}
return $key;
}
/**
* Get relative url.
*
* @param string $url Url to make relative.
* @return string
*/
public static function get_relative_url( $url ) {
$home_url = Google_Analytics::get_site_url();
// On multisite and sub-directory setup replace the home url.
if ( is_multisite() && ! is_subdomain_install() ) {
$url = \str_replace( $home_url, '/', $url );
} else {
$domain = strtolower( wp_parse_url( $home_url, PHP_URL_HOST ) );
$domain = str_replace( [ 'www.', '.' ], [ '', '\.' ], $domain );
$regex = "/http[s]?:\/\/(www\.)?$domain/mU";
$url = strtolower( trim( $url ) );
$url = preg_replace( $regex, '', $url );
}
/**
* Google API and get_permalink sends URL Encoded strings so we need
* to urldecode in order to get them to match with whats saved in DB.
*/
$url = urldecode( $url );
return \str_replace( $home_url, '', $url );
}
}
@@ -0,0 +1,432 @@
<?php
/**
* The Analytics Module
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use RankMath\Traits\Cache;
defined( 'ABSPATH' ) || exit;
/**
* Summary class.
*/
class Summary {
use Cache;
/**
* Start date.
*
* @var string
*/
public $start_date;
/**
* End date.
*
* @var string
*/
public $end_date;
/**
* Compare start date.
*
* @var string
*/
public $compare_start_date;
/**
* Compare end date.
*
* @var string
*/
public $compare_end_date;
/**
* Days.
*
* @var int
*/
public $days;
/**
* Get Widget.
*
* @return object
*/
public function get_widget() {
global $wpdb;
$cache_key = Stats::get()->get_cache_key( 'dashboard_stats_widget' );
$cache = get_transient( $cache_key );
if ( false !== $cache ) {
return $cache;
}
$stats = DB::analytics()
->selectSum( 'impressions', 'impressions' )
->selectSum( 'clicks', 'clicks' )
->selectAvg( 'position', 'position' )
->whereBetween( 'created', [ Stats::get()->start_date, Stats::get()->end_date ] )
->one();
$old_stats = DB::analytics()
->selectSum( 'impressions', 'impressions' )
->selectSum( 'clicks', 'clicks' )
->selectAvg( 'position', 'position' )
->whereBetween( 'created', [ Stats::get()->compare_start_date, Stats::get()->compare_end_date ] )
->one();
if ( is_null( $stats ) ) {
$stats = (object) [
'clicks' => 0,
'impressions' => 0,
'position' => 0,
];
}
if ( is_null( $old_stats ) ) {
$old_stats = $stats;
}
$stats->clicks = [
'total' => (int) $stats->clicks,
'previous' => (int) $old_stats->clicks,
'difference' => $stats->clicks - $old_stats->clicks,
];
$stats->impressions = [
'total' => (int) $stats->impressions,
'previous' => (int) $old_stats->impressions,
'difference' => $stats->impressions - $old_stats->impressions,
];
$stats->position = [
'total' => (float) \number_format( $stats->position, 2 ),
'previous' => (float) \number_format( $old_stats->position, 2 ),
'difference' => (float) \number_format( $stats->position - $old_stats->position, 2 ),
];
$stats->keywords = $this->get_keywords_summary();
$stats = apply_filters( 'rank_math/analytics/get_widget', $stats );
set_transient( $cache_key, $stats, DAY_IN_SECONDS * Stats::get()->days );
return $stats;
}
/**
* Get Optimization stats.
*
* @param string $post_type Selected Post Type.
*
* @return object
*/
public function get_optimization_summary( $post_type = '' ) {
global $wpdb;
$cache_group = 'rank_math_optimization_summary';
$cache_key = $this->generate_hash( $post_type );
$cache = $this->get_cache( $cache_key, $cache_group );
if ( false !== $cache ) {
return $cache;
}
$stats = (object) [
'good' => 0,
'ok' => 0,
'bad' => 0,
'noData' => 0,
'total' => 0,
'average' => 0,
];
$object_type_sql = $post_type ? ' AND object_subtype = "' . $post_type . '"' : '';
$data = $wpdb->get_results(
"SELECT COUNT(object_id) AS count,
CASE
WHEN seo_score BETWEEN 81 AND 100 THEN 'good'
WHEN seo_score BETWEEN 51 AND 80 THEN 'ok'
WHEN seo_score BETWEEN 1 AND 50 THEN 'bad'
WHEN seo_score = 0 THEN 'noData'
ELSE 'none'
END AS type
FROM {$wpdb->prefix}rank_math_analytics_objects
WHERE is_indexable = 1
{$object_type_sql}
GROUP BY type"
);
$total = 0;
foreach ( $data as $row ) {
$total += (int) $row->count;
$stats->{$row->type} = (int) $row->count;
}
$stats->total = $total;
$stats->average = 0;
// Average.
$query = DB::objects()
->selectCount( 'object_id', 'total' )
->where( 'is_indexable', 1 )
->selectSum( 'seo_score', 'score' );
if ( $object_type_sql ) {
$query->where( 'object_subtype', $post_type );
}
$average = $query->one();
$average->total += property_exists( $stats, 'noData' ) ? $stats->noData : 0; // phpcs:ignore
if ( $average->total > 0 ) {
$stats->average = \round( $average->score / $average->total, 2 );
}
$this->set_cache( $cache_key, $stats, $cache_group, DAY_IN_SECONDS );
return $stats;
}
/**
* Get analytics summary.
*
* @return object
*/
public function get_analytics_summary() {
$args = [
'start_date' => $this->start_date,
'end_date' => $this->end_date,
'compare_start_date' => $this->compare_start_date,
'compare_end_date' => $this->compare_end_date,
];
$cache_group = 'rank_math_analytics_summary';
$cache_key = $this->generate_hash( $args );
$cache = $this->get_cache( $cache_key, $cache_group );
if ( false !== $cache ) {
return $cache;
}
$stats = DB::analytics()
->selectCount( 'DISTINCT(page)', 'posts' )
->selectSum( 'impressions', 'impressions' )
->selectSum( 'clicks', 'clicks' )
->selectAvg( 'position', 'position' )
->whereBetween( 'created', [ $this->start_date, $this->end_date ] )
->one();
// Check validation.
$stats->clicks = empty( $stats->clicks ) ? 0 : $stats->clicks;
$stats->impressions = empty( $stats->impressions ) ? 0 : $stats->impressions;
$stats->position = empty( $stats->position ) ? 0 : $stats->position;
$old_stats = DB::analytics()
->selectCount( 'DISTINCT(page)', 'posts' )
->selectSum( 'impressions', 'impressions' )
->selectSum( 'clicks', 'clicks' )
->selectAvg( 'position', 'position' )
->whereBetween( 'created', [ $this->compare_start_date, $this->compare_end_date ] )
->one();
// Check validation.
$old_stats->clicks = empty( $old_stats->clicks ) ? 0 : $old_stats->clicks;
$old_stats->impressions = empty( $old_stats->impressions ) ? 0 : $old_stats->impressions;
$old_stats->position = empty( $old_stats->position ) ? 0 : $old_stats->position;
$total_ctr = 0 !== $stats->impressions ? round( ( $stats->clicks / $stats->impressions ) * 100, 2 ) : 0;
$previous_ctr = 0 !== $old_stats->impressions ? round( ( $old_stats->clicks / $old_stats->impressions ) * 100, 2 ) : 0;
$stats->ctr = [
'total' => $total_ctr,
'previous' => $previous_ctr,
'difference' => $total_ctr - $previous_ctr,
];
$stats->clicks = [
'total' => (int) $stats->clicks,
'previous' => (int) $old_stats->clicks,
'difference' => $stats->clicks - $old_stats->clicks,
];
$stats->impressions = [
'total' => (int) $stats->impressions,
'previous' => (int) $old_stats->impressions,
'difference' => $stats->impressions - $old_stats->impressions,
];
$stats->position = [
'total' => (float) \number_format( $stats->position, 2 ),
'previous' => (float) \number_format( $old_stats->position, 2 ),
'difference' => (float) \number_format( $stats->position - $old_stats->position, 2 ),
];
$stats->keywords = $this->get_keywords_summary();
$stats->graph = $this->get_analytics_summary_graph();
$stats = apply_filters( 'rank_math/analytics/summary', $stats );
$stats = array_filter( (array) $stats );
$this->set_cache( $cache_key, $stats, $cache_group, DAY_IN_SECONDS );
return $stats;
}
/**
* Get posts summary.
*
* @param string $post_type Selected Post Type.
*
* @return object
*/
public function get_posts_summary( $post_type = '' ) {
$cache_key = $this->get_cache_key( 'posts_summary', $this->days . 'days' );
$cache = ! $post_type ? get_transient( $cache_key ) : false;
if ( false !== $cache ) {
return $cache;
}
global $wpdb;
$query = DB::analytics()
->selectCount( 'DISTINCT(' . $wpdb->prefix . 'rank_math_analytics_gsc.page)', 'posts' )
->selectSum( 'impressions', 'impressions' )
->selectSum( 'clicks', 'clicks' )
->selectAvg( 'ctr', 'ctr' )
->whereBetween( $wpdb->prefix . 'rank_math_analytics_gsc.created', [ $this->start_date, $this->end_date ] );
$summary = $query->one();
$summary = apply_filters( 'rank_math/analytics/posts_summary', $summary, $post_type, $query );
$summary = wp_parse_args(
array_filter( (array) $summary ),
[
'ctr' => 0,
'posts' => 0,
'clicks' => 0,
'pageviews' => 0,
'impressions' => 0,
]
);
set_transient( $cache_key, $summary, DAY_IN_SECONDS );
return $summary;
}
/**
* Get keywords summary.
*
* @return array
*/
public function get_keywords_summary() {
global $wpdb;
// Get Total Keywords Counts.
$keywords_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT(query))
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s",
$this->start_date,
$this->end_date
)
);
$old_keywords_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT(query))
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s",
$this->compare_start_date,
$this->compare_end_date
)
);
$keywords = [
'total' => (int) $keywords_count,
'previous' => (int) $old_keywords_count,
'difference' => (int) $keywords_count - (int) $old_keywords_count,
];
return $keywords;
}
/**
* Get analytics graph data.
*
* @return array
*/
public function get_analytics_summary_graph() {
global $wpdb;
$data = new \stdClass();
// Step1. Get splitted date intervals for graph within selected date range.
$intervals = $this->get_intervals();
$sql_daterange = $this->get_sql_date_intervals( $intervals );
// Step2. Get current analytics data by splitted date intervals.
// phpcs:disable
$query = $wpdb->prepare(
"SELECT DATE_FORMAT( created, '%%Y-%%m-%%d') as date, SUM(clicks) as clicks, SUM(impressions) as impressions, AVG(position) as position, AVG(ctr) as ctr, {$sql_daterange}
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s
GROUP BY range_group",
$this->start_date,
$this->end_date
);
$analytics = $wpdb->get_results( $query );
$analytics = $this->set_dimension_as_key( $analytics, 'range_group' );
// phpcs:enable
// Step2. Get current keyword data by splitted date intervals. Keyword count should be calculated as total count of most recent date for each splitted date intervals.
// phpcs:disable
$query = $wpdb->prepare(
"SELECT t.range_group, MAX(CONCAT(t.range_group, ':', t.date, ':', t.keywords )) as mixed FROM
(SELECT COUNT(DISTINCT(query)) as keywords, Date(created) as date, {$sql_daterange}
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s
GROUP BY range_group, Date(created)) AS t
GROUP BY t.range_group",
$this->start_date,
$this->end_date
);
$keywords = $wpdb->get_results( $query );
// phpcs:enable
$keywords = $this->extract_data_from_mixed( $keywords, 'mixed', ':', [ 'keywords', 'date' ] );
$keywords = $this->set_dimension_as_key( $keywords, 'range_group' );
// merge metrics data.
$data->analytics = [];
$data->analytics = $this->get_merged_metrics( $analytics, $keywords, true );
$data->merged = $this->get_date_array(
$intervals['dates'],
[
'clicks' => [],
'impressions' => [],
'position' => [],
'ctr' => [],
'keywords' => [],
'pageviews' => [],
]
);
// Convert types.
$data->analytics = array_map( [ $this, 'normalize_graph_rows' ], $data->analytics );
// Merge for performance.
$data->merged = $this->get_merge_data_graph( $data->analytics, $data->merged, $intervals['map'] );
// For developers.
$data = apply_filters( 'rank_math/analytics/analytics_summary_graph', $data, $intervals );
$data->merged = $this->get_graph_data_flat( $data->merged );
$data->merged = array_values( $data->merged );
return $data;
}
}
@@ -0,0 +1,115 @@
<?php
/**
* Get URL Inspection data.
*
* @since 1.0.84
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use Exception;
use RankMath\Helpers\DB as DB_Helper;
defined( 'ABSPATH' ) || exit;
/**
* Url_Inspection class.
*/
class Url_Inspection {
/**
* Holds the singleton instance of this class.
*
* @var Url_Inspection
*/
private static $instance;
/**
* Singleton
*/
public static function get() {
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Schedule a new inspection for an object ID.
*
* @param string $page URL to inspect (relative).
* @param string $reschedule What to do if the job already exists: reschedule for new time, or skip and keep old time.
* @param int $delay Number of seconds to delay the inspection from now.
*/
public function schedule_inspection( $page, $reschedule = true, $delay = 0 ) {
$delay = absint( $delay );
if ( $reschedule ) {
as_unschedule_action( 'rank_math/analytics/get_inspections_data', [ $page ], 'rank-math' );
} elseif ( as_has_scheduled_action( 'rank_math/analytics/get_inspections_data', [ $page ], 'rank-math' ) ) {
// Already scheduled and reschedule = false.
return;
}
if ( 0 === $delay ) {
as_enqueue_async_action( 'rank_math/analytics/get_inspections_data', [ $page ], 'rank-math' );
return;
}
$time = time() + $delay;
as_schedule_single_action( $time, 'rank_math/analytics/get_inspections_data', [ $page ], 'rank-math' );
}
/**
* Fetch the inspection data for a URL, store it, and return it.
*
* @param string $page URL to inspect.
*/
public function inspect( $page ) {
$inspection = \RankMath\Google\Url_Inspection::get()->get_inspection_data( $page );
if ( empty( $inspection ) ) {
return [];
}
DB::store_inspection( $inspection );
return wp_parse_args( $inspection, DB::get_inspection_defaults() );
}
/**
* Get latest inspection results for each page.
*
* @param array $params Parameters.
* @param int $per_page Number of items per page.
*/
public function get_inspections( $params, $per_page ) {
// Early Bail!!
if ( ! DB_Helper::check_table_exists( 'rank_math_analytics_inspections' ) ) {
return;
}
return DB::get_inspections( $params, $per_page );
}
/**
* Check if the "Enable Index Status Tab" option is enabled.
*
* @return bool
*/
public static function is_enabled() {
$profile = get_option( 'rank_math_google_analytic_profile', [] );
if ( empty( $profile ) || ! is_array( $profile ) ) {
return false;
}
$enable_index_status = true;
if ( isset( $profile['enable_index_status'] ) ) {
$enable_index_status = $profile['enable_index_status'];
}
return $enable_index_status;
}
}
@@ -0,0 +1,125 @@
<?php
/**
* The Analytics Module
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Google\Authentication;
defined( 'ABSPATH' ) || exit;
/**
* Watcher class.
*/
class Watcher {
use Hooker;
/**
* Main instance
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Watcher
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Watcher ) ) {
$instance = new Watcher();
$instance->hooks();
}
return $instance;
}
/**
* Hooks
*/
public function hooks() {
if ( Authentication::is_authorized() ) {
$this->action( 'save_post', 'update_post_info', 99 );
}
}
/**
* Update post info for analytics.
*
* @param int $post_id Post id.
*/
public function update_post_info( $post_id ) {
$status = get_post_status( $post_id );
$post_type = get_post_type( $post_id );
if (
'publish' !== $status ||
wp_is_post_autosave( $post_id ) ||
wp_is_post_revision( $post_id ) ||
! Helper::is_post_type_accessible( $post_type )
) {
DB::objects()
->where( 'object_type', 'post' )
->where( 'object_id', $post_id )
->delete();
return;
}
// Get primary focus keyword.
$primary_keyword = get_post_meta( $post_id, 'rank_math_focus_keyword', true );
if ( $primary_keyword ) {
$primary_keyword = explode( ',', $primary_keyword );
$primary_keyword = trim( $primary_keyword[0] );
}
$page = str_replace( Helper::get_home_url(), '', urldecode( get_permalink( $post_id ) ) );
// Set argument for object row.
$object_args = [
'id' => get_post_meta( $post_id, 'rank_math_analytic_object_id', true ),
'created' => get_the_modified_date( 'Y-m-d H:i:s', $post_id ),
'title' => get_the_title( $post_id ),
'page' => $page,
'object_type' => 'post',
'object_subtype' => $post_type,
'object_id' => $post_id,
'primary_key' => $primary_keyword,
'seo_score' => $primary_keyword ? get_post_meta( $post_id, 'rank_math_seo_score', true ) : 0,
'schemas_in_use' => \RankMath\Schema\DB::get_schema_types( $post_id, true, false ),
'is_indexable' => Helper::is_post_indexable( $post_id ),
'pagespeed_refreshed' => 'NULL',
];
// Get translated object info in case multi-language plugin is installed.
$translated_objects = apply_filters( 'rank_math/analytics/get_translated_objects', $post_id );
if ( false !== $translated_objects && is_array( $translated_objects ) ) {
// Remove current object info from objects table.
DB::objects()
->where( 'object_id', $post_id )
->delete();
foreach ( $translated_objects as $obj ) {
$object_args['title'] = $obj['title'];
$object_args['page'] = $obj['url'];
DB::add_object( $object_args );
}
// Here we don't need to add `rank_math_analytic_object_id` post meta, because we always remove old translated objects info and add new one, in case of multi-lanauge.
return;
}
// Update post from objects table.
$id = DB::update_object( $object_args );
if ( $id > 0 ) {
update_post_meta( $post_id, 'rank_math_analytic_object_id', $id );
}
}
}
@@ -0,0 +1,366 @@
<?php
/**
* Google Analytics.
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Google;
defined( 'ABSPATH' ) || exit;
use WP_Error;
use RankMath\Google\Api;
use RankMath\Helpers\Str;
use RankMath\Analytics\Workflow\Base;
/**
* Analytics class.
*/
class Analytics extends Request {
/**
* Get analytics accounts.
*/
public function get_analytics_accounts() {
$accounts = [];
$v3_response = $this->http_get( 'https://www.googleapis.com/analytics/v3/management/accountSummaries' );
$v3_data = true;
if ( ! $this->is_success() || isset( $v3_response->error ) ) {
$v3_data = false;
}
if ( false !== $v3_data ) {
foreach ( $v3_response['items'] as $account ) {
if ( 'analytics#accountSummary' !== $account['kind'] ) {
continue;
}
$properties = [];
$account_id = $account['id'];
foreach ( $account['webProperties'] as $property ) {
$property_id = $property['id'];
$properties[ $property_id ] = [
'name' => $property['name'],
'id' => $property['id'],
'url' => $property['websiteUrl'],
'account_id' => $account_id,
];
foreach ( $property['profiles'] as $profile ) {
unset( $profile['kind'] );
$properties[ $property_id ]['profiles'][ $profile['id'] ] = $profile;
}
}
$accounts[ $account_id ] = [
'name' => $account['name'],
'properties' => $properties,
];
}
}
return $this->add_ga4_accounts( $accounts );
}
/**
* Get GA4 accounts info.
*
* @param array $accounts GA3 accounts info or empty array.
*
* @return array $accounts with added ga4 accounts
*/
public function add_ga4_accounts( $accounts ) {
$v4_response = $this->http_get( 'https://analyticsadmin.googleapis.com/v1alpha/accountSummaries?pageSize=200' );
if ( ! $this->is_success() || isset( $v4_response->error ) ) {
return $accounts;
}
foreach ( $v4_response['accountSummaries'] as $account ) {
if ( empty( $account['propertySummaries'] ) ) {
continue;
}
$properties = [];
$account_id = str_replace( 'accounts/', '', $account['account'] );
foreach ( $account['propertySummaries'] as $property ) {
$property_id = str_replace( 'properties/', '', $property['property'] );
$accounts[ $account_id ]['properties'][ $property_id ] = [
'name' => $property['displayName'],
'id' => $property_id,
'account_id' => $account_id,
'type' => 'GA4',
];
}
}
return $accounts;
}
/**
* Check if google analytics is connected.
*
* @return boolean Returns True if the google analytics is connected, otherwise False.
*/
public static function is_analytics_connected() {
$account = wp_parse_args(
get_option( 'rank_math_google_analytic_options' ),
[ 'view_id' => '' ]
);
return ! empty( $account['view_id'] );
}
/**
* Query analytics data from google client api.
*
* @param array $options Analytics options.
* @param boolean $days Whether to include dates.
*
* @return array
*/
public static function get_analytics( $options = [], $days = false ) {
// Check view ID.
$view_id = isset( $options['view_id'] ) ? $options['view_id'] : self::get_view_id();
if ( ! $view_id ) {
return false;
}
$stored = get_option(
'rank_math_google_analytic_options',
[
'account_id' => '',
'property_id' => '',
'view_id' => '',
'measurement_id' => '',
'stream_name' => '',
'country' => '',
'install_code' => '',
'anonymize_ip' => '',
'local_ga_js' => '',
'exclude_loggedin' => '',
]
);
// Check property ID.
$property_id = isset( $options['property_id'] ) ? $options['property_id'] : $stored['property_id'];
if ( ! $property_id ) {
return false;
}
// Check dates.
$dates = Base::get_dates();
$start_date = isset( $options['start_date'] ) ? $options['start_date'] : $dates['start_date'];
$end_date = isset( $options['end_date'] ) ? $options['end_date'] : $dates['end_date'];
if ( ! $start_date || ! $end_date ) {
return false;
}
// Request params.
$row_limit = isset( $options['row_limit'] ) ? $options['row_limit'] : Api::get()->get_row_limit();
$country = isset( $options['country'] ) ? $options['country'] : '';
if ( ! empty( $stored['country'] ) && 'all' !== $stored['country'] ) {
$country = $stored['country'];
}
// Check the property for old Google Analytics.
if ( Str::starts_with( 'UA-', $property_id ) ) {
$args = [
'viewId' => $view_id,
'pageSize' => $row_limit,
'dateRanges' => [
[
'startDate' => $start_date,
'endDate' => $end_date,
],
],
'dimensionFilterClauses' => [
[
'filters' => [
[
'dimensionName' => 'ga:medium',
'operator' => 'EXACT',
'expressions' => 'organic',
],
],
],
],
];
// Include only dates.
if ( true === $days ) {
$args = wp_parse_args(
[
'dimensions' => [
[ 'name' => 'ga:date' ],
],
],
$args
);
} else {
$args = wp_parse_args(
[
'metrics' => [
[ 'expression' => 'ga:pageviews' ],
[ 'expression' => 'ga:users' ],
],
'dimensions' => [
[ 'name' => 'ga:date' ],
[ 'name' => 'ga:pagePath' ],
[ 'name' => 'ga:hostname' ],
],
'orderBys' => [
[
'fieldName' => 'ga:pageviews',
'sortOrder' => 'DESCENDING',
],
],
],
$args
);
// Add country.
if ( ! $country ) {
$args['dimensionFilterClauses'][0]['filters'][] = [
'dimensionName' => 'ga:countryIsoCode',
'operator' => 'EXACT',
'expressions' => $country,
];
}
}
$response = Api::get()->http_post(
'https://analyticsreporting.googleapis.com/v4/reports:batchGet',
[
'reportRequests' => [ $args ],
]
);
Api::get()->log_failed_request( $response, 'analytics', $start_date, func_get_args() );
if ( ! Api::get()->is_success() ) {
return new WP_Error( 'request_failed', __( 'The Google Analytics request failed.', 'rank-math' ) );
}
if ( ! isset( $response['reports'], $response['reports'][0]['data']['rows'] ) ) {
return false;
}
return $response['reports'][0]['data']['rows'];
}
// Request for GA4 API.
$args = [
'dateRanges' => [
[
'startDate' => $start_date,
'endDate' => $end_date,
],
],
'dimensionFilter' => [
'andGroup' => [
'expressions' => [
[
'filter' => [
'fieldName' => 'streamId',
'stringFilter' => [
'matchType' => 'EXACT',
'value' => $view_id,
],
],
],
[
'filter' => [
'fieldName' => 'sessionMedium',
'stringFilter' => [
'matchType' => 'EXACT',
'value' => 'organic',
],
],
],
],
],
],
];
// Include only dates.
if ( true === $days ) {
$args = wp_parse_args(
[
'dimensions' => [
[ 'name' => 'date' ],
],
],
$args
);
} else {
$args = wp_parse_args(
[
'dimensions' => [
[ 'name' => 'hostname' ],
[ 'name' => 'pagePath' ],
[ 'name' => 'countryId' ],
[ 'name' => 'sessionMedium' ],
],
'metrics' => [
[ 'name' => 'screenPageViews' ],
[ 'name' => 'totalUsers' ],
],
],
$args
);
// Include country.
if ( $country ) {
$args['dimensionFilter']['andGroup']['expressions'][] = [
'filter' => [
'fieldName' => 'countryId',
'stringFilter' => [
'matchType' => 'EXACT',
'value' => $country,
],
],
];
}
}
$workflow = 'analytics';
Api::get()->set_workflow( $workflow );
$response = Api::get()->http_post(
'https://analyticsdata.googleapis.com/v1beta/properties/' . $property_id . ':runReport',
$args
);
Api::get()->log_failed_request( $response, $workflow, $start_date, func_get_args() );
if ( ! Api::get()->is_success() ) {
return new WP_Error( 'request_failed', __( 'The Google Analytics Console request failed.', 'rank-math' ) );
}
if ( ! isset( $response['rows'] ) ) {
return false;
}
return $response['rows'];
}
/**
* Get view id.
*
* @return string
*/
public static function get_view_id() {
static $rank_math_view_id;
if ( is_null( $rank_math_view_id ) ) {
$options = get_option( 'rank_math_google_analytic_options' );
$rank_math_view_id = ! empty( $options['view_id'] ) ? $options['view_id'] : false;
}
return $rank_math_view_id;
}
}
@@ -0,0 +1,65 @@
<?php
/**
* Minimal Google API wrapper.
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Google;
defined( 'ABSPATH' ) || exit;
/**
* Api
*/
class Api extends Console {
/**
* Access token.
*
* @var array
*/
public $token = [];
/**
* Main instance
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Api
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Api ) ) {
$instance = new Api();
$instance->setup();
}
return $instance;
}
/**
* Setup token.
*/
private function setup() {
if ( ! Authentication::is_authorized() ) {
return;
}
$tokens = Authentication::tokens();
$this->token = $tokens['access_token'];
}
/**
* Get row limit.
*
* @return int
*/
public function get_row_limit() {
return apply_filters( 'rank_math/analytics/row_limit', 10000 );
}
}
@@ -0,0 +1,156 @@
<?php
/**
* Google Authentication wrapper.
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Google;
use RankMath\Helpers\Str;
use RankMath\Data_Encryption;
use RankMath\Helpers\Param;
use RankMath\Helpers\Security;
defined( 'ABSPATH' ) || exit;
/**
* Authentication class.
*/
class Authentication {
/**
* API version.
*
* @var string
*/
protected static $api_version = '2.1';
/**
* Get or update token data.
*
* @param bool|array $data Data to save.
* @return bool|array
*/
public static function tokens( $data = null ) {
$key = 'rank_math_google_oauth_tokens';
$encrypt_keys = [
'access_token',
'refresh_token',
];
// Clear data.
if ( false === $data ) {
delete_option( $key );
return false;
}
$saved = get_option( $key, [] );
foreach ( $encrypt_keys as $enc_key ) {
if ( isset( $saved[ $enc_key ] ) ) {
$saved[ $enc_key ] = Data_Encryption::deep_decrypt( $saved[ $enc_key ] );
}
}
// Getter.
if ( is_null( $data ) ) {
return wp_parse_args( $saved, [] );
}
// Setter.
foreach ( $encrypt_keys as $enc_key ) {
if ( isset( $saved[ $enc_key ] ) ) {
$saved[ $enc_key ] = Data_Encryption::deep_encrypt( $saved[ $enc_key ] );
}
if ( isset( $data[ $enc_key ] ) ) {
$data[ $enc_key ] = Data_Encryption::deep_encrypt( $data[ $enc_key ] );
}
}
$data = wp_parse_args( $data, $saved );
update_option( $key, $data );
return $data;
}
/**
* Is google authorized.
*
* @return boolean
*/
public static function is_authorized() {
$tokens = self::tokens();
return isset( $tokens['access_token'] ) && isset( $tokens['refresh_token'] );
}
/**
* Check if token is expired.
*
* @return boolean
*/
public static function is_token_expired() {
$tokens = self::tokens();
return $tokens['expire'] && time() > $tokens['expire'];
}
/**
* Get oauth url.
*
* @return string
*/
public static function get_auth_url() {
$page = self::get_page_slug();
return Security::add_query_arg_raw(
[
'version' => defined( 'RANK_MATH_PRO_VERSION' ) ? 'pro' : 'free',
'api_version' => static::$api_version,
'redirect_uri' => rawurlencode( admin_url( 'admin.php?page=' . $page ) ),
'security' => wp_create_nonce( 'rank_math_oauth_token' ),
],
self::get_auth_app_url()
);
}
/**
* Google custom app.
*
* @return string
*/
public static function get_auth_app_url() {
return apply_filters( 'rank_math/analytics/app_url', 'https://oauth.rankmath.com' );
}
/**
* Get page slug according to request.
*
* @return string
*/
public static function get_page_slug() {
$page = Param::get( 'page' );
if ( ! empty( $page ) ) {
switch ( $page ) {
case 'rank-math-wizard':
return 'rank-math-wizard&step=analytics';
case 'rank-math-analytics':
return 'rank-math-analytics';
default:
return 'rank-math-options-general#setting-panel-analytics';
}
}
$page = wp_get_referer();
if ( ! empty( $page ) && Str::contains( 'wizard', $page ) ) {
return 'rank-math-wizard&step=analytics';
}
return 'rank-math-options-general#setting-panel-analytics';
}
}
@@ -0,0 +1,336 @@
<?php
/**
* Google Search Console.
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Google;
use RankMath\Helpers\Str;
use RankMath\Analytics\Workflow\Base;
use RankMath\Sitemap\Sitemap;
use WP_Error;
defined( 'ABSPATH' ) || exit;
/**
* Console class.
*/
class Console extends Analytics {
/**
* Add site.
*
* @param string $url Site url to add.
*
* @return bool
*/
public function add_site( $url ) {
$this->http_put( 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode( $url ) );
return $this->is_success();
}
/**
* Get site verification token.
*
* @param string $url Site url to add.
*
* @return bool|string
*/
public function get_site_verification_token( $url ) {
$args = [
'site' => [
'type' => 'SITE',
'identifier' => $url,
],
'verificationMethod' => 'META',
];
$response = $this->http_post( 'https://www.googleapis.com/siteVerification/v1/token', $args );
if ( ! $this->is_success() ) {
return false;
}
return \RankMath\CMB2::sanitize_webmaster_tags( $response['token'] );
}
/**
* Verify site token.
*
* @param string $url Site url to add.
*
* @return bool|string
*/
public function verify_site( $url ) {
$token = $this->get_site_verification_token( $url );
if ( ! $token ) {
return;
}
// Save in transient.
set_transient( 'rank_math_google_site_verification', $token, DAY_IN_SECONDS * 2 );
// Call Google site verification.
$args = [
'site' => [
'type' => 'SITE',
'identifier' => $url,
],
];
$this->http_post( 'https://www.googleapis.com/siteVerification/v1/webResource?verificationMethod=META', $args );
// Sync sitemap.
as_enqueue_async_action( 'rank_math/analytics/sync_sitemaps', [], 'rank-math' );
return $this->is_success();
}
/**
* Get sites.
*
* @return array
*/
public function get_sites() {
static $rank_math_google_sites;
if ( ! \is_null( $rank_math_google_sites ) ) {
return $rank_math_google_sites;
}
$rank_math_google_sites = [];
$response = $this->http_get( 'https://www.googleapis.com/webmasters/v3/sites' );
if ( ! $this->is_success() || empty( $response['siteEntry'] ) ) {
return $rank_math_google_sites;
}
foreach ( $response['siteEntry'] as $site ) {
$rank_math_google_sites[ $site['siteUrl'] ] = $site['siteUrl'];
}
return $rank_math_google_sites;
}
/**
* Fetch sitemaps.
*
* @param string $url Site to get sitemaps for.
* @param boolean $with_index With index data.
*
* @return array
*/
public function get_sitemaps( $url, $with_index = false ) {
$with_index = $with_index ? '?sitemapIndex=' . rawurlencode( $url . Sitemap::get_sitemap_index_slug() . '.xml' ) : '';
$response = $this->http_get( 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode( $url ) . '/sitemaps' . $with_index );
if ( ! $this->is_success() || empty( $response['sitemap'] ) ) {
return [];
}
return $response['sitemap'];
}
/**
* Submit sitemap to search console.
*
* @param string $url Site to add sitemap for.
* @param string $sitemap Sitemap url.
*
* @return array
*/
public function add_sitemap( $url, $sitemap ) {
return $this->http_put( 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode( $url ) . '/sitemaps/' . rawurlencode( $sitemap ) );
}
/**
* Delete sitemap from search console.
*
* @param string $url Site to delete sitemap for.
* @param string $sitemap Sitemap url.
*
* @return array
*/
public function delete_sitemap( $url, $sitemap ) {
return $this->http_delete( 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode( $url ) . '/sitemaps/' . rawurlencode( $sitemap ) );
}
/**
* Query analytics data from google client api.
*
* @param array $args Query arguments.
*
* @return array
*/
public function get_search_analytics( $args = [] ) {
$dates = Base::get_dates();
$start_date = isset( $args['start_date'] ) ? $args['start_date'] : $dates['start_date'];
$end_date = isset( $args['end_date'] ) ? $args['end_date'] : $dates['end_date'];
$dimensions = isset( $args['dimensions'] ) ? $args['dimensions'] : 'date';
$row_limit = isset( $args['row_limit'] ) ? $args['row_limit'] : Api::get()->get_row_limit();
$params = [
'startDate' => $start_date,
'endDate' => $end_date,
'rowLimit' => $row_limit,
'dimensions' => \is_array( $dimensions ) ? $dimensions : [ $dimensions ],
];
$stored = get_option(
'rank_math_google_analytic_profile',
[
'country' => '',
'profile' => '',
'enable_index_status' => '',
]
);
$country = isset( $args['country'] ) ? $args['country'] : $stored['country'];
$profile = isset( $args['profile'] ) ? $args['profile'] : $stored['profile'];
if ( 'all' !== $country ) {
$params['dimensionFilterGroups'] = [
[
'filters' => [
[
'dimension' => 'country',
'operator' => 'equals',
'expression' => $country,
],
],
],
];
}
if ( empty( $profile ) ) {
$profile = trailingslashit( strtolower( home_url() ) );
}
$workflow = 'console';
$this->set_workflow( $workflow );
$response = $this->http_post(
'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode( $profile ) . '/searchAnalytics/query',
$params
);
$this->log_failed_request( $response, $workflow, $start_date, func_get_args() );
if ( ! $this->is_success() ) {
return new WP_Error( 'request_failed', __( 'The Google Search Console request failed.', 'rank-math' ) );
}
if ( ! isset( $response['rows'] ) ) {
return false;
}
return $response['rows'];
}
/**
* Is site verified.
*
* @param string $url Site to verify.
*
* @return boolean
*/
public function is_site_verified( $url ) {
$response = $this->http_get( 'https://www.googleapis.com/siteVerification/v1/webResource/' . rawurlencode( $url ) );
if ( ! $this->is_success() ) {
return false;
}
return isset( $response['owners'] );
}
/**
* Sync sitemaps with google search console.
*/
public function sync_sitemaps() {
$site_url = self::get_site_url();
$data = $this->get_sitemap_to_sync();
// Submit it.
if ( ! $data['sitemaps_in_list'] ) {
$this->add_sitemap( $site_url, $data['local_sitemap'] );
}
if ( empty( $data['delete_sitemaps'] ) ) {
return;
}
// Delete it.
foreach ( $data['delete_sitemaps'] as $sitemap ) {
$this->delete_sitemap( $site_url, $sitemap );
}
}
/**
* Get sitemaps to sync.
*
* @return array
*/
private function get_sitemap_to_sync() {
$delete_sitemaps = [];
$sitemaps_in_list = false;
$site_url = self::get_site_url();
$sitemaps = $this->get_sitemaps( $site_url );
$local_sitemap = trailingslashit( $site_url ) . Sitemap::get_sitemap_index_slug() . '.xml';
// Early Bail if there are no sitemaps.
if ( empty( $sitemaps ) ) {
return compact( 'delete_sitemaps', 'sitemaps_in_list', 'local_sitemap' );
}
foreach ( $sitemaps as $sitemap ) {
if ( $sitemap['path'] === $local_sitemap ) {
$sitemaps_in_list = true;
continue;
}
$delete_sitemaps[] = $sitemap['path'];
}
return compact( 'delete_sitemaps', 'sitemaps_in_list', 'local_sitemap' );
}
/**
* Get site url.
*
* @return string
*/
public static function get_site_url() {
static $rank_math_site_url;
if ( is_null( $rank_math_site_url ) ) {
$default = trailingslashit( strtolower( home_url() ) );
$rank_math_site_url = get_option( 'rank_math_google_analytic_profile', [ 'profile' => $default ] );
$rank_math_site_url = empty( $rank_math_site_url['profile'] ) ? $default : $rank_math_site_url['profile'];
if ( Str::contains( 'sc-domain:', $rank_math_site_url ) ) {
$rank_math_site_url = str_replace( 'sc-domain:', '', $rank_math_site_url );
$rank_math_site_url = ( is_ssl() ? 'https://' : 'http://' ) . $rank_math_site_url;
}
}
return $rank_math_site_url;
}
/**
* Check if console is connected.
*
* @return boolean Returns True if the console is connected, otherwise False.
*/
public static function is_console_connected() {
$profile = wp_parse_args(
get_option( 'rank_math_google_analytic_profile' ),
[
'profile' => '',
'country' => 'all',
]
);
return ! empty( $profile['profile'] );
}
}
@@ -0,0 +1,137 @@
<?php
/**
* Google Permissions.
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Google;
defined( 'ABSPATH' ) || exit;
/**
* Permissions class.
*/
class Permissions {
const OPTION_NAME = 'rank_math_analytics_permissions';
/**
* Permission info.
*/
public static function fetch() {
$tokens = Authentication::tokens();
if ( empty( $tokens['access_token'] ) ) {
return;
}
$url = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' . $tokens['access_token'];
$response = wp_remote_get( $url );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return;
}
$response = wp_remote_retrieve_body( $response );
if ( empty( $response ) ) {
return;
}
$response = \json_decode( $response, true );
$scopes = $response['scope'];
$scopes = explode( ' ', $scopes );
$scopes = str_replace( 'https://www.googleapis.com/auth/', '', $scopes );
update_option( self::OPTION_NAME, $scopes );
}
/**
* Get permissions.
*
* @return array
*/
public static function get() {
return get_option( self::OPTION_NAME, [] );
}
/**
* If user give permission or not.
*
* @param string $permission Permission name.
* @return boolean
*/
public static function has( $permission ) {
$permissions = self::get();
return in_array( $permission, $permissions, true );
}
/**
* If user give permission or not.
*
* @return boolean
*/
public static function has_console() {
return self::has( 'webmasters' );
}
/**
* If user give permission or not.
*
* @return boolean
*/
public static function has_analytics() {
return self::has( 'analytics.readonly' ) ||
self::has( 'analytics.provision' ) ||
self::has( 'analytics.edit' );
}
/**
* If user give permission or not.
*
* @return boolean
*/
public static function has_adsense() {
return self::has( 'adsense.readonly' );
}
/**
* If user give permission or not.
*
* @return string
*/
public static function get_status() {
return [
esc_html__( 'Search Console', 'rank-math' ) => self::get_status_text( self::has_console() ),
];
}
/**
* Status text
*
* @param boolean $check Truthness.
* @return string
*/
public static function get_status_text( $check ) {
return $check ? esc_html__( 'Given', 'rank-math' ) : esc_html__( 'Not Given', 'rank-math' );
}
/**
* Print warning
*/
public static function print_warning() {
?>
<p class="warning">
<strong class="warning">
<?php esc_html_e( 'Warning:', 'rank-math' ); ?>
</strong>
<?php
/* translators: %s is the reconnect link. */
printf( wp_kses_post( __( 'You have not given the permission to fetch this data. Please <a href="%s">reconnect</a> with all required permissions.', 'rank-math' ) ), wp_nonce_url( admin_url( 'admin.php?reconnect=google' ), 'rank_math_reconnect_google' ) );
?>
</p>
<?php
}
}
@@ -0,0 +1,489 @@
<?php
/**
* Google API Request.
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Google;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Request
*/
class Request {
/**
* Workflow.
*
* @var string
*/
private $workflow = '';
/**
* Was the last request successful.
*
* @var bool
*/
private $is_success = false;
/**
* Last error.
*
* @var string
*/
private $last_error = '';
/**
* Last response.
*
* @var array
*/
private $last_response = [];
/**
* Last response header code.
*
* @var int
*/
protected $last_code = 0;
/**
* Is refresh token notice added.
*
* @var bool
*/
private $is_notice_added = false;
/**
* Access token.
*
* @var string
*/
public $token = '';
/**
* Set workflow
*
* @param string $workflow Workflow name.
*/
public function set_workflow( $workflow = '' ) {
$this->workflow = $workflow;
}
/**
* Was the last request successful?
*
* @return bool True for success, false for failure
*/
public function is_success() {
return $this->is_success;
}
/**
* Get the last error returned by either the network transport, or by the API.
* If something didn't work, this should contain the string describing the problem.
*
* @return array|false describing the error
*/
public function get_error() {
return $this->last_error ? $this->last_error : false;
}
/**
* Get an array containing the HTTP headers and the body of the API response.
*
* @return array Assoc array with keys 'headers' and 'body'
*/
public function get_response() {
return $this->last_response;
}
/**
* Make an HTTP GET request - for retrieving data.
*
* @param string $url URL to do request.
* @param array $args Assoc array of arguments (usually your data).
* @param int $timeout Timeout limit for request in seconds.
*
* @return WP_Error|array|false Assoc array of API response, decoded from JSON.
*/
public function http_get( $url, $args = [], $timeout = 10 ) {
return $this->make_request( 'GET', $url, $args, $timeout );
}
/**
* Make an HTTP POST request - for creating and updating items.
*
* @param string $url URL to do request.
* @param array $args Assoc array of arguments (usually your data).
* @param int $timeout Timeout limit for request in seconds.
*
* @return WP_Error|array|false Assoc array of API response, decoded from JSON.
*/
public function http_post( $url, $args = [], $timeout = 10 ) {
return $this->make_request( 'POST', $url, $args, $timeout );
}
/**
* Make an HTTP PUT request - for creating new items.
*
* @param string $url URL to do request.
* @param array $args Assoc array of arguments (usually your data).
* @param int $timeout Timeout limit for request in seconds.
*
* @return WP_Error|array|false Assoc array of API response, decoded from JSON.
*/
public function http_put( $url, $args = [], $timeout = 10 ) {
return $this->make_request( 'PUT', $url, $args, $timeout );
}
/**
* Make an HTTP DELETE request - for deleting data.
*
* @param string $url URL to do request.
* @param array $args Assoc array of arguments (usually your data).
* @param int $timeout Timeout limit for request in seconds.
*
* @return WP_Error|array|false Assoc array of API response, decoded from JSON.
*/
public function http_delete( $url, $args = [], $timeout = 10 ) {
return $this->make_request( 'DELETE', $url, $args, $timeout );
}
/**
* Performs the underlying HTTP request. Not very exciting.
*
* @param string $http_verb The HTTP verb to use: get, post, put, patch, delete.
* @param string $url URL to do request.
* @param array $args Assoc array of parameters to be passed.
* @param int $timeout Timeout limit for request in seconds.
*
* @return array|false Assoc array of decoded result.
*/
private function make_request( $http_verb, $url, $args = [], $timeout = 10 ) {
// Early Bail!!
if ( ! Authentication::is_authorized() ) {
return;
}
if ( ! $this->refresh_token() || ! is_scalar( $this->token ) ) {
if ( ! $this->is_notice_added ) {
$this->is_notice_added = true;
$this->is_success = false;
$this->last_error = sprintf(
/* translators: reconnect link */
wp_kses_post( __( 'There is a problem with the Google auth token. Please <a href="%1$s" class="button button-link rank-math-reconnect-google">reconnect your app</a>', 'rank-math' ) ),
wp_nonce_url( admin_url( 'admin.php?reconnect=google' ), 'rank_math_reconnect_google' )
);
$this->log_response( $http_verb, $url, $args, '', '', '', date( 'Y-m-d H:i:s' ) . ': Google auth token has been expired or is invalid' );
}
return;
}
$params = [
'timeout' => $timeout,
'method' => $http_verb,
];
$params['headers'] = [ 'Authorization' => 'Bearer ' . $this->token ];
if ( 'DELETE' === $http_verb || 'PUT' === $http_verb ) {
$params['headers']['Content-Length'] = '0';
} elseif ( 'POST' === $http_verb && ! empty( $args ) && is_array( $args ) ) {
$json = wp_json_encode( $args );
$params['body'] = $json;
$params['headers']['Content-Type'] = 'application/json';
$params['headers']['Content-Length'] = strlen( $json );
}
$this->reset();
sleep( 1 );
$response = wp_remote_request( $url, $params );
$formatted_response = $this->format_response( $response );
$this->determine_success( $response, $formatted_response );
$this->log_response( $http_verb, $url, $args, $response, $formatted_response, $params );
// Error handaling.
$code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $code ) {
// Remove workflow actions.
if ( $this->workflow ) {
as_unschedule_all_actions( 'rank_math/analytics/get_' . $this->workflow . '_data' );
}
}
do_action(
'rank_math/analytics/handle_' . $this->workflow . '_response',
[
'formatted_response' => $formatted_response,
'response' => $response,
'http_verb' => $http_verb,
'url' => $url,
'args' => $args,
'code' => $code,
]
);
return $formatted_response;
}
/**
* Log the response in analytics_debug.log file.
*
* @param string $http_verb The HTTP verb to use: get, post, put, patch, delete.
* @param string $url URL to do request.
* @param array $args Assoc array of parameters to be passed.
* @param string $response make_request response.
* @param string $formatted_response Formated response.
* @param array $params Parameters.
* @param string $text Text to append at the end of the response.
*/
private function log_response( $http_verb = '', $url = '', $args = [], $response = [], $formatted_response = '', $params = [], $text = '' ) {
do_action( 'rank_math/analytics/log', $http_verb, $url, $args, $response, $formatted_response, $params );
if ( ! apply_filters( 'rank_math/analytics/log_response', false ) ) {
return;
}
$uploads = wp_upload_dir();
$file = $uploads['basedir'] . '/rank-math/analytics-debug.log';
$wp_filesystem = Helper::get_filesystem();
// Create log file if it doesn't exist.
$wp_filesystem->touch( $file );
// Not writable? Bail.
if ( ! $wp_filesystem->is_writable( $file ) ) {
return;
}
$message = '********************************' . PHP_EOL;
$message .= date( 'Y-m-d h:i:s' ) . PHP_EOL;
$tokens = Authentication::tokens();
if ( ! empty( $tokens ) && is_array( $tokens ) && isset( $tokens['expire'] ) ) {
$message .= 'Expiry: ' . date( 'Y-m-d h:i:s', $tokens['expire'] ) . PHP_EOL;
$message .= 'Expiry Readable: ' . human_time_diff( $tokens['expire'] ) . PHP_EOL;
}
$message .= $text . PHP_EOL;
if ( is_wp_error( $response ) ) {
$message .= '<span class="fail">FAIL</span>' . PHP_EOL;
$message .= 'WP_Error: ' . $response->get_error_message() . PHP_EOL;
} elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$message .= '<span class="fail">FAIL</span>' . PHP_EOL;
} elseif ( isset( $formatted_response['error_description'] ) ) {
$message .= '<span class="fail">FAIL</span>' . PHP_EOL;
$message .= 'Bad Request' === $formatted_response['error_description'] ?
esc_html__( 'Bad request. Please check the code.', 'rank-math' ) : $formatted_response['error_description'];
} else {
$message .= '<span class="pass">PASS</span>' . PHP_EOL;
}
$message .= 'REQUEST: ' . $http_verb . ' > ' . $url . PHP_EOL;
$message .= 'REQUEST_PARAMETERS: ' . wp_json_encode( $params ) . PHP_EOL;
$message .= 'REQUEST_API_ARGUMENTS: ' . wp_json_encode( $args ) . PHP_EOL;
$message .= 'RESPONSE_CODE: ' . wp_remote_retrieve_response_code( $response ) . PHP_EOL;
$message .= 'RESPONSE_CODE_MESSAGE: ' . wp_remote_retrieve_body( $response ) . PHP_EOL;
$message .= 'RESPONSE_FORMATTED: ' . wp_json_encode( $formatted_response ) . PHP_EOL;
$message .= 'ORIGINAL_RESPONSE: ' . wp_json_encode( $response ) . PHP_EOL;
$message .= '================================' . PHP_EOL;
$message .= $wp_filesystem->get_contents( $file );
$wp_filesystem->put_contents( $file, $message );
}
/**
* Decode the response and format any error messages for debugging
*
* @param array $response The response from the curl request.
*
* @return array|false The JSON decoded into an array
*/
private function format_response( $response ) {
$this->last_response = $response;
if ( is_wp_error( $response ) ) {
return false;
}
if ( ! empty( $response['body'] ) ) {
return json_decode( $response['body'], true );
}
return false;
}
/**
* Check if the response was successful or a failure. If it failed, store the error.
*
* @param object $response The response from the curl request.
* @param array|false $formatted_response The response body payload from the curl request.
*/
private function determine_success( $response, $formatted_response ) {
if ( is_wp_error( $response ) ) {
$this->last_error = 'WP_Error: ' . $response->get_error_message();
return;
}
$this->last_code = wp_remote_retrieve_response_code( $response );
if ( in_array( $this->last_code, [ 200, 204 ], true ) ) {
$this->is_success = true;
return;
}
if ( isset( $formatted_response['error_description'] ) ) {
$this->last_error = 'Bad Request' === $formatted_response['error_description'] ?
esc_html__( 'Bad request. Please check the code.', 'rank-math' ) : $formatted_response['error_description'];
return;
}
$this->last_error = esc_html__( 'Unknown error, call get_response() to find out what happened.', 'rank-math' );
}
/**
* Reset request.
*/
private function reset() {
$this->last_code = 0;
$this->last_error = '';
$this->is_success = false;
$this->last_response = [
'body' => null,
'headers' => null,
];
}
/**
* Refresh access token when user login.
*/
public function refresh_token() {
// Bail if the user is not authenticated at all yet.
if ( ! Authentication::is_authorized() || ! Authentication::is_token_expired() ) {
return true;
}
$response = $this->get_refresh_token();
if ( ! $response ) {
return false;
}
if ( false === $response['success'] ) {
return false;
}
$tokens = Authentication::tokens();
// Save new token.
$this->token = $response['access_token'];
$tokens['expire'] = $response['expire'];
$tokens['access_token'] = $response['access_token'];
Authentication::tokens( $tokens );
return true;
}
/**
* Get the new refresh token.
*
* @return mixed
*/
protected function get_refresh_token() {
$tokens = Authentication::tokens();
if ( empty( $tokens['refresh_token'] ) ) {
return false;
}
$response = wp_remote_get(
add_query_arg(
[
'code' => $tokens['refresh_token'],
'format' => 'json',
],
Authentication::get_auth_app_url() . '/refresh.php'
)
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return false;
}
$response = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $response ) ) {
return false;
}
return $response;
}
/**
* Revoke an OAuth2 token.
*
* @return boolean Whether the token was revoked successfully.
*/
public function revoke_token() {
Authentication::tokens( false );
delete_option( 'rank_math_google_analytic_profile' );
delete_option( 'rank_math_google_analytic_options' );
delete_option( 'rankmath_google_api_failed_attempts_data' );
delete_option( 'rankmath_google_api_reconnect' );
return $this->is_success();
}
/**
* Log every failed API call.
* And kill all next scheduled event if failed count is more then three.
*
* @param array $response Response from api.
* @param string $action Action performing.
* @param string $start_date Start date fetching for (or page URI for inspections).
* @param array $args Array of arguments.
*/
public function log_failed_request( $response, $action, $start_date, $args ) {
if ( $this->is_success() ) {
return;
}
$option_key = 'rankmath_google_api_failed_attempts_data';
$reconnect_google_option_key = 'rankmath_google_api_reconnect';
if ( empty( $response['error'] ) || ! is_array( $response['error'] ) ) {
delete_option( $option_key );
delete_option( $reconnect_google_option_key );
return;
}
// Limit maximum 10 failed attempt data to log.
$failed_attempts = get_option( $option_key, [] );
$failed_attempts = ( ! empty( $failed_attempts ) && is_array( $failed_attempts ) ) ? array_slice( $failed_attempts, -9, 9 ) : [];
$failed_attempts[] = [
'action' => $action,
'args' => $args,
'error' => $response['error'],
];
update_option( $option_key, $failed_attempts, false );
// Number of allowed attempt.
if ( 3 < count( $failed_attempts ) ) {
update_option( $reconnect_google_option_key, 'search_analytics_query' );
return;
}
as_schedule_single_action(
time() + 60,
"rank_math/analytics/get_{$action}_data",
[ $start_date ],
'rank-math'
);
}
}
@@ -0,0 +1,206 @@
<?php
/**
* Google URL Inspection API.
*
* @since 1.0.84
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Google;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Analytics class.
*/
class Url_Inspection extends Request {
/**
* URL Inspection API base URL.
*
* @var string
*/
private $api_url = 'https://searchconsole.googleapis.com/v1/urlInspection/index:inspect';
/**
* Access token.
*
* @var array
*/
public $token = [];
/**
* Main instance
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Url_Inspection
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Url_Inspection ) ) {
$instance = new Url_Inspection();
$instance->setup();
}
return $instance;
}
/**
* Setup token.
*/
public function setup() {
if ( ! Authentication::is_authorized() ) {
return;
}
$tokens = Authentication::tokens();
$this->token = $tokens['access_token'];
}
/**
* Send URL to the API and return the response, or false on failure.
*
* @param string $page URL to inspect (relative).
*/
public function get_api_results( $page ) {
$lang_arr = \explode( '_', get_locale() );
$lang_code = empty( $lang_arr[1] ) ? $lang_arr[0] : $lang_arr[0] . '-' . $lang_arr[1];
$args = [
'inspectionUrl' => untrailingslashit( Helper::get_home_url() ) . $page,
'siteUrl' => Console::get_site_url(),
'languageCode' => $lang_code,
];
set_time_limit( 90 );
$workflow = 'inspections';
$this->set_workflow( $workflow );
$response = $this->http_post( $this->api_url, $args, 60 );
$this->log_failed_request( $response, $workflow, $page, func_get_args() );
if ( ! $this->is_success() ) {
return false;
}
return $response;
}
/**
* Get inspection data.
*
* @param string $page URL to inspect.
*/
public function get_inspection_data( $page ) {
$inspection = $this->get_api_results( $page );
if ( empty( $inspection ) || empty( $inspection['inspectionResult'] ) ) {
return;
}
$inspection = $this->normalize_inspection_data( $inspection );
$inspection['page'] = $page;
return $inspection;
}
/**
* Normalize inspection data.
*
* @param array $inspection Inspection data.
*/
private function normalize_inspection_data( $inspection ) {
$incoming = $inspection['inspectionResult'];
$normalized = [];
$map_properties = [
'indexStatusResult.verdict' => 'index_verdict',
'indexStatusResult.coverageState' => 'coverage_state',
'indexStatusResult.indexingState' => 'indexing_state',
'indexStatusResult.pageFetchState' => 'page_fetch_state',
'indexStatusResult.robotsTxtState' => 'robots_txt_state',
'mobileUsabilityResult.verdict' => 'mobile_usability_verdict',
'mobileUsabilityResult.issues' => 'mobile_usability_issues',
'richResultsResult.verdict' => 'rich_results_verdict',
'indexStatusResult.crawledAs' => 'crawled_as',
'indexStatusResult.googleCanonical' => 'google_canonical',
'indexStatusResult.userCanonical' => 'user_canonical',
'indexStatusResult.sitemap' => 'sitemap',
'indexStatusResult.referringUrls' => 'referring_urls',
];
$this->assign_inspection_values( $incoming, $map_properties, $normalized );
$normalized = apply_filters( 'rank_math/analytics/url_inspection_map_properties', $normalized, $incoming );
return $normalized;
}
/**
* Assign inspection field value to the data array.
*
* @param array $raw_data Raw data.
* @param string $field Field name.
* @param string $assign_to Field name to assign to.
* @param array $data Data array.
*
* @return void
*/
public function assign_inspection_value( $raw_data, $field, $assign_to, &$data ) {
$data[ $assign_to ] = $this->get_result_field( $raw_data, $field );
if ( is_array( $data[ $assign_to ] ) ) {
$data[ $assign_to ] = wp_json_encode( $data[ $assign_to ] );
} elseif ( preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/', $data[ $assign_to ], $matches ) ) {
// If it's a date, convert to MySQL format.
$data[ $assign_to ] = date( 'Y-m-d H:i:s', strtotime( $matches[0] ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Date is stored as TIMESTAMP, so the timezone is converted automatically.
}
}
/**
* Get a field from the inspection result.
*
* @param array $raw_data Incoming data.
* @param string $field Field name.
*
* @return mixed
*/
protected function get_result_field( $raw_data, $field ) {
if ( false !== strpos( $field, '.' ) ) {
$fields = explode( '.', $field );
if ( ! isset( $raw_data[ $fields[0] ] ) || ! isset( $raw_data[ $fields[0] ][ $fields[1] ] ) ) {
return '';
}
return $raw_data[ $fields[0] ][ $fields[1] ];
}
if ( ! isset( $raw_data[ $field ] ) ) {
return '';
}
return $raw_data[ $field ];
}
/**
* Assign inspection field values to the data array.
*
* @param array $raw_data Raw data.
* @param array $fields Map properties.
* @param array $data Data array.
*
* @return void
*/
public function assign_inspection_values( $raw_data, $fields, &$data ) {
foreach ( $fields as $field => $assign_to ) {
$this->assign_inspection_value( $raw_data, $field, $assign_to, $data );
}
}
}
@@ -0,0 +1 @@
<?php // Silence is golden.
@@ -0,0 +1,268 @@
<?php
/**
* The Global functionality of the plugin.
*
* Defines the functionality loaded on admin.
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\Rest
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use WP_Error;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Controller;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Rest class.
*/
class Rest extends WP_REST_Controller {
/**
* Constructor.
*/
public function __construct() {
$this->namespace = \RankMath\Rest\Rest_Helper::BASE . '/an';
}
/**
* Registers the routes for the objects of the controller.
*/
public function register_routes() {
$routes = [
'dashboard' => [
'callback' => [ $this, 'get_dashboard' ],
],
'keywordsOverview' => [
'callback' => [ $this, 'get_keywords_overview' ],
],
'postsSummary' => [
'callback' => [ Stats::get(), 'get_posts_summary' ],
],
'postsRowsByObjects' => [
'callback' => [ Stats::get(), 'get_posts_rows_by_objects' ],
],
'post/(?P<id>\d+)' => [
'callback' => [ $this, 'get_post' ],
],
'keywordsSummary' => [
'callback' => [ Stats::get(), 'get_analytics_summary' ],
],
'analyticsSummary' => [
'callback' => [ $this, 'get_analytics_summary' ],
],
'keywordsRows' => [
'callback' => [ Stats::get(), 'get_keywords_rows' ],
],
'userPreferences' => [
'callback' => [ $this, 'update_user_preferences' ],
'methods' => WP_REST_Server::CREATABLE,
],
'inspectionResults' => [
'callback' => [ $this, 'get_inspection_results' ],
],
'removeFrontendStats' => [
'callback' => [ $this, 'remove_frontend_stats' ],
'methods' => WP_REST_Server::CREATABLE,
],
];
foreach ( $routes as $route => $args ) {
$this->register_route( $route, $args );
}
}
/**
* Register a route.
*
* @param string $route Route.
* @param array $args Arguments.
*/
private function register_route( $route, $args ) {
$route_defaults = [
'methods' => WP_REST_Server::READABLE,
'permission_callback' => [ $this, 'has_permission' ],
];
$route_args = wp_parse_args( $args, $route_defaults );
register_rest_route( $this->namespace, '/' . $route, $route_args );
}
/**
* Determines if the current user can manage analytics.
*
* @return true
*/
public function has_permission() {
return current_user_can( 'rank_math_analytics' );
}
/**
* Update user perferences.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return boolean|WP_Error True on success, or WP_Error object on failure.
*/
public function update_user_preferences( WP_REST_Request $request ) {
$pref = $request->get_param( 'preferences' );
if ( empty( $pref ) ) {
return new WP_Error(
'param_value_empty',
esc_html__( 'Sorry, no preference found.', 'rank-math' )
);
}
update_user_meta(
get_current_user_id(),
'rank_math_analytics_table_columns',
$pref
);
return true;
}
/**
* Get post data.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_post( WP_REST_Request $request ) {
$id = $request->get_param( 'id' );
if ( empty( $id ) ) {
return new WP_Error(
'param_value_empty',
esc_html__( 'Sorry, no post id found.', 'rank-math' )
);
}
return rest_ensure_response( Stats::get()->get_post( $request ) );
}
/**
* Get dashboard data.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_dashboard( WP_REST_Request $request ) { // phpcs:ignore
return rest_ensure_response(
[
'stats' => Stats::get()->get_analytics_summary(),
'optimization' => Stats::get()->get_optimization_summary(),
]
);
}
/**
* Get analytics summary.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_analytics_summary( WP_REST_Request $request ) { // phpcs:ignore
$post_type = sanitize_key( $request->get_param( 'postType' ) );
return rest_ensure_response(
[
'summary' => Stats::get()->get_posts_summary( $post_type ),
'optimization' => Stats::get()->get_optimization_summary( $post_type ),
]
);
}
/**
* Get keywords overview.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_keywords_overview( WP_REST_Request $request ) { // phpcs:ignore
return rest_ensure_response(
apply_filters(
'rank_math/analytics/keywords_overview',
[
'topKeywords' => Stats::get()->get_top_keywords(),
'positionGraph' => Stats::get()->get_top_position_graph(),
]
)
);
}
/**
* Get inspection results: latest result for each post.
*
* @param WP_REST_Request $request Rest request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_inspection_results( WP_REST_Request $request ) {
$per_page = 25;
$rows = Url_Inspection::get()->get_inspections( $request->get_params(), $per_page );
if ( empty( $rows ) ) {
return [
'rows' => [ 'response' => 'No Data' ],
'rowsFound' => 0,
];
}
return rest_ensure_response(
[
'rows' => $rows,
'rowsFound' => DB::get_inspections_count( $request->get_params() ),
]
);
}
/**
* Remove frontend stats.
*
* @param WP_REST_Request $request Rest request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function remove_frontend_stats( WP_REST_Request $request ) {
if ( (bool) $request->get_param( 'toggleBar' ) ) {
$hide_bar = (bool) $request->get_param( 'hide' );
$user_id = get_current_user_id();
if ( $hide_bar ) {
return update_user_meta( $user_id, 'rank_math_hide_frontend_stats', true );
}
return delete_user_meta( $user_id, 'rank_math_hide_frontend_stats' );
}
$all_opts = rank_math()->settings->all_raw();
$general = $all_opts['general'];
$general['analytics_stats'] = 'off';
Helper::update_all_settings( $general, null, null );
return true;
}
/**
* Should update pagespeed record.
*
* @param int $id Database row id.
* @return bool
*/
private function should_update_pagespeed( $id ) {
$record = DB::objects()->where( 'id', $id )->one();
return \time() > ( \strtotime( $record->pagespeed_refreshed ) + ( DAY_IN_SECONDS * 7 ) );
}
}
@@ -0,0 +1,32 @@
<?php
/**
* Dashboard page template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
use RankMath\Helper;
use RankMath\Google\Authentication;
defined( 'ABSPATH' ) || exit;
// Header.
rank_math()->admin->display_admin_header();
$path = rank_math()->admin_dir() . 'wizard/views/'; // phpcs:ignore
?>
<div class="wrap rank-math-wrap analytics">
<span class="wp-header-end"></span>
<?php
if ( ! Helper::is_site_connected() ) {
require_once $path . 'rank-math-connect.php';
} elseif ( ! Authentication::is_authorized() ) {
require_once $path . 'google-connect.php';
} else {
echo '<div class="rank-math-analytics" id="rank-math-analytics"></div>';
}
?>
</div>
@@ -0,0 +1,22 @@
<?php
/**
* Analytics Report header template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
use RankMath\KB;
defined( 'ABSPATH' ) || exit;
?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="cta">
<tbody>
<tr class="top">
<td align="left">
<a href="<?php KB::the( 'seo-email-reporting', 'Email Report CTA' ); ?>"><?php $this->image( 'rank-math-pro.jpg', 540, 422, __( 'Rank Math PRO', 'rank-math' ) ); ?></a>
</td>
</tr>
</tbody>
</table>
@@ -0,0 +1,39 @@
<?php
/**
* Analytics Report email template footer.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
?>
</td>
</tr>
</table>
</td>
</tr>
<!-- START FOOTER -->
<tr class="footer">
<td class="wrapper">
<p class="first">
###FOOTER_HTML###
</p>
</td>
</tr>
<!-- END FOOTER -->
<!-- END MAIN CONTENT AREA -->
</table>
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>
@@ -0,0 +1,42 @@
<?php
/**
* Analytics Report header template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="report-info">
<tr>
<td>
<h1><?php esc_html_e( 'SEO Report of Your Website', 'rank-math' ); ?></h1>
<h2 class="report-date">###START_DATE### - ###END_DATE###</h2>
<a href="###SITE_URL###" target="_blank" class="site-url">###SITE_URL_SIMPLE###</a>
</td>
<td class="full-report-link">
<a href="###REPORT_URL###" target="_blank" class="full-report-link">
<?php esc_html_e( 'FULL REPORT', 'rank-math' ); ?>
<?php $this->image( 'report-icon-external.png', 12, 12, __( 'External Link Icon', 'rank-math' ) ); ?>
</a>
</td>
</tr>
</table>
<?php if ( $this->get_variable( 'stats_invalid_data' ) ) { ?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="report-error">
<tr>
<td>
<h2><?php esc_html_e( 'Uh-oh', 'rank-math' ); ?></h2>
<p><em><?php esc_html_e( 'It seems that there are no stats to show right now.', 'rank-math' ); ?></em></p>
<?php // Translators: placeholders are anchor opening and closing tags. ?>
<p><?php printf( esc_html__( 'If you can see the site data in your Search Console and Analytics accounts, but not here, then %1$s try reconnecting your account %2$s and make sure that the correct properties are selected in the %1$s Analytics Settings%2$s.', 'rank-math' ), '<a href="' . Helper::get_admin_url( 'options-general#setting-panel-analytics' ) . '">', '</a>' ); ?></p>
</td>
</tr>
</table>
<?php
}
@@ -0,0 +1,56 @@
<?php
/**
* Analytics Report header template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
?><!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title><?php esc_html_e( 'SEO Report of Your Website', 'rank-math' ); ?></title>
<?php $this->template_part( 'style' ); ?>
</head>
<body class="">
<span class="preheader"><?php esc_html_e( 'SEO Report of Your Website', 'rank-math' ); ?></span>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&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>
@@ -0,0 +1,24 @@
<?php
/**
* Analytics Report email template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
$this->template_part( 'header' );
?>
<?php $this->template_part( 'header-after' ); ?>
<?php $this->template_part( 'sections/summary' ); ?>
<?php $this->template_part( 'sections/positions' ); ?>
<?php $this->template_part( 'cta' ); ?>
<?php
$this->template_part( 'footer' );
@@ -0,0 +1,56 @@
<?php
/**
* Analytics Report summary table template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
?>
<?php if ( $this->get_variable( 'stats_invalid_data' ) ) { ?>
<?php return; ?>
<?php } ?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="stats-2">
<tr>
<td class="col-1">
<h3><?php esc_html_e( 'Top 3 Positions', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_top_3_positions' ),
'diff' => $this->get_variable( 'stats_top_3_positions_diff' ),
]
);
?>
</td>
<td class="col-2">
<h3><?php esc_html_e( '4-10 Positions', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_top_10_positions' ),
'diff' => $this->get_variable( 'stats_top_10_positions_diff' ),
]
);
?>
</td>
<td class="col-3">
<h3><?php esc_html_e( '11-50 Positions', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_top_50_positions' ),
'diff' => $this->get_variable( 'stats_top_50_positions_diff' ),
]
);
?>
</td>
</tr>
</table>
@@ -0,0 +1,81 @@
<?php
/**
* Analytics Report summary table template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
?>
<?php if ( $this->get_variable( 'stats_invalid_data' ) ) { ?>
<?php return; ?>
<?php } ?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="stats">
<tr>
<td class="col-1">
<h3><?php esc_html_e( 'Total Impressions', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_impressions' ),
'diff' => $this->get_variable( 'stats_impressions_diff' ),
'graph' => true,
'graph_data' => $this->get_graph_data( 'impressions' ),
]
);
?>
</td>
<td class="col-2">
<h3><?php esc_html_e( 'Total Clicks', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_clicks' ),
'diff' => $this->get_variable( 'stats_clicks_diff' ),
'graph' => true,
'graph_data' => $this->get_graph_data( 'clicks' ),
]
);
?>
</td>
</tr>
<tr>
<td class="col-1">
<h3><?php esc_html_e( 'Total Keywords', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_keywords' ),
'diff' => $this->get_variable( 'stats_keywords_diff' ),
'graph' => true,
'graph_data' => $this->get_graph_data( 'keywords' ),
]
);
?>
</td>
<td class="col-2">
<h3><?php esc_html_e( 'Average Position', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_position' ),
'diff' => $this->get_variable( 'stats_position_diff' ),
'graph' => true,
'graph_data' => $this->get_graph_data( 'position' ),
'graph_modifier' => -100,
'human_number' => false,
'invert' => true,
]
);
?>
</td>
</tr>
</table>
@@ -0,0 +1,69 @@
<?php
/**
* Analytics Report header template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
use RankMath\Helpers\Str;
defined( 'ABSPATH' ) || exit;
$diff_class = $diff > 0 ? 'positive' : 'negative';
if ( ! empty( $invert ) ) {
$diff_class = $diff < 0 ? 'positive' : 'negative';
}
$diff_sign = '<span class="diff-sign">' . ( 'positive' === $diff_class ? '&#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;' ] );
} ?>
@@ -0,0 +1,496 @@
<?php
/**
* Analytics Report email styling.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
?>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
/* All the styling goes here */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f7f9fb;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
}
table td {
font-size: 15px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body, td {
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;
}
.body {
background-color: #F0F4F8;
width: 100%;
}
/* Set a max-width, and make it display as block. */
.container {
display: block;
margin: 0 auto !important;
/* makes it centered */
max-width: 90%;
padding: 50px 0;
width: 600px;
}
.content {
box-sizing: border-box;
display: block;
margin: 0 auto;
width: 100%;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 6px;
width: 100%;
color: #1a1e22;
}
.wrapper {
box-sizing: border-box;
padding: 30px 30px 60px;
}
.header {
background: #724BB7;
background: linear-gradient(90deg, #724BB7 0%, #4098D7 100%);
border-radius: 8px 8px 0 0;
height: 76px;
vertical-align: middle;
padding: 0 30px;
color: #ffffff;
}
td.logo {
vertical-align: middle;
}
td.logo img {
width: auto;
height: 26px;
margin-top: 6px;
}
.period-days {
text-align: right;
vertical-align: middle;
font-weight: 500;
letter-spacing: 0.5px;
font-size: 14px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
margin-top: 10px;
width: 100%;
}
.footer .wrapper {
padding-bottom: 30px;
}
.footer td,
.footer p,
.footer span {
color: #999ba7;
font-size: 14px;
}
.footer td {
padding-top: 0;
}
.footer p.first {
padding-top: 20px;
border-top: 1px solid #e5e5e7;
line-height: 1.8;
margin-bottom: 0;
}
.footer .rank-math-contact-address {
font-style: normal;
}
.footer p:empty {
display: none;
}
.footer address {
display: inline-block;
font-style: normal;
margin-top: 10px;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-weight: 600;
line-height: 1.4;
margin: 0;
}
h1 {
font-size: 30px;
}
p,
ul,
ol {
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #22a8e6;
text-decoration: none;
}
h2.report-date {
margin: 25px 0 4px;
font-size: 18px;
}
.site-url {
color: #595d6f;
text-decoration: none;
font-size: 15px;
}
.full-report-link {
vertical-align: bottom;
text-align: right;
width: 110px;
}
.full-report-link a {
font-size: 12px;
font-weight: 600;
text-decoration: none;
}
.full-report-link img {
vertical-align: -1px;
margin-left: 2px;
}
table.report-error {
border: 2px solid #f1d400;
background: #fffdec;
margin: 10px 0;
}
table.report-error td {
padding: 5px 10px;
}
table.stats {
border-collapse: separate;
margin-top: 10px;
}
table.stats td {
width: 50%;
padding: 20px 20px;
background: #f7f9fb;
border: 10px solid #fff;
border-radius: 16px;
}
table.stats td.col-2 {
border-right: none;
}
table.stats td.col-1 {
border-left: none;
}
h3 {
font-size: 13px;
font-weight: 500;
color: #565a6b;
text-transform: uppercase;
}
.stat-value {
color: #000000;
font-size: 25px;
font-weight: 700;
}
.stat-diff {
font-size: 14px;
font-weight: 500;
}
.stat-diff.positive {
color: #339e75;
}
span.stat-diff.negative {
color: #e2454f;
}
.stat-diff.no-diff {
color: #999ba7;
}
.diff-sign {
font-size: 10px;
}
.stats-2 {
margin: 50px 0 24px;
}
.stats-2 td.col-1, .stats-2 td.col-2 {
border-right: 3px solid #f7f9fb;
}
.stats-2 td.col-2, .stats-2 td.col-3 {
padding-left: 40px;
}
.cta {
margin-bottom: 0;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%;
}
.btn > tbody > tr > td {
padding-bottom: 48px;
text-align: center;
padding-top: 34px;
}
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
border: none;
border-radius: 31px;
box-sizing: border-box;
color: #59403b;
cursor: pointer;
display: inline-block;
font-size: 16px;
font-weight: 700;
margin: 0;
padding: 18px 44px;
text-decoration: none;
text-transform: capitalize;
background: rgb(47,166,129);
background: linear-gradient( 0deg, #f7d070 0%, #f7dc6f 100%);
letter-spacing: 0.7px;
}
.btn-primary table td {
background-color: #3498db;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
}
hr {
border: 0;
border-bottom: 1px solid #F0F4F8;
margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.rankmath-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
<?php $this->template_part( 'pro-style' ); ?>
@@ -0,0 +1 @@
<?php // Silence is golden.
@@ -0,0 +1,165 @@
<?php
/**
* Search console options.
*
* @package Rank_Math
*/
use RankMath\KB;
use RankMath\Helper;
use RankMath\Analytics\DB;
use RankMath\Helpers\Str;
use RankMath\Google\Authentication;
defined( 'ABSPATH' ) || exit;
// phpcs:disable
$actions = \as_get_scheduled_actions(
[
'hook' => 'rank_math/analytics/clear_cache',
'status' => \ActionScheduler_Store::STATUS_PENDING,
]
);
$db_info = DB::info();
$is_queue_empty = empty( $actions );
$disable = ( ! Authentication::is_authorized() || ! $is_queue_empty ) ? true : false;
if ( ! empty( $db_info ) ) {
$db_info = [
/* translators: number of days */
'<div class="rank-math-console-db-info"><i class="rm-icon rm-icon-calendar"></i> ' . sprintf( esc_html__( 'Storage Days: %s', 'rank-math' ), '<strong>' . $db_info['days'] . '</strong>' ) . '</div>',
/* translators: number of rows */
'<div class="rank-math-console-db-info"><i class="rm-icon rm-icon-faq"></i> ' . sprintf( esc_html__( 'Data Rows: %s', 'rank-math' ), '<strong>' . Str::human_number( $db_info['rows'] ) . '</strong>' ) . '</div>',
/* translators: database size */
'<div class="rank-math-console-db-info"><i class="rm-icon rm-icon-database"></i> ' . sprintf( esc_html__( 'Size: %s', 'rank-math' ), '<strong>' . size_format( $db_info['size'] ) . '</strong>' ) . '</div>',
];
}
$actions = as_get_scheduled_actions(
[
'order' => 'DESC',
'hook' => 'rank_math/analytics/data_fetch',
'status' => \ActionScheduler_Store::STATUS_PENDING,
]
);
if ( Authentication::is_authorized() && ! empty( $actions ) ) {
$action = current( $actions );
$schedule = $action->get_schedule();
$next_date = $schedule->get_date();
if ( $next_date ) {
$cmb->add_field(
[
'id' => 'console_data_empty',
'type' => 'raw',
/* translators: date */
'content' => sprintf(
'<span class="next-fetch">' . __( 'Next data fetch on %s', 'rank-math' ),
date_i18n( 'd M, Y H:m:i', $next_date->getTimestamp() ) . '</span>'
),
]
);
}
}
// phpcs:enable
$cmb->add_field(
[
'id' => 'search_console_ui',
'type' => 'raw',
'file' => rank_math()->admin_dir() . '/wizard/views/search-console-ui.php',
]
);
if ( ! Authentication::is_authorized() ) {
return;
}
$is_fetching = 'fetching' === get_option( 'rank_math_analytics_first_fetch' );
$buttons = '<br>' .
'<button class="button button-small console-cache-delete" data-days="-1">' . esc_html__( 'Delete data', 'rank-math' ) . '</button>' .
'&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 );
@@ -0,0 +1,210 @@
<?php
/**
* Workflow Base.
*
* @since 1.0.54
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics\Workflow;
use RankMath\Helper;
use function has_filter;
use RankMath\Analytics\DB;
use RankMath\Traits\Hooker;
use function as_schedule_single_action;
defined( 'ABSPATH' ) || exit;
/**
* Base class.
*/
abstract class Base {
use Hooker;
/**
* Start fetching process.
*
* @param integer $days Number of days to fetch from past.
* @param string $action Action to perform.
* @return integer
*/
public function create_jobs( $days = 90, $action = 'console' ) {
$count = $this->add_data_pull( $days + 3, $action );
$time_gap = $this->get_schedule_gap();
Workflow::add_clear_cache( time() + ( $time_gap * ( $count + 1 ) ) );
update_option( 'rank_math_analytics_first_fetch', 'fetching' );
return $count;
}
/**
* Add data pull jobs.
*
* @param integer $days Number of days to fetch from past.
* @param string $action Action to perform.
* @return integer
*/
private function add_data_pull( $days, $action = 'console' ) {
$count = 1;
$start = Helper::get_midnight( time() + DAY_IN_SECONDS );
$interval = $this->get_data_interval();
$time_gap = $this->get_schedule_gap();
$hook = "get_{$action}_data";
if ( 1 === $interval ) {
for ( $current = 1; $current <= $days; $current++ ) {
$date = date_i18n( 'Y-m-d', $start - ( DAY_IN_SECONDS * $current ) );
if ( ! DB::date_exists( $date, $action ) ) {
$count++;
as_schedule_single_action(
time() + ( $time_gap * $count ),
'rank_math/analytics/' . $hook,
[ $date ],
'rank-math'
);
}
}
} else {
for ( $current = 1; $current <= $days; $current = $current + $interval ) {
for ( $j = 0; $j < $interval; $j++ ) {
$date = date_i18n( 'Y-m-d', $start - ( DAY_IN_SECONDS * ( $current + $j ) ) );
if ( ! DB::date_exists( $date, $action ) ) {
$count++;
as_schedule_single_action(
time() + ( $time_gap * $count ),
'rank_math/analytics/' . $hook,
[ $date ],
'rank-math'
);
}
}
}
}
return $count;
}
/**
* Get data interval.
*
* @return int
*/
private function get_data_interval() {
$is_custom = has_filter( 'rank_math/analytics/app_url' );
return $is_custom ? $this->do_filter( 'analytics/data_interval', 7 ) : 7;
}
/**
* Get schedule gap.
*
* @return int
*/
private function get_schedule_gap() {
return $this->do_filter( 'analytics/schedule_gap', 30 );
}
/**
* Check if google profile is updated.
*
* @param string $param Google profile param name.
* @param string $prev Previous profile data.
* @param string $new New posted profile data.
*
* @return boolean
*/
public function is_profile_updated( $param, $prev, $new ) {
if (
! is_null( $prev ) &&
! is_null( $new ) &&
isset( $prev[ $param ] ) &&
isset( $new[ $param ] ) &&
$prev[ $param ] === $new[ $param ]
) {
return false;
}
return true;
}
/**
* Function to get the dates.
*
* @param int $days Number of days.
*
* @return array
*/
public static function get_dates( $days = 90 ) {
$end = Helper::get_midnight( strtotime( '-1 day', time() ) );
$start = strtotime( '-' . $days . ' day', $end );
return [
'start_date' => date_i18n( 'Y-m-d', $start ),
'end_date' => date_i18n( 'Y-m-d', $end ),
];
}
/**
* Schedule single action
*
* @param int $days Number of days.
* @param string $action Name of the action hook.
* @param array $args Arguments to pass to callbacks when the hook triggers.
* @param string $group The group to assign this job to.
* @param boolean $unique Whether the action should be unique.
*/
public function schedule_single_action( $days = 90, $action = '', $args = [], $group = 'rank-math', $unique = false ) {
$timestamp = get_option( 'rank_math_analytics_last_single_action_schedule_time', time() );
$time_gap = $this->get_schedule_gap();
$dates = self::get_dates( $days );
// Get the analytics dates in which analytics data is actually available.
$days = apply_filters(
'rank_math/analytics/get_' . $action . '_days',
[
'start_date' => $dates['start_date'],
'end_date' => $dates['end_date'],
]
);
// No days then don't schedule the action.
if ( empty( $days ) ) {
return;
}
foreach ( $days as $day ) {
// Next schedule time.
$timestamp = $timestamp + $time_gap;
$args = wp_parse_args(
[
'start_date' => $day['start_date'],
'end_date' => $day['end_date'],
],
$args
);
as_schedule_single_action(
$timestamp,
'rank_math/analytics/get_' . $action . '_data',
$args,
$group,
$unique
);
}
Workflow::add_clear_cache( $timestamp );
// Update timestamp.
update_option( 'rank_math_analytics_last_single_action_schedule_time', $timestamp );
}
}
@@ -0,0 +1,112 @@
<?php
/**
* Google Search Console.
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\Analytics
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics\Workflow;
use Exception;
use RankMath\Helpers\DB;
use RankMath\Google\Console as GoogleConsole;
use function as_unschedule_all_actions;
defined( 'ABSPATH' ) || exit;
/**
* Console class.
*/
class Console extends Base {
/**
* Constructor.
*/
public function __construct() {
$this->create_tables();
// If console is not connected, ignore all no need to proceed.
if ( ! GoogleConsole::is_console_connected() ) {
return;
}
$this->action( 'rank_math/analytics/workflow/console', 'kill_jobs', 5, 0 );
$this->action( 'rank_math/analytics/workflow/create_tables', 'create_tables' );
$this->action( 'rank_math/analytics/workflow/console', 'create_tables', 6, 0 );
$this->action( 'rank_math/analytics/workflow/console', 'create_data_jobs', 10, 3 );
}
/**
* Unschedule all console data fetch action.
*
* Stop processing queue items, clear cronjob and delete all batches.
*/
public function kill_jobs() {
as_unschedule_all_actions( 'rank_math/analytics/get_console_data' );
}
/**
* Create tables.
*/
public function create_tables() {
global $wpdb;
$collate = $wpdb->get_charset_collate();
$table = 'rank_math_analytics_gsc';
// Early Bail!!
if ( DB::check_table_exists( $table ) ) {
return;
}
$schema = "CREATE TABLE {$wpdb->prefix}{$table} (
id bigint(20) unsigned NOT NULL auto_increment,
created timestamp NOT NULL,
query varchar(1000) NOT NULL,
page varchar(500) NOT NULL,
clicks mediumint(6) NOT NULL,
impressions mediumint(6) NOT NULL,
position double NOT NULL,
ctr double NOT NULL,
PRIMARY KEY (id),
KEY analytics_query (query(190)),
KEY analytics_page (page(190)),
KEY clicks (clicks),
KEY rank_position (position)
) $collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
try {
dbDelta( $schema );
} catch ( Exception $e ) { // phpcs:ignore
// Will log.
}
// Make sure that collations match the objects table.
$objects_coll = DB::get_table_collation( 'rank_math_analytics_objects' );
DB::check_collation( $table, 'all', $objects_coll );
}
/**
* Create jobs to fetch data.
*
* @param integer $days Number of days to fetch from past.
* @param string $prev Previous saved value.
* @param string $new New posted value.
*/
public function create_data_jobs( $days, $prev, $new ) {
// Early bail if saved & new profile are same.
if ( ! $this->is_profile_updated( 'profile', $prev, $new ) ) {
return;
}
update_option( 'rank_math_analytics_first_fetch', 'fetching' );
// Fetch now.
$this->schedule_single_action( $days, 'console' );
}
}
@@ -0,0 +1,162 @@
<?php
/**
* Google Search Console.
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\Analytics
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics\Workflow;
use Exception;
use RankMath\Helpers\DB;
use RankMath\Traits\Hooker;
use RankMath\Analytics\DB as AnalyticsDB;
use RankMath\Analytics\Url_Inspection;
use RankMath\Google\Console;
use function as_unschedule_all_actions;
defined( 'ABSPATH' ) || exit;
/**
* Inspections class.
*/
class Inspections {
use Hooker;
/**
* API Limit.
* 600 requests per minute, 2000 per day.
* We can ignore the per-minute limit, since we will use a few seconds delay after each request.
*/
const API_LIMIT = 2000;
/**
* Interval between requests.
*/
const REQUEST_GAP_SECONDS = 7;
/**
* Constructor.
*/
public function __construct() {
$this->create_tables();
// If console is not connected, ignore all, no need to proceed.
if ( ! Console::is_console_connected() ) {
return;
}
$this->action( 'rank_math/analytics/workflow/create_tables', 'create_tables' );
$this->action( 'rank_math/analytics/workflow/inspections', 'create_tables', 6, 0 );
$this->action( 'rank_math/analytics/workflow/inspections', 'create_data_jobs', 10, 0 );
}
/**
* Unschedule all inspections data fetch action.
*
* Stop processing queue items, clear cronjob and delete all batches.
*/
public static function kill_jobs() {
as_unschedule_all_actions( 'rank_math/analytics/get_inspections_data' );
}
/**
* Create tables.
*/
public function create_tables() {
global $wpdb;
$collate = $wpdb->get_charset_collate();
$table = 'rank_math_analytics_inspections';
// Early Bail!!
if ( DB::check_table_exists( $table ) ) {
return;
}
$schema = "CREATE TABLE {$wpdb->prefix}{$table} (
id bigint(20) unsigned NOT NULL auto_increment,
page varchar(500) NOT NULL,
created timestamp NOT NULL,
index_verdict varchar(64) NOT NULL, /* PASS, PARTIAL, FAIL, NEUTRAL, VERDICT_UNSPECIFIED */
indexing_state varchar(64) NOT NULL, /* INDEXING_ALLOWED, BLOCKED_BY_META_TAG, BLOCKED_BY_HTTP_HEADER, BLOCKED_BY_ROBOTS_TXT, INDEXING_STATE_UNSPECIFIED */
coverage_state text NOT NULL, /* String, e.g. 'Submitted and indexed'. */
page_fetch_state varchar(64) NOT NULL, /* SUCCESSFUL, SOFT_404, BLOCKED_ROBOTS_TXT, NOT_FOUND, ACCESS_DENIED, SERVER_ERROR, REDIRECT_ERROR, ACCESS_FORBIDDEN, BLOCKED_4XX, INTERNAL_CRAWL_ERROR, INVALID_URL, PAGE_FETCH_STATE_UNSPECIFIED */
robots_txt_state varchar(64) NOT NULL, /* ALLOWED, DISALLOWED, ROBOTS_TXT_STATE_UNSPECIFIED */
mobile_usability_verdict varchar(64) NOT NULL, /* PASS, PARTIAL, FAIL, NEUTRAL, VERDICT_UNSPECIFIED */
mobile_usability_issues longtext NOT NULL, /* JSON */
rich_results_verdict varchar(64) NOT NULL, /* PASS, PARTIAL, FAIL, NEUTRAL, VERDICT_UNSPECIFIED */
rich_results_items longtext NOT NULL, /* JSON */
last_crawl_time timestamp NOT NULL,
crawled_as varchar(64) NOT NULL, /* DESKTOP, MOBILE, CRAWLING_USER_AGENT_UNSPECIFIED */
google_canonical text NOT NULL, /* Google-chosen canonical URL. */
user_canonical text NOT NULL, /* Canonical URL declared on-page. */
sitemap text NOT NULL, /* Sitemap URL. */
referring_urls longtext NOT NULL, /* JSON */
raw_api_response longtext NOT NULL, /* JSON */
PRIMARY KEY (id),
KEY analytics_object_page (page(190)),
KEY created (created),
KEY index_verdict (index_verdict),
KEY page_fetch_state (page_fetch_state),
KEY robots_txt_state (robots_txt_state),
KEY mobile_usability_verdict (mobile_usability_verdict),
KEY rich_results_verdict (rich_results_verdict)
) $collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
try {
dbDelta( $schema );
} catch ( Exception $e ) { // phpcs:ignore
// Will log.
}
// Make sure that collations match the objects table.
$objects_coll = DB::get_table_collation( 'rank_math_analytics_objects' );
DB::check_collation( $table, 'all', $objects_coll );
}
/**
* Create jobs to fetch data.
*/
public function create_data_jobs() {
// If there are jobs left from the previous queue, don't create new jobs.
if ( as_has_scheduled_action( 'rank_math/analytics/get_inspections_data' ) ) {
return;
}
// If the option is disabled, don't create jobs.
if ( ! Url_Inspection::is_enabled() ) {
return;
}
$inspections_table = AnalyticsDB::inspections()->table;
$objects_table = AnalyticsDB::objects()->table;
$objects = AnalyticsDB::objects()
->select( [ "$objects_table.id", "$objects_table.page", "$inspections_table.created" ] )
->leftJoin( $inspections_table, "$inspections_table.page", "$objects_table.page" )
->where( "$objects_table.is_indexable", 1 )
->orderBy( "$inspections_table.created", 'ASC' )
->get();
$count = 0;
foreach ( $objects as $object ) {
$count++;
$time = time() + ( $count * self::REQUEST_GAP_SECONDS );
if ( $count > self::API_LIMIT ) {
$delay_days = floor( $count / self::API_LIMIT );
$time = strtotime( "+{$delay_days} days", $time );
}
as_schedule_single_action( $time, 'rank_math/analytics/get_inspections_data', [ $object->page ], 'rank-math' );
}
}
}
@@ -0,0 +1,322 @@
<?php
/**
* Jobs.
*
* @since 1.0.54
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics\Workflow;
use Exception;
use RankMath\Helper;
use RankMath\Google\Api;
use RankMath\Google\Console;
use RankMath\Google\Url_Inspection;
use RankMath\Analytics\DB;
use RankMath\Traits\Cache;
use RankMath\Traits\Hooker;
use RankMath\Analytics\Stats;
use RankMath\Analytics\Watcher;
defined( 'ABSPATH' ) || exit;
/**
* Jobs class.
*/
class Jobs {
use Hooker, Cache;
/**
* Main instance
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Jobs
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Jobs ) ) {
$instance = new Jobs();
$instance->hooks();
}
return $instance;
}
/**
* Hooks.
*/
public function hooks() {
$this->action( 'rank_math/analytics/flat_posts', 'do_flat_posts' );
$this->action( 'rank_math/analytics/flat_posts_completed', 'flat_posts_completed' );
add_action( 'rank_math/analytics/sync_sitemaps', [ Api::get(), 'sync_sitemaps' ] );
if ( Console::is_console_connected() ) {
$this->action( 'rank_math/analytics/clear_cache', 'clear_cache', 99 );
// Fetch missing google data action.
$this->action( 'rank_math/analytics/data_fetch', 'data_fetch' );
// Console data fetch.
$this->filter( 'rank_math/analytics/get_console_days', 'get_console_days' );
$this->action( 'rank_math/analytics/get_console_data', 'get_console_data' );
$this->action( 'rank_math/analytics/handle_console_response', 'handle_console_response' );
// Inspections data fetch.
$this->action( 'rank_math/analytics/get_inspections_data', 'get_inspections_data' );
}
}
/**
* Fetch missing console data.
*/
public function data_fetch() {
$this->check_for_missing_dates( 'console' );
}
/**
* Perform post check.
*/
public function flat_posts_completed() {
$rows = DB::objects()
->selectCount( 'id' )
->getVar();
Workflow::kill_workflows();
}
/**
* Add/update posts info from objects table.
*
* @param array $ids Posts ids to process.
*/
public function do_flat_posts( $ids ) {
Inspections::kill_jobs();
foreach ( $ids as $id ) {
Watcher::get()->update_post_info( $id );
}
}
/**
* Clear cache.
*/
public function clear_cache() {
global $wpdb;
// Delete all useless data from console data table.
$wpdb->get_results( "DELETE FROM {$wpdb->prefix}rank_math_analytics_gsc WHERE page NOT IN ( SELECT page from {$wpdb->prefix}rank_math_analytics_objects )" );
// Delete useless data from inspections table too.
$wpdb->get_results( "DELETE FROM {$wpdb->prefix}rank_math_analytics_inspections WHERE page NOT IN ( SELECT page from {$wpdb->prefix}rank_math_analytics_objects )" );
delete_transient( 'rank_math_analytics_data_info' );
DB::purge_cache();
DB::delete_data_log();
$this->calculate_stats();
update_option( 'rank_math_analytics_last_updated', time() );
Workflow::do_workflow( 'inspections' );
}
/**
* Set the console start and end dates.
*
* @param array $args Args containing start and end date.
*/
public function get_console_days( $args = [] ) {
set_time_limit( 300 );
$rows = Api::get()->get_search_analytics(
[
'start_date' => $args['start_date'],
'end_date' => $args['end_date'],
'dimensions' => [ 'date' ],
]
);
if ( empty( $rows ) || is_wp_error( $rows ) ) {
return [];
}
$empty_dates = get_option( 'rank_math_console_empty_dates', [] );
$dates = [];
foreach ( $rows as $row ) {
// Have at least few impressions.
if ( $row['impressions'] ) {
$date = $row['keys'][0];
if ( ! DB::date_exists( $date, 'console' ) && ! in_array( $date, $empty_dates, true ) ) {
$dates[] = [
'start_date' => $date,
'end_date' => $date,
];
}
}
}
return $dates;
}
/**
* Get console data.
*
* @param string $date Date to fetch data for.
*/
public function get_console_data( $date ) {
set_time_limit( 300 );
$rows = Api::get()->get_search_analytics(
[
'start_date' => $date,
'end_date' => $date,
'dimensions' => [ 'query', 'page' ],
]
);
if ( empty( $rows ) || is_wp_error( $rows ) ) {
return;
}
$rows = \array_map( [ $this, 'normalize_query_page_data' ], $rows );
try {
DB::add_query_page_bulk( $date, $rows );
// Clear the cache here.
$this->cache_flush_group( 'rank_math_rest_keywords_rows' );
$this->cache_flush_group( 'rank_math_posts_rows_by_objects' );
$this->cache_flush_group( 'rank_math_analytics_summary' );
return $rows;
} catch ( Exception $e ) {} // phpcs:ignore
}
/**
* Handlle console response.
*
* @param array $data API request and response data.
*/
public function handle_console_response( $data = [] ) {
if ( 200 !== $data['code'] ) {
return;
}
if ( isset( $data['formatted_response']['rows'] ) && ! empty( $data['formatted_response']['rows'] ) ) {
return;
}
if ( ! isset( $data['args']['startDate'] ) ) {
return;
}
$dates = get_option( 'rank_math_console_empty_dates', [] );
if ( ! $dates ) {
$dates = [];
}
$dates[] = $data['args']['startDate'];
$dates[] = $data['args']['endDate'];
$dates = array_unique( $dates );
update_option( 'rank_math_console_empty_dates', $dates );
}
/**
* Get inspection results from the API and store them in the database.
*
* @param string $page URI to fetch data for.
*/
public function get_inspections_data( $page ) {
// If the option is disabled, don't fetch data.
if ( ! \RankMath\Analytics\Url_Inspection::is_enabled() ) {
return;
}
$inspection = Url_Inspection::get()->get_inspection_data( $page );
if ( empty( $inspection ) ) {
return;
}
try {
DB::store_inspection( $inspection );
} catch ( Exception $e ) {} // phpcs:ignore
}
/**
* Check for missing dates.
*
* @param string $action Action to perform.
*/
public function check_for_missing_dates( $action ) {
$days = Helper::get_settings( 'general.console_caching_control', 90 );
Workflow::do_workflow(
$action,
$days,
null,
null
);
}
/**
* Calculate stats.
*/
private function calculate_stats() {
$ranges = [
'-7 days',
'-15 days',
'-30 days',
'-3 months',
'-6 months',
'-1 year',
];
foreach ( $ranges as $range ) {
Stats::get()->set_date_range( $range );
Stats::get()->get_top_keywords();
}
}
/**
* Normalize console data.
*
* @param array $row Single row item.
*
* @return array
*/
protected function normalize_query_page_data( $row ) {
$row = $this->normalize_data( $row );
$row['query'] = $row['keys'][0];
$row['page'] = $row['keys'][1];
unset( $row['keys'] );
return $row;
}
/**
* Normalize console data.
*
* @param array $row Single row item.
*
* @return array
*/
private function normalize_data( $row ) {
$row['ctr'] = round( $row['ctr'] * 100, 2 );
$row['position'] = round( $row['position'], 2 );
return $row;
}
}
@@ -0,0 +1,176 @@
<?php
/**
* Authentication workflow.
*
* @since 1.0.55
* @package RankMath
* @subpackage RankMath\Analytics
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics\Workflow;
use RankMath\Helper;
use RankMath\Google\Api;
use RankMath\Traits\Hooker;
use RankMath\Helpers\Str;
use RankMath\Helpers\Param;
use RankMath\Helpers\Security;
use RankMath\Analytics\DB;
use RankMath\Google\Permissions;
use RankMath\Google\Authentication;
defined( 'ABSPATH' ) || exit;
/**
* OAuth class.
*/
class OAuth {
use Hooker;
/**
* Constructor.
*/
public function __construct() {
$this->action( 'admin_init', 'process_oauth' );
$this->action( 'admin_init', 'reconnect_google' );
}
/**
* OAuth reply back
*/
public function process_oauth() {
$process_oauth = Param::get( 'process_oauth', 0, FILTER_VALIDATE_INT );
$access_token = Param::get( 'access_token', '', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
$security = Param::get( 'rankmath_security', '', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK );
// Early Bail!!
if ( empty( $security ) || ( $process_oauth < 1 && empty( $access_token ) ) ) {
return;
}
if ( ! wp_verify_nonce( $security, 'rank_math_oauth_token' ) ) {
wp_nonce_ays( 'rank_math_oauth_token' );
die();
}
$redirect = false;
// Backward compatibility.
if ( ! empty( $process_oauth ) ) {
$redirect = $this->get_tokens_from_server();
}
// New version.
if ( ! empty( $access_token ) ) {
$redirect = $this->get_tokens_from_url();
}
// Remove possible admin notice if we have new access token.
delete_option( 'rankmath_google_api_failed_attempts_data' );
delete_option( 'rankmath_google_api_reconnect' );
Permissions::fetch();
if ( ! empty( $redirect ) ) {
Helper::redirect( $redirect );
exit;
}
}
/**
* Reconnect Google.
*/
public function reconnect_google() {
if ( ! isset( $_GET['reconnect'] ) || 'google' !== $_GET['reconnect'] ) {
return;
}
if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'rank_math_reconnect_google' ) ) {
wp_nonce_ays( 'rank_math_reconnect_google' );
die();
}
if ( ! Helper::has_cap( 'analytics' ) ) {
return;
}
$rows = DB::objects()
->selectCount( 'id' )
->getVar();
if ( empty( $rows ) ) {
delete_option( 'rank_math_analytics_installed' );
}
Api::get()->revoke_token();
Workflow::kill_workflows();
wp_redirect( Authentication::get_auth_url() ); // phpcs:ignore
die();
}
/**
* Get access token from url.
*
* @return string
*/
private function get_tokens_from_url() {
$data = [
'access_token' => urldecode( Param::get( 'access_token', '', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ) ),
'refresh_token' => urldecode( Param::get( 'refresh_token', '', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ) ),
'expire' => urldecode( Param::get( 'expire', 0, FILTER_VALIDATE_INT ) ),
];
Authentication::tokens( $data );
$current_request = remove_query_arg(
[
'access_token',
'refresh_token',
'expire',
'security',
]
);
return $current_request;
}
/**
* Get access token from rankmath server.
*
* @return string
*/
private function get_tokens_from_server() {
// Bail if the user is not authenticated at all yet.
$id = Param::get( 'process_oauth', 0, FILTER_VALIDATE_INT );
if ( $id < 1 ) {
return;
}
$response = wp_remote_get( Authentication::get_auth_app_url() . '/get.php?id=' . $id );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return;
}
$response = wp_remote_retrieve_body( $response );
if ( empty( $response ) ) {
return;
}
$response = \json_decode( $response, true );
unset( $response['id'] );
// Save new token.
Authentication::tokens( $response );
$redirect = Security::remove_query_arg_raw( [ 'process_oauth', 'security' ] );
if ( Str::contains( 'rank-math-options-general', $redirect ) ) {
$redirect .= '#setting-panel-analytics';
}
Helper::remove_notification( 'rank_math_analytics_reauthenticate' );
return $redirect;
}
}
@@ -0,0 +1,152 @@
<?php
/**
* Install objects.
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics\Workflow;
use Exception;
use RankMath\Helper;
use RankMath\Helpers\DB;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Objects class.
*/
class Objects extends Base {
use Hooker;
/**
* Constructor.
*/
public function __construct() {
$done = \boolval( get_option( 'rank_math_analytics_installed' ) );
if ( $done ) {
return;
}
$this->create_tables();
$this->create_data_job();
$this->flat_posts();
update_option( 'rank_math_analytics_installed', true );
}
/**
* Create tables.
*/
public function create_tables() {
global $wpdb;
$collate = $wpdb->get_charset_collate();
$table = 'rank_math_analytics_objects';
// Early Bail!!
if ( DB::check_table_exists( $table ) ) {
return;
}
$schema = "CREATE TABLE {$wpdb->prefix}{$table} (
id bigint(20) unsigned NOT NULL auto_increment,
created timestamp NOT NULL,
title text NOT NULL,
page varchar(500) NOT NULL,
object_type varchar(100) NOT NULL,
object_subtype varchar(100) NOT NULL,
object_id bigint(20) unsigned NOT NULL,
primary_key varchar(255) NOT NULL,
seo_score tinyint NOT NULL default 0,
page_score tinyint NOT NULL default 0,
is_indexable tinyint(1) NOT NULL default 1,
schemas_in_use varchar(500),
desktop_interactive double default 0,
desktop_pagescore double default 0,
mobile_interactive double default 0,
mobile_pagescore double default 0,
pagespeed_refreshed timestamp,
PRIMARY KEY (id),
KEY analytics_object_page (page(190))
) $collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
try {
dbDelta( $schema );
} catch ( Exception $e ) { // phpcs:ignore
// Will log.
}
}
/**
* Create jobs to fetch data.
*/
public function create_data_job() {
// Clear old schedule.
wp_clear_scheduled_hook( 'rank_math/analytics/get_analytics' );
// Add action for scheduler.
$task_name = 'rank_math/analytics/data_fetch';
$fetch_gap = apply_filters( 'rank_math/analytics/fetch_gap', 7, 'objects' );
// Schedule new action only when there is no existing action.
if ( false === as_next_scheduled_action( $task_name ) ) {
$schedule_in_minute = wp_rand( 3, defined( 'RANK_MATH_PRO_FILE' ) ? 1380 : 4320 );
$time_to_schedule = ( strtotime( 'tomorrow' ) + ( $schedule_in_minute * MINUTE_IN_SECONDS ) );
as_schedule_recurring_action(
$time_to_schedule,
DAY_IN_SECONDS * $fetch_gap,
$task_name,
[],
'rank-math'
);
}
}
/**
* Flat posts
*/
public function flat_posts() {
$post_types = $this->do_filter( 'analytics/post_types', Helper::get_accessible_post_types() );
unset( $post_types['attachment'] );
$ids = get_posts(
[
'post_type' => array_keys( $post_types ),
'post_status' => 'publish',
'fields' => 'ids',
'posts_per_page' => -1,
]
);
$counter = 0;
$chunks = \array_chunk( $ids, 50 );
foreach ( $chunks as $chunk ) {
$counter++;
as_schedule_single_action(
time() + ( 60 * ( $counter / 2 ) ),
'rank_math/analytics/flat_posts',
[ $chunk ],
'rank-math'
);
}
// Check for posts.
as_schedule_single_action(
time() + ( 60 * ( ( $counter + 1 ) / 2 ) ),
'rank_math/analytics/flat_posts_completed',
[],
'rank-math'
);
// Clear cache.
Workflow::add_clear_cache( time() + ( 60 * ( ( $counter + 2 ) / 2 ) ) );
}
}
@@ -0,0 +1,161 @@
<?php
/**
* Workflow.
*
* @since 1.0.54
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics\Workflow;
use RankMath\Traits\Hooker;
use function as_enqueue_async_action;
use function as_unschedule_all_actions;
defined( 'ABSPATH' ) || exit;
/**
* Workflow class.
*/
class Workflow {
use Hooker;
/**
* Main instance
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Workflow
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Workflow ) ) {
$instance = new Workflow();
$instance->hooks();
}
return $instance;
}
/**
* Hooks.
*/
public function hooks() {
// Common.
$this->action( 'rank_math/analytics/workflow', 'maybe_first_install', 5, 0 );
$this->action( 'rank_math/analytics/workflow', 'start_workflow', 10, 4 );
$this->action( 'rank_math/analytics/workflow/create_tables', 'create_tables_only', 5 );
// Console.
$this->action( 'rank_math/analytics/workflow/console', 'init_console_workflow', 5, 0 );
// Inspections.
$this->action( 'rank_math/analytics/workflow/inspections', 'init_inspections_workflow', 5, 0 );
}
/**
* Maybe first install.
*/
public function maybe_first_install() {
new Objects();
}
/**
* Init Console workflow
*/
public function init_console_workflow() {
new Console();
}
/**
* Init Inspections workflow.
*/
public function init_inspections_workflow() {
new Inspections();
}
/**
* Create tables only.
*/
public function create_tables_only() {
( new Objects() )->create_tables();
( new Inspections() )->create_tables();
new Console();
}
/**
* Service workflow
*
* @param string $action Action to perform.
* @param integer $days Number of days to fetch from past.
* @param string $prev Previous saved value.
* @param string $new New posted value.
*/
public function start_workflow( $action, $days = 0, $prev = null, $new = null ) {
do_action(
'rank_math/analytics/workflow/' . $action,
$days,
$prev,
$new
);
}
/**
* Service workflow
*
* @param string $action Action to perform.
* @param integer $days Number of days to fetch from past.
* @param string $prev Previous saved value.
* @param string $new New posted value.
*/
public static function do_workflow( $action, $days = 0, $prev = null, $new = null ) {
as_enqueue_async_action(
'rank_math/analytics/workflow',
[
$action,
$days,
$prev,
$new,
],
'rank-math'
);
}
/**
* Kill all workflows
*
* Stop processing queue items, clear cronjob and delete all batches.
*/
public static function kill_workflows() {
as_unschedule_all_actions( 'rank_math/analytics/workflow' );
as_unschedule_all_actions( 'rank_math/analytics/clear_cache' );
as_unschedule_all_actions( 'rank_math/analytics/get_console_data' );
as_unschedule_all_actions( 'rank_math/analytics/get_analytics_data' );
as_unschedule_all_actions( 'rank_math/analytics/get_adsense_data' );
as_unschedule_all_actions( 'rank_math/analytics/get_inspections_data' );
do_action( 'rank_math/analytics/clear_cache' );
}
/**
* Add clear cache job.
*
* @param int $time Timestamp to add job for.
*/
public static function add_clear_cache( $time ) {
as_unschedule_all_actions( 'rank_math/analytics/clear_cache' );
as_schedule_single_action(
$time,
'rank_math/analytics/clear_cache',
[],
'rank-math'
);
delete_option( 'rank_math_analytics_last_single_action_schedule_time' );
}
}
@@ -0,0 +1,53 @@
<?php
/**
* The admin-side code for the BuddyPress module.
*
* @since 1.0.32
* @package RankMath
* @subpackage RankMath\BuddyPress
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\BuddyPress;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Admin class
*/
class Admin {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->filter( 'rank_math/settings/title', 'add_title_settings' );
}
/**
* Add new tab in the Titles & Meta settings for the BuddyPress module.
*
* @param array $tabs Array of option panel tabs.
*
* @return array
*/
public function add_title_settings( $tabs ) {
$tabs['buddypress'] = [
'title' => esc_html__( 'BuddyPress:', 'rank-math' ),
'type' => 'seprator',
];
$tabs['buddypress-groups'] = [
'icon' => 'rm-icon rm-icon-users',
'title' => esc_html__( 'Groups', 'rank-math' ),
'desc' => esc_html__( 'This tab contains SEO options for BuddyPress Group pages.', 'rank-math' ),
'file' => dirname( __FILE__ ) . '/views/options-titles.php',
];
return $tabs;
}
}
@@ -0,0 +1,173 @@
<?php
/**
* The BuddyPress Module.
*
* @since 1.0.32
* @package RankMath
* @subpackage RankMath\BuddyPress
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\BuddyPress;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* BuddyPress class.
*/
class BuddyPress {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
if ( is_admin() ) {
new Admin();
}
$this->filter( 'rank_math/paper/hash', 'paper' );
$this->action( 'rank_math/vars/register_extra_replacements', 'register_replacements' );
$this->filter( 'rank_math/json_ld', 'json_ld', 11 );
$this->filter( 'rank_math/frontend/title', 'change_activate_title' );
}
/**
* Filter to change the Activate page title.
*
* @param string $title Page title.
*/
public function change_activate_title( $title ) {
if ( function_exists( 'bp_is_current_component' ) && bp_is_current_component( 'activate' ) ) {
return false;
}
return $title;
}
/**
* Add BuddyPress class.
*
* @param array $hash Paper Hash.
*/
public function paper( $hash ) {
$bp_data = [
'BP_User' => bp_is_user(),
'BP_Group' => ! is_singular() && bp_is_groups_component(),
];
return array_merge( $bp_data, $hash );
}
/**
* Collect data to output in JSON-LD.
*
* @param array $data An array of data to output in JSON-LD.
*/
public function json_ld( $data ) {
if ( ! bp_is_user() ) {
return $data;
}
if ( isset( $data['richSnippet'] ) ) {
unset( $data['richSnippet'] );
}
$user_id = bp_displayed_user_id();
$data['ProfilePage'] = [
'@type' => 'ProfilePage',
'@id' => get_author_posts_url( $user_id ),
'headline' => sprintf( 'About %s', get_the_author_meta( 'display_name', $user_id ) ),
'about' => [
'@type' => 'Person',
'name' => get_the_author_meta( 'display_name', $user_id ),
'url' => esc_url( bp_core_get_user_domain( $user_id ) ),
'description' => get_the_author_meta( 'description', $user_id ),
'image' => [
'@type' => 'ImageObject',
'url' => get_avatar_url( $user_id, 96 ),
'height' => 96,
'width' => 96,
],
],
];
return $data;
}
/**
* Register variable replacements for BuddyPress groups.
*/
public function register_replacements() {
rank_math_register_var_replacement(
'group_name',
[
'name' => esc_html__( 'Group name.', 'rank-math' ),
'description' => esc_html__( 'Group name of the current group', 'rank-math' ),
'variable' => 'group_name',
'example' => $this->get_group_name(),
],
[ $this, 'get_group_name' ]
);
rank_math_register_var_replacement(
'group_desc',
[
'name' => esc_html__( 'Group Description.', 'rank-math' ),
'description' => esc_html__( 'Group description of the current group', 'rank-math' ),
'variable' => 'group_desc',
'example' => $this->get_group_desc(),
],
[ $this, 'get_group_desc' ]
);
}
/**
* Retrieves the group name.
*
* @return string
*/
public function get_group_name() {
$group = $this->get_group();
if ( ! is_object( $group ) ) {
return '';
}
return $group->name;
}
/**
* Retrieves the group description.
*
* @return string
*/
public function get_group_desc() {
$group = $this->get_group();
if ( ! is_object( $group ) ) {
return '';
}
return $group->description;
}
/**
* Returns the group object when the current page is the group page.
*
* @return null|Object
*/
private function get_group() {
if ( ! function_exists( 'groups_get_current_group' ) ) {
return '';
}
$group = groups_get_current_group();
if ( ! is_object( $group ) ) {
return '';
}
return $group;
}
}
@@ -0,0 +1,85 @@
<?php
/**
* The BuddyPress group class for the BuddyPress module.
*
* @since 1.0.32
* @package RankMath
* @subpackage RankMath\Paper
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Paper;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* BP_Group class.
*/
class BP_Group implements IPaper {
/**
* Retrieves the SEO title.
*
* @return string
*/
public function title() {
return Paper::get_from_options( 'bp_group_title' );
}
/**
* Retrieves the SEO description.
*
* @return string
*/
public function description() {
return Paper::get_from_options( 'bp_group_description' );
}
/**
* Retrieves the robots meta value.
*
* @return string
*/
public function robots() {
$robots = [];
if ( Helper::get_settings( 'titles.bp_group_custom_robots' ) ) {
$robots = Helper::get_settings( 'titles.bp_group_robots' );
}
return Paper::robots_combine( $robots, true );
}
/**
* Retrieves the advanced robots meta values.
*
* @return array The advanced robots meta values for the group.
*/
public function advanced_robots() {
$robots = [];
if ( Helper::get_settings( 'titles.bp_group_custom_robots' ) ) {
$robots = Helper::get_settings( 'titles.bp_group_advanced_robots' );
}
return Paper::advanced_robots_combine( $robots, true );
}
/**
* Retrieves the default canonical URL.
*
* @return array
*/
public function canonical() {
return '';
}
/**
* Retrieves the default meta keywords.
*
* @return string
*/
public function keywords() {
return '';
}
}
@@ -0,0 +1,93 @@
<?php
/**
* The BuddyPress user class for the BuddyPress module.
*
* @since 1.0.32
* @package RankMath
* @subpackage RankMath\Paper
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Paper;
use RankMath\User;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* BP_User class.
*/
class BP_User implements IPaper {
/**
* Retrieves the SEO title set in the user metabox.
*
* @return string The SEO title for the user.
*/
public function title() {
return '';
}
/**
* Retrieves the SEO description set in the user metabox.
*
* @return string The SEO description for the user.
*/
public function description() {
$description = User::get_meta( 'description', bp_displayed_user_id() );
if ( '' !== $description ) {
return $description;
}
return Paper::get_from_options( 'author_archive_description' );
}
/**
* Retrieves the robots set in the user metabox.
*
* @return string The robots for the specified user.
*/
public function robots() {
$robots = Paper::robots_combine( User::get_meta( 'robots', bp_displayed_user_id() ) );
if ( empty( $robots ) && Helper::get_settings( 'titles.author_custom_robots' ) ) {
$robots = Paper::robots_combine( Helper::get_settings( 'titles.author_robots' ), true );
}
return $robots;
}
/**
* Retrieves the advanced robots set in the user metabox.
*
* @return array The advanced robots for the specified user.
*/
public function advanced_robots() {
$robots = Paper::advanced_robots_combine( User::get_meta( 'advanced_robots', bp_displayed_user_id() ) );
if ( empty( $robots ) && Helper::get_settings( 'titles.author_custom_robots' ) ) {
$robots = Paper::advanced_robots_combine( Helper::get_settings( 'titles.author_advanced_robots' ), true );
}
return $robots;
}
/**
* Retrieves the default canonical URL.
*
* @return array
*/
public function canonical() {
return '';
}
/**
* Retrieves the default keywords.
*
* @return string
*/
public function keywords() {
return User::get_meta( 'focus_keyword', bp_displayed_user_id() );
}
}
@@ -0,0 +1,77 @@
<?php
/**
* The BuddyPress groups settings.
*
* @package RankMath
* @subpackage RankMath\Settings
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
$cmb->add_field(
[
'id' => 'bp_group_title',
'type' => 'text',
'name' => esc_html__( 'Group Title', 'rank-math' ),
'desc' => esc_html__( 'Title tag for groups', 'rank-math' ),
'classes' => 'rank-math-supports-variables rank-math-title',
'default' => '',
'sanitization_cb' => [ '\RankMath\CMB2', 'sanitize_textfield' ],
'attributes' => [ 'data-exclude-variables' => 'seo_title,seo_description' ],
]
);
$cmb->add_field(
[
'id' => 'bp_group_description',
'type' => 'textarea',
'name' => esc_html__( 'Group Description', 'rank-math' ),
'desc' => esc_html__( 'BuddyPress group description', 'rank-math' ),
'classes' => 'rank-math-supports-variables rank-math-description',
'attributes' => [
'data-exclude-variables' => 'seo_title,seo_description',
'rows' => 2,
],
]
);
$cmb->add_field(
[
'id' => 'bp_group_custom_robots',
'type' => 'toggle',
'name' => esc_html__( 'Group Robots Meta', 'rank-math' ),
'desc' => __( 'Select custom robots meta for Group archive pages. Otherwise the default meta will be used, as set in the Global Meta tab.', 'rank-math' ),
'options' => [
'off' => esc_html__( 'Default', 'rank-math' ),
'on' => esc_html__( 'Custom', 'rank-math' ),
],
'default' => $custom_default,
'classes' => 'rank-math-advanced-option',
]
);
$cmb->add_field(
[
'id' => 'bp_group_robots',
'type' => 'multicheck',
'name' => esc_html__( 'Group Robots Meta', 'rank-math' ),
'desc' => esc_html__( 'Custom values for robots meta tag on groups page.', 'rank-math' ),
'options' => Helper::choices_robots(),
'select_all_button' => false,
'dep' => [ [ 'bp_group_custom_robots', 'on' ] ],
'classes' => 'rank-math-advanced-option',
]
);
$cmb->add_field(
[
'id' => 'bp_group_advanced_robots',
'type' => 'advanced_robots',
'name' => esc_html__( 'Group Advanced Robots Meta', 'rank-math' ),
'sanitization_cb' => [ '\RankMath\CMB2', 'sanitize_advanced_robots' ],
'dep' => [ [ 'bp_group_custom_robots', 'on' ] ],
'classes' => 'rank-math-advanced-option',
]
);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#6c7781"><g fill="none" fill-rule="evenodd"><g transform="translate(1 1)" stroke-width="2"><circle stroke-opacity=".5" cx="18" cy="18" r="18"/><path d="M36 18c0-9.94-8.06-18-18-18"><animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="1s" repeatCount="indefinite"/></path></g></g></svg>

After

Width:  |  Height:  |  Size: 417 B

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,16 @@
<?php
/**
* Block script dependencies.
*
* @package RankMath
* @subpackage RankMath\ContentAI
* @author Rank Math <support@rankmath.com>
*/
return [
'dependencies' => [
'wp-element',
'wp-block-editor',
],
'version' => rank_math()->version,
];
@@ -0,0 +1 @@
!function(){"use strict";var e=wp.blocks,t=wp.blockEditor,n=wp.element,r=wp.i18n,a=function(){return!rankMath.isUserRegistered||!rankMath.contentAIPlan||!rankMath.contentAICredits||rankMath.contentAiMigrating},o=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":2,"name":"rank-math/command","title":"AI Assistant [Content AI]","icon":"star","category":"rank-math-blocks","description":"Generate content without any hassle, powered by Rank Math\'s Content AI.","keywords":["ai","content","rank math","rankmath","content ai","seo"],"textdomain":"rank-math-pro","editorScript":"file:../js/index.js","attributes":{"content":{"type":"string","source":"html","selector":"p","default":"<span>//</span>","__experimentalRole":"content"}}}');function c(e){return c="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},c(e)}function l(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function i(e,t,n){return(t=function(e){var t=function(e,t){if("object"!==c(e)||null===e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t||"default");if("object"!==c(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"===c(t)?t:String(t)}(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var m=o.name,u={icon:wp.element.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 17.08 18.02"},wp.element.createElement("g",{id:"Layer_2","data-name":"Layer 2"},wp.element.createElement("g",{id:"Layer_1-2","data-name":"Layer 1"},wp.element.createElement("path",{d:"M16.65,12.08a.83.83,0,0,0-1.11.35A7.38,7.38,0,1,1,9,1.63a7.11,7.11,0,0,1,.92.06A2.52,2.52,0,0,1,11,.23,8.87,8.87,0,0,0,9,0a9,9,0,1,0,8,13.19A.83.83,0,0,0,16.65,12.08Z"}),wp.element.createElement("path",{d:"M7.68,7.29A1.58,1.58,0,0,0,6.2,8.94a1.57,1.57,0,0,0,1.48,1.64A1.56,1.56,0,0,0,9.16,8.94,1.57,1.57,0,0,0,7.68,7.29Z"}),wp.element.createElement("path",{d:"M13.34,4.71a2.45,2.45,0,0,1-1,.2A2.53,2.53,0,0,1,9.93,3,7.18,7.18,0,0,0,9.12,3a6,6,0,1,0,4.22,1.73ZM10.53,11.3a.75.75,0,1,1-1.5,0v-.06a2.4,2.4,0,0,1-1.66.69,2.81,2.81,0,0,1-2.58-3,2.82,2.82,0,0,1,2.58-3A2.39,2.39,0,0,1,9,6.64a.75.75,0,0,1,1.5.07Zm2.56,0a.75.75,0,1,1-1.5,0V6.71a.75.75,0,1,1,1.5,0Z"}),wp.element.createElement("circle",{cx:"12.42",cy:"2.37",r:"1.45"})))),edit:function(e){var o=e.attributes,c=e.onReplace,l=e.setAttributes,i=o.content,m=(0,t.useBlockProps)({className:"rank-math-content-ai-command"}),u=(0,n.useRef)(null);return(0,n.useEffect)((function(){var e=u.current;e.focus();var t=document.createRange(),n=window.getSelection();t.selectNodeContents(e),t.collapse(!1),n.removeAllRanges(),n.addRange(t)}),[]),wp.element.createElement("div",m,wp.element.createElement(t.BlockControls,null),a()&&wp.element.createElement("div",{className:"rich-text",ref:u},rankMath.isUserRegistered?rankMath.contentAIPlan?wp.element.createElement(React.Fragment,null,(0,r.__)("You have exhausted your Content AI Credits.","rank-math"),wp.element.createElement("a",{href:"https://rankmath.com/kb/how-to-use-content-ai/?play-video=ioPeVIntJWw&utm_source=Plugin&utm_medium=Buy+Credits+Button&utm_campaign=WP",target:"_blank",rel:"noreferrer"},(0,r.__)("Get more","rank-math"))):wp.element.createElement(React.Fragment,null,(0,r.__)("You do not have a Content AI plan.","rank-math"),wp.element.createElement("a",{href:"https://rankmath.com/kb/how-to-use-content-ai/?play-video=ioPeVIntJWw&utm_source=Plugin&utm_medium=Buy+Plan+Button&utm_campaign=WP"},(0,r.__)("Choose your plan","rank-math"))):wp.element.createElement(React.Fragment,null,(0,r.__)("Start using Content AI by connecting your RankMath account.","rank-math"),wp.element.createElement("a",{href:rankMath.connectSiteUrl},(0,r.__)("Connect Now","rank-math")))),!a()&&wp.element.createElement(t.RichText,{tagName:"div",allowedFormats:[],value:i,onChange:function(e){return l({content:e})},onSplit:function(){return!1},onReplace:c,"data-empty":!i,isSelected:!0,ref:u}))}};(0,e.registerBlockType)(function(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?l(Object(n),!0).forEach((function(t){i(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):l(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}({name:m},o),u)}();
@@ -0,0 +1,21 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "rank-math/command",
"title": "AI Assistant [Content AI]",
"icon": "star",
"category": "rank-math-blocks",
"description": "Generate content without any hassle, powered by Rank Math's Content AI.",
"keywords": [ "ai", "content", "rank math", "rankmath", "content ai", "seo" ],
"textdomain": "rank-math-pro",
"editorScript": "file:../js/index.js",
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "p",
"default": "<span>//</span>",
"__experimentalRole": "content"
}
}
}
@@ -0,0 +1,101 @@
/**
* WordPress dependencies
*/
import {
RichText,
useBlockProps,
BlockControls,
} from '@wordpress/block-editor'
import { useRef, useEffect } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
/**
* Internal dependencies
*/
import hasError from '../../../../assets/src/page/helpers/hasError'
const getErrorMessage = () => {
if ( ! rankMath.isUserRegistered ) {
return (
<>
{
__( 'Start using Content AI by connecting your RankMath account.', 'rank-math' )
}
<a href={ rankMath.connectSiteUrl }>{ __( 'Connect Now', 'rank-math' ) }</a>
</>
)
}
if ( ! rankMath.contentAIPlan ) {
return (
<>
{
__( 'You do not have a Content AI plan.', 'rank-math' )
}
<a href="https://rankmath.com/kb/how-to-use-content-ai/?play-video=ioPeVIntJWw&utm_source=Plugin&utm_medium=Buy+Plan+Button&utm_campaign=WP">{ __( 'Choose your plan', 'rank-math' ) }</a>
</>
)
}
return (
<>
{
__( 'You have exhausted your Content AI Credits.', 'rank-math' )
}
<a href="https://rankmath.com/kb/how-to-use-content-ai/?play-video=ioPeVIntJWw&utm_source=Plugin&utm_medium=Buy+Credits+Button&utm_campaign=WP" target="_blank" rel="noreferrer">{ __( 'Get more', 'rank-math' ) }</a>
</>
)
}
export default ( {
attributes,
onReplace,
setAttributes,
} ) => {
const { content } = attributes
const blockProps = useBlockProps( {
className: 'rank-math-content-ai-command',
} )
const contentEditableRef = useRef( null )
useEffect( () => {
const { current: contentEditable } = contentEditableRef
contentEditable.focus()
const range = document.createRange()
const selection = window.getSelection()
range.selectNodeContents( contentEditable )
range.collapse( false )
selection.removeAllRanges()
selection.addRange( range )
}, [] )
return (
<div { ...blockProps }>
<BlockControls />
{
hasError() &&
<div className="rich-text" ref={ contentEditableRef }>
{ getErrorMessage() }
</div>
}
{
! hasError() &&
<RichText
tagName="div"
allowedFormats={ [] }
value={ content }
onChange={ ( newContent ) => {
return setAttributes( { content: newContent } )
} }
onSplit={ () => {
return false
} }
onReplace={ onReplace }
data-empty={ content ? false : true }
isSelected={ true }
ref={ contentEditableRef }
/>
}
</div>
)
}
@@ -0,0 +1,21 @@
/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks'
/**
* Internal dependencies
*/
import edit from './edit'
import metadata from './block.json'
const { name } = metadata
export { metadata, name }
export const settings = {
icon: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17.08 18.02"><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M16.65,12.08a.83.83,0,0,0-1.11.35A7.38,7.38,0,1,1,9,1.63a7.11,7.11,0,0,1,.92.06A2.52,2.52,0,0,1,11,.23,8.87,8.87,0,0,0,9,0a9,9,0,1,0,8,13.19A.83.83,0,0,0,16.65,12.08Z"/><path d="M7.68,7.29A1.58,1.58,0,0,0,6.2,8.94a1.57,1.57,0,0,0,1.48,1.64A1.56,1.56,0,0,0,9.16,8.94,1.57,1.57,0,0,0,7.68,7.29Z" /><path d="M13.34,4.71a2.45,2.45,0,0,1-1,.2A2.53,2.53,0,0,1,9.93,3,7.18,7.18,0,0,0,9.12,3a6,6,0,1,0,4.22,1.73ZM10.53,11.3a.75.75,0,1,1-1.5,0v-.06a2.4,2.4,0,0,1-1.66.69,2.81,2.81,0,0,1-2.58-3,2.82,2.82,0,0,1,2.58-3A2.39,2.39,0,0,1,9,6.64a.75.75,0,0,1,1.5.07Zm2.56,0a.75.75,0,1,1-1.5,0V6.71a.75.75,0,1,1,1.5,0Z" /><circle cx="12.42" cy="2.37" r="1.45" /></g></g></svg>,
edit,
}
registerBlockType( { name, ...metadata }, settings )
@@ -0,0 +1,65 @@
<?php
/**
* The TOC Block
*
* @since 1.0.104
* @package RankMath
* @subpackage RankMath\ContentAI
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use WP_Block_Type_Registry;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Content AI Command Block class.
*/
class Block_Command {
use Hooker;
/**
* Block type name.
*
* @var string
*/
private $block_type = 'rank-math/command';
/**
* The single instance of the class.
*
* @var Block_Command
*/
protected static $instance = null;
/**
* Retrieve main Block_Command instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Block_Command
*/
public static function get() {
if ( is_null( self::$instance ) && ! ( self::$instance instanceof Block_Command ) ) {
self::$instance = new Block_Command();
}
return self::$instance;
}
/**
* The Constructor.
*/
public function __construct() {
if ( WP_Block_Type_Registry::get_instance()->is_registered( $this->block_type ) ) {
return;
}
register_block_type( RANK_MATH_PATH . 'includes/modules/content-ai/blocks/command/assets/src/block.json' );
}
}
@@ -0,0 +1,209 @@
<?php
/**
* Add Content AI Bulk Action options.
*
* @since 1.0.212
* @package RankMath
* @subpackage RankMath\Content_AI_Page
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use RankMath\Traits\Hooker;
use RankMath\Helper;
use RankMath\Helpers\Str;
use RankMath\Paper\Paper;
use RankMath\Admin\Admin_Helper;
use RankMath\Post;
defined( 'ABSPATH' ) || exit;
/**
* Bulk_Actions class.
*/
class Bulk_Actions {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'admin_init', 'init_admin', 15 );
}
/**
* Init.
*/
public function init_admin() {
$post_types = Helper::get_settings( 'general.content_ai_post_types', [] );
foreach ( $post_types as $post_type ) {
$this->filter( "bulk_actions-edit-{$post_type}", 'bulk_actions', 9 );
$this->filter( "handle_bulk_actions-edit-{$post_type}", 'handle_bulk_actions', 10, 3 );
}
$taxonomies = Helper::get_accessible_taxonomies();
unset( $taxonomies['post_format'] );
$taxonomies = wp_list_pluck( $taxonomies, 'label', 'name' );
foreach ( $taxonomies as $taxonomy => $label ) {
$this->filter( "bulk_actions-edit-{$taxonomy}", 'bulk_actions' );
$this->filter( "handle_bulk_actions-edit-{$taxonomy}", 'handle_bulk_actions', 10, 3 );
}
$this->filter( 'wp_bulk_edit_seo_meta_post_args', 'update_background_process_args' );
}
/**
* Add bulk actions for applicable posts, pages, CPTs.
*
* @param array $actions Actions.
* @return array New actions.
*/
public function bulk_actions( $actions ) {
if ( ! Helper::has_cap( 'content_ai' ) ) {
return $actions;
}
$actions['rank_math_ai_options'] = __( '&#8595; Rank Math Content AI', 'rank-math' );
$actions['rank_math_content_ai_fetch_seo_title'] = esc_html__( 'Write SEO Title with AI', 'rank-math' );
$actions['rank_math_content_ai_fetch_seo_description'] = esc_html__( 'Write SEO Description with AI', 'rank-math' );
$actions['rank_math_content_ai_fetch_seo_title_description'] = esc_html__( 'Write SEO Title & Description with AI', 'rank-math' );
return $actions;
}
/**
* Handle bulk actions for applicable posts, pages, CPTs.
*
* @param string $redirect Redirect URL.
* @param string $doaction Performed action.
* @param array $object_ids Post IDs.
*
* @return string New redirect URL.
*/
public function handle_bulk_actions( $redirect, $doaction, $object_ids ) {
if ( empty( $object_ids ) || ! in_array( $doaction, [ 'rank_math_content_ai_fetch_seo_title', 'rank_math_content_ai_fetch_seo_description', 'rank_math_content_ai_fetch_seo_title_description' ], true ) ) {
return $redirect;
}
if ( ! empty( get_option( 'rank_math_content_ai_posts' ) ) ) {
Helper::add_notification(
esc_html__( 'Another bulk editing process is already running. Please try again later after the existing process is complete.', 'rank-math' ),
[
'type' => 'warning',
'id' => 'rank_math_content_ai_posts_error',
'classes' => 'rank-math-notice',
]
);
return $redirect;
}
$action = 'both';
if ( 'rank_math_content_ai_fetch_seo_title' === $doaction ) {
$action = 'title';
}
if ( 'rank_math_content_ai_fetch_seo_description' === $doaction ) {
$action = 'description';
}
$is_post_list = Admin_Helper::is_post_list();
$data = [
'action' => $action,
'language' => Helper::get_settings( 'general.content_ai_language', Helper::content_ai_default_language() ),
'posts' => [],
'is_taxonomy' => ! $is_post_list,
];
$method = $is_post_list ? 'get_post_data' : 'get_term_data';
foreach ( $object_ids as $object_id ) {
$data['posts'][] = $this->$method( $object_id );
}
Bulk_Edit_SEO_Meta::get()->start( $data );
return $redirect;
}
/**
* Change the timeout value in Background_Process to resolve the issue with notifications not appearing after completion in v1.2.
*
* @param array $args Process args.
*
* @return array
*/
public function update_background_process_args( $args ) {
$args['timeout'] = 0.01;
return $args;
}
/**
* Get Post data.
*
* @param integer $object_id Post ID.
*
* @return array Post data.
*/
private function get_post_data( $object_id ) {
$object = get_post( $object_id );
return [
'post_id' => $object_id,
'post_type' => 'download' === $object->post_type ? 'Product' : ucfirst( $object->post_type ),
'title' => get_the_title( $object_id ),
'focus_keyword' => Post::get_meta( 'focus_keyword', $object_id ),
'summary' => Helper::replace_vars( $this->get_post_description( $object ), $object ),
];
}
/**
* Get Term data.
*
* @param integer $object_id Term ID.
*
* @return array Term data.
*/
private function get_term_data( $object_id ) {
$object = get_term( $object_id );
return [
'post_id' => $object_id,
'post_type' => $object->taxonomy,
'title' => $object->name,
'focus_keyword' => get_term_meta( $object_id, 'rank_math_focus_keyword', true ),
'summary' => Helper::replace_vars( $this->get_term_description( $object ), $object ),
];
}
/**
* Get post description.
*
* @param WP_Post $object Post Instance.
*
* @return string Post description.
*/
private function get_post_description( $object ) {
$description = Post::get_meta( 'description', $object->ID );
if ( '' !== $description ) {
return $description;
}
return ! empty( $object->post_excerpt ) ? $object->post_excerpt : Str::truncate( Paper::get_from_options( "pt_{$object->post_type}_description", $object ), 160 );
}
/**
* Get post description.
*
* @param WP_Post $object Post Instance.
*
* @return string Post description.
*/
private function get_term_description( $object ) {
$description = get_term_meta( $object->term_id, 'rank_math_description', true );
if ( '' !== $description ) {
return $description;
}
return ! empty( $object->description ) ? $object->description : Str::truncate( Paper::get_from_options( "tax_{$object->taxonomy}_description", $object ), 160 );
}
}
@@ -0,0 +1,226 @@
<?php
/**
* Bulk Edit SEO Meta data from Content AI API.
*
* @since 1.0.108
* @package RankMath
* @subpackage RankMath\Status
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use RankMath\Helper;
use RankMath\Admin\Admin_Helper;
defined( 'ABSPATH' ) || exit;
/**
* Bulk_Edit_SEO_Meta class.
*/
class Bulk_Edit_SEO_Meta extends \WP_Background_Process {
/**
* Action.
*
* @var string
*/
protected $action = 'bulk_edit_seo_meta';
/**
* Main instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Bulk_Edit_SEO_Meta
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Bulk_Edit_SEO_Meta ) ) {
$instance = new Bulk_Edit_SEO_Meta();
}
return $instance;
}
/**
* Start creating batches.
*
* @param array $data Posts data.
*/
public function start( $data ) {
Helper::add_notification(
esc_html__( 'Bulk editing SEO meta started. It might take few minutes to complete the process.', 'rank-math' ),
[
'type' => 'success',
'id' => 'rank_math_content_ai_posts_started',
'classes' => 'rank-math-notice',
]
);
$action = $data['action'];
$posts = $data['posts'];
$language = $data['language'];
update_option( 'rank_math_content_ai_posts', $posts );
$chunks = array_chunk( $posts, 10, true );
foreach ( $chunks as $chunk ) {
$this->push_to_queue(
[
'posts' => $chunk,
'action' => $action,
'language' => $language,
'is_taxonomy' => ! empty( $data['is_taxonomy'] ),
]
);
}
$this->save()->dispatch();
}
/**
* Task to perform.
*
* @param string $data Posts to process.
*/
public function wizard( $data ) {
$this->task( $data );
}
/**
* Cancel the Bulk edit process.
*/
public function cancel() {
delete_option( 'rank_math_content_ai_posts' );
delete_option( 'rank_math_content_ai_posts_processed' );
parent::clear_scheduled_event();
}
/**
* Complete.
*
* Override if applicable, but ensure that the below actions are
* performed, or, call parent::complete().
*/
protected function complete() {
$posts = get_option( 'rank_math_content_ai_posts' );
delete_option( 'rank_math_content_ai_posts' );
delete_option( 'rank_math_content_ai_posts_processed' );
Helper::add_notification(
// Translators: placeholder is the number of modified items.
sprintf( _n( 'SEO meta successfully updated in %d item.', 'SEO meta successfully updated in %d items.', count( $posts ), 'rank-math' ), count( $posts ) ),
[
'type' => 'success',
'id' => 'rank_math_content_ai_posts',
'classes' => 'rank-math-notice',
]
);
parent::complete();
}
/**
* Task to perform.
*
* @param array $data Posts to process.
*
* @return bool
*/
protected function task( $data ) {
try {
$posts = json_decode( wp_remote_retrieve_body( $this->get_posts( $data ) ), true );
if ( empty( $posts['meta'] ) ) {
return false;
}
foreach ( $posts['meta'] as $post_id => $data ) {
$method = ! empty( $data['object_type'] ) && 'term' === $data['object_type'] ? 'update_term_meta' : 'update_post_meta';
if ( ! empty( $data['title'] ) ) {
$method( $post_id, 'rank_math_title', sanitize_text_field( $data['title'] ) );
}
if ( ! empty( $data['description'] ) ) {
$method( $post_id, 'rank_math_description', sanitize_textarea_field( $data['description'] ) );
}
}
$this->update_content_ai_posts_count( count( $posts['meta'] ) );
$credits = ! empty( $posts['credits'] ) ? $posts['credits'] : [];
if ( ! empty( $credits['available'] ) ) {
$credits = $credits['available'] - $credits['taken'];
Helper::update_credits( $credits );
if ( $credits <= 0 ) {
$posts_processed = get_option( 'rank_math_content_ai_posts_processed' );
delete_option( 'rank_math_content_ai_posts' );
delete_option( 'rank_math_content_ai_posts_processed' );
Helper::add_notification(
// Translators: placeholder is the number of modified posts.
sprintf( esc_html__( 'SEO meta successfully updated in %d posts. The process was stopped as you have used all the credits on your site.', 'rank-math' ), $posts_processed ),
[
'type' => 'success',
'id' => 'rank_math_content_ai_posts',
'classes' => 'rank-math-notice',
]
);
wp_clear_scheduled_hook( 'wp_bulk_edit_seo_meta_cron' );
}
}
return false;
} catch ( Exception $error ) {
return true;
}
}
/**
* Get Posts to bulk update the data.
*
* @param array $data Data to process.
*
* @return array
*/
private function get_posts( $data ) {
$connect_data = Admin_Helper::get_registration_data();
$posts = array_values( $data['posts'] );
$action = $data['action'];
$language = $data['language'];
$data = [
'posts' => $posts,
'output' => $action,
'language' => $language,
'choices' => 1,
'username' => $connect_data['username'],
'api_key' => $connect_data['api_key'],
'site_url' => $connect_data['site_url'],
'is_taxonomy' => ! empty( $data['is_taxonomy'] ),
'plugin_version' => rank_math()->version,
];
return wp_remote_post(
CONTENT_AI_URL . '/ai/bulk_seo_meta',
[
'headers' => [
'content-type' => 'application/json',
],
'timeout' => 60000,
'body' => wp_json_encode( $data ),
]
);
}
/**
* Keep count of the Content AI posts that were processed.
*
* @param int $count Number of posts processed.
*
* @return void
*/
private function update_content_ai_posts_count( $count ) {
$content_ai_posts_count = get_option( 'rank_math_content_ai_posts_processed', 0 ) + $count;
update_option( 'rank_math_content_ai_posts_processed', $content_ai_posts_count, false );
}
}
@@ -0,0 +1,374 @@
<?php
/**
* The Role Manager Module.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Content_AI_Page
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use RankMath\ContentAI\Content_AI;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Admin\Page;
use RankMath\Helpers\Param;
use WP_Block_Editor_Context;
defined( 'ABSPATH' ) || exit;
/**
* Content_AI_Page class.
*/
class Content_AI_Page {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'rank_math/admin_bar/items', 'admin_bar_items', 11 );
$this->action( 'rank_math/admin/editor_scripts', 'enqueue' );
$this->action( 'init', 'init' );
$this->filter( 'wp_insert_post_data', 'remove_unused_generated_content' );
$this->filter( 'rank_math/database/tools', 'add_tools' );
$this->filter( 'rank_math/tools/content_ai_cancel_bulk_edit_process', 'cancel_bulk_edit_process' );
if ( Param::get( 'page' ) !== 'rank-math-content-ai-page' ) {
return;
}
$this->action( 'rank_math/before_help_link', 'add_credits_remaining' );
$this->action( 'admin_footer', 'content_editor_settings' );
add_filter( 'should_load_block_editor_scripts_and_styles', '__return_true' );
}
/**
* Init function.
*/
public function init() {
$this->register_post_type();
$this->register_admin_page();
Block_Command::get();
Bulk_Edit_SEO_Meta::get();
Event_Scheduler::get();
}
/**
* Add Credits remaining text before the help link in the admin header.
*/
public function add_credits_remaining() {
// Early bail if site is not connected or doesn't have a Content AI Plan.
if ( ! Helper::is_site_connected() || ! Helper::get_content_ai_plan() ) {
return;
}
$credits = Helper::get_credits();
?>
<div class="credits-remaining">
<?php echo esc_html__( 'Credits Remaining: ', 'rank-math' ); ?>
<strong><?php echo esc_html( $credits ); ?></strong>
</div>
<?php
}
/**
* Enqueue Content AI Page CSS.
*/
public function enqueue() {
wp_enqueue_style(
'rank-math-content-ai-page',
rank_math()->plugin_url() . 'includes/modules/content-ai/assets/css/content-ai-page.css',
[ 'rank-math-common' ],
rank_math()->version
);
wp_enqueue_script(
'rank-math-content-ai-page',
rank_math()->plugin_url() . 'includes/modules/content-ai/assets/js/content-ai-page.js',
[
'lodash',
'wp-dom-ready',
'rank-math-content-ai',
],
rank_math()->version,
true
);
wp_set_script_translations( 'rank-math-content-ai-page', 'rank-math' );
}
/**
* Register admin page.
*/
public function register_admin_page() {
$uri = untrailingslashit( plugin_dir_url( __FILE__ ) );
$new_label = '<span class="rank-math-new-label" style="color:#ed5e5e;font-size:10px;font-weight:normal;">' . esc_html__( 'New!', 'rank-math' ) . '</span>';
if ( 'rank-math-content-ai-page' === Param::get( 'page' ) ) {
Helper::add_json( 'isContentAIPage', true );
Content_AI::localized_data();
}
new Page(
'rank-math-content-ai-page',
esc_html__( 'Content AI', 'rank-math' ),
[
'position' => 4,
'parent' => 'rank-math',
// Translators: placeholder is the new label.
'menu_title' => sprintf( esc_html__( 'Content AI %s', 'rank-math' ), $new_label ),
'capability' => 'rank_math_content_ai',
'render' => dirname( __FILE__ ) . '/views/main.php',
'classes' => [ 'rank-math-page' ],
'assets' => [
'styles' => [
'wp-edit-post' => '',
'rank-math-common' => '',
'rank-math-cmb2' => '',
'wp-block-library' => '',
'rank-math-content-ai-page' => $uri . '/assets/css/content-ai-page.css',
],
'scripts' => [
'lodash' => '',
'wp-components' => '',
'wp-block-library' => '',
'wp-format-library' => '',
'wp-edit-post' => '',
'wp-blocks' => '',
'wp-element' => '',
'wp-editor' => '',
'rank-math-block-faq' => rank_math()->plugin_url() . 'assets/admin/js/blocks.js',
'rank-math-analyzer' => rank_math()->plugin_url() . 'assets/admin/js/analyzer.js',
'rank-math-content-ai' => rank_math()->plugin_url() . 'includes/modules/content-ai/assets/js/content-ai.js',
'rank-math-content-ai-page' => $uri . '/assets/js/content-ai-page.js',
],
],
]
);
}
/**
* Add admin bar item.
*
* @param Admin_Bar_Menu $menu Menu class instance.
*/
public function admin_bar_items( $menu ) {
$url = Helper::get_admin_url( 'content-ai-page' );
$menu->add_sub_menu(
'content-ai-page',
[
'title' => esc_html__( 'Content AI', 'rank-math' ),
'href' => $url,
'priority' => 50,
]
);
$items = [
'content-ai-tools' => [
'title' => esc_html__( 'AI Tools', 'rank-math' ),
'href' => $url . '#ai-tools',
'meta' => [ 'title' => esc_html__( 'Content AI Tools', 'rank-math' ) ],
],
'content-ai-editor' => [
'title' => esc_html__( 'Content Editor', 'rank-math' ),
'href' => $url . '#content-editor',
'meta' => [ 'title' => esc_html__( 'Content AI Editor', 'rank-math' ) ],
],
'content-ai-chat' => [
'title' => esc_html__( 'Chat', 'rank-math' ),
'href' => $url . '#chat',
'meta' => [ 'title' => esc_html__( 'Content AI Chat', 'rank-math' ) ],
],
'content-ai-history' => [
'title' => esc_html__( 'History', 'rank-math' ),
'href' => $url . '#history',
'meta' => [ 'title' => esc_html__( 'Content AI History', 'rank-math' ) ],
],
];
foreach ( $items as $id => $args ) {
$menu->add_sub_menu( $id, $args, 'content-ai-page' );
}
}
/**
* Add Content Editor Settings.
*/
public function content_editor_settings() {
$post = $this->get_content_editor_post();
$block_editor_context = new WP_Block_Editor_Context( [ 'post' => [] ] );
// Flag that we're loading the block editor.
$current_screen = get_current_screen();
$current_screen->is_block_editor( true );
$editor_settings = [
'availableTemplates' => [],
'disablePostFormats' => true,
'autosaveInterval' => 0,
'richEditingEnabled' => user_can_richedit(),
'supportsLayout' => function_exists( 'wp_theme_has_theme_json' ) ? wp_theme_has_theme_json() : false,
'supportsTemplateMode' => false,
'enableCustomFields' => false,
];
$editor_settings = get_block_editor_settings( $editor_settings, $block_editor_context );
/**
* Scripts
*/
wp_enqueue_media( [ 'post' => $post->ID ] );
?>
<div id="editor2" data-settings='<?php echo esc_attr( wp_json_encode( $editor_settings ) ); ?>' data-post-id="<?php echo esc_attr( $post->ID ); ?>"></div>
<?php
wp_set_script_translations( 'rank-math-content-ai', 'rank-math' );
wp_set_script_translations( 'rank-math-content-ai-page', 'rank-math' );
}
/**
* Remove unsed content generated from the Toolbar option of the Content AI.
*
* @param array $data An array of slashed, sanitized, and processed post data.
*/
public function remove_unused_generated_content( $data ) {
$blocks = parse_blocks( $data['post_content'] );
if ( empty( $blocks ) ) {
return $data;
}
$update = false;
foreach ( $blocks as $key => $block ) {
if ( 'rank-math/command' === $block['blockName'] ) {
unset( $blocks[ $key ] );
$update = true;
}
}
if ( $update ) {
$data['post_content'] = serialize_blocks( $blocks );
}
return $data;
}
/**
* Add database tools.
*
* @param array $tools Array of tools.
*
* @return array
*/
public function add_tools( $tools ) {
$posts = get_option( 'rank_math_content_ai_posts' );
// Early Bail if process is not running.
if ( empty( $posts ) ) {
return $tools;
}
$processed = get_option( 'rank_math_content_ai_posts_processed' );
$tools['content_ai_cancel_bulk_edit_process'] = [
'title' => esc_html__( 'Cancel Content AI Bulk Editing Process', 'rank-math' ),
'description' => sprintf(
// Translators: placeholders are the number of posts that were processed.
esc_html__( 'Terminate the ongoing Content AI Bulk Editing Process to halt any pending modifications and revert to the previous state. The bulk metadata has been generated for %1$d out of %1$d posts so far.', 'rank-math' ),
$processed,
count( $posts )
),
'button_text' => esc_html__( 'Terminate', 'rank-math' ),
];
return $tools;
}
/**
* Function to cancel the Bulk Edit process.
*/
public function cancel_bulk_edit_process() {
Bulk_Edit_SEO_Meta::get()->cancel();
Helper::remove_notification( 'rank_math_content_ai_posts_started' );
return __( 'Bulk Editing Process Successfully Cancelled', 'rank-math' );
}
/**
* Register Content AI post type to use the post in Content Editor.
*/
private function register_post_type() {
register_post_type(
'rm_content_editor',
[
'label' => 'RM Content Editor',
'public' => false,
'publicly_queryable' => false,
'show_ui' => false,
'show_in_rest' => true,
'has_archive' => false,
'show_in_menu' => false,
'show_in_nav_menus' => false,
'delete_with_user' => false,
'exclude_from_search' => false,
'capability_type' => 'page',
'map_meta_cap' => true,
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'supports' => [ 'editor' ],
]
);
}
/**
* Get Content Editor post.
*
* @return int Post ID.
*/
private function get_content_editor_post() {
$posts = get_posts(
[
'post_type' => 'rm_content_editor',
'numberposts' => 1,
'post_status' => 'any',
]
);
if ( empty( $posts ) ) {
$post_id = wp_insert_post(
[
'post_type' => 'rm_content_editor',
'post_content' => '<!-- wp:rank-math/command /-->',
]
);
return get_post( $post_id );
}
$ai_post = current( $posts );
$content = $ai_post->post_content;
$blocks = parse_blocks( $content );
if ( ! empty( $blocks ) && count( $blocks ) < 2 && 'core/paragraph' === $blocks[0]['blockName'] ) {
$content = do_blocks( $ai_post->post_content );
$content = trim( preg_replace( '/<p[^>]*><\\/p[^>]*>/', '', $content ) );
}
if ( ! $content ) {
wp_update_post(
[
'ID' => $ai_post->ID,
'post_content' => '<!-- wp:rank-math/command /-->',
]
);
}
return $ai_post;
}
}
@@ -0,0 +1,360 @@
<?php
/**
* The Content AI module.
*
* @since 1.0.71
* @package RankMath
* @subpackage RankMath
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Helpers\Sitepress;
use RankMath\Helpers\Url;
use RankMath\Helpers\Arr;
use RankMath\Admin\Admin_Helper as AdminHelper;
use RankMath\CMB2;
use RankMath\Traits\Hooker;
use RankMath\Traits\Ajax;
defined( 'ABSPATH' ) || exit;
/**
* Content_AI class.
*/
class Content_AI {
use Hooker, Ajax;
/**
* Class constructor.
*/
public function __construct() {
$this->action( 'rest_api_init', 'init_rest_api' );
new Content_AI_Page();
new Bulk_Actions();
if ( ! Helper::has_cap( 'content_ai' ) ) {
return;
}
$this->filter( 'rank_math/analytics/post_data', 'add_contentai_data', 10, 2 );
$this->filter( 'rank_math/settings/general', 'add_settings' );
$this->action( 'rank_math/admin/editor_scripts', 'editor_scripts', 20 );
$this->filter( 'rank_math/metabox/post/values', 'add_metadata', 10, 2 );
$this->action( 'cmb2_admin_init', 'add_content_ai_metabox', 11 );
$this->action( 'rank_math/deregister_site', 'remove_credits_data' );
$this->filter( 'rank_math/elementor/dark_styles', 'add_dark_style' );
$this->filter( 'rank_math/status/rank_math_info', 'content_ai_info' );
$this->action( 'rank_math/connect/account_connected', 'refresh_content_ai_credits' );
}
/**
* Add dark style
*
* @param array $styles The dark mode styles.
*/
public function add_dark_style( $styles = [] ) {
$styles['rank-math-content-ai-dark'] = rank_math()->plugin_url() . 'includes/modules/content-ai/assets/css/content-ai-dark.css';
return $styles;
}
/**
* Add Content AI score in Single Page Site Analytics.
*
* @param array $data array.
* @param \WP_REST_Request $request post object.
* @return array $data sorted array.
*/
public function add_contentai_data( $data, \WP_REST_Request $request ) {
$post_id = $data['object_id'];
$content_ai_data = Helper::get_post_meta( 'contentai_score', $post_id );
$content_ai_score = ! empty( $content_ai_data ) ? round( array_sum( array_values( $content_ai_data ) ) / count( $content_ai_data ) ) : 0;
$data['contentAiScore'] = absint( $content_ai_score );
return $data;
}
/**
* Load the REST API endpoints.
*/
public function init_rest_api() {
$rest = new Rest();
$rest->register_routes();
}
/**
* Remove credits data when site is disconnected.
*/
public function remove_credits_data() {
delete_option( 'rank_math_ca_credits' );
}
/**
* Add module settings in the General Settings panel.
*
* @param array $tabs Array of option panel tabs.
* @return array
*/
public function add_settings( $tabs ) {
Arr::insert(
$tabs,
[
'content-ai' => [
'icon' => 'rm-icon rm-icon-content-ai',
'title' => esc_html__( 'Content AI', 'rank-math' ),
/* translators: Link to kb article */
'desc' => sprintf( esc_html__( 'Get sophisticated AI suggestions for related Keywords, Questions & Links to include in the SEO meta & Content Area. %s.', 'rank-math' ), '<a href="' . KB::get( 'content-ai-settings', 'Options Panel Content AI Tab' ) . '" target="_blank">' . esc_html__( 'Learn more', 'rank-math' ) . '</a>' ),
'file' => dirname( __FILE__ ) . '/views/options.php',
],
],
8
);
return $tabs;
}
/**
* Add link suggestion metabox.
*/
public function add_content_ai_metabox() {
if ( ! self::can_add_tab() || 'classic' !== Helper::get_current_editor() ) {
return;
}
$id = 'rank_math_metabox_content_ai';
$cmb = new_cmb2_box(
[
'id' => $id,
'title' => esc_html__( 'Content AI', 'rank-math' ),
'object_types' => array_keys( Helper::get_accessible_post_types() ),
'context' => 'side',
'priority' => 'high',
'mb_callback_args' => [ '__block_editor_compatible_meta_box' => false ],
]
);
CMB2::pre_init( $cmb );
// Move content AI metabox below the Publish box.
$this->reorder_content_ai_metabox( $id );
}
/**
* Enqueue assets for post/term/user editors.
*
* @return void
*/
public function editor_scripts() {
if ( ! self::can_add_tab() ) {
return;
}
$editor = Helper::get_current_editor();
if ( ! $editor ) {
return;
}
wp_register_style( 'rank-math-common', rank_math()->plugin_url() . 'assets/admin/css/common.css', null, rank_math()->version );
wp_enqueue_style(
'rank-math-content-ai',
rank_math()->plugin_url() . 'includes/modules/content-ai/assets/css/content-ai.css',
[ 'rank-math-common' ],
rank_math()->version
);
wp_enqueue_script(
'rank-math-content-ai',
rank_math()->plugin_url() . 'includes/modules/content-ai/assets/js/content-ai.js',
[ 'rank-math-editor' ],
rank_math()->version,
true
);
wp_set_script_translations( 'rank-math-content-ai', 'rank-math' );
$this->localized_data();
}
/**
* Add meta data to use in gutenberg.
*
* @param array $values Aray of tabs.
* @param Screen $screen Sceen object.
*
* @return array
*/
public function add_metadata( $values, $screen ) {
$countries = [];
foreach ( Helper::choices_contentai_countries() as $value => $label ) {
$countries[] = [
'value' => $value,
'label' => $label,
];
}
$values['contentAiCountry'] = Helper::get_settings( 'general.content_ai_country', 'all' );
$values['countries'] = $countries;
$values['ca_credits'] = Helper::get_credits();
$values['ca_keyword'] = '';
$values['ca_viewed'] = true;
$content_ai_viewed = get_option( 'rank_math_content_ai_viewed', false );
if ( ! $content_ai_viewed ) {
$values['ca_viewed'] = false;
update_option( 'rank_math_content_ai_viewed', true );
}
$keyword = $screen->get_meta( $screen->get_object_type(), $screen->get_object_id(), 'rank_math_ca_keyword' );
if ( empty( $keyword ) ) {
return $values;
}
$data = get_option( 'rank_math_ca_data' );
$country = empty( $keyword['country'] ) ? '' : $keyword['country'];
if (
! empty( $data[ $country ] ) &&
! empty( $data[ $country ][ mb_strtolower( $keyword['keyword'] ) ] )
) {
$values['ca_data'] = $data[ $country ][ mb_strtolower( $keyword['keyword'] ) ];
}
$values['ca_keyword'] = $keyword;
$content_ai_data = $screen->get_meta( $screen->get_object_type(), $screen->get_object_id(), 'rank_math_contentai_score' );
$content_ai_score = ! empty( $content_ai_data ) && is_array( $content_ai_data ) ? round( array_sum( array_values( $content_ai_data ) ) / count( $content_ai_data ) ) : 0;
$values['contentAiScore'] = absint( $content_ai_score );
return $values;
}
/**
* Whether to load Content AI data.
*/
public static function can_add_tab() {
return in_array( Helper::get_post_type(), (array) Helper::get_settings( 'general.content_ai_post_types' ), true );
}
/**
* Localized data to use on the Content AI page.
*/
public static function localized_data() {
Helper::add_json( 'ca_audience', (array) Helper::get_settings( 'general.content_ai_audience', 'General Audience' ) );
Helper::add_json( 'ca_tone', (array) Helper::get_settings( 'general.content_ai_tone', 'Formal' ) );
Helper::add_json( 'ca_language', Helper::get_settings( 'general.content_ai_language', Helper::content_ai_default_language() ) );
Helper::add_json( 'contentAIHistory', Helper::get_outputs() );
Helper::add_json( 'contentAIChats', Helper::get_chats() );
Helper::add_json( 'contentAIRecentPrompts', Helper::get_recent_prompts() );
Helper::add_json( 'contentAIPrompts', Helper::get_prompts() );
Helper::add_json( 'isUserRegistered', Helper::is_site_connected() );
Helper::add_json( 'connectSiteUrl', AdminHelper::get_activate_url( Url::get_current_url() ) );
Helper::add_json( 'contentAICredits', Helper::get_content_ai_credits() );
Helper::add_json( 'contentAIPlan', Helper::get_content_ai_plan() );
Helper::add_json( 'contentAIErrors', Helper::get_content_ai_errors() );
Helper::add_json( 'connectData', AdminHelper::get_registration_data() );
Helper::add_json( 'registerWriteShortcut', version_compare( get_bloginfo( 'version' ), '6.2', '>=' ) );
Helper::add_json( 'contentAiMigrating', get_site_transient( 'rank_math_content_ai_migrating_user' ) );
Helper::add_json( 'contentAiUrl', CONTENT_AI_URL . '/ai/' );
$refresh_date = Helper::get_content_ai_refresh_date();
Helper::add_json( 'contentAIRefreshDate', $refresh_date ? wp_date( 'Y-m-d g:ia', $refresh_date ) : '' );
}
/**
* Add Content AI details in System Info
*
* @param array $rankmath Array of rankmath.
*/
public function content_ai_info( $rankmath ) {
$refresh_date = Helper::get_content_ai_refresh_date();
$content_ai = [
'ca_plan' => [
'label' => esc_html__( 'Content AI Plan', 'rank-math' ),
'value' => \ucwords( Helper::get_content_ai_plan() ),
],
'ca_credits' => [
'label' => esc_html__( 'Content AI Credits', 'rank-math' ),
'value' => Helper::get_content_ai_credits(),
],
'ca_refresh_date' => [
'label' => esc_html__( 'Content AI Refresh Date', 'rank-math' ),
'value' => $refresh_date ? wp_date( 'Y-m-d g:i a', $refresh_date ) : '',
],
];
array_splice( $rankmath['fields'], 3, 0, $content_ai );
return $rankmath;
}
/**
* Refresh Content AI credits when account is connected.
*
* @param array $data Authentication data.
*
* @return void
*/
public function refresh_content_ai_credits( $data ) {
Helper::get_content_ai_credits( true );
}
/**
* Reorder the Content AI metabox in Classic editor.
*
* @param string $id Metabox ID.
* @return void
*/
private function reorder_content_ai_metabox( $id ) {
$post_type = Helper::get_post_type();
if ( ! $post_type ) {
return;
}
$user = wp_get_current_user();
$order = (array) get_user_option( 'meta-box-order_' . $post_type, $user->ID );
if ( ! empty( $order['normal'] ) && false !== strpos( $order['normal'], $id ) ) {
return;
}
$order['side'] = ! isset( $order['side'] ) ? '' : $order['side'];
if ( false !== strpos( $order['side'], $id ) ) {
return;
}
if ( false === strpos( $order['side'], 'submitdiv' ) ) {
$order['side'] = 'submitdiv,' . $order['side'];
}
if ( ',' === substr( $order['side'], -1 ) ) {
$order['side'] = substr( $order['side'], 0, -1 );
}
$current_order = [];
$current_order = explode( ',', $order['side'] );
$key = array_search( 'submitdiv', $current_order, true );
if ( false === $key ) {
return;
}
$new_order = array_merge(
array_slice( $current_order, 0, $key + 1 ),
[ $id ]
);
if ( count( $current_order ) > $key ) {
$new_order = array_merge(
$new_order,
array_slice( $current_order, $key + 1 )
);
}
$order['side'] = implode( ',', array_unique( $new_order ) );
update_user_option( $user->ID, 'meta-box-order_' . $post_type, $order, true );
}
}
@@ -0,0 +1,117 @@
<?php
/**
* The Event Scheduler for Content AI to update the prompts and credits data.
*
* @since 1.0.123
* @package RankMath
* @subpackage RankMath\ContentAI
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use WP_Block_Type_Registry;
use RankMath\Helper;
use RankMath\Admin\Admin_Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Event_Scheduler class.
*/
class Event_Scheduler {
use Hooker;
/**
* The single instance of the class.
*
* @var Event_Scheduler
*/
protected static $instance = null;
/**
* Retrieve main Block_Command instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Event_Scheduler
*/
public static function get() {
if ( is_null( self::$instance ) && ! ( self::$instance instanceof Event_Scheduler ) ) {
self::$instance = new Event_Scheduler();
}
return self::$instance;
}
/**
* The Constructor.
*/
public function __construct() {
if ( ! Helper::is_site_connected() ) {
return;
}
$credits = get_option( 'rank_math_ca_credits' );
if ( ! empty( $credits['refresh_date'] ) ) {
wp_schedule_single_event( absint( $credits['refresh_date'] ) + 60, 'rank_math/content-ai/update_plan' );
}
$this->action( 'rank_math/content-ai/update_prompts', 'update_prompts_data' );
$this->action( 'rank_math/content-ai/update_plan', 'update_content_ai_plan' );
$this->action( 'admin_footer', 'update_prompts_on_new_site' );
}
/**
* Fetch and update the prompts data daily.
*
* @return void
*/
public function update_prompts_data() {
if ( ! Helper::get_credits() || ! Helper::get_content_ai_plan() ) {
return;
}
$prompt_data = [];
$data = wp_remote_get( CONTENT_AI_URL . '/ai/default_prompts' );
$response_code = wp_remote_retrieve_response_code( $data );
if ( is_wp_error( $data ) || 200 !== $response_code ) {
return;
}
update_option( 'rank_math_prompts_updated', true );
$data = wp_remote_retrieve_body( $data );
$data = json_decode( $data, true );
if ( empty( $data ) ) {
return;
}
Helper::save_default_prompts( $data );
}
/**
* Run the credits endpoint to update the plan on reset date.
*
* @return void
*/
public function update_content_ai_plan() {
Helper::get_content_ai_credits( true );
wp_clear_scheduled_hook( 'rank_math/content-ai/update_plan' );
}
/**
* Function to update Prompts data on new sites.
*
* @return void
*/
public function update_prompts_on_new_site() {
$prompts = Helper::get_prompts();
if ( get_option( 'rank_math_prompts_updated' ) || ! empty( $prompts ) ) {
return;
}
do_action( 'rank_math/content-ai/update_prompts' );
}
}
@@ -0,0 +1,545 @@
<?php
/**
* The Global functionality of the plugin.
*
* Defines the functionality loaded on admin.
*
* @since 1.0.71
* @package RankMath
* @subpackage RankMath\Rest
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use WP_Error;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Controller;
use RankMath\Helper;
use RankMath\Admin\Admin_Helper;
defined( 'ABSPATH' ) || exit;
/**
* Rest class.
*/
class Rest extends WP_REST_Controller {
/**
* Registered data.
*
* @var array|false
*/
private $registered;
/**
* Constructor.
*/
public function __construct() {
$this->namespace = \RankMath\Rest\Rest_Helper::BASE . '/ca';
$this->registered = Admin_Helper::get_registration_data();
}
/**
* Registers the routes for the objects of the controller.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/researchKeyword',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'research_keyword' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/getCredits',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'get_credits' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/createPost',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'create_post' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/saveOutput',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'save_output' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/deleteOutput',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'delete_output' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/updateRecentPrompt',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'update_recent_prompt' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/updatePrompt',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'update_prompt' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/savePrompts',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'save_prompts' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/pingContentAI',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'ping_content_ai' ],
'permission_callback' => [ $this, 'has_ping_permission' ],
]
);
register_rest_route(
$this->namespace,
'/migrateuser',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'migrate_user' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
}
/**
* Check API key in request.
*
* @param WP_REST_Request $request Full details about the request.
* @return bool Whether the API key matches or not.
*/
public function has_ping_permission( WP_REST_Request $request ) {
if ( empty( $this->registered ) ) {
return false;
}
return $request->get_param( 'apiKey' ) === $this->registered['api_key'] &&
$request->get_param( 'username' ) === $this->registered['username'];
}
/**
* Determines if the current user can manage analytics.
*
* @return true
*/
public function has_permission() {
if ( ! Helper::has_cap( 'content_ai' ) || empty( $this->registered ) ) {
return new WP_Error(
'rest_cannot_access',
__( 'Sorry, only authenticated users can research the keyword.', 'rank-math' ),
[ 'status' => rest_authorization_required_code() ]
);
}
return true;
}
/**
* Get Content AI Credits.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return int Credits.
*/
public function get_credits( WP_REST_Request $request ) {
$credits = Helper::get_content_ai_credits( true, true );
if ( ! empty( $credits['error'] ) ) {
$error = $credits['error'];
$error_texts = Helper::get_content_ai_errors();
return [
'error' => ! empty( $error_texts[ $error ] ) ? wp_specialchars_decode( $error_texts[ $error ], ENT_QUOTES ) : $error,
'credits' => isset( $credits['credits'] ) ? $credits['credits'] : '',
];
}
return $credits;
}
/**
* Research a keyword.
*
* @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 research_keyword( WP_REST_Request $request ) {
$object_id = $request->get_param( 'objectID' );
$country = $request->get_param( 'country' );
$keyword = mb_strtolower( $request->get_param( 'keyword' ) );
$force_update = $request->get_param( 'force_update' );
$keyword_data = get_option( 'rank_math_ca_data' );
if ( ! in_array( get_post_type( $object_id ), (array) Helper::get_settings( 'general.content_ai_post_types' ), true ) ) {
return [
'data' => esc_html__( 'Content AI is not enabled on this Post type.', 'rank-math' ),
];
}
if ( ! apply_filters( 'rank_math/content_ai/call_api', true ) ) {
return [
'data' => 'show_dummy_data',
];
}
if (
! $force_update &&
! empty( $keyword_data ) &&
! empty( $keyword_data[ $country ] ) &&
! empty( $keyword_data[ $country ][ $keyword ] )
) {
update_post_meta(
$object_id,
'rank_math_ca_keyword',
[
'keyword' => $keyword,
'country' => $country,
]
);
return [
'data' => $keyword_data[ $country ][ $keyword ],
'keyword' => $keyword,
];
}
$data = $this->get_researched_data( $keyword, $country, $force_update );
if ( ! empty( $data['error'] ) ) {
return $this->get_errored_data( $data['error'] );
}
$credits = ! empty( $data['credits'] ) ? $data['credits'] : 0;
if ( ! empty( $credits ) ) {
$credits = $credits['available'] - $credits['taken'];
}
$data = $data['data']['details'];
$this->get_recommendations( $data );
update_post_meta(
$object_id,
'rank_math_ca_keyword',
[
'keyword' => $keyword,
'country' => $country,
]
);
$keyword_data[ $country ][ $keyword ] = $data;
update_option( 'rank_math_ca_data', $keyword_data, false );
Helper::update_credits( $credits );
return [
'data' => $keyword_data[ $country ][ $keyword ],
'credits' => $credits,
'keyword' => $keyword,
];
}
/**
* Create a new Post from Content AI Page.
*
* @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 create_post( WP_REST_Request $request ) {
$content = $request->get_param( 'content' );
$title = 'Content AI Post';
$blocks = parse_blocks( $content );
$current_block = ! empty( $blocks ) ? current( $blocks ) : '';
if (
! empty( $current_block ) &&
$current_block['blockName'] === 'core/heading' &&
$current_block['attrs']['level'] === 1
) {
$title = wp_strip_all_tags( $current_block['innerHTML'] );
}
$post_id = wp_insert_post(
[
'post_title' => $title,
'post_content' => $content,
]
);
return wp_specialchars_decode( add_query_arg( 'tab', 'content-ai', get_edit_post_link( $post_id ) ) );
}
/**
* Save the API output.
*
* @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 save_output( WP_REST_Request $request ) {
$outputs = $request->get_param( 'outputs' );
$endpoint = $request->get_param( 'endpoint' );
$is_chat = $request->get_param( 'isChat' );
$attributes = $request->get_param( 'attributes' );
$credits_data = $request->get_param( 'credits' );
if ( ! empty( $credits_data ) ) {
$credits = ! empty( $credits_data['credits'] ) ? $credits_data['credits'] : [];
$data = [
'credits' => ! empty( $credits['available'] ) ? $credits['available'] - $credits['taken'] : 0,
'plan' => ! empty( $credits_data['plan'] ) ? $credits_data['plan'] : '',
'refresh_date' => ! empty( $credits_data['refreshDate'] ) ? $credits_data['refreshDate'] : '',
];
Helper::update_credits( $data );
}
if ( $is_chat ) {
Helper::update_chats( current( $outputs ), end( $attributes['messages'] ), $attributes['session'], $attributes['isNew'], $attributes['regenerate'] );
return true;
}
return Helper::update_outputs( $endpoint, $outputs );
}
/**
* Delete the API output.
*
* @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 delete_output( WP_REST_Request $request ) {
$is_chat = $request->get_param( 'isChat' );
if ( $is_chat ) {
return Helper::delete_chats( $request->get_param( 'index' ) );
}
return Helper::delete_outputs();
}
/**
* Update the Prompts.
*
* @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 update_prompt( WP_REST_Request $request ) {
$prompt = $request->get_param( 'prompt' );
if ( is_string( $prompt ) ) {
return Helper::delete_prompt( $prompt );
}
return Helper::update_prompts( $prompt );
}
/**
* Save the Prompts.
*
* @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 save_prompts( WP_REST_Request $request ) {
$prompts = $request->get_param( 'prompts' );
if ( empty( $prompts ) ) {
return false;
}
return Helper::save_default_prompts( $prompts );
}
/**
* Update the Recent Prompts.
*
* @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 update_recent_prompt( WP_REST_Request $request ) {
$prompt = $request->get_param( 'prompt' );
return Helper::update_recent_prompts( $prompt );
}
/**
* Endpoing to update the AI plan and credits.
*
* @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 ping_content_ai( WP_REST_Request $request ) {
$credits = ! empty( $data['credits'] ) ? json_decode( $data['credits'], true ) : [];
$data = [
'credits' => ! empty( $credits['available'] ) ? $credits['available'] - $credits['taken'] : 0,
'plan' => $request->get_param( 'plan' ),
'refresh_date' => $request->get_param( 'refreshDate' ),
];
Helper::update_credits( $data );
return true;
}
/**
* Migrate user to nest js server.
*
* @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 migrate_user( WP_REST_Request $request ) {
return Helper::migrate_user_to_nest_js();
}
/**
* Get data from the API.
*
* @param string $keyword Researched keyword.
* @param string $country Researched country.
* @param bool $force_update Whether to force update the researched data.
*
* @return array
*/
private function get_researched_data( $keyword, $country, $force_update = false ) {
$args = [
'username' => rawurlencode( $this->registered['username'] ),
'api_key' => rawurlencode( $this->registered['api_key'] ),
'keyword' => rawurlencode( $keyword ),
'site_url' => rawurlencode( Helper::get_home_url() ),
'new_api' => 1,
];
if ( 'all' !== $country ) {
$args['locale'] = rawurlencode( $country );
}
if ( $force_update ) {
$args['force_refresh'] = 1;
}
$url = add_query_arg(
$args,
CONTENT_AI_URL . '/ai/research'
);
$data = wp_remote_get(
$url,
[
'timeout' => 60,
]
);
$response_code = wp_remote_retrieve_response_code( $data );
if ( 200 !== $response_code ) {
return [
'error' => 410 !== $response_code ? $data['response']['message'] : wp_kses_post(
sprintf(
// Translators: link to the update page.
__( 'There is a new version of Content AI available! %s the Rank Math SEO plugin to use this feature.', 'rank-math' ),
'<a href="' . esc_url( self_admin_url( 'update-core.php' ) ) . '">' . __( 'Please update', 'rank-math' ) . '</a>'
)
),
];
}
$data = wp_remote_retrieve_body( $data );
$data = json_decode( $data, true );
if ( empty( $data['error'] ) && empty( $data['data']['details'] ) ) {
return [
'error' => esc_html__( 'No data found for the researched keyword.', 'rank-math' ),
];
}
return $data;
}
/**
* Get errored data.
*
* @param array $error Error data received from the API.
*
* @return array
*/
private function get_errored_data( $error ) {
if ( empty( $error['code'] ) ) {
return [
'data' => $error,
];
}
if ( 'invalid_domain' === $error['code'] ) {
return [
'data' => esc_html__( 'This feature is not available on the localhost.', 'rank-math' ),
];
}
if ( 'domain_limit_reached' === $error['code'] ) {
return [
'data' => esc_html__( 'You have used all the free credits which are allowed to this domain.', 'rank-math' ),
];
}
return [
'data' => '',
'credits' => $error['code'],
];
}
/**
* Get the Recommendations data.
*
* @param array $data Researched data.
*/
private function get_recommendations( &$data ) {
foreach ( $data['recommendations'] as $key => $value ) {
if ( ! is_array( $value ) ) {
continue;
}
$data['recommendations'][ $key ]['total'] = array_sum( $value );
}
}
}
@@ -0,0 +1,17 @@
<?php
/**
* Content AI Page main view.
*
* @package RankMath
* @subpackage RankMath\Role_Manager
*/
defined( 'ABSPATH' ) || exit;
// Header.
rank_math()->admin->display_admin_header();
?>
<div class="wrap rank-math-wrap" id="rank-math-content-ai-page">
<div class="rank-math-box container">
</div>
</div>
@@ -0,0 +1,224 @@
<?php
/**
* Content AI general settings.
*
* @package RankMath
* @subpackage RankMath\ContentAI
*/
use RankMath\Helper;
use RankMath\KB;
use RankMath\Admin\Admin_Helper;
defined( 'ABSPATH' ) || exit;
if ( ! Helper::is_site_connected() ) {
$cmb->add_field(
[
'id' => 'rank_math_content_ai_settings',
'type' => 'raw',
'content' => '<div id="setting-panel-content-ai" class="rank-math-tab rank-math-options-panel-content exclude">
<div class="wp-core-ui rank-math-ui connect-wrap">
<a href="' . Admin_Helper::get_activate_url( admin_url( 'admin.php??page=rank-math-options-general#setting-panel-content-ai' ) ) . '" class="button button-primary button-connect button-animated" name="rank_math_activate">'
. esc_html__( 'Connect Your Rank Math Account', 'rank-math' )
. '</a>
</div>
<div id="rank-math-pro-cta" class="content-ai-settings">
<div class="rank-math-cta-box width-100 no-shadow no-padding no-border">
<h3>' . esc_html__( 'Benefits of Connecting Rank Math Account', 'rank-math' ) . '</h3>
<ul>
<li>' . esc_html__( 'Gain Access to 40+ Advanced AI Tools.', 'rank-math' ) . '</li>
<li>' . esc_html__( 'Experience the Revolutionary AI-Powered Content Editor.', 'rank-math' ) . '</li>
<li>' . esc_html__( 'Engage with RankBot, Our AI Chatbot, For SEO Advice.', 'rank-math' ) . '</li>
<li>' . esc_html__( 'Escape the Writer\'s Block Using AI to Write Inside WordPress.', 'rank-math' ) . '</li>
</ul>
</div>
</div>
</div>',
]
);
return;
}
$cmb->add_field(
[
'id' => 'content_ai_country',
'type' => 'select',
'name' => esc_html__( 'Default Country', 'rank-math' ),
'desc' => esc_html__( 'Content AI tailors keyword research to the target country for highly relevant suggestions. You can override this in individual posts/pages/CPTs.', 'rank-math' ),
'options' => Helper::choices_contentai_countries(),
'default' => 'all',
]
);
$cmb->add_field(
[
'id' => 'content_ai_tone',
'type' => 'select',
'name' => esc_html__( 'Default Tone', 'rank-math' ),
'desc' => esc_html__( 'This feature enables the default primary tone or writing style that characterizes your content. You can override this in individual tools.', 'rank-math' ),
'default' => 'Formal',
'attributes' => ( 'data-s2' ),
'options' => [
'Analytical' => esc_html__( 'Analytical', 'rank-math' ),
'Argumentative' => esc_html__( 'Argumentative', 'rank-math' ),
'Casual' => esc_html__( 'Casual', 'rank-math' ),
'Conversational' => esc_html__( 'Conversational', 'rank-math' ),
'Creative' => esc_html__( 'Creative', 'rank-math' ),
'Descriptive' => esc_html__( 'Descriptive', 'rank-math' ),
'Emotional' => esc_html__( 'Emotional', 'rank-math' ),
'Empathetic' => esc_html__( 'Empathetic', 'rank-math' ),
'Expository' => esc_html__( 'Expository', 'rank-math' ),
'Factual' => esc_html__( 'Factual', 'rank-math' ),
'Formal' => esc_html__( 'Formal', 'rank-math' ),
'Friendly' => esc_html__( 'Friendly', 'rank-math' ),
'Humorous' => esc_html__( 'Humorous', 'rank-math' ),
'Informal' => esc_html__( 'Informal', 'rank-math' ),
'Journalese' => esc_html__( 'Journalese', 'rank-math' ),
'Narrative' => esc_html__( 'Narrative', 'rank-math' ),
'Objective' => esc_html__( 'Objective', 'rank-math' ),
'Opinionated' => esc_html__( 'Opinionated', 'rank-math' ),
'Persuasive' => esc_html__( 'Persuasive', 'rank-math' ),
'Poetic' => esc_html__( 'Poetic', 'rank-math' ),
'Satirical' => esc_html__( 'Satirical', 'rank-math' ),
'Story-telling' => esc_html__( 'Story-telling', 'rank-math' ),
'Subjective' => esc_html__( 'Subjective', 'rank-math' ),
'Technical' => esc_html__( 'Technical', 'rank-math' ),
],
],
);
$cmb->add_field(
[
'id' => 'content_ai_audience',
'type' => 'select',
'name' => esc_html__( 'Default Audience', 'rank-math' ),
'desc' => esc_html__( 'This option lets you set the default audience that usually reads your content. You can override this in individual tools.', 'rank-math' ),
'default' => 'General Audience',
'attributes' => ( 'data-s2' ),
'options' => [
'Activists' => esc_html__( 'Activists', 'rank-math' ),
'Artists' => esc_html__( 'Artists', 'rank-math' ),
'Authors' => esc_html__( 'Authors', 'rank-math' ),
'Bargain Hunters' => esc_html__( 'Bargain Hunters', 'rank-math' ),
'Bloggers' => esc_html__( 'Bloggers', 'rank-math' ),
'Business Owners' => esc_html__( 'Business Owners', 'rank-math' ),
'Collectors' => esc_html__( 'Collectors', 'rank-math' ),
'Cooks' => esc_html__( 'Cooks', 'rank-math' ),
'Crafters' => esc_html__( 'Crafters', 'rank-math' ),
'Dancers' => esc_html__( 'Dancers', 'rank-math' ),
'DIYers' => esc_html__( 'DIYers', 'rank-math' ),
'Designers' => esc_html__( 'Designers', 'rank-math' ),
'Educators' => esc_html__( 'Educators', 'rank-math' ),
'Engineers' => esc_html__( 'Engineers', 'rank-math' ),
'Entrepreneurs' => esc_html__( 'Entrepreneurs', 'rank-math' ),
'Environmentalists' => esc_html__( 'Environmentalists', 'rank-math' ),
'Fashionistas' => esc_html__( 'Fashionistas', 'rank-math' ),
'Fitness Enthusiasts' => esc_html__( 'Fitness Enthusiasts', 'rank-math' ),
'Foodies' => esc_html__( 'Foodies', 'rank-math' ),
'Gaming Enthusiasts' => esc_html__( 'Gaming Enthusiasts', 'rank-math' ),
'Gardeners' => esc_html__( 'Gardeners', 'rank-math' ),
'General Audience' => esc_html__( 'General Audience', 'rank-math' ),
'Health Enthusiasts' => esc_html__( 'Health Enthusiasts', 'rank-math' ),
'Healthcare Professionals' => esc_html__( 'Healthcare Professionals', 'rank-math' ),
'Indoor Hobbyists' => esc_html__( 'Indoor Hobbyists', 'rank-math' ),
'Investors' => esc_html__( 'Investors', 'rank-math' ),
'Job Seekers' => esc_html__( 'Job Seekers', 'rank-math' ),
'Movie Buffs' => esc_html__( 'Movie Buffs', 'rank-math' ),
'Musicians' => esc_html__( 'Musicians', 'rank-math' ),
'Outdoor Enthusiasts' => esc_html__( 'Outdoor Enthusiasts', 'rank-math' ),
'Parents' => esc_html__( 'Parents', 'rank-math' ),
'Pet Owners' => esc_html__( 'Pet Owners', 'rank-math' ),
'Photographers' => esc_html__( 'Photographers', 'rank-math' ),
'Podcast Listeners' => esc_html__( 'Podcast Listeners', 'rank-math' ),
'Professionals' => esc_html__( 'Professionals', 'rank-math' ),
'Retirees' => esc_html__( 'Retirees', 'rank-math' ),
'Russian' => esc_html__( 'Russian', 'rank-math' ),
'Seniors' => esc_html__( 'Seniors', 'rank-math' ),
'Social Media Users' => esc_html__( 'Social Media Users', 'rank-math' ),
'Sports Fans' => esc_html__( 'Sports Fans', 'rank-math' ),
'Students' => esc_html__( 'Students', 'rank-math' ),
'Tech Enthusiasts' => esc_html__( 'Tech Enthusiasts', 'rank-math' ),
'Travelers' => esc_html__( 'Travelers', 'rank-math' ),
'TV Enthusiasts' => esc_html__( 'TV Enthusiasts', 'rank-math' ),
'Video Creators' => esc_html__( 'Video Creators', 'rank-math' ),
'Writers' => esc_html__( 'Writers', 'rank-math' ),
],
],
);
$cmb->add_field(
[
'id' => 'content_ai_language',
'type' => 'select',
'name' => esc_html__( 'Default Language', 'rank-math' ),
'desc' => esc_html__( 'This option lets you set the default language for content generated using Content AI. You can override this in individual tools.', 'rank-math' ),
'default' => Helper::content_ai_default_language(),
'attributes' => ( 'data-s2' ),
'options' => [
'US English' => esc_html__( 'US English', 'rank-math' ),
'UK English' => esc_html__( 'UK English', 'rank-math' ),
'Bulgarian' => esc_html__( 'Bulgarian', 'rank-math' ),
'Chinese' => esc_html__( 'Chinese', 'rank-math' ),
'Czech' => esc_html__( 'Czech', 'rank-math' ),
'Danish' => esc_html__( 'Danish', 'rank-math' ),
'Dutch' => esc_html__( 'Dutch', 'rank-math' ),
'Estonian' => esc_html__( 'Estonian', 'rank-math' ),
'Finnish' => esc_html__( 'Finnish', 'rank-math' ),
'French' => esc_html__( 'French', 'rank-math' ),
'German' => esc_html__( 'German', 'rank-math' ),
'Greek' => esc_html__( 'Greek', 'rank-math' ),
'Hebrew' => esc_html__( 'Hebrew', 'rank-math' ),
'Hungarian' => esc_html__( 'Hungarian', 'rank-math' ),
'Indonesian' => esc_html__( 'Indonesian', 'rank-math' ),
'Italian' => esc_html__( 'Italian', 'rank-math' ),
'Japanese' => esc_html__( 'Japanese', 'rank-math' ),
'Korean' => esc_html__( 'Korean', 'rank-math' ),
'Latvian' => esc_html__( 'Latvian', 'rank-math' ),
'Lithuanian' => esc_html__( 'Lithuanian', 'rank-math' ),
'Norwegian' => esc_html__( 'Norwegian', 'rank-math' ),
'Polish' => esc_html__( 'Polish', 'rank-math' ),
'Portuguese' => esc_html__( 'Portuguese', 'rank-math' ),
'Romanian' => esc_html__( 'Romanian', 'rank-math' ),
'Russian' => esc_html__( 'Russian', 'rank-math' ),
'Slovak' => esc_html__( 'Slovak', 'rank-math' ),
'Slovenian' => esc_html__( 'Slovenian', 'rank-math' ),
'Spanish' => esc_html__( 'Spanish', 'rank-math' ),
'Swedish' => esc_html__( 'Swedish', 'rank-math' ),
],
],
);
$post_types = Helper::choices_post_types();
if ( isset( $post_types['attachment'] ) ) {
unset( $post_types['attachment'] );
}
$cmb->add_field(
[
'id' => 'content_ai_post_types',
'type' => 'multicheck_inline',
'name' => esc_html__( 'Select Post Type', 'rank-math' ),
'desc' => esc_html__( 'Choose the type of posts/pages/CPTs where you want to use Content AI.', 'rank-math' ),
'options' => $post_types,
'default' => array_keys( $post_types ),
]
);
$credits = Helper::get_credits();
if ( Helper::is_site_connected() && false !== $credits ) {
$update_credits = '<a href="#" class="rank-math-tooltip update-credit">
<i class="dashicons dashicons-image-rotate"></i>
<span>' . esc_html__( 'Click to refresh the available credits.', 'rank-math' ) . '</span>
</a>';
$refresh_date = Helper::get_content_ai_refresh_date();
$cmb->add_field(
[
'id' => 'content_ai_credits',
'type' => 'raw',
/* translators: 1. Credits left 2. Buy more credits link */
'content' => '<div class="cmb-row buy-more-credits rank-math-exclude-from-search">' . $update_credits . sprintf( esc_html__( '%1$s credits left this month. Credits will renew on %2$s or you can upgrade to get more credits %3$s.', 'rank-math' ), '<strong>' . $credits . '</strong>', wp_date( 'Y-m-d g:i a', $refresh_date ), '<a href="' . KB::get( 'content-ai-pricing-tables', 'Buy CAI Credits Options Panel' ) . '" target="_blank">' . esc_html__( 'here', 'rank-math' ) . '</a>' ) . '</div>',
]
);
}
@@ -0,0 +1,204 @@
<?php
/**
* The AIOSEO Block Converter imports editor blocks (TOC) from AIOSEO to Rank Math.
*
* @since 1.0.104
* @package RankMath
* @subpackage RankMath\Status
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Tools;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* AIOSEO_Blocks class.
*/
class AIOSEO_Blocks extends \WP_Background_Process {
/**
* TOC Converter.
*
* @var AIOSEO_TOC_Converter
*/
private $toc_converter;
/**
* Action.
*
* @var string
*/
protected $action = 'convert_aioseo_blocks';
/**
* Main instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return AIOSEO_Blocks
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof AIOSEO_Blocks ) ) {
$instance = new AIOSEO_Blocks();
}
return $instance;
}
/**
* Start creating batches.
*
* @param array $posts Posts to process.
*/
public function start( $posts ) {
$chunks = array_chunk( $posts, 10 );
foreach ( $chunks as $chunk ) {
$this->push_to_queue( $chunk );
}
$this->save()->dispatch();
}
/**
* Complete.
*
* Override if applicable, but ensure that the below actions are
* performed, or, call parent::complete().
*/
protected function complete() {
$posts = get_option( 'rank_math_aioseo_block_posts' );
delete_option( 'rank_math_aioseo_block_posts' );
Helper::add_notification(
// Translators: placeholder is the number of modified posts.
sprintf( _n( 'Blocks successfully converted in %d post.', 'Blocks successfully converted in %d posts.', $posts['count'], 'rank-math' ), $posts['count'] ),
[
'type' => 'success',
'id' => 'rank_math_aioseo_block_posts',
'classes' => 'rank-math-notice',
]
);
parent::complete();
}
/**
* Task to perform.
*
* @param string $posts Posts to process.
*/
public function wizard( $posts ) {
$this->task( $posts );
}
/**
* Task to perform.
*
* @param array $posts Posts to process.
*
* @return bool
*/
protected function task( $posts ) {
try {
remove_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10 );
$this->toc_converter = new AIOSEO_TOC_Converter();
foreach ( $posts as $post_id ) {
$post = get_post( $post_id );
$this->convert( $post );
}
return false;
} catch ( Exception $error ) {
return true;
}
}
/**
* Convert post.
*
* @param WP_Post $post Post object.
*/
public function convert( $post ) {
$dirty = false;
$blocks = $this->parse_blocks( $post->post_content );
$content = '';
if ( isset( $blocks['aioseo/table-of-contents'] ) && ! empty( $blocks['aioseo/table-of-contents'] ) ) {
$dirty = true;
$content = $this->toc_converter->replace( $post->post_content, $blocks['aioseo/table-of-contents'] );
}
if ( $dirty ) {
$post->post_content = $content;
wp_update_post( $post );
}
}
/**
* Find posts with AIOSEO blocks.
*
* @return array
*/
public function find_posts() {
$posts = get_option( 'rank_math_aioseo_block_posts' );
if ( false !== $posts ) {
return $posts;
}
// TOC Posts.
$args = [
's' => 'wp:aioseo/table-of-contents ',
'post_status' => 'any',
'numberposts' => -1,
'fields' => 'ids',
'no_found_rows' => true,
'post_type' => 'any',
];
$toc_posts = get_posts( $args );
$posts_data = [
'posts' => $toc_posts,
'count' => count( $toc_posts ),
];
update_option( 'rank_math_aioseo_block_posts', $posts_data, false );
return $posts_data;
}
/**
* Parse blocks to get data.
*
* @param string $content Post content to parse.
*
* @return array
*/
private function parse_blocks( $content ) {
$parsed_blocks = parse_blocks( $content );
$blocks = [];
foreach ( $parsed_blocks as $block ) {
if ( empty( $block['blockName'] ) ) {
continue;
}
$name = strtolower( $block['blockName'] );
if ( ! isset( $blocks[ $name ] ) || ! is_array( $blocks[ $name ] ) ) {
$blocks[ $name ] = [];
}
if ( ! isset( $block['innerContent'] ) ) {
$block['innerContent'] = [];
}
if ( 'aioseo/table-of-contents' === $name ) {
$block = $this->toc_converter->convert( $block );
$blocks[ $name ][] = \serialize_block( $block );
}
}
return $blocks;
}
}
@@ -0,0 +1,114 @@
<?php
/**
* The AIOSEO TOC Block Converter.
*
* @since 1.0.104
* @package RankMath
* @subpackage RankMath\Status
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Tools;
defined( 'ABSPATH' ) || exit;
/**
* AIOSEO_TOC_Converter class.
*/
class AIOSEO_TOC_Converter {
/**
* Convert TOC blocks to Rank Math.
*
* @param array $block Block to convert.
*
* @return array
*/
public function convert( $block ) {
$attributes = $block['attrs'];
$headings = [];
$this->get_headings( $attributes['headings'], $headings );
$new_block = [
'blockName' => 'rank-math/toc-block',
'attrs' => [
'title' => '',
'headings' => $headings,
'listStyle' => $attributes['listStyle'] ?? 'ul',
'titleWrapper' => 'h2',
'excludeHeadings' => [],
],
];
$new_block['innerContent'][] = $this->get_html( $block['innerHTML'] );
return $new_block;
}
/**
* Replace block in content.
*
* @param string $post_content Post content.
* @param array $blocks Blocks.
*
* @return string
*/
public function replace( $post_content, $blocks ) {
preg_match_all( '/<!-- wp:aioseo\/table-of-contents.*-->.*<!-- \/wp:aioseo\/table-of-contents -->/iUs', $post_content, $matches );
foreach ( $matches[0] as $index => $match ) {
$post_content = \str_replace( $match, $blocks[ $index ], $post_content );
}
return $post_content;
}
/**
* Get headings from the content.
*
* @param array $data Block data.
* @param array $headings Headings.
*
* @return void
*/
public function get_headings( $data, &$headings ) {
foreach ( $data as $heading ) {
$headings[] = [
'key' => $heading['blockClientId'],
'link' => '#' . $heading['anchor'],
'content' => ! empty( $heading['editedContent'] ) ? $heading['editedContent'] : $heading['content'],
'level' => ! empty( $heading['editedLevel'] ) ? $heading['editedLevel'] : $heading['level'],
'disable' => ! empty( $heading['hidden'] ),
];
if ( ! empty( $heading['headings'] ) ) {
$this->get_headings( $heading['headings'], $headings );
}
}
}
/**
* Get TOC title.
*
* @param string $html Block HTML.
*
* @return string
*/
public function get_toc_title( $html ) {
preg_match( '#<h2.*?>(.*?)</h2>#i', $html, $found );
return ! empty( $found[1] ) ? $found[1] : '';
}
/**
* Generate HTML.
*
* @param string $html Block html.
*
* @return string
*/
private function get_html( $html ) {
$html = str_replace( 'wp-block-aioseo-table-of-contents', 'wp-block-rank-math-toc-block', $html );
$html = str_replace( '<div class="wp-block-rank-math-toc-block"><ul>', '<div class="wp-block-rank-math-toc-block"><nav><ul>', $html );
$html = str_replace( '</ul></div>', '</nav></ul></div>', $html );
return $html;
}
}
@@ -0,0 +1,450 @@
<?php
/**
* The Database_Tools is responsible for the Database Tools inside Status & Tools.
*
* @package RankMath
* @subpackage RankMath\Database_Tools
*/
namespace RankMath\Tools;
use RankMath\Helper;
use RankMath\Helpers\Str;
use RankMath\Installer;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Database_Tools class.
*/
class Database_Tools {
use Hooker;
/**
* Constructor.
*/
public function __construct() {
if ( Helper::is_heartbeat() || ! Helper::is_advanced_mode() ) {
return;
}
Yoast_Blocks::get();
AIOSEO_Blocks::get();
Remove_Schema::get();
Update_Score::get();
$this->hooks();
}
/**
* Register version control hooks.
*/
public function hooks() {
if ( ! Helper::is_plugin_active_for_network() || current_user_can( 'manage_options' ) ) {
$this->filter( 'rank_math/tools/pages', 'add_tools_page', 11 );
}
if ( Helper::is_rest() && Str::ends_with( 'toolsAction', add_query_arg( [] ) ) ) {
foreach ( $this->get_tools() as $id => $tool ) {
if ( ! method_exists( $this, $id ) ) {
continue;
}
add_filter( 'rank_math/tools/' . $id, [ $this, $id ] );
}
}
}
/**
* Display Tools data.
*/
public function display() {
?>
<table class='rank-math-status-table striped rank-math-tools-table widefat rank-math-box'>
<tbody class='tools'>
<?php foreach ( $this->get_tools() as $id => $tool ) : ?>
<tr class='<?php echo sanitize_html_class( $id ); ?>'>
<th>
<h4 class='name'><?php echo esc_html( $tool['title'] ); ?></h4>
<p class="description"><?php echo esc_html( $tool['description'] ); ?></p>
</th>
<td class='run-tool'>
<a href='#' class='button button-large button-link-delete tools-action' data-action='<?php echo esc_attr( $id ); ?>' data-confirm="<?php echo isset( $tool['confirm_text'] ) ? esc_attr( $tool['confirm_text'] ) : 'false'; ?>"><?php echo esc_html( $tool['button_text'] ); ?></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
}
/**
* Function to clear all the transients from the database.
*/
public function clear_transients() {
global $wpdb;
$transients = $wpdb->get_col(
"SELECT `option_name` AS `name`
FROM $wpdb->options
WHERE `option_name` LIKE '%\\_transient\\_rank_math%'
ORDER BY `option_name`"
);
if ( empty( $transients ) ) {
return [
'status' => 'error',
'message' => __( 'No Rank Math transients found.', 'rank-math' ),
];
}
$count = 0;
foreach ( $transients as $transient ) {
delete_option( $transient );
$count++;
}
// Translators: placeholder is the number of transients deleted.
return sprintf( _n( '%d Rank Math transient cleared.', '%d Rank Math transients cleared.', $count, 'rank-math' ), $count );
}
/**
* Function to reset the SEO Analyzer.
*/
public function clear_seo_analysis() {
$stored = get_option( 'rank_math_seo_analysis_results' );
if ( empty( $stored ) ) {
return [
'status' => 'error',
'message' => __( 'SEO Analyzer data has already been cleared.', 'rank-math' ),
];
}
delete_option( 'rank_math_seo_analysis_results' );
delete_option( 'rank_math_seo_analysis_date' );
return __( 'SEO Analyzer data successfully deleted.', 'rank-math' );
}
/**
* Function to delete all the Internal Links data.
*/
public function delete_links() {
global $wpdb;
$exists = $wpdb->get_var( "SELECT EXISTS ( SELECT 1 FROM {$wpdb->prefix}rank_math_internal_links )" );
if ( empty( $exists ) ) {
return [
'status' => 'error',
'message' => __( 'No Internal Links data found.', 'rank-math' ),
];
}
$wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}rank_math_internal_links" );
$wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}rank_math_internal_meta" );
return __( 'Internal Links successfully deleted.', 'rank-math' );
}
/**
* Function to delete all the 404 log items.
*/
public function delete_log() {
global $wpdb;
$exists = $wpdb->get_var( "SELECT EXISTS ( SELECT 1 FROM {$wpdb->prefix}rank_math_404_logs )" );
if ( empty( $exists ) ) {
return [
'status' => 'error',
'message' => __( 'No 404 log data found.', 'rank-math' ),
];
}
$wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}rank_math_404_logs;" );
return __( '404 Log successfully deleted.', 'rank-math' );
}
/**
* Function to delete all Redirections data.
*/
public function delete_redirections() {
global $wpdb;
$exists = $wpdb->get_var( "SELECT EXISTS ( SELECT 1 FROM {$wpdb->prefix}rank_math_redirections )" );
if ( empty( $exists ) ) {
return [
'status' => 'error',
'message' => __( 'No Redirections found.', 'rank-math' ),
];
}
$wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}rank_math_redirections;" );
$wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}rank_math_redirections_cache;" );
return __( 'Redirection rules successfully deleted.', 'rank-math' );
}
/**
* Re-create Database Tables.
*
* @return string
*/
public function recreate_tables() {
// Base.
Installer::create_tables( get_option( 'rank_math_modules', [] ) );
// ActionScheduler.
$this->maybe_recreate_actionscheduler_tables();
// Analytics module.
if ( Helper::is_module_active( 'analytics' ) ) {
as_enqueue_async_action(
'rank_math/analytics/workflow/create_tables',
[],
'rank-math'
);
}
return __( 'Table re-creation started. It might take a couple of minutes.', 'rank-math' );
}
/**
* Recreate ActionScheduler tables if missing.
*/
public function maybe_recreate_actionscheduler_tables() {
global $wpdb;
if ( Helper::is_woocommerce_active() ) {
return;
}
if (
! class_exists( 'ActionScheduler_HybridStore' )
|| ! class_exists( 'ActionScheduler_StoreSchema' )
|| ! class_exists( 'ActionScheduler_LoggerSchema' )
) {
return;
}
$table_list = [
'actionscheduler_actions',
'actionscheduler_logs',
'actionscheduler_groups',
'actionscheduler_claims',
];
$found_tables = $wpdb->get_col( "SHOW TABLES LIKE '{$wpdb->prefix}actionscheduler%'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
foreach ( $table_list as $table_name ) {
if ( ! in_array( $wpdb->prefix . $table_name, $found_tables, true ) ) {
$this->recreate_actionscheduler_tables();
return;
}
}
}
/**
* Force the data store schema updates.
*/
public function recreate_actionscheduler_tables() {
$store = new \ActionScheduler_HybridStore();
add_action( 'action_scheduler/created_table', [ $store, 'set_autoincrement' ], 10, 2 );
$store_schema = new \ActionScheduler_StoreSchema();
$logger_schema = new \ActionScheduler_LoggerSchema();
$store_schema->register_tables( true );
$logger_schema->register_tables( true );
remove_action( 'action_scheduler/created_table', [ $store, 'set_autoincrement' ], 10 );
}
/**
* Function to convert Yoast blocks in posts to Rank Math blocks (FAQ & HowTo).
*
* @return string
*/
public function yoast_blocks() {
$posts = Yoast_Blocks::get()->find_posts();
if ( empty( $posts['posts'] ) ) {
return [
'status' => 'error',
'message' => __( 'No posts found to convert.', 'rank-math' ),
];
}
Yoast_Blocks::get()->start( $posts['posts'] );
return __( 'Conversion started. A success message will be shown here once the process completes. You can close this page.', 'rank-math' );
}
/**
* Function to convert AIOSEO blocks in posts to Rank Math blocks (TOC).
*
* @return string
*/
public function aioseo_blocks() {
$posts = AIOSEO_Blocks::get()->find_posts();
if ( empty( $posts['posts'] ) ) {
return [
'status' => 'error',
'message' => __( 'No posts found to convert.', 'rank-math' ),
];
}
AIOSEO_Blocks::get()->start( $posts['posts'] );
return __( 'Conversion started. A success message will be shown here once the process completes. You can close this page.', 'rank-math' );
}
/**
* Function to delete old schema data.
*
* @return string
*/
public function delete_old_schema() {
$meta = Remove_Schema::get()->find();
if ( empty( $meta ) ) {
return [
'status' => 'error',
'message' => __( 'No data found to delete.', 'rank-math' ),
];
}
Remove_Schema::get()->start( $meta );
return __( 'Deletion started. A success message will be shown here once the process completes. You can close this page.', 'rank-math' );
}
/**
* Add subpage to Status & Tools screen.
*
* @param array $pages Pages.
* @return array New pages.
*/
public function add_tools_page( $pages ) {
$pages['tools'] = [
'url' => 'status',
'args' => 'view=tools',
'cap' => 'manage_options',
'title' => __( 'Database Tools', 'rank-math' ),
'class' => '\\RankMath\\Tools\\Database_Tools',
];
return $pages;
}
/**
* Get tools.
*
* @return array
*/
private function get_tools() {
$tools = [];
if ( Helper::is_module_active( 'seo-analysis' ) ) {
$tools['clear_seo_analysis'] = [
'title' => __( 'Flush SEO Analyzer Data', 'rank-math' ),
'description' => __( "Need a clean slate or not able to run the SEO Analyzer tool? Flushing the analysis data might fix the issue. Flushing SEO Analyzer data is entirely safe and doesn't remove any critical data from your website.", 'rank-math' ),
'button_text' => __( 'Clear SEO Analyzer', 'rank-math' ),
];
}
$tools['clear_transients'] = [
'title' => __( 'Remove Rank Math Transients', 'rank-math' ),
'description' => __( 'If you see any issue while using Rank Math or one of its options - clearing the Rank Math transients fixes the problem in most cases. Deleting transients does not delete ANY data added using Rank Math.', 'rank-math' ),
'button_text' => __( 'Remove transients', 'rank-math' ),
];
if ( Helper::is_module_active( '404-monitor' ) ) {
$tools['delete_log'] = [
'title' => __( 'Clear 404 Log', 'rank-math' ),
'description' => __( 'Is the 404 error log getting out of hand? Use this option to clear ALL 404 logs generated by your website in the Rank Math 404 Monitor.', 'rank-math' ),
'confirm_text' => __( 'Are you sure you want to delete the 404 log? This action is irreversible.', 'rank-math' ),
'button_text' => __( 'Clear 404 Log', 'rank-math' ),
];
}
$tools['recreate_tables'] = [
'title' => __( 'Re-create Missing Database Tables', 'rank-math' ),
'description' => __( 'Check if required tables exist and create them if not.', 'rank-math' ),
'button_text' => __( 'Re-create Tables', 'rank-math' ),
];
if ( Helper::is_module_active( 'analytics' ) ) {
$tools['analytics_fix_collations'] = [
'title' => __( 'Fix Analytics table collations', 'rank-math' ),
'description' => __( 'In some cases, the Analytics database tables or columns don\'t match with each other, which can cause database errors. This tool can fix that issue.', 'rank-math' ),
'button_text' => __( 'Fix Collations', 'rank-math' ),
];
}
$block_posts = Yoast_Blocks::get()->find_posts();
if ( is_array( $block_posts ) && ! empty( $block_posts['count'] ) ) {
$tools['yoast_blocks'] = [
'title' => __( 'Yoast Block Converter', 'rank-math' ),
'description' => __( 'Convert FAQ, HowTo, & Table of Contents Blocks created using Yoast. Use this option to easily move your previous blocks into Rank Math.', 'rank-math' ),
'confirm_text' => __( 'Are you sure you want to convert Yoast blocks into Rank Math blocks? This action is irreversible.', 'rank-math' ),
'button_text' => __( 'Convert Blocks', 'rank-math' ),
];
}
$aio_block_posts = AIOSEO_Blocks::get()->find_posts();
if ( is_array( $aio_block_posts ) && ! empty( $aio_block_posts['count'] ) ) {
$tools['aioseo_blocks'] = [
'title' => __( 'AIOSEO Block Converter', 'rank-math' ),
'description' => __( 'Convert TOC block created using AIOSEO. Use this option to easily move your previous blocks into Rank Math.', 'rank-math' ),
'confirm_text' => __( 'Are you sure you want to convert AIOSEO blocks into Rank Math blocks? This action is irreversible.', 'rank-math' ),
'button_text' => __( 'Convert Blocks', 'rank-math' ),
];
}
if ( Helper::is_module_active( 'link-counter' ) ) {
$tools['delete_links'] = [
'title' => __( 'Delete Internal Links Data', 'rank-math' ),
'description' => __( 'In some instances, the internal links data might show an inflated number or no number at all. Deleting the internal links data might fix the issue.', 'rank-math' ),
'confirm_text' => __( 'Are you sure you want to delete Internal Links Data? This action is irreversible.', 'rank-math' ),
'button_text' => __( 'Delete Internal Links', 'rank-math' ),
];
}
if ( Helper::is_module_active( 'redirections' ) ) {
$tools['delete_redirections'] = [
'title' => __( 'Delete Redirections Rules', 'rank-math' ),
'description' => __( 'Getting a redirection loop or need a fresh start? Delete all the redirections using this tool. Note: This process is irreversible and will delete ALL your redirection rules.', 'rank-math' ),
'confirm_text' => __( 'Are you sure you want to delete all the Redirection Rules? This action is irreversible.', 'rank-math' ),
'button_text' => __( 'Delete Redirections', 'rank-math' ),
];
}
if ( Helper::is_module_active( 'rich-snippet' ) && ! empty( Remove_Schema::get()->find() ) ) {
$tools['delete_old_schema'] = [
'title' => __( 'Delete Old Schema Data', 'rank-math' ),
'description' => __( 'Delete the schema data from the old format (<1.0.48). Note: This process is irreversible and will delete all the metadata prefixed with rank_math_snippet.', 'rank-math' ),
'confirm_text' => __( 'Are you sure you want to delete the old schema data? This action is irreversible.', 'rank-math' ),
'button_text' => __( 'Delete', 'rank-math' ),
];
}
if ( ! empty( Update_Score::get()->find() ) ) {
$tools['update_seo_score'] = [
'title' => __( 'Update SEO Scores', 'rank-math' ),
'description' => __( 'This tool will calculate the SEO score for the posts/pages that have a Focus Keyword set. Note: This process may take some time and the browser tab must be kept open while it is running.', 'rank-math' ),
'button_text' => __( 'Recalculate Scores', 'rank-math' ),
];
}
/**
* Filters the list of tools available on the Database Tools page.
*
* @param array $tools The tools.
*/
$tools = $this->do_filter( 'database/tools', $tools );
return $tools;
}
}
@@ -0,0 +1,123 @@
<?php
/**
* The Schema Remover tool to remove the meta data from the old format (<1.0.48).
*
* @since 1.0.65
* @package RankMath
* @subpackage RankMath\Status
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Tools;
use RankMath\Helper;
use RankMath\Admin\Database\Database;
defined( 'ABSPATH' ) || exit;
/**
* Remove_Schema class.
*/
class Remove_Schema extends \WP_Background_Process {
/**
* Action.
*
* @var string
*/
protected $action = 'delete_old_schema';
/**
* Main instance
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Remove_Schema
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Remove_Schema ) ) {
$instance = new Remove_Schema();
}
return $instance;
}
/**
* Start creating batches.
*
* @param array $meta_ids Posts to process.
*/
public function start( $meta_ids ) {
$chunks = array_chunk( $meta_ids, 100 );
foreach ( $chunks as $chunk ) {
$this->push_to_queue( $chunk );
}
$this->save()->dispatch();
}
/**
* Complete.
*
* Override if applicable, but ensure that the below actions are
* performed, or, call parent::complete().
*/
protected function complete() {
$meta_ids = get_option( 'rank_math_old_schema_data' );
delete_option( 'rank_math_old_schema_data' );
Helper::add_notification(
sprintf( 'Rank Math: Deleted %d schema data successfully.', count( $meta_ids ) ),
[
'type' => 'success',
'id' => 'rank_math_remove_old_schema_data',
'classes' => 'rank-math-notice',
]
);
parent::complete();
}
/**
* Task to perform
*
* @param array $meta_ids Posts to process.
*
* @return bool
*/
protected function task( $meta_ids ) {
try {
foreach ( $meta_ids as $meta_id ) {
delete_metadata_by_mid( 'post', $meta_id );
}
return false;
} catch ( \Exception $error ) {
return true;
}
}
/**
* Find posts with old schema data.
*
* @return array
*/
public function find() {
$meta_ids = get_option( 'rank_math_old_schema_data' );
if ( false !== $meta_ids ) {
return $meta_ids;
}
// Schema Metadata.
$meta_ids = Database::table( 'postmeta' )
->distinct()
->select( 'meta_id' )
->whereLike( 'meta_key', 'rank_math_snippet', '' )
->get();
$meta_ids = wp_list_pluck( $meta_ids, 'meta_id' );
update_option( 'rank_math_old_schema_data', $meta_ids, false );
return $meta_ids;
}
}
@@ -0,0 +1,314 @@
<?php
/**
* The tool to update SEO score on existing posts.
*
* @since 1.0.97
* @package RankMath
* @subpackage RankMath\Status
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Tools;
use RankMath\Helper;
use RankMath\Helpers\Param;
use RankMath\Traits\Hooker;
use RankMath\Paper\Paper;
use RankMath\Admin\Metabox\Screen;
use RankMath\Schema\DB;
defined( 'ABSPATH' ) || exit;
/**
* Update_Score class.
*/
class Update_Score {
use Hooker;
/**
* Batch size.
*
* @var int
*/
private $batch_size;
/**
* Screen object.
*
* @var object
*/
public $screen;
/**
* Constructor.
*/
public function __construct() {
$this->batch_size = absint( apply_filters( 'rank_math/recalculate_scores_batch_size', 25 ) );
$this->action( 'admin_footer', 'footer_modal' );
$this->filter( 'rank_math/tools/update_seo_score', 'update_seo_score' );
$this->screen = new Screen();
$this->screen->load_screen( 'post' );
if ( Param::get( 'page' ) === 'rank-math-status' && Param::get( 'view' ) === 'tools' ) {
$this->action( 'admin_enqueue_scripts', 'enqueue' );
}
}
/**
* Enqueue scripts & add JSON data needed to update the SEO score on existing posts.
*/
public function enqueue() {
$scripts = [
'lodash' => '',
'wp-data' => '',
'wp-core-data' => '',
'wp-compose' => '',
'wp-components' => '',
'wp-element' => '',
'wp-block-editor' => '',
'rank-math-analyzer' => rank_math()->plugin_url() . 'assets/admin/js/analyzer.js',
];
foreach ( $scripts as $handle => $src ) {
wp_enqueue_script( $handle, $src, [], rank_math()->version, true );
}
global $post;
$temp_post = $post;
if ( is_null( $post ) ) {
$posts = get_posts(
[
'fields' => 'id',
'posts_per_page' => 1,
'post_type' => $this->get_post_types(),
]
);
$post = isset( $posts[0] ) ? $posts[0] : null;
}
$this->screen->localize();
$post = $temp_post;
Helper::add_json( 'totalPostsWithoutScore', $this->find( false ) );
Helper::add_json( 'totalPosts', $this->find( true ) );
Helper::add_json( 'batchSize', $this->batch_size );
}
/**
* Function to Update the SEO score.
*/
public function update_seo_score() {
$args = Param::post( 'args', [], FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
$offset = isset( $args['offset'] ) ? absint( $args['offset'] ) : 0;
// We get "paged" when running from the importer.
$paged = Param::post( 'paged', 0 );
if ( $paged ) {
$offset = ( $paged - 1 ) * $this->batch_size;
}
$update_all = ! isset( $args['update_all_scores'] ) || ! empty( $args['update_all_scores'] );
$query_args = [
'post_type' => $this->get_post_types(),
'posts_per_page' => $this->batch_size,
'offset' => $offset,
'orderby' => 'ID',
'order' => 'ASC',
'status' => 'any',
'meta_query' => [
'relation' => 'AND',
[
'key' => 'rank_math_focus_keyword',
'value' => '',
'compare' => '!=',
],
],
];
if ( ! $update_all ) {
$query_args['meta_query'][] = [
'relation' => 'OR',
[
'key' => 'rank_math_seo_score',
'compare' => 'NOT EXISTS',
],
[
'key' => 'rank_math_seo_score',
'value' => '',
'compare' => '=',
],
];
}
$posts = get_posts( $query_args );
if ( empty( $posts ) ) {
return 'complete'; // Don't translate this string.
}
add_filter(
'rank_math/replacements/non_cacheable',
function( $non_cacheable ) {
$non_cacheable[] = 'excerpt';
$non_cacheable[] = 'excerpt_only';
$non_cacheable[] = 'seo_description';
$non_cacheable[] = 'keywords';
$non_cacheable[] = 'focuskw';
return $non_cacheable;
}
);
rank_math()->variables->setup();
$data = [];
foreach ( $posts as $post ) {
$post_id = $post->ID;
$post_type = $post->post_type;
$title = get_post_meta( $post_id, 'rank_math_title', true );
$title = $title ? $title : Paper::get_from_options( "pt_{$post_type}_title", $post, '%title% %sep% %sitename%' );
$keywords = array_map( 'trim', explode( ',', Helper::get_post_meta( 'focus_keyword', $post_id ) ) );
$keyword = $keywords[0];
$values = [
'title' => Helper::replace_vars( '%seo_title%', $post ),
'description' => Helper::replace_vars( '%seo_description%', $post ),
'keywords' => $keywords,
'keyword' => $keyword,
'content' => $post->post_content,
'url' => urldecode( get_the_permalink( $post_id ) ),
'hasContentAi' => ! empty( Helper::get_post_meta( 'contentai_score', $post_id ) ),
];
if ( has_post_thumbnail( $post_id ) ) {
$thumbnail_id = get_post_thumbnail_id( $post_id );
$values['thumbnail'] = get_the_post_thumbnail_url( $post_id );
$values['thumbnailAlt'] = get_post_meta( $thumbnail_id, '_wp_attachment_image_alt', true );
}
if (
( Helper::is_woocommerce_active() && 'product' === $post_type ) ||
( Helper::is_edd_active() && 'download' === $post_type )
) {
$values['isProduct'] = true;
$values['isReviewEnabled'] = 'yes' === get_option( 'woocommerce_enable_reviews', 'yes' );
$schemas = DB::get_schemas( $post_id );
if ( empty( $schemas ) && Helper::get_default_schema_type( $post_id ) ) {
$schemas = [
[ '@type' => Helper::get_default_schema_type( $post_id ) ],
];
}
$schemas = array_filter(
$schemas,
function( $schema ) {
return in_array( $schema['@type'], [ 'WooCommerceProduct', 'EDDProduct', 'Product' ], true );
}
);
$values['hasProductSchema'] = ! empty( $schemas );
}
/**
* Filter the values sent to the analyzer to calculate the SEO score.
*
* @param array $values The values to be sent to the analyzer.
*/
$data[ $post_id ] = $this->do_filter( 'recalculate_score/data', $values, $post_id );
}
return $data;
}
/**
* Ensure only one instance is loaded or can be loaded.
*
* @return Update_Score
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Update_Score ) ) {
$instance = new Update_Score();
}
return $instance;
}
/**
* Find posts with focus keyword but no SEO score.
*
* @param bool $update_all Whether to update all posts or only those without a score.
* @return int
*/
public function find( $update_all = true ) {
global $wpdb;
$post_types = $this->get_post_types();
$placeholder = implode( ', ', array_fill( 0, count( $post_types ), '%s' ) );
$query = "SELECT COUNT(ID) FROM {$wpdb->posts} as p
LEFT JOIN {$wpdb->postmeta} as pm ON p.ID = pm.post_id AND pm.meta_key = 'rank_math_focus_keyword'
WHERE p.post_type IN ({$placeholder}) AND p.post_status = 'publish' AND pm.meta_value != ''";
if ( ! $update_all ) {
$query .= " AND (SELECT COUNT(*) FROM {$wpdb->postmeta} as pm2 WHERE pm2.post_id = p.ID AND pm2.meta_key = 'rank_math_seo_score' AND pm2.meta_value != '') = 0";
}
$update_score_post_ids = $wpdb->get_var( $wpdb->prepare( $query, $post_types ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- It's prepared above.
return (int) $update_score_post_ids;
}
/**
* Modal to show the Update SEO Score progress.
*
* @return array
*/
public function footer_modal() {
if ( Param::get( 'page' ) !== 'rank-math-status' || Param::get( 'view' ) !== 'tools' ) {
return;
}
?>
<div class="rank-math-modal rank-math-modal-update-score">
<div class="rank-math-modal-content">
<div class="rank-math-modal-header">
<h3><?php esc_html_e( 'Recalculating SEO Scores', 'rank-math' ); ?></h3>
<p><?php esc_html_e( 'This process may take a while. Please keep this window open until the process is complete.', 'rank-math' ); ?></p>
</div>
<div class="rank-math-modal-body">
<div class="count">
<?php esc_html_e( 'Calculated:', 'rank-math' ); ?> <span class="update-posts-done">0</span> / <span class="update-posts-total"><?php echo esc_html( $this->find() ); ?></span>
</div>
<div class="progress-bar">
<span></span>
</div>
<div class="rank-math-modal-footer hidden">
<p>
<?php esc_html_e( 'The SEO Scores have been recalculated successfully!', 'rank-math' ); ?>
</p>
<button class="button button-large rank-math-modal-close"><?php esc_html_e( 'Close', 'rank-math' ); ?></button>
</div>
</div>
</div>
</div>
<?php
}
/**
* Get post types.
*
* @return array
*/
private function get_post_types() {
$post_types = get_post_types( [ 'public' => true ] );
if ( isset( $post_types['attachment'] ) ) {
unset( $post_types['attachment'] );
}
return $this->do_filter( 'tool/post_types', array_keys( $post_types ) );
}
}
@@ -0,0 +1,271 @@
<?php
/**
* The Yoast Block Converter imports editor blocks (FAQ, HowTo, Local Business) from Yoast to Rank Math.
*
* @since 1.0.37
* @package RankMath
* @subpackage RankMath\Status
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Tools;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Yoast_Blocks class.
*/
class Yoast_Blocks extends \WP_Background_Process {
/**
* FAQ Converter.
*
* @var Yoast_FAQ_Converter
*/
private $faq_converter;
/**
* FAQ Converter.
*
* @var Yoast_HowTo_Converter
*/
private $howto_converter;
/**
* TOC Converter.
*
* @var Yoast_TOC_Converter
*/
private $toc_converter;
/**
* Local Converter.
*
* @var Yoast_Local_Converter
*/
private $local_converter;
/**
* Action.
*
* @var string
*/
protected $action = 'convert_yoast_blocks';
/**
* Main instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Yoast_Blocks
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Yoast_Blocks ) ) {
$instance = new Yoast_Blocks();
}
return $instance;
}
/**
* Start creating batches.
*
* @param array $posts Posts to process.
*/
public function start( $posts ) {
$chunks = array_chunk( $posts, 10 );
foreach ( $chunks as $chunk ) {
$this->push_to_queue( $chunk );
}
$this->save()->dispatch();
}
/**
* Complete.
*
* Override if applicable, but ensure that the below actions are
* performed, or, call parent::complete().
*/
protected function complete() {
$posts = get_option( 'rank_math_yoast_block_posts' );
delete_option( 'rank_math_yoast_block_posts' );
Helper::add_notification(
// Translators: placeholder is the number of modified posts.
sprintf( _n( 'Blocks successfully converted in %d post.', 'Blocks successfully converted in %d posts.', $posts['count'], 'rank-math' ), $posts['count'] ),
[
'type' => 'success',
'id' => 'rank_math_yoast_block_posts',
'classes' => 'rank-math-notice',
]
);
parent::complete();
}
/**
* Task to perform.
*
* @param string $posts Posts to process.
*/
public function wizard( $posts ) {
$this->task( $posts );
}
/**
* Task to perform.
*
* @param array $posts Posts to process.
*
* @return bool
*/
protected function task( $posts ) {
try {
remove_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10 );
$this->faq_converter = new Yoast_FAQ_Converter();
$this->howto_converter = new Yoast_HowTo_Converter();
$this->local_converter = new Yoast_Local_Converter();
$this->toc_converter = new Yoast_TOC_Converter();
foreach ( $posts as $post_id ) {
$post = get_post( $post_id );
$this->convert( $post );
}
return false;
} catch ( Exception $error ) {
return true;
}
}
/**
* Convert post.
*
* @param WP_Post $post Post object.
*/
public function convert( $post ) {
$dirty = false;
$blocks = $this->parse_blocks( $post->post_content );
$content = '';
if ( isset( $blocks['yoast/faq-block'] ) && ! empty( $blocks['yoast/faq-block'] ) ) {
$dirty = true;
$content = $this->faq_converter->replace( $post->post_content, $blocks['yoast/faq-block'] );
}
if ( isset( $blocks['yoast/how-to-block'] ) && ! empty( $blocks['yoast/how-to-block'] ) ) {
$dirty = true;
$content = $this->howto_converter->replace( $post->post_content, $blocks['yoast/how-to-block'] );
}
if ( isset( $blocks['yoast-seo/table-of-contents'] ) && ! empty( $blocks['yoast-seo/table-of-contents'] ) ) {
$dirty = true;
$content = $this->toc_converter->replace( $post->post_content, $blocks['yoast-seo/table-of-contents'] );
}
if ( ! empty( array_intersect( array_keys( $blocks ), $this->local_converter->yoast_blocks ) ) ) {
$dirty = true;
$content = $this->local_converter->replace( $post->post_content, $blocks );
}
if ( $dirty ) {
$post->post_content = $content;
wp_update_post( $post );
}
}
/**
* Find posts with Yoast blocks.
*
* @return array
*/
public function find_posts() {
$posts = get_option( 'rank_math_yoast_block_posts' );
if ( false !== $posts ) {
return $posts;
}
// FAQs Posts.
$args = [
's' => 'wp:yoast/faq-block',
'post_status' => 'any',
'numberposts' => -1,
'fields' => 'ids',
'no_found_rows' => true,
'post_type' => 'any',
];
$faqs = get_posts( $args );
// HowTo Posts.
$args['s'] = 'wp:yoast/how-to-block';
$howto = get_posts( $args );
// TOC Posts.
$args['s'] = 'wp:yoast-seo/table-of-contents';
$toc = get_posts( $args );
// Local Business Posts.
$args['s'] = ':yoast-seo-local/';
$local_business = get_posts( $args );
$posts = array_merge( $faqs, $howto, $toc, $local_business );
$posts_data = [
'posts' => $posts,
'count' => count( $posts ),
];
update_option( 'rank_math_yoast_block_posts', $posts_data, false );
return $posts_data;
}
/**
* Parse blocks to get data.
*
* @param string $content Post content to parse.
*
* @return array
*/
private function parse_blocks( $content ) {
$parsed_blocks = parse_blocks( $content );
$blocks = [];
foreach ( $parsed_blocks as $block ) {
if ( empty( $block['blockName'] ) ) {
continue;
}
$name = strtolower( $block['blockName'] );
if ( ! isset( $blocks[ $name ] ) || ! is_array( $blocks[ $name ] ) ) {
$blocks[ $name ] = [];
}
if ( ! isset( $block['innerContent'] ) ) {
$block['innerContent'] = [];
}
if ( 'yoast/faq-block' === $name ) {
$block = $this->faq_converter->convert( $block );
$blocks[ $name ][] = \serialize_block( $block );
}
if ( 'yoast/how-to-block' === $name ) {
$block = $this->howto_converter->convert( $block );
$blocks[ $name ][] = \serialize_block( $block );
}
if ( 'yoast-seo/table-of-contents' === $name ) {
$block = $this->toc_converter->convert( $block );
$blocks[ $name ][] = \serialize_block( $block );
}
if ( in_array( $name, $this->local_converter->yoast_blocks, true ) ) {
$block = $this->local_converter->convert( $block );
$blocks[ $name ][] = \serialize_block( $block );
}
}
return $blocks;
}
}
@@ -0,0 +1,118 @@
<?php
/**
* The Yoast FAQ Block Converter.
*
* @since 1.0.37
* @package RankMath
* @subpackage RankMath\Status
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Tools;
defined( 'ABSPATH' ) || exit;
/**
* Yoast_FAQ_Converter class.
*/
class Yoast_FAQ_Converter {
/**
* Convert FAQ blocks to Rank Math.
*
* @param array $block Block to convert.
*
* @return array
*/
public function convert( $block ) {
$new_block = [
'blockName' => 'rank-math/faq-block',
'attrs' => [
'listStyle' => '',
'textAlign' => 'left',
'titleWrapper' => 'h3',
'listCssClasses' => '',
'titleCssClasses' => '',
'contentCssClasses' => '',
'questions' => array_map( [ $this, 'get_question' ], $block['attrs']['questions'] ),
'className' => isset( $block['attrs']['className'] ) ? $block['attrs']['className'] : '',
],
];
$new_block['innerContent'][] = $this->get_html( $new_block['attrs'] );
return $new_block;
}
/**
* Replace block in content.
*
* @param string $post_content Post content.
* @param array $blocks Blocks.
*
* @return string
*/
public function replace( $post_content, $blocks ) {
preg_match_all( '/<!-- wp:yoast\/faq-block.*-->.*<!-- \/wp:yoast\/faq-block -->/iUs', $post_content, $matches );
foreach ( $matches[0] as $index => $match ) {
$post_content = \str_replace( $match, $blocks[ $index ], $post_content );
}
return $post_content;
}
/**
* Format questions.
*
* @param array $question Question.
*
* @return array
*/
public function get_question( $question ) {
return [
'id' => uniqid( 'faq-question-' ),
'visible' => true,
'title' => $question['jsonQuestion'],
'content' => $question['jsonAnswer'],
];
}
/**
* Generate HTML.
*
* @param array $attributes Block attributes.
*
* @return string
*/
private function get_html( $attributes ) {
// HTML.
$out = [ '<div class="wp-block-rank-math-faq-block">' ];
// Questions.
foreach ( $attributes['questions'] as $question ) {
if ( empty( $question['title'] ) || empty( $question['content'] ) || empty( $question['visible'] ) ) {
continue;
}
$out[] = '<div class="rank-math-faq-item">';
$out[] = sprintf(
'<%1$s class="rank-math-question">%2$s</%1$s>',
$attributes['titleWrapper'],
$question['title']
);
$out[] = sprintf(
'<div class="rank-math-answer">%2$s</div>',
$attributes['titleWrapper'],
$question['content']
);
$out[] = '</div>';
}
$out[] = '</div>';
return join( '', $out );
}
}
@@ -0,0 +1,129 @@
<?php
/**
* The Yoast HowTo Block Converter.
*
* @since 1.0.37
* @package RankMath
* @subpackage RankMath\Status
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Tools;
defined( 'ABSPATH' ) || exit;
/**
* Yoast_HowTo_Converter class.
*/
class Yoast_HowTo_Converter {
/**
* Convert HowTo blocks to Rank Math.
*
* @param array $block Block to convert.
*
* @return array
*/
public function convert( $block ) {
$attrs = $block['attrs'];
$new_block = [
'blockName' => 'rank-math/howto-block',
'attrs' => [
'hasDuration' => isset( $attrs['hasDuration'] ) ? $attrs['hasDuration'] : '',
'days' => isset( $attrs['days'] ) ? $attrs['days'] : '',
'hours' => isset( $attrs['hours'] ) ? $attrs['hours'] : '',
'minutes' => isset( $attrs['minutes'] ) ? $attrs['minutes'] : '',
'description' => isset( $attrs['jsonDescription'] ) ? $attrs['jsonDescription'] : '',
'listStyle' => isset( $attrs['unorderedList'] ) && $attrs['unorderedList'] ? 'ol' : '',
'timeLabel' => isset( $attrs['durationText'] ) ? $attrs['durationText'] : '',
'textAlign' => 'left',
'titleWrapper' => 'h3',
'listCssClasses' => '',
'titleCssClasses' => '',
'contentCssClasses' => '',
'steps' => array_map( [ $this, 'get_step' ], $attrs['steps'] ),
'className' => isset( $attrs['className'] ) ? $attrs['className'] : '',
],
];
$new_block['innerContent'][] = $this->get_html( $new_block['attrs'] );
return $new_block;
}
/**
* Replace block in content.
*
* @param string $post_content Post content.
* @param array $blocks Blocks.
*
* @return string
*/
public function replace( $post_content, $blocks ) {
preg_match_all( '/<!-- wp:yoast\/how-to-block.*-->.*<!-- \/wp:yoast\/how-to-block -->/iUs', $post_content, $matches );
foreach ( $matches[0] as $index => $match ) {
$post_content = \str_replace( $match, $blocks[ $index ], $post_content );
}
return $post_content;
}
/**
* Format steps.
*
* @param array $step Steps.
*
* @return array
*/
public function get_step( $step ) {
return [
'id' => uniqid( 'howto-step-' ),
'visible' => true,
'title' => $step['jsonName'],
'content' => $step['jsonText'],
];
}
/**
* Generate HTML.
*
* @param array $attributes Block attributes.
*
* @return string
*/
private function get_html( $attributes ) {
// HTML.
$out = [ '<div class="wp-block-rank-math-howto-block">' ];
// Steps.
foreach ( $attributes['steps'] as $step ) {
if ( empty( $step['visible'] ) ) {
continue;
}
$out[] = '<div class="rank-math-howto-step">';
if ( ! empty( $step['title'] ) ) {
$out[] = sprintf(
'<%1$s class="rank-math-howto-title">%2$s</%1$s>',
$attributes['titleWrapper'],
$step['title']
);
}
if ( ! empty( $step['content'] ) ) {
$out[] = sprintf(
'<div class="rank-math-howto-content">%1$s</div>',
$step['content']
);
}
$out[] = '</div>';
}
$out[] = '</div>';
return join( '', $out );
}
}

Some files were not shown because too many files have changed in this diff Show More