Open Source WordPress Contribution June 2026 Recap

·

·

open source WordPress contribution June 2026

June started with a notification that six of my Co-Authors Plus pull requests had been merged in a single day. By the end of the month, I had shipped 18 pull requests across 5 different open-source projects.

This is the story of those 18 pull requests — the bugs, the reviews, the code, and what I learned from each one.


Co-Authors Plus: From PCP Compliance to Bug Fixes

The biggest story this month was Co-Authors Plus. After the PCP (Plugin Check) compliance sweep I started in late May, June saw all six PRs merged on June 7 as part of the 4.1.0 milestone. But the work didn’t stop there — I also shipped four more PRs later in the month, fixing author feeds, editor UI bugs, and adding opt-out filters.

Phase 1: ABSPATH Direct-Access Guards

PR #1284 — Merged Jun 7

The most fundamental WordPress security practice: prevent direct file access. PHPCS flagged 7 PHP files that were missing the standard ABSPATH guard. The fix was mechanical but essential:

// Added to 7 PHP files:
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

One file required special handling. php/integrations/yoast.php had the namespace declaration as the first statement after <?php. Placing the ABSPATH guard before it caused a fatal parse error. The fix was to move the namespace first:

<?php
namespace CoAuthors\Integrations;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

Lesson learned: Always check for namespace declarations when adding ABSPATH guards. PHP requires namespace as the very first statement after <?php.

Phase 2: Escaping Deprecated Hook Messages

PR #1285 — Merged Jun 7

Two __() calls inside _deprecated_hook() were passing unescaped strings to trigger_error(). PHPCS flagged WordPress.Security.EscapeOutput.OutputNotEscaped. The fix was a simple swap:

// BEFORE:
__( 'This filter is deprecated…', 'co-authors-plus' )

// AFTER:
esc_html__( 'This filter is deprecated…', 'co-authors-plus' )

Phase 3: Translator Comments for Block Files

PR #1286 — Merged Jun 7

Three block files used sprintf( __( 'Posts by %s' ) ) without translator comments. Without context, translators have no idea what %s represents. Added /* translators: %s: author display name */ comments to all three:

/* translators: %s: author display name */
$label = sprintf( esc_html__( 'Posts by %s', 'co-authors-plus' ), $author_name );

Phase 4: Modernize get_terms() Calls

PR #1287 — Merged Jun 7

The old two-parameter get_terms( $taxonomy, $args ) signature was deprecated in WordPress 4.5. Four calls across three files needed migration to the single-array form:

// BEFORE (deprecated):
get_terms( 'author', array( 'hide_empty' => false ) );

// AFTER (modern):
get_terms( array(
    'taxonomy'   => 'author',
    'hide_empty' => false,
) );

Phase 5: Input Sanitization + wp_trigger_error

PR #1288 — Merged Jun 7

Two fixes in one PR. First, validate $_POST['coauthors'][0] array index before access to resolve WordPress.Security.ValidatedSanitizedInput.InputNotValidated:

// BEFORE:
$coauthors = $_POST['coauthors'][0];

// AFTER:
$coauthors = isset( $_POST['coauthors'][0] ) ? $_POST['coauthors'][0] : '';

Second, replace trigger_error() with wp_trigger_error() in the CoAuthorsIterator constructor. Since the plugin requires WordPress 6.4+, the function is always available:

// BEFORE:
trigger_error( esc_html__( 'You can not clone the CoAuthorsIterator as it is a singleton.', 'co-authors-plus' ), E_USER_ERROR );

// AFTER:
wp_trigger_error( '', esc_html__( 'You can not clone the CoAuthorsIterator as it is a singleton.', 'co-authors-plus' ), E_USER_ERROR );

Phase 6-12: The Remaining Violations

PR #1289 — Merged Jun 7

This was the cleanup PR that addressed everything else. Here’s what it covered:

  • Phase 5b: Added wp_unslash() to all superglobal accesses before sanitization (~10 fixes across 4 files)
  • Phase 6: phpcs:ignore for NonceVerification warnings (nonce verified at page/form level)
  • Phase 7: phpcs:ignore for NoCaching direct DB queries (write operations, CLI commands)
  • Phase 8: Added $in_footer = true to 3 script registrations
  • Phase 9: phpcs:ignore for fopen (fgetcsv requires native handle)
  • Phase 10: Bumped README.md “Tested up to” from 6.9 to 7.0
  • Phase 12b: phpcs:ignore for set_time_limit, DirectDB.UnescapedDBParameter

The key pattern for wp_unslash() was consistent across all files:

// BEFORE:
$coauthors = $_POST['coauthors'];

// AFTER:
$coauthors = wp_unslash( $_POST['coauthors'] );

The in_footer parameter ensures scripts load in the footer for better page rendering performance:

