General Issues Reported on the Equalify WP Plugin #602

closed bbertucc opened this issue on Apr 7, 2026 · 1 comment
Bug
bbertucc bbertucc opened on Apr 7, 2026
# Fix before deploying to Red network ## Unbounded database query on every request `class-equalify-wp-integration-urls.php` — `get_all()` uses `posts_per_page => -1`, so it loads every published post on the site into PHP memory at once. That runs on every admin page load and every CSV request. On a UIC site with 3,000+ posts it might time out or blow the memory limit. For the admin table, push the pagination into `WP_Query` directly instead of fetching everything and slicing in PHP: ```php 'posts_per_page' => 20, 'paged' => $current_page, ``` For the CSV endpoint, fetch in chunks and stream each one: ```php $page = 1; do { $query = new WP_Query([..., 'posts_per_page' => 200, 'paged' => $page]); foreach ( $query->posts as $post ) { /* stream row */ } $page++; } while ( $page <= $query->max_num_pages ); ``` ## The CSV endpoint has no access control `class-equalify-wp-integration-public.php` — Anyone who hits `/?equalify_csv=1` triggers a full database scan. No token, no rate limiting, nothing. Across 750 public subdomains this is easy to abuse. Generate a secret token on activation and store it as a site option, then require it as a query parameter. The empty `Activator::activate()` method is the right place for this: ```php public static function activate() { if ( ! get_option( 'equalify_feed_token' ) ) { update_option( 'equalify_feed_token', wp_generate_password( 32, false ) ); } } ``` Then validate it before doing anything in `maybe_output_csv()`: ```php $token = get_option( 'equalify_feed_token' ); if ( ! hash_equals( $token, (string) get_query_var( 'equalify_csv_token' ) ) ) { wp_die( 'Forbidden', 403 ); } ``` Show the full URL (with token appended) in the admin settings panel so it's easy to copy into Equalify. ## Admin table paginates in PHP after fetching everything Same root cause as above — the table fetches all URLs then slices out 20 for the current page. Search is `array_filter` on the full result set. Pass `paged`, `posts_per_page`, and `s` directly to `WP_Query` instead. --- # High priority, but not blockers ## Disabled URLs stored as full strings The `equalify_disabled_urls` option serializes full URL strings into a single database option. MySQL's options table uses `longtext` (~64KB ceiling), and full URLs are long — a site with thousands of disabled URLs will silently start losing data. It also breaks whenever the permalink structure changes. Store post IDs instead and resolve to URLs at output time: ```php // Store: update_option( 'equalify_disabled_ids', [123, 456, 789] ); // Filter at output: $disabled = get_option( 'equalify_disabled_ids', [] ); $urls = array_filter( $urls, fn($item) => ! in_array( $item['id'], $disabled, true ) ); ``` ## Caching — use it when it's available, skip it when it's not There's no caching on the URL fetch. Since this plugin will run on environments you don't control, the right approach is to check whether an external object cache is active before relying on it. WordPress exposes `wp_using_ext_object_cache()` for exactly this — it returns `true` when Redis, Memcached, or any persistent cache drop-in is running (which it will be on Pantheon, but won't be on a stock install). ```php $cache_key = 'equalify_urls_' . get_current_blog_id(); $use_cache = wp_using_ext_object_cache(); $urls = $use_cache ? wp_cache_get( $cache_key ) : false; if ( false === $urls ) { $urls = /* run the query */; if ( $use_cache ) { wp_cache_set( $cache_key, $urls, '', HOUR_IN_SECONDS ); } } ``` Then bust the cache when content changes: ```php $bust = fn() => wp_cache_delete( 'equalify_urls_' . get_current_blog_id() ); add_action( 'save_post', $bust ); add_action( 'delete_post', $bust ); ``` On Pantheon with object-cache-pro this gets Redis automatically. On environments without a persistent cache it falls through to a live query every time — no worse than the current behavior. --- # Minor The plugin registers a public JavaScript file (empty jQuery boilerplate) on every front-end page across the whole network. It doesn't do anything. Conditionalize the enqueue so it only loads when the CSV endpoint is actually being served: ```php public function enqueue_scripts() { if ( ! get_query_var( 'equalify_csv' ) ) { return; } // ... } ```
azdak azdak commented on Apr 8, 2026
Closing, all issues addressed in https://github.com/EqualifyEverything/equalify-wp-integration/commit/da1fc49e3ead79b1bbd20093761b97490dcca19c.
View on GitHub →