381 lines
9.9 KiB
PHP
381 lines
9.9 KiB
PHP
<?php
|
|
namespace AIOSEO\Plugin\Pro\SearchStatistics;
|
|
|
|
// Exit if accessed directly.
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
use AIOSEO\Plugin\Pro\Models\SearchStatistics as Models;
|
|
use AIOSEO\Plugin\Common\Models\Post;
|
|
|
|
/**
|
|
* Handles the objects scan.
|
|
*
|
|
* @since 4.3.0
|
|
*/
|
|
class Objects {
|
|
/**
|
|
* The action name.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @var string
|
|
*/
|
|
public $action = 'aioseo_search_statistics_objects_scan';
|
|
|
|
/**
|
|
* Class constructor.
|
|
*
|
|
* @since 4.3.0
|
|
* @version 4.9.4.2 Change admin_init to init to allow frontend scheduling.
|
|
*/
|
|
public function __construct() {
|
|
if ( ! aioseo()->license->hasCoreFeature( 'search-statistics' ) ) {
|
|
return;
|
|
}
|
|
|
|
add_action( 'admin_init', [ $this, 'init' ] );
|
|
add_action( 'update_option_permalink_structure', [ $this, 'reset' ], 10 );
|
|
add_action( 'updated_option', [ $this, 'syncHomeUrl' ], 10, 3 );
|
|
|
|
add_action( $this->action, [ $this, 'scan' ] );
|
|
}
|
|
|
|
/**
|
|
* Initialize the objects.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return void
|
|
*/
|
|
public function init() {
|
|
if ( ! aioseo()->searchStatistics->api->auth->isConnected() ) {
|
|
return;
|
|
}
|
|
|
|
$this->scheduleScan();
|
|
|
|
add_action( 'save_post', [ $this, 'updatePost' ], 100 );
|
|
add_action( 'delete_post', [ $this, 'updatePost' ], 100 );
|
|
add_action( 'wp_trash_post', [ $this, 'updatePost' ], 100 );
|
|
|
|
add_action( 'edit_term', [ $this, 'updateTerm' ], 100 );
|
|
add_action( 'delete_term', [ $this, 'deleteTerm' ], 100 );
|
|
add_action( 'created_term', [ $this, 'updateTerm' ], 100 );
|
|
}
|
|
|
|
/**
|
|
* Schedules the objects scan as a recurring action.
|
|
*
|
|
* @since 4.3.0
|
|
* @version 4.9.4.2 Switch to recurring action with cache-based idle state.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function scheduleScan() {
|
|
// If we're in idle mode (no objects to scan), unschedule and don't reschedule yet.
|
|
if ( aioseo()->core->cache->get( 'as_objects_scan_idle' ) ) {
|
|
aioseo()->actionScheduler->unschedule( $this->action );
|
|
|
|
return;
|
|
}
|
|
|
|
if ( aioseo()->actionScheduler->isScheduled( $this->action ) ) {
|
|
return;
|
|
}
|
|
|
|
aioseo()->actionScheduler->scheduleRecurrent( $this->action, 10, MINUTE_IN_SECONDS );
|
|
}
|
|
|
|
/**
|
|
* Drops all object rows if the permalink structure changes.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return void
|
|
*/
|
|
public function reset() {
|
|
aioseo()->core->db->truncate( 'aioseo_search_statistics_objects' )->run();
|
|
|
|
as_unschedule_all_actions( $this->action );
|
|
|
|
// Clear the idle cache so that the scan restarts immediately.
|
|
aioseo()->core->cache->delete( 'as_objects_scan_idle' );
|
|
|
|
$this->scheduleScan();
|
|
}
|
|
|
|
/**
|
|
* Syncs the homeurl when user updates the page on front.
|
|
*
|
|
* @since 4.3.4
|
|
*
|
|
* @param string $option The option name.
|
|
* @param mixed $oldValue The old option value.
|
|
* @param mixed $value The new option value.
|
|
* @return void
|
|
*/
|
|
public function syncHomeUrl( $option, $oldValue = '', $value = '' ) {
|
|
if ( 'page_on_front' !== $option ) {
|
|
return;
|
|
}
|
|
|
|
// Get the object with '/' path, which is the homepage.
|
|
$wpObject = Models\WpObject::getObjectBy( 'path', '/' );
|
|
|
|
// If the new value is greater than 0, then it's a page id and we need to update the object.
|
|
if ( 0 < (int) $value ) {
|
|
// Sets this object to the new homepage id, so it can be updated later by post_id.
|
|
Models\WpObject::update( [
|
|
'id' => $wpObject->id ?? null,
|
|
'object_id' => $value,
|
|
'object_type' => 'post',
|
|
'object_path' => '/'
|
|
] );
|
|
|
|
// Update the post with all the needed data.
|
|
$this->updatePost( $value );
|
|
|
|
return;
|
|
}
|
|
|
|
// Otherwise, just detach the object from any post because it's not a page anymore.
|
|
Models\WpObject::update( [
|
|
'id' => $wpObject->id ?? null,
|
|
'object_id' => null,
|
|
'object_type' => null,
|
|
'object_subtype' => null,
|
|
'object_path' => '/',
|
|
] );
|
|
}
|
|
|
|
/**
|
|
* The main scan function that triggers all the sub scans.
|
|
*
|
|
* @since 4.5.0
|
|
* @version 4.9.4.2 Use recurring action with runtime lock and idle state.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function scan() {
|
|
// Runtime lock: Prevent concurrent execution of this action.
|
|
$lockKey = 'as_objects_scan_running';
|
|
if ( aioseo()->core->cache->get( $lockKey ) ) {
|
|
return;
|
|
}
|
|
|
|
// Set lock with a safety timeout in case the action fails mid-execution.
|
|
aioseo()->core->cache->update( $lockKey, true, 2 * MINUTE_IN_SECONDS );
|
|
|
|
$postsObjects = $this->scanForPosts();
|
|
$termsObjects = $this->scanForTerms();
|
|
$objects = array_merge( $postsObjects, $termsObjects );
|
|
|
|
if ( empty( $objects ) ) {
|
|
// No more objects to scan - set idle cache. The schedule method on the next init will unschedule.
|
|
aioseo()->core->cache->update( 'as_objects_scan_idle', true, DAY_IN_SECONDS );
|
|
aioseo()->core->cache->delete( $lockKey );
|
|
|
|
return;
|
|
}
|
|
|
|
Models\WpObject::bulkInsert( $objects );
|
|
|
|
aioseo()->core->cache->delete( $lockKey );
|
|
}
|
|
|
|
/**
|
|
* Checks if posts need to be updated/inserted.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return array The list of posts to insert.
|
|
*/
|
|
private function scanForPosts() {
|
|
$missingPosts = aioseo()->core->db->start( 'posts as p' )
|
|
->select( 'DISTINCT p.ID, p.post_type, asso.object_id' )
|
|
->leftJoin( 'aioseo_search_statistics_objects as asso', 'p.ID = asso.object_id' )
|
|
->where( 'p.post_status', 'publish' )
|
|
->whereIn( 'p.post_type', aioseo()->helpers->getPublicPostTypes( true ) )
|
|
->whereRaw( '(
|
|
asso.object_id IS NULL OR
|
|
asso.updated < p.post_modified_gmt
|
|
)' )
|
|
->limit( 50 )
|
|
->run()
|
|
->result();
|
|
|
|
if ( empty( $missingPosts ) ) {
|
|
return [];
|
|
}
|
|
|
|
$objectsToInsert = [];
|
|
foreach ( $missingPosts as $post ) {
|
|
$path = aioseo()->searchStatistics->helpers->getPageSlug( get_permalink( $post->ID ) );
|
|
$wpObject = Models\WpObject::getObjectBy( 'path', $path );
|
|
$object = [
|
|
'id' => $wpObject->id ?? null,
|
|
'object_id' => $post->ID,
|
|
'object_type' => 'post',
|
|
'object_subtype' => $post->post_type,
|
|
'object_path' => $path,
|
|
];
|
|
|
|
if ( ! empty( $object['id'] ) ) {
|
|
Models\WpObject::update( $object );
|
|
} else {
|
|
$objectsToInsert[] = $object;
|
|
}
|
|
}
|
|
|
|
return $objectsToInsert;
|
|
}
|
|
|
|
/**
|
|
* Checks if terms need to be updated/inserted.
|
|
*
|
|
* @since 4.5.0
|
|
*
|
|
* @return array The list of terms to insert.
|
|
*/
|
|
private function scanForTerms() {
|
|
$missingTerms = aioseo()->core->db->start( 'terms as t' )
|
|
->select( 'DISTINCT t.term_id, tt.taxonomy, asso.object_id' )
|
|
->join( 'term_taxonomy as tt', 't.term_id = tt.term_id' )
|
|
->leftJoin( 'aioseo_search_statistics_objects as asso', 't.term_id = asso.object_id' )
|
|
->leftJoin( 'aioseo_terms as at', 't.term_id = at.term_id' )
|
|
->whereIn( 'tt.taxonomy', aioseo()->helpers->getPublicTaxonomies( true ) )
|
|
->whereRaw( '(
|
|
asso.object_id IS NULL OR
|
|
asso.updated < at.updated
|
|
)' )
|
|
->limit( 50 )
|
|
->run()
|
|
->result();
|
|
|
|
if ( empty( $missingTerms ) ) {
|
|
return [];
|
|
}
|
|
|
|
$objectsToInsert = [];
|
|
foreach ( $missingTerms as $term ) {
|
|
$path = aioseo()->searchStatistics->helpers->getPageSlug( get_term_link( (int) $term->term_id, $term->taxonomy ) );
|
|
$wpObject = Models\WpObject::getObjectBy( 'path', $path );
|
|
$object = [
|
|
'id' => $wpObject->id ?? null,
|
|
'object_id' => $term->term_id,
|
|
'object_type' => 'term',
|
|
'object_subtype' => $term->taxonomy,
|
|
'object_path' => $path
|
|
];
|
|
|
|
if ( ! empty( $object['id'] ) ) {
|
|
Models\WpObject::update( $object );
|
|
} else {
|
|
$objectsToInsert[] = $object;
|
|
}
|
|
}
|
|
|
|
return $objectsToInsert;
|
|
}
|
|
|
|
/**
|
|
* Updates or deletes the data for the given post.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param int $postId The post ID.
|
|
* @return void
|
|
*/
|
|
public function updatePost( $postId ) {
|
|
if ( wp_is_post_autosave( $postId ) || wp_is_post_revision( $postId ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( 'publish' !== get_post_status( $postId ) ) {
|
|
aioseo()->core->db->delete( 'aioseo_search_statistics_objects' )
|
|
->where( 'object_id', $postId )
|
|
->where( 'object_type', 'post' )
|
|
->run();
|
|
|
|
return;
|
|
}
|
|
|
|
$post = get_post( $postId );
|
|
if ( ! is_a( $post, 'WP_Post' ) || ! in_array( $post->post_type, aioseo()->searchStatistics->helpers->getIncludedPostTypes(), true ) ) {
|
|
return;
|
|
}
|
|
|
|
$path = aioseo()->searchStatistics->helpers->getPageSlug( get_permalink( $postId ) );
|
|
$wpObject = Models\WpObject::getObjectBy( 'post_id', $postId );
|
|
$object = [
|
|
'id' => $wpObject->id ?? null,
|
|
'object_id' => $postId,
|
|
'object_type' => 'post',
|
|
'object_subtype' => $post->post_type,
|
|
'object_path' => $path,
|
|
];
|
|
|
|
if ( ! empty( $object['id'] ) ) {
|
|
Models\WpObject::update( $object );
|
|
} else {
|
|
Models\WpObject::bulkInsert( [ $object ] );
|
|
}
|
|
|
|
// Clear cache the for the posts.
|
|
aioseo()->core->cache->clearPrefix( 'aioseo_search_statistics_post_' );
|
|
}
|
|
|
|
/**
|
|
* Updates the data for the given term.
|
|
*
|
|
* @since 4.5.0
|
|
*
|
|
* @param int $termId The term ID.
|
|
* @return void
|
|
*/
|
|
public function updateTerm( $termId ) {
|
|
$term = get_term( $termId );
|
|
if ( ! is_a( $term, 'WP_Term' ) ) {
|
|
return;
|
|
}
|
|
|
|
$path = aioseo()->searchStatistics->helpers->getPageSlug( get_term_link( (int) $termId, $term->taxonomy ) );
|
|
$wpObject = Models\WpObject::getObjectBy( 'term_id', $termId );
|
|
$object = [
|
|
'id' => $wpObject->id ?? null,
|
|
'object_id' => $termId,
|
|
'object_type' => 'term',
|
|
'object_subtype' => $term->taxonomy,
|
|
'object_path' => $path
|
|
];
|
|
|
|
if ( ! empty( $object['id'] ) ) {
|
|
Models\WpObject::update( $object );
|
|
} else {
|
|
Models\WpObject::bulkInsert( [ $object ] );
|
|
}
|
|
|
|
// Clear cache the for the terms.
|
|
aioseo()->core->cache->clearPrefix( 'aioseo_search_statistics_term_' );
|
|
}
|
|
|
|
/**
|
|
* Deletes the data for the given term.
|
|
*
|
|
* @since 4.5.0
|
|
*
|
|
* @param int $termId The term ID.
|
|
* @return void
|
|
*/
|
|
public function deleteTerm( $termId ) {
|
|
aioseo()->core->db->delete( 'aioseo_search_statistics_objects' )
|
|
->where( 'object_id', $termId )
|
|
->where( 'object_type', 'term' )
|
|
->run();
|
|
|
|
// Clear cache the for the terms.
|
|
aioseo()->core->cache->clearPrefix( 'aioseo_search_statistics_term_' );
|
|
}
|
|
} |