Commit inicial - WordPress Análisis de Precios Unitarios

- WordPress core y plugins
- Tema Twenty Twenty-Four configurado
- Plugin allow-unfiltered-html.php simplificado
- .gitignore configurado para excluir wp-config.php y uploads

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-11-03 21:04:30 -06:00
commit a22573bf0b
24068 changed files with 4993111 additions and 0 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,244 @@
<?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 = __DIR__;
$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',
'json' => [
'choicesComparisonTypes' => Helper::choices_comparison_types(),
],
],
],
7
);
return $tabs;
}
/**
* Output page title actions.
*
* @return void
*/
public function page_title_actions() {
$actions = [
'settings' => [
'class' => 'page-title-action',
'href' => Helper::get_settings_url( 'general', '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
}
}
}

View File

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

View File

@@ -0,0 +1,274 @@
<?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;
use 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 = preg_replace( '/(?<=\/)(https?:\/[^\s]*)/i', '', $uri );
$uri = str_replace( Helper::get_home_url( '/' ), '', $uri );
if ( ! $uri ) {
return;
}
// 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() {
$hook = defined( 'CT_VERSION' ) ?
'oxygen_enqueue_frontend_scripts' :
(
function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ?
'wp_head' :
'get_header'
);
/**
* Allow developers to change the action hook that will trigger the 404 capture.
*/
return $this->do_filter( '404_monitor/hook', $hook );
}
}

View File

@@ -0,0 +1,283 @@
<?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() {
$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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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 esc_url( 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 esc_url( 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 esc_url( KB::get( 'fix-404', '404 Monitor Help Toggle Fix link' ) ); ?>" target="_blank">
<?php esc_html_e( 'Fix 404 Errors', 'rank-math' ); ?>
</a>
</li>
</ul>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,654 @@
<?php
/**
* The Analytics AJAX
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use WP_Error;
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\Analytics\Workflow\Console;
use RankMath\Analytics\Workflow\Inspections;
use RankMath\Analytics\Workflow\Objects;
use RankMath\Google\Console as Google_Analytics;
defined( 'ABSPATH' ) || exit;
/**
* AJAX class.
*/
class AJAX {
use \RankMath\Traits\Ajax;
/**
* Get the instance of this class.
*
* @return AJAX
*/
public static function get() {
static $instance = null;
if ( is_null( $instance ) ) {
$instance = new self();
}
return $instance;
}
/**
* 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( $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 = Google_Analytics::test_connection();
if ( false === $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::test_connection();
if ( false === $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' );
$response = $this->do_save_analytic_profile(
[
'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 ),
]
);
if ( is_wp_error( $response ) ) {
$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_options() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
$request = $this->do_save_analytic_options(
[
'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 ),
'days' => Param::get( 'days', 90, FILTER_VALIDATE_INT ),
]
);
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' ) );
}
$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();
foreach (
[
'rank_math_analytics_all_services',
'rank_math_google_analytic_options',
]
as $option_name
) {
delete_option( $option_name );
}
$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();
$this->success(
[
'days' => $db_info['days'] ?? 0,
'rows' => Str::human_number( $db_info['rows'] ?? 0 ),
'size' => size_format( $db_info['size'] ?? 0 ),
]
);
}
/**
* 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/properties/{$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'],
];
}
/**
* Save analytic profile.
*
* @param array $data Data to save.
*/
public function do_save_analytic_options( $data = [] ) {
$value = [
'account_id' => $data['account_id'] ?? '',
'property_id' => $data['property_id'] ?? '',
'view_id' => $data['view_id'] ?? '',
'measurement_id' => $data['measurement_id'] ?? '',
'stream_name' => $data['stream_name'] ?? '',
'country' => $data['country'] ?? 'all',
'install_code' => $data['install_code'] ?? false,
'anonymize_ip' => $data['anonymize_ip'] ?? false,
'local_ga_js' => $data['local_ga_js'] ?? false,
'exclude_loggedin' => $data['exclude_loggedin'] ?? false,
'days' => $data['days'] ?? 90,
];
$days = $value['days'];
$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 );
// Test Google Analytics (GA) connection request.
if ( ! empty( $value['view_id'] ) || ! empty( $value['country'] ) || ! empty( $value['property_id'] ) ) {
$request = Analytics::get_sample_response();
if ( is_wp_error( $request ) ) {
return new WP_Error(
'insufficient_permissions',
esc_html__( 'Data import will not work for this service as sufficient permissions are not given.', 'rank-math' )
);
}
}
// Start fetching analytics data.
Workflow\Workflow::do_workflow(
'analytics',
$days,
$prev,
$value
);
return true;
}
/**
* Save analytic profile.
*
* @param array $data Data to save.
*/
public function do_save_analytic_profile( $data = [] ) {
$profile = $data['profile'] ?? '';
$country = $data['country'] ?? 'all';
$days = $data['days'] ?? 90;
$enable_index_status = $data['enable_index_status'] ?? false;
$success = Api::get()->get_search_analytics(
[
'country' => $country,
'profile' => $profile,
]
);
if ( is_wp_error( $success ) ) {
return new WP_Error( 'insufficient_permissions', 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();
}
new Objects();
new Console();
new Inspections();
// Start fetching console data.
Workflow\Workflow::do_workflow(
'console',
$days,
$prev,
$value
);
return true;
}
}

View File

@@ -0,0 +1,400 @@
<?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\Analytics\Analytics;
use RankMath\Google\Authentication;
use RankMath\Analytics\Workflow\Jobs;
use RankMath\Analytics\Workflow\Workflow;
use RankMath\Helpers\Schedule;
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( 'init', 'maybe_init_email_reports' );
$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 );
$this->action( 'rank_math/settings/before_save', 'before_settings_save', 10, 2 );
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( sanitize_text_field( $_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 string $type Settings type.
* @param array $settings Settings data.
*/
public function before_settings_save( $type, $settings ) {
if ( $type !== 'general' ) {
return;
}
$console_email_reports = Helper::get_settings( 'general.console_email_reports' );
$console_email_frequency = Helper::get_settings( 'general.console_email_frequency' );
$updated_email_reports = isset( $settings['console_email_reports'] ) ? $settings['console_email_reports'] : '';
$updated_email_frequency = isset( $settings['console_email_frequency'] ) ? $settings['console_email_frequency'] : '';
// Early bail if our options are not changed.
if ( $console_email_reports === $updated_email_reports && $console_email_frequency === $updated_email_frequency ) {
return;
}
as_unschedule_all_actions( 'rank_math/analytics/email_report_event', [], 'rank-math' );
if ( ! $console_email_reports ) {
return;
}
$frequency = $updated_email_frequency ? $updated_email_frequency : 'monthly';
$this->schedule_email_reporting( $frequency );
}
/**
* 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.
*
* @return string
*/
public function replace_notice_link( $output ) {
$url = wp_nonce_url( Helper::get_settings_url( 'general', 'analytics' ) . '&enable_email_reports=1', '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' => __( 'How many times your site was clicked on in the search results.', '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 ranks for within top 100 positions.', 'rank-math' ),
'value' => true,
'data' => $data->keywords,
],
'average-position' => [
'label' => __( 'Average Position', 'rank-math' ),
'desc' => __( 'Average position of all the keywords ranking within top 100 positions.', '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'] ) && 'n/a' !== $item['total'] ? abs( $item['total'] ) : 0;
$difference = isset( $item['difference'] ) && 'n/a' !== $item['difference'] ? abs( $item['difference'] ) : 0;
$is_negative = isset( $item['difference'] ) && 'n/a' !== $item['difference'] && abs( $item['difference'] ) !== $item['difference'];
$diff_class = 'up';
if ( ( ! $revert && $is_negative ) || ( $revert && ! $is_negative && $item['difference'] > 0 ) ) {
$diff_class = 'down';
}
if ( 0.0 === floatval( $difference ) ) {
$diff_class = 'no-diff';
}
?>
<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' );
Schedule::recurring_action( $midnight, $interval_days * DAY_IN_SECONDS, 'rank_math/analytics/email_report_event', [], 'rank-math' );
}
}

View File

@@ -0,0 +1,55 @@
<?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( 'isConsoleConnected', \RankMath\Google\Console::is_console_connected() );
Helper::add_json( 'isAnalyticsConnected', \RankMath\Google\Analytics::is_analytics_connected() );
Helper::add_json( 'hideFrontendStats', get_user_meta( get_current_user_id(), 'rank_math_hide_frontend_stats', true ) );
Helper::add_json( 'links', KB::get_links() );
}
}

View File

@@ -0,0 +1,636 @@
<?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\Str;
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 = __DIR__;
$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/settings/general', 'add_settings' );
$this->action( 'admin_init', 'refresh_token_missing', 25 );
$this->action( 'admin_init', 'cancel_fetch', 5 );
$this->action( 'wp_helpers_notification_dismissed', 'notice_dismissible' );
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;
}
$this->maybe_add_cron_notice();
$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;
}
}
/**
* Store a value in the options table when CRON notice is dismissed to prevent the site from showing it again.
*
* @param string $notification_id Notification id.
*/
public function notice_dismissible( $notification_id ) {
if ( 'analytics_cron_notice' === $notification_id ) {
update_option( 'rank_math_analytics_cron_notice_dismissed', true, false );
}
}
/**
* Add Notice on Analytics page when CRON is not working on the site.
*/
private function maybe_add_cron_notice() {
if ( ! $this->page->is_current_page() || get_option( 'rank_math_analytics_cron_notice_dismissed' ) ) {
return;
}
if ( Helper::is_cron_enabled() ) {
Helper::remove_notification( 'analytics_cron_notice' );
return;
}
$message = sprintf(
/* translators: constant value */
esc_html__( 'Loopback requests to %s are blocked. This may prevent scheduled tasks from running. Please check your server configuration.', 'rank-math' ),
'<code>wp-cron.php</code>'
);
if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
$message = sprintf(
/* translators: constant value */
esc_html__( 'WordPress\'s internal cron system is disabled via %s. Please ensure a real cron job is set up, otherwise scheduled features like Analytics may not work correctly.', 'rank-math' ),
'<code>DISABLE_WP_CRON</code>'
);
}
// Show admin notification.
Helper::add_notification(
$message,
[
'type' => 'warning',
'id' => 'analytics_cron_notice',
'screen' => 'rank-math_page_rank-math-analytics',
]
);
}
/**
* 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 = '';
$time_period_count = count( $time_periods );
for ( $time_period_index = 0, $periods_included = 0, $seconds_remaining = $interval; $time_period_index < $time_period_count && $seconds_remaining > 0 && $periods_included < $periods_to_include; $time_period_index++ ) {
$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 );
$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-components',
],
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,
'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 ) {
$db_info = \RankMath\Analytics\DB::info();
$next_fetch = '';
$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 ) {
$next_fetch = sprintf(
__( 'Next update on %s (in %s)', 'rank-math' ),
date_i18n( 'd M, Y H:m:i', $next_date->getTimestamp() ),
human_time_diff( $next_date->getTimestamp() )
);
}
}
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',
'json' => apply_filters(
'rank_math/analytics/options/data',
[
'analytics' => \RankMath\Wizard\Search_Console::get_localized_data(),
'isSettingsPage' => true,
'homeUrl' => home_url(),
'fields' => [
'console_caching_control' => [
'description' => $this->get_description( 'console_caching_control' ),
],
],
'dbInfo' => [
'days' => $db_info['days'] ?? 0,
'rows' => Str::human_number( $db_info['rows'] ?? 0 ),
'size' => size_format( $db_info['size'] ?? 0 ),
],
'isFetching' => 'fetching' === get_option( 'rank_math_analytics_first_fetch' ),
'nextFetch' => $next_fetch,
'isAuthorized' => Authentication::is_authorized(),
]
),
],
],
9
);
return $tabs;
}
/**
* Get the description for a field.
*
* @param string $field_id The field ID.
*
* @return string
*/
public function get_description( $field_id ) {
if ( ! $field_id ) {
return '';
}
$description = '';
switch ( $field_id ) {
case 'console_caching_control':
// 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 );
break;
default:
$description = apply_filters( 'rank_math/analytics/options/' . $field_id . '/description', '' );
break;
}
return $description;
}
}

View File