// BEFORE:
wp_register_script( 'co-authors-plus-blocks', $src, $deps, $ver );

// AFTER:
wp_register_script( 'co-authors-plus-blocks', $src, $deps, $ver, true );

Fixing Author Feeds After the 4.0.2 Regression

PR #1303 — Merged Jun 22

This was a real-world bug that affected actual sites. In version 4.0.2, fix_author_page() started calling $wp_query->init_query_flags() to clear conflicting flags like is_category on guest author URLs. But init_query_flags() also clears is_feed — so author feed requests like /author/USERNAME/feed/ were being served as HTML author archives instead of RSS XML.

The fix was to capture and restore the feed flags, just like the existing is_paged preservation:

// Capture feed flags before reset
$is_feed         = $wp_query->is_feed;
$is_comment_feed = $wp_query->is_comment_feed;
$is_trackback    = $wp_query->is_trackback;
$is_paged        = $wp_query->is_paged;

$wp_query->init_query_flags();

// Restore all preserved flags
$wp_query->is_author       = true;
$wp_query->is_archive      = true;
$wp_query->is_feed         = $is_feed;
$wp_query->is_comment_feed = $is_comment_feed;
$wp_query->is_trackback    = $is_trackback;
$wp_query->is_paged        = $is_paged;

I also added integration tests to prevent regression — test__author_feed_preserves_is_feed_after_fix_author_page and test__author_page_does_not_force_is_feed.

Opt-Out Filter for author__in Rewrites

PR #1304 — Merged Jun 22

Version 4.0.x introduced automatic author__in → taxonomy JOIN rewriting, but with no opt-out. For large sites with complex WP_Query loops, this could be expensive. I added a coauthors_plus_is_author_query filter:

protected function is_author_query( WP_Query $query ): bool {
    if ( $query->is_author() ) {
        $is_author = true;
    } else {
        $author_in = $query->get( 'author__in' );
        $is_author = is_array( $author_in ) && ! empty( $author_in );
    }

    return (bool) apply_filters( 'coauthors_plus_is_author_query', $is_author, $query );
}

Now developers can opt out with a simple filter:

add_filter( 'coauthors_plus_is_author_query', '__return_false', 10, 2 );

Pre-Populating the Author Dropdown

PR #1305 — Merged Jun 22

This was a user experience fix. The block editor’s “Select An Author” combobox showed “No items found” as soon as it was focused, before the user typed anything. The backend endpoint already returns the first 10 alphabetical co-authors for an empty query, but the frontend was blocking that request.

The fix had two parts. First, fetch the initial list on mount:

useEffect( () => {
    if ( ! hasResolvedPost || isLoading || hasInitialLoaded.current ) {
        return;
    }
    hasInitialLoaded.current = true;
    fetchAuthors( '' );
}, [ hasResolvedPost, isLoading ] );

Second, show the full list for short queries instead of clearing the dropdown:

const onFilterValueChange = useDebounce( ( query ) => {
    if ( query.length < threshold ) {
        fetchAuthors( '' );
        return;
    }
    fetchAuthors( query );
}, 500 );

Normalizing Entity Store Values to Term IDs

PR #1283 — Merged Jun 22

After the 4.0.0 migration to WordPress core entity stores, getEditedPostAttribute('coauthors') could return full author objects instead of integer term IDs. This caused the REST API to receive [object Object] in the query string, resulting in a permanent spinner in the sidebar.

The initial fix had a term_id ?? id ?? user_id ?? ID fallback chain, but maintainer @GaryJones caught a critical issue: term IDs and user IDs are different namespaces, so substituting a user_id could render wrong authors silently.

After review feedback, the extractTermIds() utility was tightened to trust only term_id:

export const extractTermIds = ( coauthors ) => {
    if ( ! Array.isArray( coauthors ) || 0 === coauthors.length ) {
        return [];
    }

    return coauthors
        .map( ( item ) => {
            if ( Number.isInteger( item ) ) {
                return item;
            }
            if ( item && 'object' === typeof item ) {
                return item.term_id ?? null;
            }
            return null;
        } )
        .filter( ( id ) => Number.isInteger( id ) );
};

Key insight from this review: When dealing with IDs that flow to a taxonomy endpoint (get_term()), only accept IDs that are guaranteed to be term IDs. Rendering an empty panel is infinitely safer than rendering the wrong author.


WooCommerce: Three Bug Fixes, One Analytics Safety Net

June was my most active month yet on WooCommerce core. I contributed four PRs addressing bugs that ranged from a white-screen fatal to a confusing “unsaved changes” popup.

Preventing a Fatal Error When Logging Is Disabled

PR #65403 — Merged June 2026

