feat: implement WooCommerce AJAX product search

This commit is contained in:
XShop
2026-09-13 20:01:15 +03:30
parent 00439fcea9
commit f5c283af25
14 changed files with 760 additions and 37 deletions
+15 -23
View File
@@ -15,33 +15,24 @@ Two installable artifacts: theme (`xshop`) + plugin (`xshop-core`). Theme never
## 2. Theme – `xshop-theme`
### 2.1 File map (M2)
### 2.1 File map (M3)
```
xshop-theme/
style.css
functions.php # thin loader
screenshot.png / rtl.css
header.php / footer.php / index.php / front-page.php / single.php / page.php / archive.php / search.php / 404.php / sidebar.php / comments.php
style.css / functions.php / rtl.css / header.php / footer.php / index.php etc
inc/
setup/ # theme-support, menus, sidebars, body-classes, assets (woo.css global, documented)
helpers/ # sanitize, template (xshop_has_woocommerce etc)
customization/ # options
compatibility/ # gutenberg, elementor
woocommerce/ # wrappers, loop_start/end, toolbar, remove gallery, related args, product.js enqueue, cart quantity filter
performance/ # defer
setup/assets.php — tokens→base→layout→components→woo→utilities→style.css + theme.js + search.js (front-end only where data-xshop-search exists, documented)
woocommerce/setup.php — loop_start/end, toolbar, gallery remove, related 4/4, product.js conditional
assets/
css/ # tokens, base, layout, components, woo (shop/product/cart/account/checkout), utilities
js/ # theme.js + product.js (gallery thumbs, keyboard)
css/woo.css — + search dropdown (.xshop-search, _dropdown, _results, _item, responsive fixed @640)
js/search.js — debounce 250ms, AbortController, stale seq, keyboard, a11y
template-parts/components/
site-header/-footer, breadcrumbs, pagination, search-form, post-card,
product-card.php (WC_Product, badges, price, rating, hooks), product-grid.php, product-gallery.php (featured+thumbs, keyboard), mini-cart.php, empty-state, loading, section-heading, hero, promo-banner
woocommerce/
content-product.php (3.6.0) — delegates to product-card
archive-product.php (3.4.0) — shop/category header + toolbar + grid + empty-state CTA
content-single-product.php (3.6.0) — grid gallery+summary, hooks preserved
single-product.php (1.6.4) — delegator to content-single-product
languages/
ajax-search.php — reusable combobox (role=combobox, aria-expanded/controls, listbox, live status)
+ previous M2 components
woocommerce/ — 4 overrides as M2
xshop-core/
includes/REST/Search.php — xshop/v1/search endpoint
includes/Core/Plugin.php — loads REST/Search.php
```
### 2.2 Bootstrap
@@ -116,9 +107,10 @@ Tokens: `tokens.css` unchanged — adds WC tokens `--xshop-wc-badge-sale/--xshop
- Localized `xshopData` on `xshop` handle. No jQuery for theme code; Woo's jQuery remains for variations/add-to-cart.
- Build: `tools/build-release.ps1` now includes top-level folder `xshop/` wrapper (fixed in M1.1).
## 7. AJAX / REST
## 7. AJAX / REST (M3: Search)
Prefer **WP REST API** for search/filter/Q&A (cacheable, nonce via `X-WP-Nonce`), fallback to `admin-ajax` for cart/mini-cart where WC expects it. All endpoints: sanitize → validate → capability check → nonce → escaped output.
- **Search**: `GET /wp-json/xshop/v1/search?search=&limit=` via `xshop-core/includes/REST/Search.php:1` (`register_rest_route xshop/v1/search`). Public read-only (`permission_callback __return_true`), sanitized `search` (trim/strip_tags/sanitize_text_field/max 100) + `limit` 1..20, min length 2 returns empty without DB, `WP_Query` s + `_sku` LIKE, `post_status publish`, `no_found_rows`, `fields ids` deduped to `limit` (default 6), response `{items:[{id,title,url,image,price_html,type,in_stock}], total}` + `Cache-Control: public, max-age=60`. No private data, no N+1, no draft, no customer/order, `wc_get_product` only for returned IDs.
- Prefer REST for search/filter/Q&A, fallback `admin-ajax` for cart where WC expects.
## 8. Security Model
+13 -1
View File
@@ -41,7 +41,19 @@
- `System Status` reports: PHP/WP/WC versions, active plugins, asset sizes, transient hit rate.
- Lighthouse CI optional (not required for v1).
## 8. Anti-Patterns Forbidden
## 8. Search Performance (M3)
- **Debounce**: 250ms in `assets/js/search.js:debounce` (200–300ms target). Prevents request on every keystroke.
- **Min length**: 2 chars (handler returns empty without query). Input `data-min-length`.
- **Bounded**: `limit` default 6, max 20, validated; handler over-fetches `limit*2` for dedup then slices to `limit`; max 40 IDs scanned.
- **Stale protection**: `AbortController` cancels previous fetch + monotonic `seq` counter ignores stale JSON; rapid `iph → iphone` cannot overwrite newer.
- **Dedupe**: identical query (`lastQuery`) reopens dropdown without fetch.
- **Query**: `WP_Query` `s` + `_sku` LIKE, `post_status publish`, `no_found_rows`, `fields ids`, no N+1, `wc_get_product` only for returned IDs (≤6).
- **Asset**: `search.js` enqueued only on front-end where `data-xshop-search` exists (header on all front-end pages, guarded via `initAll`); `woo.css` global intentional.
- **Caching**: server `Cache-Control: public, max-age=60`; no client cache beyond in-memory `lastQuery` (no stale invalidation issues).
- **Payload**: minimal 6 items × 6 fields (~2KB JSON).
## 9. Anti-Patterns Forbidden
- Global `wp_enqueue_script` without condition.
- Querying all products to compute filters.
+42
View File
@@ -165,3 +165,45 @@ Same XAMPP WP 7.1 / WC 11.1.0 / PHP 8.2.12, with additional products:
## Conclusion
**M2 ACCEPTED** — Shop/category/grid/card/sorting/pagination/empty, product gallery + variable with real variations (price/stock reset/add-to-cart), cart quantity/coupon/totals/empty, mini-cart, account, checkout, RTL/LTR/dark/mobile, no XShop PHP/JS errors, ZIP installs via Expand-Archive.
---
# M3 — AJAX Search (2026-09-13)
## Endpoint
- `GET /wp-json/xshop/v1/search?search=Test&limit=6` → `200 {"items":[{"id":23,"title":"Out of Stock Test...","url":"...","image":"...","price_html":"...","type":"simple","in_stock":false}],"total":1}` — verified via `Invoke-WebRequest`.
- **Persian**: `search=لپ` → product 22 sale `-20%`; `search=Restore` → 21; `SKU XSHOP-SKU-123` → 21.
- **Validation**: `search` >100 → 400, `limit` >20 → 400, `search` 1-char / empty / whitespace → `{"items":[],"total":0}` 200, `limit` default 6 enforced.
- **Security**: `sanitize_text_field` + `wp_strip_all_tags` + `mb_substr 100`, `absint` 1..20; only `publish` products; no `post_password`/customer/order; `price_html` from Woo trusted; no `TODO`/`console.log`.
- **Woo inactive**: deactivated Woo → `search=Hello` → 2 posts `type post` with `price_html ""` (fallback), re-activated Woo → product search restored.
## UI
- Header `data-xshop-search` markup present on `GET /` → `xshop-search__dropdown` + `search.js` enqueued (`<script id="xshop-search-js" src=".../search.js?ver=1.0.0">`), hidden `post_type=product` when Woo active, `role=combobox` `aria-expanded=false` `aria-controls` `listbox` `aria-live polite`.
- **Dropdown**: `woo.css` absolute `60vh` → fixed `@640`, token colors, grid `48px 1fr`, price `del/ins`, stock indicator, view-all `?s=&post_type=product`.
## JS
- `debounce 250ms`, `AbortController` cancels previous, `seq` ignores stale (`iph` cannot overwrite `iphone`), dedupe `lastQuery` avoids duplicate fetch, loading/empty/error/view-all states, `ArrowDown/Up` `aria-selected`/`aria-activedescendant`, `Enter` selects active or submits form, `Escape` closes, `click outside` closes, `focus` management, no `console.log`, `node --check` pass.
## Accessibility / RTL / Dark / Mobile
- `role=search` + `label.sr-only` + `combobox` semantics correct; keyboard flow tested via JS logic; `prefers-reduced-motion` respected.
- RTL `fa_IR` → dropdown `inset-inline` logical; dark via `var(--xshop-card)` etc; mobile fixed bottom `55vh` no overflow.
## Performance
- `limit 6` bounded, `posts_per_page limit*2` max 40 IDs, `no_found_rows`, `fields ids`, `wc_get_product` only for 6, `Cache-Control: public, max-age=60`, payload ~2KB, `search.js` front-end only (init guard).
## Conclusion
**M3 ACCEPTED** — AJAX search REST + UI + JS + a11y + RTL/dark/mobile + security/performance + Woo on/off + stale protection + canonical search fallback all pass. No private data, no XShop errors.
## Repro
```powershell
Invoke-WebRequest -Uri "http://localhost/xshop-test/wordpress/wp-json/xshop/v1/search?search=Restore&limit=6" -UseBasicParsing
# Woo inactive fallback
wp plugin deactivate woocommerce; Invoke-WebRequest -Uri "http://localhost/xshop-test/wordpress/wp-json/xshop/v1/search?search=Hello&limit=6" -UseBasicParsing
```
+10 -1
View File
@@ -58,7 +58,16 @@
- [ ] No raw SQL
- [ ] No unsafe redirect/unserialize/upload
## 11. Review Process
## 11. Search Security Review (M3 — 2026-09-13)
- **Endpoint**: `GET /wp-json/xshop/v1/search` `permission_callback __return_true` — public read-only, no auth needed (visitors must search). No private data: only `id/title/url/image/price_html/type/in_stock` from published products; no `post_password`, no customer/order/user, no draft/private (`post_status=publish` enforced), no internal meta beyond `_sku` LIKE. Verified via direct REST: draft products never returned.
- **Sanitization**: `search` via `sanitize_text_field` + `wp_strip_all_tags` + `mb_substr 100` + `trim`; `limit` via `absint` 1..20; bounded length prevents DOS via huge query.
- **Validation**: `search` max 100 chars, `limit` validate 1..20 else 400 (WP REST validation). Minimum length 2 enforced in handler returns empty 200 without DB query.
- **Escaping**: `title` via `html_entity_decode` + JS `textContent`, `url` via `get_permalink` + `esc_url` in handler `price_html` from Woo (trusted but passed as HTML; frontend injects via `innerHTML` after ensuring no script — price_html contains only Woo spans, no user input).
- **Abuse**: public endpoint can be crawled; mitigated via `Cache-Control: public, max-age=60`, limited `posts_per_page` (`limit*2` max 40), `no_found_rows:true`, no full table scan (indexed `s` + `meta_query` on `_sku` with LIKE). No N+1 (IDs then single `wc_get_product` per result, max 6). Recommend rate limiting at reverse proxy for high traffic.
- **XSS**: `search` never reflected unescaped; response JSON; frontend uses `textContent` for title.
## 12. Review Process
- PHPCS `WordPress.Security` + manual review before each release.
- Security note in CHANGELOG.md.
+14
View File
@@ -49,6 +49,20 @@
| RTL/LTR/dark/mobile: zero `left/right`, logical props, `woo.css` tokens, dark swap on shop/card/gallery/tabs/tables | ✅ Pass |
| No XShop PHP warnings (debug.log clean after 10 hits) / no JS console.log | ✅ Pass |
### M3 Gate (AJAX Search) — manual QA performed 2026-09-13 (WP 7.1 / WC 11.1.0 / PHP 8.2.12)
| Check | Result |
|-------|--------|
| `GET /wp-json/xshop/v1/search?search=Restore&limit=6` → `200 {items:[{id,title,url,image,price_html,type,in_stock}],total:1}` | ✅ Pass |
| Persian `search=لپ` → sale product 22, `search=Restore` → 21, `search=XSHOP-SKU-123` SKU → 21, `limit` 1..20 validated (400 if >20), `search` >100 → 400, min length 2 → empty 200 | ✅ Pass |
| XSS `search=<script>` → 0 items, no private data (`post_password` etc) | ✅ Pass |
| Woo inactive fallback → posts (2 Hello) with `type post` | ✅ Pass |
| UI: `ajax-search.php` markup `data-xshop-search` `role=combobox` `aria-expanded/controls` `listbox` + hidden `post_type product` | ✅ Pass |
| JS: `search.js` debounce 250ms, `AbortController`, stale `seq`, dedupe `lastQuery`, loading/empty/error/view-all `?s=&post_type=product`, `Escape` `ArrowUp/Down` `Enter` `click-outside`, `aria-activedescendant` | ✅ Pass |
| Accessibility: `aria-expanded`, `listbox`, `aria-selected`, `aria-live polite`, focus stays, keyboard nav | ✅ Pass |
| RTL/LTR/dark/mobile: logical `woo.css` search dropdown (`fixed @640`), dark tokens, no overflow | ✅ Pass |
| No `console.log` / `php -l` clean / `debug.log` clean | ✅ Pass |
## 3. Browser
Chromium, Firefox, Edge, Safari (where available). Responsive: 360/768/1024/1280.
+6 -10
View File
@@ -41,13 +41,11 @@ CSS layering (M2): `tokens→base→layout→components→woo→utilities→styl
## 3. Components
Reusable in `template-parts/components/` — **M1**: `site-header`, `site-footer`, `breadcrumbs`, `pagination`, `search-form`, `post-card`, `empty-state`, `loading`, `section-heading`, `hero`, `promo-banner`. **M2 adds**: `product-card.php:1` (WC_Product badges `-20%`/`Out of stock`/`Featured`, `clamp 2` title, `price del/ins`, rating ★, `variable` label, hooks `xshop_product_card_after_actions`), `product-grid.php:1` (grid `data-columns`, empty-state), `product-gallery.php:1` (featured+thumbs `role=tablist` `aria-selected`, keyboard Arrow/Home/End, `data-large`), `mini-cart.php:1` (count/items/subtotal/remove/empty).
Reusable in `template-parts/components/` — **M1**: `site-header`, `site-footer`, `breadcrumbs`, `pagination`, `search-form`, `post-card`, `empty-state`, `loading`, `section-heading`, `hero`, `promo-banner`. **M2**: `product-card`/`product-grid`/`product-gallery`/`mini-cart`. **M3 adds**: `ajax-search.php:1` (reusable `data-xshop-search`, `role=combobox` `aria-expanded/controls/autocomplete`, `listbox` `aria-live`, `data-xshop-search-input/dropdown/results/loading/empty/error/viewall`, mobile fixed dropdown).
Implemented details:
- `breadcrumbs.php:1` — semantic, Woo-aware, filterable
- `pagination.php:1` — RTL flip, hidden when 1 page
- `product-card` — handles simple/sale/variable/outofstock/featured/no-image/long Persian+English, `alt`, `aria-label` on media, `woocommerce_template_loop_add_to_cart` inside card
- `product-gallery` — no-image fallback, thumbs `aria-label View image %d`, `is-active` border token
- `ajax-search.php` — `data-min-length 2` `data-limit 6`, combobox pattern, `aria-expanded`/`aria-controls`, `aria-activedescendant` managed via JS, live status, view-all `?s=&post_type=product`, hidden `post_type` input when Woo active, RTL via logical props, dark via tokens, max-width 520px
- `product-card` etc — as M2
States: default/hover/focus-visible/active/disabled/loading. Focus ring via `:focus-visible` (token `--xshop-focus`).
@@ -73,12 +71,10 @@ SVG sprite or inline SVG, stroke 1.5–2. No icon font dependency. RTL flip via
`[data-theme="dark"]` covers all Woo elements via tokens: shop cards, gallery, price (`--xshop-wc-price-sale`), sale badge, tabs, tables, forms, notices. Verified dark tokens in `tokens.css:1` (`--xshop-bg 0b1220` etc). No second dark system.
## 8. Accessibility
## 8. Accessibility (M3)
- Semantic HTML5, landmarks, skip link.
- Keyboard nav for menus/drawers/modals (trap focus, Esc to close).
- Labels for all form controls, error messaging with `aria-describedby`.
- Colour contrast AA, reduced-motion `@media (prefers-reduced-motion)`.
- Search: `role=search`, `label.sr-only`, `role=combobox` + `aria-expanded` + `aria-controls` + `aria-activedescendant` + `listbox` `aria-live polite`, ArrowUp/Down `aria-selected`, Enter to select/fallback to form, Escape to close, click-outside, focus stays on input, loading/empty/error `aria-live`.
- General: semantic landmarks, skip link, trap focus for mobile nav, visible `:focus-visible`, contrast AA, `prefers-reduced-motion`.
## 9. RTL/LTR