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

265 lines
7.1 KiB
PHP

<?php
namespace AIOSEO\Plugin\Pro\SearchStatistics;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Pro\Models\SearchStatistics as SearchStatisticsModels;
use AIOSEO\Plugin\Common\SearchStatistics as CommonSearchStatistics;
/**
* Index Status class.
*
* @since 4.8.2
*/
class IndexStatus extends CommonSearchStatistics\IndexStatus {
/**
* The action hook to execute when the event is run.
*
* @since 4.8.2
*
* @var string
*/
private $actionHook = 'aioseo_search_statistics_fetch_object_index_status';
/**
* Class constructor.
*
* @since 4.8.2
* @version 4.9.4.2 Change admin_init to init and switch to recurring action with idle state.
*/
public function __construct() {
add_action( 'admin_init', [ $this, 'scheduleScan' ], 20 );
add_action( $this->actionHook, [ $this, 'cronTrigger' ] );
}
/**
* The index status cron callback.
* Hooked into `{@see self::$actionHook}` action hook.
*
* @since 4.8.2
* @version 4.9.4.2 Use recurring action with runtime lock and idle state.
*
* @return void
*/
public function cronTrigger() {
// Runtime lock: Prevent concurrent execution of this action.
$lockKey = 'as_index_status_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 );
if (
! aioseo()->license->hasCoreFeature( 'search-statistics', 'index-status' ) ||
! aioseo()->searchStatistics->api->auth->isConnected()
) {
aioseo()->core->cache->delete( $lockKey );
return;
}
$quotaKey = 'search_statistics_inspection_results_cron_quota';
$quota = intval( aioseo()->core->cache->get( $quotaKey ) ?? 1600 );
if ( 1 > $quota ) {
aioseo()->core->cache->delete( $lockKey );
return;
}
$results = aioseo()->searchStatistics->urlInspection->fetchInspectionResults();
$countPathsWithoutResult = count( $results['pathsWithoutResult'] );
if (
empty( $results['error'] ) &&
$countPathsWithoutResult > 0
) {
aioseo()->core->cache->update( $quotaKey, max( 0, $quota - $countPathsWithoutResult ), aioseo()->searchStatistics->helpers->getNext8Am() - time() );
}
if ( empty( $results['pathsWithoutResult'] ) ) {
// All found paths have results - set idle cache. The schedule method on the next init will unschedule.
aioseo()->core->cache->update( 'as_index_status_idle', true, DAY_IN_SECONDS );
}
aioseo()->core->cache->delete( $lockKey );
}
/**
* Maybe schedule fetching the index status data.
*
* @since 4.8.2
* @version 4.9.4.2 Switch to recurring action with cache-based idle state.
*
* @return void
*/
public function scheduleScan() {
if (
! aioseo()->license->hasCoreFeature( 'search-statistics', 'index-status' ) ||
! aioseo()->searchStatistics->api->auth->isConnected()
) {
return;
}
// If we're in idle mode (all paths have results), unschedule and don't reschedule yet.
if ( aioseo()->core->cache->get( 'as_index_status_idle' ) ) {
aioseo()->actionScheduler->unschedule( $this->actionHook );
return;
}
if ( aioseo()->actionScheduler->isScheduled( $this->actionHook ) ) {
return;
}
aioseo()->actionScheduler->scheduleRecurrent( $this->actionHook, 10, 10 * MINUTE_IN_SECONDS );
}
/**
* Retrieves the overview.
*
* @since 4.8.2
*
* @return array The overview.
*/
public function getOverview() {
$aioTable = aioseo()->core->db->db->prefix . 'aioseo_search_statistics_objects';
$wpTable = aioseo()->core->db->db->prefix . 'posts';
$objectSubtypesArray = aioseo()->helpers->getPublicPostTypes( true );
$placeholders = implode( ',', array_fill( 0, count( $objectSubtypesArray ), '%s' ) );
// This query needs to match the one on {@see SearchStatisticsModels\WpObject::getObjects()}.
$results = aioseo()->core->db->output( 'ARRAY_A' )
->execute( aioseo()->core->db->db->prepare(
"SELECT COUNT(*) as count, aio.coverage_state as coverageState
FROM $aioTable as aio
INNER JOIN $wpTable as wp ON aio.object_id = wp.ID
WHERE aio.object_type = 'post'
AND aio.object_subtype IN ($placeholders)
GROUP BY coverageState
ORDER BY count DESC",
...$objectSubtypesArray
), true )
->result();
return [
'post' => [
'results' => array_map( function ( $v ) {
return empty( $v['coverageState'] ) ? [
'count' => $v['count'],
'coverageState' => 'empty', // This value works as a slug for Vue.
] : $v;
}, $results ),
'total' => array_sum( array_column( $results, 'count' ) ),
]
];
}
/**
* Retrieves all the objects, formatted.
*
* @since 4.8.2
*
* @param array $args The arguments.
* @return array The formatted objects.
*/
public function getFormattedObjects( $args = [] ) {
static $staticOutput = [];
$staticKey = aioseo()->helpers->createHash( $args );
if ( isset( $staticOutput[ $staticKey ] ) ) {
return $staticOutput[ $staticKey ];
}
$objects = SearchStatisticsModels\WpObject::getObjects( $args );
foreach ( $objects['rows'] as &$row ) {
$row = SearchStatisticsModels\WpObject::parseObject( $row );
}
$staticOutput[ $staticKey ] = [
'paginated' => [
'rows' => $objects['rows'],
'totals' => $objects['totals'],
]
];
return $staticOutput[ $staticKey ];
}
/**
* Returns the data for Vue.
*
* @since 4.8.2
*
* @return array The data for Vue.
*/
public function getVueData() {
if (
! aioseo()->license->hasCoreFeature( 'search-statistics', 'index-status' ) ||
! aioseo()->searchStatistics->api->auth->isConnected()
) {
return [
'objects' => parent::getFormattedObjects(),
'overview' => parent::getOverview(),
'options' => parent::getUiOptions()
];
}
// For connected sites we'll fetch the data through Vue/AJAX.
return [
'options' => $this->getUiOptions(),
];
}
/**
* Retrieves options ideally only for Vue usage on the front-end.
*
* @since 4.8.2
*
* @return array The options.
*/
protected function getUiOptions() {
$objects = aioseo()->core->db->start( 'aioseo_search_statistics_objects as aio' )
->select( 'DISTINCT aio.object_subtype' )
->where( 'aio.object_type', 'post' )
->run()
->result();
$postTypeOptions = [
[
'label' => __( 'All Post Types', 'aioseo-pro' ),
'value' => ''
]
];
foreach ( $objects as $object ) {
$objectPostType = get_post_type_object( $object->object_subtype ?? '' );
if ( ! is_object( $objectPostType ) ) {
continue;
}
$postTypeOptions[] = [
'label' => $objectPostType->labels->singular_name,
'value' => $objectPostType->name
];
}
$parentUiOptions = parent::getUiOptions();
$additionalFilters = array_merge( $parentUiOptions['table']['additionalFilters'], [
'postTypeOptions' => [
'name' => 'postType',
'options' => $postTypeOptions
]
] );
return [
'table' => [
'additionalFilters' => $additionalFilters
]
];
}
}