@@ -0,0 +1,541 @@
<?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 [];
}
if ( ! DB_Helper::check_table_exists( 'rank_math_analytics_gsc' ) ) {
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 = DB_Helper::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')" );
$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',
'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;
}
$old_id = self::objects()
->select( 'id' )
->where( 'object_id', absint( $args['object_id'] ) )
->getVar();
if ( ! empty( $old_id ) ) {
// $updated may sometimes return 0 if there is no field that is changed, even if a row with $args['object_id'] exists.
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[] = self::get_page( $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 DB_Helper::query( $wpdb->prepare( $sql, $data ) );
}
/**
* Get page slug from full URL.
*
* @param string $url Full URL to parse.
* @return string Page path/slug with leading slash.
*/
public static function get_page( $url ) {
if ( empty( $url ) || ! is_string( $url ) ) {
return '';
}
$url = urldecode( preg_replace( '/#.*$/', '', $url ) );
$url = self::remove_hash( $url );
// Parse the URL to get the path component.
$parsed_url = wp_parse_url( $url );
if ( isset( $parsed_url['path'] ) ) {
return $parsed_url['path'];
}
// Fallback: try to extract path by removing domain.
$host = Helper::get_home_url();
$url = str_replace( $host, '', $url );
// Remove ASCII domain.
$host_ascii = idn_to_ascii( $host );
$url = str_replace( $host_ascii, '', $url );
$url = preg_replace( '#^https?://(www\.)?#i', '', $url );
return $url;
}
/**
* 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 ) {
$inspections = self::inspections()->table;
$objects = self::objects()->table;
$query = self::inspections()
->selectCount( "$inspections.id", 'total' )
->leftJoin( $objects, "$inspections.page", "$objects.page" )
->where( "$objects.page", '!=', '' );
do_action_ref_array( 'rank_math/analytics/get_inspections_count_query', [ &$query, $params ] );
return $query->getVar();
}
}

View File

@@ -0,0 +1,648 @@
<?php
/**
* Analytics Email Reports.
*
* @since 1.0.68
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Google\Console;
use RankMath\Admin\Admin_Helper;
use RankMath\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* Email_Reports class.
*/
class Email_Reports {
use Hooker;
/**
* Email content variables.
*
* @var array
*/
private $variables = [];
/**
* Path to the views directory.
*
* @var array
*/
private $views_path = '';
/**
* URL to the assets directory.
*
* @var array
*/
private $assets_url = '';
/**
* Charts Account.
*
* @var string
*/
private $charts_account = 'rankmath';
/**
* Charts Key.
*
* @var string
*/
private $charts_key = '10042B42-9193-428A-ABA7-5753F3370F84';
/**
* Graph data.
*
* @var array
*/
private $graph_data = [];
/**
* Debug mode.
*
* @var boolean
*/
private $debug = false;
/**
* The constructor.
*/
public function __construct() {
if ( ! Console::is_console_connected() ) {
return;
}
$directory = __DIR__;
$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( 'wp_loaded', '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_settings_url( 'general', '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_value Default value.
* @return mixed
*/
public static function get_setting( $option, $default_value = false ) {
return Helper::get_settings( 'general.console_email_' . $option, $default_value );
}
/**
* Output image inside the email template.
*
* @param string $url Image URL.
* @param string $width Image width.
* @param string $height Image height.
* @param string $alt ALT text.
* @param array $attr Additional attributes.
* @return void
*/
public function image( $url, $width = 0, $height = 0, $alt = '', $attr = [] ) {
$atts = $attr;
$atts['border'] = '0';
if ( ! isset( $atts['src'] ) ) {
$atts['src'] = $url;
}
if ( ! isset( $atts['width'] ) && $width ) {
$atts['width'] = $width;
}
if ( ! isset( $atts['height'] ) && $height ) {
$atts['height'] = $height;
}
if ( ! isset( $atts['alt'] ) ) {
$atts['alt'] = $alt;
}
if ( ! isset( $atts['style'] ) ) {
$atts['style'] = 'border: 0; outline: none; text-decoration: none; display: inline-block;';
}
if ( substr( $atts['src'], 0, 4 ) !== 'http' && substr( $atts['src'], 0, 3 ) !== '###' ) {
$atts['src'] = $this->assets_url . 'img/' . $atts['src'];
}
$atts = $this->do_filter( 'analytics/email_report_image_atts', $atts, $url, $width, $height, $alt, $attr );
$attributes = '';
foreach ( $atts as $name => $value ) {
if ( ! empty( $value ) ) {
$value = ( 'src' === $name ) ? esc_url_raw( $value ) : esc_attr( $value );
$attributes .= ' ' . $name . '="' . $value . '"';
}
}
$image = "<img $attributes>";
$image = $this->do_filter( 'analytics/email_report_image_html', $image, $url, $width, $height, $alt, $attr );
echo $image; // phpcs:ignore
}
/**
* Gets template path.
*
* @param string $template_name Template name.
* @param bool $return_full_path Return the full path or not.
* @return string
*/
public function locate_template( $template_name, $return_full_path = true ) {
$default_paths = [ $this->views_path ];
$template_paths = $this->do_filter( 'analytics/email_report_template_paths', $default_paths );
$paths = array_reverse( $template_paths );
$located = '';
$path_partial = '';
foreach ( $paths as $path ) {
if ( file_exists( $full_path = trailingslashit( $path ) . $template_name . '.php' ) ) { // phpcs:ignore
$located = $full_path;
$path_partial = $path;
break;
}
}
return $return_full_path ? $located : $path_partial;
}
/**
* Load all graph data into memory.
*
* @return void
*/
private function load_graph_data() {
$period = self::get_period_from_frequency();
$stats = Stats::get();
$stats->set_date_range( "-{$period} days" );
$this->graph_data = (array) $stats->get_analytics_summary_graph();
}
/**
* Get data points for graph.
*
* @param string $chart Chart to get data for.
* @return array
*/
public function get_graph_data( $chart ) {
if ( empty( $this->graph_data ) ) {
$this->load_graph_data();
}
$data = [];
$group = 'merged';
$prop = $chart;
if ( 'traffic' === $chart ) {
$group = 'traffic';
$prop = 'pageviews';
}
if ( empty( $this->graph_data[ $group ] ) ) {
return $data;
}
foreach ( (array) $this->graph_data[ $group ] as $range_data ) {
$range_data = (array) $range_data;
if ( isset( $range_data[ $prop ] ) ) {
$data[] = $range_data[ $prop ];
}
}
return $data;
}
/**
* Charts API sign request.
*
* @param string $query Query.
* @param string $code Code.
* @return string
*/
private function charts_api_sign( $query, $code ) {
return hash_hmac( 'sha256', $query, $code );
}
/**
* Generate URL for the Charts API image.
*
* @param array $graph_data Graph data points.
* @param int $width Image height.
* @param int $height Image width.
*
* @return string
*/
private function charts_api_url( $graph_data, $width = 192, $height = 102 ) {
$params = [
'chco' => '80ace7',
'chds' => 'a',
'chf' => 'bg,s,f7f9fb',
'chls' => 4,
'chm' => 'B,e2eeff,0,0,0',
'chs' => "{$width}x{$height}",
'cht' => 'ls',
'chd' => 'a:' . join( ',', $graph_data ),
'icac' => $this->charts_account,
];
$query_string = urldecode( http_build_query( $params ) );
$signature = $this->charts_api_sign( $query_string, $this->charts_key );
return 'https://charts.rankmath.com/chart?' . $query_string . '&ichm=' . $signature;
}
/**
* Check if fields should be hidden.
*
* @return bool
*/
public static function are_fields_hidden() {
return apply_filters( 'rank_math/analytics/hide_email_report_options', false );
}
}

View File

@@ -0,0 +1,410 @@
<?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' );
} elseif ( 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, // phpcs:ignore
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'; // @phpstan-ignore-line
}
$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 ) {
$value = $this->get( 'measurement_id' );
}
return $value;
}
/**
* Get gtag script info
*
* @return mixed
*/
protected function get_gtag_info() {
// Get Google Analytics Property ID.
$property_id = $this->get( 'property_id' );
// Get main gtag script Url.
$url = 'https://www.googletagmanager.com/gtag/js?id=' . esc_attr( $property_id );
$gtag_opt = [];
if ( $this->get_amp_mode() ) {
$gtag_opt['linker'] = [
'domains' => [ $this->get_home_domain() ],
];
}
$gtag_inline_linker_script = '';
if ( ! empty( $gtag_opt['linker'] ) ) {
$gtag_inline_linker_script = 'gtag(\'set\', \'linker\', ' . wp_json_encode( $gtag_opt['linker'] ) . ' );';
}
unset( $gtag_opt['linker'] );
// Get Google Analytics Property ID.
$gtag_config = [];
$gtag_config = $this->do_filter( 'analytics/gtag_config', $gtag_config );
// Construct inline scripts.
$gtag_inline_script = 'window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}';
$gtag_inline_script .= $gtag_inline_linker_script;
$gtag_inline_script .= 'gtag(\'js\', new Date());';
$gtag_inline_script .= 'gtag(\'config\', \'' . esc_attr( $property_id ) . '\', {' . join( ', ', $gtag_config ) . '} );';
$gtag = $this->do_filter(
'analytics/gtag',
[
'url' => $url,
'inline' => $gtag_inline_script,
]
);
return $gtag;
}
/**
* Normalize option data
*
* @param mixed $options Array to normalize.
* @return mixed
*/
protected function normalize_it( $options ) {
foreach ( (array) $options as $key => $value ) {
$options[ $key ] = is_array( $value ) ? $this->normalize_it( $value ) : Helper::normalize_data( $value );
}
return $options;
}
/**
* Print the user tracking opt-out code
*
* This script opts out of all Google Analytics tracking, for all measurement IDs, regardless of implementation.
*
* @link https://developers.google.com/analytics/devguides/collection/analyticsjs/user-opt-out
*/
public function print_tracking_opt_out() {
if ( ! $this->is_tracking_disabled() ) {
return;
}
if ( $this->is_amp() ) :
?>
<script type="application/ld+json" id="__gaOptOutExtension"></script>
<?php else : ?>
<script type="text/javascript">window['ga-disable-<?php echo esc_js( $this->get( 'property_id' ) ); ?>'] = true;</script>
<?php
endif;
}
}

View File

@@ -0,0 +1,319 @@
<?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\Helpers\DB as DB_Helper;
use RankMath\Analytics\Stats;
defined( 'ABSPATH' ) || exit;
/**
* Keywords class.
*
* @method get_analytics_data()
*/
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 = DB_Helper::get_results( $query );
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 = DB_Helper::get_results( $query );
// 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 = DB_Helper::get_results( $query );
$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 = DB_Helper::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 = DB_Helper::get_results( $query );
// phpcs:enable
// Construct return data.
$data = $this->get_date_array(
$intervals['dates'],
[
'top3' => 0,
'top10' => 0,
'top50' => 0,
'top100' => 0,
]
);
foreach ( $position_data as $row ) {
if ( ! isset( $intervals['map'][ $row->date ] ) ) {
continue;
}
$date = $intervals['map'][ $row->date ];
if ( ! isset( $data[ $date ][ $row->position_type ] ) ) {
continue;
}
$key = $row->position_type;
$data[ $date ][ $key ] = $row->total;
}
$data = array_values( $data );
set_transient( $cache_key, $data, DAY_IN_SECONDS );
return $data;
}
/**
* Get top position total.
*
* @param array $positions Position array.
* @param array $rows Data to process.
* @param string $where What data to get total.
*
* @return array
*/
private function get_top_position_total( $positions, $rows, $where ) {
foreach ( $rows as $row ) {
$positions[ $row->position_type ][ $where ] = $row->total;
}
if ( 'difference' === $where ) {
$positions['top3']['difference'] = $positions['top3']['total'] - $positions['top3']['difference'];
$positions['top10']['difference'] = $positions['top10']['total'] - $positions['top10']['difference'];
$positions['top50']['difference'] = $positions['top50']['total'] - $positions['top50']['difference'];
$positions['top100']['difference'] = $positions['top100']['total'] - $positions['top100']['difference'];
}
return $positions;
}
}

View File

@@ -0,0 +1,118 @@
<?php
/**
* The Analytics Module
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use RankMath\Helpers\DB as DB_Helper;
defined( 'ABSPATH' ) || exit;
/**
* Objects class.
*
* @method set_page_as_key()
*/
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.
$pages = DB_Helper::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 = DB_Helper::get_var(
"SELECT count(*) FROM {$wpdb->prefix}rank_math_analytics_objects
WHERE is_indexable = 1
{$subwhere}
ORDER BY created DESC"
);
return [
'rows' => $this->set_page_as_key( $pages ),
'rowsFound' => $total_rows,
];
}
}

View File

@@ -0,0 +1,124 @@
<?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.
*
* @method get_analytics_data()
*/
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 );
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,437 @@
<?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;
use RankMath\Helpers\DB as DB_Helper;
defined( 'ABSPATH' ) || exit;
/**
* Summary class.
*
* @method get_cache_key()
* @method get_intervals()
* @method get_sql_date_intervals()
* @method set_dimension_as_key()
* @method extract_data_from_mixed()
* @method get_merged_metrics()
* @method get_merge_data_graph()
* @method get_date_array()
* @method get_graph_data_flat()
*/
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';
$hash_name = $post_type ? $post_type : 'overall';
$cache_key = $this->generate_hash( $hash_name );
$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 = DB_Helper::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();
if ( $average && $average->total > 0 ) {
$average->total += property_exists( $stats, 'noData' ) ? $stats->noData : 0; // phpcs:ignore
$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();
$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();
$total_ctr = is_null( $stats->impressions ) ? 'n/a' : round( ( $stats->clicks / $stats->impressions ) * 100, 2 );
$previous_ctr = is_null( $old_stats->impressions ) ? 'n/a' : ( 0 !== $old_stats->impressions && 'n/a' !== $old_stats->impressions ? round( ( $old_stats->clicks / $old_stats->impressions ) * 100, 2 ) : 0 );
$stats->ctr = [
'total' => $total_ctr,
'previous' => $previous_ctr,
'difference' => 'n/a' !== $total_ctr && 'n/a' !== $previous_ctr ? $total_ctr - $previous_ctr : 'n/a',
];
$stats->clicks = [
'total' => is_null( $stats->clicks ) ? 'n/a' : (int) $stats->clicks,
'previous' => is_null( $old_stats->clicks ) ? 'n/a' : (int) $old_stats->clicks,
'difference' => is_null( $stats->clicks ) || is_null( $old_stats->clicks ) ? 'n/a' : $stats->clicks - $old_stats->clicks,
];
$stats->impressions = [
'total' => is_null( $stats->impressions ) ? 'n/a' : (int) $stats->impressions,
'previous' => is_null( $old_stats->impressions ) ? 'n/a' : (int) $old_stats->impressions,
'difference' => is_null( $stats->impressions ) || is_null( $old_stats->impressions ) ? 'n/a' : $stats->impressions - $old_stats->impressions,
];
$stats->position = [
'total' => is_null( $stats->position ) ? 'n/a' : (float) \number_format( $stats->position, 2 ),
'previous' => is_null( $old_stats->position ) ? 'n/a' : (float) \number_format( $old_stats->position, 2 ),
'difference' => is_null( $old_stats->position ) || is_null( $old_stats->position ) ? 'n/a' : (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' => 'n/a',
'posts' => 'n/a',
'clicks' => 'n/a',
'pageviews' => 'n/a',
'impressions' => 'n/a',
]
);
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 = DB_Helper::get_var(
$wpdb->prepare(
"SELECT NULLIF(COUNT(DISTINCT(query)), 0)
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s",
$this->start_date,
$this->end_date
)
);
$old_keywords_count = DB_Helper::get_var(
$wpdb->prepare(
"SELECT NULLIF(COUNT(DISTINCT(query)), 0)
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s",
$this->compare_start_date,
$this->compare_end_date
)
);
$keywords = [
'total' => is_null( $keywords_count ) ? 'n/a' : (int) $keywords_count,
'previous' => is_null( $old_keywords_count ) ? 'n/a' : (int) $old_keywords_count,
'difference' => is_null( $keywords_count ) || is_null( $old_keywords_count ) ? 'n/a' : (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 = DB_Helper::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 = DB_Helper::get_results( $query );
// phpcs:enable
$keywords = $this->extract_data_from_mixed( $keywords, 'mixed', ':', [ 'keywords', 'date' ] );
$keywords = $this->set_dimension_as_key( $keywords, 'range_group' );
// merge metrics data.
$data->analytics = [];
$data->analytics = $this->get_merged_metrics( $analytics, $keywords, true );
$data->merged = $this->get_date_array(
$intervals['dates'],
[
'clicks' => [],
'impressions' => [],
'position' => [],
'ctr' => [],
'keywords' => [],
'pageviews' => [],
]
);
// Convert types.
$data->analytics = array_map( [ $this, 'normalize_graph_rows' ], $data->analytics );
// Merge for performance.
$data->merged = $this->get_merge_data_graph( $data->analytics, $data->merged, $intervals['map'] );
// For developers.
$data = apply_filters( 'rank_math/analytics/analytics_summary_graph', $data, $intervals );
$data->merged = $this->get_graph_data_flat( $data->merged );
$data->merged = array_values( $data->merged );
return $data;
}
}

View File

@@ -0,0 +1,116 @@
<?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;
use RankMath\Helpers\Schedule;
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 ) {
Schedule::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 ) {
Schedule::async_action( 'rank_math/analytics/get_inspections_data', [ $page ], 'rank-math' );
return;
}
$time = time() + $delay;
Schedule::single_action( $time, 'rank_math/analytics/get_inspections_data', [ $page ], 'rank-math' );
}
/**
* Fetch the inspection data for a URL, store it, and return it.
*
* @param string $page URL to inspect.
*/
public function inspect( $page ) {
$inspection = \RankMath\Google\Url_Inspection::get()->get_inspection_data( $page );
if ( empty( $inspection ) ) {
return [];
}
DB::store_inspection( $inspection );
return wp_parse_args( $inspection, DB::get_inspection_defaults() );
}
/**
* Get latest inspection results for each page.
*
* @param array $params Parameters.
* @param int $per_page Number of items per page.
*/
public function get_inspections( $params, $per_page ) {
// Early Bail!!
if ( ! DB_Helper::check_table_exists( 'rank_math_analytics_inspections' ) ) {
return;
}
return DB::get_inspections( $params, $per_page );
}
/**
* Check if the "Enable Index Status Tab" option is enabled.
*
* @return bool
*/
public static function is_enabled() {
$profile = get_option( 'rank_math_google_analytic_profile', [] );
if ( empty( $profile ) || ! is_array( $profile ) ) {
return false;
}
$enable_index_status = true;
if ( isset( $profile['enable_index_status'] ) ) {
$enable_index_status = $profile['enable_index_status'];
}
return $enable_index_status;
}
}

