Skip to content

Commit 7649ecf

Browse files
authored
release: fixes
- Added a static cache (Optml_Settings) to avoid repeated get_option calls within a single request, improving performance and consistency. - Fixed a potential PHP 8.2+ fatal error in get_svg_for by ensuring $sizes is always an array before destructuring (e.g. when images are missing or offloaded). Props to [@fuleinist](https://github.com/fuleinist) for the contribution. - Improved optmlSrcsetDetector logic to prevent generating 1x DPR images larger than the original size, fixing incorrect srcset capping. Props to [@fuleinist](https://github.com/fuleinist) for the contribution. - Fixed incorrect notice display when disabling the scaling option.
2 parents 4acbe68 + ed55849 commit 7649ecf

7 files changed

Lines changed: 179 additions & 6 deletions

File tree

.distignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ test-results
4949
wp-scripts.config.js
5050
README.md
5151
CHANGELOG.md
52+
AGENTS.md

AGENTS.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Agent Workflow
2+
3+
## What This Is
4+
5+
Optimole WordPress plugin — cloud-based image optimization with CDN delivery, lazy loading, WebP/AVIF conversion, media offloading, and a Digital Asset Management (DAM) interface. Requires a supported PHP and WordPress version; see the plugin header or readme for the current version and system requirements.
6+
7+
## Build & Dev Commands
8+
9+
```bash
10+
# Install dependencies
11+
npm install && composer install
12+
13+
# Build all assets (production)
14+
npm run build
15+
16+
# Development with HMR (all targets in parallel)
17+
npm run dev
18+
19+
# Build individual targets
20+
npm run build:dashboard # Admin settings UI
21+
npm run build:widget # Dashboard widget
22+
npm run build:media # Media library integration
23+
npm run build:optimizer # Frontend lazy loading script
24+
npm run build:video-player-editor
25+
npm run build:video-player-frontend
26+
```
27+
28+
## Testing
29+
30+
```bash
31+
# PHP unit tests (requires WP test suite)
32+
composer phpunit
33+
composer install-wp-tests # First-time setup
34+
35+
# Run a specific PHP test file
36+
composer phpunit -- tests/test-replacer.php
37+
38+
# Run a specific test method
39+
composer phpunit -- --filter="test_method_name"
40+
41+
# JavaScript tests
42+
npm test
43+
npm run test:watch
44+
npm run test:coverage
45+
46+
# E2E tests (Playwright — configure use.baseURL in playwright.config.ts to your local WP URL; do not run against the shared http://testing.optimole.com by default)
47+
npm run e2e:run
48+
npm run e2e:open # Interactive UI mode
49+
```
50+
51+
## Linting & Static Analysis
52+
53+
```bash
54+
# JavaScript
55+
npm run lint # ESLint
56+
npm run format # ESLint --fix
57+
58+
# PHP
59+
composer phpcs # PHP CodeSniffer (WordPress-Core standard)
60+
composer format # Auto-fix with phpcbf
61+
composer phpstan # PHPStan level 6
62+
```
63+
64+
## Architecture
65+
66+
### Two-era codebase: v1 (legacy) and v2 (modern)
67+
68+
**Legacy code (`inc/*.php`)** — Class prefix `Optml_*`, loaded by custom autoloader in `optimole-wp.php`. This is the bulk of the plugin.
69+
70+
**Modern code (`inc/v2/`)** — PSR-4 autoloaded under `OptimoleWP\` namespace via Composer. New features should go here.
71+
72+
### Plugin initialization flow
73+
74+
```
75+
optimole-wp.php → optml() → Optml_Main::instance() (singleton)
76+
├── Optml_Manager — Orchestrates replacers and loads compatibilities
77+
├── Optml_Admin — Admin dashboard UI, conflict detection
78+
├── Optml_Rest — REST API endpoints for the dashboard
79+
├── Optml_Media_Offload — Cloud offloading/restoration of media
80+
├── Optml_Dam — Digital Asset Management UI
81+
├── Optml_Video_Player — Video player blocks
82+
└── Optml_Cli — WP-CLI commands (when available)
83+
```
84+
85+
### Core subsystems
86+
87+
- **URL/Tag replacement** (`tag_replacer.php`, `url_replacer.php`, `app_replacer.php`) — Parses HTML output, rewrites image URLs to Optimole CDN URLs with optimization parameters.
88+
- **Lazy loading** (`lazyload_replacer.php`, `inc/v2/BgOptimizer/`) — Viewport-based and background-image lazy loading. Frontend JS in `assets/js/optimizer.js`.
89+
- **Media offloading** (`media_offload.php`) — Moves media to/from Optimole cloud storage, handles bulk operations.
90+
- **Settings** (`settings.php`) — Central settings schema and management, stores in `wp_options`.
91+
- **Compatibility layer** (`inc/compatibilities/`) — 47+ integrations for page builders (Elementor, Divi, Beaver Builder), caching plugins (WP Rocket, LiteSpeed), WooCommerce, etc. Each extends `Optml_compatibility`.
92+
93+
### Frontend assets (`assets/src/`)
94+
95+
React-based, built with `@wordpress/scripts`:
96+
- `dashboard/` — Main admin settings page (React + @wordpress/components)
97+
- `widget/` — Admin dashboard widget
98+
- `media/` — Media library DAM integration
99+
- `video-player/` — Editor and frontend video player blocks
100+
101+
Built output goes to `assets/build/`. The `assets/js/optimizer.js` is the frontend lazy-loading script.
102+
103+
## Task Routing (Where to Start)
104+
105+
- Bootstrap/hooks: `optimole-wp.php`, `inc/main.php`, `inc/manager.php`, `inc/filters.php`.
106+
- URL/CDN rewriting: `inc/tag_replacer.php`, `inc/url_replacer.php`, `inc/app_replacer.php` + `tests/test-replacer.php`.
107+
- Lazyload: `inc/lazyload_replacer.php`, `inc/v2/BgOptimizer/Lazyload.php`, `assets/js/optimizer.js` + `tests/test-lazyload.php`.
108+
- Admin/REST/settings: `inc/admin.php`, `inc/rest.php`, `inc/settings.php`, `assets/src/dashboard/`.
109+
- Media/DAM: `inc/media_offload.php`, `inc/dam.php`, `assets/src/media/`.
110+
- Compat/conflicts: `inc/compatibilities/`, `inc/conflicts/`, related `tests/e2e/*.spec.ts`.
111+
- Video player: `inc/video_player.php`, `assets/src/video-player/`, `tests/test-video.php`.
112+
113+
## Security Checklist (WordPress-Specific)
114+
115+
- State-changing actions: capability check + nonce verification.
116+
- Input/output: sanitize on input, escape on output.
117+
- REST: always set strict `permission_callback` for write routes.
118+
- SQL: use `$wpdb->prepare()` for dynamic queries.
119+
- Safety: do not leak secrets/tokens; validate remote and file/media operations before mutate/delete.
120+
121+
## Agent Execution Policy
122+
123+
- Keep changes surgical; avoid unrelated refactors/cleanups.
124+
- Prefer `inc/v2/` for new logic unless legacy coupling forces `inc/*.php`.
125+
- Trace hooks and callsites before editing behavior.
126+
- Add/update the smallest relevant test and run targeted checks first.
127+
- Build only touched frontend targets; call out intentional build artifacts.
128+
129+
## Code Standards
130+
131+
- **PHP**: WordPress-Core via PHPCS. Text domain: `optimole-wp`.
132+
- **JS**: WordPress ESLint config. Tabs, single quotes, semicolons.
133+
- **v2 PHP code** requires PHPStan-compatible PHPDoc annotations, full namespace paths, type hints, and constants for magic values (e.g., `Profile::DEVICE_TYPE_MOBILE` not `1`).
134+
- Error handling: return `WP_Error` for WordPress-compatible errors, `false` for simple failures.
135+
- Debug logging: `do_action( 'optml_log', $message )` when `OPTML_DEBUG` is true.
136+
137+
## Key Constants
138+
139+
Defined in `optimole-wp.php`: `OPTML_URL`, `OPTML_PATH`, `OPTML_VERSION`, `OPTML_NAMESPACE` (`'optml'`), `OPTML_BASEFILE`, `OPTML_DEBUG`, `OPTML_DEBUG_MEDIA`.

assets/js/modules/srcset-detector.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,9 +437,10 @@ export const optmlSrcsetDetector = {
437437
const targetWidth = Math.round(baseWidth * dprValue);
438438
const targetHeight = Math.round(targetWidth / aspectRatio);
439439

440-
// Don't generate 1x DPR variations larger than current size
440+
// Don't generate 1x DPR variations larger than natural image size
441441
// But allow 2x DPR variations for retina displays
442-
if (dprValue === 1 && targetWidth > currentWidth) {
442+
// Fixes: srcset capped at profiling container width (issue #1030)
443+
if (dprValue === 1 && targetWidth > naturalWidth) {
443444
return;
444445
}
445446

assets/src/dashboard/parts/connected/settings/Lazyload.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ const Lazyload = ({ settings, setSettings, setCanSave }) => {
204204
labels={{
205205
title: options_strings.performance_impact_alert_title,
206206
description:
207-
'scale' === showModal ?
207+
DISABLE_OPTION_MODAL_TYPE.scale === showModal ?
208208
options_strings.performance_impact_alert_scale_desc :
209209
options_strings.performance_impact_alert_lazy_desc,
210210
action: options_strings.performance_impact_alert_action_label,

inc/lazyload_replacer.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,11 @@ public function get_svg_for( $width, $height, $url = null ) {
529529
wp_cache_add( $key, [ $sizes[0], $sizes[1] ], 'optml_sources', DAY_IN_SECONDS );
530530
}
531531
}
532-
list( $width, $height ) = $sizes;
532+
// Check if $sizes is an array before list() assignment to prevent fatal error on PHP 8.2+
533+
// when the image file is missing (e.g., offloaded to CDN or deleted).
534+
if ( is_array( $sizes ) ) {
535+
list( $width, $height ) = $sizes;
536+
}
533537
}
534538
// If the width is not found the url might be an offloaded attachment so we can get the width and height from the metadata.
535539
if ( ! is_numeric( $width ) && ! empty( $url ) ) {

inc/manager.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ public function register_hooks() {
407407
);
408408
add_action( 'template_redirect', [ $this, 'register_after_setup' ] );
409409
add_action( 'rest_api_init', [ $this, 'process_template_redirect_content' ], PHP_INT_MIN );
410-
add_action( 'shutdown', [ $this, 'close_buffer' ] );
410+
add_action( 'shutdown', [ $this, 'close_buffer' ], PHP_INT_MIN );
411411
foreach ( self::$loaded_compatibilities as $registered_compatibility ) {
412412
$registered_compatibility->register();
413413
}

inc/settings.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ class Optml_Settings {
119119
*/
120120
private static $options;
121121

122+
/**
123+
* Cache for site settings to avoid multiple calls to get_option on the same request.
124+
*
125+
* @var array<string, mixed>|null Cached site settings.
126+
*/
127+
private static $site_settings_cache = null;
128+
122129
/**
123130
* Optml_Settings constructor.
124131
*/
@@ -440,6 +447,7 @@ public function update_frontend_banner_from_remote( $value ) {
440447

441448
if ( $update ) {
442449
self::$options = $opts;
450+
$this->set_settings_cache( 'banner_frontend', $opts['banner_frontend'] );
443451
}
444452

445453
return $update;
@@ -476,6 +484,7 @@ public function update( $key, $value ) {
476484

477485
if ( $update ) {
478486
self::$options = $opt;
487+
$this->set_settings_cache( $key, $value );
479488
}
480489
if ( apply_filters( 'optml_dont_trigger_settings_updated', false ) === false ) {
481490
/**
@@ -527,13 +536,17 @@ private function is_main_mu_site() {
527536
* @return array Site settings.
528537
*/
529538
public function get_site_settings() {
539+
if ( self::$site_settings_cache !== null ) {
540+
return self::$site_settings_cache;
541+
}
542+
530543
$service_data = $this->get( 'service_data' );
531544
$whitelist = [];
532545
if ( isset( $service_data['whitelist'] ) ) {
533546
$whitelist = $service_data['whitelist'];
534547
}
535548

536-
return [
549+
self::$site_settings_cache = [
537550
'quality' => $this->get_quality(),
538551
'admin_bar_item' => $this->get( 'admin_bar_item' ),
539552
'lazyload' => $this->get( 'lazyload' ),
@@ -578,6 +591,7 @@ public function get_site_settings() {
578591
'show_badge_icon' => $this->get( 'show_badge_icon' ),
579592
'badge_position' => $this->get( 'badge_position' ),
580593
];
594+
return self::$site_settings_cache;
581595
}
582596

583597
/**
@@ -747,6 +761,7 @@ public function reset() {
747761
$update = update_option( $this->namespace, $reset_schema );
748762
if ( $update ) {
749763
self::$options = $reset_schema;
764+
$this->set_settings_cache( 'filters', $reset_schema['filters'] );
750765
}
751766
wp_unschedule_hook( Optml_Admin::SYNC_CRON );
752767
wp_unschedule_hook( Optml_Admin::ENRICH_CRON );
@@ -872,4 +887,17 @@ public function get_cloud_sites_whitelist_default() {
872887
$site_url => 'true',
873888
];
874889
}
890+
891+
/**
892+
* Set settings cache value.
893+
*
894+
* @param string $key Cache key.
895+
* @param mixed $value Cache value.
896+
* @return void
897+
*/
898+
private function set_settings_cache( $key, $value ) {
899+
if ( self::$site_settings_cache !== null && isset( self::$site_settings_cache[ $key ] ) ) {
900+
self::$site_settings_cache[ $key ] = $value;
901+
}
902+
}
875903
}

0 commit comments

Comments
 (0)