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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#6c7781"><g fill="none" fill-rule="evenodd"><g transform="translate(1 1)" stroke-width="2"><circle stroke-opacity=".5" cx="18" cy="18" r="18"/><path d="M36 18c0-9.94-8.06-18-18-18"><animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="1s" repeatCount="indefinite"/></path></g></g></svg>

After

Width:  |  Height:  |  Size: 417 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "rank-math/command",
"title": "AI Assistant [Content AI]",
"icon": "star",
"category": "rank-math-blocks",
"description": "Generate content without any hassle, powered by Rank Math's Content AI.",
"keywords": [ "ai", "content", "rank math", "rankmath", "content ai", "seo" ],
"textdomain": "rank-math-pro",
"editorScript": "file:../js/index.js",
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "p",
"default": "<span>//</span>",
"__experimentalRole": "content"
}
}
}

View File

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

View File

@@ -0,0 +1,21 @@
/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks'
/**
* Internal dependencies
*/
import edit from './edit'
import metadata from './block.json'
const { name } = metadata
export { metadata, name }
export const settings = {
icon: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17.08 18.02"><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M16.65,12.08a.83.83,0,0,0-1.11.35A7.38,7.38,0,1,1,9,1.63a7.11,7.11,0,0,1,.92.06A2.52,2.52,0,0,1,11,.23,8.87,8.87,0,0,0,9,0a9,9,0,1,0,8,13.19A.83.83,0,0,0,16.65,12.08Z"/><path d="M7.68,7.29A1.58,1.58,0,0,0,6.2,8.94a1.57,1.57,0,0,0,1.48,1.64A1.56,1.56,0,0,0,9.16,8.94,1.57,1.57,0,0,0,7.68,7.29Z" /><path d="M13.34,4.71a2.45,2.45,0,0,1-1,.2A2.53,2.53,0,0,1,9.93,3,7.18,7.18,0,0,0,9.12,3a6,6,0,1,0,4.22,1.73ZM10.53,11.3a.75.75,0,1,1-1.5,0v-.06a2.4,2.4,0,0,1-1.66.69,2.81,2.81,0,0,1-2.58-3,2.82,2.82,0,0,1,2.58-3A2.39,2.39,0,0,1,9,6.64a.75.75,0,0,1,1.5.07Zm2.56,0a.75.75,0,1,1-1.5,0V6.71a.75.75,0,1,1,1.5,0Z" /><circle cx="12.42" cy="2.37" r="1.45" /></g></g></svg>,
edit,
}
registerBlockType( { name, ...metadata }, settings )

View File

@@ -0,0 +1,65 @@
<?php
/**
* The TOC Block
*
* @since 1.0.104
* @package RankMath
* @subpackage RankMath\ContentAI
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use WP_Block_Type_Registry;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Content AI Command Block class.
*/
class Block_Command {
use Hooker;
/**
* Block type name.
*
* @var string
*/
private $block_type = 'rank-math/command';
/**
* The single instance of the class.
*
* @var Block_Command
*/
protected static $instance = null;
/**
* Retrieve main Block_Command instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Block_Command
*/
public static function get() {
if ( is_null( self::$instance ) && ! ( self::$instance instanceof Block_Command ) ) {
self::$instance = new Block_Command();
}
return self::$instance;
}
/**
* The Constructor.
*/
public function __construct() {
if ( WP_Block_Type_Registry::get_instance()->is_registered( $this->block_type ) ) {
return;
}
register_block_type( RANK_MATH_PATH . 'includes/modules/content-ai/blocks/command/assets/src/block.json' );
}
}

View File

@@ -0,0 +1,223 @@
<?php
/**
* The Content AI module.
*
* @since 1.0.219
* @package RankMath
* @subpackage RankMath
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Helpers\Arr;
use RankMath\CMB2;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Admin class.
*/
class Admin {
use Hooker;
/**
* Content_AI object.
*
* @var object
*/
public $content_ai;
/**
* Class constructor.
*
* @param Object $content_ai Content_AI class object.
*/
public function __construct( $content_ai ) {
if ( ! is_admin() ) {
return;
}
$this->content_ai = $content_ai;
$this->filter( 'rank_math/analytics/post_data', 'add_contentai_data' );
$this->filter( 'rank_math/settings/general', 'add_settings' );
$this->action( 'cmb2_admin_init', 'add_content_ai_metabox', 11 );
$this->action( 'rank_math/deregister_site', 'remove_credits_data' );
$this->filter( 'rank_math/status/rank_math_info', 'content_ai_info' );
$this->action( 'rank_math/connect/account_connected', 'refresh_content_ai_credits' );
}
/**
* Add Content AI score in Single Page Site Analytics.
*
* @param array $data array.
* @return array $data sorted array.
*/
public function add_contentai_data( $data ) {
$post_id = $data['object_id'];
$content_ai_data = Helper::get_post_meta( 'contentai_score', $post_id );
$content_ai_score = ! empty( $content_ai_data ) ? round( array_sum( array_values( $content_ai_data ) ) / count( $content_ai_data ) ) : 0;
$data['contentAiScore'] = absint( $content_ai_score );
return $data;
}
/**
* Remove credits data when site is disconnected.
*/
public function remove_credits_data() {
delete_option( 'rank_math_ca_credits' );
}
/**
* Add module settings in the General Settings panel.
*
* @param array $tabs Array of option panel tabs.
* @return array
*/
public function add_settings( $tabs ) {
Arr::insert(
$tabs,
[
'content-ai' => [
'icon' => 'rm-icon rm-icon-content-ai',
'title' => esc_html__( 'Content AI', 'rank-math' ),
/* translators: Link to kb article */
'desc' => sprintf( esc_html__( 'Get sophisticated AI suggestions for related Keywords, Questions & Links to include in the SEO meta & Content Area. %s.', 'rank-math' ), '<a href="' . KB::get( 'content-ai-settings', 'Options Panel Content AI Tab' ) . '" target="_blank">' . esc_html__( 'Learn more', 'rank-math' ) . '</a>' ),
'file' => __DIR__ . '/views/options.php',
'json' => [
'countries' => Helper::choices_contentai_countries(),
'credits' => Helper::get_credits(),
'refreshDate' => Helper::get_content_ai_refresh_date(),
'tone' => Helper::choices_contentai_tone(),
'audience' => Helper::choices_contentai_audience(),
'language' => Helper::choices_contentai_language(),
],
],
],
8
);
return $tabs;
}
/**
* Add link suggestion metabox.
*/
public function add_content_ai_metabox() {
if ( ! $this->content_ai->can_add_tab() || 'classic' !== Helper::get_current_editor() ) {
return;
}
$id = 'rank_math_metabox_content_ai';
$cmb = new_cmb2_box(
[
'id' => $id,
'title' => esc_html__( 'Content AI', 'rank-math' ),
'object_types' => array_keys( Helper::get_accessible_post_types() ),
'context' => 'side',
'priority' => 'high',
'mb_callback_args' => [ '__block_editor_compatible_meta_box' => false ],
]
);
CMB2::pre_init( $cmb );
// Move content AI metabox below the Publish box.
$this->reorder_content_ai_metabox( $id );
}
/**
* Add Content AI details in System Info
*
* @param array $rankmath Array of rankmath.
*/
public function content_ai_info( $rankmath ) {
$refresh_date = Helper::get_content_ai_refresh_date();
$content_ai = [
'ca_plan' => [
'label' => esc_html__( 'Content AI Plan', 'rank-math' ),
'value' => \ucwords( Helper::get_content_ai_plan() ),
],
'ca_credits' => [
'label' => esc_html__( 'Content AI Credits', 'rank-math' ),
'value' => Helper::get_content_ai_credits(),
],
'ca_refresh_date' => [
'label' => esc_html__( 'Content AI Refresh Date', 'rank-math' ),
'value' => $refresh_date ? wp_date( 'Y-m-d g:i a', $refresh_date ) : '',
],
];
array_splice( $rankmath['fields'], 3, 0, $content_ai );
return $rankmath;
}
/**
* Refresh Content AI credits when account is connected.
*
* @return void
*/
public function refresh_content_ai_credits() {
Helper::get_content_ai_credits( true );
}
/**
* Reorder the Content AI metabox in Classic editor.
*
* @param string $id Metabox ID.
* @return void
*/
private function reorder_content_ai_metabox( $id ) {
$post_type = Helper::get_post_type();
if ( ! $post_type ) {
return;
}
$user = wp_get_current_user();
$order = (array) get_user_option( 'meta-box-order_' . $post_type, $user->ID );
if ( ! empty( $order['normal'] ) && false !== strpos( $order['normal'], $id ) ) {
return;
}
$order['side'] = ! isset( $order['side'] ) ? '' : $order['side'];
if ( false !== strpos( $order['side'], $id ) ) {
return;
}
if ( false === strpos( $order['side'], 'submitdiv' ) ) {
$order['side'] = 'submitdiv,' . $order['side'];
}
if ( ',' === substr( $order['side'], -1 ) ) {
$order['side'] = substr( $order['side'], 0, -1 );
}
$current_order = [];
$current_order = explode( ',', $order['side'] );
$key = array_search( 'submitdiv', $current_order, true );
if ( false === $key ) {
return;
}
$new_order = array_merge(
array_slice( $current_order, 0, $key + 1 ),
[ $id ]
);
if ( count( $current_order ) > $key ) {
$new_order = array_merge(
$new_order,
array_slice( $current_order, $key + 1 )
);
}
$order['side'] = implode( ',', array_unique( $new_order ) );
update_user_option( $user->ID, 'meta-box-order_' . $post_type, $order, true );
}
}