View File

@@ -0,0 +1,158 @@
<?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;
use RankMath\Helpers\Sitepress;
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', 101 );
}
}
/**
* 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] );
}
$permalink = $this->get_permalink( $post_id );
$page = str_replace( Helper::get_home_url(), '', urldecode( $permalink ) );
// 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 );
}
}
/**
* Get permalink.
*
* @param int $post_id Post ID.
*
* @return string
*/
public function get_permalink( $post_id ) {
$permalink = get_permalink( $post_id );
if ( ! Sitepress::get()->is_active() ) {
return $permalink;
}
$sitepress = Sitepress::get()->get_var();
$language_domains = $sitepress->get_setting( 'language_domains', [] );
if ( ! $language_domains ) {
return $permalink;
}
$details = apply_filters( 'wpml_post_language_details', null, $post_id );
$code = $details['language_code'] ?? '';
$permalink = apply_filters( 'wpml_permalink', get_the_permalink( $post_id ), $code );
foreach ( $language_domains as $key => $domain ) {
$permalink = preg_replace( "#https?://{$domain}#i", '', $permalink );
}
return $permalink;
}
}

View File

@@ -0,0 +1,323 @@
<?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\Helper;
use RankMath\Google\Api;
use RankMath\Helpers\Str;
use RankMath\Analytics\Workflow\Base;
/**
* Analytics class.
*/
class Analytics extends Request {
/**
* Connection status key.
*/
const CONNECTION_STATUS_KEY = 'rank_math_analytics_connection_error';
/**
* 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 ( ! $v4_response || ! $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'] );
}
/**
* Is valid connection
*/
public static function is_valid_connection() {
return Api::get()->get_connection_status( self::CONNECTION_STATUS_KEY );
}
/**
* Test connection
*/
public static function test_connection() {
return Api::get()->check_connection_status( self::CONNECTION_STATUS_KEY, [ __CLASS__, 'get_sample_response' ] );
}
/**
* Get sample response to test connection.
*
* @return array|false|WP_Error
*/
public static function get_sample_response() {
return self::get_analytics(
[
'row_limit' => 1,
],
true
);
}
/**
* Query analytics data from google client api.
*
* @param array $options Analytics options.
* @param boolean $days Whether to include dates.
*
* @return array|false|WP_Error
*/
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 for GA4 API.
$args = [
'limit' => isset( $options['row_limit'] ) ? $options['row_limit'] : Api::get()->get_row_limit(),
'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',
],
],
],
],
],
],
];
$dimensions = isset( $options['dimensions'] ) ? $options['dimensions'] : [];
if ( $dimensions ) {
$args = wp_parse_args(
[
'dimensions' => $dimensions,
],
$args
);
}
$metrics = isset( $options['metrics'] ) ? $options['metrics'] : [];
if ( $metrics ) {
$args = wp_parse_args(
[
'metrics' => $metrics,
],
$args
);
}
// Include only dates.
if ( true === $days ) {
$args = wp_parse_args(
[
'dimensions' => [
[ 'name' => 'date' ],
],
],
$args
);
}
$workflow = 'analytics';
Api::get()->set_workflow( $workflow );
// Request.
$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;
}
$dimensions = isset( $response['dimensionHeaders'] ) ? array_column( $response['dimensionHeaders'], 'name' ) : [];
$metrics = isset( $response['metricHeaders'] ) ? array_column( $response['metricHeaders'], 'name' ) : [];
$rows = [];
foreach ( $response['rows'] as $row ) {
$item = [];
if ( isset( $row['dimensionValues'] ) ) {
foreach ( $row['dimensionValues'] as $i => $dim ) {
$item[ $dimensions[ $i ] ] = $dim['value'];
}
}
if ( isset( $row['metricValues'] ) ) {
foreach ( $row['metricValues'] as $i => $met ) {
$item[ $metrics[ $i ] ] = (int) $met['value'];
}
}
$rows[] = $item;
}
return $rows;
}
/**
* Get view id.
*
* @return string
*/
public static function get_view_id() {
static $rank_math_view_id;
if ( is_null( $rank_math_view_id ) ) {
$options = get_option( 'rank_math_google_analytic_options' );
$rank_math_view_id = ! empty( $options['view_id'] ) ? $options['view_id'] : false;
}
return $rank_math_view_id;
}
}

View File

@@ -0,0 +1,111 @@
<?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 );
}
/**
* Get connection status.
*
* @param string $key Connection status key.
*
* @return bool
*/
public function get_connection_status( $key ) {
return ! get_option( $key, false );
}
/**
* Set connection status.
*
* @param string $key Connection status key.
* @param bool $status Connection status.
*/
public function set_connection_status( $key, $status ) {
if ( $status ) {
update_option( $key, true );
} else {
delete_option( $key );
}
}
/**
* Check connection status.
*
* @param string $key Connection status key.
* @param callable $callback Callback to check connection.
*
* @return bool
*/
public function check_connection_status( $key, $callback ) {
$this->set_connection_status( $key, false );
$response = call_user_func( $callback );
if ( is_wp_error( $response ) ) {
$this->set_connection_status( $key, true );
return false;
}
return true;
}
}

View File

@@ -0,0 +1,161 @@
<?php
/**
* Google Authentication wrapper.
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Google;
use RankMath\Helper;
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:
if ( Helper::is_react_enabled() ) {
return 'rank-math-options-general&view=analytics';
}
return 'rank-math-options-general#setting-panel-analytics';
}
}
$page = wp_get_referer();
if ( ! empty( $page ) && Str::contains( 'wizard', $page ) ) {
return 'rank-math-wizard&step=analytics';
}
return 'rank-math-options-general#setting-panel-analytics';
}
}

View File

@@ -0,0 +1,357 @@
<?php
/**
* Google Search Console.
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Google;
use RankMath\Google\Api;
use RankMath\Helpers\Str;
use RankMath\Helpers\Schedule;
use RankMath\Analytics\Workflow\Base;
use RankMath\Sitemap\Sitemap;
use WP_Error;
defined( 'ABSPATH' ) || exit;
/**
* Console class.
*/
class Console extends Analytics {
/**
* Connection status key.
*/
const CONNECTION_STATUS_KEY = 'rank_math_console_connection_error';
/**
* 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.
Schedule::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|false|WP_Error
*/
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' => true,
]
);
$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'] );
}
/**
* Is valid connection
*/
public static function is_valid_connection() {
return Api::get()->get_connection_status( self::CONNECTION_STATUS_KEY );
}
/**
* Test connection
*/
public static function test_connection() {
return Api::get()->check_connection_status( self::CONNECTION_STATUS_KEY, [ Api::get(), 'get_search_analytics' ] );
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* Google Permissions.
*
* @since 1.0.49
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Google;
defined( 'ABSPATH' ) || exit;
/**
* Permissions class.
*/
class Permissions {
const OPTION_NAME = 'rank_math_analytics_permissions';
/**
* Permission info.
*/
public static function fetch() {
$tokens = Authentication::tokens();
if ( empty( $tokens['access_token'] ) ) {
return;
}
$url = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' . $tokens['access_token'];
$response = wp_remote_get( $url );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return;
}
$response = wp_remote_retrieve_body( $response );
if ( empty( $response ) ) {
return;
}
$response = \json_decode( $response, true );
$scopes = $response['scope'];
$scopes = explode( ' ', $scopes );
$scopes = str_replace( 'https://www.googleapis.com/auth/', '', $scopes );
update_option( self::OPTION_NAME, $scopes );
}
/**
* Get permissions.
*
* @return array
*/
public static function get() {
return get_option( self::OPTION_NAME, [] );
}
/**
* If user give permission or not.
*
* @param string $permission Permission name.
* @return boolean
*/
public static function has( $permission ) {
$permissions = self::get();
return in_array( $permission, $permissions, true );
}
/**
* If user give permission or not.
*
* @return boolean
*/
public static function has_console() {
return self::has( 'webmasters' );
}
/**
* If user give permission or not.
*
* @return boolean
*/
public static function has_analytics() {
return self::has( 'analytics.readonly' ) ||
self::has( 'analytics.provision' ) ||
self::has( 'analytics.edit' );
}
/**
* If user give permission or not.
*
* @return boolean
*/
public static function has_adsense() {
return self::has( 'adsense.readonly' );
}
/**
* If user give permission or not.
*
* @return string
*/
public static function get_status() {
return [
esc_html__( 'Search Console', 'rank-math' ) => self::get_status_text( self::has_console() ),
];
}
/**
* Status text
*
* @param boolean $check Truthness.
* @return string
*/
public static function get_status_text( $check ) {
return $check ? esc_html__( 'Given', 'rank-math' ) : esc_html__( 'Not Given', 'rank-math' );
}
/**
* Print warning
*/
public static function print_warning() {
?>
<p class="warning">
<strong class="warning">
<?php esc_html_e( 'Warning:', 'rank-math' ); ?>
</strong>
<?php
/* translators: %s is the reconnect link. */
printf( wp_kses_post( __( 'You have not given the permission to fetch this data. Please <a href="%s">reconnect</a> with all required permissions.', 'rank-math' ) ), esc_url( wp_nonce_url( admin_url( 'admin.php?reconnect=google' ), 'rank_math_reconnect_google' ) ) );
?>
</p>
<?php
}
}

View File

