Skip to content

Commit 830323a

Browse files
release: fixes
- Fixed compatibility with Gutenberg v22.6.0 - Improved keyboard navigation between posts - Fixed PHP deprecated notice - Fixed alignment for add-to-cart spinner - Enhanced security
2 parents 2aa2ff8 + 0525a08 commit 830323a

8 files changed

Lines changed: 246 additions & 18 deletions

File tree

.distignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,4 @@ DEVELOPMENT.md
8787
TESTING.md
8888
tsconfig.json
8989
docs/*
90+
AGENTS.md

AGENTS.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Agent workflow
2+
3+
## Project Overview
4+
5+
Neve is an open-source WordPress theme. It uses a hybrid PHP/React architecture: traditional WordPress PHP theme code with React-based admin interfaces (dashboard, customizer controls, metabox editor).
6+
7+
## Build Commands
8+
9+
```bash
10+
# Install dependencies
11+
yarn install --frozen-lockfile
12+
COMPOSER=composer-dev.json composer install # or: yarn run composer:install
13+
14+
# Full production build (all apps + rollup + grunt + translations)
15+
yarn run build
16+
17+
# Build individual apps
18+
yarn run build:dash # Dashboard React app
19+
yarn run build:customizer # Customizer controls React app
20+
yarn run build:metabox # Metabox editor React app
21+
yarn run build:ui-components # Shared React component library (@neve-wp/components)
22+
yarn run build:rollup # Frontend vanilla JS (Rollup → modern + legacy bundles)
23+
yarn run build:grunt # SCSS → CSS compilation
24+
25+
# Watch mode (all apps)
26+
yarn run dev
27+
```
28+
29+
## Linting & Formatting
30+
31+
```bash
32+
# PHP
33+
composer run-script phpcs # WordPress Coding Standards check
34+
composer run-script format # Auto-fix PHP style
35+
composer run-script phpstan # Static analysis (level 6)
36+
37+
# JavaScript/TypeScript (per-app linting)
38+
yarn run lint:global # Frontend JS (assets/js/src/)
39+
yarn run lint:dash # Dashboard app
40+
yarn run lint:customizer # Customizer app
41+
yarn run format:global # Auto-fix frontend JS
42+
yarn run format:dash # Auto-fix dashboard JS
43+
44+
# SCSS
45+
yarn run lint:scss
46+
yarn run format:scss
47+
```
48+
49+
## Testing
50+
51+
```bash
52+
# PHP unit tests
53+
./vendor/bin/phpunit
54+
./vendor/bin/phpunit tests/test-neve.php # Single test file
55+
56+
# E2E tests (Playwright, requires WordPress environment)
57+
yarn run test:playwright
58+
npx playwright test e2e-tests/specs/some-spec.spec.ts # Single spec
59+
60+
# Bundle size check
61+
yarn run size
62+
```
63+
64+
## Architecture
65+
66+
### PHP (Backend)
67+
68+
Two PSR-4 autoloaded namespaces:
69+
- **`Neve\*`**`inc/` — Core theme: customizer, views, compatibility layers, admin
70+
- **`HFG\*`**`header-footer-grid/` — Header/footer drag-and-drop builder module
71+
72+
Key entry points:
73+
- `functions.php` — Bootstrap, constants (`NEVE_VERSION`, `NEVE_INC_DIR`, etc.)
74+
- `inc/core/core_loader.php` — Main feature factory, loads all theme features
75+
- `header-footer-grid/Main.php` — HFG module entry point
76+
77+
`inc/compatibility/` contains integrations for 20+ plugins (WooCommerce, Elementor, Beaver Builder, WPML, etc.).
78+
79+
### JavaScript/React (Frontend & Admin)
80+
81+
Three separate build systems:
82+
- **Rollup**`assets/js/src/``assets/js/build/{modern,all}/` (frontend vanilla JS with modern/legacy bundles)
83+
- **Webpack** (`@wordpress/scripts`) → `assets/apps/*/src/``assets/apps/*/build/` (React admin apps)
84+
- **Grunt**`assets/scss/` → compiled CSS + RTL variants
85+
86+
React apps in `assets/apps/`:
87+
- `components/` — Shared component library published as `@neve-wp/components` (local file dependency)
88+
- `dashboard/` — Theme settings dashboard
89+
- `customizer-controls/` — WordPress Customizer React UI
90+
- `metabox/` — Post/page metabox editor
91+
- `starter-sites/` — Demo content installer notice
92+
93+
### CSS
94+
95+
SCSS source in `assets/scss/`, compiled via Grunt. Tailwind CSS is used in some apps. RTL stylesheets are auto-generated.
96+
97+
## Sub-Folder Lookup Map
98+
99+
Use this as a fast entry point before deeper grep/search.
100+
101+
| Path | What lives here | Start here when... |
102+
|---|---|---|
103+
| `inc/` | Core theme PHP (`Neve\*`): feature bootstrapping, customizer options, admin, plugin compat, render helpers | You need to change theme behavior in PHP |
104+
| `inc/core/` | Main runtime wiring (feature loading, settings, styles, shared traits) | You are tracing initialization or global feature toggles |
105+
| `inc/customizer/` | Customizer option definitions, defaults, control types, traits | You are adding/changing Customizer settings |
106+
| `inc/views/` | PHP view layer (layouts, partials, inline output, pluggable pieces) | You are editing rendered markup/output structure |
107+
| `inc/compatibility/` | Integrations with plugins (WooCommerce, Elementor, WPML, etc.) | A bug appears only when a plugin is active |
108+
| `header-footer-grid/` | Header/Footer builder module (`HFG\*`): builder components, customizer integration, templates, assets | Work is specific to header/footer builder behavior |
109+
| `header-footer-grid/Core/Components/` | Individual HFG components and utilities | You are modifying one header/footer element |
110+
| `assets/apps/components/` | Shared React UI package (`@neve-wp/components`) consumed by other apps | Multiple apps need the same UI/control update |
111+
| `assets/apps/dashboard/` | Dashboard React app | Theme dashboard/admin UX changes |
112+
| `assets/apps/customizer-controls/` | React controls rendered in WordPress Customizer | Customizer-side React UI/control logic changes |
113+
| `assets/apps/metabox/` | Post/page metabox React app | Metabox UI or save behavior changes |
114+
| `assets/apps/starter-sites/` | Starter-sites notice/installer app | Demo/starter-site flows need updates |
115+
| `assets/js/src/` | Frontend vanilla JS sources (Rollup modern + legacy bundles) | Runtime frontend interactions outside React apps |
116+
| `assets/scss/` | Global/theme SCSS source split by components/elements/compat | Styling changes in frontend theme CSS |
117+
| `assets/customizer/` | Legacy/customizer-specific static CSS/JS | Issue is in older non-React customizer assets |
118+
| `template-parts/` | Reusable template chunks used by core theme templates | You need to adjust a reusable template fragment |
119+
| `views/` | Additional PHP view templates used by theme rendering | You are tracking frontend HTML generation |
120+
| `page-templates/` | Assignable WordPress page templates | You are changing template-level page layouts |
121+
| `woocommerce/` | WooCommerce template overrides | WooCommerce-only frontend markup/styles need changes |
122+
| `tests/` | PHPUnit tests and PHP test helpers | You are adding or updating PHP unit test coverage |
123+
| `tests/php/` | PHP static-analysis stubs (e.g. for Psalm/PHPStan) | You need or are adjusting PHP static-analysis support |
124+
| `e2e-tests/specs/` | Playwright end-to-end specs | Validating editor/admin/frontend behavior end-to-end |
125+
| `stories/` | Storybook stories and local UI playground assets | You need isolated component UI verification |
126+
| `docs/` | Project docs and contributor references | You need implementation conventions or team guidance |
127+
| `grunt/` + `Gruntfile.js` | CSS/asset build pipeline tasks | SCSS output/build-step behavior is wrong |
128+
| `rollup.config.js` | Frontend JS bundling config for `assets/js/src/` | Entry points/output/chunk behavior must change |
129+
| `webpack.config.js` | React app bundling config (`assets/apps/*`) | Build behavior for dashboard/customizer/metabox apps |

assets/scss/components/compat/woocommerce/_buttons.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ a.added_to_cart,
5353
justify-content: center;
5454
display: inline-flex !important;
5555
padding-right: 15px !important;
56+
align-items: center;
5657

5758
&::after {
5859
margin-left: 5px;

assets/scss/gutenberg-editor-style.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
-webkit-font-smoothing: antialiased;
66
-moz-osx-font-smoothing: grayscale;
77

8-
&, > * {
8+
&, > *:not(.block-canvas-cover) {
99
background-color: var(--nv-site-bg);
1010
color: var(--nv-text-color);
1111
}

inc/compatibility/woocommerce.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ private function move_checkout_coupon() {
907907
* Move the coupon field and message info after the order table.
908908
*/
909909
public function move_coupon() {
910-
wc_enqueue_js( '$( $( ".woocommerce-checkout div.woocommerce-info, .checkout_coupon, .woocommerce-form-login" ).detach() ).appendTo( "#neve-checkout-coupon" );' );
910+
wp_add_inline_script( 'neve-script', 'jQuery( jQuery( ".woocommerce-checkout div.woocommerce-info, .checkout_coupon, .woocommerce-form-login" ).detach() ).appendTo( "#neve-checkout-coupon" );' );
911911
}
912912