View File

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

View File

@@ -0,0 +1,349 @@
<?php
/**
* Add Content AI Bulk Action options.
*
* @since 1.0.212
* @package RankMath
* @subpackage RankMath\Content_AI_Page
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use RankMath\Traits\Hooker;
use RankMath\Helper;
use RankMath\Helpers\Str;
use RankMath\Paper\Paper;
use RankMath\Admin\Admin_Helper;
use RankMath\Post;
use RankMath\Term;
defined( 'ABSPATH' ) || exit;
/**
* Bulk_Actions class.
*/
class Bulk_Actions {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'init', 'init' );
$this->action( 'admin_init', 'init_admin', 15 );
$this->action( 'rank_math/content_ai/generate_alt', 'generate_image_alt' );
$this->filter( 'rank_math/database/tools', 'add_tools' );
$this->filter( 'rank_math/tools/content_ai_cancel_bulk_edit_process', 'cancel_bulk_edit_process' );
}
/**
* Init function.
*/
public function init() {
Bulk_Edit_SEO_Meta::get();
Bulk_Image_Alt::get();
}
/**
* Init.
*/
public function init_admin() {
// Add Bulk actions for Posts.
$post_types = Helper::get_settings( 'general.content_ai_post_types', [] );
foreach ( $post_types as $post_type ) {
$this->filter( "bulk_actions-edit-{$post_type}", 'bulk_actions', 9 );
$this->filter( "handle_bulk_actions-edit-{$post_type}", 'handle_bulk_actions', 10, 3 );
}
// Add Bulk Generate on Attachment page.
$this->filter( 'bulk_actions-upload', 'bulk_actions_attachment' );
$this->filter( 'handle_bulk_actions-upload', 'handle_bulk_actions', 10, 3 );
// Add Bulk Actions for Taxonomies.
$taxonomies = Helper::get_accessible_taxonomies();
unset( $taxonomies['post_format'] );
$taxonomies = wp_list_pluck( $taxonomies, 'label', 'name' );
foreach ( $taxonomies as $taxonomy => $label ) {
$this->filter( "bulk_actions-edit-{$taxonomy}", 'bulk_actions' );
$this->filter( "handle_bulk_actions-edit-{$taxonomy}", 'handle_bulk_actions', 10, 3 );
}
$this->filter( 'wp_bulk_edit_seo_meta_post_args', 'update_background_process_args' );
$this->filter( 'wp_bulk_image_alt_post_args', 'update_background_process_args' );
}
/**
* Add bulk actions for applicable posts, pages, CPTs.
*
* @param array $actions Actions.
* @return array New actions.
*/
public function bulk_actions( $actions ) {
if ( ! Helper::has_cap( 'content_ai' ) ) {
return $actions;
}
$actions['rank_math_ai_options'] = __( '&#8595; Rank Math Content AI', 'rank-math' );
$actions['rank_math_content_ai_fetch_seo_title'] = esc_html__( 'Write SEO Title with AI', 'rank-math' );
$actions['rank_math_content_ai_fetch_seo_description'] = esc_html__( 'Write SEO Description with AI', 'rank-math' );
$actions['rank_math_content_ai_fetch_seo_title_description'] = esc_html__( 'Write SEO Title & Description with AI', 'rank-math' );
$actions['rank_math_content_ai_fetch_image_alt'] = esc_html__( 'Write Image Alt Text with AI', 'rank-math' );
return $actions;
}
/**
* Add bulk actions for Attachment.
*
* @param array $actions Actions.
* @return array New actions.
*/
public function bulk_actions_attachment( $actions ) {
if ( ! Helper::has_cap( 'content_ai' ) ) {
return $actions;
}
$actions['rank_math_ai_options'] = __( '&#8595; Rank Math Content AI', 'rank-math' );
$actions['rank_math_content_ai_fetch_image_alt'] = esc_html__( 'Write Image Alt Text with AI', 'rank-math' );
return $actions;
}
/**
* Handle bulk actions for applicable posts, pages, CPTs.
*
* @param string $redirect Redirect URL.
* @param string $doaction Performed action.
* @param array $object_ids Post IDs.
*
* @return string New redirect URL.
*/
public function handle_bulk_actions( $redirect, $doaction, $object_ids ) {
if ( empty( $object_ids ) || ! in_array( $doaction, [ 'rank_math_content_ai_fetch_seo_title', 'rank_math_content_ai_fetch_seo_description', 'rank_math_content_ai_fetch_seo_title_description', 'rank_math_content_ai_fetch_image_alt' ], true ) ) {
return $redirect;
}
if ( ! empty( get_option( 'rank_math_content_ai_posts' ) ) ) {
Helper::add_notification(
esc_html__( 'Another bulk editing process is already running. Please try again later after the existing process is complete.', 'rank-math' ),
[
'type' => 'warning',
'id' => 'rank_math_content_ai_posts_error',
'classes' => 'rank-math-notice',
]
);
return $redirect;
}
if ( 'rank_math_content_ai_fetch_image_alt' === $doaction ) {
$this->generate_image_alt( $object_ids );
return $redirect;
}
$action = 'both';
if ( 'rank_math_content_ai_fetch_seo_title' === $doaction ) {
$action = 'title';
}
if ( 'rank_math_content_ai_fetch_seo_description' === $doaction ) {
$action = 'description';
}
$is_post_list = Admin_Helper::is_post_list();
$data = [
'action' => $action,
'language' => Helper::get_settings( 'general.content_ai_language', Helper::content_ai_default_language() ),
'posts' => [],
'is_taxonomy' => ! $is_post_list,
];
$method = $is_post_list ? 'get_post_data' : 'get_term_data';
foreach ( $object_ids as $object_id ) {
$data['posts'][] = $this->$method( $object_id, $action );
}
Bulk_Edit_SEO_Meta::get()->start( $data );
return $redirect;
}
/**
* Generate Image Alt for the attachmed Ids.
*
* @param array $object_ids Attachment Ids.
*/
public function generate_image_alt( $object_ids ) {
$data = [
'action' => 'image_alt',
'posts' => [],
];
foreach ( $object_ids as $object_id ) {
if ( get_post_type( $object_id ) === 'attachment' ) {
$data['posts'][ $object_id ] = [ wp_get_attachment_url( $object_id ) ];
continue;
}
// Get all <img> tags from the post content.
$images = [];
preg_match_all( '/<img\\s[^>]+>/i', get_post_field( 'post_content', $object_id ), $images );
// Keep only the image tags that have src attribute but no alt attribute.
$images = array_filter(
$images[0],
function ( $image ) {
return preg_match( '/src=[\'"]?([^\'" >]+)[\'" >]/i', $image, $matches ) && ( ! preg_match( '/alt="([^"]*)"/i', $image, $matches ) || preg_match( '/alt=""/i', $image, $matches ) );
}
);
if ( empty( $images ) ) {
continue;
}
$object = get_post( $object_id );
$data['posts'][ $object_id ] = array_filter( array_values( $images ) );
}
Bulk_Image_Alt::get()->start( $data );
}
/**
* Change the timeout value in Background_Process to resolve the issue with notifications not appearing after completion in v1.2.
*
* @param array $args Process args.
*
* @return array
*/
public function update_background_process_args( $args ) {
$args['timeout'] = 0.01;
return $args;
}
/**
* Add database tools.
*
* @param array $tools Array of tools.
*
* @return array
*/
public function add_tools( $tools ) {
$posts = get_option( 'rank_math_content_ai_posts' );
// Early Bail if process is not running.
if ( empty( $posts ) ) {
return $tools;
}
$processed = get_option( 'rank_math_content_ai_posts_processed' );
$tools['content_ai_cancel_bulk_edit_process'] = [
'title' => esc_html__( 'Cancel Content AI Bulk Editing Process', 'rank-math' ),
'description' => sprintf(
// Translators: placeholders are the number of posts that were processed.
esc_html__( 'Terminate the ongoing Content AI Bulk Editing Process to halt any pending modifications and revert to the previous state. The bulk metadata has been generated for %1$d out of %1$d posts so far.', 'rank-math' ),
$processed,
count( $posts )
),
'button_text' => esc_html__( 'Terminate', 'rank-math' ),
];
return $tools;
}
/**
* Function to cancel the Bulk Edit process.
*/
public function cancel_bulk_edit_process() {
Bulk_Edit_SEO_Meta::get()->cancel();
Helper::remove_notification( 'rank_math_content_ai_posts_started' );
return __( 'Bulk Editing Process Successfully Cancelled', 'rank-math' );
}
/**
* Get Post data.
*
* @param integer $object_id Post ID.
* @param string $action The action being performed (title, description, or both).
*
* @return array Post data.
*/
private function get_post_data( $object_id, $action = 'both' ) {
$object = get_post( $object_id );
return [
'post_id' => $object_id,
'post_type' => 'download' === $object->post_type ? 'Product' : ucfirst( $object->post_type ),
'title' => get_the_title( $object_id ),
'focus_keyword' => Post::get_meta( 'focus_keyword', $object_id ),
'summary' => Helper::replace_vars( $this->get_content_for_ai( $object, $action ), $object ),
];
}
/**
* Get Term data.
*
* @param integer $object_id Term ID.
* @param string $action The action being performed (title, description, or both).
*
* @return array Term data.
*/
private function get_term_data( $object_id, $action = 'both' ) {
$object = get_term( $object_id );
return [
'post_id' => $object_id,
'post_type' => $object->taxonomy,
'title' => $object->name,
'focus_keyword' => Term::get_meta( 'focus_keyword', $object, $object->taxonomy ),
'summary' => Helper::replace_vars( $this->get_content_for_ai( $object, $action ), $object ),
];
}
/**
* Get content for AI processing based on object type and action.
*
* @param object $current_object Object instance (WP_Post or WP_Term).
* @param string $action The action being performed (title, description, or both).
*
* @return string Content to use for AI generation.
*/
private function get_content_for_ai( $current_object, $action = 'both' ) {
// For description generation, don't use current SEO descriptions.
if ( $action === 'description' || $action === 'both' ) {
return $this->get_content_without_seo_meta( $current_object );
}
// For title generation, prioritize existing SEO description.
return $current_object instanceof \WP_Post
? Post::get_meta( 'description', $current_object->ID, $this->get_content_without_seo_meta( $current_object ) )
: Term::get_meta( 'description', $current_object, $current_object->taxonomy, $this->get_content_without_seo_meta( $current_object ) );
}
/**
* Get content from object without using SEO meta description.
*
* @param object $current_object Object instance (WP_Post or WP_Term).
*
* @return string Content from the object.
*/
private function get_content_without_seo_meta( $current_object ) {
$is_post = $current_object instanceof \WP_Post;
// For terms: term description > template.
if ( ! $is_post ) {
return ! empty( $current_object->description ) ? $current_object->description : Str::truncate( Paper::get_from_options( "tax_{$current_object->taxonomy}_description", $current_object ), 160 );
}
// For posts: excerpt > content > template.
if ( ! empty( $current_object->post_excerpt ) ) {
return $current_object->post_excerpt;
}
$content = wp_strip_all_tags( $current_object->post_content );
if ( ! empty( $content ) ) {
return Str::truncate( $content, 300 );
}
return Str::truncate( Paper::get_from_options( "pt_{$current_object->post_type}_description", $current_object ), 160 );
}
}