@@ -0,0 +1,502 @@
<?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;
use WP_Error;
use RankMath\Helpers\Schedule;
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 array|WP_Error $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|WP_Error $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;
}
$message = esc_html__( 'Unknown error, call get_response() to find out what happened.', 'rank-math' );
$body = wp_remote_retrieve_body( $response );
if ( ! empty( $body ) ) {
$body = json_decode( $body, true );
if ( ! empty( $body['error'] ) && ! empty( $body['error']['message'] ) ) {
$message = $body['error']['message'];
} elseif ( ! empty( $body['errors'] ) && is_array( $body['errors'] ) && ! empty( $body['errors'][0]['message'] ) ) {
$message = $body['errors'][0]['message'];
}
}
$this->last_error = $message;
}
/**
* 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;
}
Schedule::single_action(
time() + 60,
"rank_math/analytics/get_{$action}_data",
[ $start_date ],
'rank-math'
);
}
}

View File

@@ -0,0 +1,204 @@
<?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',
'richResultsResult.verdict' => 'rich_results_verdict',
'indexStatusResult.crawledAs' => 'crawled_as',
'indexStatusResult.googleCanonical' => 'google_canonical',
'indexStatusResult.userCanonical' => 'user_canonical',
'indexStatusResult.sitemap' => 'sitemap',
'indexStatusResult.referringUrls' => 'referring_urls',
];
$this->assign_inspection_values( $incoming, $map_properties, $normalized );
$normalized = apply_filters( 'rank_math/analytics/url_inspection_map_properties', $normalized, $incoming );
return $normalized;
}
/**
* Assign inspection field value to the data array.
*
* @param array $raw_data Raw data.
* @param string $field Field name.
* @param string $assign_to Field name to assign to.
* @param array $data Data array.
*
* @return void
*/
public function assign_inspection_value( $raw_data, $field, $assign_to, &$data ) {
$data[ $assign_to ] = $this->get_result_field( $raw_data, $field );
if ( is_array( $data[ $assign_to ] ) ) {
$data[ $assign_to ] = wp_json_encode( $data[ $assign_to ] );
} elseif ( preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/', $data[ $assign_to ], $matches ) ) {
// If it's a date, convert to MySQL format.
$data[ $assign_to ] = date( 'Y-m-d H:i:s', strtotime( $matches[0] ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Date is stored as TIMESTAMP, so the timezone is converted automatically.
}
}
/**
* Get a field from the inspection result.
*
* @param array $raw_data Incoming data.
* @param string $field Field name.
*
* @return mixed
*/
protected function get_result_field( $raw_data, $field ) {
if ( false !== strpos( $field, '.' ) ) {
$fields = explode( '.', $field );
if ( ! isset( $raw_data[ $fields[0] ] ) || ! isset( $raw_data[ $fields[0] ][ $fields[1] ] ) ) {
return '';
}
return $raw_data[ $fields[0] ][ $fields[1] ];
}
if ( ! isset( $raw_data[ $field ] ) ) {
return '';
}
return $raw_data[ $field ];
}
/**
* Assign inspection field values to the data array.
*
* @param array $raw_data Raw data.
* @param array $fields Map properties.
* @param array $data Data array.
*
* @return void
*/
public function assign_inspection_values( $raw_data, $fields, &$data ) {
foreach ( $fields as $field => $assign_to ) {
$this->assign_inspection_value( $raw_data, $field, $assign_to, $data );
}
}
}

View File

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

View File

@@ -0,0 +1,360 @@
<?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' ],
'args' => [
'id' => [
'description' => esc_html__( 'Post ID.', 'rank-math' ),
'type' => 'integer',
'required' => true,
],
],
],
'keywordsSummary' => [
'callback' => [ Stats::get(), 'get_analytics_summary' ],
],
'analyticsSummary' => [
'callback' => [ $this, 'get_analytics_summary' ],
'args' => [
'postType' => [
'description' => esc_html__( 'Post Type.', 'rank-math' ),
'type' => 'string',
],
],
],
'keywordsRows' => [
'callback' => [ Stats::get(), 'get_keywords_rows' ],
'args' => [
'page' => [
'description' => esc_html__( 'Page number.', 'rank-math' ),
'type' => 'integer',
'required' => false,
],
'perPage' => [
'description' => esc_html__( 'Results per page.', 'rank-math' ),
'type' => 'integer',
'required' => false,
],
'orderBy' => [
'description' => esc_html__( 'Order by.', 'rank-math' ),
'type' => 'string',
'required' => false,
],
'order' => [
'description' => esc_html__( 'Order.', 'rank-math' ),
'type' => 'string',
'required' => false,
],
'search' => [
'description' => esc_html__( 'Search.', 'rank-math' ),
'type' => 'string',
'required' => false,
],
],
],
'userPreferences' => [
'callback' => [ $this, 'update_user_preferences' ],
'methods' => WP_REST_Server::CREATABLE,
'args' => [
'preferences' => [
'description' => esc_html__( 'User preferences.', 'rank-math' ),
'type' => 'object',
'required' => true,
],
],
],
'inspectionResults' => [
'callback' => [ $this, 'get_inspection_results' ],
'args' => [
'page' => [
'description' => esc_html__( 'Page number.', 'rank-math' ),
'type' => 'integer',
'required' => false,
],
'perPage' => [
'description' => esc_html__( 'Results per page.', 'rank-math' ),
'type' => 'integer',
'required' => false,
],
'orderBy' => [
'description' => esc_html__( 'Order by.', 'rank-math' ),
'type' => 'string',
'required' => false,
],
'order' => [
'description' => esc_html__( 'Order.', 'rank-math' ),
'type' => 'string',
'required' => false,
],
'search' => [
'description' => esc_html__( 'Search.', 'rank-math' ),
'type' => 'string',
'required' => false,
],
'filter' => [
'description' => esc_html__( 'Filter.', 'rank-math' ),
'type' => 'string',
'required' => false,
],
'filterType' => [
'description' => esc_html__( 'Filter type.', 'rank-math' ),
'type' => 'string',
'required' => false,
],
],
],
'removeFrontendStats' => [
'callback' => [ $this, 'remove_frontend_stats' ],
'methods' => WP_REST_Server::CREATABLE,
'args' => [
'toggleBar' => [
'description' => esc_html__( 'Toggle bar.', 'rank-math' ),
'type' => 'boolean',
'required' => false,
],
'hide' => [
'description' => esc_html__( 'Hide.', 'rank-math' ),
'type' => 'boolean',
'required' => false,
],
],
],
];
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.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_dashboard() {
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.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_keywords_overview() {
return rest_ensure_response(
apply_filters(
'rank_math/analytics/keywords_overview',
[
'topKeywords' => Stats::get()->get_top_keywords(),
'positionGraph' => Stats::get()->get_top_position_graph(),
]
)
);
}
/**
* Get inspection results: latest result for each post.
*
* @param WP_REST_Request $request Rest request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_inspection_results( WP_REST_Request $request ) {
$per_page = 25;
$rows = Url_Inspection::get()->get_inspections( $request->get_params(), $per_page );
if ( empty( $rows ) ) {
return [
'rows' => [ 'response' => 'No Data' ],
'rowsFound' => 0,
];
}
return rest_ensure_response(
[
'rows' => $rows,
'rowsFound' => DB::get_inspections_count( $request->get_params() ),
]
);
}
/**
* Remove frontend stats.
*
* @param WP_REST_Request $request Rest request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function remove_frontend_stats( WP_REST_Request $request ) {
if ( (bool) $request->get_param( 'toggleBar' ) ) {
$hide_bar = (bool) $request->get_param( 'hide' );
$user_id = get_current_user_id();
if ( $hide_bar ) {
return update_user_meta( $user_id, 'rank_math_hide_frontend_stats', true );
}
return delete_user_meta( $user_id, 'rank_math_hide_frontend_stats' );
}
$all_opts = rank_math()->settings->all_raw();
$general = $all_opts['general'];
$general['analytics_stats'] = 'off';
Helper::update_all_settings( $general, null, null );
return true;
}
/**
* Should update pagespeed record.
*
* @param int $id Database row id.
* @return bool
*/
private function should_update_pagespeed( $id ) {
$record = DB::objects()->where( 'id', $id )->one();
return \time() > ( \strtotime( $record->pagespeed_refreshed ) + ( DAY_IN_SECONDS * 7 ) );
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Dashboard page template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
use RankMath\Helper;
use RankMath\Google\Authentication;
defined( 'ABSPATH' ) || exit;
$path = rank_math()->admin_dir() . 'wizard/views/'; // phpcs:ignore
?>
<div class="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="" id="rank-math-analytics"></div>';
}
?>
</div>

View File

@@ -0,0 +1,22 @@
<?php
/**
* Analytics Report header template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
use RankMath\KB;
defined( 'ABSPATH' ) || exit;
?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="cta">
<tbody>
<tr class="top">
<td align="left">
<a href="<?php KB::the( 'seo-email-reporting', 'Email Report CTA' ); ?>"><?php $this->image( 'rank-math-pro.jpg', 540, 422, __( 'Rank Math PRO', 'rank-math' ) ); ?></a>
</td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,39 @@
<?php
/**
* Analytics Report email template footer.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
?>
</td>
</tr>
</table>
</td>
</tr>
<!-- START FOOTER -->
<tr class="footer">
<td class="wrapper">
<p class="first">
###FOOTER_HTML###
</p>
</td>
</tr>
<!-- END FOOTER -->
<!-- END MAIN CONTENT AREA -->
</table>
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,42 @@
<?php
/**
* Analytics Report header template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="report-info">
<tr>
<td>
<h1><?php esc_html_e( 'SEO Report of Your Website', 'rank-math' ); ?></h1>
<h2 class="report-date">###START_DATE### - ###END_DATE###</h2>
<a href="###SITE_URL###" target="_blank" class="site-url">###SITE_URL_SIMPLE###</a>
</td>
<td class="full-report-link">
<a href="###REPORT_URL###" target="_blank" class="full-report-link">
<?php esc_html_e( 'FULL REPORT', 'rank-math' ); ?>
<?php $this->image( 'report-icon-external.png', 12, 12, __( 'External Link Icon', 'rank-math' ) ); ?>
</a>
</td>
</tr>
</table>
<?php if ( $this->get_variable( 'stats_invalid_data' ) ) { ?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="report-error">
<tr>
<td>
<h2><?php esc_html_e( 'Uh-oh', 'rank-math' ); ?></h2>
<p><em><?php esc_html_e( 'It seems that there are no stats to show right now.', 'rank-math' ); ?></em></p>
<?php // Translators: placeholders are anchor opening and closing tags. ?>
<p><?php printf( esc_html__( 'If you can see the site data in your Search Console and Analytics accounts, but not here, then %1$s try reconnecting your account %2$s and make sure that the correct properties are selected in the %1$s Analytics Settings%2$s.', 'rank-math' ), '<a href="' . esc_url( Helper::get_settings_url( 'general', 'analytics' ) ) . '">', '</a>' ); ?></p>
</td>
</tr>
</table>
<?php
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* Analytics Report header template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
?><!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title><?php esc_html_e( 'SEO Report of Your Website', 'rank-math' ); ?></title>
<?php $this->template_part( 'style' ); ?>
</head>
<body class="">
<span class="preheader"><?php esc_html_e( 'SEO Report of Your Website', 'rank-math' ); ?></span>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<table role="presentation" class="main" border="0" cellpadding="0" cellspacing="0">
<!-- START HEADER -->
<tr>
<td class="header">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td class="logo">
<a href="###LOGO_LINK###" target="_blank">
<?php $this->image( 'report-logo.png', 0, 26, __( 'Rank Math', 'rank-math' ) ); ?>
</a>
</td>
<td class="period-days">
<?php // Translators: don't translate the variable names between the #hashes#. ?>
<?php esc_html_e( 'Last ###PERIOD_DAYS### Days', 'rank-math' ); ?>
</td>
</tr>
</table>
</td>
</tr>
<!-- END HEADER -->
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td>

View File

@@ -0,0 +1,24 @@
<?php
/**
* Analytics Report email template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
$this->template_part( 'header' );
?>
<?php $this->template_part( 'header-after' ); ?>
<?php $this->template_part( 'sections/summary' ); ?>
<?php $this->template_part( 'sections/positions' ); ?>
<?php $this->template_part( 'cta' ); ?>
<?php
$this->template_part( 'footer' );

View File

@@ -0,0 +1,56 @@
<?php
/**
* Analytics Report summary table template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
?>
<?php if ( $this->get_variable( 'stats_invalid_data' ) ) { ?>
<?php return; ?>
<?php } ?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="stats-2">
<tr>
<td class="col-1">
<h3><?php esc_html_e( 'Top 3 Positions', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_top_3_positions' ),
'diff' => $this->get_variable( 'stats_top_3_positions_diff' ),
]
);
?>
</td>
<td class="col-2">
<h3><?php esc_html_e( '4-10 Positions', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_top_10_positions' ),
'diff' => $this->get_variable( 'stats_top_10_positions_diff' ),
]
);
?>
</td>
<td class="col-3">
<h3><?php esc_html_e( '11-50 Positions', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_top_50_positions' ),
'diff' => $this->get_variable( 'stats_top_50_positions_diff' ),
]
);
?>
</td>
</tr>
</table>

View File

@@ -0,0 +1,81 @@
<?php
/**
* Analytics Report summary table template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
?>
<?php if ( $this->get_variable( 'stats_invalid_data' ) ) { ?>
<?php return; ?>
<?php } ?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="stats">
<tr>
<td class="col-1">
<h3><?php esc_html_e( 'Total Impressions', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_impressions' ),
'diff' => $this->get_variable( 'stats_impressions_diff' ),
'graph' => true,
'graph_data' => $this->get_graph_data( 'impressions' ),
]
);
?>
</td>
<td class="col-2">
<h3><?php esc_html_e( 'Total Clicks', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_clicks' ),
'diff' => $this->get_variable( 'stats_clicks_diff' ),
'graph' => true,
'graph_data' => $this->get_graph_data( 'clicks' ),
]
);
?>
</td>
</tr>
<tr>
<td class="col-1">
<h3><?php esc_html_e( 'Total Keywords', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_keywords' ),
'diff' => $this->get_variable( 'stats_keywords_diff' ),
'graph' => true,
'graph_data' => $this->get_graph_data( 'keywords' ),
]
);
?>
</td>
<td class="col-2">
<h3><?php esc_html_e( 'Average Position', 'rank-math' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_position' ),
'diff' => $this->get_variable( 'stats_position_diff' ),
'graph' => true,
'graph_data' => $this->get_graph_data( 'position' ),
'graph_modifier' => -100,
'human_number' => false,
'invert' => true,
]
);
?>
</td>
</tr>
</table>

View File

@@ -0,0 +1,69 @@
<?php
/**
* Analytics Report header template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
use RankMath\Helpers\Str;
defined( 'ABSPATH' ) || exit;
$diff_class = $diff > 0 ? 'positive' : 'negative';
if ( ! empty( $invert ) ) {
$diff_class = $diff < 0 ? 'positive' : 'negative';
}
$diff_sign = '<span class="diff-sign">' . ( 'positive' === $diff_class ? '&#9650;' : '&#9660;' ) . '</span>';
if ( 0.0 === floatval( $diff ) ) {
$diff_class = 'no-diff';
$diff_sign = '';
}
$stat_value = $value;
$stat_diff = abs( $diff );
// Human number is 'true' by default.
if ( ! isset( $human_number ) || $human_number ) {
$stat_value = Str::human_number( $stat_value );
$stat_diff = Str::human_number( $stat_diff );
}
?>
<span class="stat-value">
<?php echo esc_html( $stat_value ); ?>
</span>
<span class="stat-diff <?php echo sanitize_html_class( $diff_class ); ?>">
<?php echo $diff_sign . ' ' . esc_html( $stat_diff ); // phpcs:ignore ?>
</span>
<?php
if ( ! empty( $graph ) && ! empty( $graph_data ) ) {
$show_graph = false;
// Check data points.
foreach ( $graph_data as $key => $value ) {
if ( ! empty( $value ) ) {
$show_graph = true;
}
// Adjust values.
if ( ! empty( $graph_modifier ) ) {
$graph_data[ $key ] = abs( $graph_data[ $key ] + $graph_modifier );
}
}
if ( ! $show_graph ) {
return;
}
// `img` tag size.
// Actual image size is 3x this.
$width = 64;
$height = 34;
$this->image( $this->charts_api_url( $graph_data, $width * 3, $height * 3 ), $width, $height, __( 'Data Chart', 'rank-math' ), [ 'style' => 'float: right;margin-top: -7px;' ] );
} ?>

View File

@@ -0,0 +1,496 @@
<?php
/**
* Analytics Report email styling.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
?>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
/* All the styling goes here */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f7f9fb;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
}
table td {
font-size: 15px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body, td {
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;
}
.body {
background-color: #F0F4F8;
width: 100%;
}
/* Set a max-width, and make it display as block. */
.container {
display: block;
margin: 0 auto !important;
/* makes it centered */
max-width: 90%;
padding: 50px 0;
width: 600px;
}
.content {
box-sizing: border-box;
display: block;
margin: 0 auto;
width: 100%;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 6px;
width: 100%;
color: #1a1e22;
}
.wrapper {
box-sizing: border-box;
padding: 30px 30px 60px;
}
.header {
background: #724BB7;
background: linear-gradient(90deg, #724BB7 0%, #4098D7 100%);
border-radius: 8px 8px 0 0;
height: 76px;
vertical-align: middle;
padding: 0 30px;
color: #ffffff;
}
td.logo {
vertical-align: middle;
}
td.logo img {
width: auto;
height: 26px;
margin-top: 6px;
}
.period-days {
text-align: right;
vertical-align: middle;
font-weight: 500;
letter-spacing: 0.5px;
font-size: 14px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
margin-top: 10px;
width: 100%;
}
.footer .wrapper {
padding-bottom: 30px;
}
.footer td,
.footer p,
.footer span {
color: #999ba7;
font-size: 14px;
}
.footer td {
padding-top: 0;
}
.footer p.first {
padding-top: 20px;
border-top: 1px solid #e5e5e7;
line-height: 1.8;
margin-bottom: 0;
}
.footer .rank-math-contact-address {
font-style: normal;
}
.footer p:empty {
display: none;
}
.footer address {
display: inline-block;
font-style: normal;
margin-top: 10px;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-weight: 600;
line-height: 1.4;
margin: 0;
}
h1 {
font-size: 30px;
}
p,
ul,
ol {
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #22a8e6;
text-decoration: none;
}
h2.report-date {
margin: 25px 0 4px;
font-size: 18px;
}
.site-url {
color: #595d6f;
text-decoration: none;
font-size: 15px;
}
.full-report-link {
vertical-align: bottom;
text-align: right;
width: 110px;
}
.full-report-link a {
font-size: 12px;
font-weight: 600;
text-decoration: none;
}
.full-report-link img {
vertical-align: -1px;
margin-left: 2px;
}
table.report-error {
border: 2px solid #f1d400;
background: #fffdec;
margin: 10px 0;
}
table.report-error td {
padding: 5px 10px;
}
table.stats {
border-collapse: separate;
margin-top: 10px;
}
table.stats td {
width: 50%;
padding: 20px 20px;
background: #f7f9fb;
border: 10px solid #fff;
border-radius: 16px;
}
table.stats td.col-2 {
border-right: none;
}
table.stats td.col-1 {
border-left: none;
}
h3 {
font-size: 13px;
font-weight: 500;
color: #565a6b;
text-transform: uppercase;
}
.stat-value {
color: #000000;
font-size: 25px;
font-weight: 700;
}
.stat-diff {
font-size: 14px;
font-weight: 500;
}
.stat-diff.positive {
color: #339e75;
}
span.stat-diff.negative {
color: #e2454f;
}
.stat-diff.no-diff {
color: #999ba7;
}
.diff-sign {
font-size: 10px;
}
.stats-2 {
margin: 50px 0 24px;
}
.stats-2 td.col-1, .stats-2 td.col-2 {
border-right: 3px solid #f7f9fb;
}
.stats-2 td.col-2, .stats-2 td.col-3 {
padding-left: 40px;
}
.cta {
margin-bottom: 0;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%;
}
.btn > tbody > tr > td {
padding-bottom: 48px;
text-align: center;
padding-top: 34px;
}
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
border: none;
border-radius: 31px;
box-sizing: border-box;
color: #59403b;
cursor: pointer;
display: inline-block;
font-size: 16px;
font-weight: 700;
margin: 0;
padding: 18px 44px;
text-decoration: none;
text-transform: capitalize;
background: rgb(47,166,129);
background: linear-gradient( 0deg, #f7d070 0%, #f7dc6f 100%);
letter-spacing: 0.7px;
}
.btn-primary table td {
background-color: #3498db;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
}
hr {
border: 0;
border-bottom: 1px solid #F0F4F8;
margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.rankmath-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
<?php $this->template_part( 'pro-style' ); ?>

View File

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

View File

@@ -0,0 +1,165 @@
<?php
/**
* Search console options.
*
* @package Rank_Math
*/
use RankMath\KB;
use RankMath\Helper;
use RankMath\Analytics\DB;
use RankMath\Helpers\Str;
use RankMath\Google\Authentication;
defined( 'ABSPATH' ) || exit;
// phpcs:disable
$actions = \as_get_scheduled_actions(
[
'hook' => 'rank_math/analytics/clear_cache',
'status' => \ActionScheduler_Store::STATUS_PENDING,
]
);
$db_info = DB::info();
$is_queue_empty = empty( $actions );
$disable = ( ! Authentication::is_authorized() || ! $is_queue_empty ) ? true : false;
if ( ! empty( $db_info ) ) {
$db_info = [
/* translators: number of days */
'<div class="rank-math-console-db-info"><i class="rm-icon rm-icon-calendar"></i> ' . sprintf( esc_html__( 'Storage Days: %s', 'rank-math' ), '<strong>' . $db_info['days'] . '</strong>' ) . '</div>',
/* translators: number of rows */
'<div class="rank-math-console-db-info"><i class="rm-icon rm-icon-faq"></i> ' . sprintf( esc_html__( 'Data Rows: %s', 'rank-math' ), '<strong>' . Str::human_number( $db_info['rows'] ) . '</strong>' ) . '</div>',
/* translators: database size */
'<div class="rank-math-console-db-info"><i class="rm-icon rm-icon-database"></i> ' . sprintf( esc_html__( 'Size: %s', 'rank-math' ), '<strong>' . size_format( $db_info['size'] ) . '</strong>' ) . '</div>',
];
}
$actions = as_get_scheduled_actions(
[
'order' => 'DESC',
'hook' => 'rank_math/analytics/data_fetch',
'status' => \ActionScheduler_Store::STATUS_PENDING,
]
);
if ( Authentication::is_authorized() && ! empty( $actions ) ) {
$action = current( $actions );
$schedule = $action->get_schedule();
$next_date = $schedule->get_date();
if ( $next_date ) {
$cmb->add_field(
[
'id' => 'console_data_empty',
'type' => 'raw',
/* translators: date */
'content' => sprintf(
'<span class="next-fetch">' . __( 'Next data fetch on %s', 'rank-math' ),
date_i18n( 'd M, Y H:m:i', $next_date->getTimestamp() ) . '</span>'
),
]
);
}
}
// phpcs:enable
$cmb->add_field(
[
'id' => 'search_console_ui',
'type' => 'raw',
'file' => rank_math()->admin_dir() . '/wizard/views/search-console-ui.php',
]
);
if ( ! Authentication::is_authorized() ) {
return;
}
$is_fetching = 'fetching' === get_option( 'rank_math_analytics_first_fetch' );
$buttons = '<br>' .
'<button class="button button-small console-cache-delete" data-days="-1">' . esc_html__( 'Delete data', 'rank-math' ) . '</button>' .
'&nbsp;&nbsp;<button class="button button-small console-cache-update-manually"' . ( $disable ? ' disabled="disabled"' : '' ) . '>' . ( $is_queue_empty ? esc_html__( 'Update data manually', 'rank-math' ) : esc_html__( 'Fetching in Progress', 'rank-math' ) ) . '</button>' .
'&nbsp;&nbsp;<button class="button button-link-delete button-small cancel-fetch"' . disabled( $is_fetching, false, false ) . '>' . esc_html__( 'Cancel Fetching', 'rank-math' ) . '</button>';
$buttons .= '<br>' . join( '', $db_info );
// Translators: placeholder is a link to rankmath.com, with "free version" as the anchor text.
$description = sprintf( __( 'Enter the number of days to keep Analytics data in your database. The maximum allowed days are 90 in the %s. Though, 2x data will be stored in the DB for calculating the difference properly.', 'rank-math' ), '<a href="' . KB::get( 'pro', 'Analytics DB Option' ) . '" target="_blank" rel="noopener noreferrer">' . __( 'free version', 'rank-math' ) . '</a>' );
$description = apply_filters_deprecated( 'rank_math/analytics/options/cahce_control/description', [ $description ], '1.0.61.1', 'rank_math/analytics/options/cache_control/description' );
$description = apply_filters( 'rank_math/analytics/options/cache_control/description', $description );
$cmb->add_field(
[
'id' => 'console_caching_control',
'type' => 'text',
'name' => __( 'Analytics Database', 'rank-math' ),
// translators: Anchor text 'free version', linking to pricing page.
'description' => $description,
'default' => 90,
'sanitization_cb' => function ( $value ) {
$max = apply_filters( 'rank_math/analytics/max_days_allowed', 90 );
$value = absint( $value );
if ( $value > $max ) {
$value = $max;
}
return $value;
},
'after_field' => $buttons,
]
);
$cmb->add_field(
[
'id' => 'analytics_stats',
'type' => 'toggle',
'name' => __( 'Frontend Stats Bar', 'rank-math' ),
'description' => esc_html__( 'Enable this option to show Analytics Stats on the front just after the admin bar.', 'rank-math' ),
'default' => 'on',
]
);
if ( RankMath\Analytics\Email_Reports::are_fields_hidden() ) {
return;
}
$preview_url = home_url( '?rank_math_analytics_report_preview=1' );
$report_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>', $report_title, $description ),
]
);
$cmb->add_field(
[
'id' => 'console_email_reports',
'type' => 'toggle',
'name' => __( 'Email Reports', 'rank-math' ),
'description' => __( 'Turn on email reports.', 'rank-math' ),
'default' => Helper::get_settings( 'general.console_email_reports' ) ? 'on' : 'off',
'classes' => 'nob',
]
);
$is_pro_active = defined( 'RANK_MATH_PRO_FILE' );
$pro_badge = '<span class="rank-math-pro-badge"><a href="' . KB::get( 'seo-email-reporting', 'Email Frequency Toggle' ) . '" target="_blank" rel="noopener noreferrer">' . __( 'PRO', 'rank-math' ) . '</a></span>';
$args = [
'id' => 'console_email_frequency',
'type' => 'select',
'name' => esc_html__( 'Email Frequency', 'rank-math' ) . ( ! $is_pro_active ? $pro_badge : '' ),
'desc' => wp_kses_post( __( 'Email report frequency.', 'rank-math' ) ),
'default' => 'monthly',
'options' => [
'monthly' => esc_html__( 'Every 30 days', 'rank-math' ),
],
'dep' => [ [ 'console_email_reports', 'on' ] ],
'attributes' => ! $is_pro_active ? [ 'disabled' => 'disabled' ] : [],
'before_row' => ! $is_pro_active ? '<div class="cmb-redirector-element" data-url="' . KB::get( 'seo-email-reporting', 'Email Frequency Toggle' ) . '">' : '',
'after_row' => ! $is_pro_active ? '</div>' : '',
];
$cmb->add_field( $args );

View File

@@ -0,0 +1,210 @@
<?php
/**
* Workflow Base.
*
* @since 1.0.54
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics\Workflow;
use RankMath\Helper;
use RankMath\Analytics\DB;
use RankMath\Traits\Hooker;
use RankMath\Helpers\Schedule;
use function has_filter;
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;
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;
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 $previous_value Previous profile data.
* @param string $new_value New posted profile data.
*
* @return boolean
*/
public function is_profile_updated( $param, $previous_value, $new_value ) {
if (
! is_null( $previous_value ) &&
! is_null( $new_value ) &&
isset( $previous_value[ $param ] ) &&
isset( $new_value[ $param ] ) &&
$previous_value[ $param ] === $new_value[ $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
);
Schedule::single_action(
$timestamp,
'rank_math/analytics/get_' . $action . '_data',
$args,
$group,
$unique
);
}
Workflow::add_clear_cache( $timestamp );
// Update timestamp.
update_option( 'rank_math_analytics_last_single_action_schedule_time', $timestamp );
}
}

