Files
2026-04-28 15:13:50 +02:00

400 lines
12 KiB
PHP

<?php
namespace AIOSEO\Plugin\Pro\SeoRevisions;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\SeoRevisions as CommonSeoRevisions;
use AIOSEO\Plugin\Pro\Models as ProModels;
/**
* SEO Revisions container class.
*
* @since 4.4.0
*/
class SeoRevisions extends CommonSeoRevisions\SeoRevisions {
/**
* The Helpers class instance.
*
* @since 4.4.0
*
* @var null|Helpers
*/
public $helpers = null;
/**
* All SEO Revisions related options.
*
* @since 4.4.0
*
* @var array
*/
public $options = [];
/**
* Class constructor.
*
* @since 4.4.0
*
* @return void
*/
public function __construct() {
$this->setHelpers();
$this->setOptions();
$this->initHooks();
}
/**
* Fires immediately after a post is deleted from the database.
* Hooked into `deleted_post` action hook.
*
* @since 4.4.0
*
* @param int $postId Post ID.
* @return void
*/
public function deletedPostCallback( $postId ) {
$objectRevisions = new ObjectRevisions( $postId );
$objectRevisions->deleteRevisions();
}
/**
* Fires after a term is deleted from the database.
* Hooked into `delete_term` action hook.
*
* @since 4.4.0
*
* @param int $termId Term ID.
* @return void
*/
public function deleteTermCallback( $termId ) {
$objectRevisions = new ObjectRevisions( $termId, 'term' );
$objectRevisions->deleteRevisions();
}
/**
* Fires once an AIOSEO post has been saved.
* Hooked into `aioseo_insert_post` action hook.
*
* @since 4.4.0
*
* @param int $postId Post ID.
* @return void
*/
public function aioseoInsertPostCallback( $postId ) {
// Bail if this callback is not being carried under certain action hooks or the request was not to the REST API,
// and prevent unintended revisions from being created.
if (
! doing_action( 'add_attachment' ) &&
! doing_action( 'edit_attachment' ) &&
! doing_action( 'save_post' ) &&
! aioseo()->helpers->isRestApiRequest()
) {
return;
}
$wpPost = get_post( $postId );
if ( ! is_a( $wpPost, 'WP_Post' ) ) {
return;
}
$this->maybeAddRevision( $wpPost->ID );
}
/**
* Fires once an AIOSEO term has been saved.
* Hooked into `aioseo_insert_term` action hook.
*
* @since 4.4.0
*
* @param int $termId Term ID.
* @return void
*/
public function aioseoInsertTermCallback( $termId ) {
// Bail if this callback is not being carried under the `edit_term` action hook or the request was not to the REST API and prevent unintended revisions from being created.
if (
! doing_action( 'edit_term' ) &&
! aioseo()->helpers->isRestApiRequest()
) {
return;
}
$wpTerm = get_term( $termId );
if ( ! is_a( $wpTerm, 'WP_Term' ) ) {
return;
}
$this->maybeAddRevision( $wpTerm->term_id, 'term' );
}
/**
* Retrieve the per-object database revisions limit (license level based).
*
* @since 4.4.0
*
* @return int The amount of revisions allowed for the activated license.
*/
public function getLicenseRevisionsLimit() {
if ( ! aioseo()->license->hasCoreFeature( 'seo-revisions', 'revisions' ) ) {
return 0;
}
$limit = (int) aioseo()->license->getCoreFeatureValue( 'seo-revisions', 'revisions' );
if ( ! $limit ) {
$limit = 0;
}
return $limit;
}
/**
* Returns the data for Vue.
*
* @since 4.4.0
* @version 4.8.3 Added params `$objectId` and `$objectType`.
*
* @param int|null $objectId The object ID.
* @param string $objectType The object type (post or term). Default: 'post'.
* @return array The data.
*/
public function getVueDataEdit( $objectId = null, $objectType = 'post' ) {
$objectId = $objectId ?: absint( get_the_ID() );
if ( ! empty( $_GET['tag_ID'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended
$objectId = absint( $_GET['tag_ID'] ); // phpcs:ignore HM.Security.NonceVerification.Recommended
$objectType = 'term';
}
$objectRevisions = new ObjectRevisions( $objectId, $objectType );
return [
'currentUser' => $this->getVueDataCurrentUserMeta(),
'items' => $objectRevisions->getFormattedRevisions( [ 'limit' => aioseo()->seoRevisions->options['revisions']['per_page'] ] ),
'itemsLimit' => $this->getLicenseRevisionsLimit(),
'itemsTotalCount' => $objectRevisions->getCount(),
'noteMaxlength' => aioseo()->seoRevisions->options['note']['maxlength']
];
}
/**
* Returns the data for Vue.
*
* @since 4.4.0
*
* @return array The data.
*/
public function getVueDataCompare() {
$data = [
'currentUser' => $this->getVueDataCurrentUserMeta(),
'error' => ''
];
$itemToId = ! empty( $_GET['to'] ) ? absint( $_GET['to'] ) : 0; // phpcs:ignore HM.Security.NonceVerification.Recommended
$itemTo = new ProModels\SeoRevision( $itemToId );
if ( ! $itemTo->exists() ) {
// Translators: 1 - The requested revision ID.
$data['error'] = sprintf( esc_html__( 'The revision %1$d does not exist.', 'aioseo-pro' ), $itemToId );
return $data;
}
$itemFromId = ! empty( $_GET['from'] ) ? absint( $_GET['from'] ) : 0; // phpcs:ignore HM.Security.NonceVerification.Recommended
if ( $itemFromId ) {
$itemFrom = new ProModels\SeoRevision( $itemFromId );
if ( ! $itemFrom->exists() ) {
// Translators: 1 - The requested revision ID.
$data['error'] = sprintf( esc_html__( 'The revision %1$d does not exist.', 'aioseo-pro' ), $itemFromId );
return $data;
}
}
if ( $itemFromId >= $itemToId ) {
// Translators: 1 - The requested revision ID (old), 2 - The requested revision ID (new).
$data['error'] = sprintf( esc_html__( 'The revision %1$d is greater than or equal to the revision %2$d.', 'aioseo-pro' ), $itemFromId, $itemToId );
return $data;
}
$objectRevisions = new ObjectRevisions( $itemTo->object_id, $itemTo->object_type );
$itemFrom = $itemFrom ?? $objectRevisions->getPreviousRevision( $itemTo->id );
return array_merge( $data, [
'items' => $objectRevisions->getFormattedRevisions(),
'itemFrom' => $itemFrom->exists() ? $itemFrom->formatRevision() : null,
'itemTo' => $itemTo->formatRevision(),
'itemContext' => ! empty( $_GET['context'] ) ? sanitize_text_field( wp_unslash( $_GET['context'] ) ) : '', // phpcs:ignore HM.Security.NonceVerification.Recommended
'noteMaxlength' => aioseo()->seoRevisions->options['note']['maxlength']
] );
}
/**
* Maybe create a new revision.
*
* @since 4.4.0
*
* @param int $objectId The object ID.
* @param string $objectType The object type (post or term). Default: 'post'.
* @return void
*/
private function maybeAddRevision( $objectId, $objectType = 'post' ) {
$objectRevisions = new ObjectRevisions( $objectId, $objectType );
$aioseoObject = $objectRevisions->getAioseoObject();
if ( ! is_object( $aioseoObject ) ) {
return;
}
$newRevisionData = [];
foreach ( aioseo()->seoRevisions->getEligibleFields() as $key => $label ) {
if ( ! property_exists( $aioseoObject, $key ) ) {
continue;
}
$newRevisionData[ $key ] = $aioseoObject->$key;
}
$this->helpers->parseNewRevisionData( $newRevisionData );
if (
empty( $newRevisionData ) ||
! $objectRevisions->canAddRevision( $newRevisionData )
) {
return;
}
$currentUserId = get_current_user_id();
// If this creation was triggered by e.g. WP CLI a user ID might not have been set. In this case we bail.
if ( ! $currentUserId ) {
return;
}
$objectRevisions->addRevision( $newRevisionData, $currentUserId );
}
/**
* Register SEO Revisions related hooks.
*
* @since 4.4.0
*
* @return void
*/
private function initHooks() {
add_action( 'deleted_post', [ $this, 'deletedPostCallback' ] );
add_action( 'delete_term', [ $this, 'deleteTermCallback' ] );
if ( ! aioseo()->license->hasCoreFeature( 'seo-revisions' ) ) {
return;
}
add_action( 'aioseo_insert_post', [ $this, 'aioseoInsertPostCallback' ] );
add_action( 'aioseo_insert_term', [ $this, 'aioseoInsertTermCallback' ] );
}
/**
* Set {@see self::$helpers}.
*
* @since 4.4.0
*
* @return void
*/
private function setHelpers() {
$this->helpers = new Helpers();
}
/**
* Retrieves which post/term AIOSEO fields are to be watched and saved in the database.
*
* @since 4.4.0
* @version 4.7.7 Renamed from `setEligibleFields` to `getEligibleFields`.
*
* @return array The eligible fields.
*/
public function getEligibleFields() {
$lateFields = $this->getLateEligibleFields();
// Keys with no value serve to compare data later and make sure the column changed (and they have "special" columns under the comparison page).
return [
'title' => __( 'SEO Title', 'aioseo-pro' ),
'description' => __( 'SEO Description', 'aioseo-pro' ),
'keyphrases' => '',
// 'focus' and 'additional' aren't AIOSEO DB columns.
'focus' => __( 'Focus Keyword', 'aioseo-pro' ),
'additional' => __( 'Additional Keywords', 'aioseo-pro' ),
'og_title' => __( 'Facebook Title', 'aioseo-pro' ),
'og_description' => __( 'Facebook Description', 'aioseo-pro' ),
'og_object_type' => __( 'Facebook Object Type', 'aioseo-pro' ),
'og_video' => __( 'Facebook Video URL', 'aioseo-pro' ),
'og_image_type' => __( 'Facebook Image Source', 'aioseo-pro' ),
'og_image_custom_url' => __( 'Facebook Custom Image URL', 'aioseo-pro' ),
'og_article_section' => __( 'Facebook Article Section', 'aioseo-pro' ),
'og_article_tags' => __( 'Facebook Article Tags', 'aioseo-pro' ),
'twitter_use_og' => __( 'Use Data from Facebook Tab', 'aioseo-pro' ),
'twitter_card' => __( 'Twitter Card Type', 'aioseo-pro' ),
'twitter_image_type' => __( 'Twitter Image Source', 'aioseo-pro' ),
'twitter_image_custom_url' => __( 'Twitter Custom Image URL', 'aioseo-pro' ),
'twitter_title' => __( 'Twitter Title', 'aioseo-pro' ),
'twitter_description' => __( 'Twitter Description', 'aioseo-pro' ),
'canonical_url' => __( 'Canonical URL', 'aioseo-pro' ),
'schema' => __( 'Schema In Use', 'aioseo-pro' ),
'pillar_content' => $lateFields['pillar_content'],
'keywords' => $lateFields['keywords'],
'primary_term' => $lateFields['primary_term'],
'breadcrumb_settings' => $lateFields['breadcrumb_settings'],
'robots_default' => '',
'robots_noindex' => '',
'robots_noarchive' => '',
'robots_nosnippet' => '',
'robots_nofollow' => '',
'robots_noimageindex' => '',
'robots_noodp' => '',
'robots_notranslate' => '',
'robots_max_snippet' => '',
'robots_max_videopreview' => '',
'robots_max_imagepreview' => '',
// 'robots_all_settings' isn't an AIOSEO DB column.
'robots_all_settings' => __( 'Robots Setting', 'aioseo-pro' ),
'priority' => __( 'Priority Score (value)', 'aioseo-pro' ),
'frequency' => __( 'Priority Score (frequency)', 'aioseo-pro' ),
];
}
/**
* Retrieves which post/term fields started being watched after this feature was introduced.
*
* @since 4.5.3
* @version 4.7.7 Renamed from `setLateEligibleFields` to `getLateEligibleFields`.
*
* @return array The late eligible fields.
*/
public function getLateEligibleFields() {
return [
'pillar_content' => __( 'Cornerstone Content', 'aioseo-pro' ),
'keywords' => __( 'Keywords', 'aioseo-pro' ),
'primary_term' => __( 'Primary Term(s)', 'aioseo-pro' ),
'breadcrumb_settings' => __( 'Breadcrumbs', 'aioseo-pro' ),
];
}
/**
* Set {@see self::$options}.
* Ideally set options only for Vue usage on the front-end.
*
* @since 4.4.0
*
* @return void
*/
private function setOptions() {
$this->options = [
'note' => [
'maxlength' => 90 // Max char length.
],
'revisions' => [
'per_page' => 10 // Amount of items/rows shown when editing an object.
]
];
}
}