View File

@@ -0,0 +1,226 @@
<?php
/**
* Bulk Edit SEO Meta data from Content AI API.
*
* @since 1.0.108
* @package RankMath
* @subpackage RankMath\Status
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use RankMath\Helper;
use RankMath\Admin\Admin_Helper;
defined( 'ABSPATH' ) || exit;
/**
* Bulk_Edit_SEO_Meta class.
*/
class Bulk_Edit_SEO_Meta extends \WP_Background_Process {
/**
* Action.
*
* @var string
*/
protected $action = 'bulk_edit_seo_meta';
/**
* Main instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Bulk_Edit_SEO_Meta
*/
public static function get() {
static $instance;
if ( is_null( $instance ) && ! ( $instance instanceof Bulk_Edit_SEO_Meta ) ) {
$instance = new Bulk_Edit_SEO_Meta();
}
return $instance;
}
/**
* Start creating batches.
*
* @param array $data Posts data.
*/
public function start( $data ) {
Helper::add_notification(
esc_html__( 'Bulk editing SEO meta started. It might take few minutes to complete the process.', 'rank-math' ),
[
'type' => 'success',
'id' => 'rank_math_content_ai_posts_started',
'classes' => 'rank-math-notice',
]
);
$action = $data['action'];
$posts = $data['posts'];
$language = $data['language'];
update_option( 'rank_math_content_ai_posts', $posts );
$chunks = array_chunk( $posts, 10, true );
foreach ( $chunks as $chunk ) {
$this->push_to_queue(
[
'posts' => $chunk,
'action' => $action,
'language' => $language,
'is_taxonomy' => ! empty( $data['is_taxonomy'] ),
]
);
}
$this->save()->dispatch();
}
/**
* Task to perform.
*
* @param string $data Posts to process.
*/
public function wizard( $data ) {
$this->task( $data );
}
/**
* Cancel the Bulk edit process.
*/
public function cancel() {
delete_option( 'rank_math_content_ai_posts' );
delete_option( 'rank_math_content_ai_posts_processed' );
parent::clear_scheduled_event();
}
/**
* Complete.
*
* Override if applicable, but ensure that the below actions are
* performed, or, call parent::complete().
*/
protected function complete() {
$posts = get_option( 'rank_math_content_ai_posts' );
delete_option( 'rank_math_content_ai_posts' );
delete_option( 'rank_math_content_ai_posts_processed' );
Helper::add_notification(
// Translators: placeholder is the number of modified items.
sprintf( _n( 'SEO meta successfully updated in %d item.', 'SEO meta successfully updated in %d items.', count( $posts ), 'rank-math' ), count( $posts ) ),
[
'type' => 'success',
'id' => 'rank_math_content_ai_posts',
'classes' => 'rank-math-notice',
]
);
parent::complete();
}
/**
* Task to perform.
*
* @param array $data Posts to process.
*
* @return bool
*/
protected function task( $data ) {
try {
$posts = json_decode( wp_remote_retrieve_body( $this->get_posts( $data ) ), true );
if ( empty( $posts['meta'] ) ) {
return false;
}
foreach ( $posts['meta'] as $post_id => $data ) {
$method = ! empty( $data['object_type'] ) && 'term' === $data['object_type'] ? 'update_term_meta' : 'update_post_meta';
if ( ! empty( $data['title'] ) ) {
$method( $post_id, 'rank_math_title', sanitize_text_field( $data['title'] ) );
}
if ( ! empty( $data['description'] ) ) {
$method( $post_id, 'rank_math_description', sanitize_textarea_field( $data['description'] ) );
}
}
$this->update_content_ai_posts_count( count( $posts['meta'] ) );
$credits = ! empty( $posts['credits'] ) ? $posts['credits'] : [];
if ( ! empty( $credits['available'] ) ) {
$credits = $credits['available'] - $credits['taken'];
Helper::update_credits( $credits );
if ( $credits <= 0 ) {
$posts_processed = get_option( 'rank_math_content_ai_posts_processed' );
delete_option( 'rank_math_content_ai_posts' );
delete_option( 'rank_math_content_ai_posts_processed' );
Helper::add_notification(
// Translators: placeholder is the number of modified posts.
sprintf( esc_html__( 'SEO meta successfully updated in %d posts. The process was stopped as you have used all the credits on your site.', 'rank-math' ), $posts_processed ),
[
'type' => 'success',
'id' => 'rank_math_content_ai_posts',
'classes' => 'rank-math-notice',
]
);
wp_clear_scheduled_hook( 'wp_bulk_edit_seo_meta_cron' );
}
}
return false;
} catch ( \Exception $error ) {
return true;
}
}
/**
* Get Posts to bulk update the data.
*
* @param array $data Data to process.
*
* @return array
*/
private function get_posts( $data ) {
$connect_data = Admin_Helper::get_registration_data();
$posts = array_values( $data['posts'] );
$action = $data['action'];
$language = $data['language'];
$data = [
'posts' => $posts,
'output' => $action,
'language' => $language,
'choices' => 1,
'username' => $connect_data['username'],
'api_key' => $connect_data['api_key'],
'site_url' => $connect_data['site_url'],
'is_taxonomy' => ! empty( $data['is_taxonomy'] ),
'plugin_version' => rank_math()->version,
];
return wp_remote_post(
CONTENT_AI_URL . '/ai/bulk_seo_meta',
[
'headers' => [
'content-type' => 'application/json',
],
'timeout' => 60,
'body' => wp_json_encode( $data ),
]
);
}
/**
* Keep count of the Content AI posts that were processed.
*
* @param int $count Number of posts processed.
*
* @return void
*/
private function update_content_ai_posts_count( $count ) {
$content_ai_posts_count = get_option( 'rank_math_content_ai_posts_processed', 0 ) + $count;
update_option( 'rank_math_content_ai_posts_processed', $content_ai_posts_count, false );
}
}