View File

@@ -0,0 +1,99 @@
<?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;
$table = 'rank_math_analytics_gsc';
DB::create_table(
$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)'
);
// 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_value New posted value.
*/
public function create_data_jobs( $days, $prev, $new_value ) {
// Early bail if saved & new profile are same.
if ( ! $this->is_profile_updated( 'profile', $prev, $new_value ) ) {
return;
}
update_option( 'rank_math_analytics_first_fetch', 'fetching' );
// Fetch now.
$this->schedule_single_action( $days, 'console' );
}
}

View File

@@ -0,0 +1,181 @@
<?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\Workflow\Base;
use RankMath\Analytics\DB as AnalyticsDB;
use RankMath\Analytics\Url_Inspection;
use RankMath\Google\Console;
use RankMath\Helpers\Schedule;
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;
$table = 'rank_math_analytics_inspections';
DB::create_table(
$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 */
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 rich_results_verdict (rich_results_verdict)"
);
// 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;
}
global $wpdb;
$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();
$pages = [];
foreach ( $objects as $object ) {
if ( $object->created && date( 'Y-m-d', strtotime( $object->created ) ) === date( 'Y-m-d' ) ) {
continue;
}
$pages[] = $object->page;
}
if ( empty( $pages ) ) {
return;
}
$dates = Base::get_dates();
$query = $wpdb->prepare(
"SELECT DISTINCT(page) as page, COUNT(impressions) as total
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE page IN ('" . join( "', '", $pages ) . "')
AND DATE(created) BETWEEN %s AND %s
GROUP BY page
ORDER BY total DESC",
$dates['start_date'],
$dates['end_date']
);
$top_pages = $wpdb->get_results( $query );
$top_pages = wp_list_pluck( $top_pages, 'page' );
$pages = array_merge( $top_pages, $pages );
$pages = array_unique( $pages );
$count = 0;
foreach ( $pages as $page ) {
++$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 );
}
Schedule::single_action( $time, 'rank_math/analytics/get_inspections_data', [ $page ], 'rank-math' );
}
}
}

View File

@@ -0,0 +1,326 @@
<?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\Helpers\DB as DB_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;
use 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() {
$success = Console::test_connection();
if ( $success ) {
$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.
DB_Helper::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.
DB_Helper::get_results( "DELETE FROM {$wpdb->prefix}rank_math_analytics_inspections WHERE page NOT IN ( SELECT page from {$wpdb->prefix}rank_math_analytics_objects )" );
delete_transient( 'rank_math_analytics_data_info' );
DB::purge_cache();
DB::delete_data_log();
$this->calculate_stats();
update_option( 'rank_math_analytics_last_updated', time() );
Workflow::do_workflow( 'inspections' );
}
/**
* Set the console start and end dates.
*
* @param array $args Args containing start and end date.
*/
public function get_console_days( $args = [] ) {
set_time_limit( 300 );
$rows = Api::get()->get_search_analytics(
[
'start_date' => $args['start_date'],
'end_date' => $args['end_date'],
'dimensions' => [ 'date' ],
]
);
if ( empty( $rows ) || is_wp_error( $rows ) ) {
return [];
}
$empty_dates = get_option( 'rank_math_console_empty_dates', [] );
$dates = [];
foreach ( $rows as $row ) {
// Have at least few impressions.
if ( $row['impressions'] ) {
$date = $row['keys'][0];
if ( ! DB::date_exists( $date, 'console' ) && ! in_array( $date, $empty_dates, true ) ) {
$dates[] = [
'start_date' => $date,
'end_date' => $date,
];
}
}
}
return $dates;
}
/**
* Get console data.
*
* @param string $date Date to fetch data for.
*/
public function get_console_data( $date ) {
set_time_limit( 300 );
$rows = Api::get()->get_search_analytics(
[
'start_date' => $date,
'end_date' => $date,
'dimensions' => [ 'query', 'page' ],
]
);
if ( empty( $rows ) || is_wp_error( $rows ) ) {
return;
}
$rows = \array_map( [ $this, 'normalize_query_page_data' ], $rows );
try {
DB::add_query_page_bulk( $date, $rows );
// Clear the cache here.
$this->cache_flush_group( 'rank_math_rest_keywords_rows' );
$this->cache_flush_group( 'rank_math_posts_rows_by_objects' );
$this->cache_flush_group( 'rank_math_analytics_summary' );
return $rows;
} catch ( Exception $e ) {} // phpcs:ignore
}
/**
* Handlle console response.
*
* @param array $data API request and response data.
*/
public function handle_console_response( $data = [] ) {
if ( 200 !== $data['code'] ) {
return;
}
if ( isset( $data['formatted_response']['rows'] ) && ! empty( $data['formatted_response']['rows'] ) ) {
return;
}
if ( ! isset( $data['args']['startDate'] ) ) {
return;
}
$dates = get_option( 'rank_math_console_empty_dates', [] );
if ( ! $dates ) {
$dates = [];
}
$dates[] = $data['args']['startDate'];
$dates[] = $data['args']['endDate'];
$dates = array_unique( $dates );
update_option( 'rank_math_console_empty_dates', $dates );
}
/**
* Get inspection results from the API and store them in the database.
*
* @param string $page URI to fetch data for.
*/
public function get_inspections_data( $page ) {
// If the option is disabled, don't fetch data.
if ( ! \RankMath\Analytics\Url_Inspection::is_enabled() ) {
return;
}
$inspection = Url_Inspection::get()->get_inspection_data( $page );
if ( empty( $inspection ) ) {
return;
}
try {
DB::store_inspection( $inspection );
} catch ( Exception $e ) {} // phpcs:ignore
}
/**
* Check for missing dates.
*
* @param string $action Action to perform.
*/
public function check_for_missing_dates( $action ) {
$days = Helper::get_settings( 'general.console_caching_control', 90 );
Workflow::do_workflow(
$action,
$days,
null,
null
);
}
/**
* Calculate stats.
*/
private function calculate_stats() {
$ranges = [
'-7 days',
'-15 days',
'-30 days',
'-3 months',
'-6 months',
'-1 year',
];
foreach ( $ranges as $range ) {
Stats::get()->set_date_range( $range );
Stats::get()->get_top_keywords();
}
}
/**
* Normalize console data.
*
* @param array $row Single row item.
*
* @return array
*/
protected function normalize_query_page_data( $row ) {
$row = $this->normalize_data( $row );
$row['query'] = $row['keys'][0];
$row['page'] = $row['keys'][1];
unset( $row['keys'] );
return $row;
}
/**
* Normalize console data.
*
* @param array $row Single row item.
*
* @return array
*/
private function normalize_data( $row ) {
$row['ctr'] = round( $row['ctr'] * 100, 2 );
$row['position'] = round( $row['position'], 2 );
return $row;
}
}