When WooCommerce logging is disabled, the log directory never gets created. realpath() returns false for non-existent paths, and passing false to wp_is_writable() triggers a ValueError: Path must not be empty on the System Status page.

The fix was a single guard clause:

public function get_log_directory_size(): int {
    $bytes = 0;
    $path  = realpath( Settings::get_log_directory( false ) );

    // If the log directory doesn't exist, it has no size.
    if ( empty( $path ) ) {
        return 0;
    }

    if ( wp_is_writable( $path ) ) {
        // ... iterate directory ...
    }
}

I also removed two outdated PHPStan baseline entries and added a regression test test_get_log_directory_size_missing_directory().

Fixing Hyphenated SKU Duplication

PR #65423 — Merged June 2026

When duplicating a product with a hyphenated SKU like SKU-123, the old code treated 123 as an incrementable suffix, producing SKU-124 instead of the correct SKU-123-1.

The root cause was a regex that stripped the last numeric segment:

// BEFORE (broken):
$root_sku = preg_replace( '/-[0-9]+$/', '', $product->get_sku() );
// Result for SKU-123: SKU → SKU-124

// AFTER (fixed):
$original_sku = $product->get_sku();
// Query: LIKE $original_sku . '-%'
// Then: $original_sku . '-' . ($max_suffix + 1)
// Result for SKU-123: SKU-123-1

I added comprehensive tests covering simple hyphenated SKUs, multiple hyphens, non-numeric suffixes, sequential duplicates, and case-insensitive SKU conflicts.

Fixing False “Unsaved Changes” Warning on Order Edit

PR #66001 — Merged Jun 25

The “Customer provided note” textarea in the order data meta box used id="excerpt", which collided with WordPress core’s post excerpt field. WordPress tracks #excerpt in its beforeunload dirty-state handler. When the textarea was pre-filled with an existing note, WordPress treated it as an unsaved excerpt change and popped up “Changes that you made may not be saved.”

The fix was a one-line ID change:

// BEFORE:
<textarea id="excerpt" name="customer_note" ...>

// AFTER:
<textarea id="customer_note" name="customer_note" ...>

The field still saves via name="customer_note", so behavior is unchanged. Maintainer @mikejolley tested and confirmed: “Cannot see any other thing relying on this ID.”

Preventing Analytics Fatal Errors with Plain WC_Order

PR #64407 — Merged June 2026

The Analytics Customers DataStore assumed every order was an instance of Automattic\WooCommerce\Admin\Overrides\Order, which defines get_customer_first_name() and get_customer_last_name(). When a plain WC_Order was passed (constructed without the woocommerce_order_class filter), these methods didn’t exist, causing a fatal error.

The fix wrapped plain WC_Order into Overrides\Order at the entry point:

// If a plain WC_Order is passed, wrap it in Overrides\Order
if ( ! $order instanceof Overrides\Order ) {
    $order = new Overrides\Order( $order->get_id() );
}

I also fixed two other edge cases: null date_created crashing getTimestamp(), and wc_get_order() returning false for deleted orders. The date_last_active field now cascades through available date fields:

$date_last_active = null;
if ( $order->get_date_created( 'edit' ) ) {
    $date_last_active = $order->get_date_created( 'edit' )->format( 'Y-m-d H:i:s' );
} elseif ( $order->get_date_modified( 'edit' ) ) {
    $date_last_active = $order->get_date_modified( 'edit' )->format( 'Y-m-d H:i:s' );
} elseif ( $order->get_date_paid( 'edit' ) ) {
    $date_last_active = $order->get_date_paid( 'edit' )->format( 'Y-m-d H:i:s' );
}

Newspack Theme: Comment Meta Position Customizer Toggle

PR #2691 — Merged Jun 11

Users of the Newspack theme wanted control over where comment author meta appears. I added a Customizer radio control so site editors can choose whether the avatar, name, and date appear above or below the comment content.

The walker needed to buffer comment-meta and comment-content into separate variables, then emit them in the configured order:

ob_start();
// Output comment meta (avatar, name, date)
$comment_meta = ob_get_clean();

ob_start();
// Output comment content
$comment_content = ob_get_clean();

// Emit in configured order
if ( 'above' === get_theme_mod( 'comment_meta_position', 'above' ) ) {
    echo wp_kses_post( $comment_meta );
    echo wp_kses_post( $comment_content );
} else {
    echo wp_kses_post( $comment_content );
    echo wp_kses_post( $comment_meta );
}

Maintainer @dkoo requested several improvements during review:

  1. Sanitizer: Switched from sanitize_text_field to newspack_sanitize_radio so invalid values fall back to the default
  2. Escaping: Wrapped buffered output in wp_kses_post() instead of using phpcs:ignore
  3. Exception safety: Added try/finally blocks with level tracking to prevent buffer leaks if a filter throws
  4. Label casing: Changed “Comment Meta Position” to “Comment meta position” (sentence casing)