View File

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

View File

@@ -0,0 +1,292 @@
<?php
/**
* The Role Manager Module.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Content_AI_Page
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Admin\Page;
use RankMath\Helpers\Param;
use WP_Block_Editor_Context;
defined( 'ABSPATH' ) || exit;
/**
* Content_AI_Page class.
*/
class Content_AI_Page {
use Hooker;
/**
* Content_AI object.
*
* @var object
*/
public $content_ai;
/**
* Class constructor.
*
* @param Object $content_ai Content_AI class object.
*/
public function __construct( $content_ai ) {
$this->content_ai = $content_ai;
$this->action( 'rank_math/admin_bar/items', 'admin_bar_items', 11 );
$this->action( 'init', 'init' );
$this->filter( 'wp_insert_post_data', 'remove_unused_generated_content' );
if ( Param::get( 'page' ) !== 'rank-math-content-ai-page' ) {
return;
}
$this->action( 'admin_footer', 'content_editor_settings' );
add_filter( 'should_load_block_editor_scripts_and_styles', '__return_true' );
}
/**
* Init function.
*/
public function init() {
$this->register_post_type();
$this->register_admin_page();
Block_Command::get();
Event_Scheduler::get();
}
/**
* Register admin page.
*/
public function register_admin_page() {
$uri = untrailingslashit( plugin_dir_url( __FILE__ ) );
$new_label = '<span class="rank-math-new-label" style="color:#ed5e5e;font-size:10px;font-weight:normal;">' . esc_html__( 'New!', 'rank-math' ) . '</span>';
if ( 'rank-math-content-ai-page' === Param::get( 'page' ) ) {
$this->content_ai->localized_data( [ 'isContentAIPage' => true ] );
}
new Page(
'rank-math-content-ai-page',
esc_html__( 'Content AI', 'rank-math' ),
[
'position' => 4,
'parent' => 'rank-math',
// Translators: placeholder is the new label.
'menu_title' => sprintf( esc_html__( 'Content AI %s', 'rank-math' ), $new_label ),
'capability' => 'rank_math_content_ai',
'render' => __DIR__ . '/views/main.php',
'classes' => [ 'rank-math-page' ],
'assets' => [
'styles' => [
'wp-edit-post' => '',
'rank-math-common' => '',
'rank-math-cmb2' => '',
'wp-block-library' => '',
'rank-math-content-ai-page' => $uri . '/assets/css/content-ai-page.css',
],
'scripts' => [
'lodash' => '',
'wp-components' => '',
'wp-block-library' => '',
'wp-format-library' => '',
'wp-edit-post' => '',
'wp-blocks' => '',
'wp-element' => '',
'wp-editor' => '',
'rank-math-components' => '',
'rank-math-analyzer' => rank_math()->plugin_url() . 'assets/admin/js/analyzer.js',
'rank-math-content-ai' => rank_math()->plugin_url() . 'includes/modules/content-ai/assets/js/content-ai.js',
],
],
]
);
}
/**
* Add admin bar item.
*
* @param Admin_Bar_Menu $menu Menu class instance.
*/
public function admin_bar_items( $menu ) {
$url = Helper::get_admin_url( 'content-ai-page' );
$menu->add_sub_menu(
'content-ai-page',
[
'title' => esc_html__( 'Content AI', 'rank-math' ),
'href' => $url,
'priority' => 50,
]
);
$items = [
'content-ai-tools' => [
'title' => esc_html__( 'AI Tools', 'rank-math' ),
'href' => $url . '#ai-tools',
'meta' => [ 'title' => esc_html__( 'Content AI Tools', 'rank-math' ) ],
],
'content-ai-editor' => [
'title' => esc_html__( 'Content Editor', 'rank-math' ),
'href' => $url . '#content-editor',
'meta' => [ 'title' => esc_html__( 'Content AI Editor', 'rank-math' ) ],
],
'content-ai-chat' => [
'title' => esc_html__( 'Chat', 'rank-math' ),
'href' => $url . '#chat',
'meta' => [ 'title' => esc_html__( 'Content AI Chat', 'rank-math' ) ],
],
'content-ai-history' => [
'title' => esc_html__( 'History', 'rank-math' ),
'href' => $url . '#history',
'meta' => [ 'title' => esc_html__( 'Content AI History', 'rank-math' ) ],
],
];
foreach ( $items as $id => $args ) {
$menu->add_sub_menu( $id, $args, 'content-ai-page' );
}
}
/**
* Add Content Editor Settings.
*/
public function content_editor_settings() {
$post = $this->get_content_editor_post();
$block_editor_context = new WP_Block_Editor_Context( [ 'post' => [] ] );
// Flag that we're loading the block editor.
$current_screen = get_current_screen();
$current_screen->is_block_editor( true );
$editor_settings = [
'availableTemplates' => [],
'disablePostFormats' => true,
'autosaveInterval' => 0,
'richEditingEnabled' => user_can_richedit(),
'supportsLayout' => function_exists( 'wp_theme_has_theme_json' ) ? wp_theme_has_theme_json() : false,
'supportsTemplateMode' => false,
'enableCustomFields' => false,
];
$editor_settings = get_block_editor_settings( $editor_settings, $block_editor_context );
/**
* Scripts
*/
wp_enqueue_media( [ 'post' => $post->ID ] );
?>
<div id="editor2" data-settings='<?php echo esc_attr( wp_json_encode( $editor_settings ) ); ?>' data-post-id="<?php echo esc_attr( $post->ID ); ?>"></div>
<?php
wp_set_script_translations( 'rank-math-content-ai', 'rank-math' );
wp_set_script_translations( 'rank-math-content-ai-page', 'rank-math' );
}
/**
* Remove unsed content generated from the Toolbar option of the Content AI.
*
* @param array $data An array of slashed, sanitized, and processed post data.
*/
public function remove_unused_generated_content( $data ) {
$blocks = parse_blocks( $data['post_content'] );
if ( empty( $blocks ) ) {
return $data;
}
$update = false;
foreach ( $blocks as $key => $block ) {
if ( 'rank-math/command' === $block['blockName'] ) {
unset( $blocks[ $key ] );
$update = true;
}
}
if ( $update ) {
$data['post_content'] = serialize_blocks( $blocks );
}
return $data;
}
/**
* Register Content AI post type to use the post in Content Editor.
*/
private function register_post_type() {
register_post_type(
'rm_content_editor',
[
'label' => 'RM Content Editor',
'public' => false,
'publicly_queryable' => false,
'show_ui' => false,
'show_in_rest' => true,
'has_archive' => false,
'show_in_menu' => false,
'show_in_nav_menus' => false,
'delete_with_user' => false,
'exclude_from_search' => false,
'capability_type' => 'page',
'map_meta_cap' => true,
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'supports' => [ 'editor' ],
]
);
}
/**
* Get Content Editor post.
*
* @return int Post ID.
*/
private function get_content_editor_post() {
$posts = get_posts(
[
'post_type' => 'rm_content_editor',
'numberposts' => 1,
'post_status' => 'any',
]
);
if ( empty( $posts ) ) {
$post_id = wp_insert_post(
[
'post_type' => 'rm_content_editor',
'post_content' => '<!-- wp:rank-math/command /-->',
]
);
return get_post( $post_id );
}
$ai_post = current( $posts );
$content = $ai_post->post_content;
$blocks = parse_blocks( $content );
if ( ! empty( $blocks ) && count( $blocks ) < 2 && 'core/paragraph' === $blocks[0]['blockName'] ) {
$content = do_blocks( $ai_post->post_content );
$content = trim( preg_replace( '/<p[^>]*><\\/p[^>]*>/', '', $content ) );
}
if ( ! $content ) {
wp_update_post(
[
'ID' => $ai_post->ID,
'post_content' => '<!-- wp:rank-math/command /-->',
]
);
}
return $ai_post;
}
}