View File

@@ -0,0 +1,186 @@
<?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.
// Also remove the connection errors.
foreach (
[
'rankmath_google_api_failed_attempts_data',
'rankmath_google_api_reconnect',
'rank_math_console_connection_error',
'rank_math_analytics_connection_error',
'rank_math_adsense_connection_error',
] as $option
) {
delete_option( $option );
}
Permissions::fetch();
if ( ! empty( $redirect ) ) {
Helper::redirect( $redirect );
exit;
}
}
/**
* Reconnect Google.
*/
public function reconnect_google() {
if ( ! isset( $_GET['reconnect'] ) || 'google' !== $_GET['reconnect'] ) {
return;
}
if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'rank_math_reconnect_google' ) ) {
wp_nonce_ays( 'rank_math_reconnect_google' );
die();
}
if ( ! Helper::has_cap( 'analytics' ) ) {
return;
}
$rows = DB::objects()
->selectCount( 'id' )
->getVar();
if ( empty( $rows ) ) {
delete_option( 'rank_math_analytics_installed' );
}
Api::get()->revoke_token();
Workflow::kill_workflows();
wp_redirect( Authentication::get_auth_url() ); // phpcs:ignore
die();
}
/**
* Get access token from url.
*
* @return string
*/
private function get_tokens_from_url() {
$data = [
'access_token' => urldecode( Param::get( 'access_token', '', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ) ),
'refresh_token' => urldecode( Param::get( 'refresh_token', '', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK ) ),
'expire' => urldecode( Param::get( 'expire', 0, FILTER_VALIDATE_INT ) ),
];
Authentication::tokens( $data );
$current_request = remove_query_arg(
[
'access_token',
'refresh_token',
'expire',
'security',
]
);
return $current_request;
}
/**
* Get access token from rankmath server.
*
* @return string
*/
private function get_tokens_from_server() {
// Bail if the user is not authenticated at all yet.
$id = Param::get( 'process_oauth', 0, FILTER_VALIDATE_INT );
if ( $id < 1 ) {
return;
}
$response = wp_remote_get( Authentication::get_auth_app_url() . '/get.php?id=' . $id );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return;
}
$response = wp_remote_retrieve_body( $response );
if ( empty( $response ) ) {
return;
}
$response = \json_decode( $response, true );
unset( $response['id'] );
// Save new token.
Authentication::tokens( $response );
$redirect = Security::remove_query_arg_raw( [ 'process_oauth', 'security' ] );
if ( Str::contains( 'rank-math-options-general', $redirect ) ) {
$redirect .= '#setting-panel-analytics';
}
Helper::remove_notification( 'rank_math_analytics_reauthenticate' );
return $redirect;
}
}

View File

@@ -0,0 +1,134 @@
<?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;
use RankMath\Helpers\Schedule;
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() {
DB::create_table(
'rank_math_analytics_objects',
'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))'
);
}
/**
* Create jobs to fetch data.
*/
public function create_data_job() {
// Clear old schedule.
wp_clear_scheduled_hook( 'rank_math/analytics/get_analytics' );
// Schedule new action only when there is no existing action.
if ( false === as_next_scheduled_action( 'rank_math/analytics/data_fetch' ) ) {
Helper::schedule_data_fetch();
}
}
/**
* Flat posts
*/
public function flat_posts() {
$ids = get_posts(
[
'post_type' => $this->get_post_types(),
'post_status' => 'publish',
'fields' => 'ids',
'posts_per_page' => -1,
]
);
$counter = 0;
$chunks = \array_chunk( $ids, 50 );
foreach ( $chunks as $chunk ) {
++$counter;
Schedule::single_action(
time() + ( 60 * ( $counter / 2 ) ),
'rank_math/analytics/flat_posts',
[ $chunk ],
'rank-math'
);
}
// Check for posts.
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 ) ) );
}
/**
* Get post types to process.
*/
private function get_post_types() {
$post_types = $this->do_filter( 'analytics/post_types', Helper::get_accessible_post_types() );
unset( $post_types['attachment'] );
if ( isset( $post_types['web-story'] ) ) {
unset( $post_types['web-story'] );
}
return array_keys( $post_types );
}
}

View File

@@ -0,0 +1,160 @@
<?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 RankMath\Helpers\Schedule;
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_value New posted value.
*/
public function start_workflow( $action, $days = 0, $prev = null, $new_value = null ) {
do_action(
'rank_math/analytics/workflow/' . $action,
$days,
$prev,
$new_value
);
}
/**
* 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_value New posted value.
*/
public static function do_workflow( $action, $days = 0, $prev = null, $new_value = null ) {
Schedule::async_action(
'rank_math/analytics/workflow',
[
$action,
$days,
$prev,
$new_value,
],
'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' );
Schedule::single_action(
$time,
'rank_math/analytics/clear_cache',
[],
'rank-math'
);
delete_option( 'rank_math_analytics_last_single_action_schedule_time' );
}
}

View File

@@ -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' => __DIR__ . '/views/options-titles.php',
];
return $tabs;
}
}

View File

@@ -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 ) ),
'mainEntity' => [
'@type' => 'Person',
'name' => get_the_author_meta( 'display_name', $user_id ),
'url' => function_exists( 'bp_members_get_user_url' ) ? esc_url( bp_members_get_user_url( $user_id ) ) : 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;
}
}

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,17 @@
<?php // phpcs:ignore WordPress.Files.FileName.NotHyphenatedLowercase -- This filename format is required to dynamically load the necessary block dependencies.
/**
* Block script dependencies.
*
* @package RankMath
* @subpackage RankMath\ContentAI
* @author Rank Math <support@rankmath.com>
*/
return [
'dependencies' => [
'wp-element',
'wp-block-editor',
'lodash',
],
'version' => rank_math()->version,
];

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"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"
}
}
}

View File

@@ -0,0 +1,229 @@
/**
* WordPress dependencies
*/
import {
RichText,
useBlockProps,
BlockControls,
store as blockEditorStore,
} from '@wordpress/block-editor'
import { useDispatch } from '@wordpress/data'
import { useRef, useEffect } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
/**
* Internal dependencies
*/
import hasError from '../../../../assets/src/helpers/hasError'
import insertCommandBox, { useBlock, regenerateOutput, writeMore } from '../../../../assets/src/shortcutCommand/insertCommandBox'
import getWriteAttributes from '../../../../assets/src/helpers/getWriteAttributes'
import getBlockContent from '../../../../assets/src/helpers/getBlockContent'
const getErrorMessage = () => {
// This function can be simplified now that the error message comes from the API.
// It's still useful for showing general errors not from the API.
if ( ! rankMath.contentAI.isUserRegistered ) {
return (
<>
{ __( 'Start using Content AI by connecting your RankMath account.', 'rank-math' ) }
<a href={ rankMath.connectSiteUrl }>{ __( 'Connect Now', 'rank-math' ) }</a>
</>
)
}
if ( ! rankMath.contentAI.plan ) {
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,
clientId,
} ) => {
// Note the addition of `hasApiError` in the attributes destructuring.
const { content, isAiGenerated, hasApiError, endpoint, params } = attributes
const blockProps = useBlockProps( {
className: 'rank-math-content-ai-command',
} )
const { updateBlockAttributes, removeBlock } = useDispatch( blockEditorStore )
const blockHook = useBlock // assign the hook to a variable
const contentEditableRef = useRef( null )
useEffect( () => {
const { current: contentEditable } = contentEditableRef
if ( ! contentEditable ) {
return
}
contentEditable.focus()
const range = document.createRange()
const selection = window.getSelection()
range.selectNodeContents( contentEditable )
range.collapse( false )
selection.removeAllRanges()
selection.addRange( range )
}, [] )
const handleRunCommand = () => {
const text = getBlockContent( { attributes } )
if ( ! text.trim() ) {
return
}
updateBlockAttributes(
clientId,
{
content: '',
className: '',
}
)
insertCommandBox( 'Write', getWriteAttributes( text ), clientId )
}
const handleDismissCommand = () => {
removeBlock( clientId )
}
const handleUseBlock = () => {
blockHook( clientId, attributes )
}
const handleRegenerateOutput = () => {
regenerateOutput( clientId, endpoint, params )
}
const handleWriteMore = () => {
writeMore( clientId )
}
const handleKeyDown = ( event ) => {
if ( event.key === 'Enter' && ! isAiGenerated ) {
event.preventDefault()
handleRunCommand()
}
}
const contentTrimmed = content.replace( /(<([^>]+)>)/ig, '' ).trim()
const renderActionButtons = () => {
// This is the correct condition to show the dismiss button on API error.
if ( hasApiError ) {
return (
<button
className="button button-small rank-math-content-ai-dismiss"
title={ __( 'Dismiss', 'rank-math' ) }
onClick={ handleDismissCommand }
contentEditable={ false }
>
{ __( 'Dismiss', 'rank-math' ) }
</button>
)
}
if ( isAiGenerated ) {
return (
<div className="rank-math-content-ai-command-buttons">
<button
className="button button-small rank-math-content-ai-use"
onClick={ handleUseBlock }
>
<span>{ __( 'Use', 'rank-math' ) }</span>
</button>
<button
className="button button-small rank-math-content-ai-regenerate"
onClick={ handleRegenerateOutput }
>
<span>{ __( 'Regenerate', 'rank-math' ) }</span>
</button>
<button
className="button button-small rank-math-content-ai-write-more"
onClick={ handleWriteMore }
>
<span>{ __( 'Write More', 'rank-math' ) }</span>
</button>
</div>
)
}
// This is the condition for the initial command box.
if ( contentTrimmed.length > 0 ) {
return (
<>
<button
className="rank-math-content-ai-command-button"
title={ __( 'Click or Press Enter', 'rank-math' ) }
onClick={ handleRunCommand }
contentEditable={ false }
>
<i className="rm-icon rm-icon-enter-key"></i>
</button>
<button
className="rank-math-command-dismiss-button"
title={ __( 'Dismiss', 'rank-math' ) }
onClick={ handleDismissCommand }
contentEditable={ false }
>
{ __( 'Close', 'rank-math' ) }
</button>
</>
)
}
return null
}
return (
<div { ...blockProps }>
<BlockControls />
{
hasError() &&
<div className="rich-text" ref={ contentEditableRef }>
{ getErrorMessage() }
</div>
}
{ ! hasError() &&
<>
<RichText
tagName="div"
allowedFormats={ [] }
value={ content }
onChange={ ( newContent ) => {
setAttributes( { content: newContent } )
} }
onSplit={ () => false }
onReplace={ onReplace }
data-empty={ content ? false : true }
onKeyDown={ handleKeyDown }
ref={ contentEditableRef }
/>
<div className="rank-math-content-ai-command-buttons">
{ renderActionButtons() }
</div>
</>
}
</div>
)
}

View File

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

View File

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

View File

@@ -0,0 +1,223 @@
<?php
/**
* The Content AI module.
*
* @since 1.0.219
* @package RankMath
* @subpackage RankMath
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Helpers\Arr;
use RankMath\CMB2;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Admin class.
*/
class Admin {
use Hooker;
/**
* Content_AI object.
*
* @var object
*/
public $content_ai;
/**
* Class constructor.
*
* @param Object $content_ai Content_AI class object.
*/
public function __construct( $content_ai ) {
if ( ! is_admin() ) {
return;
}
$this->content_ai = $content_ai;
$this->filter( 'rank_math/analytics/post_data', 'add_contentai_data' );
$this->filter( 'rank_math/settings/general', 'add_settings' );
$this->action( 'cmb2_admin_init', 'add_content_ai_metabox', 11 );
$this->action( 'rank_math/deregister_site', 'remove_credits_data' );
$this->filter( 'rank_math/status/rank_math_info', 'content_ai_info' );
$this->action( 'rank_math/connect/account_connected', 'refresh_content_ai_credits' );
}
/**
* Add Content AI score in Single Page Site Analytics.
*
* @param array $data array.
* @return array $data sorted array.
*/
public function add_contentai_data( $data ) {
$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;
}
/**
* 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' => __DIR__ . '/views/options.php',
'json' => [
'countries' => Helper::choices_contentai_countries(),
'credits' => Helper::get_credits(),
'refreshDate' => Helper::get_content_ai_refresh_date(),
'tone' => Helper::choices_contentai_tone(),
'audience' => Helper::choices_contentai_audience(),
'language' => Helper::choices_contentai_language(),
],
],
],
8
);
return $tabs;
}
/**
* Add link suggestion metabox.
*/
public function add_content_ai_metabox() {
if ( ! $this->content_ai->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 );
}
/**
* 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.
*
* @return void
*/
public function refresh_content_ai_credits() {
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 );
}
}

View File

