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 SEO Pro
* URL: https://rankmath.com/wordpress/plugin/seo-suite/
* Name: 404-monitor.css
*/.rank-math-export-404-panel h3{margin-top:0}.rank-math-export-404-panel form .form-field{display:inline-block;margin-bottom:0}.rank-math-export-404-panel form .form-field input[type="submit"]{vertical-align:baseline}.ui-datepicker{padding:0;margin:0;border-radius:0;background-color:#fff;border:1px solid #dfdfdf;border-top:none;box-shadow:0 3px 6px rgba(0,0,0,0.075);min-width:17em;width:auto}.ui-datepicker *{padding:0;font-family:"Open Sans", sans-serif;border-radius:0}.ui-datepicker table{font-size:13px;margin:0;border:none;border-collapse:collapse}.ui-datepicker .ui-widget-header,.ui-datepicker .ui-datepicker-header{background-image:none;border:none;color:#fff;font-weight:normal}.ui-datepicker .ui-datepicker-header .ui-state-hover{background:transparent;border-color:transparent;cursor:pointer}.ui-datepicker .ui-datepicker-title{margin:0;padding:10px 0;color:#fff;font-size:14px;line-height:14px;text-align:center}.ui-datepicker .ui-datepicker-title select{margin-top:-8px;margin-bottom:-8px}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:relative;top:0;height:34px;width:34px}.ui-datepicker .ui-state-hover.ui-datepicker-prev,.ui-datepicker .ui-state-hover.ui-datepicker-next{border:none}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-prev-hover{left:0}.ui-datepicker .ui-datepicker-next,.ui-datepicker .ui-datepicker-next-hover{right:0}.ui-datepicker .ui-datepicker-next span,.ui-datepicker .ui-datepicker-prev span{display:none}.ui-datepicker .ui-datepicker-prev{float:left}.ui-datepicker .ui-datepicker-next{float:right}.ui-datepicker .ui-datepicker-prev:before,.ui-datepicker .ui-datepicker-next:before{font:normal 20px/34px 'dashicons';padding-left:7px;color:#fff;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;width:34px;height:34px}.ui-datepicker .ui-datepicker-prev:before{content:'\f341'}.ui-datepicker .ui-datepicker-next:before{content:'\f345'}.ui-datepicker .ui-datepicker-prev-hover:before,.ui-datepicker .ui-datepicker-next-hover:before{opacity:0.7}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:33%;background:transparent;border-color:transparent;box-shadow:none;color:#fff}.ui-datepicker select.ui-datepicker-month option,.ui-datepicker select.ui-datepicker-year option{color:#333}.ui-datepicker thead{color:#fff;font-weight:600}.ui-datepicker thead th{font-weight:normal}.ui-datepicker th{padding:10px}.ui-datepicker td{padding:0;border:1px solid #f4f4f4}.ui-datepicker td.ui-datepicker-other-month{border:transparent}.ui-datepicker td.ui-datepicker-week-end{background-color:#f4f4f4;border:1px solid #f4f4f4}.ui-datepicker td.ui-datepicker-week-end.ui-datepicker-today{box-shadow:inset 0px 0px 1px 0px rgba(0,0,0,0.1)}.ui-datepicker td.ui-datepicker-today{background-color:#f0f0c0}.ui-datepicker td.ui-datepicker-current-day{background:#bbdd88}.ui-datepicker td .ui-state-default{background:transparent;border:none;text-align:center;text-decoration:none;width:auto;display:block;padding:5px 10px;font-weight:normal;color:#444}.ui-datepicker td.ui-state-disabled .ui-state-default{opacity:0.5}.ui-datepicker .ui-widget-header,.ui-datepicker .ui-datepicker-header{background:#00a0d2}.ui-datepicker thead{background:#32373c}.ui-datepicker td .ui-state-hover,.ui-datepicker td .ui-state-active{background:#0073aa;color:#fff}.ui-datepicker .ui-timepicker-div{font-size:14px}.ui-datepicker .ui-timepicker-div dl{text-align:left;padding:0 .6em}.ui-datepicker .ui-timepicker-div dl dt{float:left;clear:left;padding:0 0 0 5px}.ui-datepicker .ui-timepicker-div dl dd{margin:0 10px 10px 40%}.ui-datepicker .ui-timepicker-div dl dd select{width:100%}.ui-datepicker .ui-timepicker-div+.ui-datepicker-buttonpane{padding:.6em;text-align:left}.ui-datepicker .ui-timepicker-div+.ui-datepicker-buttonpane .button-primary,.ui-datepicker .ui-timepicker-div+.ui-datepicker-buttonpane .button-secondary{padding:0 10px 1px;border-radius:3px;margin:0 .6em .4em .4em}

View File

@@ -0,0 +1 @@
(()=>{"use strict";var e={n:t=>{var r=t&&t.__esModule?()=>t.default:()=>t;return e.d(r,{a:r}),r},d:(t,r)=>{for(var a in r)e.o(r,a)&&!e.o(t,a)&&Object.defineProperty(t,a,{enumerable:!0,get:r[a]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=jQuery;var r=e.n(t);r()((function(){r()("#rank_math_export_404_date_from, #rank_math_export_404_date_to").datepicker({dateFormat:"yy-mm-dd"});var e=r()(".rank-math-export-404-panel");r()("a.rank-math-404-monitor-export").on("click",(function(t){t.preventDefault(),e.toggleClass("hidden")}))}))})();

View File

@@ -0,0 +1,258 @@
<?php
/**
* 404 Monitor module.
*
* @since 1.0
* @package RankMathPro
* @subpackage RankMathPro\Admin
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use MyThemeShop\Helpers\Param;
use MyThemeShop\Database\Database;
use RankMathPro\Admin\CSV;
defined( 'ABSPATH' ) || exit;
/**
* Monitor class.
*
* @codeCoverageIgnore
*/
class Monitor_Pro extends CSV {
use Hooker;
/**
* Total hits cache.
*
* @var array
*/
private $total_hits_cache = [];
/**
* Constructor.
*/
public function __construct() {
$this->filter( 'rank_math/404_monitor/page_title_actions', 'page_title_actions', 20, 1 );
$this->action( 'rank_math/404_monitor/before_list_table', 'export_panel', 20 );
$this->action( 'admin_enqueue_scripts', 'enqueue', 20 );
$this->action( 'init', 'maybe_export', 20 );
$this->filter( 'rank_math/404_monitor/list_table_columns', 'manage_columns', 20 );
$this->filter( 'rank_math/404_monitor/list_table_column', 'total_hits_column', 20, 3 );
$this->filter( 'rank_math/404_monitor/get_logs_args', 'get_logs_args', 20 );
}
/**
* Add page title action for export.
*
* @param array $actions Original actions.
* @return array
*/
public function page_title_actions( $actions ) {
$actions['export'] = [
'class' => 'page-title-action',
'href' => add_query_arg( 'export-404', '1' ),
'label' => __( 'Export', 'rank-math-pro' ),
];
return $actions;
}
/**
* Output export panel.
*
* @return void
*/
public function export_panel() {
$today = date( 'Y-m-d' );
?>
<div class="rank-math-box rank-math-export-404-panel <?php echo Param::get( 'export-404' ) ? '' : 'hidden'; ?>">
<h3><?php esc_html_e( 'Export 404 Logs', 'rank-math-pro' ); ?></h3>
<p class="description">
<?php esc_html_e( 'Export and download 404 logs from a selected period of time in the form of a CSV file. Leave the from/to fields empty to export all logs.', 'rank-math-pro' ); ?>
</p>
<div class="form-wrap">
<form action="" method="get" autocomplete="off">
<input type="hidden" name="action" value="rank_math_export_404">
<?php wp_nonce_field( 'export_404' ); ?>
<div class="form-field">
<label for="rank_math_export_404_date_from">
<?php esc_html_e( 'From date', 'rank-math-pro' ); ?>
</label>
<input type="text" name="date_from" value="" id="rank_math_export_404_date_from" class="rank-math-datepicker" placeholder="<?php echo esc_attr( $today ); ?>">
</div>
<div class="form-field">
<label for="rank_math_export_404_date_to">
<?php esc_html_e( 'To date', 'rank-math-pro' ); ?>
</label>
<input type="text" name="date_to" value="" id="rank_math_export_404_date_to" class="rank-math-datepicker" placeholder="<?php echo esc_attr( $today ); ?>">
</div>
<div class="rank_math_export_404_submit_wrap form-field">
<input type="submit" value="<?php esc_attr_e( 'Export', 'rank-math-pro' ); ?>" class="button button-primary">
</div>
</form>
</div>
</div>
<?php
}
/**
* Undocumented function
*
* @return void
*/
public function maybe_export() {
if ( Param::get( 'action' ) !== 'rank_math_export_404' ) {
return;
}
if ( ! current_user_can( 'export' ) || ! Helper::has_cap( '404_monitor' ) ) {
// Todo: add error notice instead of wp_die()?
wp_die( esc_html__( 'Sorry, your user does not seem to have the necessary capabilities to export.', 'rank-math-pro' ) );
}
if ( wp_verify_nonce( Param::get( '_nonce' ), 'export_404' ) ) {
// Todo: add error notice instead of wp_die()?
wp_die( esc_html__( 'Nonce error. Please try again.', 'rank-math-pro' ) );
}
$date_from = $this->sanitize_datetime( Param::get( 'date_from' ) );
$date_to = $this->sanitize_datetime( Param::get( 'date_to' ) );
$data = $this->export_items( $date_from, $date_to );
$this->export(
[
'filename' => '404-log',
'columns' => $data['columns'],
'items' => $data['items'],
]
);
die();
}
/**
* Do export.
*
* @param string $time_from Start date (SQL DateTime format).
* @param string $time_to End date (SQL DateTime format).
*
* @return array
*/
private function export_items( $time_from = null, $time_to = null ) {
global $wpdb;
$logs_table = $wpdb->prefix . 'rank_math_404_logs';
$query = "SELECT * FROM {$logs_table} WHERE 1=1";
$where = '';
if ( $time_from ) {
$where .= " AND accessed > '{$time_from} 00:00:01'";
}
if ( $time_to ) {
$where .= " AND accessed < '{$time_to} 23:59:59'";
}
$query .= $where;
$items = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( empty( $items ) ) {
return [
'columns' => [],
'items' => [],
];
}
$columns = array_keys( $items[0] );
return [
'columns' => $columns,
'items' => $items,
];
}
/**
* Sanitize date field inputs.
*
* @param string $date Date input.
* @return string
*/
public function sanitize_datetime( $date ) {
return preg_replace( '/[^0-9 :-]/', '', $date );
}
/**
* Enqueue styles and scripts.
*
* @param string $hook The current admin page.
*
* @return void
*/
public function enqueue( $hook ) {
if ( 'rank-math_page_rank-math-404-monitor' !== $hook ) {
return;
}
$url = RANK_MATH_PRO_URL . 'includes/modules/404-monitor/assets/';
wp_enqueue_script( 'rank-math-pro-404-monitor', $url . 'js/404-monitor.js', [ 'jquery-ui-core', 'jquery-ui-datepicker' ], RANK_MATH_PRO_VERSION, true );
wp_enqueue_style( 'rank-math-pro-404-monitor', $url . 'css/404-monitor.css', [], RANK_MATH_PRO_VERSION );
}
/**
* Add extra columns for the list table.
*
* @param array $columns Original columns.
* @return array
*/
public function manage_columns( $columns ) {
if ( 'simple' === Helper::get_settings( 'general.404_monitor_mode' ) ) {
return $columns;
}
$columns['total_hits'] = esc_html__( 'Hits', 'rank-math-pro' );
return $columns;
}
/**
* Add content in the extra columns.
*
* @param string $content Original content.
* @param array $item Table item.
* @param string $column Column name.
* @return string
*/
public function total_hits_column( $content, $item, $column ) {
if ( 'total_hits' !== $column ) {
return $content;
}
if ( ! isset( $this->total_hits_cache[ $item['uri'] ] ) ) {
$this->total_hits_cache[ $item['uri'] ] = Database::table( 'rank_math_404_logs' )->selectCount( '*', 'count' )->where( 'uri', $item['uri'] )->getVar();
}
return '<a href="' . add_query_arg( [ 'uri' => $item['uri'] ], '?page=rank-math-404-monitor' ) . '">' . $this->total_hits_cache[ $item['uri'] ] . '</a>';
}
/**
* Change get_logs() args when filtering for a URI.
*
* @param array $args Original args.
* @return array
*/
public function get_logs_args( $args ) {
$uri = Param::get( 'uri' );
if ( ! $uri ) {
return $args;
}
$args['uri'] = $uri;
return $args;
}
}

View File

@@ -0,0 +1,287 @@
<?php
/**
* ACF module.
*
* @since 2.0.9
* @package RankMathPro
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\ACF;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* ACF class.
*/
class ACF {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'rank_math/sitemap/urlimages', 'add_acf_images', 10, 2 );
$this->filter( 'rank_math/sitemap/content_before_parse_html_images', 'parse_html_images', 10, 2 );
$this->action( 'rank_math/admin/settings/general', 'acf_sitemap_settings' );
}
/**
* Add new settings.
*
* @param object $cmb CMB2 instance.
*/
public function acf_sitemap_settings( $cmb ) {
$field_ids = wp_list_pluck( $cmb->prop( 'fields' ), 'id' );
$field_position = array_search( 'include_featured_image', array_keys( $field_ids ), true ) + 1;
$cmb->add_field(
[
'id' => 'include_acf_images',
'type' => 'toggle',
'name' => esc_html__( 'Include Images from the ACF Fields.', 'rank-math-pro' ),
'desc' => esc_html__( 'Include images added in the ACF fields.', 'rank-math-pro' ),
'options' => [
'off' => esc_html__( 'Default', 'rank-math-pro' ),
'on' => esc_html__( 'Custom', 'rank-math-pro' ),
],
'default' => 'off',
'dep' => [ [ 'include_images', 'on' ] ],
],
++$field_position
);
}
/**
* Add images from the ACF fields content in the Sitemap.
*
* @param string $content Post content.
* @param int $post_id Post ID.
*/
public function parse_html_images( $content, $post_id ) {
if ( ! Helper::get_settings( 'sitemap.include_acf_images' ) ) {
return $content;
}
$fields = get_field_objects( $post_id );
if ( empty( $fields ) ) {
return $content;
}
foreach ( $fields as $field ) {
if ( empty( $field['value'] ) ) {
continue;
}
if ( in_array( $field['type'], [ 'wysiwyg', 'textarea' ], true ) ) {
$content .= $field['value'];
continue;
}
if ( 'flexible_content' === $field['type'] ) {
$this->get_flexible_content( $content, $field, $post_id );
continue;
}
if ( 'repeater' === $field['type'] || 'group' === $field['type'] ) {
$this->get_sub_fields_content( $content, $field['sub_fields'], $field );
continue;
}
}
return $content;
}
/**
* Filter images to be included for the post in XML sitemap.
*
* @param array $images Array of image items.
* @param int $post_id ID of the post.
*/
public function add_acf_images( $images, $post_id ) {
if ( ! Helper::get_settings( 'sitemap.include_acf_images' ) ) {
return $images;
}
$fields = get_field_objects( $post_id );
if ( empty( $fields ) ) {
return $images;
}
$values = wp_list_pluck( $fields, 'value' );
$this->get_all_images( $images, $values );
return $images;
}
/**
* Get content from flexible_content field.
*
* @param string $content Post content.
* @param array $field Current field data.
* @param int $post_id Post ID.
*/
private function get_flexible_content( &$content, $field, $post_id ) {
if ( empty( $field['layouts'] ) || empty( current( $field['layouts'] ) ) ) {
return;
}
$this->get_sub_fields_content( $content, current( $field['layouts'] )['sub_fields'], $field );
}
/**
* Get content from ACF sub-fields.
*
* @param string $content Post content.
* @param array $sub_fields Array of subfields.
* @param array $field Current field data.
*/
private function get_sub_fields_content( &$content, $sub_fields, $field ) {
foreach ( $sub_fields as $layout ) {
if ( ! in_array( $layout['type'], [ 'wysiwyg', 'textarea' ], true ) ) {
continue;
}
foreach ( $field['value'] as $key => $value ) {
if ( $key === $layout['name'] ) {
$content .= $value;
continue;
}
$content .= is_array( $value ) && ! empty( $value[ $layout['name'] ] ) ? $value[ $layout['name'] ] : '';
}
}
}
/**
* Add Images to XML Sitemap.
*
* @param array $images Array of image items.
* @param array $field_data Current Image array.
* @param string $field_type Is field type gallery.
*/
private function add_images_to_sitemap( &$images, $field_data, $field_type ) {
if ( empty( $field_data ) ) {
return;
}
if ( in_array( $field_type, [ 'group', 'repeater', 'flexible_content' ], true ) ) {
$this->add_images_from_repeater_field( $images, $field_data );
return;
}
if ( in_array( $field_type, [ 'gallery' ], true ) ) {
foreach ( $field_data as $image ) {
$this->add_images_to_sitemap( $images, $image, 'image' );
}
return;
}
if ( 'image' !== $field_type || empty( $field_data ) ) {
return;
}
if ( is_array( $field_data ) && ! empty( $field_data['url'] ) ) {
$images[] = [
'src' => $field_data['url'],
'title' => $field_data['title'],
'alt' => $field_data['alt'],
];
} elseif ( is_int( $field_data ) ) {
$image_url = wp_get_attachment_image_url( $field_data, 'full' );
if ( $image_url ) {
$images[] = [
'src' => $image_url,
'title' => '',
'alt' => '',
];
}
} elseif ( Helper::is_image_url( $field_data ) ) {
$images[] = [
'src' => $field_data,
'title' => '',
'alt' => '',
];
}
}
/**
* Add Images to XML Sitemap from Repeater field.
*
* @param array $images Array of image items.
* @param array $field_data Current Image array.
*/
private function add_images_from_repeater_field( &$images, $field_data ) {
if ( empty( $field_data ) ) {
return;
}
foreach ( $field_data as $data ) {
if ( is_array( $data ) ) {
foreach ( $data as $image ) {
if ( is_array( $image ) ) {
$this->add_images_to_sitemap( $images, $image[0], 'image' );
} else {
$this->add_images_to_sitemap( $images, $image, 'image' );
}
}
} else {
$this->add_images_to_sitemap( $images, $data, 'image' );
}
}
}
/**
* Get all images
*
* @param array $images All images.
* @param array $data All acf field values.
*
* @return array
*/
public function get_all_images( &$images, $data ) {
if ( is_array( $data ) ) {
foreach ( array_values( $data ) as $single_data ) {
if ( is_array( $single_data ) && isset( $single_data['type'] ) && 'image' === $single_data['type'] ) {
$images[] = [
'src' => $single_data['url'],
'title' => $single_data['title'],
'alt' => $single_data['alt'],
];
$single_data = '';
}
$this->get_all_images( $images, $single_data );
}
}
if ( empty( $data ) || is_array( $data ) ) {
return $images;
}
if ( is_int( $data ) ) {
$image_url = wp_get_attachment_image_url( $data, 'full' );
if ( $image_url ) {
$images[] = [
'src' => $image_url,
'title' => '',
'alt' => '',
];
}
}
if ( Helper::is_image_url( $data ) ) {
$images[] = [
'src' => $data,
'title' => '',
'alt' => '',
];
}
return $images;
}
}

View File

@@ -0,0 +1 @@
:root{--rankmath-wp-adminbar-height: 0}@keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(-360deg)}}@keyframes bounce{from{transform:translateY(0px)}to{transform:translateY(-5px)}}@keyframes loading{0%{background-size:20% 50% ,20% 50% ,20% 50%}20%{background-size:20% 20% ,20% 50% ,20% 50%}40%{background-size:20% 100%,20% 20% ,20% 50%}60%{background-size:20% 50% ,20% 100%,20% 20%}80%{background-size:20% 50% ,20% 50% ,20% 100%}100%{background-size:20% 50% ,20% 50% ,20% 50%}}.rank-math-pagespeed-box .rank-math-pagespeed-header{display:flex;flex-flow:row wrap}.rank-math-pagespeed-box .rank-math-pagespeed-header{align-items:center}.rank-math-pagespeed-box .rank-math-pagespeed-header h3{font-size:1rem}.rank-math-pagespeed-box .rank-math-pagespeed-header span{margin-left:auto}.rank-math-pagespeed-box .rank-math-pagespeed-header .button{line-height:1;width:30px;height:30px;padding:0;text-align:center;text-decoration:none;color:#b5bfc9;border:0;fill:#b5bfc9;-webkit-text-stroke:1px white}.rank-math-pagespeed-box .rank-math-pagespeed-header .button svg,.rank-math-pagespeed-box .rank-math-pagespeed-header .button .dashicon{margin-right:0;vertical-align:-2px}.rank-math-pagespeed-box .rank-math-pagespeed-header .button:hover,.rank-math-pagespeed-box .rank-math-pagespeed-header .button:focus,.rank-math-pagespeed-box .rank-math-pagespeed-header .button:active{outline:none;background:transparent;box-shadow:none;fill:#069de3}.rank-math-pagespeed-box .rank-math-pagespeed-header .button.loading{display:inline-block;animation:rm-spin 1s linear infinite}@keyframes rm-spin{0%{transform:rotate(360deg);color:#10AC84;fill:#10AC84}25%{color:#4e8cde;fill:#4e8cde}50%{color:#ed5e5e;fill:#ed5e5e}75%{color:#FF9F43;fill:#FF9F43}100%{transform:rotate(0deg);color:#F368E0;fill:#F368E0}}.rank-math-pagespeed-box .col{margin-top:.825rem;flex:0 0 50%}.rank-math-pagespeed-box .col .rm-icon{font-size:1.25rem;margin-right:8px;vertical-align:middle}.rank-math-pagespeed-box .col strong{font-size:1rem;font-weight:500;margin-right:10px;vertical-align:middle}.rank-math-pagespeed-box .col strong.interactive-good{color:#52a652}.rank-math-pagespeed-box .col strong.interactive-fair{color:#FF9800}.rank-math-pagespeed-box .col strong.interactive-bad{color:#f0776f}.rank-math-pagespeed-box .col small{font-size:14px;font-weight:500;line-height:24px;display:inline-block;width:42px;text-align:center;color:#52a652;border-radius:5px;background:#e9f4e9}.rank-math-pagespeed-box .col small.score-fair{color:#FF9800;background:#ffefd6}.rank-math-pagespeed-box .col small.score-bad{color:#f0776f;background:#fdedec}

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,96 @@
<?php
/**
* The Analytics AJAX
*
* @since 1.4.0
* @package RankMathPro
* @subpackage RankMathPro\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics;
use RankMathPro\Google\Adsense;
use MyThemeShop\Helpers\Param;
use RankMath\Analytics\Workflow\Base;
use RankMath\Analytics\Workflow\Workflow;
defined( 'ABSPATH' ) || exit;
/**
* Ajax class.
*/
class Ajax {
use \RankMath\Traits\Ajax;
/**
* The Constructor
*/
public function __construct() {
$this->ajax( 'save_adsense_account', 'save_adsense_account' );
$this->ajax( 'check_adsense_request', 'check_adsense_request' );
}
/**
* Check the Google AdSense request.
*/
public function check_adsense_request() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'analytics' );
$dates = Base::get_dates();
$success = Adsense::get_adsense(
[
'start_date' => $dates['start_date'],
'end_date' => $dates['end_date'],
]
);
if ( is_wp_error( $success ) ) {
$this->error( esc_html__( 'Data import will not work for this service as sufficient permissions are not given.', 'rank-math-pro' ) );
}
$this->success();
}
/**
* Save adsense profile.
*/
public function save_adsense_account() {
$this->verify_nonce( 'rank-math-ajax-nonce' );
$this->has_cap_ajax( 'analytics' );
$prev = get_option( 'rank_math_google_analytic_options', [] );
$value = get_option( 'rank_math_google_analytic_options', [] );
$value['adsense_id'] = Param::post( 'accountID' );
// Test AdSense connection request.
if ( ! empty( $value['adsense_id'] ) ) {
$dates = Base::get_dates();
$request = Adsense::get_adsense(
[
'account_id' => $value['adsense_id'],
'start_date' => $dates['start_date'],
'end_date' => $dates['end_date'],
]
);
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' ) );
}
}
update_option( 'rank_math_google_analytic_options', $value );
$days = Param::get( 'days', 90, FILTER_VALIDATE_INT );
Workflow::do_workflow(
'adsense',
$days,
$prev,
$value
);
$this->success();
}
}

View File

@@ -0,0 +1,675 @@
<?php
/**
* Analytics module.
*
* @since 1.0
* @package RankMathPro
* @subpackage RankMathPro
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Analytics\Stats;
use MyThemeShop\Helpers\Param;
use MyThemeShop\Helpers\DB as DB_Helper;
// Analytics.
use RankMathPro\Google\Adsense;
use RankMath\Google\Permissions;
use RankMath\Google\Authentication;
use RankMath\Admin\Admin_Helper;
use RankMathPro\Analytics\Workflow\Jobs;
use RankMathPro\Analytics\Workflow\Workflow;
use RankMathPro\Admin\Admin_Helper as ProAdminHelper;
use RankMathPro\Analytics\DB;
defined( 'ABSPATH' ) || exit;
/**
* Analytics class.
*/
class Analytics {
use Hooker;
/**
* Constructor.
*/
public function __construct() {
$this->action( 'rank_math/admin/enqueue_scripts', 'enqueue_analytics' );
$this->action( 'rank_math/analytics/options/console', 'add_country_dropdown3' );
$this->action( 'rank_math/analytics/options/analytics', 'add_country_dropdown2' );
$this->action( 'update_option_rank_math_analytics_last_updated', 'send_summary' );
$this->action( 'rank_math/admin/settings/analytics', 'add_new_settings' );
$this->filter( 'rank_math/analytics/schedule_gap', 'schedule_gap' );
$this->filter( 'rank_math/analytics/fetch_gap', 'fetch_gap' );
$this->filter( 'rank_math/analytics/max_days_allowed', 'data_retention_period' );
$this->filter( 'rank_math/analytics/options/cache_control/description', 'change_description' );
$this->filter( 'rank_math/analytics/check_all_services', 'check_all_services' );
$this->filter( 'rank_math/analytics/user_preference', 'change_user_preference' );
$this->action( 'template_redirect', 'local_js_endpoint' );
$this->filter( 'rank_math/analytics/gtag_config', 'gtag_config' );
$this->filter( 'rank_math/status/rank_math_info', 'google_permission_info' );
$this->filter( 'rank_math/analytics/gtag', 'gtag' );
$this->filter( 'rank_math/analytics/pre_filter_data', 'filter_winning_losing_posts', 10, 3 );
$this->filter( 'rank_math/analytics/pre_filter_data', 'filter_winning_keywords', 10, 3 );
$this->action( 'cmb2_save_options-page_fields_rank-math-options-general_options', 'sync_global_settings', 25, 2 );
$this->filter( 'rank_math/metabox/post/values', 'add_metadata', 10, 2 );
$this->filter( 'rank_math/analytics/date_exists_tables', 'date_exists_tables', 10 );
if ( Helper::has_cap( 'analytics' ) ) {
$this->action( 'rank_math/admin_bar/items', 'admin_bar_items', 11 );
$this->action( 'rank_math_seo_details', 'post_column_search_traffic' );
}
if ( Helper::can_add_frontend_stats() ) {
$this->action( 'wp_enqueue_scripts', 'enqueue' );
}
Posts::get();
Keywords::get();
Jobs::get();
Workflow::get();
new Pageviews();
new Summary();
new Ajax();
new Email_Reports();
new Url_Inspection();
}
/**
* Enqueue Frontend stats script.
*/
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-pro-stats', $uri . '/assets/css/admin-bar.css', [ 'rank-math-analytics-stats' ], rank_math_pro()->version );
wp_enqueue_script( 'rank-math-analytics-pro-stats', $uri . '/assets/js/admin-bar.js', [ 'rank-math-analytics-stats' ], rank_math_pro()->version, true );
Helper::add_json( 'dateFormat', get_option( 'date_format' ) );
}
/**
* Add localized data to use in the Post Editor.
*
* @param array $values Aray of localized data.
*
* @return array
*/
public function add_metadata( $values ) {
$values['isAnalyticsConnected'] = \RankMath\Google\Analytics::is_analytics_connected();
return $values;
}
/**
* Change user perference.
*
* @param array $preference Array of preference.
* @return array
*/
public function change_user_preference( $preference ) {
Helper::add_json( 'isAdsenseConnected', ! empty( Adsense::get_adsense_id() ) );
Helper::add_json( 'isLinkModuleActive', Helper::is_module_active( 'link-counter' ) );
Helper::add_json( 'isSchemaModuleActive', Helper::is_module_active( 'rich-snippet' ) );
Helper::add_json( 'isAnalyticsConnected', \RankMath\Google\Analytics::is_analytics_connected() );
Helper::add_json( 'dateFormat', get_option( 'date_format' ) );
$preference['topKeywords']['ctr'] = false;
$preference['topKeywords']['ctr'] = false;
$preference['performance']['clicks'] = false;
return $preference;
}
/**
* Data rentention days.
*
* @return int
*/
public function data_retention_period() {
return 'pro' === Admin_Helper::get_user_plan() ? 180 : 1000;
}
/**
* Data retrival job gap in seconds.
*
* @return int
*/
public function schedule_gap() {
return 10;
}
/**
* Data retrival fetch gap in days.
*
* @return int
*/
public function fetch_gap() {
return 3;
}
/**
* Fetch adsense account.
*
* @param array $result Result array.
* @return array
*/
public function check_all_services( $result ) {
$result['adsenseAccounts'] = Adsense::get_adsense_accounts();
return $result;
}
/**
* Add admin bar item.
*
* @param Admin_Bar_Menu $menu Menu class instance.
*/
public function admin_bar_items( $menu ) {
$post_types = Helper::get_accessible_post_types();
unset( $post_types['attachment'] );
if ( is_singular( $post_types ) && Helper::is_post_indexable( get_the_ID() ) ) {
$menu->add_sub_menu(
'post_analytics',
[
'title' => esc_html__( 'Post Analytics', 'rank-math-pro' ),
'href' => Helper::get_admin_url( 'analytics#/single/' . get_the_ID() ),
'meta' => [ 'title' => esc_html__( 'Analytics Report', 'rank-math-pro' ) ],
'priority' => 20,
]
);
}
}
/**
* Enqueue scripts for the metabox.
*/
public function enqueue_analytics() {
$screen = get_current_screen();
if ( 'rank-math_page_rank-math-analytics' !== $screen->id ) {
return;
}
$url = RANK_MATH_PRO_URL . 'includes/modules/analytics/assets/';
wp_enqueue_style(
'rank-math-pro-analytics',
$url . 'css/stats.css',
null,
rank_math_pro()->version
);
wp_enqueue_script(
'rank-math-pro-analytics',
$url . 'js/stats.js',
[
'wp-components',
'wp-element',
'wp-i18n',
'wp-date',
'wp-html-entities',
'wp-api-fetch',
'rank-math-analytics',
],
rank_math_pro()->version,
true
);
}
/**
* Add country dropdown.
*/
public function add_country_dropdown3() {
$profile = wp_parse_args(
get_option( 'rank_math_google_analytic_profile' ),
[
'profile' => '',
'country' => 'all',
]
);
?>
<div class="cmb-row-col">
<label for="site-console-country"><?php esc_html_e( 'Country', 'rank-math-pro' ); ?></label>
<select class="cmb2_select site-console-country notrack" name="site-console-country" id="site-console-country" disabled="disabled">
<?php foreach ( ProAdminHelper::choices_countries_3() as $code => $label ) : ?>
<option value="<?php echo esc_attr( $code ); ?>"<?php selected( $profile['country'], $code ); ?>>
<?php echo esc_html( $label ); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php
}
/**
* Add country dropdown.
*/
public function add_country_dropdown2() {
$analytics = $this->get_settings();
?>
<div class="cmb-row-col country-option">
<label for="site-analytics-country"><?php esc_html_e( 'Country', 'rank-math-pro' ); ?></label>
<select class="cmb2_select site-analytics-country notrack" name="site-analytics-country" id="site-analytics-country" disabled="disabled">
<?php foreach ( ProAdminHelper::choices_countries() as $code => $label ) : ?>
<option value="<?php echo esc_attr( $code ); ?>"<?php selected( $analytics['country'], $code ); ?>>
<?php echo esc_html( $label ); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php
}
/**
* Get Analytics settings.
*
* @return array
*/
public function get_settings() {
return wp_parse_args(
get_option( 'rank_math_google_analytic_options' ),
[
'adsense_id' => '',
'account_id' => '',
'property_id' => '',
'view_id' => '',
'country' => 'all',
'install_code' => false,
'anonymize_ip' => false,
'local_ga_js' => false,
'exclude_loggedin' => false,
]
);
}
/**
* Send analytics summary to RankMath.com.
*/
public function send_summary() {
if ( ! Helper::get_settings( 'general.sync_global_setting' ) ) {
return;
}
$registered = Admin_Helper::get_registration_data();
if ( $registered && isset( $registered['username'] ) && isset( $registered['api_key'] ) ) {
Stats::get()->set_date_range( '-30 days' );
$stats = Stats::get()->get_analytics_summary();
\RankMathPro\Admin\Api::get()->send_summary(
[
'username' => $registered['username'],
'api_key' => $registered['api_key'],
'site_url' => esc_url( home_url() ),
'impressions' => array_values( $stats['impressions'] ),
'clicks' => array_values( $stats['clicks'] ),
'keywords' => array_values( $stats['keywords'] ),
'pageviews' => isset( $stats['pageviews'] ) && is_array( $stats['pageviews'] ) ? array_values( $stats['pageviews'] ) : [],
'adsense' => isset( $stats['adsense'] ) && is_array( $stats['adsense'] ) ? array_values( $stats['adsense'] ) : [],
]
);
}
}
/**
* Change option description.
*/
public function change_description() {
return __( 'Enter the number of days to keep Analytics data in your database. The maximum allowed days are 180. Though, 2x data will be stored in the DB for calculating the difference properly.', 'rank-math-pro' );
}
/**
* Add new settings.
*
* @param object $cmb CMB2 instance.
*/
public function add_new_settings( $cmb ) {
if ( ! Authentication::is_authorized() ) {
return;
}
$type = ! ProAdminHelper::is_business_plan() ? 'hidden' : 'toggle';
$field_ids = wp_list_pluck( $cmb->prop( 'fields' ), 'id' );
$fields_position = array_search( 'console_caching_control', array_keys( $field_ids ), true ) + 1;
$cmb->add_field(
[
'id' => 'sync_global_setting',
'type' => $type,
'name' => esc_html__( 'Monitor SEO Performance', 'rank-math-pro' ),
'desc' => sprintf(
/* translators: Link to kb article */
wp_kses_post( __( 'This option allows you to monitor the SEO performance of all of your sites in one centralized dashboard on RankMath.com, so you can check up on sites at a glance. <a href="%1$s" target="_blank">Learn more</a>.', 'rank-math-pro' ) ),
KB::get( 'help-analytics', 'Options Panel Analytics Tab Monitor Performance' )
),
'default' => 'off',
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'google_updates',
'type' => $type,
'name' => esc_html__( 'Google Core Updates in the Graphs', 'rank-math-pro' ),
'desc' => sprintf(
/* translators: Link to kb article */
__( 'This option allows you to show %s in the Analytics graphs.', 'rank-math-pro' ),
'<a href="' . KB::get( 'google-updates', 'Google Core Updates in the Graphs' ) . '" target="_blank">' . __( 'Google Core Updates', 'rank-math-pro' ) . '</a>'
),
'default' => 'on',
],
++$fields_position
);
}
/**
* Check if certain fields got updated.
*
* @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.
*/
public function sync_global_settings( $object_id, $updated ) {
if ( in_array( 'sync_global_setting', $updated, true ) ) {
\RankMathPro\Admin\Api::get()->sync_setting(
cmb2_get_option( $object_id, 'sync_global_setting' )
);
$this->send_summary();
}
}
/**
* Get local Analytics JS URL if the option is turned on.
*
* @return mixed
*/
public function get_local_gtag_js_url() {
$settings = $this->get_settings();
$validator_key = 'rank_math_local_ga_js_validator_' . md5( $settings['property_id'] );
$validator = get_transient( $validator_key );
if ( ! is_string( $validator ) || empty( $validator ) ) {
$validator = '1';
}
return add_query_arg( 'local_ga_js', $validator, trailingslashit( home_url() ) );
}
/**
* Serve Analytics JS from local cache if the option is turned on.
*
* @return void
*/
public function local_js_endpoint() {
if ( Param::get( 'local_ga_js' ) && $this->get_settings()['local_ga_js'] && $this->get_local_ga_js_contents() ) {
header( 'Content-Type: application/javascript' );
header( 'Cache-Control: max-age=604800, public' );
echo $this->get_local_ga_js_contents(); // phpcs:ignore
exit;
}
}
/**
* Get local cache of GA JS file contents or fetch new data.
*
* @param boolean $force_update Force update transient now.
* @return string
*/
public function get_local_ga_js_contents( $force_update = false ) {
$settings = $this->get_settings();
$cache_key = 'rank_math_local_ga_js_' . md5( $settings['property_id'] );
$validator_key = 'rank_math_local_ga_js_validator_' . md5( $settings['property_id'] );
$validator = md5( $cache_key . time() );
$stored = get_transient( $cache_key );
if ( false !== $stored && ! $force_update ) {
return $stored;
}
$response = wp_remote_get( 'https://www.googletagmanager.com/gtag/js?id=' . $settings['property_id'] );
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
set_transient( $cache_key, '', 12 * HOUR_IN_SECONDS );
return '';
}
$contents = wp_remote_retrieve_body( $response );
set_transient( $cache_key, $contents, 12 * HOUR_IN_SECONDS );
set_transient( $validator_key, $validator, 12 * HOUR_IN_SECONDS );
return $contents;
}
/**
* Filter gtag.js config array.
*
* @param array $config Config parameters.
* @return array
*/
public function gtag_config( $config ) {
$settings = $this->get_settings();
if ( ! empty( $settings['anonymize_ip'] ) ) {
$config[] = "'anonymize_ip': true";
}
return $config;
}
/**
* Filter function to add Google permissions used in Pro.
*
* @param array $data Array of System status data.
*/
public function google_permission_info( $data ) {
$data['fields']['permissions']['value'] = array_merge(
$data['fields']['permissions']['value'],
[
esc_html__( 'AdSense', 'rank-math-pro' ) => Permissions::get_status_text( Permissions::has_adsense() ),
esc_html__( 'Analytics', 'rank-math-pro' ) => Permissions::get_status_text( Permissions::has_analytics() ),
]
);
ksort( $data['fields']['permissions']['value'] );
return $data;
}
/**
* Filter inline JS & URL for gtag.js.
*
* @param array $gtag_data Array containing URL & inline code for the gtag script.
* @return array
*/
public function gtag( $gtag_data ) {
if ( is_admin() ) {
return $gtag_data;
}
$settings = $this->get_settings();
if ( empty( $settings['install_code'] ) ) {
return $gtag_data;
}
if ( ! empty( $settings['local_ga_js'] ) ) {
$gtag_data['url'] = $this->get_local_gtag_js_url();
}
return $gtag_data;
}
/**
* Filter winning and losing posts if needed.
*
* @param null $null Null.
* @param array $data Analytics data array.
* @param array $args Query arguments.
*
* @return mixed
*/
public function filter_winning_losing_posts( $null, $data, $args ) {
$order_by_field = $args['orderBy'];
$type = $args['type'];
$objects = $args['objects'];
if ( ! in_array( $type, [ 'win', 'lose' ], true ) ) {
return $null;
}
// Filter array by $type value.
$order_by_position = in_array( $order_by_field, [ 'diffPosition', 'position' ], true ) ? true : false;
if ( ( 'win' === $type && $order_by_position ) || ( 'lose' === $type && ! $order_by_position ) ) {
$data = array_filter(
$data,
function( $row ) use ( $order_by_field, $objects ) {
if ( $objects ) {
// Show Winning posts if difference is 80 or less.
return $row[ $order_by_field ] < 0 && $row[ $order_by_field ] > -80;
}
return $row[ $order_by_field ] < 0;
}
);
} elseif ( ( 'lose' === $type && $order_by_position ) || ( 'win' === $type && ! $order_by_position ) ) {
$data = array_filter(
$data,
function( $row ) use ( $order_by_field ) {
return $row[ $order_by_field ] > 0;
}
);
}
$data = $this->finalize_filtered_data( $data, $args );
return $data;
}
/**
* Filter winning keywords if needed.
*
* @param null $null Null.
* @param array $data Analytics data array.
* @param array $args Query arguments.
*
* @return mixed
*/
public function filter_winning_keywords( $null, $data, $args ) {
$order_by_field = $args['orderBy'];
$dimension = $args['dimension'];
if ( 'query' !== $dimension || 'diffPosition' !== $order_by_field || 'ASC' !== $args['order'] ) {
return $null;
}
// Filter array by $type value.
$data = array_filter(
$data,
function( $row ) use ( $order_by_field ) {
return $row[ $order_by_field ] < 0 && $row[ $order_by_field ] > -80;
}
);
$data = $this->finalize_filtered_data( $data, $args );
return $data;
}
/**
* Sort & limit keywords according to the args.
*
* @param array $data Data rows.
* @param array $args Query args.
*
* @return array
*/
private function finalize_filtered_data( $data, $args ) {
if ( ! empty( $args['order'] ) ) {
$sort_base_arr = array_column( $data, $args['orderBy'], $args['dimension'] );
array_multisort( $sort_base_arr, 'ASC' === $args['order'] ? SORT_ASC : SORT_DESC, $data );
}
$data = array_slice( $data, $args['offset'], $args['perpage'], true );
return $data;
}
/**
* Add search traffic value in seo detail of post lists
*
* @param int $post_id object Id.
*/
public function post_column_search_traffic( $post_id ) {
if ( ! Authentication::is_authorized() ) {
return;
}
$analytics = get_option( 'rank_math_google_analytic_options' );
$analytics_connected = ! empty( $analytics ) && ! empty( $analytics['view_id'] );
static $traffic_data;
if ( null === $traffic_data ) {
$post_ids = $this->get_queried_post_ids();
if ( empty( $post_ids ) ) {
$traffic_data = [];
return;
}
$traffic_data = $analytics_connected ? Pageviews::get_traffic_by_object_ids( $post_ids ) : Pageviews::get_impressions_by_object_ids( $post_ids );
}
if ( ! isset( $traffic_data[ $post_id ] ) ) {
return;
}
?>
<span class="rank-math-column-display rank-math-search-traffic">
<strong>
<?php
$analytics_connected
? esc_html_e( 'Search Traffic:', 'rank-math-pro' )
: esc_html_e( 'Search Impression:', 'rank-math-pro' );
?>
</strong>
<?php echo esc_html( number_format( $traffic_data[ $post_id ] ) ); ?>
</span>
<?php
}
/**
* Extend the date_exists() function to include the additional tables.
*
* @param string $tables Tables.
* @return string
*/
public function date_exists_tables( $tables ) {
$tables['analytics'] = DB_Helper::check_table_exists( 'rank_math_analytics_ga' ) ? 'rank_math_analytics_ga' : '';
$tables['adsense'] = DB_Helper::check_table_exists( 'rank_math_analytics_adsense' ) ? 'rank_math_analytics_adsense' : '';
return $tables;
}
/**
* Get queried post ids.
*/
private function get_queried_post_ids() {
global $wp_query;
if ( empty( $wp_query->posts ) ) {
return false;
}
$post_ids = array_filter(
array_map(
function( $post ) {
return isset( $post->ID ) ? $post->ID : '';
},
$wp_query->posts
)
);
return $post_ids;
}
}

View File

@@ -0,0 +1,651 @@
<?php
/**
* The Analytics module database operations
*
* @since 2.0.0
* @package RankMathPro
* @subpackage RankMathPro\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics;
use RankMath\Helper;
use RankMath\Admin\Admin_Helper;
use RankMath\Google\Analytics as Analytics_Free;
use RankMath\Analytics\Stats;
use RankMathPro\Google\Adsense;
use RankMathPro\Analytics\Keywords;
use MyThemeShop\Helpers\Str;
use MyThemeShop\Database\Database;
defined( 'ABSPATH' ) || exit;
/**
* DB class.
*/
class DB {
/**
* Get any table.
*
* @param string $table_name Table name.
*
* @return \MyThemeShop\Database\Query_Builder
*/
public static function table( $table_name ) {
return Database::table( $table_name );
}
/**
* Get console data table.
*
* @return \MyThemeShop\Database\Query_Builder
*/
public static function analytics() {
return Database::table( 'rank_math_analytics_gsc' );
}
/**
* Get analytics data table.
*
* @return \MyThemeShop\Database\Query_Builder
*/
public static function traffic() {
return Database::table( 'rank_math_analytics_ga' );
}
/**
* Get adsense data table.
*
* @return \MyThemeShop\Database\Query_Builder
*/
public static function adsense() {
return Database::table( 'rank_math_analytics_adsense' );
}
/**
* Get objects table.
*
* @return \MyThemeShop\Database\Query_Builder
*/
public static function objects() {
return Database::table( 'rank_math_analytics_objects' );
}
/**
* Get inspections table.
*
* @return \MyThemeShop\Database\Query_Builder
*/
public static function inspections() {
return Database::table( 'rank_math_analytics_inspections' );
}
/**
* Get links table.
*
* @return \MyThemeShop\Database\Query_Builder
*/
public static function links() {
return Database::table( 'rank_math_internal_meta' );
}
/**
* Get keywords table.
*
* @return \MyThemeShop\Database\Query_Builder
*/
public static function keywords() {
return Database::table( 'rank_math_analytics_keyword_manager' );
}
/**
* Delete console and analytics data.
*
* @param int $days Decide whether to delete all or delete 90 days data.
*/
public static function delete_by_days( $days ) {
if ( -1 === $days ) {
self::traffic()->truncate();
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::traffic()->whereBetween( 'created', [ $end, $start ] )->delete();
self::analytics()->whereBetween( 'created', [ $end, $start ] )->delete();
}
self::purge_cache();
return true;
}
/**
* Delete record for comparison.
*/
public static function delete_data_log() {
$days = Helper::get_settings( 'general.console_caching_control', 90 );
$start = date_i18n( 'Y-m-d H:i:s', strtotime( '-' . ( $days * 2 ) . ' days' ) );
self::traffic()->where( 'created', '<', $start )->delete();
self::analytics()->where( 'created', '<', $start )->delete();
}
/**
* Purge SC transient
*/
public static function purge_cache() {
$table = Database::table( 'options' );
$table->whereLike( 'option_name', 'rank_math_analytics_data_info' )->delete();
$table->whereLike( 'option_name', 'tracked_keywords_summary' )->delete();
$table->whereLike( 'option_name', 'top_keywords' )->delete();
$table->whereLike( 'option_name', 'top_keywords_graph' )->delete();
$table->whereLike( 'option_name', 'winning_keywords' )->delete();
$table->whereLike( 'option_name', 'losing_keywords' )->delete();
$table->whereLike( 'option_name', 'posts_summary' )->delete();
$table->whereLike( 'option_name', 'winning_posts' )->delete();
$table->whereLike( 'option_name', 'losing_posts' )->delete();
wp_cache_flush();
}
/**
* Get search console table info (for pro version only).
*
* @return array
*/
public static function info() {
global $wpdb;
$key = 'rank_math_analytics_data_info';
$data = get_transient( $key );
if ( false !== $data ) {
return $data;
}
$days = 0;
$rows = self::get_total_rows();
$size = $wpdb->get_var( 'SELECT SUM((data_length + index_length)) AS size FROM information_schema.TABLES WHERE table_schema="' . $wpdb->dbname . '" AND table_name IN ( ' . '"' . $wpdb->prefix . 'rank_math_analytics_ga", "' . $wpdb->prefix . 'rank_math_analytics_adsense"' . ' )' ); // phpcs:ignore
$data = compact( 'days', 'rows', 'size' );
set_transient( $key, $data, DAY_IN_SECONDS );
return $data;
}
/**
* Get total row count of analytics and adsense tables
*
* @return int total row count
*/
public static function get_total_rows() {
$rows = 0;
if ( Analytics_Free::is_analytics_connected() ) {
$rows += self::table( 'rank_math_analytics_ga' )
->selectCount( 'id' )
->getVar();
}
if ( Adsense::is_adsense_connected() ) {
$rows += self::table( 'rank_math_analytics_adsense' )
->selectCount( 'id' )
->getVar();
}
return $rows;
}
/**
* 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;
}
/**
* 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;
}
$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/Update a record into/from objects table.
*
* @param array $args Values to update.
*
* @return bool|int
*/
public static function update_object( $args = [] ) {
if ( empty( $args ) ) {
return false;
}
// If object exists, try to update.
$old_id = absint( $args['id'] );
if ( ! empty( $old_id ) ) {
unset( $args['id'] );
$updated = self::objects()->set( $args )
->where( 'id', $old_id )
->where( 'object_id', absint( $args['object_id'] ) )
->update();
if ( ! empty( $updated ) ) {
return $old_id;
}
}
// In case of new object or failed to update, try to add.
return self::add_object( $args );
}
/**
* Add console records.
*
* @param string $date Date of creation.
* @param array $rows Data rows to insert.
*/
public static function add_query_page_bulk( $date, $rows ) {
$chunks = array_chunk( $rows, 50 );
foreach ( $chunks as $chunk ) {
self::bulk_insert_query_page_data( $date . ' 00:00:00', $chunk );
}
}
/**
* Bulk inserts records into a table using WPDB. All rows must contain the same keys.
*
* @param string $date Date.
* @param array $rows Rows to insert.
*/
public static function bulk_insert_query_page_data( $date, $rows ) {
global $wpdb;
$data = [];
$placeholders = [];
$columns = [
'created',
'query',
'page',
'clicks',
'impressions',
'position',
'ctr',
];
$columns = '`' . implode( '`, `', $columns ) . '`';
$placeholder = [
'%s',
'%s',
'%s',
'%d',
'%d',
'%d',
'%d',
];
// Start building SQL, initialise data and placeholder arrays.
$sql = "INSERT INTO `{$wpdb->prefix}rank_math_analytics_gsc` ( $columns ) VALUES\n";
// Build placeholders for each row, and add values to data array.
foreach ( $rows as $row ) {
if (
$row['position'] > self::get_position_filter() ||
Str::contains( '?', $row['page'] )
) {
continue;
}
$data[] = $date;
$data[] = $row['query'];
$data[] = str_replace( Helper::get_home_url(), '', self::remove_hash( $row['page'] ) );
$data[] = $row['clicks'];
$data[] = $row['impressions'];
$data[] = $row['position'];
$data[] = $row['ctr'];
$placeholders[] = '(' . implode( ', ', $placeholder ) . ')';
}
// Don't run insert with empty dataset, return 0 since no rows affected.
if ( empty( $data ) ) {
return 0;
}
// Stitch all rows together.
$sql .= implode( ",\n", $placeholders );
// Run the query. Returns number of affected rows.
return $wpdb->query( $wpdb->prepare( $sql, $data ) ); // phpcs:ignore
}
/**
* Add analytic records.
*
* @param string $date Date of creation.
* @param array $rows Data rows to insert.
*/
public static function add_analytics_bulk( $date, $rows ) {
$chunks = array_chunk( $rows, 50 );
foreach ( $chunks as $chunk ) {
self::bulk_insert_analytics_data( $date . ' 00:00:00', $chunk );
}
}
/**
* Bulk inserts records into a 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_analytics_data( $date, $rows ) {
global $wpdb;
$data = [];
$placeholders = [];
$columns = [
'created',
'page',
'pageviews',
'visitors',
];
$columns = '`' . implode( '`, `', $columns ) . '`';
$placeholder = [
'%s',
'%s',
'%d',
'%d',
];
// Start building SQL, initialise data and placeholder arrays.
$sql = "INSERT INTO `{$wpdb->prefix}rank_math_analytics_ga` ( $columns ) VALUES\n";
// Build placeholders for each row, and add values to data array.
foreach ( $rows as $row ) {
$page = '';
$pageviews = '';
$visitors = '';
if ( ! isset( $row['dimensionValues'] ) ) {
if ( empty( $row['dimensions'][1] ) || Str::contains( '?', $row['dimensions'][1] ) ) {
continue;
}
$page = $row['dimensions'][2] . $row['dimensions'][1];
$pageviews = $row['metrics'][0]['values'][0];
$visitors = $row['metrics'][0]['values'][1];
} else {
if ( empty( $row['dimensionValues'][1]['value'] ) || Str::contains( '?', $row['dimensionValues'][1]['value'] ) ) {
continue;
}
$page = $row['dimensionValues'][0]['value'] . $row['dimensionValues'][1]['value'];
$pageviews = $row['metricValues'][0]['value'];
$visitors = $row['metricValues'][1]['value'];
}
if ( $page && $pageviews && $visitors ) {
$page = ( is_ssl() ? 'https' : 'http' ) . '://' . $page;
$data[] = $date;
$data[] = Stats::get_relative_url( self::remove_hash( $page ) );
$data[] = $pageviews;
$data[] = $visitors;
$placeholders[] = '(' . implode( ', ', $placeholder ) . ')';
}
}
if ( empty( $placeholders ) ) {
return 0;
}
// Stitch all rows together.
$sql .= implode( ",\n", $placeholders );
// Run the query. Returns number of affected rows.
return $wpdb->query( $wpdb->prepare( $sql, $data ) ); // phpcs:ignore
}
/**
* Remove hash part from Url.
*
* @param string $url Url to process.
* @return string
*/
public static function remove_hash( $url ) {
if ( ! Str::contains( '#', $url ) ) {
return $url;
}
$url = \explode( '#', $url );
return $url[0];
}
/**
* Add adsense records.
*
* @param array $rows Data rows to insert.
*/
public static function add_adsense( $rows ) {
if ( ! \MyThemeShop\Helpers\DB::check_table_exists( 'rank_math_analytics_adsense' ) ) {
return;
}
foreach ( $rows as $row ) {
$date = $row['cells'][0]['value'];
$earnings = floatval( $row['cells'][1]['value'] );
self::adsense()
->insert(
[
'created' => $date . ' 00:00:00',
'earnings' => $earnings,
],
[ '%s', '%f' ]
);
}
}
/**
* 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;
}
/**
* Bulk inserts records into a keyword table using WPDB. All rows must contain the same keys.
*
* @param array $rows Rows to insert.
*/
public static function bulk_insert_query_focus_keyword_data( $rows ) {
$registered = Admin_Helper::get_registration_data();
if ( ! $registered || empty( $registered['username'] ) || empty( $registered['api_key'] ) ) {
return false;
}
// Check remain keywords count can be added.
$total_keywords = Keywords::get()->get_tracked_keywords_count();
$new_keywords = Keywords::get()->extract_addable_track_keyword( implode( ',', $rows ) );
$keywords_count = count( $new_keywords );
if ( $keywords_count <= 0 ) {
return false;
}
$summary = Keywords::get()->get_tracked_keywords_quota();
$remain = $summary['available'] - $total_keywords;
if ( $remain <= 0 ) {
return false;
}
// Add remaining limit keywords.
$new_keywords = array_slice( $new_keywords, 0, $remain );
$data = [];
$placeholders = [];
$columns = [
'keyword',
'collection',
'is_active',
];
$columns = '`' . implode( '`, `', $columns ) . '`';
$placeholder = [
'%s',
'%s',
'%s',
];
// Start building SQL, initialise data and placeholder arrays.
global $wpdb;
$sql = "INSERT INTO `{$wpdb->prefix}rank_math_analytics_keyword_manager` ( $columns ) VALUES\n";
// Build placeholders for each row, and add values to data array.
foreach ( $new_keywords as $new_keyword ) {
$data[] = $new_keyword;
$data[] = 'uncategorized';
$data[] = 1;
$placeholders[] = '(' . implode( ', ', $placeholder ) . ')';
}
// Stitch all rows together.
$sql .= implode( ",\n", $placeholders );
// Run the query. Returns number of affected rows.
$count = $wpdb->query( $wpdb->prepare( $sql, $data ) ); // phpcs:ignore
$total_keywords = Keywords::get()->get_tracked_keywords_count();
$response = \RankMathPro\Admin\Api::get()->keywords_info( $registered['username'], $registered['api_key'], $total_keywords );
if ( $response ) {
update_option( 'rank_math_keyword_quota', $response );
}
return $count;
}
/**
* Get stats from DB for "Presence on Google" widget:
* All unique coverage_state values and their counts.
*/
public static function get_presence_stats() {
$results = self::inspections()
->select(
[
'coverage_state',
'COUNT(*)' => 'count',
]
)
->groupBy( 'coverage_state' )
->orderBy( 'count', 'DESC' )
->get();
$results = array_map(
'absint',
array_combine(
array_column( $results, 'coverage_state' ),
array_column( $results, 'count' )
)
);
return $results;
}
/**
* Get stats from DB for "Top Statuses" widget.
*/
public static function get_status_stats() {
$statuses = [
'VERDICT_UNSPECIFIED',
'PASS',
'PARTIAL',
'FAIL',
'NEUTRAL',
];
// Get all unique index_verdict values and their counts.
$index_statuses = self::inspections()
->select(
[
'index_verdict',
'COUNT(*)' => 'count',
]
)
->groupBy( 'index_verdict' )
->orderBy( 'count', 'DESC' )
->get();
$results = array_fill_keys( $statuses, 0 );
foreach ( $index_statuses as $status ) {
if ( empty( $status->index_verdict ) ) {
continue;
}
$results[ $status->index_verdict ] += $status->count;
}
return $results;
}
/**
* Get stats from DB for "Top Statuses" widget.
*/
public static function get_index_verdict( $page ) {
$verdict = self::inspections()
->select()
->where( 'page', '=', $page )
->one();
return (array) $verdict;
}
}

View File

@@ -0,0 +1,542 @@
<?php
/**
* Pro SEO Reports in Email.
*
* @since 2.0.0
* @package RankMathPro
* @subpackage RankMathPro\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Analytics\Stats;
use RankMath\Admin\Admin_Helper;
use RankMath\Google\Authentication;
use RankMath\Analytics\Email_Reports as Email_Reports_Base;
use RankMathPro\Admin\Admin_Helper as ProAdminHelper;
use RankMathPro\Analytics\Keywords;
use RankMathPro\Analytics\Posts;
use MyThemeShop\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* DB class.
*/
class Email_Reports {
use Hooker;
/**
* Path to the views folder.
*
* @var string
*/
public $views_path = '';
/**
* URL of the module's assets folder.
*
* @var string
*/
public $assets_url = '';
/**
* The constructor.
*/
public function __construct() {
$this->hooks();
}
/**
* Add filter & action hooks.
*
* @return void
*/
public function hooks() {
$this->views_path = dirname( __FILE__ ) . '/views/email-reports/';
$this->assets_url = plugin_dir_url( __FILE__ ) . 'assets/';
// CMB hooks.
$this->action( 'rank_math/admin/settings/analytics', 'add_options' );
// WP hooks.
$this->filter( 'admin_post_rank_math_save_wizard', 'save_wizard' );
// Rank Math hooks.
$this->filter( 'rank_math/analytics/email_report_template_paths', 'add_template_path' );
$this->filter( 'rank_math/analytics/email_report_variables', 'add_variables' );
$this->filter( 'rank_math/analytics/email_report_parameters', 'email_parameters' );
$this->filter( 'rank_math/analytics/email_report_image_atts', 'replace_logo', 10, 2 );
$this->filter( 'rank_math/analytics/email_report_periods', 'frequency_periods' );
$this->action( 'rank_math/analytics/options/wizard_after_email_report', 'wizard_options' );
}
/**
* Output CSS for required for the Pro reports.
*
* @return void
*/
public function add_pro_css() {
$this->template_part( 'pro-style' );
}
/**
* Replace logo image in template.
*
* @param array $atts All original attributes.
* @param string $url Image URL or identifier.
*
* @return array
*/
public function replace_logo( $atts, $url ) {
if ( 'report-logo.png' !== $url ) {
return $atts;
}
$atts['src'] = '###LOGO_URL###';
$atts['alt'] = '###LOGO_ALT###';
return $atts;
}
/**
* Add Pro variables.
*
* @param array $variables Original variables.
* @return array
*/
public function add_variables( $variables ) {
$variables['pro_assets_url'] = $this->assets_url;
$variables['logo_url'] = Email_Reports_Base::get_setting( 'logo', $this->get_logo_url_default() );
$variables['logo_alt'] = __( 'Logo', 'rank-math-pro' );
$image_id = Email_Reports_Base::get_setting( 'logo_id', 0 );
if ( $image_id ) {
$alt = get_post_meta( $image_id, '_wp_attachment_image_alt', true );
if ( $alt ) {
$variables['logo_alt'] = $alt;
}
}
$variables['header_background'] = Email_Reports_Base::get_setting( 'header_background', 'linear-gradient(90deg, rgba(112,83,181,1) 0%, rgba(73,153,210,1) 100%)' );
$variables['top_html'] = wp_kses_post( wpautop( Email_Reports_Base::get_setting( 'top_text', '' ) ) );
$variables['footer_html'] = wp_kses_post( Email_Reports_Base::get_setting( 'footer_text', $this->get_default_footer_text() ) );
$variables['custom_css'] = Email_Reports_Base::get_setting( 'custom_css', '' );
$variables['logo_link'] = Email_Reports_Base::get_setting( 'logo_link', KB::get( 'email-reports', 'PRO Email Report Logo' ) );
// Get Pro stats.
$period = Email_Reports_Base::get_period_from_frequency();
Stats::get()->set_date_range( "-{$period} days" );
$keywords = Keywords::get();
if ( Email_Reports_Base::get_setting( 'tracked_keywords', false ) ) {
$variables['winning_keywords'] = $keywords->get_tracked_winning_keywords();
$variables['losing_keywords'] = $keywords->get_tracked_losing_keywords();
} else {
$variables['winning_keywords'] = $keywords->get_winning_keywords();
$variables['losing_keywords'] = $keywords->get_losing_keywords();
}
$posts = Posts::get();
$variables['winning_posts'] = $posts->get_winning_posts();
$variables['losing_posts'] = $posts->get_losing_posts();
return $variables;
}
/**
* Add Email Report options.
*
* @param object $cmb CMB object.
*/
public function add_options( $cmb ) {
if ( ! Authentication::is_authorized() || Email_Reports_Base::are_fields_hidden() ) {
return;
}
$is_business = ProAdminHelper::is_business_plan();
// Add Frequency options.
$frequency_field = $cmb->get_field( 'console_email_frequency' );
// Early bail if the console_email_frequency field does not exist.
if ( empty( $frequency_field ) ) {
return;
}
$frequency_field->args['options']['every_15_days'] = esc_html__( 'Every 15 Days', 'rank-math-pro' );
$field_ids = wp_list_pluck( $cmb->prop( 'fields' ), 'id' );
$fields_position = array_search( 'console_email_frequency', array_keys( $field_ids ), true ) + 1;
if ( $is_business ) {
$frequency_field->args['options']['weekly'] = esc_html__( 'Every 7 Days', 'rank-math-pro' );
} else {
/**
* This field is repeated further down, to insert it in the correct
* position when the account type is Business.
*/
$cmb->add_field(
[
'id' => 'console_email_tracked_keywords',
'type' => 'toggle',
'name' => __( 'Include Only Tracked Keywords', 'rank-math-pro' ),
'description' => __( 'When enabled, the Winning Keywords section will only show Tracked Keywords.', 'rank-math-pro' ),
'default' => 'off',
'dep' => [ [ 'console_email_reports', 'on' ] ],
],
++$fields_position
);
return;
}
// Business options from here on.
$cmb->add_field(
[
'id' => 'console_email_send_to',
'type' => 'text',
'name' => __( 'Report Email Address', 'rank-math-pro' ),
'description' => __( 'Address where the reports will be sent. You can add multiple recipients separated with commas.', 'rank-math-pro' ),
'default' => Admin_Helper::get_registration_data()['email'],
'dep' => [ [ 'console_email_reports', 'on' ] ],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'console_email_subject',
'type' => 'text',
'name' => __( 'Report Email Subject', 'rank-math-pro' ),
'description' => __( 'Subject of the report emails.', 'rank-math-pro' ),
'default' => $this->get_subject_default(),
'dep' => [ [ 'console_email_reports', 'on' ] ],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'console_email_logo',
'type' => 'file',
'name' => __( 'Report Logo', 'rank-math-pro' ),
'description' => __( 'Logo appearing in the header part of the report.', 'rank-math-pro' ),
'default' => $this->get_logo_url_default(),
'options' => [ 'url' => false ],
'dep' => [ [ 'console_email_reports', 'on' ] ],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'console_email_logo_link',
'type' => 'text',
'name' => __( 'Logo Link', 'rank-math-pro' ),
'description' => __( 'URL where the logo link should point to.', 'rank-math-pro' ),
'default' => KB::get( 'email-reports-logo', 'PRO Email Report Logo' ),
'dep' => [ [ 'console_email_reports', 'on' ] ],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'console_email_header_background',
'type' => 'text',
'name' => __( 'Report Header Background', 'rank-math-pro' ),
'description' => __( 'Color hex code or any other valid value for the <code>background:</code> CSS property.', 'rank-math-pro' ),
'default' => $this->get_header_bg_default(),
'dep' => [ [ 'console_email_reports', 'on' ] ],
// Instant preview.
'after_field' => $this->get_bg_preview(),
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'console_email_link_full_report',
'type' => 'toggle',
'name' => __( 'Link to Full Report', 'rank-math-pro' ),
'description' => __( 'Select whether to include a link to the Full Report admin page in the email or not.', 'rank-math-pro' ),
'default' => 'on',
'dep' => [ [ 'console_email_reports', 'on' ] ],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'console_email_top_text',
'type' => 'textarea_small',
'name' => __( 'Report Top Text', 'rank-math-pro' ),
'description' => __( 'Text or basic HTML to insert below the title.', 'rank-math-pro' ),
'default' => '',
'dep' => [ [ 'console_email_reports', 'on' ] ],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'console_email_sections',
'type' => 'multicheck',
'name' => esc_html__( 'Include Sections', 'rank-math-pro' ),
'desc' => esc_html__( 'Select which tables to show in the report.', 'rank-math-pro' ),
'options' => [
'summary' => __( 'Basic Summary', 'rank-math-pro' ),
'positions' => __( 'Positions Summary', 'rank-math-pro' ),
'winning_posts' => __( 'Top Winning Posts', 'rank-math-pro' ),
'losing_posts' => __( 'Top Losing Posts', 'rank-math-pro' ),
'winning_keywords' => __( 'Top Winning Keywords', 'rank-math-pro' ),
'losing_keywords' => __( 'Top Losing Keywords', 'rank-math-pro' ),
],
'default' => [ 'summary', 'positions', 'winning_posts', 'winning_keywords', 'losing_keywords' ],
'select_all_button' => true,
'dep' => [ [ 'console_email_reports', 'on' ] ],
],
++$fields_position
);
/**
* This field is also added for non-business accounts at the beginning
* of this function.
*/
$cmb->add_field(
[
'id' => 'console_email_tracked_keywords',
'type' => 'toggle',
'name' => __( 'Include Only Tracked Keywords', 'rank-math-pro' ),
'description' => __( 'When enabled, the Winning Keywords and Losing Keywords sections will only show Tracked Keywords.', 'rank-math-pro' ),
'default' => 'off',
'dep' => [ [ 'console_email_reports', 'on' ] ],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'console_email_footer_text',
'type' => 'textarea_small',
'name' => __( 'Report Footer Text', 'rank-math-pro' ),
'description' => __( 'Text or basic HTML to insert in the footer area.', 'rank-math-pro' ),
'default' => $this->get_default_footer_text(),
'dep' => [ [ 'console_email_reports', 'on' ] ],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'console_email_custom_css',
'type' => 'textarea_small',
'name' => __( 'Additional CSS code', 'rank-math-pro' ),
'description' => __( 'Additional CSS code to customize the appearance of the reports. Insert the CSS code directly, without the wrapping style tag. Please note that the CSS support is limited in email clients and the appearance may vary greatly.', 'rank-math-pro' ),
'default' => '',
'dep' => [ [ 'console_email_reports', 'on' ] ],
],
++$fields_position
);
}
/**
* Get default value for footer text option.
*
* @return string
*/
public function get_default_footer_text() {
return join(
' ',
[
// Translators: placeholder is a link to the homepage.
sprintf( esc_html__( 'This email was sent to you as a registered member of %s.', 'rank-math-pro' ), '<a href="###SITE_URL###">###SITE_URL_SIMPLE###</a>' ),
// Translators: placeholder is a link to the settings, with "click here" as the anchor text.
sprintf( esc_html__( 'To update your email preferences, %s. ###ADDRESS###', 'rank-math-pro' ), '<a href="###SETTINGS_URL###">' . esc_html__( 'click here', 'rank-math-pro' ) . '</a>' ),
]
);
}
/**
* Change email parameters if needed.
*
* @param array $email Parameters array.
* @return array
*/
public function email_parameters( $email ) {
$email['to'] = Email_Reports_Base::get_setting( 'send_to', Admin_Helper::get_registration_data()['email'] );
$email['subject'] = Email_Reports_Base::get_setting( 'subject', $this->get_subject_default() );
return $email;
}
/**
* Get 'value' & 'diff' for the stat template part.
*
* @param mixed $data Stats data.
* @param string $item Item we want to extract.
* @return array
*/
public static function get_stats_val( $data, $item ) {
$value = isset( $data[ $item ]['total'] ) ? $data[ $item ]['total'] : 0;
$diff = isset( $data[ $item ]['difference'] ) ? $data[ $item ]['difference'] : 0;
return compact( 'value', 'diff' );
}
/**
* Output additional options in the Setup Wizard.
*
* @return void
*/
public function wizard_options() {
if ( ! ProAdminHelper::is_business_plan() ) {
return;
}
?>
<div class="cmb-row cmb-type-toggle cmb2-id-console-email-send-to" data-fieldtype="toggle">
<div class="cmb-th">
<label for="console_email_send"><?php esc_html_e( 'Report Email Address', 'rank-math-pro' ); ?></label>
</div>
<div class="cmb-td">
<input type="text" class="regular-text" name="console_email_send_to" id="console_email_send_to" value="<?php echo esc_attr( Helper::get_settings( 'general.console_email_send_to' ) ); ?>" data-hash="42cpi4bihms0">
<p class="cmb2-metabox-description"><?php esc_html_e( 'Address where the reports will be sent. You can add multiple recipients separated with commas.', 'rank-math-pro' ); ?></p>
<div class="rank-math-cmb-dependency hidden" data-relation="or"><span class="hidden" data-field="console_email_reports" data-comparison="=" data-value="on"></span></div>
</div>
</div>
<?php
}
/**
* Save additional wizard options.
*
* @return bool
*/
public function save_wizard() {
$referer = Param::post( '_wp_http_referer' );
if ( empty( $_POST ) ) {
return wp_safe_redirect( $referer );
}
check_admin_referer( 'rank-math-wizard', 'security' );
if ( ! Helper::has_cap( 'general' ) ) {
return false;
}
$send_to = Param::post( 'console_email_send_to' );
if ( ! $send_to ) {
return true;
}
$settings = rank_math()->settings->all_raw();
$settings['general']['console_email_send_to'] = $send_to;
Helper::update_all_settings( $settings['general'], null, null );
return true;
}
/**
* Add element and script for background preview.
*
* @return string
*/
public function get_bg_preview() {
$script = '
<script>
jQuery( function() {
jQuery( "#console_email_header_background" ).on( "change", function() {
jQuery( ".rank-math-preview-bg" ).css( "background", jQuery( this ).val() );
} );
} );
</script>
';
return '<div class="rank-math-preview-bg" data-title="' . esc_attr( __( 'Preview', 'rank-math-pro' ) ) . '" style="background: ' . esc_attr( Helper::get_settings( 'general.console_email_header_background', $this->get_header_bg_default() ) ) . '"></div>' . $script;
}
/**
* Get default value for the Header Background option.
*
* @return string
*/
public function get_header_bg_default() {
return 'linear-gradient(90deg, #724BB7 0%, #4098D7 100%)';
}
/**
* Get default value for the Logo URL option.
*
* @return string
*/
public function get_logo_url_default() {
$url = \rank_math()->plugin_url() . 'includes/modules/analytics/assets/img/';
return $url . 'report-logo.png';
}
/**
* Get default value for the Subject option.
*
* @return string
*/
public function get_subject_default() {
return sprintf(
// Translators: placeholder is the site URL.
__( 'Rank Math [SEO Report] - %s', 'rank-math-pro' ),
explode( '://', get_home_url() )[1]
);
}
/**
* Shorten a URL, like http://example-url...long-page/
*
* @param string $url URL to shorten.
* @param integer $max Max length in characters.
* @return string
*/
public static function shorten_url( $url, $max = 16 ) {
$length = strlen( $url );
if ( $length <= $max + 3 ) {
return $url;
}
return substr_replace( $url, '...', $max / 2, $length - $max );
}
/**
* Add pro template path to paths.
*
* @param string[] $paths Original paths.
* @return string[]
*/
public function add_template_path( $paths ) {
$paths[] = $this->views_path;
return $paths;
}
/**
* Add day numbers for new frequencies.
*
* @param array $periods Original periods.
* @return array
*/
public function frequency_periods( $periods ) {
$periods['every_15_days'] = 15;
$periods['weekly'] = 7;
return $periods;
}
}

View File

@@ -0,0 +1,752 @@
<?php
/**
* The Analytics Module
*
* @since 2.0.0
* @package RankMathPro
* @subpackage RankMathPro\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics;
use WP_REST_Request;
use RankMath\Traits\Cache;
use RankMath\Traits\Hooker;
use RankMath\Analytics\Stats;
use RankMath\Helper;
use MyThemeShop\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* Keywords class.
*/
class Keywords {
use Hooker, Cache;
/**
* Main instance
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Keywords
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Keywords ) ) {
$instance = new Keywords();
$instance->setup();
}
return $instance;
}
/**
* Initialize filter.
*/
public function setup() {
$this->filter( 'rank_math/analytics/keywords', 'add_keyword_position_graph' );
$this->filter( 'rank_math/analytics/keywords_overview', 'add_winning_losing_data' );
$this->action( 'save_post', 'add_post_focus_keyword' );
$this->action( 'init', 'get_post_type_list', 99 );
}
/**
* Get accessible post type lists to auto add the focus keywords.
*/
public function get_post_type_list() {
if ( 'rank-math-analytics' !== Param::get( 'page' ) ) {
return;
}
$post_types = array_map(
function( $post_type ) {
return 'attachment' === $post_type ? false : Helper::get_post_type_label( $post_type );
},
Helper::get_accessible_post_types()
);
Helper::add_json( 'postTypes', array_filter( $post_types ) );
Helper::add_json( 'autoAddFK', Helper::get_settings( 'general.auto_add_focus_keywords', [] ) );
}
/**
* Get keywords position data to show it in the graph.
*
* @param array $rows Rows.
* @return array
*/
public function add_keyword_position_graph( $rows ) {
$history = $this->get_graph_data_for_keywords( \array_keys( $rows ) );
$rows = Stats::get()->set_query_position( $rows, $history );
return $rows;
}
/**
* Get winning and losing keywords data.
*
* @param array $data Data.
* @return array
*/
public function add_winning_losing_data( $data ) {
$data['winningKeywords'] = $this->get_winning_keywords();
$data['losingKeywords'] = $this->get_losing_keywords();
if ( empty( $data['winningKeywords'] ) ) {
$data['winningKeywords']['response'] = 'No Data';
}
if ( empty( $data['losingKeywords'] ) ) {
$data['losingKeywords']['response'] = 'No Data';
}
return $data;
}
/**
* Extract keywords that can be added by removing the empty and the duplicate keywords.
*
* @param string $keywords Comma Separated Keyword List.
*
* @return array Keywords that can be added.
*/
public function extract_addable_track_keyword( $keywords ) {
global $wpdb;
// Split keywords.
$keywords_to_add = array_filter( array_map( 'trim', explode( ',', $keywords ) ) );
$keywords_to_check = array_filter( array_map( 'mb_strtolower', explode( ',', $keywords ) ) );
// Check if keywords already exists.
$keywords_joined = "'" . join( "', '", array_map( 'esc_sql', $keywords_to_add ) ) . "'";
$query = "SELECT keyword FROM {$wpdb->prefix}rank_math_analytics_keyword_manager as km WHERE km.keyword IN ( $keywords_joined )";
$data = $wpdb->get_results( $query ); // phpcs:ignore
// Filter out non-existing keywords.
foreach ( $data as $row ) {
$key = array_search( mb_strtolower( $row->keyword ), $keywords_to_check, true );
if ( false !== $key ) {
unset( $keywords_to_add[ $key ] );
}
}
return $keywords_to_add;
}
/**
* Add keyword to Rank Tracker.
*
* @param array $keywords Keyword List.
*/
public function add_track_keyword( $keywords ) {
foreach ( $keywords as $add_keyword ) {
DB::keywords()->insert(
[
'keyword' => $add_keyword,
'collection' => 'uncategorized',
'is_active' => true,
],
[ '%s', '%s', '%d' ]
);
}
delete_transient( Stats::get()->get_cache_key( 'tracked_keywords_summary', Stats::get()->days . 'days' ) );
}
/**
* Remove a keyword from Rank Tracker.
*
* @param string $keyword Keyword to remove.
*/
public function remove_track_keyword( $keyword ) {
DB::keywords()->where( 'keyword', $keyword )
->delete();
delete_transient( Stats::get()->get_cache_key( 'tracked_keywords_summary', Stats::get()->days . 'days' ) );
}
/**
* Delete all tracked keywords.
*/
public function delete_all_tracked_keywords() {
DB::keywords()->delete();
delete_transient( Stats::get()->get_cache_key( 'tracked_keywords_summary', Stats::get()->days . 'days' ) );
}
/**
* Get tracked keywords count.
*
* @return int Total keywords count
*/
public function get_tracked_keywords_count() {
$total = DB::keywords()
->selectCount( 'DISTINCT(keyword)', 'total' )
->where( 'is_active', 1 )
->getVar();
return (int) $total;
}
/**
* Get keywords quota.
*
* @return array Keywords usage info.
*/
public function get_tracked_keywords_quota() {
$quota = (array) get_option(
'rank_math_keyword_quota',
[
'taken' => 0,
'available' => 0,
]
);
return $quota;
}
/**
* Get tracked keywords summary.
*
* @return array Keywords usage info.
*/
public function get_tracked_keywords_summary() {
$cache_key = 'tracked_keywords_summary';
$cache_group = 'tracked_keywords_summary';
$summary = $this->get_cache( $cache_key, $cache_group );
if ( empty( $summary ) ) {
$summary = $this->get_tracked_keywords_quota();
$summary['total'] = $this->get_tracked_keywords_count();
$this->set_cache( $cache_key, $summary, $cache_group, DAY_IN_SECONDS );
}
return $summary;
}
/**
* Get winning tracked keywords.
*
* @return array Top 5 winning tracked keywords data.
*/
public function get_tracked_winning_keywords() {
return $this->get_tracked_keywords(
[
'offset' => 0,
'perpage' => 5,
'where' => 'WHERE COALESCE( ROUND( t1.position - COALESCE( t2.position, 100 ), 0 ), 0 ) < 0',
]
);
}
/**
* Get losing tracked keywords.
*
* @return array Top 5 losing tracked keywords data.
*/
public function get_tracked_losing_keywords() {
return $this->get_tracked_keywords(
[
'order' => 'DESC',
'offset' => 0,
'perpage' => 5,
'where' => 'WHERE COALESCE( ROUND( t1.position - COALESCE( t2.position, 100 ), 0 ), 0 ) > 0',
]
);
}
/**
* Get tracked keywords rows.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return array Tracked keywords data.
*/
public function get_tracked_keywords_rows( WP_REST_Request $request ) {
$per_page = 25;
$cache_args = $request->get_params();
$cache_args['per_page'] = $per_page;
$cache_group = 'rank_math_rest_tracked_keywords_rows';
$cache_key = $this->generate_hash( $cache_args );
$result = $this->get_cache( $cache_key, $cache_group );
if ( ! empty( $result ) ) {
return $result;
}
$page = ! empty( $request->get_param( 'page' ) ) ? $request->get_param( 'page' ) : 1;
$orderby = ! empty( $request->get_param( 'orderby' ) ) ? $request->get_param( 'orderby' ) : 'default';
$order = ! empty( $request->get_param( 'order' ) ) ? strtoupper( $request->get_param( 'order' ) ) : 'DESC';
$keyword = ! empty( $request->get_param( 'search' ) ) ? filter_var( urldecode( $request->get_param( 'search' ) ), FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_BACKTICK ) : '';
$offset = ( $page - 1 ) * $per_page;
$args = wp_parse_args(
[
'dimension' => 'query',
'limit' => "LIMIT {$offset}, {$per_page}",
'keyword' => $keyword,
]
);
switch ( $orderby ) {
case 'impressions':
case 'clicks':
case 'ctr':
case 'position':
$args['orderBy'] = $orderby;
$args['order'] = $order;
break;
case 'query':
$args['orderBy'] = 'keyword';
$args['order'] = $order;
break;
}
$data = $this->get_tracked_keywords_data( $args );
$data = Stats::get()->set_dimension_as_key( $data );
$history = $this->get_graph_data_for_keywords( \array_keys( $data ) );
$data = Stats::get()->set_query_position( $data, $history );
if ( 'default' === $orderby ) {
uasort(
$data,
function( $a, $b ) use ( $orderby ) {
if ( false === array_key_exists( 'position', $a ) ) {
$a['position'] = [ 'total' => '0' ];
}
if ( false === array_key_exists( 'position', $b ) ) {
$b['position'] = [ 'total' => '0' ];
}
if ( 0 === intval( $b['position']['total'] ) ) {
return 0;
}
return $a['position']['total'] > $b['position']['total'];
}
);
}
$result['rowsData'] = $data;
// get total rows by search.
$args = wp_parse_args(
[
'dimension' => 'query',
'limit' => 'LIMIT 10000',
'keyword' => $keyword,
]
);
if ( empty( $data ) ) {
$result['response'] = 'No Data';
} else {
$search_data = $this->get_tracked_keywords_data( $args );
$result['total'] = count( $search_data );
$this->set_cache( $cache_key, $result, $cache_group, DAY_IN_SECONDS );
}
return $result;
}
/**
* Get keyword rows from keyword manager table.
*
* @param array $args Array of arguments.
* @return array
*/
public function get_tracked_keywords_data( $args = [] ) {
global $wpdb;
Helper::enable_big_selects_for_queries();
$args = wp_parse_args(
$args,
[
'dimension' => 'query',
'order' => 'ASC',
'orderBy' => 'diffPosition1',
'objects' => false,
'where' => '',
'sub_where' => '',
'dates' => ' AND created BETWEEN %s AND %s',
'limit' => 'LIMIT 5',
'keyword' => '',
]
);
$where = $args['where'];
$limit = $args['limit'];
$dimension = $args['dimension'];
$sub_where = $args['sub_where'];
$dates = $args['dates'];
$keyword = trim( $args['keyword'] );
$order = sprintf( 'ORDER BY %s %s', $args['orderBy'], $args['order'] );
$dates_query = sprintf( " AND created BETWEEN '%s' AND '%s' ", Stats::get()->start_date, Stats::get()->end_date );
// Step1. Get most recent data row id for each keyword.
// phpcs:disable
$where_like_keyword = $wpdb->prepare( ' WHERE keyword LIKE %s', '%' . $wpdb->esc_like( $keyword ) . '%' );
if ( empty( $keyword ) ) {
$where_like_keyword = '';
}
$query = "SELECT MAX(id) as id FROM {$wpdb->prefix}rank_math_analytics_gsc WHERE 1 = 1 {$dates_query} AND {$dimension} IN ( SELECT keyword from {$wpdb->prefix}rank_math_analytics_keyword_manager {$where_like_keyword} GROUP BY keyword ) GROUP BY {$dimension}";
$ids = $wpdb->get_results( $query );
// phpcs:enable
// Step2. Get id list from above result.
$ids = wp_list_pluck( $ids, 'id' );
$ids_where = " AND id IN ('" . join( "', '", $ids ) . "')";
// Step3. Get most recent data row id for each keyword (for comparison).
// phpcs:disable
$dates_query = sprintf( " AND created BETWEEN '%s' AND '%s' ", Stats::get()->compare_start_date, Stats::get()->compare_end_date );
$query = "SELECT MAX(id) as id FROM {$wpdb->prefix}rank_math_analytics_gsc WHERE 1 = 1 {$dates_query} AND {$dimension} IN ( SELECT keyword from {$wpdb->prefix}rank_math_analytics_keyword_manager {$where_like_keyword} GROUP BY keyword ) GROUP BY {$dimension}";
$old_ids = $wpdb->get_results( $query );
// Step4. Get id list from above result.
$old_ids = wp_list_pluck( $old_ids, 'id' );
$old_ids_where = " AND id IN ('" . join( "', '", $old_ids ) . "')";
// Step5. Get most performing keywords first based on id list from above.
$where_like_keyword1 = $wpdb->prepare( ' WHERE km.keyword LIKE %s', '%' . $wpdb->esc_like( $keyword ) . '%' );
if ( empty( $keyword ) ) {
$where_like_keyword1 = '';
}
$positions = $wpdb->get_results(
"SELECT DISTINCT(km.keyword) as {$dimension}, COALESCE(t.position, 0) as position, COALESCE(t.diffPosition, 0) as diffPosition, COALESCE(t.diffPosition, 100) as diffPosition1, COALESCE(t.impressions, 0) as impressions, COALESCE(t.diffImpressions, 0) as diffImpressions, COALESCE(t.clicks, 0) as clicks, COALESCE(t.diffClicks, 0) as diffClicks, COALESCE(t.ctr, 0) as ctr, COALESCE(t.diffCtr, 0) as diffCtr
FROM {$wpdb->prefix}rank_math_analytics_keyword_manager km
LEFT JOIN (
SELECT
t1.{$dimension} as {$dimension}, ROUND( t1.position, 0 ) as position, ROUND( t1.impressions, 0 ) as impressions, ROUND( t1.clicks, 0 ) as clicks, ROUND( t1.ctr, 0 ) as ctr,
COALESCE( ROUND( t1.position - COALESCE( t2.position, 100 ), 0 ), 0 ) as diffPosition,
COALESCE( ROUND( t1.impressions - COALESCE( t2.impressions, 100 ), 0 ), 0 ) as diffImpressions,
COALESCE( ROUND( t1.clicks - COALESCE( t2.clicks, 100 ), 0 ), 0 ) as diffClicks,
COALESCE( ROUND( t1.ctr - COALESCE( t2.ctr, 100 ), 0 ), 0 ) as diffCtr
FROM
(SELECT a.{$dimension}, a.position, a.impressions,a.clicks,a.ctr FROM {$wpdb->prefix}rank_math_analytics_gsc AS a
WHERE 1 = 1{$ids_where}) AS t1
LEFT JOIN
(SELECT a.{$dimension}, a.position, a.impressions,a.clicks,a.ctr FROM {$wpdb->prefix}rank_math_analytics_gsc AS a
WHERE 1 = 1{$old_ids_where}) AS t2
ON t1.{$dimension} = t2.{$dimension}) AS t on t.{$dimension} = km.keyword
{$where_like_keyword1}
{$where}
{$order}
{$limit}",
ARRAY_A
);
// phpcs:enable
// Step6. Get keywords list from above results.
$keywords = array_column( $positions, 'query' );
$keywords = array_map( 'esc_sql', $keywords );
$keywords = array_map( 'strtolower', $keywords );
$keywords = '(\'' . join( '\', \'', $keywords ) . '\')';
// step7. Get other metrics data.
$query = $wpdb->prepare(
"SELECT t1.{$dimension} as {$dimension}, t1.clicks, t1.impressions, t1.ctr,
COALESCE( t1.clicks - t2.clicks, 0 ) as diffClicks,
COALESCE( t1.impressions - t2.impressions, 0 ) as diffImpressions,
COALESCE( t1.ctr - t2.ctr, 0 ) as diffCtr
FROM
( SELECT {$dimension}, SUM( clicks ) as clicks, SUM(impressions) as impressions, AVG(position) as position, AVG(ctr) as ctr FROM {$wpdb->prefix}rank_math_analytics_gsc WHERE 1 = 1{$dates} AND {$dimension} IN {$keywords} GROUP BY {$dimension}) as t1
LEFT JOIN
( SELECT {$dimension}, SUM( clicks ) as clicks, SUM(impressions) as impressions, AVG(position) as position, AVG(ctr) as ctr FROM {$wpdb->prefix}rank_math_analytics_gsc WHERE 1 = 1{$dates} AND {$dimension} IN {$keywords} GROUP BY {$dimension}) as t2
ON t1.query = t2.query",
Stats::get()->start_date,
Stats::get()->end_date,
Stats::get()->compare_start_date,
Stats::get()->compare_end_date
);
$metrics = $wpdb->get_results( $query, ARRAY_A );
// Step8. Merge above two results.
$positions = Stats::get()->set_dimension_as_key( $positions, $dimension );
$metrics = Stats::get()->set_dimension_as_key( $metrics, $dimension );
$data = Stats::get()->get_merged_metrics( $positions, $metrics );
// Step9. Construct return data.
foreach ( $data as $keyword => $row ) {
$data[ $keyword ]['graph'] = [];
$data[ $keyword ]['clicks'] = [
'total' => (int) $data[ $keyword ]['clicks'],
'difference' => (int) $data[ $keyword ]['diffClicks'],
];
$data[ $keyword ]['impressions'] = [
'total' => (int) $data[ $keyword ]['impressions'],
'difference' => (int) $data[ $keyword ]['diffImpressions'],
];
$data[ $keyword ]['position'] = [
'total' => (float) $data[ $keyword ]['position'],
'difference' => (float) $data[ $keyword ]['diffPosition'],
];
$data[ $keyword ]['ctr'] = [
'total' => (float) $data[ $keyword ]['ctr'],
'difference' => (float) $data[ $keyword ]['diffCtr'],
];
unset(
$data[ $keyword ]['diffClicks'],
$data[ $keyword ]['diffImpressions'],
$data[ $keyword ]['diffPosition'],
$data[ $keyword ]['diffCtr']
);
}
return $data;
}
/**
* Get tracked keywords.
*
* @param array $args Array of arguments.
* @return array
*/
public function get_tracked_keywords( $args = [] ) {
global $wpdb;
$args = wp_parse_args(
$args,
[
'dimension' => 'query',
'order' => 'ASC',
'orderBy' => 'diffPosition',
'offset' => 0,
'perpage' => 20000,
'sub_where' => " AND query IN ( SELECT keyword from {$wpdb->prefix}rank_math_analytics_keyword_manager )",
]
);
$data = Stats::get()->get_analytics_data( $args );
$history = $this->get_graph_data_for_keywords( \array_keys( $data ) );
$data = Stats::get()->set_query_position( $data, $history );
// Add remaining keywords.
if ( 5 !== $args['perpage'] ) {
$rows = DB::keywords()->get();
foreach ( $rows as $row ) {
if ( ! isset( $data[ $row->keyword ] ) ) {
$data[ $row->keyword ] = [
'query' => $row->keyword,
'graph' => [],
'clicks' => [
'total' => 0,
'difference' => 0,
],
'impressions' => [
'total' => 0,
'difference' => 0,
],
'position' => [
'total' => 0,
'difference' => 0,
],
'ctr' => [
'total' => 0,
'difference' => 0,
],
'pageviews' => [
'total' => 0,
'difference' => 0,
],
];
}
}
}
return $data;
}
/**
* Get most recent day's keywords.
*
* @return array
*/
public function get_recent_keywords() {
global $wpdb;
$query = $wpdb->prepare(
"SELECT query
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE DATE(created) = (SELECT MAX(DATE(created)) FROM {$wpdb->prefix}rank_math_analytics_gsc WHERE created BETWEEN %s AND %s)
GROUP BY query",
Stats::get()->start_date,
Stats::get()->end_date
);
$data = $wpdb->get_results( $query ); // phpcs:ignore
return $data;
}
/**
* Get top 5 winning keywords.
*
* @return array
*/
public function get_winning_keywords() {
$cache_key = Stats::get()->get_cache_key( 'winning_keywords', Stats::get()->days . 'days' );
$cache = get_transient( $cache_key );
if ( false !== $cache ) {
return $cache;
}
// Get most recent day's keywords only.
$keywords = $this->get_recent_keywords();
$keywords = wp_list_pluck( $keywords, 'query' );
$keywords = array_map( 'strtolower', $keywords );
$data = Stats::get()->get_analytics_data(
[
'order' => 'ASC',
'dimension' => 'query',
'where' => 'WHERE COALESCE( ROUND( t1.position - COALESCE( t2.position, 100 ), 0 ), 0 ) < 0',
]
);
$history = $this->get_graph_data_for_keywords( \array_keys( $data ) );
$data = Stats::get()->set_query_position( $data, $history );
set_transient( $cache_key, $data, DAY_IN_SECONDS );
return $data;
}
/**
* Get top 5 losing keywords.
*
* @return array
*/
public function get_losing_keywords() {
$cache_key = Stats::get()->get_cache_key( 'losing_keywords', Stats::get()->days . 'days' );
$cache = get_transient( $cache_key );
if ( false !== $cache ) {
return $cache;
}
// Get most recent day's keywords only.
$keywords = $this->get_recent_keywords();
$keywords = wp_list_pluck( $keywords, 'query' );
$keywords = array_map( 'strtolower', $keywords );
$data = Stats::get()->get_analytics_data(
[
'dimension' => 'query',
'where' => 'WHERE COALESCE( ROUND( t1.position - COALESCE( t2.position, 100 ), 0 ), 0 ) > 0',
]
);
$history = $this->get_graph_data_for_keywords( \array_keys( $data ) );
$data = Stats::get()->set_query_position( $data, $history );
set_transient( $cache_key, $data, DAY_IN_SECONDS );
return $data;
}
/**
* Get keywords graph data.
*
* @param array $keywords Keywords to get data for.
*
* @return array
*/
public function get_graph_data_for_keywords( $keywords ) {
global $wpdb;
$intervals = Stats::get()->get_intervals();
$sql_daterange = Stats::get()->get_sql_date_intervals( $intervals );
$keywords = \array_map( 'esc_sql', $keywords );
$keywords = '(\'' . join( '\', \'', $keywords ) . '\')';
$query = $wpdb->prepare(
"SELECT a.query, a.position, t.date, t.range_group
FROM {$wpdb->prefix}rank_math_analytics_gsc AS a
INNER JOIN
(SELECT query, DATE_FORMAT(created, '%%Y-%%m-%%d') as date, MAX(id) as id, {$sql_daterange}
FROM {$wpdb->prefix}rank_math_analytics_gsc
WHERE created BETWEEN %s AND %s
AND query IN {$keywords}
GROUP BY query, range_group
ORDER BY created ASC) AS t ON a.id = t.id
",
Stats::get()->start_date,
Stats::get()->end_date
);
$data = $wpdb->get_results( $query );
// phpcs:enable
$data = Stats::get()->filter_graph_rows( $data );
return array_map( [ Stats::get(), 'normalize_graph_rows' ], $data );
}
/**
* Get pages by 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 get_keyword_pages( WP_REST_Request $request ) {
global $wpdb;
$query = $wpdb->prepare(
"SELECT DISTINCT g.page
FROM {$wpdb->prefix}rank_math_analytics_gsc as g
WHERE g.query = %s AND g.created BETWEEN %s AND %s
ORDER BY g.created DESC
LIMIT 5",
$request->get_param( 'query' ),
Stats::get()->start_date,
Stats::get()->end_date
);
$data = $wpdb->get_results( $query ); // phpcs:ignore
$pages = wp_list_pluck( $data, 'page' );
$console = Stats::get()->get_analytics_data(
[
'objects' => true,
'pageview' => true,
'sub_where' => " AND page IN ('" . join( "', '", $pages ) . "')",
]
);
return $console;
}
/**
* Add focus keywords to Rank Tracker.
*
* @param int $post_id Post ID.
* @return mixed
*/
public function add_post_focus_keyword( $post_id ) {
if ( wp_is_post_revision( $post_id ) ) {
return;
}
$auto_add_fks = Helper::get_settings( 'general.auto_add_focus_keywords', [] );
if (
empty( $auto_add_fks['enable_auto_import'] ) ||
empty( $auto_add_fks['post_types'] ) ||
! in_array( get_post_type( $post_id ), $auto_add_fks['post_types'], true )
) {
return;
}
$focus_keyword = Helper::get_post_meta( 'focus_keyword', $post_id );
if ( empty( $focus_keyword ) ) {
return;
}
$keywords_data = [];
$keywords = explode( ',', $focus_keyword );
if ( ! empty( $auto_add_fks['secondary_keyword'] ) ) {
$keywords_data = $keywords;
} else {
$keywords_data[] = current( $keywords );
}
DB::bulk_insert_query_focus_keyword_data( $keywords_data );
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* The Analytics Module
*
* @since 2.0.0
* @package RankMathPro
* @subpackage RankMathPro\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Links class.
*/
class Links {
/**
* Get links by post.
*
* @param array $objects Array of ids.
* @return array
*/
public static function get_links_by_objects( $objects ) {
if ( empty( $objects ) || empty( $objects['rows'] ) ) {
return [];
}
if ( ! Helper::is_module_active( 'link-counter' ) ) {
return $objects;
}
$ids = wp_list_pluck( $objects['rows'], 'object_id' );
$links = DB::links()
->whereIn( 'object_id', \array_unique( $ids ) )
->get( ARRAY_A );
$ids = array_flip( $ids );
foreach ( $links as $link ) {
$post_id = $link['object_id'];
$object_id = $ids[ $post_id ];
$objects['rows'][ $object_id ]['links'] = [
'internal' => $link['internal_link_count'],
'external' => $link['external_link_count'],
'incoming' => $link['incoming_link_count'],
];
}
return $objects;
}
}

View File

@@ -0,0 +1,201 @@
<?php
/**
* The Analytics Module
*
* @since 2.0.0
* @package RankMathPro
* @subpackage RankMathPro\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics;
use RankMath\Analytics\Stats;
use MyThemeShop\Helpers\DB as DB_Helper;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Pageviews class.
*/
class Pageviews {
/**
* Get page views for pages.
*
* @param array $args Array of urls.
*
* @return array
*/
public static function get_pageviews( $args = [] ) {
global $wpdb;
$args = wp_parse_args(
$args,
[
'order' => 'DESC',
'orderBy' => 't1.pageviews',
'where' => '',
'sub_where' => '',
'dates' => ' AND created BETWEEN %s AND %s',
'limit' => '',
'pages' => '',
]
);
if ( empty( $args['pages'] ) ) {
return [
'rows' => [],
'rowsFound' => 0,
];
}
$args['pages'] = ' AND page IN (\'' . join( '\', \'', $args['pages'] ) . '\')';
$pages = $args['pages'];
$where = $args['where'];
$limit = $args['limit'];
$dates = $args['dates'];
$sub_where = $args['sub_where'];
$order = sprintf( 'ORDER BY %s %s', $args['orderBy'], $args['order'] );
// phpcs:disable
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT SQL_CALC_FOUND_ROWS t1.page as page, COALESCE( t1.pageviews, 0 ) as pageviews, COALESCE( t1.pageviews - t2.pageviews, 0 ) as difference
FROM ( SELECT page, SUM(pageviews) as pageviews FROM {$wpdb->prefix}rank_math_analytics_ga WHERE 1=1{$pages}{$dates}{$sub_where} GROUP BY page ) as t1
LEFT JOIN ( SELECT page, SUM(pageviews) as pageviews FROM {$wpdb->prefix}rank_math_analytics_ga WHERE 1=1{$pages}{$dates}{$sub_where} GROUP BY page ) as t2
ON t1.page = t2.page
{$where}
{$order}
{$limit}",
Stats::get()->start_date,
Stats::get()->end_date,
Stats::get()->compare_start_date,
Stats::get()->compare_end_date
),
ARRAY_A
);
$rowsFound = $wpdb->get_var( 'SELECT FOUND_ROWS()' );
// phpcs:enable
return \compact( 'rows', 'rowsFound' );
}
/**
* Get page views for pages.
*
* @param array $args Array of urls.
*
* @return array
*/
public static function get_pageviews_with_object( $args = [] ) {
global $wpdb;
Helper::enable_big_selects_for_queries();
$args = wp_parse_args(
$args,
[
'order' => 'DESC',
'dates' => ' AND created BETWEEN %s AND %s',
'limit' => '',
'sub_where' => '',
]
);
$order = $args['order'];
$limit = $args['limit'];
$dates = $args['dates'];
$subwhere = $args['sub_where'];
// phpcs:disable
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT SQL_CALC_FOUND_ROWS o.*, COALESCE( traffic.pageviews, 0 ) as pageviews, COALESCE( traffic.difference, 0 ) as difference
FROM {$wpdb->prefix}rank_math_analytics_objects as o
LEFT JOIN (SELECT t1.page as page, COALESCE( t1.pageviews, 0 ) as pageviews, COALESCE( t1.pageviews - t2.pageviews, 0 ) as difference
FROM
( SELECT page, SUM(pageviews) as pageviews FROM {$wpdb->prefix}rank_math_analytics_ga WHERE 1=1{$dates} GROUP BY page ) as t1
LEFT JOIN
( SELECT page, SUM(pageviews) as pageviews FROM {$wpdb->prefix}rank_math_analytics_ga WHERE 1=1{$dates} GROUP BY page ) as t2
ON t1.page = t2.page ) traffic ON o.page = traffic.page
WHERE o.is_indexable = '1'{$subwhere}
ORDER BY pageviews {$order}
{$limit}",
Stats::get()->start_date,
Stats::get()->end_date,
Stats::get()->compare_start_date,
Stats::get()->compare_end_date
),
ARRAY_A
);
$rowsFound = $wpdb->get_var( 'SELECT FOUND_ROWS()' );
// phpcs:enable
return \compact( 'rows', 'rowsFound' );
}
/**
* Get pageviews for single post by post Id.
*
* @param array $post_ids Post IDs.
*
* @return array
*/
public static function get_traffic_by_object_ids( $post_ids ) {
if ( ! DB_Helper::check_table_exists( 'rank_math_analytics_ga' ) ) {
return [];
}
global $wpdb;
$placeholder = implode( ', ', array_fill( 0, count( $post_ids ), '%d' ) );
// phpcs:disable
$data = $wpdb->get_results(
$wpdb->prepare(
"SELECT t2.object_id, SUM(t1.pageviews) AS traffic FROM {$wpdb->prefix}rank_math_analytics_ga AS t1
Left JOIN {$wpdb->prefix}rank_math_analytics_objects AS t2 ON t1.page=t2.page
WHERE t2.object_id IN ( {$placeholder} ) and t1.created BETWEEN Now() - interval 36 day and Now() - interval 3 day
GROUP BY t2.object_id",
$post_ids
),
ARRAY_A
);
// phpcs:enable
return array_combine( array_column( $data, 'object_id' ), array_column( $data, 'traffic' ) );
}
/**
* Get pageviews for single post by post Id.
*
* @param array $post_ids Post IDs.
*
* @return array
*/
public static function get_impressions_by_object_ids( $post_ids ) {
if ( ! DB_Helper::check_table_exists( 'rank_math_analytics_gsc' ) ) {
return [];
}
global $wpdb;
$placeholder = implode( ', ', array_fill( 0, count( $post_ids ), '%d' ) );
// phpcs:disable
$data = $wpdb->get_results(
$wpdb->prepare(
"SELECT t2.object_id, SUM(impressions) AS traffic FROM {$wpdb->prefix}rank_math_analytics_gsc AS t1
Left JOIN {$wpdb->prefix}rank_math_analytics_objects AS t2 ON t1.page=t2.page
WHERE t2.object_id IN ( {$placeholder} ) and t1.created BETWEEN Now() - interval 36 day and Now() - interval 3 day
GROUP BY t2.object_id",
$post_ids
),
ARRAY_A
);
// phpcs:enable
return array_combine( array_column( $data, 'object_id' ), array_column( $data, 'traffic' ) );
}
}

View File

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

View File

@@ -0,0 +1,535 @@
<?php
/**
* The Global functionality of the plugin.
*
* Defines the functionality loaded on admin.
*
* @since 1.0.15
* @package RankMathPro
* @subpackage RankMathPro\Rest
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics;
use WP_Error;
use WP_REST_Server;
use RankMath\Helper;
use WP_REST_Request;
use WP_REST_Controller;
use RankMath\Admin\Admin_Helper;
use RankMathPro\Google\PageSpeed;
use RankMath\SEO_Analysis\SEO_Analyzer;
use RankMathPro\Analytics\DB;
use MyThemeShop\Helpers\DB as DB_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() {
register_rest_route(
$this->namespace,
'/getKeywordPages',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ Keywords::get(), 'get_keyword_pages' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/postsOverview',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_posts_overview' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/getTrackedKeywords',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_tracked_keywords' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/getTrackedKeywordsRows',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_tracked_keywords_rows' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/getTrackedKeywordSummary',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_tracked_keyword_summary' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/trackedKeywordsOverview',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_tracked_keywords_overview' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/addTrackKeyword',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'add_track_keyword' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/autoAddFocusKeywords',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'auto_add_focus_keywords' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/removeTrackKeyword',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'remove_track_keyword' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/deleteTrackedKeywords',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'delete_all_tracked_keywords' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/getPagespeed',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'get_pagespeed' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/postsRows',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ Posts::get(), 'get_posts_rows' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/inspectionStats',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_inspection_stats' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
}
/**
* Determines if the current user can manage analytics.
*
* @return true
*/
public function has_permission() {
return current_user_can( 'rank_math_analytics' );
}
/**
* Get top 5 winning and losing posts rows.
*
* @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_overview( WP_REST_Request $request ) {
return rest_ensure_response(
[
'winningPosts' => Posts::get()->get_winning_posts(),
'losingPosts' => Posts::get()->get_losing_posts(),
]
);
}
/**
* Get tracked keywords rows.
*
* @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_tracked_keywords( WP_REST_Request $request ) {
return rest_ensure_response(
[ 'rows' => Keywords::get()->get_tracked_keywords() ]
);
}
/**
* Get tracked keywords rows.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return array
*/
public function get_tracked_keywords_rows( WP_REST_Request $request ) {
return Keywords::get()->get_tracked_keywords_rows( $request );
}
/**
* Get tracked keywords 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_tracked_keyword_summary( WP_REST_Request $request ) {
\RankMathPro\Admin\Api::get()->get_settings();
return rest_ensure_response( Keywords::get()->get_tracked_keywords_summary() );
}
/**
* Get top 5 winning and losing tracked keywords overview.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_tracked_keywords_overview( WP_REST_Request $request ) {
return rest_ensure_response(
[
'winningKeywords' => Keywords::get()->get_tracked_winning_keywords(),
'losingKeywords' => Keywords::get()->get_tracked_losing_keywords(),
]
);
}
/**
* Add track keyword to DB.
*
* @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 auto_add_focus_keywords( WP_REST_Request $request ) {
$data = $request->get_param( 'data' );
$secondary_keyword = ! empty( $data['secondary_keyword'] );
$post_types = ! empty( $data['post_types'] ) ? $data['post_types'] : [];
$all_opts = rank_math()->settings->all_raw();
$general = $all_opts['general'];
$general['auto_add_focus_keywords'] = $data;
Helper::update_all_settings( $general, null, null );
if ( empty( $post_types ) ) {
return false;
}
global $wpdb;
$focus_keywords = $wpdb->get_col(
"SELECT {$wpdb->postmeta}.meta_value FROM {$wpdb->posts} INNER JOIN {$wpdb->postmeta}
ON {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id
WHERE 1=1
AND {$wpdb->posts}.post_type IN ('" . implode( "', '", esc_sql( $post_types ) ) . "')
AND {$wpdb->posts}.post_status = 'publish'
AND {$wpdb->postmeta}.meta_key = 'rank_math_focus_keyword'
"
);
$keywords_data = [];
foreach ( $focus_keywords as $focus_keyword ) {
$keywords = explode( ',', mb_strtolower( $focus_keyword ) );
if ( $secondary_keyword ) {
$keywords_data = array_merge( $keywords, $keywords_data );
} else {
$keywords_data[] = current( $keywords );
}
}
if ( empty( $keywords_data ) ) {
return false;
}
return DB::bulk_insert_query_focus_keyword_data( array_unique( $keywords_data ) );
}
/**
* Add track keyword to DB.
*
* @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 add_track_keyword( WP_REST_Request $request ) {
$keywords = $request->get_param( 'keyword' );
$keywords = mb_strtolower( filter_var( $keywords, FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_NO_ENCODE_QUOTES ) );
if ( empty( $keywords ) ) {
return new WP_Error(
'param_value_empty',
esc_html__( 'Sorry, no keyword found.', 'rank-math-pro' )
);
}
$keywords = html_entity_decode( $keywords );
// Check remain keywords count can be added.
$total_keywords = Keywords::get()->get_tracked_keywords_count();
$new_keywords = Keywords::get()->extract_addable_track_keyword( $keywords );
$keywords_count = count( $new_keywords );
if ( $keywords_count <= 0 ) {
return false;
}
$summary = Keywords::get()->get_tracked_keywords_quota();
$remain = $summary['available'] - $total_keywords;
if ( $remain <= 0 ) {
return false;
}
// Add keywords.
Keywords::get()->add_track_keyword( $new_keywords );
$registered = Admin_Helper::get_registration_data();
if ( ! $registered || empty( $registered['username'] ) || empty( $registered['api_key'] ) ) {
return false;
}
// Send total keywords count to RankMath.
$total_keywords = Keywords::get()->get_tracked_keywords_count();
$response = \RankMathPro\Admin\Api::get()->keywords_info( $registered['username'], $registered['api_key'], $total_keywords );
if ( $response ) {
update_option( 'rank_math_keyword_quota', $response );
}
return true;
}
/**
* Remove track keyword from DB.
*
* @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 remove_track_keyword( WP_REST_Request $request ) {
$keyword = $request->get_param( 'keyword' );
if ( empty( $keyword ) ) {
return new WP_Error(
'param_value_empty',
esc_html__( 'Sorry, no keyword found.', 'rank-math-pro' )
);
}
// Remove keyword.
Keywords::get()->remove_track_keyword( $keyword );
$registered = Admin_Helper::get_registration_data();
if ( ! $registered || empty( $registered['username'] ) || empty( $registered['api_key'] ) ) {
return false;
}
// Send total keywords count to RankMath.
$total_keywords = Keywords::get()->get_tracked_keywords_count();
$response = \RankMathPro\Admin\Api::get()->keywords_info( $registered['username'], $registered['api_key'], $total_keywords );
if ( $response ) {
update_option( 'rank_math_keyword_quota', $response );
}
return true;
}
/**
* Delete all the manually tracked keywords.
*/
public function delete_all_tracked_keywords() {
// Delete all keywords.
Keywords::get()->delete_all_tracked_keywords();
$registered = Admin_Helper::get_registration_data();
if ( empty( $registered['username'] ) || empty( $registered['api_key'] ) ) {
return false;
}
// Send total keywords count as 0 to RankMath.
$response = \RankMathPro\Admin\Api::get()->keywords_info( $registered['username'], $registered['api_key'], 0 );
if ( $response ) {
update_option( 'rank_math_keyword_quota', $response );
}
return true;
}
/**
* Check if keyword can be added.
*
* @param string $keywords Comma separated keywords.
* @return bool True if remain keyword count is larger than zero.
*/
private function can_add_keyword( $keywords = '' ) {
// Check remain keywords count can be added by supposing current keyword is added.
$total_keywords = Keywords::get()->get_tracked_keywords_count();
$new_keywords = Keywords::get()->extract_addable_track_keyword( $keywords );
$keywords_count = count( $new_keywords );
$summary = Keywords::get()->get_tracked_keywords_quota();
$remain = $summary['available'] - $total_keywords - $keywords_count;
return $remain >= 0;
}
/**
* Get page speed data.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return array|bool Pagespeed info on success, false on failure.
*/
public function get_pagespeed( WP_REST_Request $request ) {
$id = $request->get_param( 'id' );
if ( empty( $id ) ) {
return new WP_Error(
'param_value_empty',
esc_html__( 'Sorry, no record id found.', 'rank-math-pro' )
);
}
$post_id = $request->get_param( 'objectID' );
if ( empty( $id ) ) {
return new WP_Error(
'param_value_empty',
esc_html__( 'Sorry, no post id found.', 'rank-math-pro' )
);
}
if ( Helper::is_localhost() ) {
return [
'page_score' => 0,
'desktop_interactive' => 0,
'desktop_pagescore' => 0,
'mobile_interactive' => 0,
'mobile_pagescore' => 0,
'pagespeed_refreshed' => current_time( 'mysql' ),
];
}
$url = get_permalink( $post_id );
$pre = apply_filters( 'rank_math/analytics/pre_pagespeed', false, $post_id, $request );
if ( false !== $pre ) {
return $pre;
}
$force = \boolval( $request->get_param( 'force' ) );
$is_admin_bar = \boolval( $request->get_param( 'isAdminBar' ) );
if ( $force || ( ! $is_admin_bar && $this->should_update_pagespeed( $id ) ) ) {
// Page Score.
$analyzer = new SEO_Analyzer();
$analyzer->set_url();
$score = $analyzer->get_page_score( $url );
$update = [];
if ( $score > 0 ) {
$update['page_score'] = $score;
}
// PageSpeed desktop.
$desktop = PageSpeed::get_pagespeed( $url, 'desktop' );
if ( ! empty( $desktop ) ) {
$update = \array_merge( $update, $desktop );
$update['pagespeed_refreshed'] = current_time( 'mysql' );
}
// PageSpeed mobile.
$mobile = PageSpeed::get_pagespeed( $url, 'mobile' );
if ( ! empty( $mobile ) ) {
$update = \array_merge( $update, $mobile );
$update['pagespeed_refreshed'] = current_time( 'mysql' );
}
}
if ( ! empty( $update ) ) {
$update['id'] = $id;
$update['object_id'] = $post_id;
DB::update_object( $update );
}
return empty( $update ) ? false : $update;
}
/**
* 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 ) );
}
/**
* Get inspection stats.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_inspection_stats() {
// Early Bail!!
if ( ! DB_Helper::check_table_exists( 'rank_math_analytics_inspections' ) ) {
return [
'presence' => [],
'status' => [],
];
}
return rest_ensure_response(
[
'presence' => Url_Inspection::get_presence_stats(),
'status' => Url_Inspection::get_status_stats(),
]
);
}
}

View File

@@ -0,0 +1,285 @@
<?php
/**
* The Analytics Module
*
* @since 2.0.0
* @package RankMathPro
* @subpackage RankMathPro\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Analytics\Stats;
use RankMath\Admin\Admin_Helper;
use RankMathPro\Admin\Admin_Helper as ProAdminHelper;
use MyThemeShop\Helpers\DB as DB_Helper;
defined( 'ABSPATH' ) || exit;
/**
* Summary class.
*/
class Summary {
use Hooker;
/**
* Constructor.
*/
public function __construct() {
if ( \RankMathPro\Google\Adsense::is_adsense_connected() ) {
$this->filter( 'rank_math/analytics/summary', 'get_adsense_summary' );
}
if ( \RankMath\Google\Analytics::is_analytics_connected() ) {
$this->filter( 'rank_math/analytics/summary', 'get_pageviews_summary' );
$this->filter( 'rank_math/analytics/get_widget', 'get_pageviews_summary' );
}
$this->filter( 'rank_math/analytics/summary', 'get_clicks_summary' );
$this->filter( 'rank_math/analytics/summary', 'get_g_update_summary' );
$this->filter( 'rank_math/analytics/posts_summary', 'get_posts_summary', 10, 3 );
$this->filter( 'rank_math/analytics/analytics_summary_graph', 'get_analytics_summary_graph', 10, 2 );
$this->filter( 'rank_math/analytics/analytics_tables_info', 'get_analytics_tables_info' );
}
/**
* Get posts summary.
*
* @param object $summary Posts summary.
* @param string $post_type Post type.
* @param string $query Query to get the summary data.
* @return object
*/
public function get_posts_summary( $summary, $post_type, $query ) {
if ( empty( $summary ) ) {
return $summary;
}
if ( $post_type && is_string( $post_type ) ) {
global $wpdb;
$query->leftJoin( $wpdb->prefix . 'rank_math_analytics_objects', $wpdb->prefix . 'rank_math_analytics_gsc.page', $wpdb->prefix . 'rank_math_analytics_objects.page' );
$query->where( $wpdb->prefix . 'rank_math_analytics_objects.object_subtype', sanitize_key( $post_type ) );
$summary = (object) $query->one();
}
$summary->pageviews = DB::traffic()
->selectSum( 'pageviews', 'pageviews' )
->whereBetween( 'created', [ Stats::get()->start_date, Stats::get()->end_date ] )
->getVar();
return $summary;
}
/**
* Get pageviews summary.
*
* @param object $stats Stats holder.
* @return object
*/
public function get_pageviews_summary( $stats ) {
$pageviews = DB::traffic()
->selectSum( 'pageviews', 'pageviews' )
->whereBetween( 'created', [ Stats::get()->start_date, Stats::get()->end_date ] )
->getVar();
$old_pageviews = DB::traffic()
->selectSum( 'pageviews', 'pageviews' )
->whereBetween( 'created', [ Stats::get()->compare_start_date, Stats::get()->compare_end_date ] )
->getVar();
$stats->pageviews = [
'total' => (int) $pageviews,
'previous' => (int) $old_pageviews,
'difference' => (int) $pageviews - (int) $old_pageviews,
];
return $stats;
}
/**
* Get adsense summary.
*
* @param object $stats Stats holder.
* @return object
*/
public function get_adsense_summary( $stats ) {
$stats->adsense = [
'total' => 0,
'previous' => 0,
'difference' => 0,
];
if ( DB_Helper::check_table_exists( 'rank_math_analytics_adsense' ) ) {
$earnings = DB::adsense()
->selectSum( 'earnings', 'earnings' )
->whereBetween( 'created', [ Stats::get()->start_date, Stats::get()->end_date ] )
->getVar();
$old_earnings = DB::adsense()
->selectSum( 'earnings', 'earnings' )
->whereBetween( 'created', [ Stats::get()->compare_start_date, Stats::get()->compare_end_date ] )
->getVar();
$stats->adsense = [
'total' => (int) $earnings,
'previous' => (int) $old_earnings,
'difference' => (int) $earnings - (int) $old_earnings,
];
}
return $stats;
}
/**
* Get analytics and adsense graph data.
*
* @param object $data Graph data.
* @param array $intervals Date intervals.
* @return array
*/
public function get_analytics_summary_graph( $data, $intervals ) {
global $wpdb;
if ( \RankMath\Google\Analytics::is_analytics_connected() ) {
$data->traffic = $this->get_traffic_graph( $intervals );
// Convert types.
$data->traffic = array_map( [ Stats::get(), 'normalize_graph_rows' ], $data->traffic );
// Merge for performance.
$data->merged = Stats::get()->get_merge_data_graph( $data->traffic, $data->merged, $intervals['map'] );
}
if ( \RankMathPro\Google\Adsense::is_adsense_connected() ) {
$data->adsense = $this->get_adsense_graph( $intervals );
// Convert types.
$data->adsense = array_map( [ Stats::get(), 'normalize_graph_rows' ], $data->adsense );
// Merge for performance.
$data->merged = Stats::get()->get_merge_data_graph( $data->adsense, $data->merged, $intervals['map'] );
}
return $data;
}
/**
* Get analytics graph data.
*
* @param array $intervals Date intervals.
* @return array
*/
public function get_traffic_graph( $intervals ) {
global $wpdb;
$sql_daterange = Stats::get()->get_sql_date_intervals( $intervals );
$query = $wpdb->prepare(
"SELECT DATE_FORMAT( created, '%%Y-%%m-%%d') as date, SUM(pageviews) as pageviews, {$sql_daterange}
FROM {$wpdb->prefix}rank_math_analytics_ga
WHERE created BETWEEN %s AND %s
GROUP BY range_group",
Stats::get()->start_date,
Stats::get()->end_date
);
$traffic_data = $wpdb->get_results( $query );
// phpcs:enable
return $traffic_data;
}
/**
* Get adsense graph data.
*
* @param array $intervals Date intervals.
* @return array
*/
public function get_adsense_graph( $intervals ) {
global $wpdb;
$adsense_data = [];
if ( DB_Helper::check_table_exists( 'rank_math_analytics_adsense' ) ) {
$sql_daterange = Stats::get()->get_sql_date_intervals( $intervals );
$query = $wpdb->prepare(
"SELECT DATE_FORMAT( created, '%%Y-%%m-%%d') as date, SUM(earnings) as earnings, {$sql_daterange}
FROM {$wpdb->prefix}rank_math_analytics_adsense
WHERE created BETWEEN %s AND %s
GROUP BY range_group",
Stats::get()->start_date,
Stats::get()->end_date
);
$adsense_data = $wpdb->get_results( $query );
// phpcs:enable
}
return $adsense_data;
}
/**
* Get clicks summary.
*
* @param object $stats Stats holder.
* @return object
*/
public function get_clicks_summary( $stats ) {
$clicks = DB::analytics()
->selectSum( 'clicks', 'clicks' )
->whereBetween( 'created', [ Stats::get()->start_date, Stats::get()->end_date ] )
->getVar();
$old_clicks = DB::analytics()
->selectSum( 'clicks', 'clicks' )
->whereBetween( 'created', [ Stats::get()->compare_start_date, Stats::get()->compare_end_date ] )
->getVar();
$stats->clicks = [
'total' => (int) $clicks,
'previous' => (int) $old_clicks,
'difference' => $clicks - $old_clicks,
];
return $stats;
}
/**
* Get google update summary.
*
* @param object $stats Stats holder.
* @return object
*/
public function get_g_update_summary( $stats ) {
if ( ! Helper::get_settings( 'general.google_updates' ) && ProAdminHelper::is_business_plan() ) {
$stats->graph->g_updates = null;
return $stats;
}
$stored = get_site_option( 'rank_math_pro_google_updates' );
$g_updates = json_decode( $stored );
$stats->graph->g_updates = $g_updates;
return $stats;
}
/**
* Get analytics tables info
*
* @param array $data Analytics tables info.
* @return array
*/
public function get_analytics_tables_info( $data ) {
$pro_data = DB::info();
$days = $data['days'] + $pro_data['days'];
$rows = $data['rows'] + $pro_data['rows'];
$size = $data['size'] + $pro_data['size'];
$data = compact( 'days', 'rows', 'size' );
return $data;
}
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* URL Inspection features.
*
* @since 3.0.8
* @package RankMathPro
* @subpackage RankMathPro
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMathPro\Analytics\DB;
/**
* Url_Inspection class.
*/
class Url_Inspection {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->filter( 'rank_math/analytics/url_inspection_map_properties', 'map_inspection_properties', 10, 2 );
$this->action( 'rank_math/analytics/get_inspections_query', 'add_filter_params', 10, 2 );
$this->action( 'rank_math/analytics/get_inspections_count_query', 'add_filter_params', 10, 2 );
$this->filter( 'rank_math/analytics/post_data', 'add_index_verdict_data', 10, 2 );
// Enqueue.
$this->action( 'rank_math/admin/enqueue_scripts', 'enqueue_scripts' );
}
/**
* Filter to alter the where clause used in the get_inspections function.
*
* @param string $where WHERE clause.
* @param array $params Parameters.
*
* @return string
*/
public function add_filter_params( $query, $params ) {
if ( empty( $params['indexingFilter'] ) ) {
return;
}
$table = DB::inspections()->table;
$query->where( "$table.coverage_state", $params['indexingFilter'] );
}
/**
* Map properties in the API result to columns in the database.
*
* @param array $normalized Normalized data.
* @param array $incoming Incoming data from the API.
*
* @return array
*/
public function map_inspection_properties( $normalized, $incoming ) {
$handler = \RankMath\Google\Url_Inspection::get();
$handler->assign_inspection_value( $incoming, 'richResultsResult.detectedItems', 'rich_results_items', $normalized );
$handler->assign_inspection_value( $incoming, 'indexStatusResult.lastCrawlTime', 'last_crawl_time', $normalized );
// Store the raw response, too.
$normalized['raw_api_response'] = wp_json_encode( $incoming );
return $normalized;
}
/**
* Get stats for "Presence on Google" widget.
*/
public static function get_presence_stats() {
return DB::get_presence_stats();
}
/**
* Get stats for "Top Statuses" widget.
*/
public static function get_status_stats() {
return DB::get_status_stats();
}
/**
* Change user perference.
*
* @param array $data array.
* @param WP_REST_Request $request post object.
* @return array $data sorted array.
*/
public function add_index_verdict_data( $data, \WP_REST_Request $request ) {
if ( ! Helper::can_add_index_status() ) {
return $data;
}
$data['indexStatus'] = DB::get_index_verdict( $data['page'] );
return $data;
}
/**
* Enqueue scripts.
*/
public function enqueue_scripts() {
$screen = get_current_screen();
if ( 'rank-math_page_rank-math-analytics' !== $screen->id ) {
return;
}
$submit_url = add_query_arg(
[
'page' => 'instant-indexing',
'tab' => 'console',
'apiaction' => 'update',
'_wpnonce' => wp_create_nonce( 'giapi-action' ),
'apipostid[]' => '',
],
admin_url( 'admin.php' )
);
$settings = get_option( 'rank-math-options-instant-indexing', [] );
Helper::add_json(
'instantIndexingSupport',
[
'isPluginActive' => is_plugin_active( 'fast-indexing-api/instant-indexing.php' ),
'isGoogleConfigured' => ! empty( $settings['json_key'] ),
'submitUrl' => $submit_url,
]
);
}
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* Google AdSense.
*
* @since 1.0.34
* @package RankMathPro
* @subpackage RankMathPro\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Google;
use RankMath\Google\Api;
use RankMath\Helpers\Security;
use RankMath\Analytics\Workflow\Base;
use WP_Error;
defined( 'ABSPATH' ) || exit;
/**
* AdSense class.
*/
class Adsense {
/**
* Get adsense accounts.
*
* @return array
*/
public static function get_adsense_accounts() {
$accounts = [];
$response = Api::get()->http_get( 'https://adsense.googleapis.com/v2/accounts' );
if (
! Api::get()->is_success() ||
isset( $response->error ) ||
! isset( $response['accounts'] ) ||
! is_array( $response['accounts'] )
) {
return $accounts;
}
foreach ( $response['accounts'] as $account ) {
$accounts[ $account['name'] ] = [
'name' => $account['displayName'],
];
}
return $accounts;
}
/**
* Query adsense data from google client api.
*
* @param string $start_date Start date.
* @param string $end_date End date.
*
* @return array
*/
public static function get_adsense( $options = [] ) {
$account_id = isset( $options['account_id'] ) ? $options['account_id'] : self::get_adsense_id();
$start_date = isset( $options['start_date'] ) ? $options['start_date'] : '';
$end_date = isset( $options['end_date'] ) ? $options['end_date'] : '';
if ( ! $account_id || ! $start_date || ! $end_date ) {
return false;
}
$request = Security::add_query_arg_raw(
[
'startDate.year' => gmdate( 'Y', strtotime( $start_date ) ),
'startDate.month' => gmdate( 'n', strtotime( $start_date ) ),
'startDate.day' => gmdate( 'j', strtotime( $start_date ) ),
'endDate.year' => gmdate( 'Y', strtotime( $end_date ) ),
'endDate.month' => gmdate( 'n', strtotime( $end_date ) ),
'endDate.day' => gmdate( 'j', strtotime( $end_date ) ),
'dimensions' => 'DATE',
'currencyCode' => 'USD',
'metrics' => 'ESTIMATED_EARNINGS',
],
'https://adsense.googleapis.com/v2/' . $account_id . '/reports:generate'
);
$workflow = 'adsense';
Api::get()->set_workflow( $workflow );
$response = Api::get()->http_get( $request );
Api::get()->log_failed_request( $response, $workflow, $start_date, func_get_args() );
if ( ! Api::get()->is_success() ) {
return new WP_Error( 'adsense_api_fail', 'Google AdSense API request failed.' );
}
if ( ! isset( $response['rows'] ) ) {
return false;
}
return $response['rows'];
}
/**
* Get adsense id.
*
* @return string
*/
public static function get_adsense_id() {
static $rank_math_adsense_id;
if ( is_null( $rank_math_adsense_id ) ) {
$options = get_option( 'rank_math_google_analytic_options' );
$rank_math_adsense_id = ! empty( $options['adsense_id'] ) ? $options['adsense_id'] : false;
}
return $rank_math_adsense_id;
}
/**
* Is adsense connected.
*
* @return boolean
*/
public static function is_adsense_connected() {
$account = wp_parse_args(
get_option( 'rank_math_google_analytic_options' ),
[ 'adsense_id' => '' ]
);
return ! empty( $account['adsense_id'] );
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* Google PageSpeed.
*
* @since 1.0.34
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Google;
use RankMath\Google\Api;
use RankMath\Helpers\Security;
defined( 'ABSPATH' ) || exit;
/**
* PageSpeed class.
*/
class PageSpeed {
/**
* Get pagespeed score info.
*
* @param string $url Url to get pagespeed for.
* @param string $strategy Data for desktop or mobile.
*
* @return array
*/
public static function get_pagespeed( $url, $strategy = 'desktop' ) {
$response = Api::get()->http_get( 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=PERFORMANCE&url=' . \rawurlencode( $url ) . '&strategy=' . \strtoupper( $strategy ), [], 30 );
if ( ! Api::get()->is_success() ) {
return false;
}
return [
$strategy . '_interactive' => round( \floatval( $response['lighthouseResult']['audits']['interactive']['displayValue'] ), 0 ),
$strategy . '_pagescore' => round( $response['lighthouseResult']['categories']['performance']['score'] * 100, 0 ),
];
}
}

View File

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

View File

@@ -0,0 +1,44 @@
<?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-pro' ); ?></h1>
###TOP_HTML###
<h2 class="report-date">###START_DATE### - ###END_DATE###</h2>
<a href="###SITE_URL###" target="_blank" class="site-url">###SITE_URL_SIMPLE###</a>
</td>
<?php if ( $this->get_setting( 'link_full_report', true ) ) : ?>
<td class="full-report-link">
<a href="###REPORT_URL###" target="_blank" class="full-report-link">
<?php esc_html_e( 'FULL REPORT', 'rank-math-pro' ); ?>
<?php $this->image( 'report-icon-external.png', 12, 12, __( 'External Link Icon', 'rank-math-pro' ) ); ?>
</a>
</td>
<?php endif; ?>
</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-pro' ); ?></h2>
<p><em><?php esc_html_e( 'It seems that there are no stats to show right now.', 'rank-math-pro' ); ?></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-pro' ), '<a href="' . Helper::get_admin_url( 'options-general#setting-panel-analytics' ) . '">', '</a>' ); ?></p>
</td>
</tr>
</table>
<?php } ?>

View File

@@ -0,0 +1,69 @@
<?php
/**
* Analytics Report email styling.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
?>
<style>
.header {
background: ###HEADER_BACKGROUND###;
}
tr.keywords-table-spacer {
height: 14px;
}
table.stats-table tr.table-heading {
background: #243B53;
color: #fff;
font-weight: 500;
}
.stats-table {
overflow: hidden;
}
.stats-table td {
padding: 10px;
}
.stats-table tr:nth-child(2n+1) {
background: #F0F4F8;
}
.stats-table .stat-value {
font-size: 16px;
}
.stats-table .stat-diff, .stats-table .diff-sign {
font-size: 14px;
}
.report-heading {
margin: 40px 0 20px 0;
}
span.post-title, span.post-url {
display: block;
max-width: 250px;
}
.stats-table a {
color: #3f77d6;
font-size: 15px;
font-weight: 600;
}
span.post-url {
color: #92949f;
font-size: 14px;
font-weight: normal;
}
###CUSTOM_CSS###
</style>

View File

@@ -0,0 +1,45 @@
<?php
/**
* Analytics Report email template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
$sections = $this->get_setting( 'sections', [ 'summary', 'positions', 'winning_posts', 'winning_keywords', 'losing_keywords' ] );
$analytics = get_option( 'rank_math_google_analytic_options' );
$is_analytics_connected = ! empty( $analytics ) && ! empty( $analytics['view_id'] );
// Header & optional sections.
$this->template_part( 'header' );
$this->template_part( 'header-after' );
foreach ( $sections as $section ) {
$template = str_replace( '_', '-', $section );
$this->template_part( "sections/{$template}", [ 'analytics_connected' => $is_analytics_connected ] );
}
// phpcs:enable
?>
<?php if ( $this->get_setting( 'link_full_report', true ) ) : ?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="details-button">
<tr class="button">
<td align="left">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left" style="padding-bottom: 0;">
<a href="###REPORT_URL###" target="_blank"><?php esc_html_e( 'VIEW DETAILED ANALYTICS', 'rank-math-pro' ); ?></a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<?php endif; ?>
<?php $this->template_part( 'footer' ); ?>

View File

@@ -0,0 +1,65 @@
<?php
/**
* Analytics Report Losing Keywords.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
use RankMathPro\Analytics\Email_Reports;
use MyThemeShop\Helpers\Str;
defined( 'ABSPATH' ) || exit;
$keywords = (array) $this->get_variable( 'losing_keywords' );
?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="report-heading">
<tr>
<td>
<h2><?php esc_html_e( 'Top Losing Keywords', 'rank-math-pro' ); ?></h2>
</td>
</tr>
</table>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="keywords-table-wrapper">
<tr>
<td>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="keywords-table stats-table losing-keywords">
<tr class="table-heading">
<td class="col-1">
<?php esc_html_e( 'Keywords', 'rank-math-pro' ); ?>
</td>
<td class="col-2">
<?php esc_html_e( 'Impressions', 'rank-math-pro' ); ?>
</td>
<td class="col-3">
<?php esc_html_e( 'Position', 'rank-math-pro' ); ?>
</td>
</tr>
<?php foreach ( $keywords as $keyword => $data ) : ?>
<?php if ( ! is_array( $data ) ) { continue; } ?>
<tr>
<td style="width:280px;box-sizing:border-box;">
<span title="<?php echo esc_html( $keyword ); ?>"><?php echo esc_html( Str::truncate( $keyword, 36, '...' ) ); ?></span>
</td>
<td>
<?php $this->template_part( 'stat', Email_Reports::get_stats_val( $data, 'impressions' ) ); ?>
</td>
<td>
<?php $this->template_part( 'stat', array_merge( Email_Reports::get_stats_val( $data, 'position' ), [ 'invert' => true ] ) ); ?>
</td>
</tr>
<?php endforeach; ?>
<?php if ( empty( $keywords ) ) : ?>
<tr>
<td colspan="3">
<?php esc_html_e( 'No data to show.', 'rank-math-pro' ); ?>
</td>
</tr>
<?php endif; ?>
</table>
</td>
</tr>
</table>

View File

@@ -0,0 +1,73 @@
<?php
/**
* Analytics Report Losing Posts.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
use RankMathPro\Analytics\Email_Reports;
use MyThemeShop\Helpers\Str;
defined( 'ABSPATH' ) || exit;
$posts = (array) $this->get_variable( 'losing_posts' );
?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="report-heading">
<tr>
<td>
<h2><?php esc_html_e( 'Top Losing Posts', 'rank-math-pro' ); ?></h2>
</td>
</tr>
</table>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="traffic-table stats-table">
<tr class="table-heading">
<td class="col-1">
<?php esc_html_e( 'Post', 'rank-math-pro' ); ?>
</td>
<?php if ( ! empty( $analytics_connected ) ) : ?>
<td class="col-2">
<?php esc_html_e( 'Search Traffic', 'rank-math-pro' ); ?>
</td>
<?php else : ?>
<td class="col-2">
<?php esc_html_e( 'Impressions', 'rank-math-pro' ); ?>
</td>
<?php endif; ?>
<td class="col-3">
<?php esc_html_e( 'Position', 'rank-math-pro' ); ?>
</td>
</tr>
<?php foreach ( $posts as $post_url => $data ) : // phpcs:disable ?>
<?php if ( ! is_array( $data ) ) { continue; } ?>
<tr>
<td>
<a href="###SITE_URL###<?php echo esc_attr( $post_url ); ?>" target="_blank">
<span class="post-title"><?php echo esc_html( Str::truncate( ( ! empty( $data['title'] ) ? $data['title'] : $data['page'] ), 55, '...' ) ); ?></span>
<span class="post-url"><?php echo esc_html( Email_Reports::shorten_url( $post_url, 30, '...' ) ); ?></span>
</a>
</td>
<td>
<?php if ( ! empty( $analytics_connected ) ) : ?>
<?php $this->template_part( 'stat', Email_Reports::get_stats_val( $data, 'pageviews' ) ); ?>
<?php else : ?>
<?php $this->template_part( 'stat', Email_Reports::get_stats_val( $data, 'impressions' ) ); ?>
<?php endif; ?>
</td>
<td>
<?php $this->template_part( 'stat', array_merge( Email_Reports::get_stats_val( $data, 'position' ), [ 'invert' => true ] ) ); ?>
</td>
</tr>
<?php endforeach; ?>
<?php if ( empty( $posts ) ) : ?>
<tr>
<td colspan="3">
<?php esc_html_e( 'No data to show.', 'rank-math-pro' ); ?>
</td>
</tr>
<?php endif; ?>
</table>

View File

@@ -0,0 +1,117 @@
<?php
/**
* Analytics Report summary table template.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
defined( 'ABSPATH' ) || exit;
$analytics = get_option( 'rank_math_google_analytic_options' );
$is_analytics_connected = ! empty( $analytics ) && ! empty( $analytics['view_id'] );
?>
<?php if ( $this->get_variable( 'stats_invalid_data' ) ) { ?>
<?php return; ?>
<?php } ?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="stats">
<tr>
<?php if ( $is_analytics_connected ) : ?>
<td class="col-1">
<h3><?php esc_html_e( 'Search Traffic', 'rank-math-pro' ); ?></h3>
<?php
$this->template_part(
'stat',
[
'value' => $this->get_variable( 'stats_traffic' ),
'diff' => $this->get_variable( 'stats_traffic_diff' ),
'graph' => true,
'graph_data' => $this->get_graph_data( 'traffic' ),
]
);
?>
</td>
<?php else : ?>
<td class="col-1">
<h3><?php esc_html_e( 'Total Impressions', 'rank-math-pro' ); ?></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>
<?php endif; ?>
<?php if ( ! $is_analytics_connected ) : ?>
<td class="col-2">
<h3><?php esc_html_e( 'Total Clicks', 'rank-math-pro' ); ?></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>
<?php else : ?>
<td class="col-2">
<h3><?php esc_html_e( 'Total Impressions', 'rank-math-pro' ); ?></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>
<?php endif; ?>
</tr>
<tr>
<td class="col-1">
<h3><?php esc_html_e( 'Total Keywords', 'rank-math-pro' ); ?></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-pro' ); ?></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,
]
);
?>
</td>
</tr>
</table>

View File

@@ -0,0 +1,65 @@
<?php
/**
* Analytics Report Winning Keywords.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
use RankMathPro\Analytics\Email_Reports;
use MyThemeShop\Helpers\Str;
defined( 'ABSPATH' ) || exit;
$keywords = (array) $this->get_variable( 'winning_keywords' );
?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="report-heading">
<tr>
<td>
<h2><?php esc_html_e( 'Top Winning Keywords', 'rank-math-pro' ); ?></h2>
</td>
</tr>
</table>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="keywords-table-wrapper">
<tr>
<td>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="keywords-table stats-table winning-keywords">
<tr class="table-heading">
<td class="col-1">
<?php esc_html_e( 'Keywords', 'rank-math-pro' ); ?>
</td>
<td class="col-2">
<?php esc_html_e( 'Impressions', 'rank-math-pro' ); ?>
</td>
<td class="col-3">
<?php esc_html_e( 'Position', 'rank-math-pro' ); ?>
</td>
</tr>
<?php foreach ( $keywords as $keyword => $data ) : ?>
<?php if ( ! is_array( $data ) ) { continue; } ?>
<tr>
<td style="width:280px;box-sizing:border-box;">
<span title="<?php echo esc_html( $keyword ); ?>"><?php echo esc_html( Str::truncate( $keyword, 36, '...' ) ); ?></span>
</td>
<td>
<?php $this->template_part( 'stat', Email_Reports::get_stats_val( $data, 'impressions' ) ); ?>
</td>
<td>
<?php $this->template_part( 'stat', array_merge( Email_Reports::get_stats_val( $data, 'position' ), [ 'invert' => true ] ) ); ?>
</td>
</tr>
<?php endforeach; ?>
<?php if ( empty( $keywords ) ) : ?>
<tr>
<td colspan="3">
<?php esc_html_e( 'No data to show.', 'rank-math-pro' ); ?>
</td>
</tr>
<?php endif; ?>
</table>
</td>
</tr>
</table>

View File

@@ -0,0 +1,73 @@
<?php
/**
* Analytics Report Winning Posts.
*
* @package RankMath
* @subpackage RankMath\Admin
*/
use RankMathPro\Analytics\Email_Reports;
use MyThemeShop\Helpers\Str;
defined( 'ABSPATH' ) || exit;
$posts = (array) $this->get_variable( 'winning_posts' );
?>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="report-heading">
<tr>
<td>
<h2><?php esc_html_e( 'Top Winning Posts', 'rank-math-pro' ); ?></h2>
</td>
</tr>
</table>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="traffic-table stats-table">
<tr class="table-heading">
<td class="col-1">
<?php esc_html_e( 'Post', 'rank-math-pro' ); ?>
</td>
<?php if ( ! empty( $analytics_connected ) ) : ?>
<td class="col-2">
<?php esc_html_e( 'Search Traffic', 'rank-math-pro' ); ?>
</td>
<?php else : ?>
<td class="col-2">
<?php esc_html_e( 'Impressions', 'rank-math-pro' ); ?>
</td>
<?php endif; ?>
<td class="col-3">
<?php esc_html_e( 'Position', 'rank-math-pro' ); ?>
</td>
</tr>
<?php foreach ( $posts as $post_url => $data ) : // phpcs:disable ?>
<?php if ( ! is_array( $data ) ) { continue; } ?>
<tr>
<td>
<a href="###SITE_URL###<?php echo esc_attr( $post_url ); ?>" target="_blank">
<span class="post-title"><?php echo esc_html( Str::truncate( ( ! empty( $data['title'] ) ? $data['title'] : $data['page'] ), 55, '...' ) ); ?></span>
<span class="post-url"><?php echo esc_html( Email_Reports::shorten_url( $post_url, 30, '...' ) ); ?></span>
</a>
</td>
<td>
<?php if ( ! empty( $analytics_connected ) ) : ?>
<?php $this->template_part( 'stat', Email_Reports::get_stats_val( $data, 'pageviews' ) ); ?>
<?php else : ?>
<?php $this->template_part( 'stat', Email_Reports::get_stats_val( $data, 'impressions' ) ); ?>
<?php endif; ?>
</td>
<td>
<?php $this->template_part( 'stat', array_merge( Email_Reports::get_stats_val( $data, 'position' ), [ 'invert' => true ] ) ); ?>
</td>
</tr>
<?php endforeach; ?>
<?php if ( empty( $posts ) ) : ?>
<tr>
<td colspan="3">
<?php esc_html_e( 'No data to show.', 'rank-math-pro' ); ?>
</td>
</tr>
<?php endif; ?>
</table>

View File

@@ -0,0 +1,93 @@
<?php
/**
* Google Adsense.
*
* @since 1.0.49
* @package RankMathPro
* @subpackage RankMathPro\Adsense
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics\Workflow;
use Exception;
use MyThemeShop\Helpers\DB;
use RankMath\Analytics\Workflow\Base;
use function as_unschedule_all_actions;
defined( 'ABSPATH' ) || exit;
/**
* Adsense class.
*/
class Adsense extends Base {
/**
* Constructor.
*/
public function __construct() {
// If adsense is not connected, no need to proceed.
if ( ! \RankMathPro\Google\Adsense::is_adsense_connected() ) {
return;
}
$this->action( 'rank_math/analytics/workflow/adsense', 'kill_jobs', 5, 0 );
$this->action( 'rank_math/analytics/workflow/create_tables', 'create_tables' );
$this->action( 'rank_math/analytics/workflow/adsense', 'create_tables', 6, 0 );
$this->action( 'rank_math/analytics/workflow/adsense', 'create_data_jobs', 10, 3 );
}
/**
* Kill jobs.
*
* Stop processing queue items, clear cronjob and delete all batches.
*/
public function kill_jobs() {
as_unschedule_all_actions( 'rank_math/analytics/get_adsense_data' );
}
/**
* Create tables.
*/
public function create_tables() {
global $wpdb;
$collate = $wpdb->get_charset_collate();
$table = 'rank_math_analytics_adsense';
// Early Bail!!
if ( DB::check_table_exists( $table ) ) {
return;
}
$schema = "CREATE TABLE {$wpdb->prefix}{$table} (
id bigint(20) unsigned NOT NULL auto_increment,
created timestamp NOT NULL,
earnings double NOT NULL default 0,
PRIMARY KEY (id)
) $collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
try {
dbDelta( $schema );
} catch ( Exception $e ) { // phpcs:ignore
// Will log.
}
}
/**
* Create jobs to fetch data.
*
* @param integer $days Number of days to fetch from past.
* @param string $prev Previous saved value.
* @param string $new New posted value.
*/
public function create_data_jobs( $days, $prev, $new ) {
// If saved and new profile are same.
if ( ! $this->is_profile_updated( 'adsense_id', $prev, $new ) ) {
return;
}
$this->schedule_single_action( $days, 'adsense' );
}
}

View File

@@ -0,0 +1,98 @@
<?php
/**
* Google Analytics.
*
* @since 1.0.49
* @package RankMathPro
* @subpackage RankMathPro\Analytics
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics\Workflow;
use Exception;
use MyThemeShop\Helpers\DB;
use RankMath\Analytics\Workflow\Base;
use function as_unschedule_all_actions;
defined( 'ABSPATH' ) || exit;
/**
* Analytics class.
*/
class Analytics extends Base {
/**
* Constructor.
*/
public function __construct() {
// If analytics is not connected, no need to proceed.
if ( ! \RankMath\Google\Analytics::is_analytics_connected() ) {
return;
}
$this->action( 'rank_math/analytics/workflow/analytics', 'kill_jobs', 5, 0 );
$this->action( 'rank_math/analytics/workflow/create_tables', 'create_tables' );
$this->action( 'rank_math/analytics/workflow/analytics', 'create_tables', 6, 0 );
$this->action( 'rank_math/analytics/workflow/analytics', 'create_data_jobs', 10, 3 );
}
/**
* Kill jobs.
*
* Stop processing queue items, clear cronjob and delete all batches.
*/
public function kill_jobs() {
as_unschedule_all_actions( 'rank_math/analytics/get_analytics_data' );
}
/**
* Create tables.
*/
public function create_tables() {
global $wpdb;
$collate = $wpdb->get_charset_collate();
$table = 'rank_math_analytics_ga';
// Early Bail!!
if ( DB::check_table_exists( $table ) ) {
return;
}
$schema = "CREATE TABLE {$wpdb->prefix}{$table} (
id bigint(20) unsigned NOT NULL auto_increment,
page varchar(500) NOT NULL,
created timestamp NOT NULL,
pageviews mediumint(6) NOT NULL,
visitors mediumint(6) NOT NULL,
PRIMARY KEY (id),
KEY analytics_object_analytics (page(190))
) $collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
try {
dbDelta( $schema );
} catch ( Exception $e ) { // phpcs:ignore
// Will log.
}
}
/**
* Create jobs to fetch data.
*
* @param integer $days Number of days to fetch from past.
* @param string $prev Previous saved value.
* @param string $new New posted value.
*/
public function create_data_jobs( $days, $prev, $new ) {
// If saved and new profile are same.
if ( ! $this->is_profile_updated( 'view_id', $prev, $new ) ) {
return;
}
// Fetch now.
$this->schedule_single_action( $days, 'analytics' );
}
}

View File

@@ -0,0 +1,325 @@
<?php
/**
* Jobs.
*
* @since 1.0.54
* @package RankMathPro
* @subpackage RankMathPro\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics\Workflow;
use DateTime;
use Exception;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMathPro\Analytics\DB;
use RankMath\Analytics\Workflow\Base;
use RankMath\Analytics\DB as AnalyticsDB;
use RankMathPro\Google\Adsense;
use RankMath\Google\Analytics;
use RankMath\Analytics\Workflow\Jobs as AnalyticsJobs;
defined( 'ABSPATH' ) || exit;
/**
* Jobs class.
*/
class Jobs {
use Hooker;
/**
* Is an Analytics account connected?
*
* @var boolean
*/
private $analytics_connected = false;
/**
* Is an AdSense account connected?
*
* @var boolean
*/
private $adsense_connected = false;
/**
* 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->analytics_connected = Analytics::is_analytics_connected();
$this->adsense_connected = \RankMathPro\Google\Adsense::is_adsense_connected();
// Check missing data for analytics and adsense.
$this->action( 'rank_math/analytics/data_fetch', 'data_fetch' );
// Data Fetcher.
if ( $this->adsense_connected ) {
$this->filter( 'rank_math/analytics/get_adsense_days', 'get_adsense_days' );
$this->action( 'rank_math/analytics/get_adsense_data', 'get_adsense_data', 10, 2 );
}
if ( $this->analytics_connected ) {
$this->action( 'rank_math/analytics/get_analytics_days', 'get_analytics_days' );
$this->action( 'rank_math/analytics/get_analytics_data', 'get_analytics_data' );
$this->action( 'rank_math/analytics/handle_analytics_response', 'handle_analytics_response' );
$this->action( 'rank_math/analytics/clear_cache', 'clear_cache' );
}
// Cache.
$this->action( 'rank_math/analytics/purge_cache', 'purge_cache' );
$this->action( 'rank_math/analytics/delete_by_days', 'delete_by_days' );
$this->action( 'rank_math/analytics/delete_data_log', 'delete_data_log' );
}
/**
* Check missing data for analytics and adsense. Perform this task periodically.
*/
public function data_fetch() {
if ( $this->analytics_connected ) {
AnalyticsJobs::get()->check_for_missing_dates( 'analytics' );
}
if ( $this->adsense_connected ) {
AnalyticsJobs::get()->check_for_missing_dates( 'adsense' );
}
}
/**
* Set the analytics start and end dates.
*/
public function get_analytics_days( $args = [] ) {
$rows = Analytics::get_analytics(
[
'start_date' => $args['start_date'],
'end_date' => $args['end_date'],
],
true
);
if ( is_wp_error( $rows ) || empty( $rows ) ) {
return [];
}
$empty_dates = get_option( 'rank_math_analytics_empty_dates', [] );
$dates = [];
foreach ( $rows as $row ) {
$date = '';
// GA4
if ( isset( $row['dimensionValues'] ) ) {
$date = $row['dimensionValues'][0]['value'];
} elseif ( isset( $row['dimensions'] ) ) {
$date = $row['dimensions'][0];
}
if ( ! empty( $date ) ) {
$date = substr( $date, 0, 4 ) . '-' . substr( $date, 4, 2 ) . '-' . substr( $date, 6, 2 );
if ( ! AnalyticsDB::date_exists( $date, 'analytics' ) && ! in_array( $date, $empty_dates, true ) ) {
$dates[] = [
'start_date' => $date,
'end_date' => $date,
];
}
}
}
return $dates;
}
/**
* Get analytics data and save it into database.
*
* @param string $date Date to fetch data for.
*/
public function get_analytics_data( $date ) {
$rows = Analytics::get_analytics(
[
'start_date' => $date,
'end_date' => $date,
]
);
if ( is_wp_error( $rows ) || empty( $rows ) ) {
return [];
}
try {
DB::add_analytics_bulk( $date, $rows );
return $rows;
} catch ( Exception $e ) {} // phpcs:ignore
}
/**
* Set the AdSense start and end dates.
*/
public function get_adsense_days( $args = [] ) {
$dates = [];
$begin = new DateTime( $args['start_date'] );
$end = new DateTime( $args['end_date'] );
$missing_dates = [];
for ( $i = $end; $i >= $begin; $i->modify( '-1 day' ) ) {
$date = $i->format( 'Y-m-d' );
if ( ! AnalyticsDB::date_exists( $date, 'adsense' ) ) {
$missing_dates[] = $date;
}
}
if ( empty( $missing_dates ) ) {
$dates[] = [
'start_date' => $args['start_date'],
'end_date' => $args['end_date'],
];
return $dates;
}
// Request for one date range because its not large data to send individual request for each date.
$dates[] = [
'start_date' => $missing_dates[ count( $missing_dates ) - 1 ],
'end_date' => $missing_dates[0],
];
return $dates;
}
/**
* Get adsense data and save it into database.
*
* @param string $start_date The start date to fetch.
* @param string $end_date The end date to fetch.
*/
public function get_adsense_data( $start_date = '', $end_date = '' ) {
$rows = Adsense::get_adsense(
[
'start_date' => $start_date,
'end_date' => $end_date,
]
);
if ( is_wp_error( $rows ) || empty( $rows ) ) {
return [];
}
try {
DB::add_adsense( $rows );
return $rows;
} catch ( Exception $e ) {} // phpcs:ignore
}
/**
* Handlle analytics response.
*
* @param array $data API request and response data.
*/
public function handle_analytics_response( $data = [] ) {
if ( 200 !== $data['code'] ) {
return;
}
if ( isset( $data['formatted_response']['rows'] ) && ! empty( $data['formatted_response']['rows'] ) ) {
return;
}
$dates = get_option( 'rank_math_analytics_empty_dates', [] );
if ( ! $dates ) {
$dates = [];
}
$dates[] = $data['args']['dateRanges'][0]['startDate'];
$dates[] = $data['args']['dateRanges'][0]['endDate'];
$dates = array_unique( $dates );
update_option( 'rank_math_analytics_empty_dates', $dates );
}
/**
* Clear cache.
*/
public function clear_cache() {
global $wpdb;
// Delete all useless data from analytics data table.
$wpdb->get_results( "DELETE FROM {$wpdb->prefix}rank_math_analytics_ga WHERE page NOT IN ( SELECT page from {$wpdb->prefix}rank_math_analytics_objects )" );
}
/**
* Purge cache.
*
* @param object $table Table insance.
*/
public function purge_cache( $table ) {
$table->whereLike( 'option_name', 'losing_posts' )->delete();
$table->whereLike( 'option_name', 'winning_posts' )->delete();
$table->whereLike( 'option_name', 'losing_keywords' )->delete();
$table->whereLike( 'option_name', 'winning_keywords' )->delete();
$table->whereLike( 'option_name', 'tracked_keywords_summary' )->delete();
}
/**
* Delete analytics and adsense data by days.
*
* @param int $days Decide whether to delete all or delete 90 days data.
*/
public function delete_by_days( $days ) {
if ( -1 === $days ) {
if ( $this->analytics_connected ) {
DB::traffic()->truncate();
}
if ( $this->adsense_connected ) {
DB::adsense()->truncate();
}
return;
}
$start = date_i18n( 'Y-m-d H:i:s', strtotime( '-1 days' ) );
$end = date_i18n( 'Y-m-d H:i:s', strtotime( '-' . $days . ' days' ) );
if ( $this->analytics_connected ) {
DB::traffic()->whereBetween( 'created', [ $end, $start ] )->delete();
}
if ( $this->adsense_connected ) {
DB::adsense()->whereBetween( 'created', [ $end, $start ] )->delete();
}
}
/**
* Delete record for comparison.
*
* @param string $start Start date.
*/
public function delete_data_log( $start ) {
if ( $this->analytics_connected ) {
DB::traffic()->where( 'created', '<', $start )->delete();
}
if ( $this->adsense_connected ) {
DB::adsense()->where( 'created', '<', $start )->delete();
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* Install Keyword manager.
*
* @since 1.0.49
* @package RankMathPro
* @subpackage RankMathPro\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics\Workflow;
use Exception;
use MyThemeShop\Helpers\DB;
use RankMath\Analytics\Workflow\Base;
defined( 'ABSPATH' ) || exit;
/**
* Keywords class.
*/
class Keywords extends Base {
/**
* Constructor.
*/
public function __construct() {
$done = \boolval( get_option( 'rank_math_analytics_pro_installed' ) );
if ( $done ) {
return;
}
$this->create_keywords_tables();
update_option( 'rank_math_analytics_pro_installed', true );
}
/**
* Create keywords tables.
*/
public function create_keywords_tables() {
global $wpdb;
$collate = $wpdb->get_charset_collate();
$table = 'rank_math_analytics_keyword_manager';
// Early Bail!!
if ( DB::check_table_exists( $table ) ) {
return;
}
$schema = "CREATE TABLE {$wpdb->prefix}{$table} (
id bigint(20) unsigned NOT NULL auto_increment,
keyword varchar(1000) NOT NULL,
collection varchar(200) NULL,
is_active tinyint(1) NOT NULL default 1,
PRIMARY KEY (id)
) $collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
try {
dbDelta( $schema );
} catch ( Exception $e ) { // phpcs:ignore
// Will log.
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* Workflow.
*
* @since 1.0.54
* @package RankMathPro
* @subpackage RankMathPro\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Analytics\Workflow;
use RankMath\Traits\Hooker;
use function as_enqueue_async_action;
use function as_unschedule_all_actions;
defined( 'ABSPATH' ) || exit;
/**
* Workflow class.
*/
class Workflow {
use Hooker;
/**
* Main instance
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Workflow
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Workflow ) ) {
$instance = new Workflow();
$instance->hooks();
}
return $instance;
}
/**
* Hooks.
*/
public function hooks() {
// Common.
$this->action( 'rank_math/analytics/workflow', 'maybe_first_install', 5, 0 );
$this->action( 'rank_math/analytics/workflow/create_tables', 'create_tables_only', 5 );
// Services.
$this->action( 'rank_math/analytics/workflow/analytics', 'init_analytics_workflow', 5, 0 );
$this->action( 'rank_math/analytics/workflow/adsense', 'init_adsense_workflow', 5, 0 );
}
/**
* Maybe first install.
*/
public function maybe_first_install() {
new \RankMathPro\Analytics\Workflow\Keywords();
}
/**
* Init Analytics workflow
*/
public function init_analytics_workflow() {
new \RankMathPro\Analytics\Workflow\Analytics();
}
/**
* Init Adsense workflow
*/
public function init_adsense_workflow() {
new \RankMathPro\Analytics\Workflow\Adsense();
}
/**
* Create tables only.
*/
public function create_tables_only() {
new \RankMathPro\Analytics\Workflow\Analytics();
new \RankMathPro\Analytics\Workflow\Adsense();
( new \RankMathPro\Analytics\Workflow\Keywords() )->create_keywords_tables();
}
}

View File

@@ -0,0 +1 @@
(()=>{"use strict";var a={n:t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return a.d(e,{a:e}),e},d:(t,e)=>{for(var r in e)a.o(e,r)&&!a.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},o:(a,t)=>Object.prototype.hasOwnProperty.call(a,t)};const t=jQuery;var e;(e=a.n(t)())((function(){e(".rank-math-mark-solved a").on("click",(function(a){a.preventDefault();var t=e(this),r=t.data("is-solved");return e.ajax({url:rankMath.ajaxurl,type:"POST",dataType:"json",data:{action:"rank_math_mark_answer_solved",security:rankMath.security,topic:t.data("topic-id"),reply:t.data("id"),isSolved:r}}).done((function(){if(r)return t.text(t.data("unsolved-text")),void e(".rank-math-mark-solved.rank-math-hidden").removeClass("rank-math-hidden");e(".rank-math-mark-solved").addClass("rank-math-hidden"),t.parent().removeClass("rank-math-hidden"),t.text(t.data("solved-text"))})),!1}))}))})();

View File

@@ -0,0 +1,182 @@
<?php
/**
* BBPress module.
*
* @since 1.0
* @package RankMathPro
* @subpackage RankMathPro
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro;
use RankMath\Traits\Hooker;
use RankMath\Traits\Ajax;
use MyThemeShop\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* BBPress class.
*
* @codeCoverageIgnore
*/
class BBPress {
use Hooker, Ajax;
/**
* Post meta key for solved answers.
*
* @var string
*/
public $meta_key = '';
/**
* Constructor.
*/
public function __construct() {
$this->meta_key = 'rank_math_bbpress_solved_answer';
$this->action( 'wp', 'hooks' );
$this->ajax( 'mark_answer_solved', 'mark_answer_solved' );
$this->action( 'rank_math/json_ld', 'add_qa_schema', 99 );
}
/**
* Init hooks.
*/
public function hooks() {
if ( ! is_singular( 'topic' ) || ! current_user_can( 'moderate', get_the_ID() ) ) {
return;
}
$this->action( 'bbp_get_reply_content', 'add_solved_answer_button', 9, 2 );
$this->action( 'wp_enqueue_scripts', 'enqueue' );
$this->action( 'wp_footer', 'add_css' );
}
/**
* Enqueue Script required by plugin.
*/
public function enqueue() {
wp_enqueue_script( 'rank-math-bbpress', RANK_MATH_PRO_URL . 'includes/modules/bbPress/assets/js/bbpress.js', [ 'jquery' ], RANK_MATH_PRO_VERSION, true );
}
/**
* Add Mark Reply as Solved button to the Reply content.
*
* @param string $content Original reply content.
* @param int $reply_id Reply ID.
*
* @return string $content New content.
*/
public function add_solved_answer_button( $content, $reply_id ) {
$reply = bbp_get_reply( $reply_id );
if ( empty( $reply ) ) {
return $content;
}
$topic_id = bbp_get_reply_topic_id();
$answered = (int) get_post_meta( $topic_id, $this->meta_key, true );
$solved_text = __( 'Mark Unsolved.', 'rank-math-pro' );
$unsolved_text = __( 'Mark Solved.', 'rank-math-pro' );
$is_solved = $reply_id === $answered;
$class = metadata_exists( 'post', $topic_id, $this->meta_key ) && ! $is_solved ? 'rank-math-hidden' : '';
$text = $is_solved ? $solved_text : $unsolved_text;
$content .= '
<div class="rank-math-mark-solved ' . esc_attr( $class ) . '">
<a
href="#"
data-id="' . esc_attr( $reply_id ) . '"
data-topic-id="' . esc_attr( $topic_id ) . '"
data-solved-text="' . esc_attr( $solved_text ) . '"
data-unsolved-text="' . esc_attr( $unsolved_text ) . '"
data-is-solved="' . $is_solved . '">'
. apply_filters( 'rank_math/bbpress/solved_text', $text, $is_solved )
. '</a>
</div>';
return $content;
}
/**
* AJAX function to mark answer as solved.
*/
public function mark_answer_solved() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$topic = Param::post( 'topic' );
if ( ! current_user_can( 'moderate', $topic ) ) {
return false;
}
$is_solved = Param::post( 'isSolved' );
if ( $is_solved ) {
return delete_post_meta( $topic, $this->meta_key );
}
$reply = Param::post( 'reply' );
return update_post_meta( $topic, $this->meta_key, $reply );
}
/**
* Add QA Schema Data.
*
* @param array $data Array of json-ld data.
* @return array
*/
public function add_qa_schema( $data ) {
if ( ! is_singular( 'topic' ) ) {
return $data;
}
global $post;
$approved_answer = get_post_meta( $post->ID, $this->meta_key, true );
if ( ! $approved_answer ) {
return $data;
}
$data[] = [
'@type' => 'QAPage',
'mainEntity' => [
'@type' => 'Question',
'name' => get_the_title( $post ),
'text' => get_the_excerpt( $post ),
'dateCreated' => get_post_time( 'Y-m-d\TH:i:sP', false ),
'answerCount' => get_post_meta( $post->ID, '_bbp_reply_count', true ),
'author' => [
'@type' => 'Person',
'name' => get_the_author(),
],
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => get_post_field( 'post_content', $approved_answer ),
'dateCreated' => get_post_time( 'Y-m-d\TH:i:sP', false, $approved_answer ),
'url' => bbp_get_reply_url( $approved_answer ),
'author' => [
'@type' => 'Person',
'name' => bbp_get_reply_author( $approved_answer ),
],
],
],
];
return $data;
}
/**
* Add CSS inline, once.
*/
public function add_css() {
?>
<style type="text/css">
.rank-math-mark-solved {
text-align: right;
}
.rank-math-hidden {
display: none;
}
</style>
<?php
}
}

View File

@@ -0,0 +1 @@
(()=>{"use strict";const r=wp.i18n,t=wp.hooks;(0,t.addFilter)("rank_math_content_ai_help_text","rank-math-pro",(function(){return(0,r.__)("Contact your SEO service provider for more AI credits.","rank-math-pro")})),(0,t.addFilter)("rank_math_content_ai_credits_notice","rank-math-pro",(function(){return(0,r.__)("You have used all of your AI credits and need to purchase more from your SEO service provider.","rank-math-pro")}))})();

View File

@@ -0,0 +1,13 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { addFilter } from '@wordpress/hooks'
addFilter( 'rank_math_content_ai_help_text', 'rank-math-pro', () => {
return __( 'Contact your SEO service provider for more AI credits.', 'rank-math-pro' )
} )
addFilter( 'rank_math_content_ai_credits_notice', 'rank-math-pro', () => {
return __( 'You have used all of your AI credits and need to purchase more from your SEO service provider.', 'rank-math-pro' )
} )

View File

@@ -0,0 +1,52 @@
<?php
/**
* The Content AI module.
*
* @since 3.0.25
* @package RankMath
* @subpackage RankMathPro
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro;
use RankMath\ContentAI\Content_AI as Content_AI_Free;
use RankMath\Helper;
use RankMathPro\Admin\Admin_Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Content_AI class.
*/
class Content_AI {
use Hooker;
/**
* Class constructor.
*/
public function __construct() {
if ( ! Admin_Helper::is_business_plan() || ! Content_AI_Free::can_add_tab() || ! Helper::get_current_editor() ) {
return;
}
$this->action( 'rank_math/admin/editor_scripts', 'editor_scripts', 19 );
}
/**
* Enqueue assets for post editors.
*
* @return void
*/
public function editor_scripts() {
wp_enqueue_script(
'rank-math-pro-content-ai',
RANK_MATH_PRO_URL . 'includes/modules/content-ai/assets/js/content-ai.js',
[ 'rank-math-content-ai' ],
rank_math_pro()->version,
true
);
}
}

View File

@@ -0,0 +1,648 @@
<?php
/**
* Image SEO module.
*
* @since 1.0
* @package RankMathPro
* @subpackage RankMathPro\Admin
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro;
use stdClass;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use MyThemeShop\Helpers\HTML;
defined( 'ABSPATH' ) || exit;
/**
* Image_Seo class.
*
* @codeCoverageIgnore
*/
class Image_Seo_Pro {
use Hooker;
/**
* Change the case of the alt attribute.
*
* @var string
*/
public $alt_change_case;
/**
* Change the case of the title attribute.
*
* @var string
*/
public $title_change_case;
/**
* Constructor.
*/
public function __construct() {
$this->action( 'rank_math/admin/settings/images', 'add_options' );
if ( Helper::get_settings( 'general.add_avatar_alt' ) ) {
$this->filter( 'get_avatar', 'avatar_add_missing_alt', 99, 6 );
}
if ( Helper::get_settings( 'general.add_img_caption' ) ) {
$this->filter( 'shortcode_atts_caption', 'add_caption', 99, 1 );
$this->filter( 'the_content', 'content_add_caption', 99 );
}
if ( Helper::get_settings( 'general.add_img_description' ) ) {
$this->filter( 'the_content', 'add_description', 99 );
}
$replacements = Helper::get_settings( 'general.image_replacements' );
if ( ! empty( $replacements ) ) {
$this->filter( 'the_content', 'attribute_replacements', 20 );
$this->filter( 'post_thumbnail_html', 'attribute_replacements', 20 );
$this->filter( 'woocommerce_single_product_image_thumbnail_html', 'attribute_replacements', 20 );
$this->filter( 'shortcode_atts_caption', 'caption_replacements', 20, 3 );
}
$this->action( 'wp_head', 'maybe_change_attributes_case', 110 );
if ( Helper::get_settings( 'general.img_caption_change_case' ) ) {
$this->filter( 'shortcode_atts_caption', 'change_caption_case', 110, 1 );
$this->filter( 'the_content', 'change_content_caption_case', 110 );
}
if ( Helper::get_settings( 'general.img_description_change_case' ) ) {
$this->filter( 'the_content', 'change_description_case', 110 );
}
$this->action( 'rank_math/vars/register_extra_replacements', 'register_replacements' );
$this->filter( 'cmb2_field_arguments', 'maybe_exclude_image_vars', 10 );
}
/**
* Registers variable replacements for the Image SEO Pro module.
*/
public function register_replacements() {
rank_math_register_var_replacement(
'imagealt',
[
'name' => esc_html__( 'Image Alt', 'rank-math-pro' ),
'description' => esc_html__( 'Alt text set for the current image.', 'rank-math-pro' ),
'variable' => 'imagealt',
'example' => '',
'nocache' => true,
],
[ $this, 'get_imagealt' ]
);
rank_math_register_var_replacement(
'imagetitle',
[
'name' => esc_html__( 'Image Title', 'rank-math-pro' ),
'description' => esc_html__( 'Title text set for the current image.', 'rank-math-pro' ),
'variable' => 'imagetitle',
'example' => '',
'nocache' => true,
],
[ $this, 'get_imagetitle' ]
);
}
/**
* Filter CMB field arguments to exclude `imagealt` & `imagetitle` when they are not needed.
*
* @param array $args Arguments array.
* @return array
*/
public function maybe_exclude_image_vars( $args ) {
if ( empty( $args['classes'] ) ) {
return $args;
}
$classes = is_array( $args['classes'] ) ? $args['classes'] : explode( ' ', $args['classes'] );
if ( ! in_array( 'rank-math-supports-variables', $classes, true ) ) {
return $args;
}
if ( ! is_string( $args['id'] ) || strpos( $args['id'], 'img_' ) !== false ) {
return $args;
}
if ( ! isset( $args['attributes']['data-exclude-variables'] ) ) {
$args['attributes']['data-exclude-variables'] = '';
}
$args['attributes']['data-exclude-variables'] .= ',imagealt,imagetitle';
$args['attributes']['data-exclude-variables'] = trim( $args['attributes']['data-exclude-variables'], ',' );
return $args;
}
/**
* Get the alt attribute of the attachment to use as a replacement.
* See rank_math_register_var_replacement().
*
* @codeCoverageIgnore
*
* @param string $var_args Variable name, for example %custom%. The '%' signs are optional.
* @param object $replacement_args Additional title, description and example values for the variable.
*
* @return bool Replacement was registered successfully or not.
*/
public function get_imagealt( $var_args, $replacement_args = null ) {
if ( empty( $replacement_args->alttext ) ) {
return null;
}
return $replacement_args->alttext;
}
/**
* Get the title attribute of the attachment to use as a replacement.
* See rank_math_register_var_replacement().
*
* @codeCoverageIgnore
*
* @param string $var_args Variable name, for example %custom%. The '%' signs are optional.
* @param object $replacement_args Additional title, description and example values for the variable.
*
* @return bool Replacement was registered successfully or not.
*/
public function get_imagetitle( $var_args, $replacement_args = null ) {
if ( empty( $replacement_args->titletext ) ) {
return null;
}
return $replacement_args->titletext;
}
/**
* Change case of alt & title attributes if needed.
*
* @return void
*/
public function maybe_change_attributes_case() {
// Change image title and alt casing.
$this->alt_change_case = Helper::get_settings( 'general.img_alt_change_case' );
$this->title_change_case = Helper::get_settings( 'general.img_title_change_case' );
if ( $this->alt_change_case || $this->title_change_case ) {
$this->filter( 'the_content', 'change_attribute_case', 30 );
$this->filter( 'post_thumbnail_html', 'change_attribute_case', 30 );
$this->filter( 'woocommerce_single_product_image_thumbnail_html', 'change_attribute_case', 30 );
}
}
/**
* Change case of alt & title attributes in a post content string.
*
* @param string $content Post content.
* @return string New post content.
*/
public function change_attribute_case( $content ) {
if ( empty( $content ) ) {
return $content;
}
$stripped_content = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
preg_match_all( '/<img ([^>]+)\/?>/iU', $stripped_content, $matches, PREG_SET_ORDER );
if ( empty( $matches ) ) {
return $content;
}
foreach ( $matches as $image ) {
$is_dirty = false;
$attrs = HTML::extract_attributes( $image[0] );
if ( ! isset( $attrs['src'] ) ) {
continue;
}
$this->set_image_attribute( $attrs, 'alt', $this->alt_change_case, $is_dirty );
$this->set_image_attribute( $attrs, 'title', $this->title_change_case, $is_dirty );
if ( $is_dirty ) {
$new = '<img' . HTML::attributes_to_string( $attrs ) . '>';
$content = str_replace( $image[0], $new, $content );
}
}
return $content;
}
/**
* Change image attribute case after checking condition.
*
* @param array $attrs Array which hold rel attribute.
* @param string $attribute Attribute to set.
* @param boolean $condition Condition to check.
* @param boolean $is_dirty Is dirty variable.
*/
private function set_image_attribute( &$attrs, $attribute, $condition, &$is_dirty ) {
if ( $condition && ! empty( $attrs[ $attribute ] ) ) {
$is_dirty = true;
$attrs[ $attribute ] = $this->change_case( $attrs[ $attribute ], $condition );
}
}
/**
* Turn first character of every sentence to uppercase.
*
* @param string $string Original sring.
*
* @return string New string.
*/
private function sentence_case( $string ) {
$sentences = preg_split( '/([.?!]+)/', $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
$new_string = '';
foreach ( $sentences as $key => $sentence ) {
$new_string .= ( $key & 1 ) === 0 ?
$this->mb_ucfirst( trim( $sentence ) ) :
$sentence . ' ';
}
return trim( $new_string );
}
/**
* Multibyte ucfirst().
*
* @param string $string String.
*
* @return string New string.
*/
private function mb_ucfirst( $string ) {
return mb_strtoupper( mb_substr( $string, 0, 1 ) ) . mb_strtolower( mb_substr( $string, 1 ) );
}
/**
* Change case of string.
*
* @param string $string String to change.
* @param string $case Case type to change to.
*
* @return string New string.
*/
private function change_case( $string, $case ) {
$cases_hash = [
'titlecase' => MB_CASE_TITLE,
'sentencecase' => MB_CASE_LOWER,
'lowercase' => MB_CASE_LOWER,
'uppercase' => MB_CASE_UPPER,
];
if ( ! isset( $cases_hash[ $case ] ) ) {
return $string;
}
if ( 'sentencecase' === $case ) {
return $this->sentence_case( $string );
}
return mb_convert_case( $string, $cases_hash[ $case ] );
}
/**
* Add alt attribute for avatars if they don't have one.
*
* @param string $avatar Avatar HTML.
* @param mixed $id_or_email User ID or email.
* @param int $size Width in px.
* @param string $default Fallback.
* @param string $alt Alt attribute value.
* @param array $args Avatar args.
*
* @return string New avatar HTML.
*/
public function avatar_add_missing_alt( $avatar, $id_or_email, $size, $default, $alt, $args ) { // phpcs:ignore
if ( is_admin() ) {
return $avatar;
}
if ( empty( $avatar ) ) {
return $avatar;
}
if ( ! empty( $alt ) ) {
return $avatar;
}
if ( ! preg_match( '/<img ([^>]+)\/?>/iU', $avatar ) ) {
return $avatar;
}
$attrs = HTML::extract_attributes( $avatar );
if ( ! empty( $attrs['alt'] ) ) {
return $avatar;
}
$new_alt = '';
if ( is_a( $id_or_email, 'WP_Comment' ) ) {
$user = $id_or_email->comment_author;
} elseif ( is_int( $id_or_email ) ) {
// This is a user ID.
$user = get_user_by( 'id', $id_or_email );
} elseif ( is_string( $id_or_email ) ) {
$user = get_user_by( 'email', $id_or_email );
}
if ( is_a( $user, 'WP_User' ) ) {
$new_alt = $user->get( 'display_name' );
} elseif ( is_string( $user ) ) {
$new_alt = $user;
} else {
return $avatar;
}
// Translators: placeholder is the username or email.
$attrs['alt'] = sprintf( __( 'Avatar of %s', 'rank-math-pro' ), $new_alt );
$attrs['alt'] = apply_filters( 'rank_math_pro/images/avatar_alt', $attrs['alt'], $id_or_email );
$new = '<img' . HTML::attributes_to_string( $attrs ) . '>';
// Change image title and alt casing.
$this->alt_change_case = Helper::get_settings( 'general.img_alt_change_case' );
$this->title_change_case = Helper::get_settings( 'general.img_title_change_case' );
if ( $this->alt_change_case || $this->title_change_case ) {
$new = $this->change_attribute_case( $new );
}
return $new;
}
/**
* Add missing caption text if needed.
*
* @param string $out Shortcode output.
*
* @return string New shortcode output.
*/
public function add_caption( $out ) {
if ( ! empty( $out['caption'] ) ) {
return $out;
}
$out['caption'] = trim( Helper::replace_vars( Helper::get_settings( 'general.img_caption_format' ), $this->get_post() ) );
return $out;
}
/**
* Change case for captions.
*
* @param string $out Shortcode output.
*
* @return string New shortcode output.
*/
public function change_caption_case( $out ) {
if ( empty( $out['caption'] ) ) {
return $out;
}
$out['caption'] = $this->change_case( $out['caption'], Helper::get_settings( 'general.img_caption_change_case' ) );
return $out;
}
/**
* Change case for captions in Image Blocks.
*
* @param string $content Content output.
*
* @return string New output.
*/
public function change_content_caption_case( $content ) {
$content = preg_replace_callback( '/(<figure[^<]+class="([^"]+ )?(wp-block-image|wp-caption).+<figcaption[^>]*>)([^<]+)(<\/figcaption>)/sU', [ $this, 'caption_case_cb' ], $content );
return $content;
}
/**
* Change case for captions in Image Blocks.
*
* @param string $content Content output.
*
* @return string New output.
*/
public function content_add_caption( $content ) {
$content = preg_replace_callback( '/<figure class="([^"]+ )?wp-block-image .+<\/figure>/sU', [ $this, 'add_caption_cb' ], $content );
return $content;
}
/**
* Change case for captions in Image Blocks.
*
* @param string $matches Content output.
*
* @return string New output.
*/
public function caption_case_cb( $matches ) {
return $matches[1] . $this->change_case( $matches[4], Helper::get_settings( 'general.img_caption_change_case' ) ) . $matches[5];
}
/**
* Add caption in Image Blocks.
*
* @param string $matches Content output.
*
* @return string New output.
*/
public function add_caption_cb( $matches ) {
if ( stripos( $matches[0], '<figcaption' ) !== false ) {
return $matches[0];
}
$caption = trim( Helper::replace_vars( Helper::get_settings( 'general.img_caption_format' ), $this->get_post( $matches[0] ) ) );
return str_replace( '</figure>', '<figcaption>' . $caption . '</figcaption></figure>', $matches[0] );
}
/**
* Add missing attachment description if needed.
*
* @param string $content Original content.
*
* @return string New content.
*/
public function add_description( $content ) {
if ( get_post_type() !== 'attachment' ) {
return $content;
}
$content_stripped = wp_strip_all_tags( $content );
if ( ! empty( $content_stripped ) ) {
return $content;
}
return $content . trim( Helper::replace_vars( Helper::get_settings( 'general.img_description_format' ), $this->get_post() ) );
}
/**
* Change case for image description.
*
* @param string $content Original content.
*
* @return string New content.
*/
public function change_description_case( $content ) {
if ( get_post_type() !== 'attachment' ) {
return $content;
}
$parts = preg_split( '/(<[^>]+>)/', $content, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
$new = '';
foreach ( $parts as $i => $part ) {
if ( '<' === substr( trim( $part ), 0, 1 ) ) {
$new .= $part;
continue;
}
$new .= $this->change_case( $part, Helper::get_settings( 'general.img_description_change_case' ) );
}
return $new;
}
/**
* Search & replace in alt & title attributes.
*
* @param string $content Original post content.
*
* @return string New post content.
*/
public function attribute_replacements( $content ) {
$replacements = Helper::get_settings( 'general.image_replacements' );
foreach ( $replacements as $replacement_id => $replacement ) {
if ( ! count( array_intersect( $replacement['replace_in'], [ 'alt', 'title' ] ) ) ) {
continue;
}
foreach ( $replacement['replace_in'] as $attr ) {
if ( 'caption' === $attr ) {
continue;
}
$content = $this->attribute_replacement( $content, $replacement['find'], $replacement['replace'], $attr );
}
}
return $content;
}
/**
* Do the replacement in an attribute.
*
* @param string $content Original content.
* @param string $find Search string.
* @param string $replace Replacement string.
* @param string $attribute Attribute to look for.
*
* @return string New content.
*/
public function attribute_replacement( $content, $find, $replace, $attribute ) {
$stripped_content = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
preg_match_all( '/<img ([^>]+)\/?>/iU', $stripped_content, $matches, PREG_SET_ORDER );
if ( empty( $matches ) ) {
return $content;
}
foreach ( $matches as $image ) {
$attrs = HTML::extract_attributes( $image[0] );
if ( ! isset( $attrs['src'] ) ) {
continue;
}
if ( empty( $attrs[ $attribute ] ) ) {
continue;
}
$attrs[ $attribute ] = str_replace( $find, $replace, $attrs[ $attribute ] );
$new = '<img' . HTML::attributes_to_string( $attrs ) . '>';
$content = str_replace( $image[0], $new, $content );
}
return $content;
}
/**
* Search & replace in image captions.
*
* @param string $out Shortcode output.
* @param array $pairs Possible attributes.
* @param array $atts Shortcode attributes.
*
* @return string New shortcode output.
*/
public function caption_replacements( $out, $pairs, $atts ) {
$replacements = Helper::get_settings( 'general.image_replacements' );
foreach ( $replacements as $replacement_id => $replacement ) {
if ( ! in_array( 'caption', $replacement['replace_in'], true ) ) {
continue;
}
$caption = $atts['caption'];
if ( empty( $caption ) ) {
continue;
}
$new_caption = str_replace( $replacement['find'], $replacement['replace'], $caption );
$out = str_replace( $caption, $new_caption, $out );
}
return $out;
}
/**
* Get post object.
*
* @return object
*/
private function get_post( $image = [] ) {
$post = \get_post();
if ( empty( $post ) ) {
$post = new stdClass();
}
if ( empty( $image ) ) {
return $post;
}
$attrs = HTML::extract_attributes( $image );
if ( empty( $attrs['src'] ) ) {
return $post;
}
$post->filename = $attrs['src'];
// Lazy load support.
if ( ! empty( $attrs['data-src'] ) ) {
$post->filename = $attrs['data-src'];
} elseif ( ! empty( $attrs['data-layzr'] ) ) {
$post->filename = $attrs['data-layzr'];
} elseif ( ! empty( $attrs['nitro-lazy-srcset'] ) ) {
$post->filename = $attrs['nitro-lazy-srcset'];
}
return $post;
}
/**
* Add options to Image SEO module.
*
* @param object $cmb CMB object.
*/
public function add_options( $cmb ) {
$field_ids = wp_list_pluck( $cmb->prop( 'fields' ), 'id' );
$fields_position = array_search( 'img_title_format', array_keys( $field_ids ), true ) + 1;
include_once dirname( __FILE__ ) . '/options.php';
}
}

View File

@@ -0,0 +1,191 @@
<?php
/**
* The images settings.
*
* @package RankMath
* @subpackage RankMath\Settings
*/
defined( 'ABSPATH' ) || exit;
$cmb->add_field(
[
'id' => 'add_img_caption',
'type' => 'toggle',
'name' => esc_html__( 'Add missing image caption', 'rank-math-pro' ),
'desc' => wp_kses_post( __( 'Add a caption for all images without a caption automatically. The caption is dynamically applied when the content is displayed, the stored content is not changed.', 'rank-math-pro' ) ),
'default' => 'off',
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'img_caption_format',
'type' => 'text',
'name' => esc_html__( 'Caption format', 'rank-math-pro' ),
'desc' => wp_kses_post( __( 'Format used for the new captions.', 'rank-math-pro' ) ),
'classes' => 'large-text rank-math-supports-variables dropdown-up',
'default' => '%title% %count(title)%',
'dep' => [ [ 'add_img_caption', 'on' ] ],
'sanitization_cb' => false,
'attributes' => [ 'data-exclude-variables' => 'seo_title,seo_description' ],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'add_img_description',
'type' => 'toggle',
'name' => esc_html__( 'Add missing image description', 'rank-math-pro' ),
'desc' => wp_kses_post( __( 'Add a description for all images without a description automatically. The description is dynamically applied when the content is displayed, the stored content is not changed.', 'rank-math-pro' ) ),
'default' => 'off',
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'img_description_format',
'type' => 'text',
'name' => esc_html__( 'Description format', 'rank-math-pro' ),
'desc' => wp_kses_post( __( 'Format used for the new descriptions.', 'rank-math-pro' ) ),
'classes' => 'large-text rank-math-supports-variables dropdown-up',
'default' => '%title% %count(title)%',
'dep' => [ [ 'add_img_description', 'on' ] ],
'sanitization_cb' => false,
'attributes' => [ 'data-exclude-variables' => 'seo_title,seo_description' ],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'img_title_change_case',
'type' => 'select',
'name' => esc_html__( 'Change title casing', 'rank-math-pro' ),
'desc' => wp_kses_post( __( 'Capitalization settings for the <code>title</code> attribute values. This will be applied for <strong>all</strong> <code>title</code> attributes.', 'rank-math-pro' ) ),
'default' => 'off',
'options' => [
'off' => esc_html__( 'No change', 'rank-math-pro' ),
'titlecase' => esc_html__( 'Title Casing', 'rank-math-pro' ),
'sentencecase' => esc_html__( 'Sentence casing', 'rank-math-pro' ),
'lowercase' => esc_html__( 'all lowercase', 'rank-math-pro' ),
'uppercase' => esc_html__( 'ALL UPPERCASE', 'rank-math-pro' ),
],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'img_alt_change_case',
'type' => 'select',
'name' => esc_html__( 'Change alt attribute casing', 'rank-math-pro' ),
'desc' => wp_kses_post( __( 'Capitalization settings for the <code>alt</code> attribute values. This will be applied for <strong>all</strong> <code>alt</code> attributes.', 'rank-math-pro' ) ),
'default' => 'off',
'options' => [
'off' => esc_html__( 'No change', 'rank-math-pro' ),
'titlecase' => esc_html__( 'Title Casing', 'rank-math-pro' ),
'sentencecase' => esc_html__( 'Sentence casing', 'rank-math-pro' ),
'lowercase' => esc_html__( 'all lowercase', 'rank-math-pro' ),
'uppercase' => esc_html__( 'ALL UPPERCASE', 'rank-math-pro' ),
],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'img_description_change_case',
'type' => 'select',
'name' => esc_html__( 'Change description casing', 'rank-math-pro' ),
'desc' => wp_kses_post( __( 'Capitalization settings for the image descriptions. This will be applied for <strong>all</strong> image descriptions.', 'rank-math-pro' ) ),
'default' => 'off',
'options' => [
'off' => esc_html__( 'No change', 'rank-math-pro' ),
'titlecase' => esc_html__( 'Title Casing', 'rank-math-pro' ),
'sentencecase' => esc_html__( 'Sentence casing', 'rank-math-pro' ),
'lowercase' => esc_html__( 'all lowercase', 'rank-math-pro' ),
'uppercase' => esc_html__( 'ALL UPPERCASE', 'rank-math-pro' ),
],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'img_caption_change_case',
'type' => 'select',
'name' => esc_html__( 'Change caption casing', 'rank-math-pro' ),
'desc' => wp_kses_post( __( 'Capitalization settings for the image captions. This will be applied for <strong>all</strong> image captions.', 'rank-math-pro' ) ),
'default' => 'off',
'options' => [
'off' => esc_html__( 'No change', 'rank-math-pro' ),
'titlecase' => esc_html__( 'Title Casing', 'rank-math-pro' ),
'sentencecase' => esc_html__( 'Sentence casing', 'rank-math-pro' ),
'lowercase' => esc_html__( 'all lowercase', 'rank-math-pro' ),
'uppercase' => esc_html__( 'ALL UPPERCASE', 'rank-math-pro' ),
],
],
++$fields_position
);
$cmb->add_field(
[
'id' => 'add_avatar_alt',
'type' => 'toggle',
'name' => esc_html__( 'Add ALT attributes for avatars', 'rank-math-pro' ),
'desc' => wp_kses_post( __( 'Add <code>alt</code> attributes for commenter profile pictures (avatars) automatically. The alt attribute value will be the username.', 'rank-math-pro' ) ),
'default' => 'off',
],
++$fields_position
);
$replacement_fields = $cmb->add_field( //phpcs:ignore
[
'id' => 'image_replacements',
'type' => 'group',
'name' => esc_html__( 'Replacements', 'rank-math-pro' ),
'desc' => esc_html__( 'Replace characters or words in the alt tags, title tags, or in the captions.', 'rank-math-pro' ),
'options' => [
'add_button' => esc_html__( 'Add another', 'rank-math-pro' ),
'remove_button' => esc_html__( 'Remove', 'rank-math-pro' ),
],
'classes' => 'cmb-group-text-only',
],
++$fields_position
);
$cmb->add_group_field(
$replacement_fields,
[
'id' => 'find',
'type' => 'text',
'attributes' => [ 'placeholder' => esc_attr__( 'Find', 'rank-math-pro' ) ],
]
);
$cmb->add_group_field(
$replacement_fields,
[
'id' => 'replace',
'type' => 'text',
'attributes' => [ 'placeholder' => esc_attr__( 'Replace', 'rank-math-pro' ) ],
]
);
$cmb->add_group_field(
$replacement_fields,
[
'id' => 'replace_in',
'type' => 'multicheck',
'options' => [
'alt' => __( 'Alt', 'rank-math-pro' ),
'title' => __( 'Title', 'rank-math-pro' ),
'caption' => __( 'Caption', 'rank-math-pro' ),
],
]
);

View File

@@ -0,0 +1 @@
.rank-math-business-wrapper .rank-math-directions-wrapper .rank-math-directions-result{display:none}.rank-math-business-wrapper .rank-math-directions-wrapper.show .rank-math-directions-result{display:block}

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
(()=>{"use strict";var t={n:e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return t.d(n,{a:n}),n},d:(e,n)=>{for(var o in n)t.o(n,o)&&!t.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:n[o]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)};const e=jQuery;var n=t.n(e);const o=lodash,r=wp.i18n;function i(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=rankMath.links[t]||"";if(!n)return"#";if(!e)return n;var o={utm_source:"Plugin",utm_medium:encodeURIComponent(e),utm_campaign:"WP"};return n+"?"+Object.keys(o).map((function(t){return"".concat(t,"=").concat(o[t])})).join("&")}function a(t){return a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},a(t)}function s(t,e){for(var n=0;n<e.length;n++){var o=e[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,(r=o.key,i=void 0,i=function(t,e){if("object"!==a(t)||null===t)return t;var n=t[Symbol.toPrimitive];if(void 0!==n){var o=n.call(t,e||"default");if("object"!==a(o))return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===e?String:Number)(t)}(r,"string"),"symbol"===a(i)?i:String(i)),o)}var r,i}var u=function(){function t(){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.pointers=this.getPointers(),this.showPointer=this.showPointer.bind(this),this.init()}var e,a,u;return e=t,(a=[{key:"init",value:function(){var t=this;(0,o.forEach)(this.pointers,(function(e,n){return t.showPointer(n),!1})),n()(".rank-math-toolbar-score").parent().hasClass("is-pressed")||n()(".rank-math-toolbar-score").parent().trigger("click")}},{key:"showPointer",value:function(t){var e=this,o=this.pointers[t],i=n().extend(o.options,{pointerClass:"wp-pointer rm-pointer",close:function(){o.next&&e.showPointer(o.next)},buttons:function(t,e){var o="wp-pointer-3"===e.pointer[0].id?(0,r.__)("Finish","rank-math-pro"):(0,r.__)("Next","rank-math-pro"),i=n()('<a class="close" href="#">'+(0,r.__)("Dismiss","rank-math-pro")+"</a>"),a=n()('<a class="button button-primary" href="#">'+o+"</a>"),s=n()('<div class="rm-pointer-buttons" />');return i.on("click.pointer",(function(t){t.preventDefault(),e.element.pointer("destroy")})),a.on("click.pointer",(function(t){t.preventDefault(),e.element.pointer("close")})),s.append(i),s.append(a),s}}),a=n()(o.target).pointer(i);a.pointer("open"),o.next_trigger&&n()(o.next_trigger.target).on(o.next_trigger.event,(function(){setTimeout((function(){a.pointer("close")}),400)}))}},{key:"getPointers",value:function(){return{title:{target:".editor-post-title__input",next:"schema",options:{content:"<h3>"+(0,r.__)("Local Business Name","rank-math-pro")+"</h3><p>"+(0,r.__)("Give your business's new location a name here. This field is required and will be visible to users.","rank-math-pro")+"</p>"}},schema:{target:".components-tab-panel__tabs-item.rank-math-schema-tab",next:"content",options:{content:"<h3>"+(0,r.__)("Local Business Schema","rank-math-pro")+"</h3><p>"+(0,r.__)('Add your local business\'s details here with "Local Business" Schema Markup in order to be eligible for local SERP features.',"rank-math-pro")+"</p>",position:{edge:"right",align:"left"}}},content:{target:".is-root-container",next:"submitdiv",options:{content:"<h3>"+(0,r.__)("Show Business Information","rank-math-pro")+"</h3><p>"+(0,r.sprintf)((0,r.__)("Make sure to add the Local Business Block or %s to display your business data.","rank-math-pro"),'<a href="'+i("location-data-shortcode","Show Business Information")+'" target="_blank">[rank_math_local] shortcode</a>')+"</p>",position:{edge:"bottom",align:"middle"}}},submitdiv:{target:".editor-post-publish-button__button",next:"",options:{content:"<h3>"+(0,r.__)("Publish your location!","rank-math-pro")+"</h3><p>"+(0,r.__)("When you're done editing, don't forget to hit \"publish\" to create this location.","rank-math-pro")+"</p>"}}}}}])&&s(e.prototype,a),u&&s(e,u),Object.defineProperty(e,"prototype",{writable:!1}),t}();n()(window).on("load",(function(){new u}))})();

View File

@@ -0,0 +1,87 @@
<?php
/**
* The Local_Seo Module
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Local_Seo;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Admin\Admin_Helper;
use RankMath\Sitemap\Router;
defined( 'ABSPATH' ) || exit;
/**
* Admin class.
*/
class Admin {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->filter( 'rank_math/settings/title', 'add_settings' );
$this->filter( 'rank_math/settings/sitemap', 'add_sitemap_settings', 11 );
$this->filter( 'rank_math/settings/snippet/types', 'add_local_business_schema_type', 10, 2 );
}
/**
* Add module settings into general optional panel.
*
* @param array $tabs Array of option panel tabs.
*
* @return array
*/
public function add_settings( $tabs ) {
$tabs['local']['file'] = dirname( __FILE__ ) . '/views/titles-options.php';
return $tabs;
}
/**
* Add module settings into general optional panel.
*
* @param array $tabs Array of option panel tabs.
*
* @return array
*/
public function add_sitemap_settings( $tabs ) {
$sitemap_url = Router::get_base_url( 'locations.kml' );
$tabs['kml-file'] = [
'icon' => 'rm-icon rm-icon-local-seo',
'title' => esc_html__( 'Local Sitemap', 'rank-math-pro' ),
'desc' => wp_kses_post( sprintf( __( 'KML is a file format used to display geographic data in an Earth browser such as Google Earth. More information: <a href="%s" target="_blank">Locations KML</a>', 'rank-math-pro' ), KB::get( 'kml-sitemap', 'Options Panel Sitemap Local Tab' ) ) ),
'file' => dirname( __FILE__ ) . '/views/sitemap-settings.php',
/* translators: KML File Url */
'after_row' => '<div class="notice notice-alt notice-info info inline rank-math-notice"><p>' . sprintf( esc_html__( 'Your Locations KML file can be found here: %s', 'rank-math-pro' ), '<a href="' . $sitemap_url . '" target="_blank">' . $sitemap_url . '</a>' ) . '</p></div>',
];
return $tabs;
}
/**
* Add Pro schema types in Schema settings choices array.
*
* @param array $types Schema types.
* @param string $post_type Post type.
*/
public function add_local_business_schema_type( $types, $post_type ) {
if ( 'rank_math_locations' === $post_type ) {
$types = [
'off' => esc_html__( 'None', 'rank-math-pro' ),
'LocalBusiness' => esc_html__( 'Local Business', 'rank-math-pro' ),
];
}
return $types;
}
}

View File

@@ -0,0 +1,199 @@
<?php
/**
* The Local_Seo Module
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Local_Seo;
use RankMath\Helper;
use RankMath\Post;
use RankMath\Traits\Hooker;
use RankMath\Schema\DB;
defined( 'ABSPATH' ) || exit;
/**
* Frontend class.
*/
class Frontend {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'rank_math/json_ld', 'add_location_schema', 100, 2 );
$this->action( 'rank_math/head', 'add_location_tags', 90 );
new Search();
}
/**
* Add Locations Metatags to head.
*/
public function add_location_tags() {
if ( ! is_singular( 'rank_math_locations' ) ) {
return;
}
$schema = DB::get_schemas( Post::get_page_id() );
if ( empty( $schema ) ) {
return;
}
$schema = current( $schema );
$meta_tags = [
'placename' => ! empty( $schema['address']['addressLocality'] ) ? $schema['address']['addressLocality'] : '',
'position' => ! empty( $schema['geo']['latitude'] ) ? $schema['geo']['latitude'] . ';' . $schema['geo']['longitude'] : '',
'region' => ! empty( $schema['address']['addressCountry'] ) ? $schema['address']['addressCountry'] : '',
];
foreach ( $meta_tags as $name => $value ) {
if ( ! $value ) {
continue;
}
printf( '<meta name="geo.%1$s" content="%2$s" />' . "\n", esc_attr( $name ), esc_attr( $value ) );
}
}
/**
* Add Locations Schema.
*
* @param array $data Array of json-ld data.
* @param JsonLD $jsonld Instance of jsonld.
*
* @return array
*/
public function add_location_schema( $data, $jsonld ) {
if ( ! is_singular( 'rank_math_locations' ) ) {
return $data;
}
global $post;
$schemas = DB::get_schemas( $post->ID );
$schema_key = key( $schemas );
if ( ! isset( $data[ $schema_key ] ) ) {
return $data;
}
$entity = $data[ $schema_key ];
$this->add_place_entity( $data, $entity, $jsonld, ! empty( $schemas[ $schema_key ]['metadata']['open247'] ) );
$this->validate_publisher_data( $data, $jsonld );
$data[ $schema_key ] = $this->validate_locations_data( $entity, $data );
return $data;
}
/**
* Add Schema Place entity on Rank Math locations posts.
*
* @param array $data Array of json-ld data.
* @param array $entity Location data.
* @param JsonLD $jsonld Instance of jsonld.
* @param bool $is_open247 Whether business is open 24*7.
*/
private function add_place_entity( &$data, &$entity, $jsonld, $is_open247 ) {
$properties = [];
foreach ( [ 'openingHoursSpecification', 'geo' ] as $property ) {
if ( isset( $entity[ $property ] ) ) {
$properties[ $property ] = $entity[ $property ];
}
}
if ( isset( $entity['address'] ) ) {
$properties['address'] = $entity['address'];
}
if ( $is_open247 ) {
$properties['openingHoursSpecification'] = [
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
],
'opens' => '00:00',
'closes' => '23:59',
];
}
if ( empty( $properties ) ) {
return;
}
$data['place'] = array_merge(
[
'@type' => 'Place',
'@id' => $jsonld->parts['canonical'] . '#place',
],
$properties
);
}
/**
* Change Publisher Data when multiple locations option is enabled.
*
* @param array $data Array of json-ld data.
* @param JsonLD $jsonld Instance of jsonld.
*
* @return array
*/
private function validate_publisher_data( &$data, $jsonld ) {
if ( empty( $data['publisher'] ) ) {
return;
}
$data['publisher'] = [
'@type' => 'Organization',
'@id' => $data['publisher']['@id'],
'name' => $jsonld->get_website_name(),
'logo' => [
'@type' => 'ImageObject',
'url' => Helper::get_settings( 'titles.knowledgegraph_logo' ),
],
];
}
/**
* Validate Locations data before adding it in ld+json.
*
* @param array $entity Location data.
* @param array $data Array of json-ld data.
*
* @return array
*/
private function validate_locations_data( $entity, $data ) {
// Remove invalid properties.
foreach ( [ 'isPartOf', 'publisher', 'inLanguage' ] as $property ) {
if ( isset( $entity[ $property ] ) ) {
unset( $entity[ $property ] );
}
}
// Add Parent Organization.
if (
! empty( $data['publisher'] ) &&
Helper::get_settings( 'titles.same_organization_locations', false )
) {
$entity['parentOrganization'] = [ '@id' => $data['publisher']['@id'] ];
}
// Add reference to the place entity.
if ( isset( $data['place'] ) ) {
$entity['location'] = [ '@id' => $data['place']['@id'] ];
}
return $entity;
}
}

View File

@@ -0,0 +1,96 @@
<?php
/**
* The KML File
*
* @since 2.1.2
* @package RankMath
* @subpackage RankMathPro\Local_Seo
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Local_Seo;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Sitemap\Cache_Watcher;
use RankMath\Schema\DB;
defined( 'ABSPATH' ) || exit;
/**
* KML_File class.
*/
class KML_File {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->filter( 'rank_math/sitemap/locations/data', 'add_location_data' );
$this->action( 'save_post_rank_math_locations', 'save_post' );
}
/**
* Check for relevant post type before invalidation.
*
* @param int $post_id Post ID to possibly invalidate for.
*/
public function save_post( $post_id ) {
if (
wp_is_post_revision( $post_id ) ||
false === Helper::is_post_indexable( $post_id )
) {
return false;
}
Cache_Watcher::invalidate( 'locations' );
}
/**
* Generate the KML file contents.
*
* @return string $kml KML file content.
*/
public function add_location_data() {
$rm_locations = get_posts(
[
'post_type' => 'rank_math_locations',
'numberposts' => -1,
]
);
if ( empty( $rm_locations ) ) {
return [];
}
$locations = [];
foreach ( $rm_locations as $rm_location ) {
$locations_data = current( DB::get_schemas( $rm_location->ID ) );
if ( empty( $locations_data ) ) {
continue;
}
rank_math()->variables->setup();
$name = ! empty( $locations_data['name'] ) ? $locations_data['name'] : '%seo_title%';
$description = ! empty( $locations_data['description'] ) ? $locations_data['description'] : '%seo_description%';
$address = '';
if ( ! empty( $locations_data['address'] ) && isset( $locations_data['address']['@type'] ) ) {
unset( $locations_data['address']['@type'] );
$address = $locations_data['address'];
}
$locations[] = [
'name' => Helper::replace_vars( $name, $rm_location ),
'description' => Helper::replace_vars( $description, $rm_location ),
'email' => ! empty( $locations_data['email'] ) ? Helper::replace_vars( $locations_data['email'], $rm_location ) : '',
'phone' => ! empty( $locations_data['telephone'] ) ? Helper::replace_vars( $locations_data['telephone'], $rm_location ) : '',
'url' => get_the_permalink( $rm_location ),
'address' => ! empty( $locations_data['address'] ) ? $locations_data['address'] : '',
'coords' => ! empty( $locations_data['geo'] ) ? $locations_data['geo'] : '',
];
}
return $locations;
}
}

View File

@@ -0,0 +1,315 @@
<?php
/**
* The Local_Seo Module
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Local_Seo;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Sitemap\Cache_Watcher;
defined( 'ABSPATH' ) || exit;
/**
* Local_Seo class.
*/
class Local_Seo {
use Hooker;
/**
* Post Singular Name.
*
* @var string
*/
private $post_singular_name = 'Location';
/**
* Post Type.
*
* @var string
*/
private $post_type = 'rank_math_locations';
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'init', 'init' );
$this->action( 'rank_math/schema/update', 'update_post_schema_info', 10, 2 );
$this->action( 'save_post', 'invalidate_cache' );
$this->filter( 'classic_editor_enabled_editors_for_post_type', 'force_block_editor', 20, 2 );
$this->filter( 'rank_math/sitemap/locations', 'add_kml_file' );
$this->filter( "manage_{$this->post_type}_posts_columns", 'posts_columns' );
$this->filter( "manage_{$this->post_type}_posts_custom_column", 'posts_custom_column', 10, 2 );
$this->filter( "bulk_actions-edit-{$this->post_type}", 'post_bulk_actions', 20 );
$this->includes();
}
/**
* Update post info for analytics.
*
* @param int $object_id Object ID.
* @param array $schemas Schema data.
* @param string $object_type Object type.
*/
public function update_post_schema_info( $object_id, $schemas, $object_type = 'post' ) {
if ( 'post' !== $object_type || 'rank_math_locations' !== get_post_type( $object_id ) ) {
return;
}
$schema = current( $schemas );
if ( ! isset( $schema['geo'], $schema['geo']['latitude'], $schema['geo']['longitude'] ) ) {
return;
}
update_post_meta( $object_id, 'rank_math_local_business_latitide', $schema['geo']['latitude'] );
update_post_meta( $object_id, 'rank_math_local_business_longitude', $schema['geo']['longitude'] );
}
/**
* Intialize.
*/
public function init() {
if ( ! Helper::get_settings( 'titles.use_multiple_locations', false ) ) {
return;
}
$this->post_singular_name = Helper::get_settings( 'titles.locations_post_type_label', 'Location' );
$this->register_location_post_type();
$this->register_location_taxonomy();
}
/**
* Filters the editors that are enabled for the post type.
*
* @param array $editors Associative array of the editors and whether they are enabled for the post type.
* @param string $post_type The post type.
*/
public function force_block_editor( $editors, $post_type ) {
if ( 'rank_math_locations' !== $post_type || ! $this->do_filter( 'schema/cpt_force_gutenberg', true ) ) {
return $editors;
}
$editors['classic_editor'] = false;
return $editors;
}
/**
* Add Locations KML file in the sitemap
*/
public function add_kml_file() {
return Helper::get_settings( 'sitemap.local_sitemap', true );
}
/**
* Include required files.
*/
private function includes() {
if ( is_admin() ) {
new Admin();
new RM_Pointers();
return;
}
if ( Helper::get_settings( 'titles.use_multiple_locations', false ) ) {
new Frontend();
new Location_Shortcode();
new KML_File();
}
}
/**
* Register Locations post type.
*/
private function register_location_post_type() {
$plural_label = Helper::get_settings( 'titles.locations_post_type_plural_label', 'RM Locations' );
$post_type_slug = Helper::get_settings( 'titles.locations_post_type_base', 'locations' );
$labels = [
'name' => $this->post_singular_name,
'singular_name' => $this->post_singular_name,
'menu_name' => $plural_label,
/* translators: Post Type Plural Name */
'all_items' => sprintf( esc_html__( 'All %s', 'rank-math-pro' ), $plural_label ),
/* translators: Post Type Singular Name */
'add_new_item' => sprintf( esc_html__( 'Add New %s', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'new_item' => sprintf( esc_html__( 'New %s', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'edit_item' => sprintf( esc_html__( 'Edit %s', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'update_item' => sprintf( esc_html__( 'Update %s', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'view_item' => sprintf( esc_html__( 'View %s', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Plural Name */
'view_items' => sprintf( esc_html__( 'View %s', 'rank-math-pro' ), $plural_label ),
/* translators: Post Type Singular Name */
'search_items' => sprintf( esc_html__( 'Search %s', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'not_found' => sprintf( esc_html__( 'No %s found.', 'rank-math-pro' ), $plural_label ),
/* translators: Post Type Singular Name */
'not_found_in_trash' => sprintf( esc_html__( 'No %s found in Trash.', 'rank-math-pro' ), $plural_label ),
/* translators: Post Type Singular Name */
'item_published' => sprintf( esc_html__( '%s published.', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'item_published_privately' => sprintf( esc_html__( '%s published privately.', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'item_reverted_to_draft' => sprintf( esc_html__( '%s reverted to draft.', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'item_scheduled' => sprintf( esc_html__( '%s scheduled.', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'item_updated' => sprintf( esc_html__( '%s updated.', 'rank-math-pro' ), $this->post_singular_name ),
];
$capability = 'rank_math_general';
$args = [
'label' => $this->post_singular_name,
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'hierarchical' => false,
'has_archive' => $post_type_slug,
'menu_icon' => 'dashicons-location',
'query_var' => true,
'show_in_rest' => true,
'rest_base' => 'rank-math-locations',
'supports' => [ 'title', 'editor', 'excerpt', 'author', 'thumbnail', 'revisions', 'custom-fields', 'page-attributes', 'publicize' ],
'rewrite' => [
'slug' => $post_type_slug,
'with_front' => $this->filter( 'locations/front', true ),
],
'capabilities' => [
'edit_post' => $capability,
'read_post' => $capability,
'delete_post' => $capability,
'edit_posts' => $capability,
'edit_others_posts' => $capability,
'publish_posts' => $capability,
'read_private_posts' => $capability,
'create_posts' => $capability,
],
];
register_post_type( $this->post_type, $args );
}
/**
* Register Locations Category taxonomy.
*/
private function register_location_taxonomy() {
$category_slug = esc_html( Helper::get_settings( 'titles.locations_category_base', 'locations-category' ) );
$labels = [
/* translators: Post Type Singular Name */
'name' => sprintf( esc_html__( '%s categories', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'singular_name' => sprintf( esc_html__( '%s category', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'all_items' => sprintf( esc_html__( 'All %s categories', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'edit_item' => sprintf( esc_html__( 'Edit %s category', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'update_item' => sprintf( esc_html__( 'Update %s category', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'add_new_item' => sprintf( esc_html__( 'Add New %s category', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'new_item_name' => sprintf( esc_html__( 'New %s category', 'rank-math-pro' ), $this->post_singular_name ),
/* translators: Post Type Singular Name */
'menu_name' => sprintf( esc_html__( '%s categories', 'rank-math-pro' ), $this->post_singular_name ),
'search_items' => esc_html__( 'Search categories', 'rank-math-pro' ),
'parent_item' => esc_html__( 'Parent Category', 'rank-math-pro' ),
'parent_item_colon' => esc_html__( 'Parent Category:', 'rank-math-pro' ),
];
$args = [
'hierarchical' => true,
'labels' => $labels,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'show_in_rest' => true,
'rewrite' => [ 'slug' => $category_slug ],
];
register_taxonomy( 'rank_math_location_category', [ $this->post_type ], $args );
}
/**
* Check for relevant post type before invalidation.
*
* @param int $post_id Post ID to possibly invalidate for.
*/
public function invalidate_cache( $post_id ) {
if ( get_post_type( $post_id ) !== $this->post_type ) {
return false;
}
Cache_Watcher::clear( [ 'locations' ] );
}
/**
* Add custom columns for post type.
*
* @param array $columns Current columns.
*
* @return array
*/
public function posts_columns( $columns ) {
$columns['address'] = __( 'Address', 'rank-math-pro' );
$columns['telephone'] = __( 'Phone', 'rank-math-pro' );
return $columns;
}
/**
* Add the content in the custom columns.
*
* @param string $column Column name.
* @param int $post_id Post ID.
*
* @return void
*/
public function posts_custom_column( $column, $post_id ) {
$schemas = \RankMath\Schema\DB::get_schemas( $post_id );
if ( empty( $schemas ) ) {
return;
}
$schema = reset( $schemas );
if ( empty( $schema[ $column ] ) ) {
return;
}
switch ( $column ) {
case 'address':
unset( $schema['address']['@type'] );
echo esc_html( join( ' ', $schema['address'] ) );
break;
case 'telephone':
echo esc_html( $schema['telephone'] );
break;
}
}
/**
* Remove unneeded bulk actions.
*
* @param array $actions Actions.
* @return array New actions.
*/
public function post_bulk_actions( $actions ) {
unset( $actions['rank_math_bulk_schema_none'], $actions['rank_math_bulk_schema_default'] );
return $actions;
}
}

View File

@@ -0,0 +1,567 @@
<?php
/**
* The Schema Shortcode
*
* @since 1.0.24
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Local_Seo;
use RankMath\Helper;
use RankMath\Schema\DB;
use RankMath\Traits\Hooker;
use RankMath\Traits\Shortcode;
use MyThemeShop\Helpers\Str;
defined( 'ABSPATH' ) || exit;
/**
* Snippet_Shortcode class.
*/
class Location_Shortcode {
use Hooker, Shortcode;
/**
* Shortcode attributes.
*
* @var array
*/
public $atts = [];
/**
* Address Instance.
*
* @var Address
*/
public $address;
/**
* Opening Hours Instance.
*
* @var Opening_Hours
*/
public $opening_hours;
/**
* Map Instance.
*
* @var Map
*/
public $map;
/**
* Store Locator Instance.
*
* @var Store_Locator
*/
public $store_locator;
/**
* API Key.
*
* @var string
*/
public $api_key;
/**
* The Constructor.
*/
public function __construct() {
$this->address = new Address();
$this->opening_hours = new Opening_Hours();
$this->map = new Map();
$this->store_locator = new Store_Locator();
$this->api_key = Helper::get_settings( 'titles.maps_api_key' );
// Add Yoast compatibility shortcodes.
$this->add_shortcode( 'wpseo_all_locations', 'yoast_locations' );
$this->add_shortcode( 'wpseo_storelocator', 'yoast_store_locator' );
$this->add_shortcode( 'wpseo_opening_hours', 'yoast_opening_hours' );
$this->add_shortcode( 'wpseo_map', 'yoast_map' );
$this->add_shortcode( 'rank_math_local', 'local_shortcode' );
$this->action( 'wp_enqueue_scripts', 'enqueue' );
$this->action( 'wp_enqueue_scripts', 'enqueue' );
if ( ! function_exists( 'register_block_type' ) ) {
return;
}
register_block_type(
'rank-math/local-business',
[
'render_callback' => [ $this, 'local_shortcode' ],
'attributes' => $this->get_attributes(),
]
);
}
/**
* Enqueue Map scripts.
*/
public function enqueue() {
if ( ! $this->api_key ) {
return;
}
wp_register_script( 'rank-math-google-maps', '//maps.googleapis.com/maps/api/js?&key=' . rawurlencode( $this->api_key ), [], rank_math_pro()->version, true );
wp_register_script( 'rank-math-google-maps-cluster', 'https://developers-dot-devsite-v2-prod.appspot.com/maps/documentation/javascript/examples/markerclusterer/markerclustererplus@4.0.1.min.js', [], rank_math_pro()->version, true );
wp_register_script( 'rank-math-local', RANK_MATH_PRO_URL . 'includes/modules/local-seo/assets/js/rank-math-local.js', [ 'jquery', 'lodash', 'rank-math-google-maps', 'rank-math-google-maps-cluster' ], rank_math_pro()->version, true );
}
/**
* Location shortcode.
*
* @param array $atts Optional. Shortcode arguments.
*
* @return string Shortcode output.
*/
public function local_shortcode( $atts ) {
$defaults = [];
foreach ( $this->get_attributes() as $key => $attribute ) {
$defaults[ $key ] = $attribute['default'];
}
$this->atts = shortcode_atts(
$defaults,
$atts,
'rank_math_local'
);
if ( ! $this->api_key && is_user_logged_in() && in_array( $this->atts['type'], [ 'store-locator', 'map' ], true ) ) {
return sprintf(
/* Translators: %s expands to General Settings Link. */
esc_html__( 'This page can\'t load Google Maps correctly. Please add %s.', 'rank-math-pro' ),
'<a href="' . Helper::get_admin_url( 'options-titles#setting-panel-local' ) . '" target="_blank">' . esc_html__( 'API Key', 'rank-math-pro' ) . '</a>'
);
}
wp_enqueue_style( 'rank-math-local-business', RANK_MATH_PRO_URL . 'includes/modules/local-seo/assets/css/local-business.css', null, rank_math_pro()->version );
if ( 'store-locator' === $this->atts['type'] ) {
return $this->store_locator->get_data( $this );
}
return $this->get_shortcode_data();
}
/**
* Yoast Map compatibility functionality.
*
* @param array $atts Array of arguments.
* @return string
*/
public function yoast_map( $atts ) {
$atts['type'] = 'map';
return $this->yoast_locations( $atts );
}
/**
* Yoast Opening Hours compatibility functionality.
*
* @param array $atts Array of arguments.
* @return string
*/
public function yoast_opening_hours( $atts ) {
$atts['type'] = 'opening-hours';
return $this->yoast_locations( $atts );
}
/**
* Yoast Store Locator compatibility functionality.
*
* @param array $atts Array of arguments.
* @return string
*/
public function yoast_store_locator( $atts ) {
$atts['type'] = 'store-locator';
return $this->yoast_locations( $atts );
}
/**
* Yoast locations compatibility functionality.
*
* @param array $args Array of arguments.
* @return string
*/
public function yoast_locations( $args ) {
$defaults = [
'id' => '',
'number' => -1,
'type' => 'address',
'term_id' => '',
'orderby' => 'menu_order title',
'order' => 'ASC',
'show_state' => true,
'show_country' => true,
'show_phone' => true,
'show_phone_2' => true,
'show_fax' => true,
'show_email' => true,
'show_url' => false,
'show_logo' => false,
'show_opening_hours' => false,
'hide_closed' => false,
'oneline' => false,
'echo' => false,
'comment' => '',
'radius' => 10,
'max_number' => '',
'show_radius' => false,
'show_nearest_suggestion' => true,
'show_map' => true,
'show_filter' => false,
'map_width' => '100%',
'scrollable' => true,
'draggable' => true,
'marker_clustering' => false,
'map_style' => 'ROADMAP',
'show_route' => true,
'show_route_label' => '',
'show_category_filter' => false,
'height' => 300,
'zoom' => -1,
'show_open_label' => false,
'show_days' => '',
'center' => '',
'default_show_infowindow' => false,
];
$new_atts = [];
$atts = shortcode_atts( $defaults, $args, 'wpseo_local_show_all_locations' );
$data = [
'type' => 'type',
'id' => 'locations',
'number' => 'limit',
'max_number' => 'limit',
'term_id' => 'terms',
'show_state' => 'show_state',
'show_country' => 'show_country',
'show_phone' => 'show_telephone',
'show_phone_2' => 'show_secondary_number',
'show_fax' => 'show_fax',
'show_email' => 'show_email',
'show_url' => 'show_url',
'show_logo' => 'show_logo',
'show_opening_hours' => 'show_opening_hours',
'show_days' => 'show_days',
'hide_closed' => 'hide_closed_days',
'oneline' => 'show_on_one_line',
'comment' => 'opening_hours_note',
'radius' => 'search_radius',
'show_radius' => 'show_radius',
'show_nearest_suggestion' => 'show_nearest_location',
'show_map' => 'show_map',
'show_filter' => 'show_category_filter',
'map_width' => 'map_width',
'map_height' => 'map_height',
'center' => 'map_center',
'zoom' => 'zoom_level',
'scrollable' => 'allow_scrolling',
'draggable' => 'allow_dragging',
'marker_clustering' => 'show_marker_clustering',
'map_style' => 'map_style',
'show_route' => 'show_route_planner',
'show_route_label' => 'route_label',
'show_category_filter' => 'show_category_filter',
'default_show_infowindow' => 'show_infowindow',
];
foreach ( $atts as $key => $value ) {
if ( ! isset( $data[ $key ] ) ) {
continue;
}
$new_atts[ $data[ $key ] ] = $value;
}
return $this->local_shortcode( $new_atts );
}
/**
* Get Location Shortcode data.
*
* @return string Shortcode data.
*/
private function get_shortcode_data() {
$args = [
'post_type' => 'rank_math_locations',
'numberposts' => empty( $this->atts['limit'] ) ? -1 : (int) $this->atts['limit'],
'include' => (int) $this->atts['locations'],
];
if ( ! empty( $this->atts['terms'] ) ) {
$args['tax_query'] = [
[
'taxonomy' => 'rank_math_location_category',
'field' => 'term_id',
'terms' => $this->atts['terms'],
],
];
}
/**
* Filter to change Locations query args.
*
* @param array $args Arguments to retrieve locations.
* @return array $args.
*/
$args = $this->do_filter( 'location_args', $args );
$locations = get_posts( $args );
if ( empty( $locations ) ) {
return esc_html__( 'Sorry, no locations were found.', 'rank-math-pro' );
}
if ( 'map' === $this->atts['type'] ) {
return $this->map->get_data( $this, $locations );
}
$data = '';
foreach ( $locations as $location ) {
$schema = DB::get_schemas( $location->ID );
if ( empty( $schema ) ) {
continue;
}
$schema = current( $this->replace_variables( $schema, $location ) );
$data .= '<div class="rank-math-business-wrapper">';
$data .= $this->get_title( $schema );
$data .= $this->get_image( $schema );
if ( 'address' === $this->atts['type'] ) {
$data .= $this->address->get_data( $this, $schema );
}
if ( 'opening-hours' === $this->atts['type'] || $this->atts['show_opening_hours'] ) {
$data .= $this->opening_hours->get_data( $this, $schema );
}
$data .= '</div>';
}
return $data;
}
/**
* Get Location Title.
*
* @param Object $schema Location schema data.
*
* @return string Shortcode data.
*/
public function get_title( $schema ) {
if ( empty( $this->atts['show_company_name'] ) || empty( $schema['name'] ) ) {
return;
}
return '<h3 class="rank-math-business-name">' . esc_html( $schema['name'] ) . '</h3>';
}
/**
* Get Location Image.
*
* @param Object $schema Schema Data.
*
* @return string Shortcode data.
*/
public function get_image( $schema ) {
if ( empty( $this->atts['show_logo'] ) || empty( $schema['image'] ) ) {
return;
}
return '<div class="rank-math-business-image"><img src="' . esc_url( $schema['image']['url'] ) . '" /><div>';
}
/**
* Replace variable.
*
* @param array $schemas Schema to replace.
* @param object $location Location Post Object.
* @return array
*/
public function replace_variables( $schemas, $location = [] ) {
$new_schemas = [];
foreach ( $schemas as $key => $schema ) {
if ( is_array( $schema ) ) {
$new_schemas[ $key ] = $this->replace_variables( $schema, $location );
continue;
}
$new_schemas[ $key ] = Str::contains( '%', $schema ) ? Helper::replace_seo_fields( $schema, $location ) : $schema;
}
return $new_schemas;
}
/**
* Shortcode & Block default attributes.
*/
private function get_attributes() {
return [
'type' => [
'default' => 'address',
'type' => 'string',
],
'locations' => [
'default' => '',
'type' => 'string',
],
'terms' => [
'default' => [],
'type' => 'array',
],
'limit' => [
'default' => Helper::get_settings( 'titles.limit_results', 10 ),
'type' => 'integer',
],
'show_company_name' => [
'type' => 'boolean',
'default' => true,
],
'show_company_address' => [
'type' => 'boolean',
'default' => true,
],
'show_on_one_line' => [
'type' => 'boolean',
'default' => false,
],
'show_state' => [
'type' => 'boolean',
'default' => true,
],
'show_country' => [
'type' => 'boolean',
'default' => true,
],
'show_telephone' => [
'type' => 'boolean',
'default' => true,
],
'show_secondary_number' => [
'type' => 'boolean',
'default' => true,
],
'show_fax' => [
'type' => 'boolean',
'default' => false,
],
'show_email' => [
'type' => 'boolean',
'default' => true,
],
'show_url' => [
'type' => 'boolean',
'default' => true,
],
'show_logo' => [
'type' => 'boolean',
'default' => true,
],
'show_vat_id' => [
'type' => 'boolean',
'default' => false,
],
'show_tax_id' => [
'type' => 'boolean',
'default' => false,
],
'show_coc_id' => [
'type' => 'boolean',
'default' => false,
],
'show_priceRange' => [
'type' => 'boolean',
'default' => false,
],
'show_opening_hours' => [
'type' => 'boolean',
'default' => false,
],
'show_days' => [
'type' => 'string',
'default' => 'Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday',
],
'hide_closed_days' => [
'type' => 'boolean',
'default' => false,
],
'show_opening_now_label' => [
'type' => 'boolean',
'default' => false,
],
'opening_hours_note' => [
'type' => 'string',
'default' => 'Open Now',
],
'show_map' => [
'type' => 'boolean',
'default' => false,
],
'map_style' => [
'type' => 'string',
'default' => Helper::get_settings( 'titles.map_style', 'roadmap' ),
],
'map_width' => [
'type' => 'string',
'default' => '100%',
],
'map_height' => [
'type' => 'string',
'default' => '300px',
],
'zoom_level' => [
'type' => 'integer',
'default' => -1,
],
'allow_zoom' => [
'type' => 'boolean',
'default' => true,
],
'allow_scrolling' => [
'type' => 'boolean',
'default' => true,
],
'allow_dragging' => [
'type' => 'boolean',
'default' => true,
],
'show_route_planner' => [
'type' => 'boolean',
'default' => true,
],
'route_label' => [
'type' => 'string',
'default' => Helper::get_settings( 'titles.route_label' ),
],
'show_category_filter' => [
'type' => 'boolean',
'default' => false,
],
'show_marker_clustering' => [
'type' => 'boolean',
'default' => true,
],
'show_infowindow' => [
'type' => 'boolean',
'default' => true,
],
'show_radius' => [
'type' => 'boolean',
'default' => true,
],
'show_nearest_location' => [
'type' => 'boolean',
'default' => true,
],
'search_radius' => [
'type' => 'string',
'default' => '10',
],
];
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* The Rank Math Tutorial class.
*
* @since 2.1.2
* @package RankMath
* @subpackage RankMathPro
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2020, WooCommerce
* The following code is a derivative work of the code from the WooCommerce(https://github.com/woocommerce/woocommerce), which is licensed under GPL v3.
*/
namespace RankMathPro\Local_Seo;
use RankMath\Traits\Hooker;
use MyThemeShop\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* RM_Pointers Class.
*/
class RM_Pointers {
use Hooker;
/**
* Constructor.
*/
public function __construct() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$this->action( 'admin_enqueue_scripts', 'enqueue_pointers' );
}
/**
* Enqueue pointers and add script to page.
*/
public function enqueue_pointers() {
$screen = get_current_screen();
if ( ! $screen || 'rank_math_locations' !== $screen->id ) {
return;
}
if ( get_option( 'rank_math_remove_locations_tutorial' ) ) {
return;
}
update_option( 'rank_math_remove_locations_tutorial', true );
wp_enqueue_style( 'wp-pointer' );
wp_enqueue_script(
'rank-math-pro-pointers',
RANK_MATH_PRO_URL . 'includes/modules/local-seo/assets/js/rank-math-pointers.js',
[
'jquery',
'wp-pointer',
'wp-i18n',
'lodash',
],
rank_math_pro()->version,
true
);
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* The Local_Seo Module
*
* @since 1.0.0
* @package RankMathPro
* @subpackage RankMath
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Local_Seo;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Search class.
*/
class Search {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'wp', 'integrations' );
}
/**
* Initialize integrations.
*/
public function integrations() {
if ( ! is_search() || ! Helper::get_settings( 'titles.locations_enhanced_search' ) ) {
return;
}
$this->filter( 'the_excerpt', 'add_locations_data' );
}
/**
* Add Locations data to search results.
*
* @param string $excerpt Post excerpt.
* @return string $excerpt Processed excerpt.
*/
public function add_locations_data( $excerpt ) {
global $post;
if ( get_post_type( $post->ID ) !== 'rank_math_locations' ) {
return $excerpt;
}
$excerpt .= do_shortcode( '[rank_math_local type="address" locations="' . $post->ID . '" show_company_name=""]' );
return $excerpt;
}
}

View File

@@ -0,0 +1,122 @@
<?php
/**
* The Address shortcode Class.
*
* @since 1.0.1
* @package RankMath
* @subpackage RankMathPro
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Local_Seo;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Address class.
*/
class Address {
/**
* Get Address Data.
*
* @param Location_Shortcode $shortcode Location_Shortcode Instance.
* @param array $schema Array of Schema data.
*
* @return string
*/
public function get_data( $shortcode, $schema ) {
$atts = $shortcode->atts;
$data = $this->get_address( $schema, $atts );
$schema = $schema['metadata'] + $schema;
$labels = [
'telephone' => [
'key' => 'telephone',
'label' => esc_html__( 'Phone', 'rank-math-pro' ),
],
'secondary_number' => [
'key' => 'secondary_number',
'label' => esc_html__( 'Secondary phone', 'rank-math-pro' ),
],
'fax' => [
'key' => 'faxNumber',
'label' => esc_html__( 'Fax', 'rank-math-pro' ),
],
'email' => [
'key' => 'email',
'label' => esc_html__( 'Email', 'rank-math-pro' ),
],
'url' => [
'key' => 'url',
'label' => esc_html__( 'URL', 'rank-math-pro' ),
],
'vat_id' => [
'key' => 'vatID',
'label' => esc_html__( 'VAT ID', 'rank-math-pro' ),
],
'tax_id' => [
'key' => 'taxID',
'label' => esc_html__( 'Tax ID', 'rank-math-pro' ),
],
'coc_id' => [
'key' => 'coc_id',
'label' => esc_html__( 'Chamber of Commerce ID', 'rank-math-pro' ),
],
'priceRange' => [
'key' => 'priceRange',
'label' => esc_html__( 'Price indication', 'rank-math-pro' ),
],
];
foreach ( $labels as $key => $label ) {
if ( empty( $atts[ "show_$key" ] ) || empty( $schema[ $label['key'] ] ) ) {
continue;
}
$value = esc_html( $schema[ $label['key'] ] );
if ( 'email' === $key ) {
$value = '<a href="mailto:' . $value . '">' . $value . '</a>';
}
if ( in_array( $key, [ 'telephone', 'secondary_number' ], true ) ) {
$value = '<a href="tel:' . $value . '">' . $value . '</a>';
}
$data .= '<div><strong>' . $label['label'] . '</strong>: ' . $value . '</div>';
}
return $data;
}
/**
* Get Address Data.
*
* @param array $schema Array of Schema data.
* @param array $atts Shortcode attributes.
*
* @return string
*/
public function get_address( $schema, $atts = [] ) {
$address = array_filter( $schema['address'] );
if ( false === $address || empty( $atts['show_company_address'] ) ) {
return '';
}
$hash = array_filter(
[
'streetAddress' => true,
'addressLocality' => true,
'addressRegion' => ! empty( $atts['show_state'] ),
'addressCountry' => ! empty( $atts['show_country'] ),
'postalCode' => true,
]
);
$glue = empty( $atts['show_on_one_line'] ) ? ',<br />' : ', ';
$data = implode( $glue, array_intersect_key( $address, $hash ) );
return '<h5>' . esc_html__( 'Address:', 'rank-math-pro' ) . '</h5><address>' . $data . '</address>';
}
}

View File

@@ -0,0 +1,119 @@
<?php
/**
* The Map shortcode Class.
*
* @since 1.0.1
* @package RankMath
* @subpackage RankMathPro
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Local_Seo;
use RankMath\Schema\DB;
defined( 'ABSPATH' ) || exit;
/**
* Map class.
*/
class Map {
/**
* Shortcode Instance.
*
* @var Location_Shortcode
*/
public $shortcode;
/**
* Get Address Data.
*
* @param Location_Shortcode $shortcode Location_Shortcode Instance.
* @param array $locations Locations data.
* @return string
*/
public function get_data( $shortcode, $locations ) {
$this->shortcode = $shortcode;
$atts = $shortcode->atts;
$options = [
'map_style' => $atts['map_style'],
'allow_zoom' => $atts['allow_zoom'],
'zoom_level' => $atts['zoom_level'],
'allow_dragging' => $atts['allow_dragging'],
'show_clustering' => $atts['show_marker_clustering'],
'show_infowindow' => $atts['show_infowindow'],
];
$terms_data = [];
foreach ( $locations as $location ) {
$schema = DB::get_schemas( $location->ID );
if ( empty( $schema ) ) {
continue;
}
$schema = current( $shortcode->replace_variables( $schema ) );
if ( empty( $schema['geo']['latitude'] ) || empty( $schema['geo']['longitude'] ) ) {
continue;
}
$options['locations'][ $location->ID ] = [
'content' => $this->get_infobox_content( $location->ID, $schema ),
'lat' => $schema['geo']['latitude'],
'lng' => $schema['geo']['longitude'],
];
if ( ! empty( $atts['show_category_filter'] ) && 'map' === $atts['type'] ) {
$terms = get_the_terms( $location->ID, 'rank_math_location_category' );
if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
$terms_data = array_merge( $terms_data, $terms );
$options['locations'][ $location->ID ]['terms'] = wp_list_pluck( $terms, 'term_id' );
}
}
}
if ( empty( $options['locations'] ) ) {
return;
}
wp_enqueue_script( 'rank-math-local' );
$width = ! empty( $atts['map_width'] ) ? $atts['map_width'] : '100%';
$height = ! empty( $atts['map_height'] ) ? $atts['map_height'] : '500px';
$style = sprintf( 'style="width: %s; height: %s"', $width, $height );
ob_start();
?>
<div class="rank-math-local-map-wrapper">
<div class="rank-math-local-map" data-map-options="<?php echo esc_attr( wp_json_encode( $options ) ); ?>" <?php printf( 'style="width: %s; height: %s"', esc_attr( $width ), esc_attr( $height ) ); ?>></div>
<?php
if ( ! empty( $terms_data ) ) {
echo '<select id="rank-math-select-category">';
echo '<option value="">' . esc_html__( 'Select Category', 'rank-math-pro' ) . '</option>';
foreach ( $terms_data as $term ) {
echo '<option value="' . esc_attr( $term->term_id ) . '">' . esc_html( $term->name ) . '</option>';
}
echo '</select>';
}
?>
</div>
<?php
return ob_get_clean();
}
/**
* Get Infobox Content.
*
* @param int $location_id The Location ID.
* @param array $schema Schema Data.
* @return string
*/
public function get_infobox_content( $location_id, $schema ) {
return '<div class="rank-math-infobox-wrapper">
<h5><a href="' . esc_url( get_the_permalink( $location_id ) ) . '">' . esc_html( get_the_title( $location_id ) ) . '</a></h5>
<p>' . $this->shortcode->address->get_data( $this->shortcode, $schema ) . '</p>
</div>';
}
}

View File

@@ -0,0 +1,158 @@
<?php
/**
* The Opening Hours shortcode Class.
*
* @since 1.0.1
* @package RankMath
* @subpackage RankMathPro
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Local_Seo;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Opening_Hours class.
*/
class Opening_Hours {
/**
* Get Opening_Hours Data.
*
* @param Location_Shortcode $shortcode Location_Shortcode Instance.
* @param array $schema Schema data.
* @return string
*/
public function get_data( $shortcode, $schema ) {
if ( ! isset( $schema['openingHoursSpecification'] ) ) {
return '<p>' . esc_html__( 'Open 24/7', 'rank-math-pro' ) . '</p>';
}
if ( empty( $schema['openingHoursSpecification'] ) ) {
return false;
}
$days = $this->normalize_days( $schema, $shortcode );
ob_start();
?>
<h5><?php esc_html_e( 'Opening Hours:', 'rank-math-pro' ); ?></h5>
<div class="rank-math-business-opening-hours">
<?php
foreach ( $days as $day => $hours ) {
$time = ! empty( $hours['time'] ) ? implode( ' and ', $hours['time'] ) : esc_html__( 'Closed', 'rank-math-pro' );
$time = str_replace( '-', ' &ndash; ', $time );
printf(
'<div class="rank-math-opening-hours"><span class="rank-math-opening-days">%1$s</span> : <span class="rank-math-opening-time">%2$s</span> <span class="rank-math-business-open">%3$s</span></div>',
esc_html( $this->get_localized_day( $day ) ),
esc_html( $time ),
esc_html( $hours['isOpen'] )
);
}
?>
</div>
<?php
return ob_get_clean();
}
/**
* Get Local Time.
*
* @param Location_Shortcode $shortcode Location_Shortcode Instance.
* @param array $schema Schema data.
* @return string
*/
private function get_local_time( $shortcode, $schema ) {
if ( empty( $shortcode->atts['show_opening_now_label'] ) ) {
return false;
}
$timezone = ! empty( $schema['metadata']['timeZone'] ) ? $schema['metadata']['timeZone'] : wp_timezone_string();
$local_datetime = new \DateTime( 'now', new \DateTimeZone( $timezone ) );
return [
'day' => $local_datetime->format( 'l' ),
'time' => strtotime( $local_datetime->format( 'H:i' ) ),
];
}
/**
* Normalize Weekdays.
*
* @param array $schema Schema data.
* @param Location_Shortcode $shortcode Location_Shortcode Instance.
* @return array
*/
private function normalize_days( $schema, $shortcode ) {
$hours = $schema['openingHoursSpecification'];
$days = explode( ',', $shortcode->atts['show_days'] );
$format = ! isset( $schema['metadata']['use_24h_format'] ) ? Helper::get_settings( 'titles.opening_hours_format' ) : empty( $schema['metadata']['use_24h_format'] );
$data = [];
$local_time = $this->get_local_time( $shortcode, $schema );
foreach ( $days as $day ) {
$day = ucfirst( trim( $day ) );
$data[ $day ] = [
'isOpen' => '',
];
foreach ( $hours as $hour ) {
if ( ! in_array( $day, (array) $hour['dayOfWeek'], true ) ) {
continue;
}
$open = strtotime( $hour['opens'] );
$close = strtotime( $hour['closes'] );
$is_open = ! empty( $local_time ) &&
$day === $local_time['day'] &&
$local_time['time'] >= $open &&
$local_time['time'] <= $close;
$data[ $day ]['time'][] = $format ? date_i18n( 'g:i a', $open ) . ' - ' . date_i18n( 'g:i a', $close ) : $hour['opens'] . ' - ' . $hour['closes'];
$data[ $day ]['isOpen'] = $is_open ? $this->get_opening_hours_note( $shortcode ) : '';
}
if ( $shortcode->atts['hide_closed_days'] && empty( $data[ $day ]['time'] ) ) {
unset( $data[ $day ] );
}
}
return $data;
}
/**
* Get Opening Hours note.
*
* @param Location_Shortcode $shortcode Location_Shortcode Instance.
* @return string
*/
private function get_opening_hours_note( $shortcode ) {
return empty( $shortcode->atts['opening_hours_note'] ) ? esc_html__( 'Open now', 'rank-math-pro' ) : esc_html( $shortcode->atts['opening_hours_note'] );
}
/**
* Retrieve the full translated weekday word.
*
* @param string $day Day to translate.
*
* @return string
*/
private function get_localized_day( $day ) {
global $wp_locale;
$hash = [
'Sunday' => 0,
'Monday' => 1,
'Tuesday' => 2,
'Wednesday' => 3,
'Thursday' => 4,
'Friday' => 5,
'Saturday' => 6,
];
return ! isset( $hash[ $day ] ) ? $day : $wp_locale->get_weekday( $hash[ $day ] );
}
}

View File

@@ -0,0 +1,232 @@
<?php
/**
* The Store Locator shortcode Class.
*
* @since 1.0.1
* @package RankMath
* @subpackage RankMathPro
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Local_Seo;
use RankMath\Helper;
use RankMath\Post;
use RankMath\Schema\DB;
use MyThemeShop\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* Store_Locator class.
*/
class Store_Locator {
/**
* Get Store_Locator Data.
*
* @param Location_Shortcode $shortcode Location_Shortcode Instance.
* @return string
*/
public function get_data( $shortcode ) {
$unit = 'miles' === Helper::get_settings( 'titles.map_unit', 'kilometers' ) ? 'mi' : 'km';
$radius = Param::post( 'rank-math-search-radius', 20 );
$address = Param::post( 'rank-math-search-address' );
$category = Param::post( 'rank-math-location-category' );
$terms = empty( $shortcode->atts['show_category_filter'] )
? []
: get_terms(
[
'taxonomy' => 'rank_math_location_category',
'fields' => 'id=>name',
]
);
wp_enqueue_script( 'rank-math-local' );
ob_start();
?>
<div class="rank-math-business-wrapper">
<form id="rank-math-local-store-locator" method="post" action="#rank-math-local-store-locator">
<?php if ( ! empty( $shortcode->atts['show_radius'] ) ) { ?>
<div class="rank-math-form-field">
<select name="rank-math-search-radius">
<?php
foreach ( [ 1, 5, 10, 20, 40, 50, 75, 100, 200, 300, 400, 500, 1000 ] as $value ) {
echo "<option value='{$value}' " . selected( $radius, $value, true ) . ">{$value}{$unit}</option>";
}
?>
</select>
</div>
<?php } ?>
<?php if ( ! empty( $terms ) ) { ?>
<div class="rank-math-form-field">
<select name="rank-math-location-category">
<option value=""><?php echo esc_html__( 'Select Category', 'rank-math-pro' ); ?></option>
<?php foreach ( $terms as $term_id => $term_name ) { ?>
<option value="<?php echo esc_attr( $term_id ); ?>" <?php selected( $category, $term_id ); ?>><?php echo esc_html( $term_name ); ?></option>
<?php } ?>
</select>
</div>
<?php } ?>
<div class="rank-math-form-field">
<input type="text" name="rank-math-search-address" id="rank-math-search-address" placeholder="<?php echo esc_html__( 'Address, Suburb, Region, Zip or Landmark', 'rank-math-pro' ); ?>" value="<?php echo esc_attr( $address ); ?>" />
<input type="hidden" name="lat" id="rank-math-lat" />
<input type="hidden" name="lng" id="rank-math-lng" />
</div>
<?php $this->detect_location(); ?>
<div class="rank-math-form-field">
<button type="submit" name="rank-math-submit" value="search"><?php echo esc_html__( 'Search', 'rank-math-pro' ); ?></button>
</div>
</form>
<?php
echo $this->get_results( $shortcode, $unit );
echo '</div>';
return ob_get_clean();
}
/**
* Add detect current location button.
*
* @return string
*/
private function detect_location() {
if ( ! Helper::get_settings( 'titles.enable_location_detection' ) ) {
return;
}
echo '<a href="#" id="rank-math-current-location">' . esc_html__( 'Detect Location', 'rank-math-pro' ) . '</a>';
}
/**
* Get Map Results.
*
* @param Location_Shortcode $shortcode Location_Shortcode Instance.
* @param string $unit Map measurement unit.
* @return string
*/
private function get_results( $shortcode, $unit ) {
if ( ! Param::post( 'rank-math-search-address' ) ) {
return false;
}
global $wpdb;
// Radius of the earth 3959 miles or 6371 kilometers.
$earth_radius = 'mi' === $unit ? 3959 : 6371;
$radius = ! empty( $shortcode->atts['show_radius'] ) ? Param::post( 'rank-math-search-radius', 20 ) : $shortcode->atts['search_radius'];
$latitude = Param::post( 'lat' );
$longitude = Param::post( 'lng' );
$category = Param::post( 'rank-math-location-category', 0, FILTER_VALIDATE_INT );
$inner_join = '';
if ( $category ) {
$inner_join .= $wpdb->prepare(
"INNER JOIN $wpdb->term_relationships AS tr ON p.ID = tr.object_id
INNER JOIN $wpdb->term_taxonomy AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
AND tt.taxonomy = 'rank_math_location_category'
AND tt.term_id = %d",
$category
);
}
$nearby_locations = $wpdb->get_results(
$wpdb->prepare(
"SELECT DISTINCT
p.*,
map_lat.meta_value as locLat,
map_lng.meta_value as locLong,
( %d * acos(
cos( radians( %s ) )
* cos( radians( map_lat.meta_value ) )
* cos( radians( map_lng.meta_value ) - radians( %s ) )
+ sin( radians( %s ) )
* sin( radians( map_lat.meta_value ) )
) )
AS distance
FROM $wpdb->posts p
INNER JOIN $wpdb->postmeta map_lat ON p.ID = map_lat.post_id
INNER JOIN $wpdb->postmeta map_lng ON p.ID = map_lng.post_id
$inner_join
WHERE 1 = 1
AND p.post_type = 'rank_math_locations'
AND p.post_status = 'publish'
AND map_lat.meta_key = 'rank_math_local_business_latitide'
AND map_lng.meta_key = 'rank_math_local_business_longitude'
HAVING distance < %s
ORDER BY distance ASC",
$earth_radius,
$latitude,
$longitude,
$latitude,
$radius
)
);
//phpcs:enable
if ( empty( $nearby_locations ) ) {
return esc_html__( 'Sorry, no locations were found.', 'rank-math-pro' );
}
$data = ! empty( $shortcode->atts['show_map'] ) ? $shortcode->map->get_data( $shortcode, $nearby_locations ) : '';
foreach ( $nearby_locations as $location ) {
$schema = DB::get_schemas( $location->ID );
if ( empty( $schema ) ) {
continue;
}
$schema = current( $shortcode->replace_variables( $schema, $location ) );
$data .= $shortcode->get_title( $schema );
$data .= $shortcode->address->get_data( $shortcode, $schema );
$data .= ! empty( $shortcode->atts['show_opening_hours'] ) ? $shortcode->opening_hours->get_data( $shortcode, $schema ) : '';
$data .= $this->get_directions( $location, $shortcode );
}
return $data;
}
/**
* Get Map Results.
*
* @param Object $location Current Location Post.
* @param Location_Shortcode $shortcode Location_Shortcode Instance.
* @return string
*/
public function get_directions( $location, $shortcode ) {
if ( empty( $shortcode->atts['show_route_planner'] ) || empty( $shortcode->atts['show_map'] ) ) {
return '';
}
$lat = Post::get_meta( 'local_business_latitide', $location->ID );
$lng = Post::get_meta( 'local_business_longitude', $location->ID );
ob_start();
?>
<div class="rank-math-directions-wrapper">
<a href="#" class="rank-math-show-route" data-toggle-text="<?php echo esc_html__( 'Hide route', 'rank-math-pro' ); ?>">
<?php echo esc_html( $shortcode->atts['route_label'] ); ?>
</a>
<div class="rank-math-directions-result">
<h3><?php echo esc_html__( 'Route', 'rank-math-pro' ); ?></h3>
<div class="rank-math-directions-form">
<form method="post">
<div class="rank-math-form-field">
<label for="origin"><?php echo esc_html__( 'Your location:', 'rank-math-pro' ); ?></label>
<input type="text" name="origin" id="rank-math-origin" value="<?php echo esc_attr( Param::post( 'rank-math-search-address' ) ); ?>" />
<input type="submit" name="get-direction" value="<?php echo esc_html__( 'Show route', 'rank-math-pro' ); ?>" />
<input type="hidden" name="rank-math-lat" id="rank-math-lat" value="<?php echo esc_attr( $lat ); ?>" />
<input type="hidden" name="rank-math-lng" id="rank-math-lng" value="<?php echo esc_attr( $lng ); ?>" />
</div>
</form>
</div>
<div class="rank-math-directions"></div>
</div>
</div>
<?php
return ob_get_clean();
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* Locations KML File settings.
*
* @since 2.2.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
defined( 'ABSPATH' ) || exit;
$cmb->add_field(
[
'id' => 'local_sitemap',
'type' => 'toggle',
'name' => esc_html__( 'Include KML File in the Sitemap', 'rank-math-pro' ),
'desc' => esc_html__( 'locations.kml Sitemap is generated automatically when the Local SEO module is enabled, and the geo-coordinates are added.', 'rank-math-pro' ),
'default' => 'on',
]
);

View File

@@ -0,0 +1,764 @@
<?php
/**
* The local seo settings.
*
* @package RankMath
* @subpackage RankMathPro
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
$company = [ [ 'knowledgegraph_type', 'company' ] ];
$person = [ [ 'knowledgegraph_type', 'person' ] ];
$use_multiple_locations = [ 'relation' => 'and' ] + $company;
$use_multiple_locations[] = [ 'use_multiple_locations', 'on' ];
$hide_on_multiple_locations = [ 'relation' => 'and' ] + $company;
$hide_on_multiple_locations[] = [ 'use_multiple_locations', 'on', '!=' ];
$countries = [
'' => __( 'Choose a country', 'rank-math-pro' ),
'AX' => __( 'Åland Islands', 'rank-math-pro' ),
'AF' => __( 'Afghanistan', 'rank-math-pro' ),
'AL' => __( 'Albania', 'rank-math-pro' ),
'DZ' => __( 'Algeria', 'rank-math-pro' ),
'AD' => __( 'Andorra', 'rank-math-pro' ),
'AO' => __( 'Angola', 'rank-math-pro' ),
'AI' => __( 'Anguilla', 'rank-math-pro' ),
'AQ' => __( 'Antarctica', 'rank-math-pro' ),
'AG' => __( 'Antigua and Barbuda', 'rank-math-pro' ),
'AR' => __( 'Argentina', 'rank-math-pro' ),
'AM' => __( 'Armenia', 'rank-math-pro' ),
'AW' => __( 'Aruba', 'rank-math-pro' ),
'AU' => __( 'Australia', 'rank-math-pro' ),
'AT' => __( 'Austria', 'rank-math-pro' ),
'AZ' => __( 'Azerbaijan', 'rank-math-pro' ),
'BS' => __( 'Bahamas', 'rank-math-pro' ),
'BH' => __( 'Bahrain', 'rank-math-pro' ),
'BD' => __( 'Bangladesh', 'rank-math-pro' ),
'BB' => __( 'Barbados', 'rank-math-pro' ),
'BY' => __( 'Belarus', 'rank-math-pro' ),
'PW' => __( 'Belau', 'rank-math-pro' ),
'BE' => __( 'Belgium', 'rank-math-pro' ),
'BZ' => __( 'Belize', 'rank-math-pro' ),
'BJ' => __( 'Benin', 'rank-math-pro' ),
'BM' => __( 'Bermuda', 'rank-math-pro' ),
'BT' => __( 'Bhutan', 'rank-math-pro' ),
'BO' => __( 'Bolivia', 'rank-math-pro' ),
'BQ' => __( 'Bonaire, Sint Eustatius and Saba', 'rank-math-pro' ),
'BA' => __( 'Bosnia and Herzegovina', 'rank-math-pro' ),
'BW' => __( 'Botswana', 'rank-math-pro' ),
'BV' => __( 'Bouvet Island', 'rank-math-pro' ),
'BR' => __( 'Brazil', 'rank-math-pro' ),
'IO' => __( 'British Indian Ocean Territory', 'rank-math-pro' ),
'VG' => __( 'British Virgin Islands', 'rank-math-pro' ),
'BN' => __( 'Brunei', 'rank-math-pro' ),
'BG' => __( 'Bulgaria', 'rank-math-pro' ),
'BF' => __( 'Burkina Faso', 'rank-math-pro' ),
'BI' => __( 'Burundi', 'rank-math-pro' ),
'KH' => __( 'Cambodia', 'rank-math-pro' ),
'CM' => __( 'Cameroon', 'rank-math-pro' ),
'CA' => __( 'Canada', 'rank-math-pro' ),
'CV' => __( 'Cape Verde', 'rank-math-pro' ),
'KY' => __( 'Cayman Islands', 'rank-math-pro' ),
'CF' => __( 'Central African Republic', 'rank-math-pro' ),
'TD' => __( 'Chad', 'rank-math-pro' ),
'CL' => __( 'Chile', 'rank-math-pro' ),
'CN' => __( 'China', 'rank-math-pro' ),
'CX' => __( 'Christmas Island', 'rank-math-pro' ),
'CC' => __( 'Cocos (Keeling) Islands', 'rank-math-pro' ),
'CO' => __( 'Colombia', 'rank-math-pro' ),
'KM' => __( 'Comoros', 'rank-math-pro' ),
'CG' => __( 'Congo (Brazzaville)', 'rank-math-pro' ),
'CD' => __( 'Congo (Kinshasa)', 'rank-math-pro' ),
'CK' => __( 'Cook Islands', 'rank-math-pro' ),
'CR' => __( 'Costa Rica', 'rank-math-pro' ),
'HR' => __( 'Croatia', 'rank-math-pro' ),
'CU' => __( 'Cuba', 'rank-math-pro' ),
'CW' => __( 'Curaçao', 'rank-math-pro' ),
'CY' => __( 'Cyprus', 'rank-math-pro' ),
'CZ' => __( 'Czech Republic', 'rank-math-pro' ),
'DK' => __( 'Denmark', 'rank-math-pro' ),
'DJ' => __( 'Djibouti', 'rank-math-pro' ),
'DM' => __( 'Dominica', 'rank-math-pro' ),
'DO' => __( 'Dominican Republic', 'rank-math-pro' ),
'EC' => __( 'Ecuador', 'rank-math-pro' ),
'EG' => __( 'Egypt', 'rank-math-pro' ),
'SV' => __( 'El Salvador', 'rank-math-pro' ),
'GQ' => __( 'Equatorial Guinea', 'rank-math-pro' ),
'ER' => __( 'Eritrea', 'rank-math-pro' ),
'EE' => __( 'Estonia', 'rank-math-pro' ),
'ET' => __( 'Ethiopia', 'rank-math-pro' ),
'FK' => __( 'Falkland Islands', 'rank-math-pro' ),
'FO' => __( 'Faroe Islands', 'rank-math-pro' ),
'FJ' => __( 'Fiji', 'rank-math-pro' ),
'FI' => __( 'Finland', 'rank-math-pro' ),
'FR' => __( 'France', 'rank-math-pro' ),
'GF' => __( 'French Guiana', 'rank-math-pro' ),
'PF' => __( 'French Polynesia', 'rank-math-pro' ),
'TF' => __( 'French Southern Territories', 'rank-math-pro' ),
'GA' => __( 'Gabon', 'rank-math-pro' ),
'GM' => __( 'Gambia', 'rank-math-pro' ),
'GE' => __( 'Georgia', 'rank-math-pro' ),
'DE' => __( 'Germany', 'rank-math-pro' ),
'GH' => __( 'Ghana', 'rank-math-pro' ),
'GI' => __( 'Gibraltar', 'rank-math-pro' ),
'GR' => __( 'Greece', 'rank-math-pro' ),
'GL' => __( 'Greenland', 'rank-math-pro' ),
'GD' => __( 'Grenada', 'rank-math-pro' ),
'GP' => __( 'Guadeloupe', 'rank-math-pro' ),
'GT' => __( 'Guatemala', 'rank-math-pro' ),
'GG' => __( 'Guernsey', 'rank-math-pro' ),
'GN' => __( 'Guinea', 'rank-math-pro' ),
'GW' => __( 'Guinea-Bissau', 'rank-math-pro' ),
'GY' => __( 'Guyana', 'rank-math-pro' ),
'HT' => __( 'Haiti', 'rank-math-pro' ),
'HM' => __( 'Heard Island and McDonald Islands', 'rank-math-pro' ),
'HN' => __( 'Honduras', 'rank-math-pro' ),
'HK' => __( 'Hong Kong', 'rank-math-pro' ),
'HU' => __( 'Hungary', 'rank-math-pro' ),
'IS' => __( 'Iceland', 'rank-math-pro' ),
'IN' => __( 'India', 'rank-math-pro' ),
'ID' => __( 'Indonesia', 'rank-math-pro' ),
'IR' => __( 'Iran', 'rank-math-pro' ),
'IQ' => __( 'Iraq', 'rank-math-pro' ),
'IM' => __( 'Isle of Man', 'rank-math-pro' ),
'IL' => __( 'Israel', 'rank-math-pro' ),
'IT' => __( 'Italy', 'rank-math-pro' ),
'CI' => __( 'Ivory Coast', 'rank-math-pro' ),
'JM' => __( 'Jamaica', 'rank-math-pro' ),
'JP' => __( 'Japan', 'rank-math-pro' ),
'JE' => __( 'Jersey', 'rank-math-pro' ),
'JO' => __( 'Jordan', 'rank-math-pro' ),
'KZ' => __( 'Kazakhstan', 'rank-math-pro' ),
'KE' => __( 'Kenya', 'rank-math-pro' ),
'KI' => __( 'Kiribati', 'rank-math-pro' ),
'KW' => __( 'Kuwait', 'rank-math-pro' ),
'KG' => __( 'Kyrgyzstan', 'rank-math-pro' ),
'LA' => __( 'Laos', 'rank-math-pro' ),
'LV' => __( 'Latvia', 'rank-math-pro' ),
'LB' => __( 'Lebanon', 'rank-math-pro' ),
'LS' => __( 'Lesotho', 'rank-math-pro' ),
'LR' => __( 'Liberia', 'rank-math-pro' ),
'LY' => __( 'Libya', 'rank-math-pro' ),
'LI' => __( 'Liechtenstein', 'rank-math-pro' ),
'LT' => __( 'Lithuania', 'rank-math-pro' ),
'LU' => __( 'Luxembourg', 'rank-math-pro' ),
'MO' => __( 'Macao S.A.R., China', 'rank-math-pro' ),
'MK' => __( 'Macedonia', 'rank-math-pro' ),
'MG' => __( 'Madagascar', 'rank-math-pro' ),
'MW' => __( 'Malawi', 'rank-math-pro' ),
'MY' => __( 'Malaysia', 'rank-math-pro' ),
'MV' => __( 'Maldives', 'rank-math-pro' ),
'ML' => __( 'Mali', 'rank-math-pro' ),
'MT' => __( 'Malta', 'rank-math-pro' ),
'MH' => __( 'Marshall Islands', 'rank-math-pro' ),
'MQ' => __( 'Martinique', 'rank-math-pro' ),
'MR' => __( 'Mauritania', 'rank-math-pro' ),
'MU' => __( 'Mauritius', 'rank-math-pro' ),
'YT' => __( 'Mayotte', 'rank-math-pro' ),
'MX' => __( 'Mexico', 'rank-math-pro' ),
'FM' => __( 'Micronesia', 'rank-math-pro' ),
'MD' => __( 'Moldova', 'rank-math-pro' ),
'MC' => __( 'Monaco', 'rank-math-pro' ),
'MN' => __( 'Mongolia', 'rank-math-pro' ),
'ME' => __( 'Montenegro', 'rank-math-pro' ),
'MS' => __( 'Montserrat', 'rank-math-pro' ),
'MA' => __( 'Morocco', 'rank-math-pro' ),
'MZ' => __( 'Mozambique', 'rank-math-pro' ),
'MM' => __( 'Myanmar', 'rank-math-pro' ),
'NA' => __( 'Namibia', 'rank-math-pro' ),
'NR' => __( 'Nauru', 'rank-math-pro' ),
'NP' => __( 'Nepal', 'rank-math-pro' ),
'NL' => __( 'Netherlands', 'rank-math-pro' ),
'AN' => __( 'Netherlands Antilles', 'rank-math-pro' ),
'NC' => __( 'New Caledonia', 'rank-math-pro' ),
'NZ' => __( 'New Zealand', 'rank-math-pro' ),
'NI' => __( 'Nicaragua', 'rank-math-pro' ),
'NE' => __( 'Niger', 'rank-math-pro' ),
'NG' => __( 'Nigeria', 'rank-math-pro' ),
'NU' => __( 'Niue', 'rank-math-pro' ),
'NF' => __( 'Norfolk Island', 'rank-math-pro' ),
'KP' => __( 'North Korea', 'rank-math-pro' ),
'NO' => __( 'Norway', 'rank-math-pro' ),
'OM' => __( 'Oman', 'rank-math-pro' ),
'PK' => __( 'Pakistan', 'rank-math-pro' ),
'PS' => __( 'Palestinian Territory', 'rank-math-pro' ),
'PA' => __( 'Panama', 'rank-math-pro' ),
'PG' => __( 'Papua New Guinea', 'rank-math-pro' ),
'PY' => __( 'Paraguay', 'rank-math-pro' ),
'PE' => __( 'Peru', 'rank-math-pro' ),
'PH' => __( 'Philippines', 'rank-math-pro' ),
'PN' => __( 'Pitcairn', 'rank-math-pro' ),
'PL' => __( 'Poland', 'rank-math-pro' ),
'PT' => __( 'Portugal', 'rank-math-pro' ),
'QA' => __( 'Qatar', 'rank-math-pro' ),
'IE' => __( 'Republic of Ireland', 'rank-math-pro' ),
'RE' => __( 'Reunion', 'rank-math-pro' ),
'RO' => __( 'Romania', 'rank-math-pro' ),
'RU' => __( 'Russia', 'rank-math-pro' ),
'RW' => __( 'Rwanda', 'rank-math-pro' ),
'ST' => __( 'São Tomé and Príncipe', 'rank-math-pro' ),
'BL' => __( 'Saint Barthélemy', 'rank-math-pro' ),
'SH' => __( 'Saint Helena', 'rank-math-pro' ),
'KN' => __( 'Saint Kitts and Nevis', 'rank-math-pro' ),
'LC' => __( 'Saint Lucia', 'rank-math-pro' ),
'SX' => __( 'Saint Martin (Dutch part)', 'rank-math-pro' ),
'MF' => __( 'Saint Martin (French part)', 'rank-math-pro' ),
'PM' => __( 'Saint Pierre and Miquelon', 'rank-math-pro' ),
'VC' => __( 'Saint Vincent and the Grenadines', 'rank-math-pro' ),
'SM' => __( 'San Marino', 'rank-math-pro' ),
'SA' => __( 'Saudi Arabia', 'rank-math-pro' ),
'SN' => __( 'Senegal', 'rank-math-pro' ),
'RS' => __( 'Serbia', 'rank-math-pro' ),
'SC' => __( 'Seychelles', 'rank-math-pro' ),
'SL' => __( 'Sierra Leone', 'rank-math-pro' ),
'SG' => __( 'Singapore', 'rank-math-pro' ),
'SK' => __( 'Slovakia', 'rank-math-pro' ),
'SI' => __( 'Slovenia', 'rank-math-pro' ),
'SB' => __( 'Solomon Islands', 'rank-math-pro' ),
'SO' => __( 'Somalia', 'rank-math-pro' ),
'ZA' => __( 'South Africa', 'rank-math-pro' ),
'GS' => __( 'South Georgia/Sandwich Islands', 'rank-math-pro' ),
'KR' => __( 'South Korea', 'rank-math-pro' ),
'SS' => __( 'South Sudan', 'rank-math-pro' ),
'ES' => __( 'Spain', 'rank-math-pro' ),
'LK' => __( 'Sri Lanka', 'rank-math-pro' ),
'SD' => __( 'Sudan', 'rank-math-pro' ),
'SR' => __( 'Suriname', 'rank-math-pro' ),
'SJ' => __( 'Svalbard and Jan Mayen', 'rank-math-pro' ),
'SZ' => __( 'Swaziland', 'rank-math-pro' ),
'SE' => __( 'Sweden', 'rank-math-pro' ),
'CH' => __( 'Switzerland', 'rank-math-pro' ),
'SY' => __( 'Syria', 'rank-math-pro' ),
'TW' => __( 'Taiwan', 'rank-math-pro' ),
'TJ' => __( 'Tajikistan', 'rank-math-pro' ),
'TZ' => __( 'Tanzania', 'rank-math-pro' ),
'TH' => __( 'Thailand', 'rank-math-pro' ),
'TL' => __( 'Timor-Leste', 'rank-math-pro' ),
'TG' => __( 'Togo', 'rank-math-pro' ),
'TK' => __( 'Tokelau', 'rank-math-pro' ),
'TO' => __( 'Tonga', 'rank-math-pro' ),
'TT' => __( 'Trinidad and Tobago', 'rank-math-pro' ),
'TN' => __( 'Tunisia', 'rank-math-pro' ),
'TR' => __( 'Turkey', 'rank-math-pro' ),
'TM' => __( 'Turkmenistan', 'rank-math-pro' ),
'TC' => __( 'Turks and Caicos Islands', 'rank-math-pro' ),
'TV' => __( 'Tuvalu', 'rank-math-pro' ),
'UG' => __( 'Uganda', 'rank-math-pro' ),
'UA' => __( 'Ukraine', 'rank-math-pro' ),
'AE' => __( 'United Arab Emirates', 'rank-math-pro' ),
'GB' => __( 'United Kingdom (UK)', 'rank-math-pro' ),
'US' => __( 'United States (US)', 'rank-math-pro' ),
'UY' => __( 'Uruguay', 'rank-math-pro' ),
'UZ' => __( 'Uzbekistan', 'rank-math-pro' ),
'VU' => __( 'Vanuatu', 'rank-math-pro' ),
'VA' => __( 'Vatican', 'rank-math-pro' ),
'VE' => __( 'Venezuela', 'rank-math-pro' ),
'VN' => __( 'Vietnam', 'rank-math-pro' ),
'WF' => __( 'Wallis and Futuna', 'rank-math-pro' ),
'EH' => __( 'Western Sahara', 'rank-math-pro' ),
'WS' => __( 'Western Samoa', 'rank-math-pro' ),
'YE' => __( 'Yemen', 'rank-math-pro' ),
'ZM' => __( 'Zambia', 'rank-math-pro' ),
'ZW' => __( 'Zimbabwe', 'rank-math-pro' ),
];
$cmb->add_field(
[
'id' => 'knowledgegraph_type',
'type' => 'radio_inline',
'name' => esc_html__( 'Person or Company', 'rank-math-pro' ),
'options' => [
'person' => esc_html__( 'Person', 'rank-math-pro' ),
'company' => esc_html__( 'Organization', 'rank-math-pro' ),
],
'desc' => esc_html__( 'Choose whether the site represents a person or an organization.', 'rank-math-pro' ),
'default' => 'person',
]
);
$cmb->add_field(
[
'id' => 'website_name',
'type' => 'text',
'name' => esc_html__( 'Website Name', 'rank-math-pro' ),
'desc' => esc_html__( 'Enter the name of your site to appear in search results.', 'rank-math-pro' ),
'default' => get_bloginfo( 'name' ),
]
);
$cmb->add_field(
[
'id' => 'website_alternate_name',
'type' => 'text',
'name' => esc_html__( 'Website Alternate Name', 'rank-math-pro' ),
'desc' => esc_html__( 'An alternate version of your site name (for example, an acronym or shorter name).', 'rank-math-pro' ),
]
);
$cmb->add_field(
[
'id' => 'knowledgegraph_name',
'type' => 'text',
'name' => esc_html__( 'Person/Organization Name', 'rank-math-pro' ),
'desc' => esc_html__( 'Your name or company name intended to feature in Google\'s Knowledge Panel.', 'rank-math-pro' ),
'default' => get_bloginfo( 'name' ),
]
);
$cmb->add_field(
[
'id' => 'knowledgegraph_logo',
'type' => 'file',
'name' => esc_html__( 'Logo', 'rank-math-pro' ),
'desc' => __( '<strong>Min Size: 112Χ112px</strong>.<br /> A squared image is preferred by the search engines.', 'rank-math-pro' ),
'options' => [ 'url' => false ],
]
);
$cmb->add_field(
[
'id' => 'url',
'type' => 'text_url',
'name' => esc_html__( 'URL', 'rank-math-pro' ),
'desc' => esc_html__( 'URL of the item.', 'rank-math-pro' ),
'default' => home_url(),
]
);
$cmb->add_field(
[
'id' => 'use_multiple_locations',
'type' => 'toggle',
'name' => esc_html__( 'Use Multiple Locations', 'rank-math-pro' ),
'desc' => esc_html__( 'Once you save the changes, we will create a new custom post type called "Locations" where you can add multiple locations of your business/organization.', 'rank-math-pro' ),
'options' => [
'off' => esc_html__( 'Default', 'rank-math-pro' ),
'on' => esc_html__( 'Custom', 'rank-math-pro' ),
],
'default' => 'off',
'dep' => $company,
]
);
$cmb->add_field(
[
'id' => 'email',
'type' => 'text',
'name' => esc_html__( 'Email', 'rank-math-pro' ),
'desc' => esc_html__( 'Search engines display your email address.', 'rank-math-pro' ),
]
);
$cmb->add_field(
[
'id' => 'phone',
'type' => 'text',
'name' => esc_html__( 'Phone', 'rank-math-pro' ),
'desc' => esc_html__( 'Search engines may prominently display your contact phone number for mobile users.', 'rank-math-pro' ),
'dep' => $person,
]
);
$cmb->add_field(
[
'id' => 'local_address',
'type' => 'address',
'name' => esc_html__( 'Address', 'rank-math-pro' ),
'dep' => [ [ 'use_multiple_locations', 'on', '!=' ] ],
]
);
$cmb->add_field(
[
'id' => 'local_address_format',
'type' => 'textarea_small',
'name' => esc_html__( 'Address Format', 'rank-math-pro' ),
'desc' => wp_kses_post( __( 'Format used when the address is displayed using the <code>[rank_math_contact_info]</code> shortcode.<br><strong>Available Tags: {address}, {locality}, {region}, {postalcode}, {country}, {gps}</strong>', 'rank-math-pro' ) ),
'default' => '{address} {locality}, {region} {postalcode}',
'classes' => 'rank-math-address-format',
'attributes' => [
'rows' => 2,
'placeholder' => '{address} {locality}, {region} {country}. {postalcode}.',
],
'dep' => $company,
]
);
$cmb->add_field(
[
'id' => 'local_business_type',
'type' => 'select',
'name' => esc_html__( 'Business Type', 'rank-math-pro' ),
'options' => Helper::choices_business_types( true ),
'attributes' => ( 'data-s2' ),
'dep' => $hide_on_multiple_locations,
]
);
$opening_hours = $cmb->add_field(
[
'id' => 'opening_hours',
'type' => 'group',
'name' => esc_html__( 'Opening Hours', 'rank-math-pro' ),
'desc' => esc_html__( 'Select opening hours. You can add multiple sets if you have different opening or closing hours on some days or if you have a mid-day break. Times are specified using 24:00 time.', 'rank-math-pro' ),
'options' => [
'add_button' => esc_html__( 'Add time', 'rank-math-pro' ),
'remove_button' => esc_html__( 'Remove', 'rank-math-pro' ),
],
'dep' => $hide_on_multiple_locations,
'classes' => 'cmb-group-text-only',
]
);
$cmb->add_group_field(
$opening_hours,
[
'id' => 'day',
'type' => 'select',
'options' => [
'Monday' => esc_html__( 'Monday', 'rank-math-pro' ),
'Tuesday' => esc_html__( 'Tuesday', 'rank-math-pro' ),
'Wednesday' => esc_html__( 'Wednesday', 'rank-math-pro' ),
'Thursday' => esc_html__( 'Thursday', 'rank-math-pro' ),
'Friday' => esc_html__( 'Friday', 'rank-math-pro' ),
'Saturday' => esc_html__( 'Saturday', 'rank-math-pro' ),
'Sunday' => esc_html__( 'Sunday', 'rank-math-pro' ),
],
]
);
$cmb->add_group_field(
$opening_hours,
[
'id' => 'time',
'type' => 'text',
'attributes' => [ 'placeholder' => esc_html__( 'e.g. 09:00-17:00', 'rank-math-pro' ) ],
]
);
$cmb->add_field(
[
'id' => 'opening_hours_format',
'type' => 'switch',
'name' => esc_html__( 'Opening Hours Format', 'rank-math-pro' ),
'options' => [
'off' => '24:00',
'on' => '12:00',
],
'desc' => esc_html__( 'Time format used in the contact shortcode.', 'rank-math-pro' ),
'default' => 'off',
'dep' => $company,
]
);
$phones = $cmb->add_field(
[
'id' => 'phone_numbers',
'type' => 'group',
'name' => esc_html__( 'Phone Number', 'rank-math-pro' ),
'desc' => esc_html__( 'Search engines may prominently display your contact phone number for mobile users.', 'rank-math-pro' ),
'options' => [
'add_button' => esc_html__( 'Add number', 'rank-math-pro' ),
'remove_button' => esc_html__( 'Remove', 'rank-math-pro' ),
],
'dep' => $hide_on_multiple_locations,
'classes' => 'cmb-group-text-only',
]
);
$cmb->add_group_field(
$phones,
[
'id' => 'type',
'type' => 'select',
'options' => Helper::choices_phone_types(),
'default' => 'customer_support',
]
);
$cmb->add_group_field(
$phones,
[
'id' => 'number',
'type' => 'text',
'attributes' => [ 'placeholder' => esc_html__( 'Format: +1-401-555-1212', 'rank-math-pro' ) ],
]
);
$cmb->add_field(
[
'id' => 'price_range',
'type' => 'text',
'name' => esc_html__( 'Price Range', 'rank-math-pro' ),
'desc' => esc_html__( 'The price range of the business, for example $$$.', 'rank-math-pro' ),
'dep' => $hide_on_multiple_locations,
]
);
$cmb->add_field(
[
'id' => 'hide_opening_hours',
'type' => 'switch',
'name' => esc_html__( 'Hide Opening Hours', 'rank-math-pro' ),
'desc' => esc_html__( 'Don\'t add opening hours data in Schema', 'rank-math-pro' ),
'options' => [
'off' => esc_html__( 'No', 'rank-math-pro' ),
'on' => esc_html__( 'Yes', 'rank-math-pro' ),
],
'default' => 'off',
'dep' => $use_multiple_locations,
]
);
$hide_opening_hours = [ 'relation' => 'and' ] + $use_multiple_locations;
$hide_opening_hours[] = [ 'hide_opening_hours', 'on', '!=' ];
$cmb->add_field(
[
'id' => 'closed_label',
'type' => 'text',
'name' => esc_html__( 'Closed label', 'rank-math-pro' ),
'desc' => esc_html__( 'Text to show in Opening hours when business is closed.', 'rank-math-pro' ),
'default' => 'Closed',
'dep' => $hide_opening_hours,
]
);
$cmb->add_field(
[
'id' => 'open_24_7',
'type' => 'text',
'name' => esc_html__( 'Open 24/7 label', 'rank-math-pro' ),
'desc' => esc_html__( 'Select the text to display alongside your opening hours when your store is open 24/7.', 'rank-math-pro' ),
'default' => 'Open 24/7',
'dep' => $hide_opening_hours,
]
);
$cmb->add_field(
[
'id' => 'open_24h',
'type' => 'text',
'name' => esc_html__( 'Open 24h label', 'rank-math-pro' ),
'default' => 'Open 24h',
'dep' => $hide_opening_hours,
]
);
$cmb->add_field(
[
'id' => 'map_unit',
'type' => 'select',
'name' => esc_html__( 'Measurement system', 'rank-math-pro' ),
'desc' => esc_html__( 'Select your preferred measurement system (miles or kilometers).', 'rank-math-pro' ),
'options' => [
'kilometers' => esc_html__( 'Kilometers', 'rank-math-pro' ),
'miles' => esc_html__( 'Miles', 'rank-math-pro' ),
],
'default' => 'kilometers',
'dep' => $use_multiple_locations,
]
);
$cmb->add_field(
[
'id' => 'map_style',
'type' => 'select',
'name' => esc_html__( 'Map Style', 'rank-math-pro' ),
'desc' => esc_html__( 'Select the map style you wish to use on the frontend of your website.', 'rank-math-pro' ),
'options' => [
'hybrid' => esc_html__( 'Hybrid', 'rank-math-pro' ),
'satellite' => esc_html__( 'Satellite', 'rank-math-pro' ),
'roadmap' => esc_html__( 'Roadmap', 'rank-math-pro' ),
'terrain' => esc_html__( 'Terrain', 'rank-math-pro' ),
],
'default' => 'hybrid',
'dep' => $use_multiple_locations,
]
);
$cmb->add_field(
[
'id' => 'limit_results',
'type' => 'text',
'name' => esc_html__( 'Maximum number of locations to show', 'rank-math-pro' ),
'desc' => esc_html__( 'Limit the number of locations shown on your website to those nearest your user.', 'rank-math-pro' ),
'default' => 10,
'dep' => $use_multiple_locations,
'attributes' => [
'type' => 'number',
'pattern' => '\d*',
'class' => 'small-text',
],
'sanitization_cb' => 'absint',
'escape_cb' => 'absint',
]
);
$cmb->add_field(
[
'id' => 'primary_country',
'type' => 'select',
'options' => $countries,
'name' => esc_html__( 'Primary Country', 'rank-math-pro' ),
'desc' => esc_html__( 'Select your organizations primary country of operation. This helps improve the accuracy of the store locator.', 'rank-math-pro' ),
'attributes' => ( 'data-s2' ),
'dep' => $use_multiple_locations,
]
);
$cmb->add_field(
[
'id' => 'route_label',
'type' => 'text',
'name' => esc_html__( 'Show Route label', 'rank-math-pro' ),
'desc' => esc_html__( 'Customize the label of the option users can use can click to get directions to your business location on the frontend.', 'rank-math-pro' ),
'default' => 'My Route',
'dep' => $use_multiple_locations,
]
);
$cmb->add_field(
[
'id' => 'enable_location_detection',
'type' => 'toggle',
'name' => esc_html__( 'Location Detection', 'rank-math-pro' ),
'desc' => esc_html__( 'Automatically detect the user\'s location as the starting point.', 'rank-math-pro' ),
'options' => [
'off' => esc_html__( 'Default', 'rank-math-pro' ),
'on' => esc_html__( 'Custom', 'rank-math-pro' ),
],
'default' => 'off',
'dep' => $use_multiple_locations,
]
);
$cmb->add_field(
[
'id' => 'same_organization_locations',
'type' => 'toggle',
'name' => esc_html__( 'All Locations are part of the same Organization', 'rank-math-pro' ),
'desc' => esc_html__( 'Enable if all of the locations where you serve customers are a part of the same legal entity.', 'rank-math-pro' ),
'options' => [
'off' => esc_html__( 'Default', 'rank-math-pro' ),
'on' => esc_html__( 'Custom', 'rank-math-pro' ),
],
'default' => 'off',
'dep' => $use_multiple_locations,
]
);
$cmb->add_field(
[
'id' => 'locations_enhanced_search',
'type' => 'toggle',
'name' => esc_html__( 'Enhanced Search', 'rank-math-pro' ),
'desc' => esc_html__( 'Include business locations in site-wide search results.', 'rank-math-pro' ),
'options' => [
'off' => esc_html__( 'Default', 'rank-math-pro' ),
'on' => esc_html__( 'Custom', 'rank-math-pro' ),
],
'default' => 'off',
'dep' => $use_multiple_locations,
]
);
$cmb->add_field(
[
'id' => 'maps_api_key',
'type' => 'text',
'name' => esc_html__( 'Google Maps API Key', 'rank-math-pro' ),
/* translators: %s expands to "Google Maps Embed API" https://developers.google.com/maps/documentation/embed/ */
'desc' => sprintf( esc_html__( 'An API Key is required to display embedded Google Maps on your site. Get it here: %s', 'rank-math-pro' ), '<a href="https://developers.google.com/maps/documentation/javascript/get-api-key" target="_blank">' . __( 'Google Maps Embed API', 'rank-math-pro' ) . '</a>' ),
'dep' => $company,
'attributes' => [ 'type' => 'password' ],
]
);
$cmb->add_field(
[
'id' => 'geo',
'type' => 'text',
'name' => esc_html__( 'Geo Coordinates', 'rank-math-pro' ),
'desc' => esc_html__( 'Latitude and longitude values separated by comma.', 'rank-math-pro' ),
'dep' => $company,
]
);
$cmb->add_field(
[
'id' => 'locations_post_type_base',
'type' => 'text',
'name' => esc_html__( 'Locations Post Type Base', 'rank-math-pro' ),
'default' => 'locations',
'desc' => '<code>' . home_url( 'locations/africa/' ) . '</code>',
'dep' => $use_multiple_locations,
]
);
$cmb->add_field(
[
'id' => 'locations_category_base',
'type' => 'text',
'name' => esc_html__( 'Locations Category Base', 'rank-math-pro' ),
'default' => 'locations-category',
'desc' => '<code>' . home_url( 'locations-category/regional-offices/' ) . '</code>',
'dep' => $use_multiple_locations,
]
);
$cmb->add_field(
[
'id' => 'locations_post_type_label',
'type' => 'text',
'name' => esc_html__( 'Location Post Type Name', 'rank-math-pro' ),
'default' => 'RM Locations',
'desc' => esc_html__( 'The label that appears in the sidebar for the custom post type where you can add & manage locations.', 'rank-math-pro' ),
'dep' => $use_multiple_locations,
]
);
$cmb->add_field(
[
'id' => 'locations_post_type_plural_label',
'type' => 'text',
'name' => esc_html__( 'Locations Post Type Name (Plural)', 'rank-math-pro' ),
'default' => 'RM Locations',
'desc' => esc_html__( 'As above, but the label that would be applicable for more than one location (default: RM Locations).', 'rank-math-pro' ),
'dep' => $use_multiple_locations,
]
);
$about_page = Helper::get_settings( 'titles.local_seo_about_page' );
$about_options = [ '' => __( 'Select Page', 'rank-math-pro' ) ];
if ( $about_page ) {
$about_options[ $about_page ] = get_the_title( $about_page );
}
$cmb->add_field(
[
'id' => 'local_seo_about_page',
'type' => 'select',
'options' => $about_options,
'name' => esc_html__( 'About Page', 'rank-math-pro' ),
'desc' => esc_html__( 'Select a page on your site where you want to show the LocalBusiness meta data.', 'rank-math-pro' ),
'attributes' => ( 'data-s2-pages' ),
]
);
$contact_page = Helper::get_settings( 'titles.local_seo_contact_page' );
$contact_options = [ '' => __( 'Select Page', 'rank-math-pro' ) ];
if ( $contact_page ) {
$contact_options[ $contact_page ] = get_the_title( $contact_page );
}
$cmb->add_field(
[
'id' => 'local_seo_contact_page',
'type' => 'select',
'options' => $contact_options,
'name' => esc_html__( 'Contact Page', 'rank-math-pro' ),
'desc' => esc_html__( 'Select a page on your site where you want to show the LocalBusiness meta data.', 'rank-math-pro' ),
'attributes' => ( 'data-s2-pages' ),
]
);

View File

@@ -0,0 +1 @@
(()=>{"use strict";const e=wp.hooks,o=wp.i18n,n=wp.element,t=wp.compose,a=wp.data,r=wp.components;const l=(0,t.compose)((0,a.withSelect)((function(e){return{robots:e("rank-math-pro").getNewsRobots()}})),(0,a.withDispatch)((function(e){return{updateNewsRobots:function(o){e("rank-math-pro").updateNewsRobots(o)}}})))((function(e){return wp.element.createElement(n.Fragment,null,wp.element.createElement(r.PanelBody,{initialOpen:!0},wp.element.createElement(r.RadioControl,{label:(0,o.__)("Googlebot-News index","rank-math-pro"),help:(0,o.__)('Using an "Index" or "NoIndex" option allows you to control what Google News Bot (not to be confused with Google Search Bot) can include or not include in the Google News Index.',"rank-math-pro"),selected:e.robots,options:[{value:"index",label:(0,o.__)("Index","rank-math-pro")},{value:"noindex",label:(0,o.__)("No Index","rank-math-pro")}],onChange:e.updateNewsRobots})))}));(0,e.addAction)("rank_math_loaded","rank-math-pro",(function(){(0,e.addFilter)("rankMath.advanced.newsSitemap","rank-math-pro",(function(){return l}))}))})();

View File

@@ -0,0 +1,118 @@
<?php
/**
* The News Sitemap Metabox.
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Sitemap;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Admin\Admin_Helper;
use RankMath\Sitemap\Cache_Watcher;
use MyThemeShop\Helpers\Param;
use MyThemeShop\Helpers\WordPress;
defined( 'ABSPATH' ) || exit;
/**
* News_Metabox class.
*/
class News_Metabox {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
if ( ! Helper::has_cap( 'sitemap' ) ) {
return;
}
$this->action( 'save_post', 'save_post' );
$this->action( 'rank_math/admin/editor_scripts', 'enqueue_news_sitemap', 11 );
$this->filter( 'rank_math/metabox/post/values', 'add_metadata', 10, 2 );
}
/**
* Enqueue scripts for the metabox.
*/
public function enqueue_news_sitemap() {
if ( ! $this->can_add_tab() ) {
return;
}
wp_enqueue_script(
'rank-math-pro-news',
RANK_MATH_PRO_URL . 'includes/modules/news-sitemap/assets/js/news-sitemap.js',
[ 'rank-math-pro-editor' ],
rank_math_pro()->version,
true
);
}
/**
* Add meta data to use in gutenberg.
*
* @param array $values Aray of tabs.
* @param Screen $screen Sceen object.
*
* @return array
*/
public function add_metadata( $values, $screen ) {
$object_id = $screen->get_object_id();
$object_type = $screen->get_object_type();
$robots = $screen->get_meta( $object_type, $object_id, 'rank_math_news_sitemap_robots' );
$values['newsSitemap'] = [
'robots' => $robots ? $robots : 'index',
];
return $values;
}
/**
* Check for relevant post type before invalidation.
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*
* @param int $post_id Post ID to possibly invalidate for.
*/
public function save_post( $post_id ) {
if (
wp_is_post_revision( $post_id ) ||
! $this->can_add_tab( get_post_type( $post_id ) ) ||
false === Helper::is_post_indexable( $post_id )
) {
return false;
}
Cache_Watcher::invalidate( 'news' );
}
/**
* Show field check callback.
*
* @param string $post_type Current Post Type.
*
* @return boolean
*/
private function can_add_tab( $post_type = false ) {
if ( Admin_Helper::is_term_profile_page() || Admin_Helper::is_posts_page() ) {
return false;
}
$post_type = $post_type ? $post_type : WordPress::get_post_type();
return in_array(
$post_type,
(array) Helper::get_settings( 'sitemap.news_sitemap_post_type' ),
true
);
}
}

View File

@@ -0,0 +1,300 @@
<?php
/**
* The Sitemap Module
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Sitemap;
use RankMath\Helper;
use RankMath\Sitemap\Router;
use RankMath\Sitemap\Providers\Post_Type;
defined( 'ABSPATH' ) || exit;
/**
* News_Provider class.
*/
class News_Provider extends Post_Type {
/**
* Indicate that this provider should show an empty sitemap instead of a 404.
*
* @var boolean
*/
public $should_show_empty = true;
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
* @return boolean
*/
public function handles_type( $type ) {
return 'news' === $type;
}
/**
* Get set of sitemaps index link data.
*
* @param int $max_entries Entries per sitemap.
* @return array
*/
public function get_index_links( $max_entries ) { // phpcs:ignore
global $wpdb;
$index = [];
$post_types = Helper::get_settings( 'sitemap.news_sitemap_post_type' );
$posts = $this->get_posts( $post_types, 1, 0 );
if ( empty( $posts ) ) {
return $index;
}
$item = $this->do_filter(
'sitemap/index/entry',
[
'loc' => Router::get_base_url( 'news-sitemap.xml' ),
'lastmod' => get_lastpostdate( 'gmt' ),
],
'news',
);
if ( $item ) {
$index[] = $item;
}
return $index;
}
/**
* Get set of sitemap link data.
*
* @param string $type Sitemap type.
* @param int $max_entries Entries per sitemap.
* @param int $current_page Current page of the sitemap.
* @return array
*/
public function get_sitemap_links( $type, $max_entries, $current_page ) { // phpcs:ignore
$links = [];
$post_types = Helper::get_settings( 'sitemap.news_sitemap_post_type' );
$posts = $this->get_posts( $post_types, 1000, 0 );
if ( empty( $posts ) ) {
return $links;
}
foreach ( $posts as $post ) {
if ( ! Helper::is_post_indexable( $post->ID ) ) {
continue;
}
if ( ! News_Sitemap::is_post_indexable( $post->ID ) ) {
continue;
}
$url = $this->get_url( $post );
if ( ! isset( $url['loc'] ) ) {
continue;
}
/**
* Filter URL entry before it gets added to the sitemap.
*
* @param array $url Array of URL parts.
* @param string $type URL type.
* @param object $user Data object for the URL.
*/
$url = $this->do_filter( 'sitemap/entry', $url, 'post', $post );
if ( empty( $url ) ) {
continue;
}
if ( (int) $post->ID === $this->get_page_for_posts_id() || (int) $post->ID === $this->get_page_on_front_id() ) {
array_unshift( $links, $url );
continue;
}
$links[] = $url;
}
return $links;
}
/**
* Retrieve set of posts with optimized query routine.
*
* @param array $post_types Post type to retrieve.
* @param int $count Count of posts to retrieve.
* @param int $offset Starting offset.
*
* @return object[]
*/
protected function get_posts( $post_types, $count, $offset ) { // phpcs:ignore
global $wpdb;
if ( empty( $post_types ) ) {
return [];
}
if ( ! is_array( $post_types ) ) {
$post_types = [ $post_types ];
}
/**
* Get posts for the last two days only.
*/
$sql = "
SELECT *
FROM {$wpdb->posts}
WHERE post_status='publish'
AND ( TIMESTAMPDIFF( MINUTE, post_date_gmt, UTC_TIMESTAMP() ) <= ( 48 * 60 ) )
AND post_type IN ( '" . join( "', '", esc_sql( $post_types ) ) . "' )
";
$terms_query = '';
foreach ( $post_types as $post_type ) {
$terms = current( Helper::get_settings( "sitemap.news_sitemap_exclude_{$post_type}_terms", [] ) );
if ( empty( $terms ) ) {
continue;
}
array_map(
function ( $key ) use ( $terms, &$terms_query, $wpdb ) {
$placeholders = implode( ', ', array_fill( 0, count( $terms[ $key ] ), '%d' ) );
$terms_sql = "
{$wpdb->posts}.ID NOT IN (
SELECT object_id
FROM {$wpdb->term_relationships}
WHERE term_taxonomy_id IN ($placeholders)
)
AND";
$terms_query .= $wpdb->prepare( $terms_sql, $terms[ $key ] ); // phpcs:ignore
},
array_keys( $terms )
);
}
// Remove the last AND, no way to tell the last items.
if ( $terms_query ) {
$terms_query = preg_replace( '/(AND)$/i', '', $terms_query );
$terms_query = "( {$terms_query} )";
$sql .= 'AND ' . $terms_query;
}
$sql .= "
AND post_password = ''
ORDER BY post_date_gmt DESC
LIMIT 0, %d
";
$count = max( 1, min( 1000, $count ) );
return $wpdb->get_results( $wpdb->prepare( $sql, $count ) ); // phpcs:ignore
}
/**
* Produce array of URL parts for given post object.
*
* @param object $post Post object to get URL parts for.
* @return array|boolean
*/
protected function get_url( $post ) {
$url = [];
/**
* Filter the URL Rank Math SEO uses in the XML sitemap.
*
* Note that only absolute local URLs are allowed as the check after this removes external URLs.
*
* @param string $url URL to use in the XML sitemap
* @param object $post Post object for the URL.
*/
$url['loc'] = $this->do_filter( 'sitemap/xml_post_url', get_permalink( $post ), $post );
/**
* Do not include external URLs.
*
* @see https://wordpress.org/plugins/page-links-to/ can rewrite permalinks to external URLs.
*/
if ( 'external' === $this->get_classifier()->classify( $url['loc'] ) ) {
return false;
}
$canonical = Helper::get_post_meta( 'canonical', $post->ID );
if ( '' !== $canonical && $canonical !== $url['loc'] ) {
/*
* Let's assume that if a canonical is set for this page and it's different from
* the URL of this post, that page is either already in the XML sitemap OR is on
* an external site, either way, we shouldn't include it here.
*/
return false;
}
rank_math()->variables->setup();
$url['title'] = $this->get_title( $post );
$url['publication_date'] = $post->post_date_gmt;
return $url;
}
/**
* Get Post Title.
*
* @param WP_Post $post Post Object.
*
* @return string
*/
private function get_title( $post ) {
$title = Helper::get_post_meta( 'title', $post->ID );
return $title ? $title : $post->post_title;
}
/**
* Filter Posts by excluded terms.
*
* @param object $posts News Sitemap Posts.
*
* @return object[]
*/
private function filter_posts_by_terms( $posts ) {
if ( empty( $posts ) ) {
return $posts;
}
foreach ( $posts as $key => $post ) {
$exclude_terms = current( Helper::get_settings( "sitemap.news_sitemap_exclude_{$post->post_type}_terms", [] ) );
if ( empty( $exclude_terms ) ) {
continue;
}
$exclude_terms = array_merge( ...array_values( $exclude_terms ) );
$taxonomies = get_object_taxonomies( $post->post_type, 'names' );
if ( empty( $taxonomies ) ) {
continue;
}
$terms = wp_get_object_terms(
$post->ID,
$taxonomies,
[
'fields' => 'ids',
]
);
if ( ! empty( array_intersect( $terms, $exclude_terms ) ) ) {
unset( $posts[ $key ] );
}
}
return $posts;
}
}

View File

@@ -0,0 +1,323 @@
<?php
/**
* The News Sitemap Module
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Sitemap;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Helpers\Locale;
use RankMath\Sitemap\Cache_Watcher;
use RankMath\Traits\Hooker;
use RankMath\Sitemap\Router;
use MyThemeShop\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* News_Sitemap class.
*/
class News_Sitemap {
use Hooker;
/**
* NEWS Publication.
*
* @var string
*/
protected $news_publication = null;
/**
* The Constructor.
*/
public function __construct() {
if ( is_admin() ) {
$this->filter( 'rank_math/settings/sitemap', 'add_settings', 11 );
}
new News_Metabox();
$this->action( 'rank_math/head', 'robots', 10 );
$this->filter( 'rank_math/sitemap/providers', 'add_provider' );
$this->filter( 'rank_math/sitemap/news_urlset', 'xml_urlset' );
$this->filter( 'rank_math/sitemap/xsl_news', 'sitemap_xsl' );
$this->filter( 'rank_math/sitemap/news_stylesheet_url', 'stylesheet_url' );
$this->filter( 'rank_math/sitemap/news_sitemap_url', 'sitemap_url', 10, 2 );
$this->filter( 'rank_math/schema/default_type', 'change_default_schema_type', 10, 3 );
$this->filter( 'rank_math/snippet/rich_snippet_article_entity', 'add_copyrights_data' );
$this->action( 'admin_post_rank-math-options-sitemap', 'save_exclude_terms_data', 9 );
$this->action( 'transition_post_status', 'status_transition', 10, 3 );
}
/**
* Function to pass empty array to exclude terms data when term is not selected for a Post type.
* This code is needed to save empty group value since CMB2 doesn't allow it.
*
* @since 2.8.1
* @return void
*/
public function save_exclude_terms_data() {
$post_types = Helper::get_settings( 'sitemap.news_sitemap_post_type', [] );
if ( empty( $post_types ) ) {
return;
}
foreach ( $post_types as $post_type ) {
if ( ! isset( $_POST["news_sitemap_exclude_{$post_type}_terms"] ) ) { //phpcs:ignore
$_POST["news_sitemap_exclude_{$post_type}_terms"] = []; //phpcs:ignore
}
}
}
/**
* Output the meta robots tag.
*/
public function robots() {
if ( ! is_singular() ) {
return;
}
$post = get_post();
/**
* Filter: 'rank_math/sitemap/news/noindex' - Allow preventing of outputting noindex tag.
*
* @api string $meta_robots The noindex tag.
*
* @param object $post The post.
*/
if ( ! $this->do_filter( 'sitemap/news/noindex', true, $post ) || self::is_post_indexable( $post->ID ) ) {
return;
}
echo '<meta name="Googlebot-News" content="noindex" />' . "\n";
}
/**
* Check if post is indexable.
*
* @param int $post_id Post ID to check.
*
* @return boolean
*/
public static function is_post_indexable( $post_id ) {
$robots = get_post_meta( $post_id, 'rank_math_news_sitemap_robots', true );
if ( ! empty( $robots ) && 'noindex' === $robots ) {
return false;
}
return true;
}
/**
* Add module settings into general optional panel.
*
* @param array $tabs Array of option panel tabs.
*
* @return array
*/
public function add_settings( $tabs ) {
$sitemap_url = Router::get_base_url( 'news-sitemap.xml' );
$tabs['news-sitemap'] = [
'icon' => 'fa fa-newspaper-o',
'title' => esc_html__( 'News Sitemap', 'rank-math-pro' ),
'icon' => 'rm-icon rm-icon-post',
'desc' => wp_kses_post( sprintf( __( 'News Sitemaps allow you to control which content you submit to Google News. More information: <a href="%s" target="_blank">News Sitemaps overview</a>', 'rank-math-pro' ), KB::get( 'news-sitemap', 'Options Panel Sitemap News Tab' ) ) ),
'file' => dirname( __FILE__ ) . '/settings-news.php',
/* translators: News Sitemap Url */
'after_row' => '<div class="notice notice-alt notice-info info inline rank-math-notice"><p>' . sprintf( esc_html__( 'Your News Sitemap index can be found here: : %s', 'rank-math-pro' ), '<a href="' . $sitemap_url . '" target="_blank">' . $sitemap_url . '</a>' ) . '</p></div>',
];
return $tabs;
}
/**
* Add news sitemap provider.
*
* @param array $providers Sitemap provider registry.
*/
public function add_provider( $providers ) {
$providers[] = new \RankMathPro\Sitemap\News_Provider();
return $providers;
}
/**
* Produce XML output for google news urlset.
*
* @return string
*/
public function xml_urlset() {
return '<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
. 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd '
. 'http://www.google.com/schemas/sitemap-news/0.9 http://www.google.com/schemas/sitemap-news/0.9/sitemap-news.xsd" '
. 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" '
. 'xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">' . "\n";
}
/**
* Stylesheet Url for google news.
*
* @param string $url Current stylesheet url.
* @return string
*/
public function stylesheet_url( $url ) { // phpcs:ignore
$stylesheet_url = preg_replace( '/(^http[s]?:)/', '', Router::get_base_url( 'news-sitemap.xsl' ) );
return '<?xml-stylesheet type="text/xsl" href="' . $stylesheet_url . '"?>';
}
/**
* Stylesheet for google news.
*
* @param string $title Title for stylesheet.
*/
public function sitemap_xsl( $title ) { // phpcs:ignore
require_once 'sitemap-xsl.php';
}
/**
* Build the `<url>` tag for a given URL.
*
* @param array $url Array of parts that make up this entry.
* @param Renderer $renderer Sitemap renderer class object.
* @return string
*/
public function sitemap_url( $url, $renderer ) {
$date = null;
if ( ! empty( $url['publication_date'] ) ) {
// Create a DateTime object date in the correct timezone.
$date = $renderer->timezone->format_date( $url['publication_date'] );
}
$output = $renderer->newline( '<url>', 1 );
$output .= $renderer->newline( '<loc>' . $renderer->encode_url_rfc3986( htmlspecialchars( $url['loc'] ) ) . '</loc>', 2 );
$output .= $renderer->newline( '<news:news>', 2 );
$output .= $this->get_news_publication( $renderer );
$output .= empty( $date ) ? '' : $renderer->newline( '<news:publication_date>' . htmlspecialchars( $date ) . '</news:publication_date>', 3 );
$output .= $renderer->add_cdata( $url['title'], 'news:title', 3 );
$output .= $renderer->newline( '</news:news>', 2 );
$output .= $renderer->newline( '</url>', 1 );
/**
* Filters the output for the sitemap url tag.
*
* @param string $output The output for the sitemap url tag.
* @param array $url The sitemap url array on which the output is based.
*/
return $this->do_filter( 'sitemap_url', $output, $url );
}
/**
* Get News Pub Tags.
*
* @param Renderer $renderer Sitemap renderer class object.
* @return string
*/
private function get_news_publication( $renderer ) {
if ( is_null( $this->news_publication ) ) {
$lang = Locale::get_site_language();
$name = Helper::get_settings( 'sitemap.news_sitemap_publication_name' );
$name = $name ? $name : get_bloginfo( 'name' );
$this->news_publication = '';
$this->news_publication .= $renderer->newline( '<news:publication>', 3 );
$this->news_publication .= $renderer->newline( '<news:name>' . esc_html( $name ) . '</news:name>', 4 );
$this->news_publication .= $renderer->newline( '<news:language>' . $lang . '</news:language>', 4 );
$this->news_publication .= $renderer->newline( '</news:publication>', 3 );
}
return $this->news_publication;
}
/**
* Change default schema type on News Posts.
*
* @param string $schema Default schema type.
* @param string $post_type Current Post Type.
* @param int $post_id Current Post ID.
*
* @return string
*/
public function change_default_schema_type( $schema, $post_type, $post_id ) {
$news_post_types = (array) Helper::get_settings( 'sitemap.news_sitemap_post_type' );
if ( ! in_array( $post_type, $news_post_types, true ) ) {
return $schema;
}
$exclude_terms = (array) Helper::get_settings( "sitemap.news_sitemap_exclude_{$post_type}_terms" );
if ( empty( $exclude_terms[0] ) ) {
return 'NewsArticle';
}
$has_excluded_term = false;
foreach ( $exclude_terms[0] as $taxonomy => $terms ) {
if ( has_term( $terms, $taxonomy, $post_id ) ) {
$has_excluded_term = true;
break;
}
}
return $has_excluded_term ? $schema : 'NewsArticle';
}
/**
* Filter to add Copyrights data in Article Schema on News Posts.
*
* @param array $entity Snippet Data.
* @return array
*/
public function add_copyrights_data( $entity ) {
global $post;
if ( is_null( $post ) ) {
return $entity;
}
$news_post_types = (array) Helper::get_settings( 'sitemap.news_sitemap_post_type' );
if ( ! in_array( $post->post_type, $news_post_types, true ) ) {
return $entity;
}
$entity['copyrightYear'] = get_the_modified_date( 'Y', $post );
if ( ! empty( $entity['publisher'] ) ) {
$entity['copyrightHolder'] = $entity['publisher'];
}
return $entity;
}
/**
* Invalidate News Sitemap cache when a scheduled post is published.
*
* @param string $new_status New Status.
* @param string $old_status Old Status.
* @param object $post Post Object.
*/
public function status_transition( $new_status, $old_status, $post ) {
if ( $old_status === $new_status || 'publish' !== $new_status ) {
return;
}
$news_post_types = (array) Helper::get_settings( 'sitemap.news_sitemap_post_type', [] );
if ( ! in_array( $post->post_type, $news_post_types, true ) ) {
return;
}
if ( false === Helper::is_post_indexable( $post->ID ) ) {
return;
}
Cache_Watcher::invalidate( 'news' );
}
}

View File

@@ -0,0 +1,97 @@
<?php
/**
* Sitemap - News
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
$cmb->add_field(
[
'id' => 'news_sitemap_publication_name',
'type' => 'text',
'name' => esc_html__( 'Google News Publication Name', 'rank-math-pro' ),
'desc' => wp_kses_post( __( 'The name of the news publication. It must match the name exactly as it appears on your articles in news.google.com, omitting any trailing parentheticals. <a href="https://support.google.com/news/publisher-center/answer/9606710" target="_blank">More information at support.google.com</a>', 'rank-math-pro' ) ),
]
);
$post_types = Helper::choices_post_types();
if ( isset( $post_types['attachment'] ) && Helper::get_settings( 'general.attachment_redirect_urls', true ) ) {
unset( $post_types['attachment'] );
}
$cmb->add_field(
[
'id' => 'news_sitemap_post_type',
'type' => 'multicheck_inline',
'name' => esc_html__( 'News Post Type', 'rank-math-pro' ),
'desc' => esc_html__( 'Select the post type you use for News articles.', 'rank-math-pro' ),
'options' => $post_types,
]
);
$post_types = Helper::get_settings( 'sitemap.news_sitemap_post_type', [] );
if ( empty( $post_types ) ) {
return;
}
foreach ( $post_types as $post_type ) {
$taxonomies = Helper::get_object_taxonomies( $post_type, 'objects' );
if ( empty( $taxonomies ) ) {
continue;
}
$post_type_obj = get_post_type_object( $post_type );
$post_type_label = $post_type_obj->labels->singular_name;
$group_field_id = '';
foreach ( $taxonomies as $taxonomy => $data ) {
if ( empty( $data->show_ui ) ) {
continue;
}
$terms = get_terms(
[
'taxonomy' => $taxonomy,
'show_ui' => true,
'fields' => 'id=>name',
'update_term_meta_cache' => false,
]
);
if ( 0 === count( $terms ) ) {
continue;
}
if ( ! $group_field_id ) {
$group_field_id = $cmb->add_field(
[
'id' => "news_sitemap_exclude_{$post_type}_terms",
'type' => 'group',
/* translators: Post Type */
'name' => sprintf( __( 'Exclude %s Terms ', 'rank-math-pro' ), $post_type_label ),
'classes' => 'news-sitemap-exclude-terms cmb-group-text-only',
'repeatable' => false,
]
);
}
$cmb->add_group_field(
$group_field_id,
[
'name' => '',
'id' => $taxonomy,
'type' => 'multicheck_inline',
'options' => $terms,
'classes' => 'cmb-field-list',
/* translators: 1. Taxonomy Name 2. Post Type */
'desc' => sprintf( esc_html__( '%1$s to exclude for %2$s.', 'rank-math-pro' ), $data->label, $post_type_label ),
]
);
}
}

View File

@@ -0,0 +1,184 @@
<?php
/**
* Sitemap stylesheet.
*
* @package RankMath
* @subpackage RankMath\Sitemap
*/
use RankMath\KB;
use RankMath\Sitemap\Router;
use RankMath\Sitemap\Sitemap;
defined( 'ABSPATH' ) || exit;
// Echo so opening tag doesn't get confused for PHP.
echo '<?xml version="1.0" encoding="UTF-8"?>';
?>
<xsl:stylesheet version="2.0"
xmlns:html="http://www.w3.org/TR/REC-html40"
xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title><?php echo esc_html( $title ); ?></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
body {
font-size: 14px;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
margin: 0;
color: #545353;
}
a {
color: #05809e;
text-decoration: none;
}
h1 {
font-size: 24px;
font-family: Verdana,Geneva,sans-serif;
font-weight: normal;
margin: 0;
}
#description {
background-color: #4275f4;
padding: 20px 40px;
color: #fff;
padding: 30px 30px 20px;
}
#description h1,
#description p,
#description a {
color: #fff;
margin: 0;
font-size: 1.1em;
}
#description h1 {
font-size: 2em;
margin-bottom: 1em;
}
#description p {
margin-top: 5px;
}
#content {
padding: 20px 30px;
background: #fff;
max-width: 75%;
margin: 0 auto;
}
table {
border: none;
border-collapse: collapse;
font-size: .9em;
width: 100%;
}
th {
background-color: #4275f4;
color: #fff;
text-align: left;
padding: 15px;
font-size: 14px;
cursor: pointer;
}
td {
padding: 10px;
border-bottom: 1px solid #ddd;
}
tbody tr:nth-child(even) {
background-color: #f7f7f7;
}
table td a {
display: block;
}
table td a img {
max-height: 30px;
margin: 6px 3px;
}
</style>
</head>
<body>
<div id="description">
<h1><?php esc_html_e( 'XML Sitemap', 'rank-math-pro' ); ?></h1>
<?php if ( false === $this->do_filter( 'sitemap/remove_credit', false ) ) : ?>
<p>
<?php
printf(
wp_kses_post(
/* translators: link to rankmath.com */
__( 'This XML Sitemap is generated by <a href="%s" target="_blank">Rank Math WordPress SEO Plugin</a>. It is what search engines like Google use to crawl and re-crawl posts/pages/products/images/archives on your website.', 'rank-math-pro' )
),
KB::get( 'seo-suite' )
);
?>
</p>
<?php endif; ?>
<p>
<?php
printf(
wp_kses_post(
/* translators: link to rankmath.com */
__( 'Learn more about <a href="%s" target="_blank">XML Sitemaps</a>.', 'rank-math-pro' )
),
'http://sitemaps.org'
);
?>
</p>
</div>
<div id="content">
<p>
<?php
printf(
/* translators: xsl value count */
__( 'This XML Sitemap contains <strong>%s</strong> URLs.', 'rank-math-pro' ),
'<xsl:value-of select="count(sitemap:urlset/sitemap:url)"/>'
);
?>
</p>
<p class="expl">
<?php
printf(
/* translators: xsl value count */
__( '<a href="%s">&#8592; Sitemap Index</a>', 'rank-math-pro' ),
esc_url( Router::get_base_url( Sitemap::get_sitemap_index_slug() . '.xml' ) )
);
?>
</p>
<table id="sitemap" cellpadding="3">
<thead>
<tr>
<th width="40%">Title</th>
<th width="15%"><?php esc_html_e( 'Publication Date', 'rank-math-pro' ); ?></th>
</tr>
</thead>
<tbody>
<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:for-each select="sitemap:urlset/sitemap:url">
<tr>
<td>
<xsl:variable name="itemURL">
<xsl:value-of select="sitemap:loc"/>
</xsl:variable>
<a href="{$itemURL}">
<xsl:value-of select="news:news/news:title"/>
</a>
</td>
<td>
<xsl:value-of select="concat(substring(news:news/news:publication_date,0,11),concat(' ', substring(news:news/news:publication_date,12,8)),concat(' ', substring(news:news/news:publication_date,20,6)))"/>
</td>
</tr>
</xsl:for-each>
</tbody>
</table>
</div>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View File

@@ -0,0 +1,202 @@
<?php
/**
* Add Podcasts RSS feed.
*
* @since 3.0.17
* @package RankMath
* @subpackage RankMathPro\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Podcast;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Media_RSS class.
*/
class Podcast_RSS {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$prefix = 'rss2_podcast';
if ( apply_filters( 'rank_math/podcast/enhance_all_feeds', true ) ) {
$prefix = 'rss2';
}
remove_action( 'rss2_head', 'rss2_site_icon' );
$this->action( "{$prefix}_ns", 'add_namespace' );
$this->action( "{$prefix}_head", 'add_channel_data' );
$this->action( "{$prefix}_item", 'add_podcast_data', 10, 1 );
}
/**
* Add namespace to RSS feed.
*/
public function add_namespace() {
if ( apply_filters( 'rank_math/rss/add_podcasts_namespace', true ) ) {
echo 'xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" ';
}
$this->filter( 'get_wp_title_rss', 'feed_title' );
$this->filter( 'bloginfo_rss', 'feed_description', 10, 2 );
}
/**
* Change the feed title.
*
* @param string $wp_title_rss The current blog title.
*/
public function feed_title( $wp_title_rss ) {
$podcast_title = Helper::get_settings( 'general.podcast_title' );
if ( $podcast_title ) {
return Helper::replace_vars( $podcast_title );
}
return $wp_title_rss;
}
/**
* Change the feed description.
*
* @param string $value RSS container for the blog information.
* @param string $show The type of blog information to retrieve.
*/
public function feed_description( $value, $show ) {
if ( 'description' !== $show ) {
return $value;
}
$podcast_description = Helper::get_settings( 'general.podcast_description' );
if ( $podcast_description ) {
return Helper::replace_vars( $podcast_description );
}
return $value;
}
/**
* Add Podcast channel data
*/
public function add_channel_data() {
$category = Helper::get_settings( 'general.podcast_category' );
if ( $category ) {
$this->newline( '<itunes:category text="' . esc_attr( $category ) . '" />', 1 );
}
$author_name = Helper::get_settings( 'general.podcast_owner' );
$author_email = Helper::get_settings( 'general.podcast_owner_email' );
if ( $author_email ) {
$this->newline( '<itunes:author>' . esc_html( $author_name ) . '</itunes:author>', 1 );
$this->newline( '<itunes:owner>', 1 );
if ( $author_email ) {
$this->newline( '<itunes:name>' . esc_html( $author_name ) . '</itunes:name>', 2 );
}
$this->newline( '<itunes:email>' . esc_html( $author_email ) . '</itunes:email>', 2 );
$this->newline( '</itunes:owner>', 1 );
}
$image = Helper::get_settings( 'general.podcast_image' );
if ( $image ) {
$this->newline( '<itunes:image href="' . esc_url( $image ) . '" />', 1 );
$this->newline( '<image>', 1 );
$this->newline( '<title>' . get_wp_title_rss() . '</title>', 2 );
$this->newline( '<url>' . esc_url( $image ) . '</url>', 2 );
$this->newline( '<link>' . get_bloginfo_rss( 'url' ) . '</link>', 2 );
$this->newline( '</image>', 1 );
}
$title = Helper::get_settings( 'general.podcast_title' );
if ( $title ) {
$this->newline( '<itunes:subtitle>' . esc_html( Helper::replace_vars( $title ) ) . '</itunes:subtitle>', 1 );
}
$summary = Helper::get_settings( 'general.podcast_description' );
if ( $summary ) {
$this->newline( '<itunes:summary>' . esc_html( Helper::replace_vars( $summary ) ) . '</itunes:summary>', 1 );
}
$is_explicit = Helper::get_settings( 'general.podcast_explicit' ) ? 'yes' : 'clean';
$this->newline( '<itunes:explicit>' . $is_explicit . '</itunes:explicit>', 1 );
$copyright = Helper::get_settings( 'general.podcast_copyright_text' );
if ( $copyright ) {
$copyright = str_replace( '©', '&#xA9;', $copyright );
$this->newline( '<copyright>' . esc_html( $copyright ) . '</copyright>', 1 );
}
}
/**
* Add Podcast Data in RSS feed.
*
* @see https://support.google.com/podcast-publishers/answer/9889544
* @see https://podcasters.apple.com/support/823-podcast-requirements
*/
public function add_podcast_data() {
global $post;
$podcast = get_post_meta( $post->ID, 'rank_math_schema_PodcastEpisode', true );
if ( empty( $podcast ) ) {
return;
}
$title = ! empty( $podcast['name'] ) ? Helper::replace_vars( $podcast['name'], $post ) : '';
$description = ! empty( $podcast['description'] ) ? Helper::replace_vars( $podcast['description'], $post ) : '';
$audio_file = $podcast['associatedMedia']['contentUrl'];
$duration = ! empty( $podcast['timeRequired'] ) ? Helper::duration_to_seconds( $podcast['timeRequired'] ) : '';
$image = ! empty( $podcast['thumbnailUrl'] ) ? Helper::replace_vars( $podcast['thumbnailUrl'], $post ) : '';
$author = ! empty( $podcast['author'] ) ? Helper::replace_vars( $podcast['author']['name'], $post ) : '';
$is_explicit = empty( $podcast['isFamilyFriendly'] ) ? 'yes' : 'clean';
$episode_number = ! empty( $podcast['episodeNumber'] ) ? $podcast['episodeNumber'] : '';
$season_number = ! empty( $podcast['partOfSeason'] ) && ! empty( $podcast['partOfSeason']['seasonNumber'] ) ? $podcast['partOfSeason']['seasonNumber'] : '';
if ( $title ) {
$this->newline( '<itunes:title>' . wp_kses_post( $title ) . '</itunes:title>' );
}
if ( $description ) {
$this->newline( '<itunes:summary>' . wp_kses_post( $description ) . '</itunes:summary>', 2 );
}
if ( $image ) {
$this->newline( '<itunes:image href="' . esc_url( $image ) . '" />', 2 );
}
if ( $duration ) {
$this->newline( '<itunes:duration>' . $duration . '</itunes:duration>', 2 );
}
if ( $author ) {
$this->newline( '<itunes:author>' . esc_html( $author ) . '</itunes:author>', 2 );
}
if ( $season_number ) {
$this->newline( '<itunes:season>' . esc_html( $season_number ) . '</itunes:season>', 2 );
}
if ( $episode_number ) {
$this->newline( '<itunes:episode>' . esc_html( $episode_number ) . '</itunes:episode>', 2 );
}
$this->newline( '<itunes:explicit>' . $is_explicit . '</itunes:explicit>', 2 );
$tracking_prefix = Helper::get_settings( 'general.podcast_tracking_prefix' );
$this->newline( '<enclosure url="' . esc_attr( $tracking_prefix ) . esc_url( $audio_file ) . '" length="' . $duration . '" type="audio/mpeg" />', 2 );
}
/**
* Write a newline with indent count.
*
* @param string $content Content to write.
* @param integer $indent Count of indent.
*/
private function newline( $content, $indent = 0 ) {
echo str_repeat( "\t", $indent ) . $content . "\n";
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* The Podcast Schema.
*
* @since 3.0.17
* @package RankMath
* @subpackage RankMathPro\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Podcast;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use MyThemeShop\Helpers\Arr;
defined( 'ABSPATH' ) || exit;
/**
* Podcast class.
*/
class Podcast {
use Hooker;
/**
* Store podcast feed slug.
*/
private $podcast;
/**
* The Constructor.
*/
public function __construct() {
$this->filter( 'rank_math/settings/general', 'add_settings' );
$this->action( 'init', 'init' );
$this->action( 'rank_math/vars/register_extra_replacements', 'register_replacements' );
}
/**
* Intialize.
*/
public function init() {
$this->podcast = $this->do_filter( 'podcast/feed', 'podcast' );
add_feed( $this->podcast, [ $this, 'podcast_feed' ] );
new Podcast_RSS();
new Publish_Podcast();
}
/**
* Registers variable replacements for Rank Math Pro.
*/
public function register_replacements() {
rank_math_register_var_replacement(
'podcast_image',
[
'name' => esc_html__( 'Podcast Image', 'rank-math-pro' ),
'description' => esc_html__( 'Podcast channel image configured in the Rank Math Settings.', 'rank-math-pro' ),
'variable' => 'podcast_image',
'example' => '',
],
[ $this, 'get_podcast_image' ]
);
}
/**
* Get Podcast image from the Settings.
*
* @return string Podcast image.
*/
public function get_podcast_image() {
return Helper::get_settings( 'general.podcast_image' );
}
/**
* 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,
[
'podcast' => [
'icon' => 'rm-icon rm-icon-podcast',
'title' => esc_html__( 'Podcast', 'rank-math-pro' ),
/* translators: Link to kb article */
'desc' => sprintf( esc_html__( 'Make your podcasts discoverable via Google Podcasts, Apple Podcasts, and similar services. %s.', 'rank-math' ), '<a href="' . KB::get( 'podcast-settings', 'Options Panel Podcast Tab' ) . '" target="_blank">' . esc_html__( 'Learn more', 'rank-math-pro' ) . '</a>' ),
'file' => dirname( __FILE__ ) . '/views/options.php',
/* translators: Link to Podcast RSS feed */
'after_row' => '<div class="notice notice-alt notice-info info inline rank-math-notice"><p>' . sprintf( esc_html__( 'Your Podcast RSS feed can be found here: %s', 'rank-math-pro' ), '<a href="' . get_feed_link( $this->podcast ) . '" target="_blank">' . get_feed_link( $this->podcast ) . '</a>' ) . '</p></div>',
],
],
12
);
return $tabs;
}
/**
* Add all podcasts feed to /feed/podcast.
*/
public function podcast_feed() {
require dirname( __FILE__ ) . '/views/feed-rss2.php';
}
/**
* Get podcasts
*/
public function get_podcasts() {
$post_types = array_filter(
Helper::get_accessible_post_types(),
function( $post_type ) {
return 'attachment' !== $post_type;
}
);
$args = $this->do_filter(
'podcast_args',
[
'post_type' => array_keys( $post_types ),
'posts_per_page' => get_option( 'posts_per_rss' ),
'meta_query' => [
[
'key' => 'rank_math_schema_PodcastEpisode',
'compare' => 'EXISTS',
],
],
]
);
return new \WP_Query( $args );
}
}

View File

@@ -0,0 +1,113 @@
<?php
/**
* Add Podcasts RSS feed.
*
* @since 3.0.17
* @package RankMath
* @subpackage RankMathPro\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Podcast;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Schema\DB;
defined( 'ABSPATH' ) || exit;
/**
* Publish_Podcast class.
*/
class Publish_Podcast {
use Hooker;
/**
* Has Podcast schema.
*
* @var string
*/
protected $has_podcast_schema = false;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'rank_math/schema/update', 'publish_podcast' );
$this->action( 'rank_math/pre_update_schema', 'has_podcast_schema' );
$this->action( 'rss2_podcast_head', 'add_hub_urls' );
}
/**
* Check if current post already have a Podcast schema.
*
* @param int $post_id Current Post ID.
*/
public function has_podcast_schema( $post_id ) {
$schema_types = DB::get_schema_types( $post_id );
$this->has_podcast_schema = ! empty( $schema_types ) && in_array( 'PodcastEpisode', explode( ', ', $schema_types ), true );
}
/**
* Publish podcast when a new post is published.
*
* @param int $post_id Current Post ID.
*/
public function publish_podcast( $post_id ) {
if ( $this->has_podcast_schema ) {
return;
}
$podcast = get_post_meta( $post_id, 'rank_math_schema_PodcastEpisode', true );
if ( empty( $podcast ) ) {
return;
}
$hub_urls = $this->get_hub_urls();
if ( empty( $hub_urls ) ) {
return;
}
$user_agent = $this->do_filter( 'podcast/useragent', 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ) );
$podcast_feed = esc_url( home_url( 'feed/podcast' ) );
$args = [
'timeout' => 100,
'user-agent' => "$user_agent; PubSubHubbub/WebSub",
'body' => "hub.mode=publish&hub.url={$podcast_feed}",
];
foreach ( $hub_urls as $hub_url ) {
wp_remote_post( $hub_url, $args );
}
}
/**
* Add Hub urls to podcast feed.
*/
public function add_hub_urls() {
$hub_urls = $this->get_hub_urls();
if ( empty( $hub_urls ) ) {
return;
}
foreach ( $hub_urls as $hub_url ) {
echo '<atom:link rel="hub" href="' . esc_url( $hub_url ) . '" />';
}
}
/**
* Get podcast Hub URLs.
*/
private function get_hub_urls() {
return $this->do_filter(
'podcast/hub_urls',
[
'https://pubsubhubbub.appspot.com',
'https://pubsubhubbub.superfeedr.com',
'https://websubhub.com/hub',
]
);
}
}

View File

@@ -0,0 +1,134 @@
<?php
/**
* RSS2 Feed Template for displaying RSS2 Posts feed.
* Forked from WordPress Core - feed-rss2.php.
*
* @package WordPress
*/
header( 'Content-Type: ' . feed_content_type( 'rss2' ) . '; charset=' . get_option( 'blog_charset' ), true );
$more = 1;
echo '<?xml version="1.0" encoding="' . get_option( 'blog_charset' ) . '"?' . '>';
/**
* Filter to remove Podcast feed credit.
*
* @param boolean Defaults to false.
*/
if ( ! apply_filters( 'rank_math/podcast/remove_credit', false ) ) {
echo "\n<!-- This Podcast feed is generated by Rank Math PRO SEO plugin - rankmath.com -->\n";
}
/**
* Fires between the xml and rss tags in a feed.
*
* @param string $context Type of feed. Possible values include 'rss2', 'rss2-comments',
* 'rdf', 'atom', and 'atom-comments'.
*/
do_action( 'rss_tag_pre', 'rss2' );
?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
<?php
/**
* Fires at the end of the RSS root to add namespaces.
*/
do_action( 'rss2_ns' );
do_action( 'rss2_podcast_ns' );
?>
>
<channel>
<title><?php wp_title_rss(); ?></title>
<atom:link href="<?php self_link(); ?>" rel="self" type="application/rss+xml" />
<link><?php bloginfo_rss( 'url' ); ?></link>
<description><?php bloginfo_rss( 'description' ); ?></description>
<lastBuildDate><?php echo get_feed_build_date( 'r' ); ?></lastBuildDate>
<language><?php bloginfo_rss( 'language' ); ?></language>
<sy:updatePeriod>
<?php
$duration = 'hourly';
/**
* Filters how often to update the RSS feed.
*
* @param string $duration The update period. Accepts 'hourly', 'daily', 'weekly', 'monthly',
* 'yearly'. Default 'hourly'.
*/
echo apply_filters( 'rss_update_period', $duration );
?>
</sy:updatePeriod>
<sy:updateFrequency>
<?php
$frequency = '1';
/**
* Filters the RSS update frequency.
*
* @param string $frequency An integer passed as a string representing the frequency
* of RSS updates within the update period. Default '1'.
*/
echo apply_filters( 'rss_update_frequency', $frequency );
?>
</sy:updateFrequency>
<?php
/**
* Fires at the end of the RSS2 Feed Header.
*/
do_action( 'rss2_head' );
do_action( 'rss2_podcast_head' );
$podcast_query = $this->get_podcasts();
while ( $podcast_query->have_posts() ) :
$podcast_query->the_post();
?>
<item>
<title><?php the_title_rss(); ?></title>
<link><?php the_permalink_rss(); ?></link>
<?php if ( get_comments_number() || comments_open() ) : ?>
<comments><?php comments_link_feed(); ?></comments>
<?php endif; ?>
<dc:creator><![CDATA[<?php the_author(); ?>]]></dc:creator>
<pubDate><?php echo mysql2date( 'D, d M Y H:i:s +0000', get_post_time( 'Y-m-d H:i:s', true ), false ); ?></pubDate>
<?php the_category_rss( 'rss2' ); ?>
<guid isPermaLink="false"><?php the_guid(); ?></guid>
<?php if ( get_option( 'rss_use_excerpt' ) ) : ?>
<description><![CDATA[<?php the_excerpt(); ?>]]></description>
<?php else : ?>
<description><![CDATA[<?php the_excerpt(); ?>]]></description>
<?php
$content = apply_filters( 'the_content', get_the_content() );
$content = str_replace( ']]>', ']]&gt;', $content );
?>
<?php if ( strlen( $content ) > 0 ) : ?>
<content:encoded><![CDATA[<?php echo $content; ?>]]></content:encoded>
<?php else : ?>
<content:encoded><![CDATA[<?php the_excerpt(); ?>]]></content:encoded>
<?php endif; ?>
<?php endif; ?>
<?php if ( get_comments_number() || comments_open() ) : ?>
<wfw:commentRss><?php echo esc_url( get_post_comments_feed_link( null, 'rss2' ) ); ?></wfw:commentRss>
<slash:comments><?php echo get_comments_number(); ?></slash:comments>
<?php endif; ?>
<?php rss_enclosure(); ?>
<?php
/**
* Fires at the end of each RSS2 feed item.
*/
do_action( 'rss2_item' );
do_action( 'rss2_podcast_item' );
?>
</item>
<?php endwhile; ?>
</channel>
</rss>

View File

@@ -0,0 +1,119 @@
<?php
/**
* Podcast general settings.
*
* @package RankMath
* @subpackage RankMathPro\Schema
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
$cmb->add_field(
[
'id' => 'podcast_title',
'type' => 'text',
'name' => esc_html__( 'Podcast Name', 'rank-math-pro' ),
'desc' => esc_html__( 'Name of the podcast.', 'rank-math-pro' ),
'classes' => 'rank-math-supports-variables',
'default' => '%sitename%',
]
);
$cmb->add_field(
[
'id' => 'podcast_description',
'type' => 'textarea_small',
'name' => esc_html__( 'Podcast Description', 'rank-math-pro' ),
'desc' => esc_html__( 'A plaintext description of the podcast.', 'rank-math-pro' ),
'classes' => 'rank-math-supports-variables',
'default' => '%sitedesc%',
]
);
$cmb->add_field(
[
'id' => 'podcast_owner',
'type' => 'text',
'name' => esc_html__( 'Owner Name', 'rank-math-pro' ),
'desc' => esc_html__( 'The podcast owner contact name.', 'rank-math-pro' ),
]
);
$cmb->add_field(
[
'id' => 'podcast_owner_email',
'type' => 'text',
'name' => esc_html__( 'Owner Email ', 'rank-math-pro' ),
'desc' => esc_html__( 'The email address of the podcast owner. Please make sure the email address is active and monitored.', 'rank-math-pro' ),
]
);
$cmb->add_field(
[
'id' => 'podcast_category',
'type' => 'select',
'name' => esc_html__( 'Podcast Category', 'rank-math-pro' ),
'options' => [
'' => esc_html__( 'None', 'rank-math-pro' ),
'Arts' => esc_html__( 'Arts', 'rank-math-pro' ),
'Business' => esc_html__( 'Business', 'rank-math-pro' ),
'Comedy' => esc_html__( 'Comedy', 'rank-math-pro' ),
'Education' => esc_html__( 'Education', 'rank-math-pro' ),
'Games &amp; Hobbies' => esc_html__( 'Games &amp; Hobbies', 'rank-math-pro' ),
'Government &amp; Organizations' => esc_html__( 'Government &amp; Organizations', 'rank-math-pro' ),
'Health' => esc_html__( 'Health', 'rank-math-pro' ),
'Kids &amp; Family' => esc_html__( 'Kids &amp; Family', 'rank-math-pro' ),
'Music' => esc_html__( 'Music', 'rank-math-pro' ),
'News &amp; Politics' => esc_html__( 'News &amp; Politics', 'rank-math-pro' ),
'Religion &amp; Spirituality' => esc_html__( 'Religion &amp; Spirituality', 'rank-math-pro' ),
'Science &amp; Medicine' => esc_html__( 'Science &amp; Medicine', 'rank-math-pro' ),
'Society &amp; Culture' => esc_html__( 'Society &amp; Culture', 'rank-math-pro' ),
'Sports &amp; Recreation' => esc_html__( 'Sports &amp; Recreation', 'rank-math-pro' ),
'TV &amp; Film' => esc_html__( 'TV &amp; Film', 'rank-math-pro' ),
'Technology' => esc_html__( 'Technology', 'rank-math-pro' ),
],
'default' => '',
'desc' => esc_html__( 'Select the category that best reflects the content of your show.', 'rank-math-pro' ),
]
);
$cmb->add_field(
[
'id' => 'podcast_image',
'type' => 'file',
'name' => esc_html__( 'Podcast Image', 'rank-math-pro' ),
'desc' => __( '<strong>Min Size: 1400x1400px, Max Size: 3000x3000px</strong>.<br /> The filesize should not exceed 0.5MB.', 'rank-math-pro' ),
'options' => [ 'url' => false ],
]
);
$cmb->add_field(
[
'id' => 'podcast_tracking_prefix',
'type' => 'text',
'name' => esc_html__( 'Tracking Prefix', 'rank-math-pro' ),
'desc' => esc_html__( 'Add the tracking prefix provided by your tracking service like Chartable, Podsights, Podtrac, etc.', 'rank-math-pro' ),
]
);
$cmb->add_field(
[
'id' => 'podcast_explicit',
'type' => 'toggle',
'name' => esc_html__( 'Is Explicit', 'rank-math-pro' ),
'desc' => esc_html__( 'Indicates whether the podcast is explicit language or adult content.', 'rank-math-pro' ),
'default' => 'off',
]
);
$cmb->add_field(
[
'id' => 'podcast_copyright_text',
'type' => 'text',
'name' => esc_html__( 'Copyright Text', 'rank-math-pro' ),
'desc' => esc_html__( 'Add copyright details if your show is copyrighted.', 'rank-math-pro' ),
]
);

View File

@@ -0,0 +1,5 @@
/*!
* Plugin: Rank Math SEO Pro
* URL: https://rankmath.com/wordpress/plugin/seo-suite/
* Name: post-list.css
*/.tablenav .category-filter-submit,.tablenav .clear-redirection-category-filter{margin-left:5px}.tablenav .clear-redirection-category-filter{display:inline-block;text-decoration:none;line-height:20px;vertical-align:text-top}.tablenav .clear-redirection-category-filter.hidden{display:none}.tablenav .clear-redirection-category-filter .dashicons{color:#72777c}.tablenav .clear-redirection-category-filter:hover .dashicons{color:#c00}.form-field.term-slug-wrap,th.column-slug,td.column-slug{display:none}th.column-posts,td.column-posts{display:none}.cmb2-id-redirection-category .cmb2-metabox-description{margin-bottom:20px}.cmb2-id-redirection-category .manage-redirection-categories-link{font-weight:bold;margin-left:20px}.cmb2-id-redirection-category .add-redirection-category-input{height:40px;line-height:40px}.rank-math-redirections-csv-export{padding:0 20px 20px;border-top:1px solid #b5bfc9}.rank-math-redirections-csv-export .csv-export-footer{margin-top:10px}.rank-math-redirections-csv-export .input-loading{display:none}.rank-math-redirections-csv-import{padding:0 20px 20px}.rank-math-redirections-csv-import ul.description{list-style-type:disc;list-style-position:outside;padding-left:20px;font-size:13px;margin:2px 0 15px;color:#666}#start_date,#end_date{width:120px}.rank-math-redirection-status-locked .row-actions .activate,.rank-math-redirection-status-locked .row-actions .deactivate{display:none}.rank-math-redirection-status-locked .column-sources:before{content:"\f469";font:400 18px/1 dashicons;speak:none;display:inline-block;padding:0 2px 0 0;vertical-align:top;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-decoration:none !important;color:#888}

View File

@@ -0,0 +1 @@
(()=>{"use strict";var e={n:t=>{var a=t&&t.__esModule?()=>t.default:()=>t;return e.d(a,{a}),a},d:(t,a)=>{for(var i in a)e.o(a,i)&&!e.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:a[i]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=jQuery;var a;(a=e.n(t)())((function(){a("#bulk-action-selector-top, #bulk-action-selector-bottom").on("change",(function(){var e=a(this),t=e.closest(".tablenav").find(".redirection-category-filter"),i=e.closest(".tablenav").find(".category-filter-submit");"bulk_add_redirection_category"===e.val()?(t.data("oldval",t.val()).val("none").insertAfter(e),i.addClass("hidden")):(t.insertBefore(i),t.data("oldval")&&(t.val(t.data("oldval")),t.data("oldval","")),i.removeClass("hidden"))})),a(".add-redirection-category-button").on("click",(function(){var e=a(this),t=e.siblings(".add-redirection-category-input");e.addClass("disabled"),t.prop("disabled",!0),a.ajax({url:ajaxurl,type:"POST",dataType:"xml",data:{action:"add-rank_math_redirection_category","_ajax_nonce-add-rank_math_redirection_category":rankMath.add_redirection_category_nonce,newrank_math_redirection_category:t.val(),newrank_math_redirection_category_parent:-1}}).always((function(i){t.val(""),e.removeClass("disabled"),t.prop("disabled",!1);var n=i.getElementsByTagName("response_data")[0].innerHTML.slice(9,-3);a(n).appendTo(e.siblings(".cmb2-checkbox-list")).find("input").prop("name","redirection_category[]")}))})),a(".add-redirection-category-input").on("keydown",(function(e){if(13===e.keyCode)return e.preventDefault(),e.stopPropagation(),a(this).siblings(".add-redirection-category-button").trigger("click"),!1})),a("#rank-math-contextual-help-link").on("click",(function(e){e.preventDefault(),a("#contextual-help-link").trigger("click")})),a("#csv-panel-export-redirections").on("submit",(function(){var e=a(this);e.find('button[type="submit"]').prop("disabled",!0).siblings(".input-loading").css("visibility","visible"),setTimeout((function(){e.find('button[type="submit"]').prop("disabled",!1).siblings(".input-loading").css("visibility","hidden")}),5e3)})),a("#panel-import").on("submit",(function(){return confirm(rankMath.confirmRedirectionsCsvImport)})),a("#csv-import-redirections-cancel").on("click",(function(){return confirm(rankMath.confirmRedirectionsCsvCancel)})),a("#csv-import-redirections-progress-details").length&&setTimeout((function e(){a.ajax({url:ajaxurl,type:"GET",dataType:"html",data:{action:"csv_import_redirections_progress",_ajax_nonce:rankMath.csvProgressNonce}}).done((function(t){a("#csv-import-redirections-progress-details").html(t),a(t).find("#csv-import-progress-value").length?setTimeout(e,3e3):a("#csv-import-cancel").addClass("disabled").prop("disabled",!0).siblings(".input-loading").hide()}))}),3e3),"#import-export-box"===window.location.hash&&a(".rank-math-redirections-form.rank-math-importexport-form").show();var e=a("#start_date"),t=e.val(),i=a("#end_date"),n=i.val(),o=a(".cmb2-id-status"),r=!1,c=!1,s=new Date;s.setUTCHours(0,0,0,0);var d=function(e){o.css("opacity","0.4").find("input").prop("disabled",!0).filter('[value="'+e+'"]').prop("checked",!0)},l=function(e,t){e>s?d("inactive"):t>s&&(isNaN(e)||e>=s)?d("active"):o.css("opacity","1").find("input").prop("disabled",!1),p(e,t)},p=function(t,a){if("function"==typeof i.datepicker){if(isNaN(t))i.datepicker("option","minDate",null);else{var n=new Date(t);n.setUTCDate(n.getUTCDate()+1),i.datepicker("option","minDate",n)}if(isNaN(a))e.datepicker("option","maxDate",null);else{var o=new Date(a);o.setUTCDate(o.getUTCDate()-1),e.datepicker("option","maxDate",o)}}};e.on("change",(function(e){var i=a(this).val(),o=new Date(i),r=new Date(n);if(n&&i&&o>=r)return a(this).val(""),void(t="");t=i,l(o,r)})).on("focus",(function(t){if(!r){var a=new Date(s);a.setUTCDate(a.getUTCDate()+1),e.datepicker("option","minDate",a),r=!0}})),i.on("change",(function(e){var i=a(this).val(),o=new Date(i),r=new Date(t);return t&&i&&o<=r||o<=s?(a(this).val(""),void(n="")):(n=i,void l(r,o))})).on("focus",(function(e){if(!c){var t=new Date(s);t.setUTCDate(t.getUTCDate()+1),i.datepicker("option","minDate",t),c=!0}})).trigger("change")}))})();

View File

@@ -0,0 +1,563 @@
<?php
/**
* Redirection categories.
*
* @since 3.0.11
* @package RankMathPro
* @subpackage RankMathPro\Redirections
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Redirections;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use MyThemeShop\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* Categories class.
*/
class Categories {
use Hooker;
/**
* Save categories with a delay so that we know the new redirection ID.
*
* @var array
*/
private $save_categories = [];
/**
* Import categories from the Redirection plugin.
*
* @var array
*/
private $import_categories = [];
/**
* Whether categories have been imported from the Redirection plugin.
*
* @var boolean
*/
private $categories_imported = false;
/**
* Register hooks.
*/
public function __construct() {
// Redirection categories.
$this->action( 'init', 'register_categories', 20 );
$this->action( 'cmb2_admin_init', 'cmb_init', 99 );
$this->action( 'admin_post_rank_math_save_redirections', 'save_category', 8 );
$this->action( 'wp_loaded', 'filter_category', 5 );
$this->action( 'rank_math/redirection/extra_tablenav', 'category_filter', 20, 1 );
$this->action( 'rank_math/redirection/get_redirections_query', 'get_redirections_query', 20, 2 );
$this->action( 'rank_math/redirection/after_import', 'import_redirection_categories', 20, 2 );
$this->action( 'rank_math_redirection_category_add_form', 'back_to_redirections_link', 20 );
$this->filter( 'rank_math/redirection/bulk_actions', 'bulk_actions', 20, 1 );
$this->filter( 'wp_loaded', 'handle_bulk_actions', 8, 3 );
$this->filter( 'rank_math/redirection/admin_columns', 'add_category_column', 20, 1 );
$this->filter( 'rank_math/redirection/admin_column_category', 'category_column_content', 20, 2 );
$this->filter( 'parent_file', 'fix_categories_parent_menu', 20, 1 );
$this->filter( 'submenu_file', 'fix_categories_sub_menu', 20, 2 );
$this->filter( 'rank_math/redirections/page_title_actions', 'page_title_actions', 20, 1 );
}
/**
* Register redirection categories taxonomy.
*
* @return void
*/
public function register_categories() {
new CSV_Import_Export_Redirections\CSV_Import_Export_Redirections();
$tax_labels = [
'name' => _x( 'Redirection Categories', 'taxonomy general name', 'rank-math-pro' ),
'singular_name' => _x( 'Redirection Category', 'taxonomy singular name', 'rank-math-pro' ),
'search_items' => __( 'Search Redirection Categories', 'rank-math-pro' ),
'all_items' => __( 'All Redirection Categories', 'rank-math-pro' ),
'parent_item' => __( 'Parent Category', 'rank-math-pro' ),
'parent_item_colon' => __( 'Parent Category:', 'rank-math-pro' ),
'edit_item' => __( 'Edit Category', 'rank-math-pro' ),
'update_item' => __( 'Update Category', 'rank-math-pro' ),
'add_new_item' => __( 'Add New Category', 'rank-math-pro' ),
'new_item_name' => __( 'New Category Name', 'rank-math-pro' ),
'menu_name' => __( 'Redirection Categories', 'rank-math-pro' ),
];
$tax_args = [
'labels' => $tax_labels,
'public' => false,
'rewrite' => false,
'show_ui' => true,
'show_admin_column' => false,
'query_var' => false,
'hierarchical' => true,
'capabilities' => [
'manage_terms' => 'rank_math_redirections',
'edit_terms' => 'rank_math_redirections',
'delete_terms' => 'rank_math_redirections',
'assign_terms' => 'rank_math_redirections',
],
];
register_taxonomy( 'rank_math_redirection_category', 'rank_math_redirection', $tax_args );
}
/**
* Add bulk actions for Redirections screen.
*
* @param array $actions Original actions.
* @return array
*/
public function bulk_actions( $actions ) {
if ( Param::get( 'status' ) === 'trashed' ) {
return $actions;
}
$actions['bulk_add_redirection_category'] = __( 'Add to Category', 'rank-math-pro' );
return $actions;
}
/**
* Handle new bulk actions.
*
* @return void
*/
public function handle_bulk_actions() {
if ( ! is_admin() ) {
return;
}
if ( ! isset( $_POST['action'] ) || $_POST['action'] !== 'bulk_add_redirection_category' || ! Helper::has_cap( 'redirections' ) ) {
return;
}
check_admin_referer( 'bulk-redirections' );
$category_filter = ! empty( $_POST['redirection_category_filter_top'] ) ? $_POST['redirection_category_filter_top'] : $_POST['redirection_category_filter_bottom'];
if ( empty( $category_filter ) ) {
return;
}
$ids = (array) wp_parse_id_list( $_REQUEST['redirection'] );
if ( empty( $ids ) ) {
Helper::add_notification( __( 'No valid ID provided.', 'rank-math-pro' ) );
return;
}
foreach ( $ids as $id ) {
wp_set_object_terms( $id, absint( $category_filter ), 'rank_math_redirection_category', apply_filters( 'rank_math_pro/redirection/bulk_append_categories', true ) );
}
// Translators: placeholder is the number of updated redirections.
Helper::add_notification( sprintf( __( '%d redirections have been assigned to the category.', 'rank-math-pro' ), count( $ids ) ) );
}
/**
* Hook CMB2 init process.
*/
public function cmb_init() {
$this->action( 'cmb2_init_hookup_rank-math-redirections', 'add_category_cmb_field', 110 );
}
/**
* Add new fields to CMB form.
*
* @param object $cmb CMB object.
* @return void
*/
public function add_category_cmb_field( $cmb ) {
$field_ids = wp_list_pluck( $cmb->prop( 'fields' ), 'id' );
$field_position = array_search( 'header_code', array_keys( $field_ids ), true ) + 1;
$add_button = '<span class="button button-secondary add-redirection-category-button">' . esc_html__( 'Add New', 'rank-math-pro' ) . '</span>';
$add_input = '<input type="text" class="add-redirection-category-input exclude" placeholder="' . esc_attr__( 'New Category', 'rank-math-pro' ) . '" value="">';
$manage_link = '<a href="' . admin_url( 'edit-tags.php?taxonomy=rank_math_redirection_category' ) . '" class="manage-redirection-categories-link">' . esc_html__( 'Manage Categories', 'rank-math-pro' ) . '</a>';
$terms = get_terms(
[
'taxonomy' => 'rank_math_redirection_category',
'hide_empty' => false,
]
);
$default_options = [];
if ( $this->is_editing() ) {
$default_options = $this->get_redirection_categories( Param::get( 'redirection' ), [ 'fields' => 'ids' ] );
}
$ids = wp_list_pluck( $terms, 'term_id' );
$names = wp_list_pluck( $terms, 'name' );
$multicheck_options = array_combine( $ids, $names );
$cmb->add_field(
[
'id' => 'redirection_category',
'type' => 'multicheck_inline',
'name' => esc_html__( 'Redirection Category', 'rank-math-pro' ),
'desc' => esc_html__( 'Organize your redirections in categories.', 'rank-math-pro' ),
'options' => $multicheck_options,
'select_all_button' => false,
'after_field' => $add_input . $add_button . $manage_link,
'default' => $default_options,
],
++$field_position
);
$cmb->add_field(
[
'id' => 'set_redirection_category',
'type' => 'hidden',
'default' => '1',
],
++$field_position
);
}
/**
* Select correct parent item when we are editing the Redirection Categories.
*
* @param string $parent_file Original parent file.
* @return string
*/
public function fix_categories_parent_menu( $parent_file ) {
global $pagenow;
if ( in_array( $pagenow, [ 'edit-tags.php', 'term.php' ], true ) && Param::get( 'taxonomy' ) === 'rank_math_redirection_category' ) {
$parent_file = 'rank-math';
}
return $parent_file;
}
/**
* Select correct submenu item when we are editing the Redirection Categories.
*
* @param string $submenu_file Original submenu file.
* @param string $parent_file Selected parent file.
* @return string
*/
public function fix_categories_sub_menu( $submenu_file, $parent_file ) {
global $pagenow;
if ( in_array( $pagenow, [ 'edit-tags.php', 'term.php' ], true ) && Param::get( 'taxonomy' ) === 'rank_math_redirection_category' ) {
$submenu_file = 'rank-math-redirections';
}
return $submenu_file;
}
/**
* Add "Category" column for Redirections screen.
*
* @param array $columns Original columns.
* @return array
*/
public function add_category_column( $columns ) {
$columns['category'] = __( 'Category', 'rank-math-pro' );
return $columns;
}
/**
* Add content in the new "Category" column fields.
*
* @param bool $false False.
* @param array $item Item data.
* @return string
*/
public function category_column_content( $false, $item ) {
$format = '<span class="%1$s">%2$s</span>';
$categories = $this->get_redirection_categories( $item['id'] );
$classes = '';
$cats = '';
$count = 0;
foreach ( $categories as $category ) {
$count++;
if ( $count > 10 ) {
$cats .= '...';
break;
}
$url = Helper::get_admin_url( 'redirections', [ 'redirection_category' => $category->term_id ] );
$cats .= '<a href="' . $url . '">' . $category->name . '</a>, ';
}
$cats = rtrim( $cats, ', ' );
if ( empty( $cats ) ) {
$cats = __( 'Uncategorized', 'rank-math-pro' );
$classes .= ' uncategorized';
}
return sprintf( $format, $classes, $cats );
}
/**
* Get categories for a redirection.
*
* @param int $redirection_id Redirection ID.
* @param array $args Array of query string of term query parameters.
* @return array
*/
public function get_redirection_categories( $redirection_id, $args = [] ) {
return wp_get_object_terms( $redirection_id, 'rank_math_redirection_category', $args );
}
/**
* Redirect to filtered URL when the category filter dropdown is used.
*
* @return void
*/
public function filter_category() {
if ( ! is_admin() ) {
return;
}
if ( ! isset( $_POST['rank_math_filter_redirections_top'] ) && ! isset( $_POST['rank_math_filter_redirections_bottom'] ) ) {
return;
}
if ( ! Helper::has_cap( 'redirections' ) ) {
return;
}
$category_filter = isset( $_POST['rank_math_filter_redirections_top'] ) ? $_POST['redirection_category_filter_top'] : $_POST['redirection_category_filter_bottom'];
if ( ! $category_filter || 'none' === $category_filter ) {
wp_safe_redirect( Helper::get_admin_url( 'redirections' ) );
exit;
}
wp_safe_redirect( Helper::get_admin_url( 'redirections', [ 'redirection_category' => $category_filter ] ) );
exit;
}
/**
* Save category when creating or editing a redirection.
*
* @return boolean
*/
public function save_category() {
// If no form submission, bail!
if ( empty( $_POST ) ) {
return false;
}
if ( ! isset( $_POST['set_redirection_category'] ) ) {
return false;
}
check_admin_referer( 'rank-math-save-redirections', 'security' );
if ( ! Helper::has_cap( 'redirections' ) ) {
return false;
}
$cmb = cmb2_get_metabox( 'rank-math-redirections' );
$values = $cmb->get_sanitized_values( $_POST );
$values['redirection_category'] = isset( $values['redirection_category'] ) && is_array( $values['redirection_category'] ) ? $values['redirection_category'] : [];
unset( $_POST['redirection_category'], $_POST['set_redirection_category'] );
if ( empty( $values['id'] ) ) {
$this->save_categories = $values['redirection_category'];
$this->action( 'rank_math/redirection/saved', 'save_category_after_add' );
return true;
}
wp_set_object_terms( $values['id'], array_map( 'absint', $values['redirection_category'] ), 'rank_math_redirection_category' );
return true;
}
/**
* Save categories with a delay so that we know the new redirection ID.
*
* @param object $redirection Redirection object passed to the hook.
* @return bool
*/
public function save_category_after_add( $redirection ) {
wp_set_object_terms( $redirection->get_id(), array_map( 'absint', $this->save_categories ), 'rank_math_redirection_category' );
return true;
}
/**
* Output category filter dropdown and the submit button for it in the tablenav area.
*
* @param string $which "top" or "bottom".
* @return void
*/
public function category_filter( $which ) {
if ( $this->is_trashed_page() ) {
return;
}
$dropdown_args = [
'taxonomy' => 'rank_math_redirection_category',
'show_option_all' => false,
'show_option_none' => __( 'Select Category', 'rank-math-pro' ),
'option_none_value' => 'none',
'echo' => false,
'hierarchical' => true,
'name' => 'redirection_category_filter_' . $which,
'id' => 'redirection-category-filter-' . $which,
'selected' => Param::get( 'redirection_category', '' ),
'class' => 'redirection-category-filter',
'hide_empty' => false,
];
$submit_args = [
__( 'Filter', 'rank-math-pro' ), // text.
'secondary category-filter-submit', // type.
'rank_math_filter_redirections_' . $which, // name.
false, // wrap.
];
$clear_label = __( 'Clear Filter', 'rank-math-pro' );
$clear_url = Helper::get_admin_url( 'redirections' );
$clear_classes = 'clear-redirection-category-filter';
$clear_classes .= $dropdown_args['selected'] ? '' : ' hidden';
$clear_button = '<a href="' . $clear_url . '" class="' . esc_attr( $clear_classes ) . '" title="' . esc_attr( $clear_label ) . '"><span class="dashicons dashicons-dismiss"></span> ' . $clear_label . '</a>';
$categories = rtrim( wp_dropdown_categories( $dropdown_args ) );
$submit_button = call_user_func_array( 'get_submit_button', $submit_args );
echo sprintf( '%1$s%2$s%3$s', $categories, $submit_button, $clear_button );
}
/**
* Extend get_redirections query to filter by category.
*
* @param object $table Table object.
* @param array $args Get redirections function args.
* @return void
*/
public function get_redirections_query( $table, $args ) {
$categories = Param::get( 'redirection_category' );
if ( ! $categories ) {
return;
}
$categories = array_map( 'absint', (array) $categories );
global $wpdb;
$table->leftJoin( $wpdb->term_relationships, $wpdb->prefix . 'rank_math_redirections.id', $wpdb->term_relationships . '.object_id' );
$table->leftJoin( $wpdb->term_taxonomy, $wpdb->term_taxonomy . '.term_taxonomy_id', $wpdb->term_relationships . '.term_taxonomy_id' );
$table->whereIn( $wpdb->term_taxonomy . '.term_id', $categories );
}
/**
* Import category from the Redirection plugin after importing the redirection itself.
*
* @param int $redirection_id Redirection ID of the imported redirection.
* @param array $source_data_row Data related to the redirection in the original table.
* @return void
*/
public function import_redirection_categories( $redirection_id, $source_data_row ) {
if ( ! isset( $this->categories_imported ) ) {
$this->import_categories_from_redirection_plugin();
}
if ( ! $source_data_row->group_id || ! isset( $this->import_categories[ $source_data_row->group_id ] ) ) {
return;
}
wp_set_object_terms( $redirection_id, $this->import_categories[ $source_data_row->group_id ], 'rank_math_redirection_category' );
}
/**
* Import "groups" from Redirection plugin as redirection categories.
*
* @return bool
*/
public function import_categories_from_redirection_plugin() {
global $wpdb;
$count = 0;
$rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}redirection_groups" );
$this->import_categories = [];
if ( empty( $rows ) ) {
$this->categories_imported = true;
return false;
}
foreach ( (array) $rows as $row ) {
$insert = wp_insert_term( $row->name, 'rank_math_redirection_category' );
if ( ! is_array( $insert ) || ! isset( $insert['term_id'] ) ) {
continue;
}
$this->import_categories[ $row->id ] = $insert['term_id'];
}
$this->categories_imported = true;
return true;
}
/**
* Show link to go back to the Redirections from the Redirections Categories screen.
*
* @param string $taxonomy Current taxonomy.
*
* @return void
*/
public function back_to_redirections_link( $taxonomy ) {
$link = Helper::get_admin_url( 'redirections' );
echo '<p><a href="' . esc_url( $link ) . '">' . esc_html__( '&larr; Go Back to the Redirections', 'rank-math-pro' ) . '</a></p>';
}
/**
* Is editing a record.
*
* @return int|boolean
*/
public function is_editing() {
if ( 'edit' !== Param::get( 'action' ) ) {
return false;
}
return Param::get( 'redirection', false, FILTER_VALIDATE_INT );
}
/**
* Checks if page status is set to trashed.
*
* @return bool
*/
protected function is_trashed_page() {
return 'trashed' === Param::get( 'status' );
}
/**
* Add page title action for categories.
*
* @param array $actions Original actions.
* @return array
*/
public function page_title_actions( $actions ) {
// Move Settings button to the end.
$tmp_settings = false;
if ( isset( $actions['settings'] ) ) {
$tmp_settings = $actions['settings'];
unset( $actions['settings'] );
}
$actions['manage_categories'] = [
'class' => 'page-title-action',
'href' => admin_url( 'edit-tags.php?taxonomy=rank_math_redirection_category' ),
'label' => __( 'Manage Categories', 'rank-math-pro' ),
];
if ( $tmp_settings ) {
$actions['settings'] = $tmp_settings;
}
return $actions;
}
}

View File

@@ -0,0 +1,380 @@
<?php
/**
* The Redirections Module.
*
* @since 2.10
* @package RankMathPro
* @subpackage RankMathPro\Redirections
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Redirections;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Redirections\DB;
use MyThemeShop\Helpers\Param;
use RankMath\Admin\Admin_Helper;
use RankMath\Redirections\Redirection;
defined( 'ABSPATH' ) || exit;
/**
* Redirections class.
*/
class Redirections {
use Hooker;
/**
* Register hooks.
*/
public function __construct() {
$this->action( 'admin_enqueue_scripts', 'admin_scripts', 20 );
// Sync to .htaccess.
$this->action( 'rank_math/redirections/export_tab_content', 'add_export_tab_content', 25 );
$this->action( 'admin_init', 'maybe_sync_htaccess', 120 );
// Auto-delete auto-redirections.
if ( Helper::get_settings( 'general.redirections_post_redirect' ) ) {
$this->action( 'rank_math/redirection/post_updated', 'mark_redirected_post', 20, 2 );
$this->action( 'rank_math/redirection/term_updated', 'mark_redirected_term', 20, 2 );
$this->action( 'pre_delete_term', 'delete_auto_term_redirects', 20 );
$this->action( 'before_delete_post', 'delete_auto_post_redirects', 20, 1 );
}
// Support query parameters.
$this->filter( 'rank_math/redirection/redirection_match', 'match_query_parameters', 10, 3 );
new Categories();
new Schedule();
}
/**
* Output extra content for the Export tab.
*
* @return void
*/
public function add_export_tab_content() {
?>
<div class="rank-math-redirections-csv-export">
<h4><?php esc_html_e( 'Sync to .htaccess', 'rank-math-pro' ); ?></h4>
<p class="description">
<?php esc_html_e( 'Copy all active redirections to the .htaccess file.', 'rank-math-pro' ); ?>
</p>
<div class="csv-export-footer">
<?php wp_nonce_field( 'rank_math_pro_htaccess_sync_redirections', '_wpnonce_htaccess', true, true ); ?>
<button type="submit" class="button button-secondary" id="export-redirections-csv" name="rank-math-redirections-export" value="htaccess-sync"><?php esc_html_e( 'Sync to .htaccess', 'rank-math-pro' ); ?></button>
<span class="input-loading"></span>
</div>
</div>
<?php
}
/**
* Start export if requested and allowed.
*
* @return void
*/
public function maybe_sync_htaccess() {
if ( ! is_admin() || Param::post( 'rank-math-redirections-export' ) !== 'htaccess-sync' ) {
return;
}
if ( ! wp_verify_nonce( Param::post( '_wpnonce_htaccess' ), 'rank_math_pro_htaccess_sync_redirections' ) ) {
wp_die( esc_html__( 'Invalid nonce.', 'rank-math-pro' ) );
}
if ( ! current_user_can( 'export' ) || ! current_user_can( 'rank_math_redirections' ) || ! Helper::has_cap( 'edit_htaccess' ) ) {
wp_die( esc_html__( 'Sorry, you are not allowed to export redirections on this site.', 'rank-math-pro' ) );
}
$htaccess = Admin_Helper::get_htaccess_data();
if ( false === $htaccess || ! $htaccess['writable'] || ! Helper::is_filesystem_direct() ) {
wp_die( esc_html__( 'The redirections could not be synced because the .htaccess file does not exist or it is not writable.', 'rank-math-pro' ) );
}
$rules = $this->get_htaccess_rules();
if ( ! $rules ) {
Helper::add_notification( __( 'No valid redirection found.', 'rank-math-pro' ) );
return;
}
// Remove existing block.
$filtered_htaccess_content = trim( preg_replace( '/\# Created by Rank Math[\s\S]+?# Rank Math Redirections END/si', '', $htaccess['content'] ) );
$this->write_htaccess( $filtered_htaccess_content . PHP_EOL . $rules );
Helper::add_notification( __( 'Redirections successfully synced to the .htaccess file.', 'rank-math-pro' ) );
}
/**
* Function to get htaccess rules.
*
* @return string
*/
public function get_htaccess_rules() {
$items = DB::get_redirections(
[
'limit' => 1000,
'status' => 'active',
]
);
$text[] = '# Created by Rank Math';
$text[] = '# ' . date_i18n( 'r' );
$text[] = '# Rank Math ' . trim( rank_math()->version ) . ' - https://rankmath.com/';
$text[] = '';
$text = array_merge( $text, $this->apache( $items['redirections'] ) );
$text[] = '';
$text[] = '# Rank Math Redirections END';
return implode( PHP_EOL, $text ) . PHP_EOL;
}
/**
* Apache rewrite rules.
*
* @param array $items Array of DB items.
*
* @return string
*/
private function apache( $items ) {
$output[] = '<IfModule mod_rewrite.c>';
foreach ( $items as $item ) {
$this->apache_item( $item, $output );
}
$output[] = '</IfModule>';
return $output;
}
/**
* Format Apache single item.
*
* @param array $item Single item.
* @param array $output Output array.
*/
private function apache_item( $item, &$output ) {
$target = '410' === $item['header_code'] ? '- [G]' : sprintf( '%s [R=%d,L]', $this->encode2nd( $item['url_to'] ), $item['header_code'] );
$sources = maybe_unserialize( $item['sources'] );
foreach ( $sources as $from ) {
$url = $from['pattern'];
if ( 'regex' !== $from['comparison'] && strpos( $url, '?' ) !== false || strpos( $url, '&' ) !== false ) {
$url_parts = parse_url( $url );
$url = $url_parts['path'];
$output[] = sprintf( 'RewriteCond %%{QUERY_STRING} ^%s$', preg_quote( $url_parts['query'] ) );
}
// Get rewrite string.
$output[] = sprintf( '%sRewriteRule %s %s', ( $this->is_valid_regex( $from ) ? '' : '# ' ), $this->get_comparison( $url, $from ), $target );
}
}
/**
* Encode URL.
*
* @param string $url URL to encode.
*
* @return string
*/
private function encode2nd( $url ) {
$url = urlencode( $url );
$url = str_replace( '%2F', '/', $url );
$url = str_replace( '%3F', '?', $url );
$url = str_replace( '%3A', ':', $url );
$url = str_replace( '%3D', '=', $url );
$url = str_replace( '%26', '&', $url );
$url = str_replace( '%25', '%', $url );
$url = str_replace( '+', '%20', $url );
$url = str_replace( '%24', '$', $url );
return $url;
}
/**
* Check if it's a valid pattern.
*
* So we don't break the site when it's inserted in the .htaccess.
*
* @param array $source Source array.
*
* @return string
*/
private function is_valid_regex( $source ) {
if ( 'regex' == $source['comparison'] && @preg_match( $source['pattern'], null ) === false ) { // phpcs:ignore
return false;
}
return true;
}
/**
* Encode regex.
*
* @param string $url URL to encode.
*
* @return string
*/
private function encode_regex( $url ) {
$url = preg_replace( '/[^a-zA-Z0-9\s](.*)[^a-zA-Z0-9\s][imsxeADSUXJu]*/', '$1', $url ); // Strip delimiters.
$url = preg_replace( "/[\r\n\t].*?$/s", '', $url ); // Remove newlines.
$url = preg_replace( '/[^\PC\s]/u', '', $url ); // Remove any invalid characters.
$url = str_replace( ' ', '%20', $url ); // Make sure spaces are quoted.
$url = str_replace( '%24', '$', $url );
$url = ltrim( $url, '/' ); // No leading slash.
$url = preg_replace( '@^\^/@', '^', $url ); // If pattern has a ^ at the start then ensure we don't have a slash immediately.
return $url;
}
/**
* Get comparison pattern.
*
* @param string $url URL for comparison.
* @param array $from Comparison type and URL.
*
* @return string
*/
private function get_comparison( $url, $from ) {
$comparison = $from['comparison'];
if ( 'regex' === $comparison ) {
return $this->encode_regex( $from['pattern'] );
}
$hash = [
'exact' => '^{url}/?$',
'contains' => '^(.*){url}(.*)$',
'start' => '^{url}',
'end' => '{url}/?$',
];
$url = preg_quote( $url );
return isset( $hash[ $comparison ] ) ? str_replace( '{url}', $url, $hash[ $comparison ] ) : $url;
}
/**
* Update htaccess file.
*
* @param string $content Htaccess content.
* @return string|bool
*/
private function write_htaccess( $content ) {
if ( empty( $content ) ) {
return false;
}
global $wp_filesystem;
$htaccess_file = get_home_path() . '.htaccess';
return $wp_filesystem->put_contents( $htaccess_file, $content );
}
/**
* Store auto redirection for post.
*
* @param int $redirection_id Redirection ID.
* @param int $post_id Post ID.
* @return void
*/
public function mark_redirected_post( $redirection_id, $post_id ) {
$redirects = get_post_meta( $post_id, 'rank_math_auto_redirect', true );
if ( empty( $redirects ) ) {
$redirects = [];
}
if ( ! in_array( $redirection_id, $redirects, true ) ) {
$redirects[] = $redirection_id;
}
update_post_meta( $post_id, 'rank_math_auto_redirect', $redirects );
}
/**
* Store auto redirection for term.
*
* @param int $redirection_id Redirection ID.
* @param int $term_id Term ID.
* @return void
*/
public function mark_redirected_term( $redirection_id, $term_id ) {
$redirects = get_term_meta( $term_id, 'rank_math_auto_redirect', true );
if ( empty( $redirects ) ) {
$redirects = [];
}
if ( ! in_array( $redirection_id, $redirects, true ) ) {
$redirects[] = $redirection_id;
}
update_term_meta( $term_id, 'rank_math_auto_redirect', $redirects );
}
/**
* Delete auto-created post redirects.
*
* @param int $post_id Post ID.
* @return void
*/
public function delete_auto_post_redirects( $post_id ) {
$redirects = get_post_meta( $post_id, 'rank_math_auto_redirect', true );
if ( empty( $redirects ) ) {
return;
}
DB::delete( $redirects );
}
/**
* Delete auto-created term redirects.
*
* @param int $term_id Term ID.
* @return void
*/
public function delete_auto_term_redirects( $term_id ) {
$redirects = get_term_meta( $term_id, 'rank_math_auto_redirect', true );
if ( empty( $redirects ) ) {
return;
}
DB::delete( $redirects );
}
/**
* Enqueue styles and scripts for Redirections & Redirection Categories screens.
*
* @param string $hook Page hook prefix.
*
* @return void
*/
public function admin_scripts( $hook ) {
$screen = get_current_screen();
if ( ! in_array( $screen->id, [ 'rank-math_page_rank-math-redirections', 'edit-rank_math_redirection_category' ], true ) ) {
return;
}
$url = RANK_MATH_PRO_URL . 'includes/modules/redirections/assets/';
Helper::add_json( 'add_redirection_category_nonce', wp_create_nonce( 'add-rank_math_redirection_category' ) );
wp_enqueue_style( 'rank-math-pro-redirections', $url . 'css/redirections.css', [], RANK_MATH_PRO_VERSION );
wp_enqueue_script( 'rank-math-pro-redirections', $url . 'js/redirections.js', [], RANK_MATH_PRO_VERSION, true );
}
/**
* Add support for query parameters in redirections.
*
* @param bool $match Whether the redirection matches the current URL.
* @param array $redirection The redirection data.
*/
public function match_query_parameters( $match, $redirection ) {
if ( empty( $redirection ) || $match ) {
return $match;
}
$full_uri = Redirection::get_full_uri();
return DB::compare_sources( $redirection['sources'], $full_uri );
}
}

View File

@@ -0,0 +1,396 @@
<?php
/**
* Scheduled activation and deactivation.
*
* @since 3.0.11
* @package RankMathPro
* @subpackage RankMathPro\Redirections
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Redirections;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Redirections\DB;
use MyThemeShop\Helpers\Param;
use MyThemeShop\Helpers\WordPress;
defined( 'ABSPATH' ) || exit;
/**
* Schedule class.
*/
class Schedule {
use Hooker;
/**
* Save start and end date.
*
* @var array
*/
private $save_start_end = [];
/**
* Register hooks.
*/
public function __construct() {
$this->action( 'init', 'disallow_scheduled_bulk_status_change', 5 );
$this->action( 'cmb2_admin_init', 'cmb_init', 99 );
$this->action( 'admin_post_rank_math_save_redirections', 'save_start_end', 9 );
$this->action( 'rank_math/redirections/scheduled_activate', 'scheduled_activation_event', 10, 1 );
$this->action( 'rank_math/redirections/scheduled_deactivate', 'scheduled_deactivation_event', 10, 1 );
$this->action( 'rank_math/redirection/deleted', 'delete_scheduled_event', 10, 1 );
$this->filter( 'rank_math/redirection/row_classes', 'row_classes', 10, 2 );
}
/**
* Hook CMB2 init process.
*/
public function cmb_init() {
$this->action( 'cmb2_init_hookup_rank-math-redirections', 'add_start_end_cmb_fields', 120 );
}
/**
* Add new fields to CMB form.
*
* @param object $cmb CMB object.
* @return void
*/
public function add_start_end_cmb_fields( $cmb ) {
$field_ids = wp_list_pluck( $cmb->prop( 'fields' ), 'id' );
$field_position = array_search( 'status', array_keys( $field_ids ), true ) + 1;
$current_redirection = Param::get( 'redirection' );
$cmb->add_field(
[
'id' => 'start_date',
'type' => 'text_date',
'name' => esc_html__( 'Scheduled Activation', 'rank-math-pro' ),
'desc' => esc_html__( 'Redirection will be activated on this date (optional).', 'rank-math-pro' ),
'date_format' => 'Y-m-d',
'default' => $this->get_start_date( $current_redirection ),
'attributes' => [
'placeholder' => $this->get_past_date( $current_redirection, 'start' ),
'class' => 'cmb2-text-small cmb2-datepicker exclude',
'autocomplete' => 'off',
],
],
++$field_position
);
$cmb->add_field(
[
'id' => 'end_date',
'type' => 'text_date',
'name' => esc_html__( 'Scheduled Deactivation', 'rank-math-pro' ),
'desc' => esc_html__( 'Redirection will be deactivated on this date (optional).', 'rank-math-pro' ),
'date_format' => 'Y-m-d',
'default' => $this->get_end_date( $current_redirection ),
'attributes' => [
'placeholder' => $this->get_past_date( $current_redirection, 'end' ),
'class' => 'cmb2-text-small cmb2-datepicker exclude',
'autocomplete' => 'off',
],
],
++$field_position
);
}
/**
* Save start and end date.
*/
public function save_start_end() {
// If no form submission, bail!
if ( empty( $_POST ) ) {
return false;
}
if ( ! isset( $_POST['start_date'] ) || ! isset( $_POST['end_date'] ) ) {
return false;
}
check_admin_referer( 'rank-math-save-redirections', 'security' );
if ( ! Helper::has_cap( 'redirections' ) ) {
return false;
}
$cmb = cmb2_get_metabox( 'rank-math-redirections' );
$values = $cmb->get_sanitized_values( $_POST );
$this->save_start_end = [
'start_date' => isset( $values['start_date'] ) ? $values['start_date'] : '',
'end_date' => isset( $values['end_date'] ) ? $values['end_date'] : '',
];
unset( $_POST['start_date'], $_POST['end_date'] );
if ( empty( $values['id'] ) ) {
$this->action( 'rank_math/redirection/saved', 'save_start_end_after_add' );
return true;
}
$this->save_start_end_dates( $values['id'] );
}
/**
* Clear all scheduled activations for a redirection.
*
* @param int $redirection_id Redirection ID.
* @return void
*/
public function clear_scheduled_activation( $redirection_id ) {
as_unschedule_all_actions( 'rank_math/redirections/scheduled_activate', [ (int) $redirection_id ], 'rank-math' );
}
/**
* Schedule activation for a redirection for the given date.
*
* @param int $redirection_id Redirection ID.
* @param string $start_date Date to activate.
* @return void
*/
public function schedule_activation( $redirection_id, $start_date ) {
as_schedule_single_action( $start_date, 'rank_math/redirections/scheduled_activate', [ (int) $redirection_id ], 'rank-math' );
}
/**
* Clear all scheduled deactivations for a redirection.
*
* @param int $redirection_id Redirection ID.
* @return void
*/
public function clear_scheduled_deactivation( $redirection_id ) {
as_unschedule_all_actions( 'rank_math/redirections/scheduled_deactivate', [ (int) $redirection_id ], 'rank-math' );
}
/**
* Schedule deactivation for a redirection for the given date.
*
* @param int $redirection_id Redirection ID.
* @param string $end_date Date to deactivate.
* @return void
*/
public function schedule_deactivation( $redirection_id, $end_date ) {
as_schedule_single_action( $end_date, 'rank_math/redirections/scheduled_deactivate', [ (int) $redirection_id ], 'rank-math' );
}
/**
* Scheduled event callback to activate a redirection.
*
* @param int $redirection_id Redirection ID.
* @return void
*/
public function scheduled_activation_event( $redirection_id ) {
DB::change_status( [ $redirection_id ], 'active' );
}
/**
* Scheduled event callback to deactivate a redirection.
*
* @param int $redirection_id Redirection ID.
* @return void
*/
public function scheduled_deactivation_event( $redirection_id ) {
DB::change_status( [ $redirection_id ], 'inactive' );
}
/**
* Get scheduled activation date for a redirection.
*
* @param int $redirection_id Redirection ID.
* @return string
*/
public function get_start_date( $redirection_id ) {
if ( ! $redirection_id ) {
return '';
}
$timestamp = as_next_scheduled_action( 'rank_math/redirections/scheduled_activate', [ (int) $redirection_id ], 'rank-math' );
if ( ! $timestamp ) {
return '';
}
return gmdate( 'Y-m-d', $timestamp );
}
/**
* Get scheduled deactivation date for a redirection.
*
* @param int $redirection_id Redirection ID.
* @return string
*/
public function get_end_date( $redirection_id ) {
if ( ! $redirection_id ) {
return '';
}
$timestamp = as_next_scheduled_action( 'rank_math/redirections/scheduled_deactivate', [ (int) $redirection_id ], 'rank-math' );
if ( ! $timestamp ) {
return '';
}
return gmdate( 'Y-m-d', $timestamp );
}
/**
* Get last completed scheduled activation/deactivation date for a redirection.
*
* @param int $redirection_id Redirection ID.
* @param string $type Type of date ("start" or "end").
* @return string
*/
public function get_past_date( $redirection_id, $type = 'start' ) {
$hook = 'scheduled_activate';
if ( 'end' === $type ) {
$hook = 'scheduled_deactivate';
}
$args = [
'hook' => "rank_math/redirections/$hook",
'args' => [ (int) $redirection_id ],
'status' => \ActionScheduler_Store::STATUS_COMPLETE,
'per_page' => 1,
'orderby' => 'action_id',
'order' => 'DESC',
];
$actions = as_get_scheduled_actions( $args );
if ( empty( $actions ) ) {
return '';
}
return gmdate( 'Y-m-d', reset( $actions )->get_schedule()->get_date()->getTimestamp() );
}
/**
* Save scheduled start/end dates for newly created redirections.
*
* @param object $redirection Redirection object passed to the hook.
* @return bool
*/
public function save_start_end_after_add( $redirection ) {
$this->save_start_end_dates( $redirection->get_id() );
return true;
}
/**
* Save scheduled start/end dates for a redirection.
* The dates were previously added to the $this->save_start_end array.
*
* @param int $redirection_id Redirection ID.
*/
public function save_start_end_dates( $redirection_id ) {
$start_date = strtotime( $this->save_start_end['start_date'] );
$this->clear_scheduled_activation( $redirection_id );
if ( $start_date ) {
$this->schedule_activation( $redirection_id, $start_date );
}
$end_date = strtotime( $this->save_start_end['end_date'] );
$this->clear_scheduled_deactivation( $redirection_id );
if ( $end_date ) {
$this->schedule_deactivation( $redirection_id, $end_date );
}
// Set active status.
$now = time();
if ( ( $start_date && $start_date > $now ) || ( $end_date && $end_date < $now ) ) {
$_POST['status'] = 'inactive';
} elseif ( $start_date && $start_date <= $now && ( ! $end_date || $end_date > $now ) ) {
$_POST['status'] = 'active';
}
}
/**
* Add status-locked class to the row if the status is locked.
*
* @param array $classes The classes for the row.
* @param array $item The object for the row.
*
* @return array
*/
public function row_classes( $classes, $item ) {
if ( $this->is_status_locked( $item['id'] ) ) {
$classes .= ' rank-math-redirection-status-locked';
}
return $classes;
}
/**
* Check if the status is locked because of a start/end date in the future.
*
* @param int $redirection_id Redirection ID.
*/
private function is_status_locked( $redirection_id ) {
$start = $this->get_start_date( $redirection_id );
$start_date = new \DateTime( $start );
$start_date = $start_date->getTimestamp();
$end = $this->get_end_date( $redirection_id );
$end_date = new \DateTime( $end );
$end_date = $end_date->getTimestamp();
$today = new \DateTime();
$today->setTime( 0, 0, 0 );
$today = $today->getTimestamp();
if ( $start && $start_date > $today ) {
return true;
} elseif ( $end && $end_date > $today && ( ! $start || $start_date >= $today ) ) {
return true;
}
return false;
}
/**
* Disable bulk status change if the selected item has a locked status.
*/
public function disallow_scheduled_bulk_status_change() {
$action = WordPress::get_request_action();
if ( false === $action || empty( $_REQUEST['redirection'] ) || ! in_array( $action, [ 'activate', 'deactivate' ], true ) ) {
return;
}
$ids = (array) wp_parse_id_list( $_REQUEST['redirection'] );
if ( empty( $ids ) ) {
return;
}
$could_not_change = [];
foreach ( $ids as $id ) {
if ( $this->is_status_locked( $id ) ) {
$key = array_search( $id, $_REQUEST['redirection'] );
if ( false !== $key ) {
$could_not_change[] = $id;
unset( $_REQUEST['redirection'][ $key ] );
}
}
}
if ( ! empty( $could_not_change ) ) {
$message = __( 'One or more of the selected redirections could not be changed because they are scheduled for future activation/deactivation.', 'rank-math-pro' );
Helper::add_notification( $message, [ 'type' => 'error' ] );
}
}
/**
* Make sure to delete scheduled actions when a redirection is deleted.
*
* @param array $redirection_ids Redirection IDs.
*/
public function delete_scheduled_event( $redirection_ids ) {
if ( ! is_array( $redirection_ids ) ) {
$redirection_ids = [ $redirection_ids ];
}
foreach ( $redirection_ids as $redirection_id ) {
$this->clear_scheduled_activation( $redirection_id );
$this->clear_scheduled_deactivation( $redirection_id );
}
}
}

View File

@@ -0,0 +1,544 @@
<?php
/**
* The admin-specific functionality of the plugin.
*
* @since 1.0
* @package RankMathPro
* @subpackage RankMathPro\Admin
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Redirections\CSV_Import_Export_Redirections;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use MyThemeShop\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* CSV Import Export Redirections class.
*
* @codeCoverageIgnore
*/
class CSV_Import_Export_Redirections {
use Hooker;
/**
* Register hooks.
*/
public function __construct() {
$this->filter( 'rank_math/redirections/page_title_actions', 'change_export_button_label', 20 );
$this->filter( 'rank_math/redirections/export_tabs', 'add_import_tab', 20 );
$this->action( 'rank_math/redirections/import_tab_content', 'add_import_tab_content', 20 );
$this->action( 'rank_math/redirections/export_tab_content', 'add_export_tab_content', 20 );
$this->action( 'admin_init', 'maybe_do_import', 99 );
$this->action( 'admin_init', 'maybe_do_export', 110 );
$this->action( 'admin_init', 'maybe_cancel_import', 120 );
$this->action( 'wp_ajax_csv_import_redirections_progress', 'csv_import_progress' );
$this->action( 'admin_enqueue_scripts', 'enqueue' );
$this->action( 'admin_head-rank-math_page_rank-math-redirections', 'add_help_tab', 20 );
Import_Background_Process::get();
}
/**
* Add instructions in contextual help.
*
* @return void
*/
public function add_help_tab() {
$screen = get_current_screen();
$content = '<ul class="description"><li>';
// Translators: placeholder is a comma-separated list of columns.
$content .= sprintf( esc_html__( 'Use the following columns in the CSV file (the order does not matter): %s', 'rank-math-pro' ), '<code>id, source, matching, destination, type, category, status, ignore</code>' );
$content .= '</li><li>';
// Translators: placeholders are column names.
$content .= sprintf( esc_html__( 'Only the %1$s and the %2$s columns are required, the others are optional.', 'rank-math-pro' ), '<code>source</code>', '<code>destination</code>' );
$content .= '</li><li>';
// Translators: placeholder 1 is the column name, placeholder 2 is the possible column value ("case").
$content .= sprintf( esc_html__( 'The %1$s column may contain the value %2$s, or nothing.', 'rank-math-pro' ), '<code>ignore</code>', '<code>case</code>' );
$content .= '</li><li>';
// Translators: placeholder is the column name.
$content .= sprintf( esc_html__( 'If the numeric ID is specified in the %s column, then the redirection will be edited. If it is not set or empty, then a new redirection will be created.', 'rank-math-pro' ), '<code>id</code>' );
$content .= '</li><li>';
// Translators: placeholder is the filter name.
$content .= sprintf( esc_html__( 'If an imported redirection differs from an existing redirection (or another imported redirection) only by the source value, then those redirections will be merged into a single redirection with multiple sources. You can change this behavior with the %s filter hook.', 'rank-math-pro' ), '<code>rank_math/admin/csv_import_redirection_update</code>' );
$content .= '</li><li>';
// Translators: 1 is the command name, 2 is the column name.
$content .= sprintf( esc_html__( 'Use %1$s (case-sensitive) as the value for the %2$s column to delete a redirection.', 'rank-math-pro' ), '<code>DELETE</code>', '<code>destination</code>' );
$content .= '</li><li>';
// Translators: placeholder is a link to the KB article.
$content .= sprintf( esc_html__( 'For more information, please see %s.', 'rank-math-pro' ), '<a href="https://rankmath.com/kb/how-to-manage-redirects-via-csv/" target="_blank">' . __( 'our Knowledge Base article', 'rank-math-pro' ) . '</a>' );
$content .= '</li></ul>';
$screen->add_help_tab(
[
'id' => 'csv_import_redirections',
'title' => __( 'CSV Import', 'rank-math-pro' ),
'content' => $content,
]
);
}
/**
* Add Import tab in redirections import-export panel.
*
* @param array $tabs Original tabs.
* @return array
*/
public function add_import_tab( $tabs ) {
$tabs_new = [];
$tabs_new['import'] = [
'name' => __( 'Import', 'rank-math-pro' ),
'icon' => 'rm-icon-import',
'class' => 'active-tab',
];
if ( isset( $tabs['export']['class'] ) ) {
$tabs['export']['class'] = str_replace( 'active-tab', '', $tabs['export']['class'] );
}
return array_merge( $tabs_new, $tabs );
}
/**
* Output contents for the Import tab.
*
* @return void
*/
public function add_import_tab_content() {
$import_in_progress = (bool) get_option( 'rank_math_csv_import_redirections' );
?>
<div class="rank-math-redirections-csv-import">
<?php if ( ! $import_in_progress ) : ?>
<p><label for="csv-import-me"><strong><?php esc_html_e( 'CSV File', 'rank-math-pro' ); ?></label></strong><p>
<input type="file" name="csv-redirections-import-me" id="csv-redirections-import-me" value="" accept=".csv">
<br>
<span class="validation-message"><?php esc_html_e( 'Please select a CSV file to import.', 'rank-math-pro' ); ?></span>
<p class="description">
<?php // Translators: placeholder is a comma-separated list of columns. ?>
<?php printf( esc_html__( 'Import a CSV file to create or update redirections. The file must include at least the following columns: %s', 'rank-math-pro' ), '<code>source, destination</code>' ); ?>
<button type="button" id="rank-math-contextual-help-link" class="button button-small show-settings"><?php esc_html_e( 'More details', 'rank-math-pro' ); ?></button>
</p>
<?php else : ?>
<div id="csv-import-redirections-progress-details">
<?php self::import_progress_details(); ?>
</div>
<?php endif; ?>
<div class="csv-export-footer">
<?php wp_nonce_field( 'rank_math_pro_csv_import_redirections' ); ?>
<input type="hidden" name="object_id" value="csv-import-redirections-plz">
<input type="hidden" name="action" value="wp_handle_upload">
<?php if ( $import_in_progress ) : ?>
<a href="<?php echo esc_url( wp_nonce_url( add_query_arg( [ 'rank_math_cancel_csv_import_redirections' => 1 ] ), 'rank_math_cancel_csv_import_redirections' ) ); ?>" id="csv-import-cancel" class="button button-link-delete csv-import-redirections-cancel"><?php esc_html_e( 'Cancel Import', 'rank-math-pro' ); ?></a>
<?php else : ?>
<button type="submit" class="button button-primary"><?php esc_html_e( 'Import CSV', 'rank-math-pro' ); ?></button>
<?php endif; ?>
</div>
</div>
<?php
}
/**
* Output extra content for the Export tab.
*
* @return void
*/
public function add_export_tab_content() {
?>
<div class="rank-math-redirections-csv-export">
<h4><?php esc_html_e( 'Export CSV', 'rank-math-pro' ); ?></h4>
<input type="checkbox" class="cmb2-option" name="include_deactivated" id="include_deactivated" value="1" checked="checked"> <label for="include_deactivated"><?php esc_html_e( 'Include deactivated redirections', 'rank-math-pro' ); ?></label>
<div class="csv-export-footer">
<?php wp_nonce_field( 'rank_math_pro_csv_export_redirections', '_wpnonce', true, true ); ?>
<button type="submit" class="button button-secondary" id="export-redirections-csv" name="rank-math-redirections-export" value="csv"><?php esc_html_e( 'Export CSV', 'rank-math-pro' ); ?></button>
<span class="input-loading"></span>
</div>
</div>
<?php
}
/**
* Change "Export" button label to "Import & Export".
*
* @param array $buttons Original buttons array.
* @return array
*/
public function change_export_button_label( $buttons ) {
$buttons['import_export']['label'] = __( 'Import & Export', 'rank-math-pro' );
return $buttons;
}
/**
* Check if current screen is Status & Tools > Import / Export.
*
* @return bool
*/
public function is_redirections_screen() {
return is_admin() && ! wp_doing_ajax() && isset( $_GET['page'] ) && 'rank-math-redirections' === $_GET['page']; // phpcs:ignore
}
/**
* Add notice after import is started.
*
* @return void
*/
public function add_notice() {
if ( ! $this->is_redirections_screen() ) {
return;
}
Helper::add_notification( esc_html__( 'CSV import is in progress...', 'rank-math-pro' ), [ 'type' => 'success' ] );
}
/**
* Start export if requested and allowed.
*
* @return void
*/
public function maybe_do_export() {
if ( ! is_admin() || Param::post( 'rank-math-redirections-export' ) !== 'csv' ) {
return;
}
if ( ! wp_verify_nonce( isset( $_REQUEST['_wpnonce'] ) ? $_REQUEST['_wpnonce'] : '', 'rank_math_pro_csv_export_redirections' ) ) {
wp_die( esc_html__( 'Invalid nonce.', 'rank-math-pro' ) );
}
if ( ! current_user_can( 'export' ) || ! current_user_can( 'rank_math_redirections' ) ) {
wp_die( esc_html__( 'Sorry, you are not allowed to export redirections on this site.', 'rank-math-pro' ) );
}
$settings = [
'include_deactivated' => (bool) ! empty( $_POST['include_deactivated'] ),
];
$exporter = new Exporter( $settings );
$exporter->process_export();
}
/**
* Start import if requested and allowed.
*
* @return void
*/
public function maybe_do_import() {
if ( ! is_admin() || empty( $_POST['object_id'] ) || 'csv-import-redirections-plz' !== $_POST['object_id'] ) {
return;
}
if ( empty( $_FILES['csv-redirections-import-me'] ) || empty( $_FILES['csv-redirections-import-me']['name'] ) ) {
wp_die( esc_html__( 'Please select a file to import.', 'rank-math-pro' ) );
}
if ( ! wp_verify_nonce( isset( $_REQUEST['_wpnonce'] ) ? $_REQUEST['_wpnonce'] : '', 'rank_math_pro_csv_import_redirections' ) ) {
wp_die( esc_html__( 'Invalid nonce.', 'rank-math-pro' ) );
}
if ( ! current_user_can( 'import' ) || ! current_user_can( 'rank_math_redirections' ) ) {
wp_die( esc_html__( 'Sorry, you are not allowed to import redirections on this site.', 'rank-math-pro' ) );
}
// Rename file.
$info = pathinfo( $_FILES['csv-redirections-import-me']['name'] );
$_FILES['csv-redirections-import-me']['name'] = uniqid( 'rm-csv-redirections-' ) . ( ! empty( $info['extension'] ) ? '.' . $info['extension'] : '' );
// Handle file.
$this->filter( 'upload_mimes', 'allow_csv_upload' );
$file = wp_handle_upload( $_FILES['csv-redirections-import-me'], [ 'test_form' => false ] );
$this->remove_filter( 'upload_mimes', 'allow_csv_upload', 10 );
if ( ! $this->validate_file( $file ) ) {
return false;
}
$settings = [
'no_overwrite' => ! empty( $_POST['no_overwrite'] ),
];
$importer = new Importer();
$importer->start( $file['file'], $settings );
}
/**
* Allow CSV file upload.
*
* @param array $types Mime types keyed by the file extension regex corresponding to those types.
* @return array
*/
public function allow_csv_upload( $types ) {
$types['csv'] = 'text/csv';
return $types;
}
/**
* Check if uploaded file is valid CSV or not.
*
* @param mixed $file File data array or object.
* @return bool
*/
public function validate_file( $file ) {
if ( is_wp_error( $file ) ) {
Helper::add_notification( esc_html__( 'CSV could not be imported:', 'rank-math-pro' ) . ' ' . $file->get_error_message(), [ 'type' => 'error' ] );
return false;
}
if ( isset( $file['error'] ) ) {
Helper::add_notification( esc_html__( 'CSV could not be imported:', 'rank-math-pro' ) . ' ' . $file['error'], [ 'type' => 'error' ] );
return false;
}
if ( ! isset( $file['file'] ) ) {
Helper::add_notification( esc_html__( 'CSV could not be imported: Upload failed.', 'rank-math-pro' ), [ 'type' => 'error' ] );
return false;
}
if ( ! isset( $file['type'] ) || 'text/csv' !== $file['type'] ) {
\unlink( $file['file'] );
Helper::add_notification( esc_html__( 'CSV could not be imported: File type error.', 'rank-math-pro' ), [ 'type' => 'error' ] );
return false;
}
return true;
}
/**
* Get import/export CSV columns.
*
* @return array
*/
public static function get_columns() {
$columns = [
'id',
'source',
'matching',
'destination',
'type',
'category',
'status',
];
/**
* Filter columns array.
*/
return apply_filters( 'rank_math/admin/csv_export_redirections_columns', $columns );
}
/**
* Get object types.
*
* @return array
*/
public static function get_possible_object_types() {
$object_types = [
'post' => __( 'Posts', 'rank-math-pro' ),
'term' => __( 'Terms', 'rank-math-pro' ),
'user' => __( 'Users', 'rank-math-pro' ),
];
/**
* Filter object types array.
*/
return apply_filters( 'rank_math/admin/csv_export_redirections_object_types', $object_types );
}
/**
* Check if cancel request is valid.
*
* @return void
*/
public static function maybe_cancel_import() {
if ( ! is_admin() || empty( $_GET['rank_math_cancel_csv_import_redirections'] ) ) {
return;
}
if ( ! wp_verify_nonce( isset( $_REQUEST['_wpnonce'] ) ? $_REQUEST['_wpnonce'] : '', 'rank_math_cancel_csv_import_redirections' ) ) {
Helper::add_notification( esc_html__( 'Import could not be canceled: invalid nonce. Please try again.', 'rank-math-pro' ), [ 'type' => 'error' ] );
wp_safe_redirect( remove_query_arg( 'rank_math_cancel_csv_import_redirections' ) );
exit;
}
if ( ! current_user_can( 'import' ) ) {
Helper::add_notification( esc_html__( 'Import could not be canceled: you are not allowed to import content to this site.', 'rank-math-pro' ), [ 'type' => 'error' ] );
wp_safe_redirect( remove_query_arg( 'rank_math_cancel_csv_import_redirections' ) );
exit;
}
self::cancel_import();
}
/**
* Cancel import.
*
* @param bool $silent Import silently.
* @return void
*/
public static function cancel_import( $silent = false ) {
$file_path = get_option( 'rank_math_csv_import_redirections' );
delete_option( 'rank_math_csv_import_redirections' );
delete_option( 'rank_math_csv_import_redirections_total' );
delete_option( 'rank_math_csv_import_redirections_status' );
delete_option( 'rank_math_csv_import_redirections_settings' );
Import_Background_Process::get()->cancel_process();
if ( ! $file_path ) {
if ( ! $silent ) {
Helper::add_notification( esc_html__( 'Import could not be canceled.', 'rank-math-pro' ), [ 'type' => 'error' ] );
}
wp_safe_redirect( remove_query_arg( 'rank_math_cancel_csv_import_redirections' ) );
exit;
}
unlink( $file_path );
if ( ! $silent ) {
Helper::add_notification(
__( 'CSV import canceled.', 'rank-math-pro' ),
[
'type' => 'success',
'classes' => 'is-dismissible',
]
);
}
wp_safe_redirect( remove_query_arg( 'rank_math_cancel_csv_import_redirections' ) );
exit;
}
/**
* Show import progress via AJAX.
*
* @return void
*/
public function csv_import_progress() {
check_ajax_referer( 'rank_math_csv_progress' );
if ( ! current_user_can( 'import' ) ) {
exit( '0' );
}
self::import_progress_details();
exit;
}
/**
* Output import progress details.
*
* @return void
*/
public static function import_progress_details() {
$import_in_progress = (bool) get_option( 'rank_math_csv_import_redirections' );
if ( $import_in_progress ) {
$total_lines = (int) get_option( 'rank_math_csv_import_redirections_total' );
$remaining_items = Import_Background_Process::get()->count_remaining_items();
$progress = $total_lines ? ( $total_lines - $remaining_items + 1 ) / $total_lines * 100 : 0;
?>
<p><?php esc_html_e( 'Import in progress...', 'rank-math-pro' ); ?></p>
<p class="csv-import-redirections-status">
<?php // Translators: placeholders represent count like 15/36. ?>
<?php printf( esc_html__( 'Items processed: %1$s/%2$s', 'rank-math-pro' ), absint( min( $total_lines, $total_lines - $remaining_items + 1 ) ), absint( $total_lines ) ); ?>
</p>
<div id="csv-import-redirections-progress-bar">
<div class="total">
<div class="progress-bar" style="width: <?php echo absint( $progress ); ?>%;"></div>
</div>
<input type="hidden" id="csv-import-redirections-progress-value" value="<?php echo absint( $progress ); ?>">
</div>
<?php
} else {
$status = (array) get_option( 'rank_math_csv_import_redirections_status', [] );
$classes = 'import-finished';
if ( ! empty( $status['errors'] ) ) {
$classes .= ' import-errors';
}
$message = self::get_import_complete_message();
?>
<p class="<?php echo esc_attr( $classes ); ?>"><?php echo wp_kses_post( $message ); ?></p>
<?php
}
}
/**
* Get status message after import is complete.
*
* @return string
*/
public static function get_import_complete_message() {
$status = (array) get_option( 'rank_math_csv_import_redirections_status', [] );
$imported_rows = is_countable( $status['imported_rows'] ) ? (array) $status['imported_rows'] : [];
$message = sprintf(
// Translators: placeholder is the number of rows imported.
__( 'CSV import completed. Successfully imported %d rows.', 'rank-math-pro' ),
count( $imported_rows )
);
if ( ! empty( $status['errors'] ) ) {
$message = __( 'CSV import completed.', 'rank-math-pro' ) . ' ';
$message .= sprintf(
// Translators: placeholder is the number of rows imported.
__( 'Imported %d rows.', 'rank-math-pro' ) . ' ',
count( $imported_rows )
);
if ( ! empty( $status['errors'] ) ) {
$message .= __( 'One or more errors occured while importing: ', 'rank-math-pro' ) . '<br>';
$message .= '<code>' . join( '</code><br><code>', $status['errors'] ) . '</code><br>';
}
if ( ! empty( $status['failed_rows'] ) ) {
$message .= '<br>' . __( 'The following lines could not be imported: ', 'rank-math-pro' ) . '<br>';
$message .= '<code>' . join( ', ', $status['failed_rows'] ) . '</code>';
}
}
if ( isset( $status['actions']['merged'] ) ) {
$status['actions']['created'] += $status['actions']['merged'];
}
foreach ( $status['actions'] as $action => $times_taken ) {
$message .= '<br><br>';
$message .= '<code>' . self::get_localized_action( $action ) . ': ' . $times_taken . '</code>';
}
return $message;
}
/**
* Get localization for import action word.
*
* @param string $action Action word.
* @return string
*/
public static function get_localized_action( $action ) {
$actions = [
'created' => __( 'Created', 'rank-math-pro' ),
'updated' => __( 'Updated', 'rank-math-pro' ),
'deleted' => __( 'Deleted', 'rank-math-pro' ),
'merged' => __( 'Merged', 'rank-math-pro' ),
];
if ( isset( $actions[ $action ] ) ) {
return $actions[ $action ];
}
return $action;
}
/**
* Enqueue styles.
*
* @return void
*/
public function enqueue() {
if ( ! $this->is_redirections_screen() ) {
return;
}
Helper::add_json( 'redirectionImportAction', add_query_arg( 'importexport', '1' ) . '#import-export-box' );
Helper::add_json( 'confirmRedirectionsCsvImport', __( 'Are you sure you want to import redirections from this CSV file?', 'rank-math-pro' ) );
Helper::add_json( 'confirmRedirectionsCsvCancel', __( 'Are you sure you want to stop the import process?', 'rank-math-pro' ) );
Helper::add_json( 'csvProgressNonce', wp_create_nonce( 'rank_math_csv_progress' ) );
wp_enqueue_script( 'rank-math-pro-redirections', RANK_MATH_PRO_URL . 'includes/modules/redirections/assets/js/redirections.js', [], RANK_MATH_PRO_VERSION, true );
wp_enqueue_style( 'rank-math-pro-redirections', RANK_MATH_PRO_URL . 'includes/modules/redirections/assets/css/redirections.css', [], RANK_MATH_PRO_VERSION );
}
}

View File

@@ -0,0 +1,200 @@
<?php
/**
* The CSV Export class.
*
* @since 1.0
* @package RankMathPro
* @subpackage RankMathPro\Admin
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Redirections\CSV_Import_Export_Redirections;
use RankMath\Helper;
use RankMath\Redirections\DB;
use RankMath\Redirections\Cache;
use RankMathPro\Admin\CSV;
defined( 'ABSPATH' ) || exit;
/**
* CSV Export.
*
* @codeCoverageIgnore
*/
class Exporter extends CSV {
/**
* Settings array.
*
* @var array
*/
private $settings = [];
/**
* Columns to export.
*
* @var array
*/
private $columns = [];
/**
* Constructor.
*
* @param array $options Export options.
* @return void
*/
public function __construct( $options ) {
$defaults = [
'include_deactivated' => true,
];
$this->settings = wp_parse_args( $options, $defaults );
$this->columns = CSV_Import_Export_Redirections::get_columns();
$this->columns[] = 'ignore';
}
/**
* Do export.
*
* @return void
*/
public function process_export() {
$this->export(
[
'filename' => 'rank-math-redirections',
'columns' => $this->columns,
'items' => $this->get_items(),
]
);
exit;
}
/**
* Get value for given column.
*
* @param string $column Column name.
* @param object $object WP_Post, WP_Term or WP_User.
*
* @return string
*/
public function get_column_value( $column, $object ) {
$val = '';
switch ( $column ) {
case 'id':
$val = $object->id;
break;
case 'source':
$val = $object->source_processed;
break;
case 'matching':
$val = $object->matching_processed;
break;
case 'destination':
$val = $object->url_to;
break;
case 'type':
$val = $object->header_code;
break;
case 'category':
$val = $object->categories_processed;
break;
case 'status':
$val = $object->status;
break;
case 'ignore':
$val = $object->ignore;
break;
}
return $this->escape_csv( apply_filters( "rank_math/admin/csv_export_redirections_column_{$column}", $val, $object ) ); //phpcs:ignore
}
/**
* Get all redirection IDs.
*
* @return array
*/
public function get_ids() {
global $wpdb;
$table = $wpdb->prefix . 'rank_math_redirections';
$statuses = [ 'active' ];
if ( $this->settings['include_deactivated'] ) {
$statuses[] = 'inactive';
}
$where = 'status IN (\'' . join( '\',\'', $statuses ) . '\')';
$post_ids = $wpdb->get_col( "SELECT ID FROM {$table} WHERE $where" ); // phpcs:ignore
return $post_ids;
}
/**
* Export all redirections.
*
* @return array
*/
public function get_items() {
global $wpdb;
$items = [];
$ids = $this->get_ids();
if ( ! $ids ) {
return $items;
}
$primary_column = 'id';
$table = $wpdb->prefix . 'rank_math_redirections';
$cols = $this->columns;
// Fetch 50 at a time rather than loading the entire table into memory.
while ( $next_batch = array_splice( $ids, 0, 50 ) ) { // phpcs:ignore
$where = 'WHERE ' . $primary_column . ' IN (' . join( ',', $next_batch ) . ')';
$objects = $wpdb->get_results( "SELECT * FROM {$table} $where" ); // phpcs:ignore
$current_object = 0;
// Begin Loop.
foreach ( $objects as $object ) {
$current_object++;
$this->process_categories( $object );
$sources = maybe_unserialize( $object->sources, true );
foreach ( $sources as $source ) {
$single_source = $object;
$single_source->source_processed = $source['pattern'];
$single_source->matching_processed = $source['comparison'];
$single_source->ignore = $source['ignore'];
$columns = [];
foreach ( $cols as $column ) {
$columns[] = $this->get_column_value( $column, $single_source ); // phpcs:ignore
}
$items[] = $columns;
}
}
}
return $items;
}
/**
* Process sources & categories data for export.
*
* @param object $object Redirection row.
* @return void
*/
public function process_categories( &$object ) {
$object->categories_processed = '';
$terms = wp_get_object_terms( $object->id, 'rank_math_redirection_category' );
if ( is_a( $terms, 'WP_Error' ) || ! is_array( $terms ) || empty( $terms ) ) {
return;
}
$object->categories_processed = join( ', ', wp_list_pluck( $terms, 'slug' ) );
}
}

View File

@@ -0,0 +1,157 @@
<?php
/**
* The CSV Import class.
*
* @since 1.0
* @package RankMathPro
* @subpackage RankMathPro\Admin
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Redirections\CSV_Import_Export_Redirections;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* CSV Import Export class.
*
* @codeCoverageIgnore
*/
class Import_Background_Process extends \WP_Background_Process {
/**
* Prefix.
*
* (default value: 'wp')
*
* @var string
* @access protected
*/
protected $prefix = 'rank_math';
/**
* Action.
*
* @var string
*/
protected $action = 'csv_import_redirections';
/**
* Importer instance.
*
* @var Importer
*/
private $importer;
/**
* Main instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Import_Background_Process
*/
public static function get() {
static $instance;
if ( is_null( $instance ) || ! ( $instance instanceof Import_Background_Process ) ) {
$instance = new Import_Background_Process();
}
return $instance;
}
/**
* Start creating batches.
*
* @param [type] $posts [description].
*/
public function start( $lines_number ) {
$chunks = array_chunk( range( 0, $lines_number ), apply_filters( 'rank_math/admin/csv_import_redirections_chunk_size', 100 ) );
foreach ( $chunks as $chunk ) {
$this->push_to_queue( $chunk );
}
$this->save()->dispatch();
}
/**
* Task.
*
* Override this method to perform any actions required on each
* queue item. Return the modified item for further processing
* in the next pass through. Or, return false to remove the
* item from the queue.
*
* @param mixed $item Queue item to iterate over.
*
* @return mixed
*/
protected function task( $item ) {
try {
$this->importer = new Importer();
foreach ( $item as $row ) {
$this->importer->import_line( $row );
}
$this->importer->batch_done( $item );
return false;
} catch ( \Exception $error ) {
return true;
}
}
/**
* Import complete. Clear options & add notification.
*
* @return void
*/
protected function complete() {
unlink( get_option( 'rank_math_csv_import_redirections' ) );
delete_option( 'rank_math_csv_import_redirections' );
delete_option( 'rank_math_csv_import_redirections_total' );
delete_option( 'rank_math_csv_import_redirections_settings' );
$status = (array) get_option( 'rank_math_csv_import_redirections_status', [] );
$notification_args = [
'type' => 'success',
'classes' => 'is-dismissible',
];
if ( ! empty( $status['errors'] ) ) {
$notification_args = [
'type' => 'error',
'classes' => 'is-dismissible',
];
}
Helper::add_notification(
CSV_Import_Export_Redirections::get_import_complete_message(),
$notification_args
);
parent::complete();
}
/**
* Count remaining items in batch.
*
* @return int
*/
public function count_remaining_items() {
if ( $this->is_queue_empty() ) {
// This fixes an issue where get_batch() runs too early and results in a PHP notice.
return get_option( 'rank_math_csv_import_redirections_total' );
}
$batch = $this->get_batch();
$count = 0;
if ( ! empty( $batch->data ) && is_array( $batch->data ) ) {
foreach ( $batch->data as $items ) {
$count += count( $items );
}
}
return $count;
}
}

View File

@@ -0,0 +1,293 @@
<?php
/**
* The CSV Import class.
*
* @since 1.0
* @package RankMathPro
* @subpackage RankMathPro\Admin
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Redirections\CSV_Import_Export_Redirections;
use RankMath\Redirections\DB;
use RankMath\Redirections\Redirection;
use MyThemeShop\Helpers\Arr;
defined( 'ABSPATH' ) || exit;
/**
* CSV Importer class.
*
* @codeCoverageIgnore
*/
class Import_Row {
/**
* Column defaults for optional columns.
*
* @var array
*/
private $column_defaults = [
'matching' => 'exact',
'type' => '301',
'category' => '',
'status' => 'active',
];
/**
* Stores whether import was successful or not.
*
* @var boolean
*/
public $success = false;
/**
* Stores what kind of import action has been done - create, update, or delete.
*
* @var boolean
*/
public $action = '';
/**
* Stores import error.
*
* @var string
*/
public $error = '';
/**
* Stores row data.
*
* @var array
*/
private $data = [];
/**
* Stores import settings.
*
* @var array
*/
private $settings = [];
/**
* Stores columns.
*
* @var array
*/
private $columns = [];
/**
* Constructor.
*
* @param array $data Row data.
* @param array $settings Import settings.
* @return void
*/
public function __construct( $data, $settings ) {
$this->data = $data;
$this->settings = $settings;
$this->import_redirection( $this->data );
/**
* Do custom action after importing a row.
*/
do_action( 'rank_math/admin/csv_import_redirection_row', $data, $settings, $this );
}
/**
* Get column default value.
*
* @param string $column Column name.
* @return string
*/
public function get_column_default( $column ) {
if ( isset( $this->column_defaults[ $column ] ) ) {
return $this->column_defaults[ $column ];
}
return '';
}
/**
* Magic getter.
*
* Return column value if is set and column name is in allowed columns list.
*
* @param string $property Property we want to get.
* @return string
*/
public function __get( $property ) {
if ( in_array( $property, $this->get_columns(), true ) && isset( $this->data[ $property ] ) ) {
return $this->data[ $property ];
}
return $this->get_column_default( $property );
}
/**
* Get CSV columns.
*
* @return array
*/
public function get_columns() {
if ( ! empty( $this->columns ) ) {
return $this->columns;
}
$this->columns = CSV_Import_Export_Redirections::get_columns();
$this->columns[] = 'ignore';
return $this->columns;
}
/**
* Create or update redirection.
*
* @param array $data Redirection data.
* @return mixed
*/
public function import_redirection( $data = [] ) {
$exist = DB::get_redirection( $data );
/**
* Filter to modify the redirection data before updating a redirection.
* Pass a false value to skip the update and create a new redirection instead.
*
* @param array|false $data Redirection data.
*/
$exist = apply_filters( 'rank_math/admin/csv_import_redirection_update', $exist, $data, $this );
if ( $exist ) {
return $this->update_redirection( $exist, $data );
}
return $this->create_redirection();
}
/**
* Insert redirection.
*
* @return mixed
*/
public function create_redirection() {
$sources = $this->get_sources();
if ( ! $sources || ( in_array( $this->type, [ '301', '302', '307' ], true ) && ! $this->destination ) ) {
return;
}
$redirection = Redirection::from(
[
'id' => '',
'url_to' => $this->destination,
'sources' => $sources,
'header_code' => $this->type,
'status' => $this->status,
]
);
$redirection->set_nocache( true );
if ( false === $redirection->save() ) {
return;
}
if ( $this->category ) {
// Create category if it doesn't exist.
wp_set_object_terms( $redirection->id, Arr::from_string( $this->category ), 'rank_math_redirection_category' );
}
$this->success = true;
$this->action = 'created';
return $redirection->id;
}
/**
* Edit an existing redirection.
*
* @param array $data Redirection exist.
* @return mixed
*/
public function update_redirection( $data, $input = [] ) {
if ( 'DELETE' === $this->destination ) {
$this->success = true;
$this->action = 'deleted';
return $this->delete_redirection( $data['id'] );
}
$sources = $this->get_sources();
$sources = array_unique( array_merge( $sources, $data['sources'] ), SORT_REGULAR );
$url_to = ! empty( $input['destination'] ) ? $input['destination'] : $data['url_to'];
$header_code = ! empty( $input['type'] ) ? $input['type'] : $data['header_code'];
$redirection = Redirection::from(
[
'id' => $data['id'],
'sources' => $sources,
'url_to' => $url_to,
'header_code' => $header_code,
'status' => $data['status'],
]
);
if ( false === $redirection->save() ) {
return;
}
$category = $this->category ? Arr::from_string( $this->category ) : [];
wp_set_object_terms( $redirection->id, $category, 'rank_math_redirection_category', true );
$this->success = true;
$this->action = ( empty( $input['id'] ) ) ? 'merged' : 'updated';
return $redirection->id;
}
/**
* Get stored error message or default.
*
* @return string
*/
public function get_error() {
if ( ! empty( $this->error ) ) {
return $this->error;
}
return esc_html__( 'Could not import redirection.', 'rank-math-pro' );
}
/**
* Delete a redirection.
*
* @param int $id Redirection ID.
* @return mixed
*/
public function delete_redirection( $id ) {
return DB::delete( $id );
}
/**
* Get correctly formatted sources array for saving.
*
* @return array
*/
public function get_sources() {
$sources = [];
if ( substr( $this->source, 0, 1 ) === '[' && substr( $this->source, -1 ) === ']' ) {
$sources = json_decode( $this->source, true );
if ( ! is_array( $sources ) || empty( $sources ) ) {
return false;
}
return $sources;
}
$sources = [
[
'pattern' => $this->source,
'comparison' => $this->matching,
'ignore' => $this->ignore,
],
];
return $sources;
}
}

View File

@@ -0,0 +1,363 @@
<?php
/**
* The CSV Import class.
*
* @since 1.0
* @package RankMathPro
* @subpackage RankMathPro\Admin
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Redirections\CSV_Import_Export_Redirections;
use MyThemeShop\Helpers\Arr;
defined( 'ABSPATH' ) || exit;
/**
* CSV Importer class.
*
* @codeCoverageIgnore
*/
class Importer {
/**
* Term slug => ID cache.
*
* @var array
*/
private static $term_ids = [];
/**
* Settings array. Default values.
*
* @var array
*/
private $settings = [];
/**
* Lines in the CSV that could not be imported for any reason.
*
* @var array
*/
private $failed_rows = [];
/**
* Lines in the CSV that could be imported successfully.
*
* @var array
*/
private $imported_rows = [];
/**
* Import actions taken.
*
* @var array
*/
private $actions = [];
/**
* Error messages.
*
* @var array
*/
private $errors = [];
/**
* SPL file object.
*
* @var \SplFileObject
*/
private $spl;
/**
* Column headers.
*
* @var array
*/
private $column_headers = [];
/**
* Start import from file.
*
* @param string $file Path to temporary CSV file.
* @param string $settings Import settings.
* @return void
*/
public function start( $file, $settings = [] ) {
update_option( 'rank_math_csv_import_redirections', $file );
update_option( 'rank_math_csv_import_redirections_settings', $settings );
delete_option( 'rank_math_csv_import_redirections_status' );
$this->settings = apply_filters( 'rank_math/admin/csv_import_redirections_settings', $settings );
$lines = $this->count_lines( $file );
update_option( 'rank_math_csv_import_redirections_total', $lines );
Import_Background_Process::get()->start( $lines );
}
/**
* Count all lines in CSV file.
*
* @param mixed $file Path to CSV.
* @return int
*/
public function count_lines( $file ) {
$file = new \SplFileObject( $file );
while ( $file->valid() ) {
$file->fgets();
}
$count = $file->key();
// Check if last line is empty.
$file->seek( $count );
$contents = $file->current();
if ( empty( trim( $contents ) ) ) {
$count--;
}
// Unlock file.
$file = null;
return $count;
}
/**
* Get specified line from CSV.
*
* @param string $file Path to file.
* @param int $line Line number.
* @return string
*/
public function get_line( $file, $line ) {
if ( empty( $this->spl ) ) {
$this->spl = new \SplFileObject( $file );
}
if ( ! $this->spl->eof() ) {
$this->spl->seek( $line );
$contents = $this->spl->current();
}
return $contents;
}
/**
* Parse and return column headers (first line in CSV).
*
* @param string $file Path to file.
* @return array
*/
public function get_column_headers( $file ) {
if ( ! empty( $this->column_headers ) ) {
return $this->column_headers;
}
if ( empty( $this->spl ) ) {
$this->spl = new \SplFileObject( $file );
}
if ( ! $this->spl->eof() ) {
$this->spl->seek( 0 );
$contents = $this->spl->current();
}
if ( empty( $contents ) ) {
return [];
}
$this->column_headers = Arr::from_string( $contents, apply_filters( 'rank_math/csv_import/separator', ',' ) );
return $this->column_headers;
}
/**
* Import specified line.
*
* @param int $line_number Selected line number.
* @return void
*/
public function import_line( $line_number ) {
// Skip headers.
if ( 0 === $line_number ) {
return;
}
$file = get_option( 'rank_math_csv_import_redirections' );
if ( ! $file ) {
$this->add_error( esc_html__( 'Missing import file.', 'rank-math-pro' ), 'missing_file' );
CSV_Import_Export_Redirections::cancel_import( true );
return;
}
$headers = $this->get_column_headers( $file );
if ( empty( $headers ) ) {
$this->add_error( esc_html__( 'Missing CSV headers.', 'rank-math-pro' ), 'missing_headers' );
return;
}
$required_columns = [ 'source', 'destination' ];
if ( count( array_intersect( $headers, $required_columns ) ) !== count( $required_columns ) ) {
$this->add_error( esc_html__( 'Missing one or more required columns.', 'rank-math-pro' ), 'missing_required_columns' );
return;
}
$raw_data = $this->get_line( $file, $line_number );
if ( empty( $raw_data ) ) {
$total_lines = (int) get_option( 'rank_math_csv_import_redirections_total' );
// Last line can be empty, that is not an error.
if ( $line_number !== $total_lines ) {
$this->add_error( esc_html__( 'Empty column data.', 'rank-math-pro' ), 'missing_data' );
$this->row_failed( $line_number );
}
return;
}
$csv_separator = apply_filters( 'rank_math/csv_import/separator', ',' );
$decoded = str_getcsv( $raw_data, $csv_separator );
if ( count( $headers ) !== count( $decoded ) ) {
$this->add_error( esc_html__( 'Columns number mismatch.', 'rank-math-pro' ), 'columns_number_mismatch' );
$this->row_failed( $line_number );
return;
}
$data = array_combine( $headers, $decoded );
$import_row = new Import_Row( $data, $this->settings );
if ( ! $import_row->success ) {
$this->add_error( $import_row->get_error(), 'row_import_error' );
$this->row_failed( $line_number );
return;
}
$this->row_imported( $line_number, $import_row->action );
}
/**
* Get term ID from slug.
*
* @param string $term_slug Term slug.
* @return int
*/
public static function get_term_id( $term_slug ) {
if ( ! empty( self::$term_ids[ $term_slug ] ) ) {
return self::$term_ids[ $term_slug ];
}
global $wpdb;
$where = $wpdb->prepare( 'slug = %s', $term_slug );
self::$term_ids[ $term_slug ] = $wpdb->get_var( "SELECT term_id FROM {$wpdb->terms} WHERE $where" ); // phpcs:ignore
return self::$term_ids[ $term_slug ];
}
/**
* After each batch is finished.
*
* @param array $items Processed items.
*/
public function batch_done( $items ) {
unset( $this->spl );
$status = (array) get_option( 'rank_math_csv_import_redirections_status', [] );
if ( ! isset( $status['errors'] ) || ! is_array( $status['errors'] ) ) {
$status['errors'] = [];
}
if ( ! isset( $status['failed_rows'] ) || ! is_array( $status['failed_rows'] ) ) {
$status['failed_rows'] = [];
}
if ( ! isset( $status['imported_rows'] ) || ! is_array( $status['imported_rows'] ) ) {
$status['imported_rows'] = [];
}
if ( ! isset( $status['actions'] ) || ! is_array( $status['actions'] ) ) {
$status['actions'] = [];
}
foreach ( $this->actions as $action => $number ) {
if ( ! isset( $status['actions'][ $action ] ) ) {
$status['actions'][ $action ] = 0;
}
$status['actions'][ $action ] += $number;
}
$status['imported_rows'] = array_merge( $status['imported_rows'], $this->get_imported_rows() );
$errors = $this->get_errors();
if ( $errors ) {
$status['errors'] = array_merge( $status['errors'], $errors );
$status['failed_rows'] = array_merge( $status['failed_rows'], $this->get_failed_rows() );
}
update_option( 'rank_math_csv_import_redirections_status', $status );
}
/**
* Set row import status.
*
* @param string $row New status.
* @return void
*/
private function row_failed( $row ) {
$this->failed_rows[] = $row + 1;
}
/**
* Log successful import of one row.
*
* @param int $row Line number.
* @param string $action Action taken.
* @return void
*/
private function row_imported( $row, $action = '' ) {
$this->imported_rows[] = $row + 1;
if ( $action && is_scalar( $action ) ) {
if ( ! isset( $this->actions[ $action ] ) ) {
$this->actions[ $action ] = 0;
}
$this->actions[ $action ]++;
}
}
/**
* Get failed rows array.
*
* @return array
*/
private function get_failed_rows() {
return $this->failed_rows;
}
/**
* Get failed rows array.
*
* @return array
*/
private function get_imported_rows() {
return $this->imported_rows;
}
/**
* Get all import errors.
*
* @return mixed Array of errors or false if there is no error.
*/
public function get_errors() {
return empty( $this->errors ) ? false : $this->errors;
}
/**
* Add import error.
*
* @param string $error_message New error.
* @param int $error_id Error ID.
* @return void
*/
public function add_error( $error_message, $error_id = null ) {
if ( is_null( $error_id ) ) {
$this->errors[] = $error_message;
return;
}
$this->errors[ $error_id ] = $error_message;
}
}

View File

@@ -0,0 +1 @@
.recipe-instructions-data{margin-bottom:15px}.recipe-instructions-data .inner-wrapper{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:15px}.recipe-instructions-data .inner-wrapper img{margin-left:15px;max-width:200px}.rank-math-review-notes{margin-top:20px;margin-bottom:20px;float:left;width:100%}.rank-math-review-notes h4{margin-top:0;margin-bottom:20px}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
(()=>{"use strict";const e=lodash,t=wp.i18n,a=wp.hooks,n=wp.data,m=wp.components;(0,a.addFilter)("rank_math_schema_list","rank-math-pro",(function(a){if((0,e.isEmpty)(rankMath.activeTemplates))return a;var l=wp.element.createElement("div",{className:"rank-math-schema-active-templates"},wp.element.createElement("h4",{className:"rank-math-schema-section-title"},(0,t.__)("Global Templates in Use","rank-math-pro")),(0,e.map)(rankMath.activeTemplates,(function(a,l){return wp.element.createElement("div",{key:l,id:"rank-math-schema-item",className:"rank-math-schema-item row"},wp.element.createElement("strong",{className:"rank-math-schema-name"},(0,e.get)(a,"metadata.title",a["@type"])),wp.element.createElement("span",{className:"rank-math-schema-item-actions"},wp.element.createElement(m.Button,{isLink:!0,className:"button rank-math-delete-schema",onClick:function(){return(0,n.dispatch)("rank-math-pro").deleteActiveTemplate(l)}},wp.element.createElement("i",{className:"rm-icon rm-icon-trash"}),wp.element.createElement("span",null,(0,t.__)("Delete","rank-math-pro")))))})));return wp.element.createElement(React.Fragment,null,a,l)}))})();

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,446 @@
<?php
/**
* The Schema Module
*
* @since 2.0.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Schema;
use RankMath\Helper;
use RankMath\Admin\Admin_Helper;
use RankMath\Schema\DB;
use RankMath\Traits\Hooker;
use RankMath\Rest\Sanitize;
use MyThemeShop\Helpers\Str;
use MyThemeShop\Helpers\Param;
use WP_Screen;
defined( 'ABSPATH' ) || exit;
/**
* Admin class.
*/
class Admin {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'admin_enqueue_scripts', 'overwrite_wplink', 100 );
$this->action( 'rank_math/admin/before_editor_scripts', 'admin_scripts' );
$this->action( 'rank_math/admin/editor_scripts', 'deregister_scripts', 99 );
$this->action( 'save_post', 'save', 10, 2 );
$this->action( 'edit_form_after_title', 'render_div' );
$this->filter( 'rank_math/filter_metadata', 'filter_metadata', 10, 2 );
$this->filter( 'rank_math/settings/snippet/types', 'add_pro_schema_types' );
$this->filter( 'rank_math/schema/filter_data', 'update_schema_data' );
new Taxonomy();
}
/**
* Update schema data.
*
* @param array $schemas Schema data.
*/
public function update_schema_data( $schemas ) {
if ( empty( $schemas ) ) {
return $schemas;
}
foreach ( $schemas as $schema_key => $schema ) {
if ( empty( $schema['review'] ) ) {
continue;
}
if (
! empty( $schema['review']['positiveNotes'] ) &&
! empty( $schema['review']['positiveNotes']['itemListElement'] ) &&
is_string( $schema['review']['positiveNotes']['itemListElement'] )
) {
$notes = explode( PHP_EOL, $schema['review']['positiveNotes']['itemListElement'] );
$schemas[ $schema_key ]['review']['positiveNotes']['itemListElement'] = [];
foreach ( $notes as $key => $note ) {
$schemas[ $schema_key ]['review']['positiveNotes']['itemListElement'][] = [
'@type' => 'ListItem',
'position' => $key + 1,
'name' => $note,
];
}
}
if (
! empty( $schema['review']['negativeNotes'] ) &&
! empty( $schema['review']['negativeNotes']['itemListElement'] ) &&
is_string( $schema['review']['negativeNotes']['itemListElement'] )
) {
$notes = explode( PHP_EOL, $schema['review']['negativeNotes']['itemListElement'] );
$schemas[ $schema_key ]['review']['negativeNotes']['itemListElement'] = [];
foreach ( $notes as $key => $note ) {
$schemas[ $schema_key ]['review']['negativeNotes']['itemListElement'][] = [
'@type' => 'ListItem',
'position' => $key + 1,
'name' => $note,
];
}
}
}
return $schemas;
}
/**
* Add Pro schema types in Schema settings choices array.
*
* @param array $types Schema types.
*/
public function add_pro_schema_types( $types ) {
$types = array_merge(
$types,
[
'dataset' => esc_html__( 'DataSet', 'rank-math-pro' ),
'FactCheck' => esc_html__( 'Fact Check', 'rank-math-pro' ),
'movie' => esc_html__( 'Movie', 'rank-math-pro' ),
]
);
unset( $types['off'] );
ksort( $types, SORT_NATURAL | SORT_FLAG_CASE );
return [ 'off' => esc_html__( 'None (Click here to set one)', 'rank-math-pro' ) ] + $types;
}
/**
* Overwrite wplink script file.
* Rank Math adds new options in the link popup when editing a post.
*/
public function overwrite_wplink() {
if ( ! Admin_Helper::is_post_edit() || Admin_Helper::is_posts_page() ) {
return;
}
wp_deregister_script( 'rank-math-formats' );
wp_register_script(
'rank-math-formats',
RANK_MATH_PRO_URL . 'assets/admin/js/gutenberg-formats.js',
[],
rank_math_pro()->version,
true
);
wp_deregister_script( 'wplink' );
wp_register_script( 'wplink', RANK_MATH_PRO_URL . 'assets/admin/js/wplink.js', [ 'jquery', 'wp-a11y' ], rank_math_pro()->version, true );
wp_localize_script(
'wplink',
'wpLinkL10n',
[
'title' => esc_html__( 'Insert/edit link', 'rank-math-pro' ),
'update' => esc_html__( 'Update', 'rank-math-pro' ),
'save' => esc_html__( 'Add Link', 'rank-math-pro' ),
'noTitle' => esc_html__( '(no title)', 'rank-math-pro' ),
'noMatchesFound' => esc_html__( 'No matches found.', 'rank-math-pro' ),
'linkSelected' => esc_html__( 'Link selected.', 'rank-math-pro' ),
'linkInserted' => esc_html__( 'Link inserted.', 'rank-math-pro' ),
'relCheckbox' => '<code>rel="nofollow"</code>',
'sponsoredCheckbox' => '<code>rel="sponsored"</code>',
'aboutCheckbox' => '<code>about</code>',
'mentionsCheckbox' => '<code>mentions</code>',
'schemaMarkupLabel' => esc_html__( 'Use in Schema Markup', 'rank-math-pro' ),
'linkTitle' => esc_html__( 'Link Title', 'rank-math-pro' ),
]
);
}
/**
* Enqueue Styles and Scripts required for the schema functionality on Gutenberg & Classic editor.
*
* @return void
*/
public function admin_scripts() {
if ( ! $this->can_enqueue_scripts() ) {
return;
}
$this->localize_data();
wp_enqueue_style( 'rank-math-schema-pro', RANK_MATH_PRO_URL . 'includes/modules/schema/assets/css/schema.css', [ 'rank-math-schema' ], rank_math_pro()->version );
wp_enqueue_script(
'rank-math-pro-schema-filters',
RANK_MATH_PRO_URL . 'includes/modules/schema/assets/js/schemaFilters.js',
[
'wp-plugins',
'wp-components',
'wp-hooks',
'wp-api-fetch',
'lodash',
],
rank_math_pro()->version,
true
);
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : false;
if ( $screen instanceof WP_Screen && 'rank_math_schema' === $screen->post_type ) {
Helper::add_json( 'isTemplateScreen', true );
wp_enqueue_script(
'rank-math-pro-schema',
rank_math()->plugin_url() . 'includes/modules/schema/assets/js/schema-template.js',
[
'clipboard',
'wp-autop',
'wp-components',
'wp-editor',
'wp-edit-post',
'wp-element',
'wp-i18n',
'wp-plugins',
'rank-math-analyzer',
],
rank_math_pro()->version,
true
);
return;
}
wp_enqueue_script( 'rank-math-schema-pro', RANK_MATH_PRO_URL . 'includes/modules/schema/assets/js/schema.js', [ 'rank-math-schema' ], rank_math_pro()->version, true );
}
/**
* Deregister some scripts.
*/
public function deregister_scripts() {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : false;
if ( ! $screen instanceof WP_Screen || 'post' !== $screen->base || Helper::is_elementor_editor() || 'rank_math_schema' !== $screen->post_type ) {
return;
}
wp_deregister_script( 'rank-math-editor' );
wp_deregister_script( 'rank-math-schema' );
if ( wp_script_is( 'rank-math-pro-editor', 'registered' ) ) {
wp_deregister_script( 'rank-math-pro-editor' );
}
}
/**
* Render app div
*/
public function render_div() {
$screen = get_current_screen();
if ( 'rank_math_schema' !== $screen->post_type ) {
return;
}
Helper::add_json( 'postType', 'rank_math_schema' );
wp_nonce_field( 'rank_math_schema_template', 'security' );
?>
<div id="rank-math-schema-template"></div>
<textarea name="rank_math_schema" rows="8" cols="80" class="rank-math-schema"></textarea>
<input type="text" name="rank_math_schema_meta_id" class="rank-math-schema-meta-id" value="">
<?php
}
/**
* Save post data.
*
* @param int $post_id Post id.
* @param object $post Post object.
*/
public function save( $post_id, $post ) {
if (
! isset( $_POST['security'] ) ||
! wp_verify_nonce( $_POST['security'], 'rank_math_schema_template' ) ||
! isset( $_POST['rank_math_schema'] )
) {
return $post_id;
}
$post_type = get_post_type_object( $post->post_type );
if ( ! current_user_can( $post_type->cap->edit_post, $post_id ) ) {
return $post_id;
}
$sanitizer = Sanitize::get();
$schema = stripslashes_deep( $_POST['rank_math_schema'] );
$schema = json_decode( $schema, true );
$schema = $sanitizer->sanitize( 'rank_math_schema', $schema );
$meta_key = 'rank_math_schema_' . $schema['@type'];
update_post_meta( $post_id, $meta_key, $schema );
// Publish Schema Template post.
if ( 'rank_math_schema' === $post->post_type && 'publish' !== $post->post_status ) {
wp_update_post(
[
'ID' => $post_id,
'post_status' => 'publish',
]
);
}
}
/**
* Add excluded template conditions in the Schema template and remove it from the metadata.
*
* @param array $meta Meta data to update.
* @param WP_REST_Request $request Full details about the request.
*
* @return array Processed metadata.
*/
public function filter_metadata( $meta, $request ) {
foreach ( $meta as $meta_key => $meta_value ) {
if ( ! Str::starts_with( 'rank_math_exclude_template_', $meta_key ) ) {
continue;
}
$template_id = absint( \str_replace( 'rank_math_exclude_template_', '', $meta_key ) );
$schema_data = DB::get_schemas( $template_id );
$meta_id = key( $schema_data );
$meta_key = 'rank_math_schema_' . $schema_data[ $meta_id ]['@type'];
$schema_data[ $meta_id ]['metadata']['displayConditions'][] = [
'condition' => 'exclude',
'category' => 'singular',
'type' => $request->get_param( 'objectType' ),
'value' => $request->get_param( 'objectID' ),
];
$db_id = absint( str_replace( 'schema-', '', $meta_id ) );
update_metadata_by_mid( 'post', $db_id, $schema_data[ $meta_id ], $meta_key );
unset( $meta[ "rank_math_exclude_template_{$template_id}" ] );
}
return $meta;
}
/**
* [get_schema_templates description]
*
* @return array
*/
protected function get_schema_templates() {
$posts = get_posts(
[
'post_type' => 'rank_math_schema',
'posts_per_page' => -1,
'orderby' => 'title',
'order' => 'ASC',
]
);
if ( empty( $posts ) ) {
return [];
}
$templates = [];
foreach ( $posts as $post ) {
$data = DB::get_template_type( $post->ID );
$data['title'] = $post->post_title;
$data['id'] = $post->ID;
$templates[] = $data;
}
return $templates;
}
/**
* Whether to enqueue schema scripts on the page.
*
* @return bool
*/
private function can_enqueue_scripts() {
if ( ! Helper::has_cap( 'onpage_snippet' ) ) {
return false;
}
if ( ! Helper::is_divi_frontend_editor() && ! is_admin() ) {
return false;
}
global $pagenow;
if ( 'edit-tags.php' === $pagenow ) {
return false;
}
if ( Admin_Helper::is_term_edit() ) {
$taxonomy = Param::request( 'taxonomy' );
return true !== apply_filters(
'rank_math/snippet/remove_taxonomy_data',
Helper::get_settings( 'titles.remove_' . $taxonomy . '_snippet_data' ),
$taxonomy
);
}
return ( Admin_Helper::is_post_edit() || Helper::is_divi_frontend_editor() ) && ! Admin_Helper::is_posts_page();
}
/**
* Add active templates to the schemas json
*
* @return array
*/
private function get_active_templates() {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : false;
if ( $screen instanceof WP_Screen && 'rank_math_schema' === $screen->post_type ) {
return [];
}
$templates = Display_Conditions::get_schema_templates();
if ( empty( $templates ) ) {
return [];
}
$schemas = [];
foreach ( $templates as $template ) {
$template['schema']['isTemplate'] = true;
$schemas[ $template['id'] ] = $template['schema'];
}
return $schemas;
}
/**
* Localized data.
*/
private function localize_data() {
$post = get_post();
Helper::add_json( 'postStatus', get_post_field( 'post_status', $post ) );
Helper::add_json( 'postLink', get_permalink( $post ) );
Helper::add_json( 'schemaTemplates', $this->get_schema_templates() );
Helper::add_json( 'activeTemplates', $this->get_active_templates() );
Helper::add_json( 'accessiblePostTypes', Helper::get_accessible_post_types() );
Helper::add_json( 'accessibleTaxonomies', Helper::get_accessible_taxonomies() );
Helper::add_json( 'postTaxonomies', $this->get_post_taxonomies() );
}
/**
* Get Post taxonomies.
*/
private function get_post_taxonomies() {
$post_types = Helper::get_accessible_post_types();
$data = [];
foreach ( $post_types as $post_type ) {
$taxonomies = Helper::get_object_taxonomies( $post_type );
if ( empty( $taxonomies ) ) {
continue;
}
unset( $taxonomies['off'] );
$data[ $post_type ] = [ 'all' => esc_html__( 'All Taxonomies', 'rank-math-pro' ) ] + $taxonomies;
}
return $data;
}
}

View File

@@ -0,0 +1,223 @@
<?php
/**
* The Schema AJAX
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Schema;
use MyThemeShop\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* Ajax class.
*/
class Ajax {
use \RankMath\Traits\Hooker;
use \RankMath\Traits\Ajax;
/**
* The Constructor.
*/
public function __construct() {
$this->ajax( 'fetch_from_url', 'fetch_from_url' );
$this->ajax( 'get_conditions_data', 'get_conditions_data' );
}
/**
* Fetch from url.
*/
public function fetch_from_url() {
$this->verify_nonce( 'rank-math-ajax-nonce' );
$this->has_cap_ajax( 'general' );
$url = Param::post( 'url', false, FILTER_VALIDATE_URL );
if ( ! $url || strtolower( substr( $url, 0, 4 ) ) !== 'http' ) {
$this->error( esc_html__( 'No url found.', 'rank-math-pro' ) );
}
$parser = new Parser();
$output = $parser->from_url( $url );
if ( is_wp_error( $output ) ) {
$this->error( $output->get_error_message() );
}
$this->success( [ 'json' => $output ] );
}
/**
* Get posts/terms/author data.
*/
public function get_conditions_data() {
$this->verify_nonce( 'rank-math-ajax-nonce' );
$this->has_cap_ajax( 'general' );
$method = 'singular' === Param::get( 'category', false ) ? 'get_singular' : 'get_terms';
$data = $this->{$method}(
Param::get( 'userInput', false ),
Param::get( 'type', false ),
Param::get( 'value', false ),
Param::get( 'postTaxonomy', false )
);
$this->success( [ 'data' => $data ] );
}
/**
* Get posts by searched string & post type.
*
* @param string $search Searched String.
* @param string $type Post Type.
* @param int $value Object ID.
* @param int $taxonomy Is taxonomy.
*/
private function get_singular( $search, $type, $value, $taxonomy ) {
if ( 'null' === $search && $value ) {
if ( $taxonomy && taxonomy_exists( $taxonomy ) ) {
$data = [
'value' => $value,
'title' => get_term( $value )->name,
];
} else {
$data = [
'value' => $value,
'title' => get_the_title( $value ),
];
}
return $data;
}
if ( $taxonomy && 'all' !== $taxonomy ) {
$terms = get_terms(
[
'taxonomy' => $taxonomy,
'fields' => 'id=>name',
'search' => $search,
'hide_empty' => false,
]
);
if ( empty( $terms ) ) {
return [];
}
foreach ( $terms as $id => $name ) {
$data[] = [
'value' => $id,
'title' => $name,
];
}
return $data;
}
$posts = get_posts(
[
'post_type' => $type,
's' => $search,
'numberposts' => -1,
]
);
if ( empty( $posts ) ) {
return [];
}
$data = [];
foreach ( $posts as $post ) {
$data[] = [
'value' => $post->ID,
'title' => $post->post_title,
];
}
return $data;
}
/**
* Get terms by searched string and taxonomy.
*
* @param string $search Searched String.
* @param string $type Object type.
* @param int $value Term ID.
* @param string $taxonomy Taxonomy name.
*/
private function get_terms( $search, $type, $value, $taxonomy ) {
$data = [];
if ( 'author' === $type ) {
return $this->get_authors( $search, $value );
}
if ( 'null' === $search && $value ) {
$term = get_term_by( 'id', absint( $value ), $type );
return [
'value' => $value,
'title' => ! empty( $term ) ? $term->name : '',
];
}
$terms = get_terms(
[
'taxonomy' => $type,
'search' => $search,
'hide_empty' => false,
]
);
if ( ! empty( $terms ) ) {
foreach ( $terms as $term ) {
$data[] = [
'value' => $term->term_id,
'title' => $term->name,
];
}
}
return $data;
}
/**
* Get terms by searched string and taxonomy.
*
* @param string $search Searched String.
* @param int $value Term ID.
*/
private function get_authors( $search, $value ) {
if ( 'null' === $search && $value ) {
$author = get_user_by( 'id', $value );
return [
'value' => $value,
'title' => ! empty( $author ) ? $author->display_name : '',
];
}
$data = [];
$authors = get_users(
[
'search' => '*' . $search . '*',
'search_columns' => [ 'display_name', 'user_nicename', 'user_login', 'user_email' ],
]
);
if ( ! empty( $authors ) ) {
foreach ( $authors as $author ) {
$data[] = [
'value' => $author->ID,
'title' => $author->data->display_name,
];
}
}
return $data;
}
}

View File

@@ -0,0 +1,292 @@
<?php
/**
* Outputs specific schema code from Schema Template
*
* @since 2.0.7
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Schema;
use RankMath\Helper;
use RankMath\Schema\DB;
use RankMath\Traits\Hooker;
use MyThemeShop\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* Display Conditions class.
*/
class Display_Conditions {
use Hooker;
/**
* Display conditions data.
*
* @var array
*/
private static $conditions = [];
/**
* Insert Schema data.
*
* @var array
*/
private static $insert_schemas = [];
/**
* Get Schema data from Schema Templates post type.
*
* @param array $data Array of json-ld data.
* @param JsonLD $jsonld Instance of jsonld.
*
* @return array
*/
public static function get_schema_templates( $data = [], $jsonld = [] ) {
global $wpdb;
$templates = $wpdb->get_col( "SELECT ID FROM {$wpdb->prefix}posts WHERE post_type='rank_math_schema' AND post_status='publish'" );
if ( empty( $templates ) ) {
return;
}
$newdata = [];
foreach ( $templates as $template ) {
self::$conditions = [
'general' => '',
'singular' => '',
'archive' => '',
];
$schema = DB::get_schemas( $template );
self::prepare_inserted_schemas( current( $schema ) );
if ( ! self::can_add( current( $schema ) ) ) {
continue;
}
if ( is_admin() || Helper::is_divi_frontend_editor() ) {
$newdata[] = [
'id' => $template,
'schema' => current( $schema ),
];
continue;
}
DB::unpublish_jobposting_post( $jsonld, $schema );
$schema = $jsonld->replace_variables( $schema );
$schema = $jsonld->filter( $schema, $jsonld, $data );
$newdata[] = $schema;
}
return $newdata;
}
/**
* Whether schema can be added to current page
*
* @param array $schema Schema Data.
*
* @return boolean
*/
private static function can_add( $schema ) {
if ( empty( $schema ) || empty( $schema['metadata']['displayConditions'] ) ) {
return false;
}
foreach ( $schema['metadata']['displayConditions'] as $condition ) {
$operator = $condition['condition'];
if ( 'insert' === $operator ) {
// We handle the insert condition in the prepare_inserted_schemas() method.
continue;
}
$category = $condition['category'];
$taxonomy = ! empty( $condition['postTaxonomy'] ) ? $condition['postTaxonomy'] : '';
$type = $condition['type'];
$value = $condition['value'];
$method = "can_add_{$category}";
// Skip if already confirmed.
if ( 'include' === $operator && self::$conditions[ $category ] ) {
continue;
}
self::$conditions[ $category ] = self::$method( $operator, $type, $value, $taxonomy );
}
// Add Schema if the only condition is "Include / Entire Site".
if ( ! empty( self::$conditions['general'] ) && 1 === count( $schema['metadata']['displayConditions'] ) ) {
return true;
}
if ( ( is_singular() || is_admin() ) && isset( self::$conditions['singular'] ) ) {
return self::$conditions['singular'];
}
if ( ( is_archive() || is_search() ) && isset( self::$conditions['archive'] ) ) {
return self::$conditions['archive'];
}
return ! empty( self::$conditions['general'] );
}
/**
* Prepare inserted schemas: check if they can be added to current page, and if so, add them to the $insert_schemas static array.
*
* @param array $schema Schema Data.
*/
private static function prepare_inserted_schemas( $schema ) {
if ( empty( $schema ) || empty( $schema['metadata']['displayConditions'] ) ) {
return;
}
foreach ( $schema['metadata']['displayConditions'] as $condition ) {
$operator = $condition['condition'];
if ( 'insert' !== $operator ) {
continue;
}
if ( empty( $schema['metadata']['title'] ) ) {
continue;
}
$in_schema = $condition['category'];
if ( 'custom' === $in_schema ) {
if ( empty( $condition['value'] ) ) {
continue;
}
$in_schema = $condition['value'];
}
if ( 'ProfilePage' === $in_schema && ! is_singular() ) {
continue;
}
if ( 'ProfilePage' === $in_schema && ! empty( $condition['authorID'] ) ) {
$author_ids = wp_parse_id_list( $condition['authorID'] );
global $post;
if ( ! in_array( (int) $post->post_author, $author_ids, true ) ) {
continue;
}
}
$with_key = $schema['metadata']['title'];
self::$insert_schemas[ $in_schema ][] = [
'key' => $with_key,
'schema' => $schema,
];
}
}
/**
* Get inserted schemas.
*
* @return array
*/
public static function get_insertable_schemas() {
return self::$insert_schemas;
}
/**
* Whether schema can be added to current page
*
* @param string $operator Comparision Operator.
*
* @return boolean
*/
private static function can_add_general( $operator ) {
return 'include' === $operator;
}
/**
* Whether schema can be added on archive page
*
* @param string $operator Comparision Operator.
* @param string $type Post/Taxonoy type.
* @param string $value Post/Term ID.
*
* @return boolean
*/
private static function can_add_archive( $operator, $type, $value ) {
if ( 'search' === $type ) {
return 'include' === $operator && is_search();
}
if ( ! is_archive() ) {
return false;
}
if ( 'all' === $type ) {
return 'include' === $operator;
}
if ( 'author' === $type ) {
return is_author() && 'include' === $operator && is_author( $value );
}
if ( 'category' === $type ) {
return ! is_category() ? self::$conditions['archive'] : 'include' === $operator && is_category( $value );
}
if ( 'post_tag' === $type ) {
return ! is_tag() ? self::$conditions['archive'] : 'include' === $operator && is_tag( $value );
}
return 'include' === $operator && is_tax( $type, $value );
}
/**
* Whether schema can be added on single page
*
* @param string $operator Comparision Operator.
* @param string $type Post/Taxonoy type.
* @param string $value Post/Term ID.
* @param string $taxonomy Post Taxonomy.
*
* @return boolean
*/
private static function can_add_singular( $operator, $type, $value, $taxonomy ) {
$post = is_admin() || is_singular() ? get_post( get_the_ID() ) : [];
if ( empty( $post ) ) {
return false;
}
if ( 'all' === $type ) {
return 'include' === $operator;
}
if ( $type !== $post->post_type ) {
return false;
}
if ( ! $value ) {
return 'include' === $operator;
}
if ( $taxonomy && 'exclude' === $operator ) {
return ! has_term( $value, $taxonomy );
}
if ( $taxonomy ) {
return 'include' === $operator && has_term( $value, $taxonomy );
}
if ( absint( $post->ID ) === absint( $value ) ) {
return 'include' === $operator;
}
return 'exclude' === $operator;
}
}

View File

@@ -0,0 +1,661 @@
<?php
/**
* Outputs specific schema code from Schema Template
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Schema;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Schema\DB;
use MyThemeShop\Helpers\Str;
use MyThemeShop\Helpers\HTML;
defined( 'ABSPATH' ) || exit;
/**
* Schema Frontend class.
*/
class Frontend {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->filter( 'rank_math/json_ld', 'add_about_mention_attributes', 11 );
$this->filter( 'rank_math/json_ld', 'add_template_schema', 8, 2 );
$this->filter( 'rank_math/json_ld', 'add_schema_from_shortcode', 8, 2 );
$this->filter( 'rank_math/json_ld', 'convert_schema_to_item_list', 99, 2 );
$this->filter( 'rank_math/json_ld', 'validate_schema_data', 999 );
$this->filter( 'rank_math/json_ld', 'add_subjectof_property', 99 );
$this->filter( 'rank_math/json_ld', 'insert_template_schema', 20, 2 );
$this->action( 'rank_math/schema/preview/validate', 'validate_preview_data' );
$this->filter( 'rank_math/snippet/rich_snippet_itemlist_entity', 'filter_item_list_schema' );
$this->filter( 'rank_math/schema/valid_types', 'valid_types' );
$this->filter( 'rank_math/snippet/rich_snippet_product_entity', 'add_manufacturer_property' );
$this->filter( 'rank_math/snippet/rich_snippet_product_entity', 'remove_empty_offers' );
$this->filter( 'rank_math/snippet/rich_snippet_videoobject_entity', 'convert_familyfriendly_property' );
$this->filter( 'rank_math/snippet/rich_snippet_podcastepisode_entity', 'convert_familyfriendly_property' );
$this->filter( 'rank_math/snippet/rich_snippet_entity', 'schema_entity' );
new Display_Conditions();
new Snippet_Pro_Shortcode();
if ( $this->do_filter( 'link/remove_schema_attribute', false ) ) {
$this->filter( 'the_content', 'remove_schema_attribute', 11 );
}
// Schema Preview.
$this->filter( 'query_vars', 'add_query_vars' );
$this->filter( 'init', 'add_endpoint' );
$this->action( 'template_redirect', 'schema_preview_template' );
}
/**
* Add the 'photos' query variable so WordPress won't mangle it.
*
* @param array $vars Array of vars.
*/
public function add_query_vars( $vars ) {
$vars[] = 'schema-preview';
return $vars;
}
/**
* Add endpoint
*/
public function add_endpoint() {
add_rewrite_endpoint( 'schema-preview', EP_PERMALINK | EP_PAGES | EP_ROOT );
}
/**
* Schema preview template
*/
public function schema_preview_template() {
global $wp_query;
// if this is not a request for schema preview or a singular or home object then bail.
if (
! isset( $wp_query->query_vars['schema-preview'] ) ||
( ! is_singular() && ! is_home() && ! is_category() && ! is_tag() && ! is_tax() )
) {
return;
}
header( 'Content-Type: application/json' );
do_action( 'rank_math/json_ld/preview' );
exit;
}
/**
* Add nofollow and target attributes to link.
*
* @param string $content Post content.
* @return string
*/
public function remove_schema_attribute( $content ) {
preg_match_all( '/<(a\s[^>]+)>/', $content, $matches );
if ( empty( $matches ) || empty( $matches[0] ) ) {
return $content;
}
foreach ( $matches[0] as $link ) {
$attrs = HTML::extract_attributes( $link );
if ( ! isset( $attrs['data-schema-attribute'] ) ) {
continue;
}
unset( $attrs['data-schema-attribute'] );
$content = str_replace( $link, '<a' . HTML::attributes_to_string( $attrs ) . '>', $content );
}
return $content;
}
/**
* Filter functiont to extend valid schema types to use in Rank Math generated schema object.
*
* @param array $types Valid Schema types.
*
* @return array
*/
public function valid_types( $types ) {
return array_merge( $types, [ 'movie', 'dataset', 'claimreview' ] );
}
/**
* Validate Code Validation Schema data before displaying it in Preview window.
*
* @param array $schemas Array of json-ld data.
* @return array
*
* @since 2.6.1
*/
public function validate_preview_data( $schemas ) {
foreach ( $schemas as $schema_key => $schema ) {
if ( empty( $schema['subjectOf'] ) ) {
continue;
}
foreach ( $schema['subjectOf'] as $key => $property ) {
if ( empty( $schemas[ $key ] ) ) {
continue;
}
$schema['subjectOf'][ $key ] = $schemas[ $key ];
unset( $schemas[ $key ] );
}
$schema['subjectOf'] = array_values( $schema['subjectOf'] );
$schemas[ $schema_key ] = $schema;
}
return $schemas;
}
/**
* Add FAQ/HowTo schema in subjectOf property of primary schema.
*
* @param array $schemas Array of json-ld data.
* @return array
*
* @since 1.0.62
*/
public function add_subjectof_property( $schemas ) {
if ( empty( $schemas ) ) {
return $schemas;
}
foreach ( $schemas as $id => $schema ) {
if ( ! Str::starts_with( 'schema-', $id ) && 'richSnippet' !== $id ) {
continue;
}
$this->add_prop_subjectof( $schema, $schemas );
if ( ! empty( $schema['subjectOf'] ) ) {
$schemas[ $id ] = $schema;
break;
}
}
return $schemas;
}
/**
* Add subjectOf property in current schema entity.
*
* @param array $entity Schema Entity.
* @param array $schemas Array of json-ld data.
*
* @since 1.0.62
*/
private function add_prop_subjectof( &$entity, &$schemas ) {
if (
! isset( $entity['@type'] ) ||
empty( $entity['isPrimary'] ) ||
! empty( $entity['isCustom'] ) ||
in_array( $entity['@type'], [ 'FAQPage', 'HowTo' ], true )
) {
return;
}
global $wp_query;
$subject_of = [];
foreach ( $schemas as $key => $schema ) {
if ( ! isset( $schema['@type'] ) || ! in_array( $schema['@type'], [ 'FAQPage', 'HowTo' ], true ) ) {
continue;
}
if ( isset( $schema['isPrimary'] ) ) {
unset( $schema['isPrimary'] );
}
if ( isset( $schema['isCustom'] ) ) {
unset( $schema['isCustom'] );
}
if ( isset( $wp_query->query_vars['schema-preview'] ) ) {
$subject_of[ $key ] = $schema;
continue;
}
$subject_of[] = $schema;
unset( $schemas[ $key ] );
}
$entity['subjectOf'] = $subject_of;
}
/**
* Get Default Schema Data.
*
* @param array $data Array of json-ld data.
* @param JsonLD $jsonld Instance of jsonld.
*
* @return array
*/
public function convert_schema_to_item_list( $data, $jsonld ) {
$schemas = array_filter(
$data,
function( $schema ) {
if ( isset( $schema['@type'] ) && in_array( $schema['@type'], [ 'Course', 'Movie', 'Recipe', 'Restaurant' ], true ) ) {
return true;
}
return false;
}
);
if ( 2 > count( $schemas ) ) {
return $data;
}
$data['itemList'] = [
'@type' => 'ItemList',
'itemListElement' => [],
];
$count = 1;
foreach ( $schemas as $id => $schema ) {
unset( $data[ $id ] );
$schema['url'] = $jsonld->parts['url'] . '#' . $id;
if ( isset( $schema['isPrimary'] ) ) {
unset( $schema['isPrimary'] );
}
$data['itemList']['itemListElement'][] = [
'@type' => 'ListItem',
'position' => $count,
'item' => $schema,
];
$count++;
}
return $data;
}
/**
* Add Schema data from Schema Templates.
*
* @param array $data Array of json-ld data.
* @param JsonLD $jsonld Instance of jsonld.
*
* @return array
*/
public function add_template_schema( $data, $jsonld ) {
$schemas = Display_Conditions::get_schema_templates( $data, $jsonld );
if ( empty( $schemas ) ) {
return $data;
}
foreach ( $schemas as $schema ) {
$data = array_merge( $data, $schema );
}
return $data;
}
/**
* Insert the appropriate Schema data from Schema Templates.
*
* @param array $data Array of json-ld data.
* @param JsonLD $jsonld Instance of jsonld.
*
* @return array
*/
public function insert_template_schema( $data, $jsonld ) {
$schema_array = Display_Conditions::get_insertable_schemas();
if ( empty( $schema_array ) ) {
return $data;
}
foreach ( $schema_array as $insert_in => $schemas ) {
// If the $insert_in is not a @type present in the data, then skip it.
$insert_key = false;
foreach ( $data as $key => $schema ) {
if ( $key === $insert_in ) {
$insert_key = $key;
break;
}
if ( ! isset( $schema['@type'] ) ) {
continue;
}
if ( $schema['@type'] === $insert_in ) {
$insert_key = $key;
break;
}
}
if ( ! $insert_key ) {
continue;
}
// Now insert the schema(s).
foreach ( $schemas as $schema ) {
$schema_key = $schema['key'];
$schema_data = $schema['schema'];
unset( $schema_data['isPrimary'], $schema_data['isCustom'], $schema_data['isTemplate'], $schema_data['metadata'] );
$schema_data = $jsonld->replace_variables( $schema_data );
foreach ( $schema_data as $key => $value ) {
if ( ! isset( $data[ $insert_key ][ $key ] ) ) {
$data[ $insert_key ][ $key ] = $value;
}
}
}
}
return $data;
}
/**
* Add About & Mention attributes to Webpage schema.
*
* @param array $data Array of json-ld data.
* @return array
*/
public function add_about_mention_attributes( $data ) {
if ( ! is_singular() || empty( $data['WebPage'] ) ) {
return $data;
}
global $post;
if ( ! $post->post_content ) {
return $data;
}
preg_match_all( '|<a[^>]+>([^<]+)</a>|', $post->post_content, $matches );
if ( empty( $matches ) || empty( $matches[0] ) ) {
return $data;
}
foreach ( $matches[0] as $link ) {
$attrs = HTML::extract_attributes( $link );
if ( empty( $attrs['data-schema-attribute'] ) ) {
continue;
}
$attributes = explode( ' ', $attrs['data-schema-attribute'] );
if ( in_array( 'about', $attributes, true ) ) {
$data['WebPage']['about'][] = [
'@type' => 'Thing',
'name' => wp_strip_all_tags( $link ),
'sameAs' => $attrs['href'],
];
}
if ( in_array( 'mentions', $attributes, true ) ) {
$data['WebPage']['mentions'][] = [
'@type' => 'Thing',
'name' => wp_strip_all_tags( $link ),
'sameAs' => $attrs['href'],
];
}
}
return $data;
}
/**
* Filter to change the itemList schema data.
*
* @param array $schema Snippet Data.
* @return array
*/
public function filter_item_list_schema( $schema ) {
if ( ! is_archive() ) {
return $schema;
}
$elements = [];
$count = 1;
while ( have_posts() ) {
the_post();
$elements[] = [
'@type' => 'ListItem',
'position' => $count,
'url' => get_the_permalink(),
];
$count++;
}
wp_reset_postdata();
$schema['itemListElement'] = $elements;
return $schema;
}
/**
* Validate Schema Data.
*
* @param array $schemas Array of json-ld data.
*
* @return array
*/
public function validate_schema_data( $schemas ) {
if ( empty( $schemas ) ) {
return $schemas;
}
$validate_types = [ 'Dataset', 'LocalBusiness' ];
foreach ( $schemas as $id => $schema ) {
$type = isset( $schema['@type'] ) ? $schema['@type'] : '';
if ( ! Str::starts_with( 'schema-', $id ) || ! in_array( $type, $validate_types, true ) ) {
continue;
}
$hash = [
'isPartOf' => true,
'publisher' => 'LocalBusiness' === $type,
'inLanguage' => 'LocalBusiness' === $type,
];
foreach ( $hash as $property => $value ) {
if ( ! $value || ! isset( $schema[ $property ] ) ) {
continue;
}
if ( 'Dataset' === $type && 'isPartOf' === $property && ! empty( $schema[ $property ]['@type'] ) ) {
continue;
}
unset( $schemas[ $id ][ $property ] );
}
if ( 'Dataset' === $type && ! empty( $schema['publisher'] ) ) {
$schemas[ $id ]['creator'] = $schema['publisher'];
unset( $schemas[ $id ]['publisher'] );
}
}
return $schemas;
}
/**
* Get Schema data from Schema Templates post type.
*
* @param array $data Array of json-ld data.
* @param JsonLD $jsonld Instance of jsonld.
*
* @return array
*/
public function add_schema_from_shortcode( $data, $jsonld ) {
if ( ! is_singular() || ! $this->do_filter( 'rank_math/schema/add_shortcode_schema', true ) ) {
return $data;
}
global $post;
$blocks = parse_blocks( $post->post_content );
if ( ! empty( $blocks ) ) {
foreach ( $blocks as $block ) {
if ( 'rank-math/rich-snippet' !== $block['blockName'] ) {
continue;
}
$id = isset( $block['attrs']['id'] ) ? $block['attrs']['id'] : '';
$post_id = isset( $block['attrs']['post_id'] ) ? $block['attrs']['post_id'] : '';
if ( ! $id && ! $post_id ) {
continue;
}
$data = array_merge( $data, $this->get_schema_data_by_id( $id, $post_id, $jsonld, $data ) );
}
}
$regex = '/\[rank_math_rich_snippet (.*)\]/m';
preg_match_all( $regex, $post->post_content, $matches, PREG_SET_ORDER, 0 );
if ( ! empty( $matches ) ) {
foreach ( $matches as $key => $match ) {
parse_str( str_replace( ' ', '&', $match[1] ), $output );
$post_id = isset( $output['post_id'] ) ? str_replace( [ '"', "'" ], '', $output['post_id'] ) : '';
$id = isset( $output['id'] ) ? str_replace( [ '"', "'" ], '', $output['id'] ) : '';
$data = array_merge( $data, $this->get_schema_data_by_id( $id, $post_id, $jsonld, $data ) );
}
}
return $data;
}
/**
* Add Manufacturer property to Product schema.
*
* @param array $schema Product schema data.
* @return array
*/
public function add_manufacturer_property( $schema ) {
if ( empty( $schema['manufacturer'] ) ) {
return $schema;
}
$type = Helper::get_settings( 'titles.knowledgegraph_type' );
$type = 'company' === $type ? 'organization' : 'person';
$schema['manufacturer'] = [ '@id' => home_url( "/#{$type}" ) ];
return $schema;
}
/**
* Remove empty offers data from the Product schema.
*
* @param array $schema Product schema data.
* @return array
*/
public function remove_empty_offers( $schema ) {
if (
empty( $schema['offers'] ) ||
empty( $schema['review'] ) ||
(
empty( $schema['review']['positiveNotes'] ) &&
empty( $schema['review']['negativeNotes'] )
)
) {
return $schema;
}
if ( ! empty( $schema['offers']['price'] ) ) {
return $schema;
}
unset( $schema['offers'] );
return $schema;
}
/**
* Backward compatibility code to move the positiveNotes & negativeNotes properties in review.
*
* @param array $schema Schema data.
* @return array
*
* @since 3.0.19
*/
public function schema_entity( $schema ) {
if ( empty( $schema['review'] ) ) {
return $schema;
}
if ( ! empty( $schema['positiveNotes'] ) ) {
$schema['review']['positiveNotes'] = $schema['positiveNotes'];
unset( $schema['positiveNotes'] );
}
if ( ! empty( $schema['negativeNotes'] ) ) {
$schema['review']['negativeNotes'] = $schema['negativeNotes'];
unset( $schema['negativeNotes'] );
}
return $schema;
}
/**
* Convert isFamilyFriendly property used in Video schema to boolean.
*
* @param array $schema Video schema data.
* @return array
*
* @since 2.13.0
*/
public function convert_familyfriendly_property( $schema ) {
if ( empty( $schema['isFamilyFriendly'] ) ) {
return $schema;
}
$schema['isFamilyFriendly'] = 'True';
return $schema;
}
/**
* Get Schema data by ID.
*
* @param string $id Schema shortcode ID.
* @param int $post_id Post ID.
* @param JsonLD $jsonld Instance of jsonld.
* @param array $data Array of json-ld data.
*
* @return array
*/
private function get_schema_data_by_id( $id, $post_id, $jsonld, $data ) {
$schemas = $id ? DB::get_schema_by_shortcode_id( trim( $id ) ) : DB::get_schemas( trim( $post_id ) );
$current_post_id = get_the_ID();
if (
empty( $schemas ) ||
(
isset( $schemas['post_id'] ) && $current_post_id === (int) $schemas['post_id']
) ||
$post_id === $current_post_id
) {
return [];
}
$post_id = isset( $schemas['post_id'] ) ? $schemas['post_id'] : $post_id;
$schemas = isset( $schemas['schema'] ) ? [ $schemas['schema'] ] : $schemas;
$schemas = $jsonld->replace_variables( $schemas, get_post( $post_id ) );
$schemas = $jsonld->filter( $schemas, $jsonld, $data );
if ( isset( $schemas[0]['isPrimary'] ) ) {
unset( $schemas[0]['isPrimary'] );
}
return $schemas;
}
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* Add Media RSS feed.
*
* @since 1.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Schema;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Media_RSS class.
*/
class Media_RSS {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
if ( Helper::get_settings( 'general.disable_media_rss' ) ) {
return;
}
$this->action( 'rss2_ns', 'add_namespace' );
$this->action( 'rss2_item', 'add_video_data', 10, 1 );
}
/**
* Add namespace to RSS feed.
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
public function add_namespace() {
if ( apply_filters( 'rank_math/rss/add_media_namespace', true ) ) {
echo ' xmlns:media="http://search.yahoo.com/mrss/" ';
}
}
/**
* Add Video Data in RSS feed.
*
* @see https://www.rssboard.org/media-rss
* @see https://support.google.com/news/publisher-center/answer/9545420?hl=en
*/
public function add_video_data() {
global $post;
$video_schema = get_post_meta( $post->ID, 'rank_math_schema_VideoObject', true );
if ( empty( $video_schema ) ) {
return;
}
$url = ! empty( $video_schema['contentUrl'] ) ? $video_schema['contentUrl'] : ( ! empty( $video_schema['embedUrl'] ) ? $video_schema['embedUrl'] : '' );
if ( ! $url ) {
return;
}
$attrs = ! empty( $video_schema['width'] ) ? ' width="' . esc_attr( $video_schema['width'] ) . '"' : '';
$attrs .= ! empty( $video_schema['height'] ) ? ' height="' . esc_attr( $video_schema['height'] ) . '"' : '';
$duration = ! empty( $video_schema['duration'] ) ? Helper::duration_to_seconds( $video_schema['duration'] ) : '';
$name = ! empty( $video_schema['name'] ) ? Helper::replace_vars( $video_schema['name'], $post ) : '';
$description = ! empty( $video_schema['description'] ) ? Helper::replace_vars( $video_schema['description'], $post ) : '';
$thumbnail = ! empty( $video_schema['thumbnailUrl'] ) ? Helper::replace_vars( $video_schema['thumbnailUrl'], $post ) : '';
$tags = ! empty( $video_schema['metadata']['tags'] ) ? Helper::replace_vars( $video_schema['metadata']['tags'] ) : '';
$categories = ! empty( $video_schema['metadata']['category'] ) ? Helper::replace_vars( $video_schema['metadata']['category'] ) : '';
$rating = ! empty( $video_schema['isFamilyFriendly'] ) ? 'nonadult' : 'adult';
$this->newline( '<media:content url="' . esc_url( $url ) . '" medium="video"' . $attrs . '>' );
$this->newline( '<media:player url="' . esc_url( $url ) . '" />', 3 );
if ( $name ) {
$this->newline( '<media:title type="plain">' . esc_html( $name ) . '</media:title>', 3 );
}
if ( $description ) {
$this->newline( '<media:description type="html"><![CDATA[' . wp_kses_post( $description ) . ']]></media:description>', 3 );
}
if ( $thumbnail ) {
$this->newline( '<media:thumbnail url="' . esc_url( $thumbnail ) . '" />', 3 );
}
if ( $tags ) {
$this->newline( '<media:keywords>' . esc_html( $tags ) . '</media:keywords>', 3 );
}
if ( $categories ) {
$this->newline( '<media:category>' . esc_html( $categories ) . '</media:category>', 3 );
}
if ( $rating ) {
$this->newline( '<media:rating scheme="urn:simple">' . esc_html( $rating ) . '</media:rating>', 3 );
}
$this->newline( '</media:content>', 2 );
}
/**
* Write a newline with indent count.
*
* @param string $content Content to write.
* @param integer $indent Count of indent.
*/
private function newline( $content, $indent = 0 ) {
echo str_repeat( "\t", $indent ) . $content . "\n";
}
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* The Schema Parser
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Schema;
use WP_Error;
use DOMXpath;
use DOMDocument;
defined( 'ABSPATH' ) || exit;
/**
* Parser class.
*/
class Parser {
/**
* Get json from url.
*
* @param string $url Url to fetch html from.
*/
public function from_url( $url ) {
$response = wp_remote_get(
$url,
[
'timeout' => 30,
'sslverify' => false,
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
$response = wp_remote_retrieve_body( $response );
if ( empty( $response ) ) {
return new WP_Error( 'body_not_found', esc_html__( 'No html body found.', 'rank-math-pro' ) );
}
return $this->from_html( $response );
}
/**
* Get json from html.
*
* @param string $html HTML to parse.
*/
public function from_html( $html ) {
libxml_use_internal_errors( 1 );
// DOM.
$dom = new DOMDocument();
$dom->loadHTML( $html );
// XPath.
$xpath = new DOMXpath( $dom );
$scripts = $xpath->query( '//script[@type="application/ld+json"]' );
$json = [];
foreach ( $scripts as $script ) {
$json[] = json_decode( trim( $script->nodeValue ) ); // phpcs:ignore
}
return $json;
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* The Schema Template Post Type
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Schema;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Post_Type class.
*/
class Post_Type {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'init', 'register' );
$this->action( 'admin_menu', 'add_menu', 11 );
$this->action( 'parent_file', 'parent_file' );
$this->action( 'submenu_file', 'submenu_file' );
}
/**
* Register template post type.
*/
public function register() {
$labels = [
'name' => _x( 'Schemas', 'Post Type General Name', 'rank-math-pro' ),
'singular_name' => _x( 'Schema', 'Post Type Singular Name', 'rank-math-pro' ),
'menu_name' => __( 'Schemas', 'rank-math-pro' ),
'name_admin_bar' => __( 'Schema', 'rank-math-pro' ),
'all_items' => __( 'All Schemas', 'rank-math-pro' ),
'add_new_item' => __( 'Add New Schema', 'rank-math-pro' ),
'new_item' => __( 'New Schema', 'rank-math-pro' ),
'edit_item' => __( 'Edit Schema', 'rank-math-pro' ),
'update_item' => __( 'Update Schema', 'rank-math-pro' ),
'view_item' => __( 'View Schema', 'rank-math-pro' ),
'view_items' => __( 'View Schemas', 'rank-math-pro' ),
'search_items' => __( 'Search schemas', 'rank-math-pro' ),
];
$capability = 'rank_math_onpage_snippet';
$args = [
'label' => __( 'Schema', 'rank-math-pro' ),
'description' => __( 'Rank Math Schema Templates', 'rank-math-pro' ),
'labels' => $labels,
'supports' => [ 'title' ],
'hierarchical' => false,
'public' => false,
'show_ui' => true,
'show_in_menu' => false,
'menu_position' => 5,
'show_in_admin_bar' => false,
'show_in_nav_menus' => false,
'can_export' => true,
'has_archive' => false,
'exclude_from_search' => true,
'publicly_queryable' => false,
'rewrite' => false,
'capability_type' => 'page',
'capabilities' => [
'edit_post' => $capability,
'read_post' => $capability,
'delete_post' => $capability,
'edit_posts' => $capability,
'edit_others_posts' => $capability,
'publish_posts' => $capability,
'read_private_posts' => $capability,
'create_posts' => $capability,
],
'show_in_rest' => true,
];
register_post_type( 'rank_math_schema', $args );
}
/**
* Add post type as submenu.
*/
public function add_menu() {
if ( ! Helper::has_cap( 'onpage_snippet' ) ) {
return;
}
add_submenu_page(
'rank-math',
esc_html__( 'Schema Templates', 'rank-math-pro' ),
esc_html__( 'Schema Templates', 'rank-math-pro' ),
'edit_posts',
'edit.php?post_type=rank_math_schema'
);
}
/**
* Fix parent active menu
*
* @param string $file Filename.
* @return string
*/
public function parent_file( $file ) {
$screen = get_current_screen();
if ( in_array( $screen->base, [ 'post', 'edit' ], true ) && 'rank_math_schema' === $screen->post_type ) {
$file = 'rank-math';
}
return $file;
}
/**
* Fix submenu active menu
*
* @param string $file Filename.
* @return string
*/
public function submenu_file( $file ) {
$screen = get_current_screen();
if ( in_array( $screen->base, [ 'post', 'edit' ], true ) && 'rank_math_schema' === $screen->post_type ) {
$file = 'edit.php?post_type=rank_math_schema';
}
return $file;
}
}

View File

@@ -0,0 +1,165 @@
<?php
/**
* The REST API.
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Schema;
use WP_Error;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Controller;
use RankMath\Helper;
use RankMath\Schema\DB;
use RankMath\Traits\Meta;
use RankMath\Rest\Sanitize;
defined( 'ABSPATH' ) || exit;
/**
* Rest class.
*/
class Rest extends WP_REST_Controller {
use Meta;
/**
* Constructor.
*/
public function __construct() {
$this->namespace = \RankMath\Rest\Rest_Helper::BASE;
}
/**
* Registers the routes for the objects of the controller.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/saveTemplate',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'save_template' ],
'args' => [
'schema' => [
'required' => true,
'description' => esc_html__( 'Schema to add.', 'rank-math-pro' ),
'validate_callback' => [ '\\RankMath\\Rest\\Rest_Helper', 'is_param_empty' ],
],
],
'permission_callback' => [ $this, 'get_permissions_check' ],
]
);
register_rest_route(
$this->namespace,
'/getVideoData',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'get_video_data' ],
'args' => $this->get_video_args(),
'permission_callback' => [ '\\RankMath\\Rest\\Rest_Helper', 'can_manage_options' ],
]
);
}
/**
* Get Video details.
*
* @param WP_REST_Request $request Full details about the request.
* @return bool Whether the API key matches or not.
*/
public function get_video_data( WP_REST_Request $request ) {
$object_id = $request->get_param( 'objectID' );
$url = $request->get_param( 'url' );
$post_type = get_post_type( $object_id );
if ( false === filter_var( $url, FILTER_VALIDATE_URL ) || ! Helper::get_settings( "titles.pt_{$post_type}_autodetect_video", 'on' ) ) {
return [];
}
global $wp_embed;
return ( new \RankMathPro\Schema\Video\Parser( get_post( $object_id ) ) )->get_metadata( $wp_embed->autoembed( $url ) );
}
/**
* Update metadata.
*
* @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_template( WP_REST_Request $request ) {
$sanitizer = Sanitize::get();
$schema = $request->get_param( 'schema' );
$post_id = $request->get_param( 'postId' );
foreach ( $schema as $id => $value ) {
$schema[ $id ] = $sanitizer->sanitize( $id, $value );
}
if ( $post_id ) {
DB::delete_schema_data( $post_id );
}
$meta_key = 'rank_math_schema_' . $schema['@type'];
$template_id = wp_insert_post(
[
'ID' => $post_id,
'post_status' => 'publish',
'post_type' => 'rank_math_schema',
'post_title' => $schema['metadata']['title'],
]
);
update_post_meta( $template_id, $meta_key, $schema );
return [
'id' => $template_id,
'link' => get_edit_post_link( $template_id ),
];
}
/**
* Checks whether a given request has permission to read post.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public static function get_permissions_check( $request ) { // phpcs:ignore
if ( current_user_can( 'edit_posts' ) ) {
return true;
}
return new WP_Error(
'rest_cannot_edit',
__( 'Sorry, you are not allowed to save template.', 'rank-math-pro' ),
[ 'status' => rest_authorization_required_code() ]
);
}
/**
* Get video arguments.
*
* @return array
*/
private function get_video_args() {
return [
'objectID' => [
'type' => 'integer',
'required' => true,
'description' => esc_html__( 'Object unique id', 'rank-math-pro' ),
'validate_callback' => [ '\\RankMath\\Rest\\Rest_Helper', 'is_param_empty' ],
],
'url' => [
'required' => true,
'description' => esc_html__( 'Video URL.', 'rank-math-pro' ),
'validate_callback' => [ '\\RankMath\\Rest\\Rest_Helper', 'is_param_empty' ],
],
];
}
}

View File

@@ -0,0 +1,288 @@
<?php
/**
* The Schema Module
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Schema;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use MyThemeShop\Helpers\Str;
defined( 'ABSPATH' ) || exit;
/**
* Schema class.
*/
class Schema {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->includes();
$this->hooks();
}
/**
* Include required files.
*/
public function includes() {
new Admin();
new Ajax();
new Post_Type();
new \RankMath\Schema\Schema();
new Frontend();
new Video();
}
/**
* Register hooks.
*/
public function hooks() {
$this->action( 'enqueue_block_editor_assets', 'editor_assets', 9 );
$this->filter( 'rank_math/schema/block/howto-block', 'add_graph', 11, 2 );
$this->filter( 'rank_math/schema/block/howto/content', 'block_content', 11, 3 );
}
/**
* Enqueue Styles and Scripts required for blocks at backend.
*/
public function editor_assets() {
wp_enqueue_script(
'rank-math-howto-block',
RANK_MATH_PRO_URL . 'assets/admin/js/blocks.js',
[],
rank_math_pro()->version,
true
);
if ( Helper::is_module_active( 'local-seo' ) ) {
Helper::add_json( 'previewImage', RANK_MATH_PRO_URL . 'includes/modules/local-seo/assets/img/map-placeholder.jpg' );
Helper::add_json( 'mapStyle', Helper::get_settings( 'titles.map_style', 'roadmap' ) );
Helper::add_json( 'limitLocations', Helper::get_settings( 'titles.limit_results', 10 ) );
}
}
/**
* Display additional content in the HowTo Block.
*
* @param string $output Schema data.
* @param array $data Output data.
* @param array $attributes Schema attributes.
*/
public function block_content( $output, $data, $attributes ) {
$data[] = $this->build_estimated_cost( $attributes );
$data[] = $this->build_supplies( $attributes );
$data[] = $this->build_tools( $attributes );
$data[] = $this->build_materials( $attributes );
return join( "\n", $data );
}
/**
* FAQ rich snippet.
*
* @param array $data Array of JSON-LD data.
* @param array $block JsonLD Instance.
*
* @return array
*/
public function add_graph( $data, $block ) {
$attrs = $block['attrs'];
$this->add_estimated_cost( $data['howto'], $attrs );
$this->add_supplies( $data['howto'], $attrs );
$this->add_tools( $data['howto'], $attrs );
$this->add_materials( $data['howto'], $attrs );
return $data;
}
/**
* [build_estimated_cost description]
*
* @param [type] $attrs [description].
*
* @return [type] [description]
*/
private function build_estimated_cost( $attrs ) {
if ( empty( $attrs['estimatedCost'] ) ) {
return;
}
$currency = ! empty( $attrs['estimatedCostCurrency'] ) ? $attrs['estimatedCostCurrency'] : 'USD';
return sprintf(
'<p class="rank-math-howto-estimatedCost"><strong>%2$s</strong> <span>%1$s</span></p>',
$attrs['estimatedCost'] . ' ' . $currency,
__( 'Estimated Cost:', 'rank-math-pro' )
);
}
/**
* [build_estimated_cost description]
*
* @param [type] $attrs [description].
*
* @return [type] [description]
*/
private function build_supplies( $attrs ) {
if ( empty( $attrs['supply'] ) ) {
return;
}
$supplies = Str::to_arr_no_empty( $attrs['supply'] );
if ( empty( $supplies ) ) {
return;
}
return sprintf(
'<p class="rank-math-howto-supply"><strong>%2$s</strong> <ul><li>%1$s</li></ul></p>',
implode( '</li><li>', $supplies ),
__( 'Supply:', 'rank-math-pro' )
);
}
/**
* [build_estimated_cost description]
*
* @param [type] $attrs [description].
*
* @return [type] [description]
*/
private function build_tools( $attrs ) {
if ( empty( $attrs['tools'] ) ) {
return;
}
$tools = Str::to_arr_no_empty( $attrs['tools'] );
if ( empty( $tools ) ) {
return;
}
return sprintf(
'<p class="rank-math-howto-tools"><strong>%2$s</strong> <ul><li>%1$s</li></ul></p>',
implode( '</li><li>', $tools ),
__( 'Tools:', 'rank-math-pro' )
);
}
/**
* [build_estimated_cost description]
*
* @param [type] $attrs [description].
*
* @return [type] [description]
*/
private function build_materials( $attrs ) {
if ( empty( $attrs['material'] ) ) {
return;
}
$tools = Str::to_arr_no_empty( $attrs['tools'] );
if ( empty( $tools ) ) {
return;
}
return sprintf(
'<p class="rank-math-howto-tools"><strong>%2$s</strong> <span>%1$s</span></p>',
$attrs['material'],
__( 'Materials:', 'rank-math-pro' )
);
}
/**
* Add Duration.
*
* @param [type] $data [description].
* @param [type] $attrs [description].
*/
private function add_estimated_cost( &$data, $attrs ) {
if ( empty( $attrs['estimatedCost'] ) ) {
return;
}
$data['estimatedCost'] = [
'@type' => 'MonetaryAmount',
'currency' => ! empty( $attrs['estimatedCostCurrency'] ) ? $attrs['estimatedCostCurrency'] : 'USD',
'value' => $attrs['estimatedCost'],
];
}
/**
* Add Duration.
*
* @param [type] $data [description].
* @param [type] $attrs [description].
*/
private function add_supplies( &$data, $attrs ) {
if ( empty( $attrs['supply'] ) ) {
return;
}
$supplies = Str::to_arr_no_empty( $attrs['supply'] );
if ( empty( $supplies ) ) {
return;
}
$supply = [];
foreach ( $supplies as $value ) {
$supply[] = [
'@type' => 'HowToSupply',
'name' => $value,
];
}
$data['supply'] = $supply;
}
/**
* Add Duration.
*
* @param [type] $data [description].
* @param [type] $attrs [description].
*/
private function add_tools( &$data, $attrs ) {
if ( empty( $attrs['tools'] ) ) {
return;
}
$tools = Str::to_arr_no_empty( $attrs['tools'] );
if ( empty( $tools ) ) {
return;
}
$tool = [];
foreach ( $tools as $value ) {
$tool[] = [
'@type' => 'HowToTool',
'name' => $value,
];
}
$data['tool'] = $tool;
}
/**
* Add Duration.
*
* @param [type] $data [description].
* @param [type] $attrs [description].
*/
private function add_materials( &$data, $attrs ) {
if ( empty( $attrs['material'] ) ) {
return;
}
$data['material'] = $attrs['material'];
}
}

View File

@@ -0,0 +1,145 @@
<?php
/**
* Outputs specific schema code from Schema Template
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Schema;
use RankMath\Traits\Hooker;
use RankMath\Schema\Snippet_Shortcode;
defined( 'ABSPATH' ) || exit;
/**
* Schema Frontend class.
*/
class Snippet_Pro_Shortcode extends Snippet_Shortcode {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->filter( 'rank_math/snippet/html', 'add_shortcode_view', 10, 4 );
$this->filter( 'rank_math/snippet/after_schema_content', 'show_review_notes' );
$this->filter( 'shortcode_atts_rank_math_rich_snippet', 'register_fields_attribute', 10, 4 );
$this->filter( 'rank_math/schema/shortcode/filter_attributes', 'filter_attributes', 10, 2 );
}
/**
* Filter schema fields.
*
* @param array $schema Schema data.
* @param array $atts The user defined shortcode attributes.
*
* @return array Filtered Schema fields.
*/
public function filter_attributes( $schema, $atts ) {
if ( empty( $atts['fields'] ) ) {
return $schema;
}
$fields = explode( ',', $atts['fields'] );
$fields[] = '@type';
return array_intersect_key( $schema, array_flip( $fields ) );
}
/**
* Filters shortcode attributes.
*
* If the third parameter of the shortcode_atts() function is present then this filter is available.
*
* @param array $out The output array of shortcode attributes.
* @param array $pairs The supported attributes and their defaults.
* @param array $atts The user defined shortcode attributes.
* @param string $shortcode The shortcode name.
*/
public function register_fields_attribute( $out, $pairs, $atts, $shortcode ) { // phpcs:ignore
return wp_parse_args( $atts, $out );
}
/**
* Filter to change the rank_math_rich_snippet shortcode content.
*
* @param string $html Shortcode content.
* @param string $schema Current Schema data.
* @param string $post Current Post Object.
* @param string $shortcode Shortcode class instance.
*
* @return string Shortcode Content.
*/
public function add_shortcode_view( $html, $schema, $post, $shortcode ) { // phpcs:ignore
wp_enqueue_style( 'rank-math-review-pro-snippet', RANK_MATH_PRO_URL . 'includes/modules/schema/assets/css/rank-math-snippet.css', null, rank_math_pro()->version );
$type = \strtolower( $schema['@type'] );
if ( ! in_array( $type, [ 'dataset', 'movie', 'claimreview', 'faqpage', 'howto', 'jobposting', 'product', 'recipe', 'podcastepisode' ], true ) ) {
return $html;
}
ob_start();
echo '<div id="rank-math-rich-snippet-wrapper">';
include "shortcode/$type.php";
$this->show_review_notes( $shortcode );
echo '</div>';
return ob_get_clean();
}
/**
* Display Pros & Cons.
*
* @since 3.0.18
*/
public function show_review_notes( $shortcode ) {
$labels = [
'pros' => __( 'Pros', 'rank-math-pro' ),
'cons' => __( 'Cons', 'rank-math-pro' ),
];
/**
* Filter: Allow changing the Pros & Cons labels.
*
* @param array $labels {
* @type string $pros Pros label.
* @type string $cons Cons label.
* }
*/
$labels = $this->do_filter( 'schema/review_notes_labels', $labels );
$positive_notes = ! empty( $shortcode->get_field_value( 'positiveNotes' ) ) ? $shortcode->get_field_value( 'positiveNotes' ) : $shortcode->get_field_value( 'review.positiveNotes' );
if ( ! empty( $positive_notes['itemListElement'] ) ) {
?>
<div class="rank-math-review-notes rank-math-review-pros">
<h4><?php echo esc_html( $labels['pros'] ); ?></h4>
<ul>
<?php foreach ( $positive_notes['itemListElement'] as $positive_note ) { ?>
<li><?php echo esc_html( $positive_note['name'] ); ?></li>
<?php } ?>
</ul>
</div>
<?php
}
$negative_notes = ! empty( $shortcode->get_field_value( 'negativeNotes' ) ) ? $shortcode->get_field_value( 'negativeNotes' ) : $shortcode->get_field_value( 'review.negativeNotes' );
if ( ! empty( $negative_notes['itemListElement'] ) ) {
?>
<div class="rank-math-review-notes rank-math-review-cons">
<h4><?php echo esc_html( $labels['cons'] ); ?></h4>
<ul>
<?php foreach ( $negative_notes['itemListElement'] as $negative_note ) { ?>
<li><?php echo esc_html( $negative_note['name'] ); ?></li>
<?php } ?>
</ul>
</div>
<?php
}
}
}

View File

@@ -0,0 +1,255 @@
<?php
/**
* The Schema Module
*
* @since 2.1.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Schema;
use RankMath\Helper;
use RankMath\Admin\Admin_Helper;
use RankMath\Schema\DB;
use RankMath\Rest\Sanitize;
use RankMath\Traits\Hooker;
use MyThemeShop\Helpers\Arr;
use MyThemeShop\Helpers\Str;
use MyThemeShop\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* Taxonomy class.
*/
class Taxonomy extends Admin {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'init', 'init', 9999 );
$this->action( 'rank_math/json_ld', 'add_schema', 11, 2 );
if (
! Helper::has_cap( 'onpage_snippet' ) ||
! Admin_Helper::is_term_edit() ||
! $this->can_add( Param::request( 'taxonomy' ) )
) {
return;
}
$this->action( 'rank_math/admin/editor_scripts', 'enqueue' );
}
/**
* Add schema-preview rewrite rule for taxonomies.
*/
public function init() {
$taxonomies = Helper::get_accessible_taxonomies();
foreach ( $taxonomies as $slug => $taxonomy ) {
if ( empty( $taxonomy->rewrite['slug'] ) || ! $this->can_add( $slug ) ) {
continue;
}
add_rewrite_rule(
$taxonomy->rewrite['slug'] . '/(.+?)/schema-preview/?$',
'index.php?' . $slug . '=$matches[1]&schema-preview=$matches[3]',
'top'
);
}
}
/**
* Enqueue Styles and Scripts required for metabox.
*/
public function enqueue() {
global $pagenow;
$cmb = cmb2_get_metabox( 'rank_math_metabox' );
if ( false === $cmb || 'edit-tags.php' === $pagenow ) {
return;
}
$schemas = $this->get_schema_data( $cmb->object_id() );
Helper::add_json( 'schemas', $schemas );
Helper::add_json( 'customSchemaImage', esc_url( rank_math()->plugin_url() . 'includes/modules/schema/assets/img/custom-schema-builder.jpg' ) );
Helper::add_json( 'postLink', get_term_link( (int) $cmb->object_id() ) );
Helper::add_json( 'activeTemplates', $this->get_active_templates() );
wp_enqueue_style( 'rank-math-schema', rank_math()->plugin_url() . 'includes/modules/schema/assets/css/schema.css', [ 'wp-components', 'rank-math-editor' ], rank_math()->version );
wp_enqueue_script( 'rank-math-schema', rank_math()->plugin_url() . 'includes/modules/schema/assets/js/schema-gutenberg.js', [ 'rank-math-editor', 'clipboard' ], rank_math()->version, true );
wp_enqueue_style( 'rank-math-schema-pro', RANK_MATH_PRO_URL . 'includes/modules/schema/assets/css/schema.css', null, rank_math_pro()->version );
wp_enqueue_script(
'rank-math-pro-schema-filters',
RANK_MATH_PRO_URL . 'includes/modules/schema/assets/js/schemaFilters.js',
[
'wp-plugins',
'wp-components',
'wp-hooks',
'wp-api-fetch',
'lodash',
],
rank_math_pro()->version,
true
);
wp_enqueue_script( 'rank-math-schema-pro', RANK_MATH_PRO_URL . 'includes/modules/schema/assets/js/schema.js', [ 'rank-math-editor' ], rank_math_pro()->version, true );
}
/**
* Get Default Schema Data.
*
* @param array $data Array of json-ld data.
* @param JsonLD $jsonld Instance of jsonld.
*
* @return array
*/
public function add_schema( $data, $jsonld ) {
if ( ! is_category() && ! is_tag() && ! is_tax() ) {
return $data;
}
$queried_object = get_queried_object();
if (
empty( $queried_object ) ||
is_wp_error( $queried_object ) ||
! $this->can_add( $queried_object->taxonomy )
) {
return $data;
}
$schemas = DB::get_schemas( $queried_object->term_id, 'termmeta' );
if ( empty( $schemas ) ) {
return $data;
}
if ( isset( $data['ItemList'] ) ) {
unset( $data['ItemList'] );
}
if ( isset( $data['ProductsPage'] ) ) {
unset( $data['ProductsPage'] );
}
if ( isset( $data['WebPage'] ) ) {
$data['WebPage']['@type'] = [
'WebPage',
'CollectionPage',
];
}
$schemas = $jsonld->replace_variables( $schemas );
$schemas = $jsonld->filter( $schemas, $jsonld, $data );
return array_merge( $data, $schemas );
}
/**
* Get Schema Data.
*
* @param int $term_id Term ID.
*
* @return array $schemas Schema Data.
*/
private function get_schema_data( $term_id ) {
$schemas = DB::get_schemas( $term_id, 'termmeta' );
if ( ! empty( $schemas ) || metadata_exists( 'term', $term_id, 'rank_math_rich_snippet' ) ) {
return $schemas;
}
return [];
}
/**
* Add active templates to the schemas json
*
* @return array
*/
public function get_active_templates() {
$templates = $this->get_schema_templates();
if ( empty( $templates ) ) {
return [];
}
$screen = get_current_screen();
$schemas = [];
foreach ( $templates as $template ) {
if ( empty( $template['schema']['metadata']['displayConditions'] ) ) {
continue;
}
$conditions = $template['schema']['metadata']['displayConditions'];
$can_add = false;
$data = [];
foreach ( $conditions as $condition ) {
$category = $condition['category'];
if ( 'archive' !== $category ) {
continue;
}
$operator = $condition['condition'];
if ( ! empty( $data[ $category ] ) && 'exclude' !== $operator ) {
continue;
}
$type = $condition['type'];
$value = $condition['value'];
if ( 'general' === $category ) {
$data[ $category ] = 'include' === $operator;
continue;
}
if ( $value && absint( Param::get( 'tag_ID' ) ) === $value ) {
$data[ $category ] = 'include' === $operator;
break;
}
if ( 'all' === $type ) {
$data[ $category ] = 'include' === $operator;
} elseif ( $type !== $screen->taxonomy ) {
$data[ $category ] = false;
} elseif ( ! $value ) {
$data[ $category ] = 'include' === $operator;
} elseif ( Param::get( 'tag_ID' ) !== $value ) {
$data[ $category ] = isset( $data[ $category ] ) ? $data[ $category ] : false;
} else {
$data[ $category ] = 'include' === $operator;
}
}
if ( isset( $data['archive'] ) ) {
$can_add = $data['archive'];
} else {
$can_add = ! empty( $data['general'] );
}
if ( $can_add ) {
$template['schema']['isTemplate'] = true;
$schemas[ $template['id'] ] = $template['schema'];
}
}
return $schemas;
}
/**
* Can add Schema data on current taxonomy
*
* @param string $taxonomy Taxonomy name.
*
* @return bool
*/
private function can_add( $taxonomy ) {
return Helper::get_settings( 'titles.tax_' . $taxonomy . '_add_meta_box' ) &&
true !== apply_filters(
'rank_math/snippet/remove_taxonomy_data',
Helper::get_settings( 'titles.remove_' . $taxonomy . '_snippet_data' ),
$taxonomy
);
}
}

View File

@@ -0,0 +1,155 @@
<?php
/**
* The Video Schema generator.
*
* @since 2.0.9
* @package RankMathPro
* @subpackage RankMathPro\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Schema;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Video_Schema_Generator class.
*/
class Video_Schema_Generator extends \WP_Background_Process {
/**
* Prefix.
*
* (default value: 'wp')
*
* @var string
* @access protected
*/
protected $prefix = 'rank_math';
/**
* Action.
*
* @var string
*/
protected $action = 'add_video_schema';
/**
* Main instance
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Video_Schema_Generator
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Video_Schema_Generator ) ) {
$instance = new Video_Schema_Generator();
}
return $instance;
}
/**
* Start creating batches.
*
* @param [type] $posts [description].
*/
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() {
delete_option( 'rank_math_video_posts' );
Helper::add_notification(
esc_html__( 'Rank Math: Added Video Schema to posts successfully.', 'rank-math-pro' ),
[
'type' => 'success',
'id' => 'rank_math_video_posts',
'classes' => 'rank-math-notice',
]
);
parent::complete();
}
/**
* Task to perform
*
* @param array $posts Posts to process.
*
* @return bool
*/
protected function task( $posts ) {
try {
foreach ( $posts as $post ) {
$this->convert( $post );
}
return false;
} catch ( Exception $error ) {
return true;
}
}
/**
* Convert post.
*
* @param int $post_id Post ID.
*/
public function convert( $post_id ) {
update_post_meta( $post_id, '_rank_math_video', 'true' );
( new Video\Parser( get_post( $post_id ) ) )->save();
}
/**
* Find posts.
*
* @return array
*/
public function find_posts() {
global $wpdb;
$posts = get_option( 'rank_math_video_posts' );
if ( false !== $posts ) {
return $posts;
}
// Schema Posts.
$post_types = array_filter(
Helper::get_accessible_post_types(),
function( $post_type ) {
return 'attachment' !== $post_type && Helper::get_settings( "titles.pt_{$post_type}_autodetect_video", 'on' );
}
);
$posts = get_posts(
[
'post_type' => array_keys( $post_types ),
'numberposts' => -1,
'fields' => 'ids',
'meta_query' => [
[
'key' => 'rank_math_schema_VideoObject',
'compare' => 'NOT EXISTS',
],
],
]
);
update_option( 'rank_math_video_posts', $posts, false );
return $posts;
}
}

View File

@@ -0,0 +1,222 @@
<?php
/**
* The Video Schema.
*
* @since 1.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMathPro\Schema;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use MyThemeShop\Helpers\Conditional;
defined( 'ABSPATH' ) || exit;
/**
* Video class.
*/
class Video {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
Video_Schema_Generator::get();
if ( Conditional::is_rest() ) {
$this->filter( 'rank_math/tools/generate_video_schema', 'generate_video_schema' );
}
$this->action( 'rank_math/pre_update_metadata', 'detect_video_in_content', 10, 3 );
if ( is_admin() ) {
$this->action( 'cmb2_admin_init', 'add_video_settings' );
$this->action( 'rank_math/admin/settings/others', 'add_media_rss_field' );
$this->filter( 'rank_math/database/tools', 'generate_video_schema_tool' );
return;
}
$this->action( 'rank_math/opengraph/facebook', 'add_video_tags', 99 );
new Media_RSS();
}
/**
* Add auto-detect Video fields in Titles & Meta settings.
*/
public function add_video_settings() {
foreach ( Helper::get_accessible_post_types() as $post_type ) {
$this->action( "rank_math/admin/settings/post-type-{$post_type}", 'add_video_schema_fields', 10, 2 );
}
}
/**
* Add auto-generate video schema settings.
*
* @param object $cmb CMB2 instance.
* @param array $tab Current settings tab.
*/
public function add_video_schema_fields( $cmb, $tab ) {
if ( 'attachment' === $tab['post_type'] ) {
return;
}
$field_ids = wp_list_pluck( $cmb->prop( 'fields' ), 'id' );
$field_position = array_search( "pt_{$tab['post_type']}_default_article_type", array_keys( $field_ids ), true ) + 1;
$cmb->add_field(
[
'id' => 'pt_' . $tab['post_type'] . '_autodetect_video',
'type' => 'toggle',
'name' => esc_html__( 'Autodetect Video', 'rank-math-pro' ),
'desc' => esc_html__( 'Populate automatic Video Schema by auto-detecting any video in the content.', 'rank-math-pro' ),
'options' => [
'off' => esc_html__( 'Default', 'rank-math-pro' ),
'on' => esc_html__( 'Custom', 'rank-math-pro' ),
],
'default' => 'on',
],
++$field_position
);
$cmb->add_field(
[
'id' => 'pt_' . $tab['post_type'] . '_autogenerate_image',
'type' => 'toggle',
'name' => esc_html__( 'Autogenerate Image', 'rank-math-pro' ),
'desc' => esc_html__( 'Auto-generate image for the auto detected video.', 'rank-math-pro' ),
'options' => [
'off' => esc_html__( 'Default', 'rank-math-pro' ),
'on' => esc_html__( 'Custom', 'rank-math-pro' ),
],
'default' => 'off',
'dep' => [ [ 'pt_' . $tab['post_type'] . '_autodetect_video', 'on' ] ],
],
++$field_position
);
}
/**
* Add new settings.
*
* @param object $cmb CMB2 instance.
*/
public function add_media_rss_field( $cmb ) {
$field_ids = wp_list_pluck( $cmb->prop( 'fields' ), 'id' );
$field_position = array_search( 'rss_after_content', array_keys( $field_ids ), true ) + 1;
$cmb->add_field(
[
'id' => 'disable_media_rss',
'type' => 'toggle',
'name' => esc_html__( 'Remove Media Data from RSS feed', 'rank-math-pro' ),
'desc' => esc_html__( 'Remove Media Data from RSS feed', 'rank-math-pro' ),
'options' => [
'off' => esc_html__( 'Default', 'rank-math-pro' ),
'on' => esc_html__( 'Custom', 'rank-math-pro' ),
],
'default' => 'off',
],
++$field_position
);
}
/**
* Output the video tags.
*
* @link https://yandex.com/support/video/partners/open-graph.html#player
*
* @param OpenGraph $opengraph The current opengraph network object.
*/
public function add_video_tags( $opengraph ) {
if ( ! is_singular() ) {
return;
}
global $post;
$video_data = get_post_meta( $post->ID, 'rank_math_schema_VideoObject', true );
if ( empty( $video_data ) ) {
return;
}
$tags = [
'ya:ovs:adult' => ! empty( $video_data['isFamilyFriendly'] ) ? false : true,
'ya:ovs:upload_date' => ! empty( $video_data['uploadDate'] ) ? Helper::replace_vars( $video_data['uploadDate'], $post ) : '',
'ya:ovs:allow_embed' => ! empty( $video_data['embedUrl'] ) ? 'true' : 'false',
];
foreach ( $tags as $tag => $value ) {
$opengraph->tag( $tag, $value );
}
}
/**
* Automatically add Video Schema when post is updated.
*
* @param int $object_id Object ID.
* @param int $object_type Object type.
* @param string $content Updated post content.
*/
public function detect_video_in_content( $object_id, $object_type, $content = '' ) {
if ( 'post' !== $object_type ) {
return;
}
$post = get_post( $object_id );
if ( $content ) {
$post->post_content = $content;
}
( new Video\Parser( $post ) )->save();
}
/**
* Add database tools.
*
* @param array $tools Array of tools.
*
* @return array
*/
public function generate_video_schema_tool( $tools ) {
$posts = Video_Schema_Generator::get()->find_posts();
if ( empty( $posts ) ) {
return $tools;
}
$generate_video_schema = [
'generate_video_schema' => [
'title' => esc_html__( 'Generate Video Schema for Old Posts/Pages', 'rank-math-pro' ),
'description' => esc_html__( 'Add Video schema to posts which have YouTube or Vimeo Video in the content. Applies to only those Posts/Pages/CPTs in which Autodetect Video Option is On.', 'rank-math-pro' ),
'confirm_text' => esc_html__( 'Are you sure you want to add Video Schema to the posts/pages with the video in the content? This action is irreversible.', 'rank-math-pro' ),
'button_text' => esc_html__( 'Generate', 'rank-math-pro' ),
],
];
$index = array_search( 'recreate_tables', array_keys( $tools ), true );
$pos = false === $index ? count( $tools ) : $index + 1;
$tools = array_slice( $tools, 0, $pos, true ) + $generate_video_schema + array_slice( $tools, $pos, count( $tools ) - 1, true );
return $tools;
}
/**
* Detect Video in the content and add schema.
*
* @return string
*/
public function generate_video_schema() {
$posts = Video_Schema_Generator::get()->find_posts();
if ( empty( $posts ) ) {
return esc_html__( 'No posts found to convert.', 'rank-math-pro' );
}
Video_Schema_Generator::get()->start( $posts );
return esc_html__( 'Conversion started. A success message will be shown here once the process completes. You can close this page.', 'rank-math-pro' );
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* Shortcode - course
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
$shortcode->get_description( $schema['claimReviewed'] );
$shortcode->get_image();
?>
<div class="rank-math-review-data">
<?php
$shortcode->get_field(
esc_html__( 'URL', 'rank-math-pro' ),
'url'
);
?>
<?php
$shortcode->get_field(
esc_html__( 'Author Name', 'rank-math-pro' ),
'itemReviewed.author.name'
);
?>
<?php
$shortcode->get_field(
esc_html__( 'Published Date', 'rank-math-pro' ),
'itemReviewed.datePublished'
);
?>
<?php
$shortcode->get_field(
esc_html__( 'Appearance Headline', 'rank-math-pro' ),
'itemReviewed.appearance.headline'
);
?>
<?php
$shortcode->get_field(
esc_html__( 'Appearance URL', 'rank-math-pro' ),
'itemReviewed.appearance.url'
);
?>
<?php
$shortcode->get_field(
esc_html__( 'Appearance Author', 'rank-math-pro' ),
'itemReviewed.appearance.author.name'
);
?>
<?php
$shortcode->get_field(
esc_html__( 'Appearance Published Date', 'rank-math-pro' ),
'itemReviewed.appearance.datePublished'
);
?>
<?php
$shortcode->get_field(
esc_html__( 'Alternate Name', 'rank-math-pro' ),
'reviewRating.alternateName'
);
?>
<?php $shortcode->show_ratings( 'reviewRating.ratingValue' ); ?>
</div>

View File

@@ -0,0 +1,120 @@
<?php
/**
* Shortcode - course
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
$shortcode->get_title();
$shortcode->get_image();
?>
<div class="rank-math-review-data">
<?php $shortcode->get_description(); ?>
<?php
$shortcode->get_field(
esc_html__( 'URL', 'rank-math-pro' ),
'url'
);
?>
<?php
$shortcode->get_field(
esc_html__( 'Reference Web page ', 'rank-math-pro' ),
'sameAs'
);
?>
<?php
$identifier = $shortcode->get_field_value( 'identifier' );
if ( ! empty( $identifier ) ) {
$identifiers = explode( PHP_EOL, $identifier );
$shortcode->output_field(
esc_html__( 'Identifier', 'rank-math-pro' ),
'<ul><li>' . join( '</li><li>', $identifiers ) . '</li></ul>'
);
}
?>
<?php
$keyword = $shortcode->get_field_value( 'keywords' );
if ( ! empty( $keyword ) ) {
$keywords = explode( PHP_EOL, $keyword );
$shortcode->output_field(
esc_html__( 'Keywords', 'rank-math-pro' ),
'<ul><li>' . join( '</li><li>', $keywords ) . '</li></ul>'
);
}
?>
<?php
$shortcode->get_field(
esc_html__( 'License', 'rank-math-pro' ),
'license'
);
?>
<?php
$shortcode->get_field(
esc_html__( 'Catalog', 'rank-math-pro' ),
'includedInDataCatalog.name'
);
?>
<?php
$shortcode->get_field(
esc_html__( 'Temporal Coverage', 'rank-math-pro' ),
'temporalCoverage'
);
?>
<?php
$shortcode->get_field(
esc_html__( 'Special Coverage', 'rank-math-pro' ),
'spatialCoverage'
);
?>
<?php
$data_sets = $shortcode->get_field_value( 'hasPart' );
$labels = [
'name' => esc_html__( 'Name', 'rank-math-pro' ),
'description' => esc_html__( 'Description', 'rank-math-pro' ),
'license' => esc_html__( 'License', 'rank-math-pro' ),
];
if ( ! empty( $data_sets ) ) {
echo '<h3>' . esc_html__( 'Data Sets', 'rank-math-pro' ) . '</h3>';
foreach ( $data_sets as $data_set ) {
echo '<div>';
foreach ( $labels as $key => $label ) {
echo "<p><strong>{$label}</strong>: {$data_set[$key]}</p>"; // phpcs:ignore
}
echo '</div>';
}
}
?>
<?php
$distributions = $shortcode->get_field_value( 'distribution' );
$labels = [
'encodingFormat' => esc_html__( 'Format', 'rank-math-pro' ),
'contentUrl' => esc_html__( 'URL', 'rank-math-pro' ),
];
if ( ! empty( $distributions ) ) {
echo '<h3>' . esc_html__( 'Distribution', 'rank-math-pro' ) . '</h3>';
foreach ( $distributions as $distribution ) {
echo '<div>';
foreach ( $labels as $key => $label ) {
echo "<p><strong>{$label}</strong>: {$distribution[$key]}</p>"; // phpcs:ignore
}
echo '</div>';
}
}
?>
</div>

View File

@@ -0,0 +1,39 @@
<?php
/**
* Shortcode - FAQPage
*
* @package RankMath
* @subpackage RankMath\Schema
*/
use RankMath\Schema\Block_FAQ;
defined( 'ABSPATH' ) || exit;
if ( empty( $schema['mainEntity'] ) ) {
return;
}
$attributes = [
'questions' => [],
'listStyle' => '',
'titleWrapper' => 'h3',
'sizeSlug' => 'thumbnail',
'listCssClasses' => '',
'titleCssClasses' => '',
'contentCssClasses' => '',
'textAlign' => 'left',
];
foreach ( $schema['mainEntity'] as $index => $main_entity ) {
$attributes['questions'][] = [
'id' => 'faq-' . ( $index + 1 ),
'title' => $main_entity['name'],
'content' => $main_entity['acceptedAnswer']['text'],
'visible' => 1,
'imageID' => 0,
'imageUrl' => isset( $main_entity['image'] ) ? $main_entity['image'] : '',
];
}
echo Block_FAQ::markup( $attributes ); // phpcs:ignore

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