View File

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

View File

@@ -0,0 +1,136 @@
<?php
/**
* The Event Scheduler for Content AI to update the prompts and credits data.
*
* @since 1.0.123
* @package RankMath
* @subpackage RankMath\ContentAI
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use WP_Block_Type_Registry;
use RankMath\Helper;
use RankMath\Admin\Admin_Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Event_Scheduler class.
*/
class Event_Scheduler {
use Hooker;
/**
* The single instance of the class.
*
* @var Event_Scheduler
*/
protected static $instance = null;
/**
* Retrieve main Block_Command instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Event_Scheduler
*/
public static function get() {
if ( is_null( self::$instance ) && ! ( self::$instance instanceof Event_Scheduler ) ) {
self::$instance = new Event_Scheduler();
}
return self::$instance;
}
/**
* The Constructor.
*/
public function __construct() {
if ( ! Helper::is_site_connected() ) {
return;
}
$credits = get_option( 'rank_math_ca_credits' );
if ( ! empty( $credits['refresh_date'] ) ) {
wp_schedule_single_event( absint( $credits['refresh_date'] ) + 60, 'rank_math/content-ai/update_plan' );
}
$this->action( 'rank_math/content-ai/update_prompts', 'update_prompts_data' );
$this->action( 'rank_math/content-ai/update_plan', 'update_content_ai_plan' );
$this->action( 'admin_footer', 'update_prompts_on_new_site' );
}
/**
* Fetch and update the prompts data daily.
*
* @return void
*/
public function update_prompts_data() {
if ( ! Helper::get_credits() || ! Helper::get_content_ai_plan() ) {
return;
}
$registered = Admin_Helper::get_registration_data();
if ( empty( $registered ) || empty( $registered['username'] ) ) {
return;
}
$prompt_data = [];
$data = wp_remote_post(
CONTENT_AI_URL . '/ai/default_prompts',
[
'headers' => [
'Content-type' => 'application/json',
],
'body' => wp_json_encode(
[
'username' => $registered['username'],
'api_key' => $registered['api_key'],
'site_url' => $registered['site_url'],
]
),
]
);
$response_code = wp_remote_retrieve_response_code( $data );
if ( is_wp_error( $data ) || ! in_array( $response_code, [ 200, 201 ], true ) ) {
return;
}
update_option( 'rank_math_prompts_updated', true );
$data = wp_remote_retrieve_body( $data );
$data = json_decode( $data, true );
if ( empty( $data ) ) {
return;
}
Helper::save_default_prompts( $data );
}
/**
* Run the credits endpoint to update the plan on reset date.
*
* @return void
*/
public function update_content_ai_plan() {
Helper::get_content_ai_credits( true );
wp_clear_scheduled_hook( 'rank_math/content-ai/update_plan' );
}
/**
* Function to update Prompts data on new sites.
*
* @return void
*/
public function update_prompts_on_new_site() {
$prompts = Helper::get_prompts();
if ( get_option( 'rank_math_prompts_updated' ) || ! empty( $prompts ) ) {
return;
}
do_action( 'rank_math/content-ai/update_prompts' );
}
}

View File