@@ -0,0 +1,196 @@
<?php
/**
* The Content AI module.
*
* @since 1.0.219
* @package RankMath
* @subpackage RankMath
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Admin class.
*/
class Assets {
use Hooker;
/**
* Content_AI object.
*
* @var object
*/
public $content_ai;
/**
* Class constructor.
*
* @param Object $content_ai Content_AI class object.
*/
public function __construct( $content_ai ) {
$this->content_ai = $content_ai;
$this->action( 'rank_math/admin/editor_scripts', 'editor_scripts', 20 );
$this->filter( 'rank_math/elementor/dark_styles', 'add_dark_style' );
$this->action( 'admin_enqueue_scripts', 'media_scripts', 20 );
}
/**
* 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;
}
/**
* Enqueue Content AI files in the enabled post types.
*
* @param WP_Screen $screen Post screen object.
*
* @return void
*/
public function editor_scripts( $screen ) {
if ( ! $this->content_ai->can_add_tab() || ! Helper::get_current_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_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_set_script_translations( 'rank-math-content-ai', 'rank-math' );
$this->content_ai->localized_data( $this->get_post_localized_data( $screen ) );
}
/**
* Enqueue our inject-generate-alt-text script on the Edit Media page (post.php with post_type=attachment).
*/
public function media_scripts() {
$screen = \function_exists( 'get_current_screen' ) ? get_current_screen() : false;
if ( ! $screen || 'attachment' !== $screen->post_type ) {
return;
}
wp_enqueue_script(
'rank-math-content-ai-media',
rank_math()->plugin_url() . 'includes/modules/content-ai/assets/js/content-ai-media.js',
[ 'jquery', 'wp-api-fetch', 'lodash', 'wp-element', 'wp-components' ],
rank_math()->version,
true
);
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', 'wp-components' ],
rank_math()->version
);
$this->content_ai->localized_data();
}
/**
* Add meta data to use in gutenberg.
*
* @param Screen $screen Sceen object.
*
* @return array
*/
private function get_post_localized_data( $screen ) {
$values = [];
$countries = [];
foreach ( Helper::choices_contentai_countries() as $value => $label ) {
$countries[] = [
'value' => $value,
'label' => $label,
];
}
$values = [
'country' => Helper::get_settings( 'general.content_ai_country', 'all' ),
'countries' => $countries,
'viewed' => true,
'keyword' => '',
'score' => [
'keywords' => 0,
'wordCount' => 0,
'linkCount' => 0,
'headingCount' => 0,
'mediaCount' => 0,
],
];
/**
* Filter the enabled tests for Content AI research.
*
* @since 1.0.243
* @return array
*/
$values['enabledTests'] = $this->do_filter(
'content_ai/research_tests',
[
'wordCount',
'linkCount',
'headingCount',
'mediaCount',
]
);
$content_ai_viewed = get_option( 'rank_math_content_ai_viewed', false );
if ( ! $content_ai_viewed ) {
$values['viewed'] = false;
update_option( 'rank_math_content_ai_viewed', true, false );
}
$researched_values = $screen->get_meta( $screen->get_object_type(), $screen->get_object_id(), 'rank_math_ca_keyword' );
if ( empty( $researched_values ) ) {
return $values;
}
$data = get_option( 'rank_math_ca_data' );
$keyword = empty( $researched_values['keyword'] ) ? '' : $researched_values['keyword'];
$country = empty( $researched_values['country'] ) ? '' : $researched_values['country'];
if (
! empty( $data[ $country ] ) &&
! empty( $data[ $country ][ mb_strtolower( $keyword ) ] )
) {
$values['researchedData'] = $data[ $country ][ mb_strtolower( $keyword ) ];
}
$values['keyword'] = $keyword;
$values['country'] = $country;
$content_ai_data = $screen->get_meta( $screen->get_object_type(), $screen->get_object_id(), 'rank_math_contentai_score' );
$values['score'] = (object) $content_ai_data;
return $values;
}
}

View File

@@ -0,0 +1,349 @@
<?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;
use RankMath\Term;
defined( 'ABSPATH' ) || exit;
/**
* Bulk_Actions class.
*/
class Bulk_Actions {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'init', 'init' );
$this->action( 'admin_init', 'init_admin', 15 );
$this->action( 'rank_math/content_ai/generate_alt', 'generate_image_alt' );
$this->filter( 'rank_math/database/tools', 'add_tools' );
$this->filter( 'rank_math/tools/content_ai_cancel_bulk_edit_process', 'cancel_bulk_edit_process' );
}
/**
* Init function.
*/
public function init() {
Bulk_Edit_SEO_Meta::get();
Bulk_Image_Alt::get();
}
/**
* Init.
*/
public function init_admin() {
// Add Bulk actions for Posts.
$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 );
}
// Add Bulk Generate on Attachment page.
$this->filter( 'bulk_actions-upload', 'bulk_actions_attachment' );
$this->filter( 'handle_bulk_actions-upload', 'handle_bulk_actions', 10, 3 );
// Add Bulk Actions for Taxonomies.
$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' );
$this->filter( 'wp_bulk_image_alt_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' );
$actions['rank_math_content_ai_fetch_image_alt'] = esc_html__( 'Write Image Alt Text with AI', 'rank-math' );
return $actions;
}
/**
* Add bulk actions for Attachment.
*
* @param array $actions Actions.
* @return array New actions.
*/
public function bulk_actions_attachment( $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_image_alt'] = esc_html__( 'Write Image Alt Text 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', 'rank_math_content_ai_fetch_image_alt' ], 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;
}
if ( 'rank_math_content_ai_fetch_image_alt' === $doaction ) {
$this->generate_image_alt( $object_ids );
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, $action );
}
Bulk_Edit_SEO_Meta::get()->start( $data );
return $redirect;
}
/**
* Generate Image Alt for the attachmed Ids.
*
* @param array $object_ids Attachment Ids.
*/
public function generate_image_alt( $object_ids ) {
$data = [
'action' => 'image_alt',
'posts' => [],
];
foreach ( $object_ids as $object_id ) {
if ( get_post_type( $object_id ) === 'attachment' ) {
$data['posts'][ $object_id ] = [ wp_get_attachment_url( $object_id ) ];
continue;
}
// Get all <img> tags from the post content.
$images = [];
preg_match_all( '/<img\\s[^>]+>/i', get_post_field( 'post_content', $object_id ), $images );
// Keep only the image tags that have src attribute but no alt attribute.
$images = array_filter(
$images[0],
function ( $image ) {
return preg_match( '/src=[\'"]?([^\'" >]+)[\'" >]/i', $image, $matches ) && ( ! preg_match( '/alt="([^"]*)"/i', $image, $matches ) || preg_match( '/alt=""/i', $image, $matches ) );
}
);
if ( empty( $images ) ) {
continue;
}
$object = get_post( $object_id );
$data['posts'][ $object_id ] = array_filter( array_values( $images ) );
}
Bulk_Image_Alt::get()->start( $data );
}
/**
* 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;
}
/**
* 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' );
}
/**
* Get Post data.
*
* @param integer $object_id Post ID.
* @param string $action The action being performed (title, description, or both).
*
* @return array Post data.
*/
private function get_post_data( $object_id, $action = 'both' ) {
$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_content_for_ai( $object, $action ), $object ),
];
}
/**
* Get Term data.
*
* @param integer $object_id Term ID.
* @param string $action The action being performed (title, description, or both).
*
* @return array Term data.
*/
private function get_term_data( $object_id, $action = 'both' ) {
$object = get_term( $object_id );
return [
'post_id' => $object_id,
'post_type' => $object->taxonomy,
'title' => $object->name,
'focus_keyword' => Term::get_meta( 'focus_keyword', $object, $object->taxonomy ),
'summary' => Helper::replace_vars( $this->get_content_for_ai( $object, $action ), $object ),
];
}
/**
* Get content for AI processing based on object type and action.
*
* @param object $current_object Object instance (WP_Post or WP_Term).
* @param string $action The action being performed (title, description, or both).
*
* @return string Content to use for AI generation.
*/
private function get_content_for_ai( $current_object, $action = 'both' ) {
// For description generation, don't use current SEO descriptions.
if ( $action === 'description' || $action === 'both' ) {
return $this->get_content_without_seo_meta( $current_object );
}
// For title generation, prioritize existing SEO description.
return $current_object instanceof \WP_Post
? Post::get_meta( 'description', $current_object->ID, $this->get_content_without_seo_meta( $current_object ) )
: Term::get_meta( 'description', $current_object, $current_object->taxonomy, $this->get_content_without_seo_meta( $current_object ) );
}
/**
* Get content from object without using SEO meta description.
*
* @param object $current_object Object instance (WP_Post or WP_Term).
*
* @return string Content from the object.
*/
private function get_content_without_seo_meta( $current_object ) {
$is_post = $current_object instanceof \WP_Post;
// For terms: term description > template.
if ( ! $is_post ) {
return ! empty( $current_object->description ) ? $current_object->description : Str::truncate( Paper::get_from_options( "tax_{$current_object->taxonomy}_description", $current_object ), 160 );
}
// For posts: excerpt > content > template.
if ( ! empty( $current_object->post_excerpt ) ) {
return $current_object->post_excerpt;
}
$content = wp_strip_all_tags( $current_object->post_content );
if ( ! empty( $content ) ) {
return Str::truncate( $content, 300 );
}
return Str::truncate( Paper::get_from_options( "pt_{$current_object->post_type}_description", $current_object ), 160 );
}
}

View File

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

View File

@@ -0,0 +1,378 @@
<?php
/**
* Bulk Image Alt generation using the Content AI API.
*
* @since 1.0.218
* @package RankMath
* @subpackage RankMath\ContentAI
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use RankMath\Helper;
use RankMath\Admin\Admin_Helper;
defined( 'ABSPATH' ) || exit;
/**
* Bulk_Image_Alt class.
*/
class Bulk_Image_Alt extends \WP_Background_Process {
/**
* Action.
*
* @var string
*/
protected $action = 'bulk_image_alt';
/**
* Main instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Bulk_Image_Alt
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Bulk_Image_Alt ) ) {
$instance = new Bulk_Image_Alt();
}
return $instance;
}
/**
* Start creating batches.
*
* @param array $data Posts data.
*/
public function start( $data ) {
Helper::add_notification(
esc_html__( 'Bulk image alt generation 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',
]
);
update_option( 'rank_math_content_ai_posts', array_keys( $data['posts'] ) );
foreach ( $data['posts'] as $post_id => $images ) {
$chunks = array_chunk( $images, 5, true );
foreach ( $chunks as $chunk ) {
$this->push_to_queue(
[
'post_id' => $post_id,
'images' => array_values( $chunk ),
]
);
}
}
$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 posts.
sprintf( _n( 'Image alt attributes successfully updated in %d post.', 'Image alt attributes successfully updated in %d posts.', 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 {
if ( empty( $data['images'] ) ) {
$this->update_content_ai_posts_count();
return false;
}
$is_attachment = get_post_type( $data['post_id'] ) === 'attachment';
$api_output = json_decode( wp_remote_retrieve_body( $this->get_image_alts( $data, $is_attachment ) ), true );
// Early bail if API returns and error.
if ( ! empty( $api_output['error'] ) ) {
$notice = ! empty( $api_output['message'] ) ? $api_output['message'] : esc_html__( 'Bulk image alt generation failed.', 'rank-math' );
Helper::add_notification(
$notice,
[
'type' => 'error',
'id' => 'rank_math_content_ai_posts',
'classes' => 'rank-math-notice',
]
);
$this->cancel();
return;
}
if ( empty( $api_output['altTexts'] ) ) {
$this->update_content_ai_posts_count();
return false;
}
$this->update_image_alt( $api_output['altTexts'], $data, $is_attachment );
$this->update_content_ai_posts_count();
$credits = ! empty( $api_output['credits'] ) ? $api_output['credits'] : [];
if ( isset( $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__( 'Image alt attributes 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_image_alt_cron' );
}
}
return false;
} catch ( \Exception $error ) {
return true;
}
}
/**
* Get Posts to bulk update the data.
*
* @param array $data Data to process.
* @param boolean $is_attachment Whether the current post is attachment.
*
* @return array
*/
private function get_image_alts( $data, $is_attachment ) {
$connect_data = Admin_Helper::get_registration_data();
// Convert images to the new format with base64 data.
$images = [];
$image_urls = $is_attachment ? $data['images'] : $this->extract_urls_from_tags( $data['images'] );
foreach ( $image_urls as $image_url ) {
$image_id = $this->get_image_id_from_url( $image_url );
$base64_data = $this->convert_image_to_base64( $image_url );
if ( ! empty( $base64_data ) ) {
$images[] = [
'id' => $image_id,
'image' => $base64_data,
];
}
}
if ( empty( $images ) ) {
return;
}
$request_data = [
'images' => $images,
'username' => $connect_data['username'],
'api_key' => $connect_data['api_key'],
'site_url' => $connect_data['site_url'],
'plugin_version' => rank_math()->version,
'language' => Helper::get_settings( 'general.content_ai_language', Helper::content_ai_default_language() ),
];
return wp_remote_post(
CONTENT_AI_URL . '/ai/generate_image_alt_v2',
[
'headers' => [
'content-type' => 'application/json',
],
'timeout' => 60000,
'body' => wp_json_encode( $request_data ),
]
);
}
/**
* Extract URLs from image tags.
*
* @param array $image_tags Array of image HTML tags.
* @return array Array of image URLs.
*/
private function extract_urls_from_tags( $image_tags ) {
$urls = [];
foreach ( $image_tags as $image_tag ) {
$url = $this->get_image_source( $image_tag );
if ( ! empty( $url ) ) {
$urls[] = $url;
}
}
return $urls;
}
/**
* Convert image URL to base64 encoded data.
*
* @param string $image_url Image URL to convert.
* @return string Base64 encoded image data.
*/
private function convert_image_to_base64( $image_url ) {
// Validate URL.
if ( ! filter_var( $image_url, FILTER_VALIDATE_URL ) ) {
return '';
}
// Get image content.
$response = wp_remote_get( $image_url, [ 'timeout' => 30 ] );
if ( is_wp_error( $response ) ) {
return '';
}
$response_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $response_code ) {
return '';
}
$image_content = wp_remote_retrieve_body( $response );
if ( empty( $image_content ) ) {
return '';
}
// Get image info to determine MIME type.
$image_info = wp_check_filetype( $image_url );
$mime_type = ! empty( $image_info['type'] ) ? $image_info['type'] : 'image/jpeg';
// Convert to base64 with proper data URL format.
$base64 = base64_encode( $image_content ); // phpcs:ignore -- Verified as safe usage.
return 'data:' . $mime_type . ';base64,' . $base64;
}
/**
* Extract filename from image URL.
*
* @param string $image_url Image URL.
* @return string Filename.
*/
private function get_image_id_from_url( $image_url ) {
$url_parts = explode( '/', $image_url );
$filename = end( $url_parts );
if ( empty( $filename ) ) {
$filename = 'image.jpg';
}
// Remove query parameters if present.
$filename = strtok( $filename, '?' );
return $filename;
}
/**
* Keep count of the Content AI posts that were processed.
*
* @return void
*/
private function update_content_ai_posts_count() {
$content_ai_posts_count = get_option( 'rank_math_content_ai_posts_processed', 0 ) + 1;
update_option( 'rank_math_content_ai_posts_processed', $content_ai_posts_count, false );
}
/**
* Update Image alt value.
*
* @param array $alt_texts Alt texts returned by the API.
* @param array $data Data to process.
* @param boolean $is_attachment Whether the current post is attachment.
*
* @return void
*/
private function update_image_alt( $alt_texts, $data, $is_attachment ) {
if ( $is_attachment ) {
update_post_meta( $data['post_id'], '_wp_attachment_image_alt', sanitize_text_field( current( $alt_texts ) ) );
return;
}
$post = get_post( $data['post_id'] );
foreach ( $data['images'] as $image ) {
$image_src = $this->get_image_source( $image );
$image_id = $this->get_image_id_from_url( $image_src );
$image_alt = ! empty( $alt_texts[ $image_id ] ) ? $alt_texts[ $image_id ] : '';
if ( ! $image_alt ) {
continue;
}
// Remove any existing empty alt attributes.
$img_tag = preg_replace( '/ alt=(""|\'\')/i', '', $image );
// Add the new alt attribute.
$img_tag = str_replace( '<img ', '<img alt="' . esc_attr( $image_alt ) . '" ', $img_tag );
// Replace the old img tag with the new one in the post content.
$post->post_content = str_replace( $image, $img_tag, $post->post_content );
}
wp_update_post( $post );
}
/**
* Get Image source from image tag.
*
* @param string $image Image tag.
*
* @return string
*/
private function get_image_source( $image ) {
// The $data['images'] contains an array of img tags, so we need to extract the src attribute from each one.
preg_match( '/src=[\'"]?([^\'" >]+)[\'" >]/i', $image, $matches );
return $matches[1];
}
}

View File

@@ -0,0 +1,292 @@
<?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\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;
/**
* Content_AI object.
*
* @var object
*/
public $content_ai;
/**
* Class constructor.
*
* @param Object $content_ai Content_AI class object.
*/
public function __construct( $content_ai ) {
$this->content_ai = $content_ai;
$this->action( 'rank_math/admin_bar/items', 'admin_bar_items', 11 );
$this->action( 'init', 'init' );
$this->filter( 'wp_insert_post_data', 'remove_unused_generated_content' );
if ( Param::get( 'page' ) !== 'rank-math-content-ai-page' ) {
return;
}
$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();
Event_Scheduler::get();
}
/**
* 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' ) ) {
$this->content_ai->localized_data( [ 'isContentAIPage' => true ] );
}
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' => __DIR__ . '/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-components' => '',
'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',
],
],
]
);
}
/**
* 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;
}
/**
* 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;
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* The Content AI module.
*
* @since 1.0.71
* @package RankMath
* @subpackage RankMath
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use RankMath\Traits\Hooker;
use RankMath\Helper;
use RankMath\Admin\Admin_Helper;
use RankMath\Helpers\Url;
defined( 'ABSPATH' ) || exit;
/**
* Content_AI class.
*/
class Content_AI {
use Hooker;
/**
* Class constructor.
*/
public function __construct() {
$this->action( 'rest_api_init', 'init_rest_api' );
new Content_AI_Page( $this );
new Bulk_Actions();
if ( ! Helper::has_cap( 'content_ai' ) ) {
return;
}
new Admin( $this );
new Assets( $this );
}
/**
* Load the REST API endpoints.
*/
public function init_rest_api() {
$rest = new Rest();
$rest->register_routes();
}
/**
* 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.
*
* @param array $data Localized data for posts.
*/
public function localized_data( $data = [] ) {
$refresh_date = Helper::get_content_ai_refresh_date();
Helper::add_json(
'contentAI',
array_merge(
$data,
[
'audience' => (array) Helper::get_settings( 'general.content_ai_audience', 'General Audience' ),
'tone' => (array) Helper::get_settings( 'general.content_ai_tone', 'Formal' ),
'language' => Helper::get_settings( 'general.content_ai_language', Helper::content_ai_default_language() ),
'history' => Helper::get_outputs(),
'chats' => Helper::get_chats(),
'recentPrompts' => Helper::get_recent_prompts(),
'prompts' => Helper::get_prompts(),
'isUserRegistered' => Helper::is_site_connected(),
'connectData' => Admin_Helper::get_registration_data(),
'connectSiteUrl' => Admin_Helper::get_activate_url( Url::get_current_url() ),
'credits' => Helper::get_content_ai_credits(),
'plan' => Helper::get_content_ai_plan(),
'errors' => Helper::get_content_ai_errors(),
'registerWriteShortcut' => version_compare( get_bloginfo( 'version' ), '6.2', '>=' ),
'isMigrating' => get_site_transient( 'rank_math_content_ai_migrating_user' ),
'url' => CONTENT_AI_URL . '/ai/',
'resetDate' => $refresh_date ? wp_date( 'Y-m-d g:ia', $refresh_date ) : '',
]
)
);
}
}

View File

@@ -0,0 +1,136 @@
<?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;
}
$registered = Admin_Helper::get_registration_data();
if ( empty( $registered ) || empty( $registered['username'] ) ) {
return;
}
$prompt_data = [];
$data = wp_remote_post(
CONTENT_AI_URL . '/ai/default_prompts',
[
'headers' => [
'Content-type' => 'application/json',
],
'body' => wp_json_encode(
[
'username' => $registered['username'],
'api_key' => $registered['api_key'],
'site_url' => $registered['site_url'],
]
),
]
);
$response_code = wp_remote_retrieve_response_code( $data );
if ( is_wp_error( $data ) || ! in_array( $response_code, [ 200, 201 ], true ) ) {
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' );
}
}

