265 lines
7.1 KiB
PHP
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
|
|
]
|
|
];
|
|
}
|
|
} |