WordPress Plugin Check: Enabling CDN Detection

PR #1179 — Merged in 2026

Plugins loading assets from external CDNs (like cdn.jsdelivr.net, unpkg.com, or bootstrapcdn.com) violate WordPress.org Plugin Guideline #7 — privacy requirements. The detection code already existed in the codebase (EnqueuedResourceOffloadingSniff), but it was never added to the ruleset configuration file.

The fix was just 5 lines of XML:

<!-- Check for external CDN asset loading (privacy guideline #7). -->
<rule ref="PluginCheck.CodeAnalysis.EnqueuedResourceOffloading">
    <severity>7</severity>
</rule>

The sniff detects over 20 known CDN services using static code analysis. When a plugin does this:

wp_enqueue_script(
    'phone-input-js',
    'https://cdn.jsdelivr.net/npm/[email protected]/build/js/intlTelInput.min.js',
    array(),
    '21.2.4',
    true
);

Plugin Check now flags it with: Found call to wp_enqueue_script() with external resource. Offloading scripts to your servers or any remote service is disallowed.


WordPress Plugin Check: Detecting PHP Error Reporting Changes

PR #1317 — Merged Jun 2026

This was a new static check that flags plugins which modify PHP error-reporting configurations or redefine core WordPress debug constants in production. This prevents sensitive information disclosure.

The initial implementation used an AST-based approach with nikic/php-parser, but after review feedback from @davidperezgar, I refactored it into a dedicated PHPCS sniff (PhpErrorReportingSniff) matching the project’s existing pattern.

The sniff detects 11 patterns including:

// Detected patterns:
error_reporting( E_ALL );
ini_set( 'display_errors', '1' );
ini_alter( 'error_reporting', '1' );
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', true );
define( 'SCRIPT_DEBUG', true );
const WP_DEBUG = true;

First-argument detection uses PHPCSUtils\Utils\PassedParameters, eliminating false positives for calls like ini_set( some_function(), 'error_reporting' ). Multi-const declarations like const A = 1, WP_DEBUG = true; are fully inspected.


WordPress Two Factor: Clean Uninstall

PR #903 — Merged Jun 29

The two_factor_enabled_providers option (added in 0.16) was not being removed when the plugin was uninstalled, leaving an orphaned row in wp_options. The fix was straightforward: add the option key to the uninstall list and centralize it in a class constant.

const ENABLED_PROVIDERS_OPTION_KEY = 'two_factor_enabled_providers';

// In the uninstall method:
$option_keys = array(
    self::ENABLED_PROVIDERS_OPTION_KEY,
);

I also replaced the literal string in the main plugin file and settings UI with the class constant, so the key now lives in one place. A unit test (test_uninstall_removes_enabled_providers_option) covers the new cleanup path.



What I Learned This Month

Reviews are not obstacles. PR #1283 (term ID normalization) went through two rounds of review and a significant code change because @GaryJones caught a security issue I had missed. The final code is safer because of it. Every review made my code better.

Systematic approaches scale. The Co-Authors Plus PCP sweep (#1284–#1289) taught me that breaking a large compliance effort into phases (ABSPATH → escaping → i18n → deprecations → sanitization → edge cases) makes it manageable and reviewable.

Edge cases live at boundaries. The WooCommerce analytics fix (#64407) showed that type assumptions at function boundaries are where bugs hide. A plain WC_Order vs Overrides\Order is invisible in most code paths until it crashes. Guard at the boundary.

Small UX fixes matter. The “unsaved changes” popup fix (#66001) was a one-line ID change. The author dropdown fix (#1305) was a few lines of JavaScript. Both affect thousands of users every day. Not all impactful work is complex.


Looking Ahead

June was my most productive month in open source. Eighteen pull requests across five projects — from PHP security to JavaScript UI fixes to XML ruleset configuration. Each one taught me something new about the WordPress ecosystem and about writing careful, reviewable code.

If you’re reading this and thinking about contributing to open source, start where the friction is. The bug that annoys you is probably annoying others too. Fix it. Submit the PR. The community is waiting for your contribution.


This article is part of my monthly open source contribution series. Read the previous months:

External Resources

Meet the author

Faisal Ahammad

I’m a former Support Engineer at Saturday Drive Inc. (AKA Ninja Forms) and a GTE for the #bn_BD language. As an active contributor to WordPress, I have contributed to over 60 themes, plugins, and the WordPress core. I also run a small YouTube channel where I share my knowledge.

Leave a Reply

Your email address will not be published. Required fields are marked *

More from the blog


Recommended Topics


Popular Tags

formidable forms free stuff gravity forms ninja forms oop open source php top cash back Topcashback wordpress