View File

@@ -0,0 +1,726 @@
<?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' ],
'args' => $this->get_research_keyword_args(),
]
);
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' ],
'args' => [
'content' => [
'description' => esc_html__( 'The content of the new post.', 'rank-math' ),
'type' => 'string',
'required' => true,
],
'title' => [
'description' => esc_html__( 'The title of the new post.', 'rank-math' ),
'type' => 'string',
'required' => false,
],
],
]
);
register_rest_route(
$this->namespace,
'/saveOutput',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'save_output' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'outputs' => [
'description' => esc_html__( 'An array of AI-generated and existing outputs to be saved.', 'rank-math' ),
'type' => 'array',
'required' => true,
],
'endpoint' => [
'description' => esc_html__( 'The API endpoint for which the output was generated.', 'rank-math' ),
'type' => 'string',
'required' => true,
],
'isChat' => [
'description' => esc_html__( 'Indicates if the request was for the Chat endpoint.', 'rank-math' ),
'type' => 'boolean',
'required' => false,
],
'attributes' => [
'description' => esc_html__( 'The parameters used to generate the AI output.', 'rank-math' ),
'type' => 'object',
'required' => false,
],
'credits' => [
'description' => esc_html__( 'Credit usage details returned by the API.', 'rank-math' ),
'type' => 'object',
'required' => false,
],
],
]
);
register_rest_route(
$this->namespace,
'/deleteOutput',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'delete_output' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'isChat' => [
'description' => esc_html__( 'Indicates if the request to delete the output was for the Chat endpoint.', 'rank-math' ),
'type' => 'boolean',
'required' => false,
],
'index' => [
'description' => esc_html__( 'The output index to delete, applicable only to the Chat endpoint.', 'rank-math' ),
'type' => 'integer',
'required' => false,
],
],
]
);
register_rest_route(
$this->namespace,
'/updateRecentPrompt',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'update_recent_prompt' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'prompt' => [
'description' => esc_html__( 'The selected prompt to be updated in the recent prompts.', 'rank-math' ),
'type' => 'string',
'required' => true,
],
],
]
);
register_rest_route(
$this->namespace,
'/updatePrompt',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'update_prompt' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'prompt' => [
'description' => esc_html__( 'The prompt data to be saved in the database.', 'rank-math' ),
'required' => true,
'validate_callback' => [ '\\RankMath\\Rest\\Rest_Helper', 'is_param_empty' ],
],
],
]
);
register_rest_route(
$this->namespace,
'/savePrompts',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'save_prompts' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'prompts' => [
'description' => esc_html__( 'A list of prompts received from the API to be saved in the database.', 'rank-math' ),
'type' => 'array',
'required' => true,
],
],
]
);
register_rest_route(
$this->namespace,
'/pingContentAI',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'ping_content_ai' ],
'permission_callback' => [ $this, 'has_ping_permission' ],
'args' => [
'plan' => [
'description' => esc_html__( 'Content AI plan to update in the Database.', 'rank-math' ),
'type' => 'string',
'required' => true,
],
'refreshDate' => [
'description' => esc_html__( 'Content AI reset date to update in the Database', 'rank-math' ),
'type' => 'string',
'required' => true,
],
],
]
);
register_rest_route(
$this->namespace,
'/generateAlt',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'generate_alt' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'attachmentIds' => [
'description' => esc_html__( 'List of attachment IDs for which to generate alt text.', 'rank-math' ),
'type' => 'array',
'required' => true,
],
],
]
);
register_rest_route(
$this->namespace,
'/updateCredits',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'update_credits' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'credits' => [
'description' => esc_html__( 'Credit usage details returned by the API.', 'rank-math' ),
'type' => 'integer',
'required' => true,
],
],
]
);
}
/**
* 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.
*
* @return int Credits.
*/
public function get_credits() {
$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( 'forceUpdate' );
$keyword_data = get_option( 'rank_math_ca_data' );
$post_type = 0 === $object_id ? 'page' : get_post_type( $object_id );
if ( ! in_array( $post_type, (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, $post_type, $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,
];
}
/**
* Get the arguments for the researchKeyword route.
*
* @return array
*/
public function get_research_keyword_args() {
return [
'keyword' => [
'description' => esc_html__( 'The keyword to be researched.', 'rank-math' ),
'type' => 'string',
'required' => true,
],
'country' => [
'description' => esc_html__( 'The country for which the keyword should be researched.', 'rank-math' ),
'type' => 'string',
'required' => true,
],
'objectID' => [
'description' => esc_html__( 'The ID of the post initiating the keyword research request.', 'rank-math' ),
'type' => 'integer',
'required' => true,
],
'force_update' => [
'description' => esc_html__( 'If true, forces a fresh research request.', 'rank-math' ),
'type' => 'boolean',
'required' => false,
],
];
}
/**
* 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 = $request->get_param( 'title' );
$title = $title ? $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( $request->get_param( 'credits' ) ) ? json_decode( $request->get_param( '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;
}
/**
* Endpoint to generate Image Alt.
*
* @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 generate_alt( WP_REST_Request $request ) {
$ids = $request->get_param( 'attachmentIds' );
if ( empty( $ids ) ) {
return false;
}
do_action( 'rank_math/content_ai/generate_alt', $ids );
return true;
}
/**
* Endpoint to Update the credits 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 update_credits( WP_REST_Request $request ) {
$credits = $request->get_param( 'credits' );
Helper::update_credits( $credits );
return true;
}
/**
* Get data from the API.
*
* @param string $keyword Researched keyword.
* @param string $post_type Researched post type.
* @param string $country Researched country.
* @param bool $force_update Whether to force update the researched data.
*
* @return array
*/
private function get_researched_data( $keyword, $post_type, $country, $force_update = false ) {
$args = [
'username' => rawurlencode( $this->registered['username'] ),
'api_key' => rawurlencode( $this->registered['api_key'] ),
'keyword' => rawurlencode( $keyword ),
'post_type' => rawurlencode( $post_type ),
'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' => $this->research_keyword_error( $data, $response_code ),
];
}
$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;
}
/**
* Update the error message based on the pre-defined Content AI errors defined in the plugin.
*
* @param array $response API response.
* @param int $response_code API response code.
*
* @return string Error message.
*/
private function research_keyword_error( $response, $response_code ) {
if ( $response_code === 410 ) {
return 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>'
)
);
}
$error_texts = Helper::get_content_ai_errors();
$data = wp_remote_retrieve_body( $response );
$data = json_decode( $data, true );
return isset( $data['message'] ) && isset( $error_texts[ $data['message'] ] ) ? $error_texts[ $data['message'] ] : $response['response']['message'];
}
/**
* 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 );
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* Content AI Page main view.
*
* @package RankMath
* @subpackage RankMath\Role_Manager
*/
defined( 'ABSPATH' ) || exit;
?>
<div id="rank-math-content-ai-page">
<div class="rank-math-box container">
</div>
</div>

View File

@@ -0,0 +1,226 @@
<?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( Helper::get_settings_url( 'general', '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' ),
'Arabic' => esc_html__( 'Arabic', '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' ),
'Turkish' => esc_html__( 'Turkish', '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>',
]
);
}

View File

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

View File

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

View File

@@ -0,0 +1,423 @@
<?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\Helpers\Arr;
use RankMath\Helpers\Schedule;
use RankMath\Installer;
use RankMath\Traits\Hooker;
use RankMath\Helpers\DB as DB_Helper;
use RankMath\Helpers\Sitepress;
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();
Update_Score::get();
$this->hooks();
}
/**
* Register version control hooks.
*/
public function hooks() {
if ( Helper::is_rest() && Str::contains( '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 ] );
}
}
}
/**
* Get localized JSON data to be used on the Database Tools view of the Status & Tools page.
*/
public static function get_json_data() {
return [
'tools' => self::get_tools(),
];
}
/**
* Function to clear all the transients from the database.
*/
public function clear_transients() {
global $wpdb;
$transients = DB_Helper::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 = DB_Helper::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' ),
];
}
DB_Helper::query( "TRUNCATE TABLE {$wpdb->prefix}rank_math_internal_links" );
DB_Helper::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 = DB_Helper::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' ),
];
}
DB_Helper::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 = DB_Helper::get_var( "SELECT EXISTS ( SELECT 1 FROM {$wpdb->prefix}rank_math_redirections )" );
if ( empty( $exists ) ) {
return [
'status' => 'error',
'message' => __( 'No Redirections found.', 'rank-math' ),
];
}
DB_Helper::query( "TRUNCATE TABLE {$wpdb->prefix}rank_math_redirections;" );
DB_Helper::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' ) ) {
Schedule::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 = DB_Helper::get_col( "SHOW TABLES LIKE '{$wpdb->prefix}actionscheduler%'" );
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' );
}
/**
* Get tools.
*
* @return array
*/
private static 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 ( ! 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' ),
];
}
if ( Helper::is_module_active( 'analytics' ) && Helper::has_cap( 'analytics' ) ) {
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' ),
],
],
3
);
$description = __( 'Missing some posts/pages in the Analytics data? Clear the index and build a new one for more accurate stats.', 'rank-math' );
$sitepress = Sitepress::get()->is_active() ? Sitepress::get()->get_var() : false;
if ( Sitepress::get()->is_per_domain() && ! empty( $sitepress->get_setting( 'auto_adjust_ids', null ) ) ) {
$description .= '<br /><br /><i>' . sprintf(
/* translators: 1: settings URL, 2: settings text */
__( 'To properly rebuild Analytics posts in secondary languages, please disable the %1$s when using a different domain per language.', 'rank-math' ),
'<a href="' . esc_url( admin_url( 'admin.php?page=sitepress-multilingual-cms/menu/languages.php#lang-sec-8' ) ) . '">' . __( 'Make themes work multilingual option in WPML settings', 'rank-math' ) . '</a>'
) . '</i>';
}
Arr::insert(
$tools,
[
'analytics_reindex_posts' => [
'title' => __( 'Rebuild Index for Analytics', 'rank-math' ),
'description' => $description,
'button_text' => __( 'Rebuild Index', 'rank-math' ),
],
],
3
);
}
/**
* Filters the list of tools available on the Database Tools page.
*
* @param array $tools The tools.
*/
$tools = apply_filters( 'rank_math/database/tools', $tools );
return $tools;
}
}

View File

@@ -0,0 +1,252 @@
<?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\Helpers\DB as DB_Helper;
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->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' ) {
$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' => '',
'wp-wordcount' => '',
'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; //phpcs:ignore -- Overriding $post is required to load the localized data for the post.
}
$this->screen->localize();
$post = $temp_post; //phpcs:ignore -- Overriding $post is required to load the localized data for the 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' => wpautop( $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 );
}
/**
* 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 = DB_Helper::get_var( $wpdb->prepare( $query, $post_types ) );
return (int) $update_score_post_ids;
}
/**
* 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 ) );
}
}

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