913913
/**

inc/views/partials/excerpt.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ private function get_post_excerpt( $context, $post_id = null ) {
4848
$length = $this->get_excerpt_length();
4949

5050
$output = '';
51-
$output .= '<div class="excerpt-wrap entry-summary">';
51+
$output .= '<div class="excerpt-wrap entry-summary" tabindex="0">';
5252
$output .= $this->get_excerpt( $length, $post_id );
5353
$output .= '</div>';
5454

inc/views/pluggable/pagination.php

Lines changed: 111 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,14 @@ public function get_posts( \WP_REST_Request $request ) {
5555
return new \WP_REST_Response( '' );
5656
}
5757

58-
$query_args = $request->get_body();
59-
$args = json_decode( $query_args, true );
60-
61-
$per_page = get_option( 'posts_per_page' );
58+
$page_number = absint( $request['page_number'] );
59+
$query_args = $request->get_body();
60+
$args = json_decode( $query_args, true );
61+
$per_page = get_option( 'posts_per_page' );
6262
if ( $per_page > 100 ) {
6363
$per_page = 100;
6464
}
65+
$args = $this->sanitize_infinite_scroll_query_args( is_array( $args ) ? $args : array() );
6566

6667
/**
6768
* If homepage is set to 'A static page', there will be a parameter inside the query named 'pagename'.
@@ -73,24 +74,17 @@ public function get_posts( \WP_REST_Request $request ) {
7374
}
7475

7576
$args['posts_per_page'] = $per_page;
76-
77-
if ( empty( $args['post_type'] ) ) {
78-
$args['post_type'] = 'post';
79-
}
80-
81-
$args['paged'] = $request['page_number'];
82-
$args['ignore_sticky_posts'] = 1;
83-
$args['post_status'] = 'publish';
77+
$args['paged'] = $page_number;
8478

8579
if ( ! empty( $request['lang'] ) ) {
8680
if ( defined( 'POLYLANG_VERSION' ) ) {
87-
$args['lang'] = $request['lang'];
81+
$args['lang'] = sanitize_text_field( $request['lang'] );
8882
}
8983

9084
if ( defined( 'ICL_SITEPRESS_VERSION' ) ) {
9185
global $sitepress;
9286
if ( gettype( $sitepress ) === 'object' && method_exists( $sitepress, 'switch_lang' ) ) {
93-
$sitepress->switch_lang( $request['lang'] );
87+
$sitepress->switch_lang( sanitize_text_field( $request['lang'] ) );
9488
}
9589
}
9690
}
@@ -302,6 +296,109 @@ public function render_post_navigation() {
302296
echo '</div>';
303297
}
304298

299+
/**
300+
* Sanitize query arguments for infinite scroll to prevent query manipulation.
301+
*
302+
* This method implements a strict allowlist approach to prevent:
303+
* - Expensive database queries (DoS risk via meta_query, tax_query, etc.)
304+
* - Exposure of unintended content types
305+
* - Manipulation of query parameters by anonymous users
306+
*
307+
* @param array<string, mixed> $args Raw query arguments from client request.
308+
*
309+
* @return array<string, mixed> Sanitized query arguments safe for WP_Query.
310+
*/
311+
private function sanitize_infinite_scroll_query_args( $args ) {
312+
// Define allowlist of safe query parameters for public infinite scroll.
313+
$allowed_keys = array(
314+
'category_name',
315+
'tag',
316+
's',
317+
'order',
318+
'orderby',
319+
'author',
320+
'author_name',
321+
'year',
322+
'monthnum',
323+
'day',
324+
);
325+
326+
$sanitized = array();
327+
foreach ( $allowed_keys as $key ) {
328+
if ( isset( $args[ $key ] ) ) {
329+
$sanitized[ $key ] = $args[ $key ];
330+
}
331+
}
332+
333+
if ( isset( $sanitized['category_name'] ) ) {
334+
$sanitized['category_name'] = sanitize_text_field( $sanitized['category_name'] );
335+
}
336+
if ( isset( $sanitized['tag'] ) ) {
337+
$sanitized['tag'] = sanitize_text_field( $sanitized['tag'] );
338+
}
339+
if ( isset( $sanitized['s'] ) ) {
340+
$sanitized['s'] = sanitize_text_field( $sanitized['s'] );
341+
}
342+
if ( isset( $sanitized['order'] ) ) {
343+
$order_upper = is_string( $sanitized['order'] ) ? strtoupper( $sanitized['order'] ) : '';
344+
$sanitized['order'] = in_array( $order_upper, array( 'ASC', 'DESC' ), true ) ? $order_upper : 'DESC';
345+
}
346+
if ( isset( $sanitized['orderby'] ) ) {
347+
$safe_orderby = array( 'date', 'title', 'author', 'modified', 'comment_count' );
348+
$sanitized['orderby'] = in_array( $sanitized['orderby'], $safe_orderby, true ) ? $sanitized['orderby'] : 'date';
349+
}
350+
if ( isset( $sanitized['author'] ) ) {
351+
$sanitized['author'] = absint( $sanitized['author'] );
352+
}
353+
if ( isset( $sanitized['author_name'] ) ) {
354+
$sanitized['author_name'] = sanitize_user( $sanitized['author_name'] );
355+
}
356+
if ( isset( $sanitized['year'] ) ) {
357+
$sanitized['year'] = absint( $sanitized['year'] );
358+
}
359+
if ( isset( $sanitized['monthnum'] ) ) {
360+
$sanitized['monthnum'] = absint( $sanitized['monthnum'] );
361+
}
362+
if ( isset( $sanitized['day'] ) ) {
363+
$sanitized['day'] = absint( $sanitized['day'] );
364+
}
365+
366+
$post_type = ( ! empty( $args['post_type'] ) && is_string( $args['post_type'] ) ) ? sanitize_key( $args['post_type'] ) : 'post';
367+
$post_type_obj = get_post_type_object( $post_type );
368+
369+
// Only allow if post type exists and is publicly queryable.
370+
if ( $post_type_obj && $post_type_obj->publicly_queryable ) {
371+
$sanitized['post_type'] = $post_type;
372+
} else {
373+
$sanitized['post_type'] = 'post';
374+
}
375+
376+
// Explicitly unset dangerous query args that could be smuggled in.
377+
$dangerous_keys = array_flip(
378+
array(
379+
'meta_query',
380+
'meta_key',
381+
'meta_value',
382+
'meta_value_num',
383+
'meta_compare',
384+
'tax_query',
385+
'fields',
386+
'post__in',
387+
'post__not_in',
388+
'post_parent',
389+
'post_parent__in',
390+
'post_parent__not_in',
391+
)
392+
);
393+
$sanitized = array_diff_key( $sanitized, $dangerous_keys );
394+
395+
// Force safe defaults for core query behavior.
396+
$sanitized['post_status'] = 'publish';
397+
$sanitized['ignore_sticky_posts'] = 1;
398+
399+
return $sanitized;
400+
}
401+
305402
/**
306403
* Go to page option is enabled
307404
*

inc/views/template_parts.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ public function link_excerpt_more( $moretag, $post_id = null ) {
473473
);
474474

475475
// Return $new_moretag if 'text' key is not set in $read_more_args.
476-
if ( ! isset( $read_more_args['text'] ) ) {
476+
if ( ! isset( $read_more_args['text'] ) || empty( $read_more_args['text'] ) ) {
477477
return $new_moretag;
478478
}
479479

0 commit comments

Comments
 (0)