getPostOrGetSanitize('requestId', ABJ_404_Solution_AjaxRequestLedger::UNKNOWN_ID)); $checkpointRequestId = ABJ_404_Solution_AjaxDiagnosticRequestPolicy::instrumentedRequestId(array( 'action' => 'ajaxUpdatePaginationLinks', 'request_id' => $requestId, )); /** @var ABJ_404_Solution_ViewReadServiceInterface $viewReadService */ $viewReadService = ABJ_404_Solution_AjaxCheckpointLogger::around( $checkpointRequestId, 'service_resolve_view_read_service', static fn() => abj_service('view_read_service') ); $abj404logic = ABJ_404_Solution_AjaxCheckpointLogger::around( $checkpointRequestId, 'service_resolve_plugin_logic', static fn() => abj_service('plugin_logic') ); global $abj404view; $rowsPerPage = absint($requestReader->getPostOrGetSanitize('rowsPerPage')); $subpage = $requestReader->getPostOrGetSanitize('subpage'); $nonce = $requestReader->getPostOrGetSanitize('nonce'); $page = $requestReader->getPostOrGetSanitize('page', ''); $filterText = $requestReader->getPostOrGetSanitize('filterText', ''); $filter = $requestReader->getPostOrGetSanitize('filter', ''); $orderby = $requestReader->getPostOrGetSanitize('orderby', ''); $detectOnly = ((string)$requestReader->getPostOrGetSanitize('detectOnly', '0') === '1'); $part = self::normalizePart((string)$requestReader->getPostOrGetSanitize('part', 'all')); $cacheMode = self::normalizeCacheMode((string)$requestReader->getPostOrGetSanitize('cacheMode', 'normal')); $currentSignature = self::normalizeCurrentSignature((string)$requestReader->getPostOrGetSanitize('currentSignature', '')); $retryCount = min(2, absint($requestReader->getPostOrGetSanitize('retryCount', '0'))); $detachAbPayloadKey = ABJ_404_Solution_DetachAbExperiment::payloadKey(array( 'subpage' => (string)$subpage, 'page' => (string)$page, 'rows_per_page' => $rowsPerPage, 'filter_text' => (string)$filterText, 'filter' => (string)$filter, 'orderby' => (string)$orderby, 'detect_only' => $detectOnly ? 1 : 0, 'cache_mode' => $cacheMode, 'current_signature' => $currentSignature, )); // Ledger identity fields join this workload fingerprint to the browser session. $ledger = ABJ_404_Solution_AjaxRequestLedger::readFields($requestReader); $isPluginAdmin = false; $context = array_merge(array( 'action' => 'ajaxUpdatePaginationLinks', 'page' => $page, 'subpage' => $subpage, 'rowsPerPage' => $rowsPerPage, 'filterText_length' => strlen((string)$filterText), 'filter' => $filter, 'orderby' => $orderby, 'part' => $part, 'detach_ab_payload_key' => $detachAbPayloadKey, 'request_id' => $requestId, 'retry_count' => $retryCount, 'detectOnly' => $detectOnly ? 1 : 0, 'cacheMode' => $cacheMode, 'currentSignature_length' => strlen($currentSignature), 'request_uri' => array_key_exists('REQUEST_URI', $_SERVER) ? $_SERVER['REQUEST_URI'] : '', 'user_id' => function_exists('get_current_user_id') ? get_current_user_id() : 0, 'handler_class' => __CLASS__, ), $ledger); $context = ABJ_404_Solution_AjaxAdminEndpointSupport::startAjaxDebugContext($context, 'Ajax_GetPaginationLinks::handle'); ABJ_404_Solution_AjaxRequestLedger::recordHeaderMismatchIfAny( $checkpointRequestId, (string)$ledger['header_request_id']); // Entering the handler proper: authorization, queries and rendering. // A census row stranded here is a request that never finished its own // work, which is a different bug from one that finished in 1.3s and // then held its worker for three minutes (report 193's actual shape). ABJ_404_Solution_SameSiteRequestCensus::markPhase( ABJ_404_Solution_SameSiteRequestCensus::PHASE_HANDLER); try { if (!ABJ_404_Solution_AjaxAdminEndpointSupport::requireAdminWithNonceOrRespond( 'abj404_updatePaginationLink', $context, 'ajaxUpdatePaginationLinks', array('nonce_value' => $nonce) )) { return; } $isPluginAdmin = true; ABJ_404_Solution_RequestPhaseTimeline::stamp('auth'); ABJ_404_Solution_SameSiteRequestCensus::markAuthorized(); // First-attempt diagnostics stay on the zero-write fast path. A browser // retry means the user already observed a transient failure, so // arm the durable flight recorder now that nonce and admin access // have been proved. Never trust raw retryCount before this point. if ($retryCount > 0) { $context = ABJ_404_Solution_AjaxAdminEndpointSupport:: armAuthorizedRetryDiagnostics($context); $checkpointRequestId = ABJ_404_Solution_AjaxDiagnosticRequestPolicy:: instrumentedRequestId($context); self::promoteNamedRetryParent($context); } // Rate limiting to prevent abuse. High ceilings: this endpoint is hit by first-paint // table loads, filter typing, pagination, and background detect-only checks. $maxRequestsPerMinute = $detectOnly ? 3000 : 1500; if (!self::checkRateLimitOrRespond($maxRequestsPerMinute, $context)) { return; } ABJ_404_Solution_AjaxStageDiagnostics::beginRequest($context); ABJ_404_Solution_AdminTableResponseParts::beginRead(); if (ABJ_404_Solution_AjaxClientReportBeaconResponder::respondIfReportOnly($requestId)) { return; } // Update the perpage option (but only if provided). Some environments may omit // rowsPerPage on Enter key events; avoid unnecessary option writes. Wrapped as // its own checkpoint pair: the elapsed time includes any update_option hook // callbacks other plugins have registered (matrix coverage req. 2). if ($part === 'all' || $part === 'table') { ABJ_404_Solution_AjaxCheckpointLogger::around( $checkpointRequestId, 'update_per_page_option', static function () use ($abj404logic, $rowsPerPage) { self::updatePerPageOption($abj404logic, $rowsPerPage); } ); } /** @var ABJ_404_Solution_View $view */ $view = ABJ_404_Solution_AjaxAdminEndpointSupport::resolveViewInstance($abj404view); $token = (string)$requestReader->getPostOrGetSanitize('flightToken', ''); if ($token !== '' && preg_match('/^[0-9a-f]{64}$/D', $token) !== 1) { throw new ABJ_404_Solution_ViewQueryFailureException('Invalid flightToken.', array( 'code' => 'TABLE_FLIGHT_TOKEN', 'hint' => 'Start a new table request.')); } // Resolve once, then pin the renderer to these same effective // options. Omitted sort/page inputs cannot collide with a different // saved preference, and another request cannot change this build. $tableOptions = $abj404logic->settingsUpdate()->getTableOptions((string)$subpage); $user = wp_get_current_user(); $userFields = get_object_vars($user); $capabilities = is_array($userFields['allcaps'] ?? null) ? $userFields['allcaps'] : array(); ksort($capabilities); $identity = array( 'version' => 1, 'blog' => get_current_blog_id(), 'user' => $context['user_id'], 'access' => $capabilities, 'session' => wp_get_session_token(), 'locale' => get_locale(), 'page' => $page, 'subpage' => $subpage, 'part' => $part, 'options' => $tableOptions, 'detectOnly' => $detectOnly, 'cacheMode' => $cacheMode, 'currentSignature' => $currentSignature, ); $flight = new ABJ_404_Solution_AdminTableFlight(); $data = $flight->admitOrReplayResponse($identity, array('token' => $token, 'started_at' => self::requestStartForFlight()), static function () use ($part, $subpage, $view, $viewReadService, $detectOnly, $currentSignature, $tableOptions, &$context): array { return ABJ_404_Solution_AdminTableResponseParts::buildForMode(array( 'part' => $part, 'subpage' => $subpage, 'view' => $view, 'viewReadService' => $viewReadService, 'tableOptions' => $tableOptions, 'detectOnly' => $detectOnly, 'currentSignature' => $currentSignature, 'queryTimeoutSeconds' => ABJ_404_Solution_AdminTableResponseParts::queryBudgetOptions($detectOnly)['_abj404_query_timeout'], ), $context); }); $data['requestId'] = $requestId; $data['retryCount'] = $retryCount; ABJ_404_Solution_AjaxStageDiagnostics::finishRequest('complete'); ABJ_404_Solution_AjaxAdminEndpointSupport::markAjaxResponseSent(); ABJ_404_Solution_AjaxAdminEndpointSupport::getAndClearAjaxBufferedOutput(); ABJ_404_Solution_AjaxResponseEmitter::sendJsonResponseAndExit($data, 200); return; } catch (Throwable $e) { ABJ_404_Solution_AjaxStageDiagnostics::finishRequest('error'); self::handlePaginationLinksException( $e, $isPluginAdmin, $context ); return; } } /** @return float A valid request-start anchor for the table flight. */ private static function requestStartForFlight(): float { $requestStart = $_SERVER['REQUEST_TIME_FLOAT'] ?? null; if (!is_int($requestStart) && !is_float($requestStart)) { return abj_clock()->nowFloat(); } if (!is_finite($requestStart) || $requestStart <= 0) { return abj_clock()->nowFloat(); } return (float)$requestStart; } private static function normalizeCacheMode(string $cacheModeRaw): string { return in_array($cacheModeRaw, array('normal', 'cache_or_pending', 'refresh_cache'), true) ? $cacheModeRaw : 'normal'; } private static function normalizePart(string $partRaw): string { return in_array($partRaw, array('all', 'table', 'counts', 'pagination'), true) ? $partRaw : 'all'; } private static function normalizeCurrentSignature(string $currentSignature): string { $currentSignature = strtolower(trim($currentSignature)); if (strlen($currentSignature) > 128) { return substr($currentSignature, 0, 128); } return $currentSignature; } /** * Promote the named parent attempt's timeline into the durable ledger. * The retry names the attempt it follows up on; the parent finished (or * vanished) before this request started, and its own record is the only * account of what the browser observed. A retry that names no parent * promotes nothing. * * @param array $context * @return void */ private static function promoteNamedRetryParent(array $context): void { $parentId = $context['retry_parent_id'] ?? ''; if (!is_scalar($parentId) || (string)$parentId === '') { return; } ABJ_404_Solution_RequestTimelinePromoter::promoteNamed(array( 'request_id' => (string)$parentId, 'reason' => ABJ_404_Solution_StrandedRequestLedger::REASON_RETRY_PARENT, )); } /** * @param array $context */ private static function checkRateLimitOrRespond(int $maxRequestsPerMinute, array $context): bool { $checkpointRequestId = ABJ_404_Solution_AjaxDiagnosticRequestPolicy::instrumentedRequestId($context); $rateLimited = ABJ_404_Solution_AjaxCheckpointLogger::around( $checkpointRequestId, 'rate_limit_check', static fn() => ABJ_404_Solution_Ajax_Php::consumeRateLimit('update_pagination', $maxRequestsPerMinute, 60) ); if (!$rateLimited) { return true; } ABJ_404_Solution_AjaxCheckpointLogger::record($checkpointRequestId, 'rate_limit_branch', array( 'max_requests_per_minute' => $maxRequestsPerMinute, )); ABJ_404_Solution_AjaxAdminEndpointSupport::safeLogAjaxFailureBranch( 'rate_limit', 'AJAX rate limit in ajaxUpdatePaginationLinks.', static fn() => $context ); ABJ_404_Solution_AjaxAdminEndpointSupport::markAjaxResponseSent(); $payload = ABJ_404_Solution_AjaxErrorEnvelope::build('Rate limit exceeded. Please try again later.', null, false); ABJ_404_Solution_AjaxAdminEndpointSupport::getAndClearAjaxBufferedOutput(); ABJ_404_Solution_AjaxResponseEmitter::sendJsonResponseAndExit($payload, 429); return false; } /** * @param mixed $abj404logic */ private static function updatePerPageOption($abj404logic, int $rowsPerPage): void { if ($rowsPerPage <= 0 || !is_object($abj404logic)) { return; } if (method_exists($abj404logic, 'adminActions')) { $abj404logic->adminActions()->updatePerPageOption($rowsPerPage); return; } if (method_exists($abj404logic, 'updatePerPageOption')) { $abj404logic->updatePerPageOption($rowsPerPage); } } /** * Handle endpoint exceptions with the standard admin diagnostics envelope. * @param Throwable $e * @param bool $isPluginAdmin * @param array $context * @return void */ private static function handlePaginationLinksException( Throwable $e, bool $isPluginAdmin, array $context ): void { $failure = ABJ_404_Solution_AjaxAdminEndpointSupport::safeLogAjaxFailureBranch( 'exception_caught', 'AJAX exception in ajaxUpdatePaginationLinks.', static function () use ($e, $isPluginAdmin, $context): array { $resolvedIsPluginAdmin = ABJ_404_Solution_AjaxAdminEndpointSupport:: resolveIsPluginAdminFallback($isPluginAdmin, true); if (isset($GLOBALS['abj404_ajax_context']) && is_array($GLOBALS['abj404_ajax_context'])) { $GLOBALS['abj404_ajax_context']['is_plugin_admin'] = $resolvedIsPluginAdmin; } $details = ABJ_404_Solution_AjaxFailureDetailsBuilder::pagination($e, $context); return array( 'is_plugin_admin' => $resolvedIsPluginAdmin, 'details' => $details, ); }, $e ); if (!is_array($failure)) { $failure = array(); } $isPluginAdmin = (bool)($failure['is_plugin_admin'] ?? $isPluginAdmin); $details = isset($failure['details']) && is_array($failure['details']) ? $failure['details'] : array(); $capturedOutput = ABJ_404_Solution_AjaxAdminEndpointSupport::getAndClearAjaxBufferedOutput(); if ($capturedOutput !== '') { $details['buffered_output'] = substr($capturedOutput, 0, 8000); } ABJ_404_Solution_AjaxAdminEndpointSupport::markAjaxResponseSent(); $payload = ABJ_404_Solution_AjaxErrorEnvelope::build( 'Server error while updating the table.', $details, $isPluginAdmin ); $responseRequestId = $context['request_id'] ?? null; $responseRetryCount = $context['retry_count'] ?? null; $payload['requestId'] = is_string($responseRequestId) ? $responseRequestId : 'unknown00'; $payload['retryCount'] = min( 2, ABJ_404_Solution_ExactInteger::readOr($responseRetryCount, 0, 0) ); ABJ_404_Solution_AjaxResponseEmitter::sendJsonResponseAndExit($payload, 500); } }