diff --git a/assets/js/aksrt-runtime.js b/assets/js/aksrt-runtime.js index adfaf899..b5095aff 100644 --- a/assets/js/aksrt-runtime.js +++ b/assets/js/aksrt-runtime.js @@ -626,10 +626,24 @@ // §10.11 — [data-action] / [ajax-action] universal buttons // ══════════════════════════════════════════════════════════════ + // Only these data-action values are network AJAX. UI-only actions + // (toggle-theme, editor toolbar, download, etc.) must not hit admin-ajax. + var DATA_ACTION_AJAX = { + like: true, + favorite: true, + follow: true, + checkin: true + }; + document.addEventListener('click', function (e) { // [data-action] — like/follow/checkin with localStorage de-dup var btn = e.target.closest('[data-action]'); if (btn) { + var dataAction = btn.getAttribute('data-action') || ''; + if (!DATA_ACTION_AJAX[dataAction]) { + // Leave non-AJAX data-action handlers (theme toggle, toolbar, download) alone. + return; + } e.preventDefault(); handleDataAction(btn); return; @@ -648,6 +662,8 @@ if (btn.classList.contains('is-loading')) return; var action = btn.getAttribute('data-action'); + if (!action || !DATA_ACTION_AJAX[action]) return; + var id = btn.getAttribute('data-id') || ''; var storageKey = 'aksrt_action_' + action + '_' + id; @@ -663,9 +679,14 @@ btn.classList.add('is-loading'); var ajaxUrl = (window.aksrtData && aksrtData.ajaxUrl) || '/wp-admin/admin-ajax.php'; + if (!(window.aksrtData && aksrtData.nonce)) { + btn.classList.remove('is-loading'); + aksrt.notyf(__('action_failed', '操作失败'), 'danger'); + return; + } var params = new URLSearchParams(); params.set('action', 'aksrt_' + action); - if (window.aksrtData && aksrtData.nonce) params.set('nonce', aksrtData.nonce); + params.set('nonce', aksrtData.nonce); params.set('id', id); var extraData = btn.getAttribute('data-action-data'); diff --git a/assets/js/core/pagination.js b/assets/js/core/pagination.js index 8b77df71..cd415ede 100644 --- a/assets/js/core/pagination.js +++ b/assets/js/core/pagination.js @@ -4,6 +4,10 @@ (function () { "use strict"; + function hasAjaxConfig() { + return !!(window.aksrtData && aksrtData.ajaxUrl && aksrtData.nonce); + } + function initAjaxPagination(root) { var containers = (root || document).querySelectorAll(".aksrt-pagination-ajax, .ajax-pagination"); if (!containers.length) return; @@ -11,6 +15,7 @@ var triggerText = (window.aksrtData && aksrtData.ajaxTrigger) || "加载更多"; var nomoreText = (window.aksrtData && aksrtData.ajaxNomore) || "没有更多了"; var loadingText = (window.aksrtData && aksrtData.ajaxLoading) || "正在加载..."; + var errorText = (window.aksrtData && aksrtData.ajaxLoadError) || "加载失败,请重试"; containers.forEach(function (container) { if (container.dataset.aksrtPaginationBound === "1") return; @@ -23,8 +28,12 @@ var postId = container.dataset.postId || ""; var commentOrder = container.dataset.commentOrder || "newest"; var loading = false; + var failed = false; var loadMoreBtn = container.querySelector(".aksrt-load-more-btn, .ajax-next"); var indicator = container.querySelector(".aksrt-loading-indicator"); + var observer = null; + var iasEnabled = false; + var successfulAutoLoads = 0; var target = container.previousElementSibling; if (target && target.classList && target.classList.contains("content-stream__footer")) { target = target.previousElementSibling; @@ -54,8 +63,43 @@ } } - function loadNextPage() { - if (loading || nextPage > maxPages || !target) return; + function stopInfinite(markNomore) { + if (observer) { + observer.disconnect(); + observer = null; + } + iasEnabled = false; + container.classList.remove("ias-pending"); + if (markNomore && loadMoreBtn) { + loadMoreBtn.textContent = nomoreText; + loadMoreBtn.classList.remove("is-loading"); + loadMoreBtn.classList.add("ajax-nomore"); + loadMoreBtn.style.pointerEvents = "none"; + } else if (loadMoreBtn && !loadMoreBtn.classList.contains("ajax-nomore")) { + loadMoreBtn.classList.remove("is-loading"); + loadMoreBtn.textContent = failed ? errorText : triggerText; + loadMoreBtn.style.pointerEvents = ""; + } + } + + function markFailed(message) { + failed = true; + stopInfinite(false); + if (loadMoreBtn) { + loadMoreBtn.textContent = message || errorText; + loadMoreBtn.classList.remove("is-loading"); + loadMoreBtn.classList.remove("ajax-nomore"); + loadMoreBtn.style.pointerEvents = ""; + } + } + + function loadNextPage(fromAuto) { + if (loading || failed || nextPage > maxPages || !target) return Promise.resolve(false); + if (!hasAjaxConfig()) { + markFailed(errorText); + return Promise.resolve(false); + } + loading = true; if (indicator) indicator.style.display = "flex"; if (loadMoreBtn) { @@ -88,49 +132,65 @@ if (authorOnly === "1") body.set("author_only", "1"); } - fetch(aksrtData.ajaxUrl, { + return fetch(aksrtData.ajaxUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, credentials: "same-origin", body: body.toString(), }) .then(function (r) { - return r.json(); + if (!r.ok) { + throw new Error("HTTP " + r.status); + } + return r.text().then(function (text) { + try { + return JSON.parse(text); + } catch (err) { + throw new Error("Invalid JSON"); + } + }); }) .then(function (res) { - if (res.success && res.data && res.data.html) { - var temp = document.createElement("div"); - temp.innerHTML = res.data.html; - var newItems = temp.children; - while (newItems.length > 0) { - target.appendChild(newItems[0]); - } - if (isComment && window.AksrtFrontend) { - AksrtFrontend.rehydrate(target, "ajax"); - } - nextPage++; - container.dataset.nextPage = String(nextPage); + if (!(res && res.success && res.data && typeof res.data.html === "string")) { + var msg = res && res.data && res.data.message ? res.data.message : errorText; + markFailed(msg); + return false; + } - if (typeof initLazyLoad === "function") initLazyLoad(); + var temp = document.createElement("div"); + temp.innerHTML = res.data.html; + var newItems = temp.children; + while (newItems.length > 0) { + target.appendChild(newItems[0]); + } + if (isComment && window.AksrtFrontend) { + AksrtFrontend.rehydrate(target, "ajax"); + } + nextPage++; + container.dataset.nextPage = String(nextPage); + if (fromAuto) { + successfulAutoLoads++; + } - if (nextPage > maxPages || !res.data.has_more) { - if (loadMoreBtn) { - loadMoreBtn.textContent = nomoreText; - loadMoreBtn.classList.remove("is-loading"); - loadMoreBtn.classList.add("ajax-nomore"); - loadMoreBtn.style.pointerEvents = "none"; - } else { - container.innerHTML = '' + nomoreText + ''; - } + if (typeof initLazyLoad === "function") initLazyLoad(target); + + if (nextPage > maxPages || !res.data.has_more) { + stopInfinite(true); + if (!loadMoreBtn) { + container.innerHTML = '' + nomoreText + ''; } } + return true; + }) + .catch(function () { + markFailed(errorText); + return false; }) - .catch(function () {}) .finally(function () { loading = false; hideSkeleton(); if (indicator) indicator.style.display = "none"; - if (loadMoreBtn && !loadMoreBtn.classList.contains("ajax-nomore")) { + if (loadMoreBtn && !loadMoreBtn.classList.contains("ajax-nomore") && !failed) { loadMoreBtn.classList.remove("is-loading"); loadMoreBtn.textContent = triggerText; } @@ -162,43 +222,55 @@ window.location.href = archiveUrl.toString(); return; } - loadNextPage(); + // Manual click can recover from a previous auto-load failure. + failed = false; + loadNextPage(false); }); } if (mode === "infinite" && "IntersectionObserver" in window) { if (isComment && window.aksrtData && aksrtData.commentIasAuto === false) { - } else { - var sentinel = document.createElement("div"); - sentinel.className = "aksrt-scroll-sentinel"; - container.appendChild(sentinel); - var lazyloadAction = container.dataset.lazyloadAction || ""; - var iasMax = isComment - ? ((window.aksrtData && typeof aksrtData.commentIasMax !== "undefined") ? parseInt(aksrtData.commentIasMax, 10) : 0) - : ((window.aksrtData && typeof aksrtData.iasMax !== "undefined") ? parseInt(aksrtData.iasMax, 10) : 0); - var initialPage = nextPage - 1; - var observer = new IntersectionObserver( - function (entries) { - if (entries[0].isIntersecting && !loading) { - if (iasMax > 0 && (nextPage - initialPage) >= iasMax) { - observer.disconnect(); - if (loadMoreBtn) { - loadMoreBtn.textContent = nomoreText; - loadMoreBtn.classList.add("ajax-nomore"); - loadMoreBtn.style.pointerEvents = "none"; - } - return; - } - loadNextPage(); - } - }, - { rootMargin: "400px" } - ); - if (lazyloadAction === "ias") { - container.classList.add("ias-pending"); - } - observer.observe(sentinel); + return; } + if (!hasAjaxConfig()) { + return; + } + + var sentinel = document.createElement("div"); + sentinel.className = "aksrt-scroll-sentinel"; + container.appendChild(sentinel); + var lazyloadAction = container.dataset.lazyloadAction || ""; + var iasMax = isComment + ? ((window.aksrtData && typeof aksrtData.commentIasMax !== "undefined") ? parseInt(aksrtData.commentIasMax, 10) : 0) + : ((window.aksrtData && typeof aksrtData.iasMax !== "undefined") ? parseInt(aksrtData.iasMax, 10) : 0); + + observer = new IntersectionObserver( + function (entries) { + if (!iasEnabled || failed) return; + if (!(entries[0] && entries[0].isIntersecting) || loading) return; + if (iasMax > 0 && successfulAutoLoads >= iasMax) { + stopInfinite(true); + return; + } + loadNextPage(true); + }, + { rootMargin: "200px" } + ); + + if (lazyloadAction === "ias") { + container.classList.add("ias-pending"); + } + + // Policy A: do not auto-fire on first paint; wait for real user scroll. + var enableOnScroll = function () { + if (iasEnabled || failed) return; + iasEnabled = true; + container.classList.remove("ias-pending"); + observer.observe(sentinel); + }; + window.addEventListener("scroll", enableOnScroll, { passive: true, once: true }); + window.addEventListener("touchmove", enableOnScroll, { passive: true, once: true }); + window.addEventListener("wheel", enableOnScroll, { passive: true, once: true }); } }); } diff --git a/inc/classes/class-aksrt-admin.php b/inc/classes/class-aksrt-admin.php index 960d376e..00511553 100644 --- a/inc/classes/class-aksrt-admin.php +++ b/inc/classes/class-aksrt-admin.php @@ -281,7 +281,11 @@ class AKSRT_Admin { public function ajax_load_more_posts(): void { - check_ajax_referer('aksrt_public_nonce', 'nonce'); + $nonce = isset($_POST['nonce']) ? (string) wp_unslash($_POST['nonce']) : ''; + if ($nonce === '' || !wp_verify_nonce($nonce, 'aksrt_public_nonce')) { + wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403); + } + $page = max(1, (int) ($_POST['page'] ?? 1)); $query_vars = json_decode(wp_unslash((string) ($_POST['query_vars'] ?? '{}')), true); $render_config = json_decode(wp_unslash((string) ($_POST['render_config'] ?? '{}')), true); @@ -300,13 +304,19 @@ class AKSRT_Admin { $render_config = array_merge($resolved, $render_config); } $query_vars['paged'] = $page; + if (empty($query_vars['post_status'])) { + $query_vars['post_status'] = 'publish'; + } + if (empty($query_vars['post_type'])) { + $query_vars['post_type'] = 'post'; + } $query = new WP_Query($query_vars); ob_start(); if (function_exists('aksrt_render_post_list')) { aksrt_render_post_list($query, $render_config, true); } - $html = ob_get_clean(); + $html = (string) ob_get_clean(); wp_send_json_success([ 'html' => $html, @@ -316,11 +326,15 @@ class AKSRT_Admin { } public function ajax_load_more_comments(): void { - check_ajax_referer('aksrt_public_nonce', 'nonce'); + $nonce = isset($_POST['nonce']) ? (string) wp_unslash($_POST['nonce']) : ''; + if ($nonce === '' || !wp_verify_nonce($nonce, 'aksrt_public_nonce')) { + wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403); + } + $post_id = absint($_POST['post_id'] ?? 0); $page = max(1, (int) ($_POST['page'] ?? 1)); if ($post_id <= 0) { - wp_send_json_error(['message' => __('无效的文章 ID', 'aksrt')]); + wp_send_json_error(['message' => __('无效的文章 ID', 'aksrt')], 400); } $order = sanitize_key((string) ($_POST['comment_order'] ?? $_POST['order'] ?? 'newest')); diff --git a/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php b/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php index d223bd33..4a408bb7 100644 --- a/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php +++ b/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php @@ -798,7 +798,8 @@ trait AKSRT_Scripts_Part_1b { foreach ($core_modules as $handle => $file) { $path = get_template_directory() . '/assets/js/core/' . $file; if (file_exists($path)) { - $deps = ['aksrt-rehydrate']; + // Depend on aksrt-runtime so window.aksrtData (localized below) is available. + $deps = ['aksrt-rehydrate', 'aksrt-runtime']; if ($handle === 'aksrt-core-slider') { $deps = array_merge($deps, $needs_swiper ? ['aksrt-swiper'] : []); } @@ -1183,7 +1184,12 @@ trait AKSRT_Scripts_Part_1b { $runtime_routes = $this->resolve_frontend_routes(); // 数据注入 - 全站统一消费 window.aksrtData - wp_localize_script('aksrt-script', 'aksrtData', [ + // Attach to aksrt-runtime (loaded before all core modules) so ajaxUrl/nonce + // exist before pagination/search/article handlers run. + $aksrt_data_handle = wp_script_is('aksrt-runtime', 'registered') || wp_script_is('aksrt-runtime', 'enqueued') + ? 'aksrt-runtime' + : 'aksrt-script'; + wp_localize_script($aksrt_data_handle, 'aksrtData', [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aksrt_public_nonce'), 'restNonce' => wp_create_nonce('wp_rest'),