@@ -0,0 +1,726 @@
<?php
/**
* The Global functionality of the plugin.
*
* Defines the functionality loaded on admin.
*
* @since 1.0.71
* @package RankMath
* @subpackage RankMath\Rest
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\ContentAI;
use WP_Error;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Controller;
use RankMath\Helper;
use RankMath\Admin\Admin_Helper;
defined( 'ABSPATH' ) || exit;
/**
* Rest class.
*/
class Rest extends WP_REST_Controller {
/**
* Registered data.
*
* @var array|false
*/
private $registered;
/**
* Constructor.
*/
public function __construct() {
$this->namespace = \RankMath\Rest\Rest_Helper::BASE . '/ca';
$this->registered = Admin_Helper::get_registration_data();
}
/**
* Registers the routes for the objects of the controller.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/researchKeyword',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'research_keyword' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => $this->get_research_keyword_args(),
]
);
register_rest_route(
$this->namespace,
'/getCredits',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'get_credits' ],
'permission_callback' => [ $this, 'has_permission' ],
]
);
register_rest_route(
$this->namespace,
'/createPost',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'create_post' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'content' => [
'description' => esc_html__( 'The content of the new post.', 'rank-math' ),
'type' => 'string',
'required' => true,
],
'title' => [
'description' => esc_html__( 'The title of the new post.', 'rank-math' ),
'type' => 'string',
'required' => false,
],
],
]
);
register_rest_route(
$this->namespace,
'/saveOutput',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'save_output' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'outputs' => [
'description' => esc_html__( 'An array of AI-generated and existing outputs to be saved.', 'rank-math' ),
'type' => 'array',
'required' => true,
],
'endpoint' => [
'description' => esc_html__( 'The API endpoint for which the output was generated.', 'rank-math' ),
'type' => 'string',
'required' => true,
],
'isChat' => [
'description' => esc_html__( 'Indicates if the request was for the Chat endpoint.', 'rank-math' ),
'type' => 'boolean',
'required' => false,
],
'attributes' => [
'description' => esc_html__( 'The parameters used to generate the AI output.', 'rank-math' ),
'type' => 'object',
'required' => false,
],
'credits' => [
'description' => esc_html__( 'Credit usage details returned by the API.', 'rank-math' ),
'type' => 'object',
'required' => false,
],
],
]
);
register_rest_route(
$this->namespace,
'/deleteOutput',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'delete_output' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'isChat' => [
'description' => esc_html__( 'Indicates if the request to delete the output was for the Chat endpoint.', 'rank-math' ),
'type' => 'boolean',
'required' => false,
],
'index' => [
'description' => esc_html__( 'The output index to delete, applicable only to the Chat endpoint.', 'rank-math' ),
'type' => 'integer',
'required' => false,
],
],
]
);
register_rest_route(
$this->namespace,
'/updateRecentPrompt',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'update_recent_prompt' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'prompt' => [
'description' => esc_html__( 'The selected prompt to be updated in the recent prompts.', 'rank-math' ),
'type' => 'string',
'required' => true,
],
],
]
);
register_rest_route(
$this->namespace,
'/updatePrompt',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'update_prompt' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'prompt' => [
'description' => esc_html__( 'The prompt data to be saved in the database.', 'rank-math' ),
'required' => true,
'validate_callback' => [ '\\RankMath\\Rest\\Rest_Helper', 'is_param_empty' ],
],
],
]
);
register_rest_route(
$this->namespace,
'/savePrompts',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'save_prompts' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'prompts' => [
'description' => esc_html__( 'A list of prompts received from the API to be saved in the database.', 'rank-math' ),
'type' => 'array',
'required' => true,
],
],
]
);
register_rest_route(
$this->namespace,
'/pingContentAI',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'ping_content_ai' ],
'permission_callback' => [ $this, 'has_ping_permission' ],
'args' => [
'plan' => [
'description' => esc_html__( 'Content AI plan to update in the Database.', 'rank-math' ),
'type' => 'string',
'required' => true,
],
'refreshDate' => [
'description' => esc_html__( 'Content AI reset date to update in the Database', 'rank-math' ),
'type' => 'string',
'required' => true,
],
],
]
);
register_rest_route(
$this->namespace,
'/generateAlt',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'generate_alt' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'attachmentIds' => [
'description' => esc_html__( 'List of attachment IDs for which to generate alt text.', 'rank-math' ),
'type' => 'array',
'required' => true,
],
],
]
);
register_rest_route(
$this->namespace,
'/updateCredits',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'update_credits' ],
'permission_callback' => [ $this, 'has_permission' ],
'args' => [
'credits' => [
'description' => esc_html__( 'Credit usage details returned by the API.', 'rank-math' ),
'type' => 'integer',
'required' => true,
],
],
]
);
}
/**
* Check API key in request.
*
* @param WP_REST_Request $request Full details about the request.
* @return bool Whether the API key matches or not.
*/
public function has_ping_permission( WP_REST_Request $request ) {
if ( empty( $this->registered ) ) {
return false;
}
return $request->get_param( 'apiKey' ) === $this->registered['api_key'] &&
$request->get_param( 'username' ) === $this->registered['username'];
}
/**
* Determines if the current user can manage analytics.
*
* @return true
*/
public function has_permission() {
if ( ! Helper::has_cap( 'content_ai' ) || empty( $this->registered ) ) {
return new WP_Error(
'rest_cannot_access',
__( 'Sorry, only authenticated users can research the keyword.', 'rank-math' ),
[ 'status' => rest_authorization_required_code() ]
);
}
return true;
}
/**
* Get Content AI Credits.
*
* @return int Credits.
*/
public function get_credits() {
$credits = Helper::get_content_ai_credits( true, true );
if ( ! empty( $credits['error'] ) ) {
$error = $credits['error'];
$error_texts = Helper::get_content_ai_errors();
return [
'error' => ! empty( $error_texts[ $error ] ) ? wp_specialchars_decode( $error_texts[ $error ], ENT_QUOTES ) : $error,
'credits' => isset( $credits['credits'] ) ? $credits['credits'] : '',
];
}
return $credits;
}
/**
* Research a keyword.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function research_keyword( WP_REST_Request $request ) {
$object_id = $request->get_param( 'objectID' );
$country = $request->get_param( 'country' );
$keyword = mb_strtolower( $request->get_param( 'keyword' ) );
$force_update = $request->get_param( 'forceUpdate' );
$keyword_data = get_option( 'rank_math_ca_data' );
$post_type = 0 === $object_id ? 'page' : get_post_type( $object_id );
if ( ! in_array( $post_type, (array) Helper::get_settings( 'general.content_ai_post_types' ), true ) ) {
return [
'data' => esc_html__( 'Content AI is not enabled on this Post type.', 'rank-math' ),
];
}
if ( ! apply_filters( 'rank_math/content_ai/call_api', true ) ) {
return [
'data' => 'show_dummy_data',
];
}
if (
! $force_update &&
! empty( $keyword_data ) &&
! empty( $keyword_data[ $country ] ) &&
! empty( $keyword_data[ $country ][ $keyword ] )
) {
update_post_meta(
$object_id,
'rank_math_ca_keyword',
[
'keyword' => $keyword,
'country' => $country,
]
);
return [
'data' => $keyword_data[ $country ][ $keyword ],
'keyword' => $keyword,
];
}
$data = $this->get_researched_data( $keyword, $post_type, $country, $force_update );
if ( ! empty( $data['error'] ) ) {
return $this->get_errored_data( $data['error'] );
}
$credits = ! empty( $data['credits'] ) ? $data['credits'] : 0;
if ( ! empty( $credits ) ) {
$credits = $credits['available'] - $credits['taken'];
}
$data = $data['data']['details'];
$this->get_recommendations( $data );
update_post_meta(
$object_id,
'rank_math_ca_keyword',
[
'keyword' => $keyword,
'country' => $country,
]
);
$keyword_data[ $country ][ $keyword ] = $data;
update_option( 'rank_math_ca_data', $keyword_data, false );
Helper::update_credits( $credits );
return [
'data' => $keyword_data[ $country ][ $keyword ],
'credits' => $credits,
'keyword' => $keyword,
];
}
/**
* Get the arguments for the researchKeyword route.
*
* @return array
*/
public function get_research_keyword_args() {
return [
'keyword' => [
'description' => esc_html__( 'The keyword to be researched.', 'rank-math' ),
'type' => 'string',
'required' => true,
],
'country' => [
'description' => esc_html__( 'The country for which the keyword should be researched.', 'rank-math' ),
'type' => 'string',
'required' => true,
],
'objectID' => [
'description' => esc_html__( 'The ID of the post initiating the keyword research request.', 'rank-math' ),
'type' => 'integer',
'required' => true,
],
'force_update' => [
'description' => esc_html__( 'If true, forces a fresh research request.', 'rank-math' ),
'type' => 'boolean',
'required' => false,
],
];
}
/**
* Create a new Post from Content AI Page.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_post( WP_REST_Request $request ) {
$content = $request->get_param( 'content' );
$title = $request->get_param( 'title' );
$title = $title ? $title : 'Content AI Post';
$blocks = parse_blocks( $content );
$current_block = ! empty( $blocks ) ? current( $blocks ) : '';
if (
! empty( $current_block ) &&
$current_block['blockName'] === 'core/heading' &&
$current_block['attrs']['level'] === 1
) {
$title = wp_strip_all_tags( $current_block['innerHTML'] );
}
$post_id = wp_insert_post(
[
'post_title' => $title,
'post_content' => $content,
]
);
return wp_specialchars_decode( add_query_arg( 'tab', 'content-ai', get_edit_post_link( $post_id ) ) );
}
/**
* Save the API output.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function save_output( WP_REST_Request $request ) {
$outputs = $request->get_param( 'outputs' );
$endpoint = $request->get_param( 'endpoint' );
$is_chat = $request->get_param( 'isChat' );
$attributes = $request->get_param( 'attributes' );
$credits_data = $request->get_param( 'credits' );
if ( ! empty( $credits_data ) ) {
$credits = ! empty( $credits_data['credits'] ) ? $credits_data['credits'] : [];
$data = [
'credits' => ! empty( $credits['available'] ) ? $credits['available'] - $credits['taken'] : 0,
'plan' => ! empty( $credits_data['plan'] ) ? $credits_data['plan'] : '',
'refresh_date' => ! empty( $credits_data['refreshDate'] ) ? $credits_data['refreshDate'] : '',
];
Helper::update_credits( $data );
}
if ( $is_chat ) {
Helper::update_chats( current( $outputs ), end( $attributes['messages'] ), $attributes['session'], $attributes['isNew'], $attributes['regenerate'] );
return true;
}
return Helper::update_outputs( $endpoint, $outputs );
}
/**
* Delete the API output.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_output( WP_REST_Request $request ) {
$is_chat = $request->get_param( 'isChat' );
if ( $is_chat ) {
return Helper::delete_chats( $request->get_param( 'index' ) );
}
return Helper::delete_outputs();
}
/**
* Update the Prompts.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_prompt( WP_REST_Request $request ) {
$prompt = $request->get_param( 'prompt' );
if ( is_string( $prompt ) ) {
return Helper::delete_prompt( $prompt );
}
return Helper::update_prompts( $prompt );
}
/**
* Save the Prompts.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function save_prompts( WP_REST_Request $request ) {
$prompts = $request->get_param( 'prompts' );
if ( empty( $prompts ) ) {
return false;
}
return Helper::save_default_prompts( $prompts );
}
/**
* Update the Recent Prompts.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_recent_prompt( WP_REST_Request $request ) {
$prompt = $request->get_param( 'prompt' );
return Helper::update_recent_prompts( $prompt );
}
/**
* Endpoing to update the AI plan and credits.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function ping_content_ai( WP_REST_Request $request ) {
$credits = ! empty( $request->get_param( 'credits' ) ) ? json_decode( $request->get_param( 'credits' ), true ) : [];
$data = [
'credits' => ! empty( $credits['available'] ) ? $credits['available'] - $credits['taken'] : 0,
'plan' => $request->get_param( 'plan' ),
'refresh_date' => $request->get_param( 'refreshDate' ),
];
Helper::update_credits( $data );
return true;
}
/**
* Endpoint to generate Image Alt.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function generate_alt( WP_REST_Request $request ) {
$ids = $request->get_param( 'attachmentIds' );
if ( empty( $ids ) ) {
return false;
}
do_action( 'rank_math/content_ai/generate_alt', $ids );
return true;
}
/**
* Endpoint to Update the credits data.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_credits( WP_REST_Request $request ) {
$credits = $request->get_param( 'credits' );
Helper::update_credits( $credits );
return true;
}
/**
* Get data from the API.
*
* @param string $keyword Researched keyword.
* @param string $post_type Researched post type.
* @param string $country Researched country.
* @param bool $force_update Whether to force update the researched data.
*
* @return array
*/
private function get_researched_data( $keyword, $post_type, $country, $force_update = false ) {
$args = [
'username' => rawurlencode( $this->registered['username'] ),
'api_key' => rawurlencode( $this->registered['api_key'] ),
'keyword' => rawurlencode( $keyword ),
'post_type' => rawurlencode( $post_type ),
'site_url' => rawurlencode( Helper::get_home_url() ),
'new_api' => 1,
];
if ( 'all' !== $country ) {
$args['locale'] = rawurlencode( $country );
}
if ( $force_update ) {
$args['force_refresh'] = 1;
}
$url = add_query_arg(
$args,
CONTENT_AI_URL . '/ai/research'
);
$data = wp_remote_get(
$url,
[
'timeout' => 60,
]
);
$response_code = wp_remote_retrieve_response_code( $data );
if ( 200 !== $response_code ) {
return [
'error' => $this->research_keyword_error( $data, $response_code ),
];
}
$data = wp_remote_retrieve_body( $data );
$data = json_decode( $data, true );
if ( empty( $data['error'] ) && empty( $data['data']['details'] ) ) {
return [
'error' => esc_html__( 'No data found for the researched keyword.', 'rank-math' ),
];
}
return $data;
}
/**
* Update the error message based on the pre-defined Content AI errors defined in the plugin.
*
* @param array $response API response.
* @param int $response_code API response code.
*
* @return string Error message.
*/
private function research_keyword_error( $response, $response_code ) {
if ( $response_code === 410 ) {
return wp_kses_post(
sprintf(
// Translators: link to the update page.
__( 'There is a new version of Content AI available! %s the Rank Math SEO plugin to use this feature.', 'rank-math' ),
'<a href="' . esc_url( self_admin_url( 'update-core.php' ) ) . '">' . __( 'Please update', 'rank-math' ) . '</a>'
)
);
}
$error_texts = Helper::get_content_ai_errors();
$data = wp_remote_retrieve_body( $response );
$data = json_decode( $data, true );
return isset( $data['message'] ) && isset( $error_texts[ $data['message'] ] ) ? $error_texts[ $data['message'] ] : $response['response']['message'];
}
/**
* Get errored data.
*
* @param array $error Error data received from the API.
*
* @return array
*/
private function get_errored_data( $error ) {
if ( empty( $error['code'] ) ) {
return [
'data' => $error,
];
}
if ( 'invalid_domain' === $error['code'] ) {
return [
'data' => esc_html__( 'This feature is not available on the localhost.', 'rank-math' ),
];
}
if ( 'domain_limit_reached' === $error['code'] ) {
return [
'data' => esc_html__( 'You have used all the free credits which are allowed to this domain.', 'rank-math' ),
];
}
return [
'data' => '',
'credits' => $error['code'],
];
}
/**
* Get the Recommendations data.
*
* @param array $data Researched data.
*/
private function get_recommendations( &$data ) {
foreach ( $data['recommendations'] as $key => $value ) {
if ( ! is_array( $value ) ) {
continue;
}
$data['recommendations'][ $key ]['total'] = array_sum( $value );
}
}
}

View File

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

View File

@@ -0,0 +1,226 @@
<?php
/**
* Content AI general settings.
*
* @package RankMath
* @subpackage RankMath\ContentAI
*/
use RankMath\Helper;
use RankMath\KB;
use RankMath\Admin\Admin_Helper;
defined( 'ABSPATH' ) || exit;
if ( ! Helper::is_site_connected() ) {
$cmb->add_field(
[
'id' => 'rank_math_content_ai_settings',
'type' => 'raw',
'content' => '<div id="setting-panel-content-ai" class="rank-math-tab rank-math-options-panel-content exclude">
<div class="wp-core-ui rank-math-ui connect-wrap">
<a href="' . Admin_Helper::get_activate_url( Helper::get_settings_url( 'general', 'content-ai' ) ) . '" class="button button-primary button-connect button-animated" name="rank_math_activate">'
. esc_html__( 'Connect Your Rank Math Account', 'rank-math' )
. '</a>
</div>
<div id="rank-math-pro-cta" class="content-ai-settings">
<div class="rank-math-cta-box width-100 no-shadow no-padding no-border">
<h3>' . esc_html__( 'Benefits of Connecting Rank Math Account', 'rank-math' ) . '</h3>
<ul>
<li>' . esc_html__( 'Gain Access to 40+ Advanced AI Tools.', 'rank-math' ) . '</li>
<li>' . esc_html__( 'Experience the Revolutionary AI-Powered Content Editor.', 'rank-math' ) . '</li>
<li>' . esc_html__( 'Engage with RankBot, Our AI Chatbot, For SEO Advice.', 'rank-math' ) . '</li>
<li>' . esc_html__( 'Escape the Writer\'s Block Using AI to Write Inside WordPress.', 'rank-math' ) . '</li>
</ul>
</div>
</div>
</div>',
]
);
return;
}
$cmb->add_field(
[
'id' => 'content_ai_country',
'type' => 'select',
'name' => esc_html__( 'Default Country', 'rank-math' ),
'desc' => esc_html__( 'Content AI tailors keyword research to the target country for highly relevant suggestions. You can override this in individual posts/pages/CPTs.', 'rank-math' ),
'options' => Helper::choices_contentai_countries(),
'default' => 'all',
]
);
$cmb->add_field(
[
'id' => 'content_ai_tone',
'type' => 'select',
'name' => esc_html__( 'Default Tone', 'rank-math' ),
'desc' => esc_html__( 'This feature enables the default primary tone or writing style that characterizes your content. You can override this in individual tools.', 'rank-math' ),
'default' => 'Formal',
'attributes' => ( 'data-s2' ),
'options' => [
'Analytical' => esc_html__( 'Analytical', 'rank-math' ),
'Argumentative' => esc_html__( 'Argumentative', 'rank-math' ),
'Casual' => esc_html__( 'Casual', 'rank-math' ),
'Conversational' => esc_html__( 'Conversational', 'rank-math' ),
'Creative' => esc_html__( 'Creative', 'rank-math' ),
'Descriptive' => esc_html__( 'Descriptive', 'rank-math' ),
'Emotional' => esc_html__( 'Emotional', 'rank-math' ),
'Empathetic' => esc_html__( 'Empathetic', 'rank-math' ),
'Expository' => esc_html__( 'Expository', 'rank-math' ),
'Factual' => esc_html__( 'Factual', 'rank-math' ),
'Formal' => esc_html__( 'Formal', 'rank-math' ),
'Friendly' => esc_html__( 'Friendly', 'rank-math' ),
'Humorous' => esc_html__( 'Humorous', 'rank-math' ),
'Informal' => esc_html__( 'Informal', 'rank-math' ),
'Journalese' => esc_html__( 'Journalese', 'rank-math' ),
'Narrative' => esc_html__( 'Narrative', 'rank-math' ),
'Objective' => esc_html__( 'Objective', 'rank-math' ),
'Opinionated' => esc_html__( 'Opinionated', 'rank-math' ),
'Persuasive' => esc_html__( 'Persuasive', 'rank-math' ),
'Poetic' => esc_html__( 'Poetic', 'rank-math' ),
'Satirical' => esc_html__( 'Satirical', 'rank-math' ),
'Story-telling' => esc_html__( 'Story-telling', 'rank-math' ),
'Subjective' => esc_html__( 'Subjective', 'rank-math' ),
'Technical' => esc_html__( 'Technical', 'rank-math' ),
],
],
);
$cmb->add_field(
[
'id' => 'content_ai_audience',
'type' => 'select',
'name' => esc_html__( 'Default Audience', 'rank-math' ),
'desc' => esc_html__( 'This option lets you set the default audience that usually reads your content. You can override this in individual tools.', 'rank-math' ),
'default' => 'General Audience',
'attributes' => ( 'data-s2' ),
'options' => [
'Activists' => esc_html__( 'Activists', 'rank-math' ),
'Artists' => esc_html__( 'Artists', 'rank-math' ),
'Authors' => esc_html__( 'Authors', 'rank-math' ),
'Bargain Hunters' => esc_html__( 'Bargain Hunters', 'rank-math' ),
'Bloggers' => esc_html__( 'Bloggers', 'rank-math' ),
'Business Owners' => esc_html__( 'Business Owners', 'rank-math' ),
'Collectors' => esc_html__( 'Collectors', 'rank-math' ),
'Cooks' => esc_html__( 'Cooks', 'rank-math' ),
'Crafters' => esc_html__( 'Crafters', 'rank-math' ),
'Dancers' => esc_html__( 'Dancers', 'rank-math' ),
'DIYers' => esc_html__( 'DIYers', 'rank-math' ),
'Designers' => esc_html__( 'Designers', 'rank-math' ),
'Educators' => esc_html__( 'Educators', 'rank-math' ),
'Engineers' => esc_html__( 'Engineers', 'rank-math' ),
'Entrepreneurs' => esc_html__( 'Entrepreneurs', 'rank-math' ),
'Environmentalists' => esc_html__( 'Environmentalists', 'rank-math' ),
'Fashionistas' => esc_html__( 'Fashionistas', 'rank-math' ),
'Fitness Enthusiasts' => esc_html__( 'Fitness Enthusiasts', 'rank-math' ),
'Foodies' => esc_html__( 'Foodies', 'rank-math' ),
'Gaming Enthusiasts' => esc_html__( 'Gaming Enthusiasts', 'rank-math' ),
'Gardeners' => esc_html__( 'Gardeners', 'rank-math' ),
'General Audience' => esc_html__( 'General Audience', 'rank-math' ),
'Health Enthusiasts' => esc_html__( 'Health Enthusiasts', 'rank-math' ),
'Healthcare Professionals' => esc_html__( 'Healthcare Professionals', 'rank-math' ),
'Indoor Hobbyists' => esc_html__( 'Indoor Hobbyists', 'rank-math' ),
'Investors' => esc_html__( 'Investors', 'rank-math' ),
'Job Seekers' => esc_html__( 'Job Seekers', 'rank-math' ),
'Movie Buffs' => esc_html__( 'Movie Buffs', 'rank-math' ),
'Musicians' => esc_html__( 'Musicians', 'rank-math' ),
'Outdoor Enthusiasts' => esc_html__( 'Outdoor Enthusiasts', 'rank-math' ),
'Parents' => esc_html__( 'Parents', 'rank-math' ),
'Pet Owners' => esc_html__( 'Pet Owners', 'rank-math' ),
'Photographers' => esc_html__( 'Photographers', 'rank-math' ),
'Podcast Listeners' => esc_html__( 'Podcast Listeners', 'rank-math' ),
'Professionals' => esc_html__( 'Professionals', 'rank-math' ),
'Retirees' => esc_html__( 'Retirees', 'rank-math' ),
'Russian' => esc_html__( 'Russian', 'rank-math' ),
'Seniors' => esc_html__( 'Seniors', 'rank-math' ),
'Social Media Users' => esc_html__( 'Social Media Users', 'rank-math' ),
'Sports Fans' => esc_html__( 'Sports Fans', 'rank-math' ),
'Students' => esc_html__( 'Students', 'rank-math' ),
'Tech Enthusiasts' => esc_html__( 'Tech Enthusiasts', 'rank-math' ),
'Travelers' => esc_html__( 'Travelers', 'rank-math' ),
'TV Enthusiasts' => esc_html__( 'TV Enthusiasts', 'rank-math' ),
'Video Creators' => esc_html__( 'Video Creators', 'rank-math' ),
'Writers' => esc_html__( 'Writers', 'rank-math' ),
],
],
);
$cmb->add_field(
[
'id' => 'content_ai_language',
'type' => 'select',
'name' => esc_html__( 'Default Language', 'rank-math' ),
'desc' => esc_html__( 'This option lets you set the default language for content generated using Content AI. You can override this in individual tools.', 'rank-math' ),
'default' => Helper::content_ai_default_language(),
'attributes' => ( 'data-s2' ),
'options' => [
'US English' => esc_html__( 'US English', 'rank-math' ),
'UK English' => esc_html__( 'UK English', 'rank-math' ),
'Arabic' => esc_html__( 'Arabic', 'rank-math' ),
'Bulgarian' => esc_html__( 'Bulgarian', 'rank-math' ),
'Chinese' => esc_html__( 'Chinese', 'rank-math' ),
'Czech' => esc_html__( 'Czech', 'rank-math' ),
'Danish' => esc_html__( 'Danish', 'rank-math' ),
'Dutch' => esc_html__( 'Dutch', 'rank-math' ),
'Estonian' => esc_html__( 'Estonian', 'rank-math' ),
'Finnish' => esc_html__( 'Finnish', 'rank-math' ),
'French' => esc_html__( 'French', 'rank-math' ),
'German' => esc_html__( 'German', 'rank-math' ),
'Greek' => esc_html__( 'Greek', 'rank-math' ),
'Hebrew' => esc_html__( 'Hebrew', 'rank-math' ),
'Hungarian' => esc_html__( 'Hungarian', 'rank-math' ),
'Indonesian' => esc_html__( 'Indonesian', 'rank-math' ),
'Italian' => esc_html__( 'Italian', 'rank-math' ),
'Japanese' => esc_html__( 'Japanese', 'rank-math' ),
'Korean' => esc_html__( 'Korean', 'rank-math' ),
'Latvian' => esc_html__( 'Latvian', 'rank-math' ),
'Lithuanian' => esc_html__( 'Lithuanian', 'rank-math' ),
'Norwegian' => esc_html__( 'Norwegian', 'rank-math' ),
'Polish' => esc_html__( 'Polish', 'rank-math' ),
'Portuguese' => esc_html__( 'Portuguese', 'rank-math' ),
'Romanian' => esc_html__( 'Romanian', 'rank-math' ),
'Russian' => esc_html__( 'Russian', 'rank-math' ),
'Slovak' => esc_html__( 'Slovak', 'rank-math' ),
'Slovenian' => esc_html__( 'Slovenian', 'rank-math' ),
'Spanish' => esc_html__( 'Spanish', 'rank-math' ),
'Swedish' => esc_html__( 'Swedish', 'rank-math' ),
'Turkish' => esc_html__( 'Turkish', 'rank-math' ),
],
],
);
$post_types = Helper::choices_post_types();
if ( isset( $post_types['attachment'] ) ) {
unset( $post_types['attachment'] );
}
$cmb->add_field(
[
'id' => 'content_ai_post_types',
'type' => 'multicheck_inline',
'name' => esc_html__( 'Select Post Type', 'rank-math' ),
'desc' => esc_html__( 'Choose the type of posts/pages/CPTs where you want to use Content AI.', 'rank-math' ),
'options' => $post_types,
'default' => array_keys( $post_types ),
]
);
$credits = Helper::get_credits();
if ( Helper::is_site_connected() && false !== $credits ) {
$update_credits = '<a href="#" class="rank-math-tooltip update-credit">
<i class="dashicons dashicons-image-rotate"></i>
<span>' . esc_html__( 'Click to refresh the available credits.', 'rank-math' ) . '</span>
</a>';
$refresh_date = Helper::get_content_ai_refresh_date();
$cmb->add_field(
[
'id' => 'content_ai_credits',
'type' => 'raw',
/* translators: 1. Credits left 2. Buy more credits link */
'content' => '<div class="cmb-row buy-more-credits rank-math-exclude-from-search">' . $update_credits . sprintf( esc_html__( '%1$s credits left this month. Credits will renew on %2$s or you can upgrade to get more credits %3$s.', 'rank-math' ), '<strong>' . $credits . '</strong>', wp_date( 'Y-m-d g:i a', $refresh_date ), '<a href="' . KB::get( 'content-ai-pricing-tables', 'Buy CAI Credits Options Panel' ) . '" target="_blank">' . esc_html__( 'here', 'rank-math' ) . '</a>' ) . '</div>',
]
);
}