Open Source Contribution April 2026: Eleven PRs, Three Plugins, One Good Month

·

·

Open source contribution recap for April 2026 showing WordPress plugin code and pull requests on GitHub

Some months you ship a few pull requests. Other months you find yourself deep inside three different WordPress plugins, fixing bugs that haunt thousands of users, building features that did not exist before, and cleaning up code so the next developer does not curse your name.

April 2026 was the second kind.

I contributed 11 pull requests to 3 open source projects this month. All of them merged. Here is the story behind each one, along with the actual code that made the difference.

The LifterLMS Bug That Trapped Students

LifterLMS is a learning management system for WordPress. Thousands of course creators use it to sell and deliver online courses. When something breaks, real students get stuck.

Two bugs caught my attention this month.

The Vanishing “Mark Complete” Button

Imagine this. You are a student. You finish a quiz, pass it, and the lesson marks itself complete. Later you want to review the material. You click “Mark Incomplete” to reset your progress and study again.

But when you go to mark it complete a second time, the button is gone. Only “Take Quiz” remains. You already passed the quiz. Why should you retake it?

This was exactly what happened in LifterLMS issue #3058. The root cause sat in a single function that unconditionally hid the “Mark Complete” button whenever a quiz was attached to a lesson. It never checked whether the student had already met the quiz requirements.

The fix in PR #3092 added a check before hiding the button. If the student already has a passing quiz attempt, the button stays visible.

// The key addition: check if user already met quiz requirements
$user_id = get_current_user_id();
if ( $user_id && $lesson->is_quiz_enabled() ) {
    $student = llms_get_student( $user_id );
    if ( $student ) {
        $quiz_id = $lesson->get( 'quiz' );
        $attempt = $student->quizzes()->get_best_attempt( $quiz_id );

        if ( $attempt ) {
            $passing_required = llms_parse_bool( $lesson->get( 'require_passing_grade' ) );
            // Show button if: passing not required, OR attempt is passing
            if ( ! $passing_required || $attempt->is_passing() ) {
                $show = true;
            }
        }
    }
}

The logic mirrors what LifterLMS already used when deciding whether to prevent lesson completion. Consistency matters. When two parts of a codebase make the same decision, they should use the same rules.

When Copy-Paste Attacks the Course Builder

The second LifterLMS bug was subtler. Course creators spend hours inside the Course Builder, naming lessons, quizzes, and sections. Sometimes they copy text from Word or Google Docs and paste it into a title field.

When they did, the HTML formatting came along for the ride. Bold text, colored fonts, weird spans. The title fields turned into a formatting warzone.

The paste handler already existed in _Editable.js. It stripped formatting cleanly. The problem? It only ran on elements with a data-formatting attribute. Title fields in the lesson, quiz, and section templates did not have that attribute.

PR #3093 solved it with a single line in the event bindings:

// Before: only elements with data-formatting got the handler
'paste .llms-input[data-formatting]': 'on_paste',

// After: also targets contenteditable fields without data-formatting
'paste .llms-input[contenteditable]:not([data-formatting])': 'on_paste',

The CSS selector does all the work. Elements with data-formatting keep their existing behavior. Elements without it, which are the plain-text title fields, now strip formatting on paste. One line. Bug gone.

WordPress Gutenberg block showing Mailchimp campaign archive with campaign titles and dates
WordPress Gutenberg block showing Mailchimp campaign archive with campaign titles and dates

Building Something New for Mailchimp for WordPress

Mailchimp for WordPress (MC4WP) is one of the most popular Mailchimp integration plugins. Over two million active installs. When you contribute here, your code reaches a lot of people.

A Campaign Archive Block That Did Not Exist

The plugin had no built-in way to display a history of sent campaigns. If you wanted to show your newsletter archive on your site, you needed another plugin or a manual embed. Issue #811 asked for exactly this feature.

PR #829 delivered a complete solution: a [mc4wp_campaigns] shortcode paired with a native Gutenberg block. Both share the same PHP rendering logic.

The architecture is clean. A single class, MC4WP_Campaign_Archive, handles everything:

// Shortcode registration
add_shortcode( 'mc4wp_campaigns', [ $this, 'shortcode' ] );

// The API call with smart field limiting
$result = $api->get_campaigns( [
    'status'     => 'sent',
    'count'      => $count,
    'sort_field' => 'send_time',
    'sort_dir'   => 'DESC',
    'fields'     => 'campaigns.id,campaigns.settings.title,campaigns.settings.subject_line,campaigns.send_time,campaigns.long_archive_url',
] );

Notice the fields parameter. It limits the API response to only the five properties the shortcode actually needs. No bloat. A one-hour transient cache prevents hammering the Mailchimp API on every page load.

The Gutenberg block gives editors three controls: number of campaigns, title type (campaign title or email subject line), and a show date toggle. Under the hood, the block calls the same shortcode. No duplicated logic.

Moving Sign-Ups Off the Critical Path

The second MC4WP contribution tackled a performance problem. When a user submitted a form through an integration like WooCommerce or WP Comment, the plugin made a synchronous API call to Mailchimp before returning the page. The user waited for that network request to finish.

PR #833 moved the API call to a background task. The sign-up data gets captured at submission time, then processed later via Action Scheduler or WP Cron.

// Before: synchronous API call blocked the response
$result = $mailchimp->list_subscribe( ... );

// After: capture context, defer the work
if ( function_exists( 'as_enqueue_async_action' ) ) {
    as_enqueue_async_action( 'mc4wp_integration_subscribe', array( $args ) );
} else {
    wp_schedule_single_event( time(), 'mc4wp_integration_subscribe', array( $args ) );
}
return true;

The function returns true immediately. The user sees their confirmation page instantly. The Mailchimp API call happens moments later in the background. If Action Scheduler is available, it uses that. Otherwise it falls back to WordPress Cron. No dependencies forced, no breaking changes.

Seven Pull Requests for Co-Authors Plus

Co-Authors Plus is Automattic’s plugin for assigning multiple bylines to a post. It powers author attribution on sites like WordPress.com VIP. The codebase is mature, which means bugs are subtle and fixes require care.

This was where I spent most of April. Seven PRs, spanning documentation fixes, bug squashing, and architecture refactors.

The Small Things That Break User Trust

Two documentation PRs tackled issues that seem trivial but erode trust. PR #1247 fixed a broken changelog link in the README. The relative path ./CHANGELOG.md worked on GitHub but redirected to an invalid URL on WordPress.org. One absolute URL fixed it.

PR #1248 corrected the plugin ZIP filename in the installation guide. The README said coauthors-plus.zip but the actual file distributed on WordPress.org is co-authors-plus.zip. Two missing hyphens. A user following the instructions would look for a file that does not exist.

These are not glamorous fixes. But when a user follows your README and hits a dead end, they do not think “minor documentation bug.” They think your plugin is broken. Small fixes, outsized trust impact.

When Guest Authors Vanish from Queries

PR #1249 fixed a genuinely complex bug. WP_Query supports author__in as an array of author IDs. It also supports comma-separated author strings like 'author' => '1,2,3'. But Co-Authors Plus only modified SQL queries for single-author archive pages (is_author()). Any programmatic query using author__in silently missed co-authored posts.

The fix required a new dispatch system. A helper method detects whether the query is an author query, then routes to the appropriate SQL filter:

$author_ids = $this->get_author_ids_from_query( $query );
if ( ! empty( $author_ids ) && ( ! $query->is_author() || count( $author_ids ) > 1 ) ) {
    return $this->posts_where_filter_multi_author( $where, $query );
}

This covers three cases: author__in arrays, comma-separated author strings, and single-author archives. Each follows the right code path. A new get_author_ids_from_query() helper normalizes the different input formats into a clean array of integers.

The PHP Warning That Security Scanners Trigger

Guest author pages in Co-Authors Plus had a strange bug. When a vulnerability scanner hit a guest author URL with ?cat=1 appended, WordPress set both is_category = true and is_author = true on the query. The plugin correctly replaced the queried object with the guest author, but left the conflicting category flags intact.

The result? Core functions like single_term_title() tried reading queried_object->name, which does not exist on a guest author object. PHP warnings flooded the logs.

PR #1250 solved this by resetting all query flags and selectively re-enabling only the ones that apply:

// Preserve is_paged before resetting all flags
$is_paged = $wp_query->is_paged;
$wp_query->init_query_flags();
$wp_query->is_paged   = $is_paged;
$wp_query->is_author  = true;
$wp_query->is_archive = true;

The first version of this fix manually cleared nine specific flags. Reviewer Gary Jones pushed for a more robust approach. Using init_query_flags() resets every flag WordPress might set, including ones added in future versions. Then only is_author, is_archive, and is_paged get turned back on. Future-proof and clean.

Refactoring Without Breaking Anything

PR #1251 was a pure refactor, extracted directly from code added in PR #1249. Both the single-author and multi-author SQL paths built the same taxonomy JOIN and WHERE clauses, but the logic was duplicated across two methods.

Two new protected helpers solved this:

// Resolves a coauthor to their taxonomy terms
protected function collect_coauthor_terms( $coauthor ): array;

// Builds the OR-chain WHERE clause
protected function build_terms_clauses( array $terms ): string;

Both code paths now delegate to these helpers. If term resolution logic changes in the future, one edit fixes both paths. No behavior changed, all existing tests passed, and the maintainer approved it.

The Guest Author Name That Would Not Stop Repeating

The final Co-Authors Plus fix addressed a bug where guest author names duplicated themselves in bylines. If a post had three guest authors, the first author’s name appeared three times. Website links also went missing for guest authors.

PR #1255 traced the problem to coauthors_links_single(). The function relied on WordPress template tags like get_the_author() and get_the_author_meta(), which read from the global $authordata variable. When the the_author filter ran, it joined all co-authors into one string, but $authordata kept pointing to the first author. Every guest author got the first author’s name.

The fix was straightforward but required changing the mental model. Stop relying on global state. Read properties directly from the passed author object:

// Before: relied on global $authordata
if ( 'guest-author' === $author->type && get_the_author_meta( 'website' ) ) {
    return sprintf( ... esc_html( get_the_author() ) ... );
}

// After: reads directly from the passed object
$display_name = isset( $author->display_name ) ? $author->display_name : '';

if ( 'guest-author' === $author->type && ! empty( $author->website ) ) {
    return sprintf(
        '<a href="%s" title="%s" rel="author external">%s</a>',
        esc_url( $author->website ),
        esc_attr( sprintf( __( 'Visit %s’s website', 'co-authors-plus' ), esc_html( $display_name ) ) ),
        esc_html( $display_name )
    );
}

This also fixed the missing website links. Guest author websites are stored in post meta, not in wp_usermeta. get_the_author_meta( 'website' ) could never find them. Reading $author->website directly works because the guest author object already carries that data.

I also wrote dedicated integration tests for this fix, including one that explicitly sets global $authordata to a different author and verifies the function ignores it completely.

A Pre-April Cleanup That Made It In

PR #1209 was a small refactor merged in April. It removed a redundant get_user_by() database call in the guest author deletion logic. The user object was already available from an earlier fetch. One less query, one less point of failure.

// Before: redundant DB call
$user_data = get_user_by( 'id', $delete_id );
$associated_user = $this->guest_authors->get_guest_author_by(
    'linked_account', $user_data->data->user_login
);

// After: reuse existing object
if ( $delete_user ) {
    $associated_user = $this->guest_authors->get_guest_author_by(
        'linked_account', $delete_user->user_login
    );
}

What I Learned This Month

Three patterns stood out across these eleven pull requests.

First, global state is the enemy. The Co-Authors Plus guest author name duplication bug existed because a function trusted $authordata instead of its own arguments. The LifterLMS Mark Complete bug existed because a function made a binary decision without checking user state. When your function depends on implicit context, it will break in ways that are hard to debug.

Second, a good reviewer makes your code better. Gary Jones on Co-Authors Plus pushed me from a nine-flag manual reset to init_query_flags(), from a fragile dispatch to a clean one, and from duplicated SQL assembly to shared helpers. Every PR he reviewed came out stronger. This is the open source dynamic at its best.

Third, small fixes compound. The two documentation PRs took minutes to write. The refactoring PR extracted helpers from code I had just written. None of these were “big” contributions alone. Together they made the codebase measurably better for the next contributor.

What Is Next

May is already here. Co-Authors Plus 4.0.2 shipped with six of my fixes. LifterLMS 10.0 will include the quiz button and paste handler fixes. The Mailchimp campaign archive is live for over two million sites.

If you maintain a WordPress plugin and any of these bugs sound familiar, check the linked PRs. The code is open. Use it. Improve it. That is the whole point.

Previous open source contribution recap -> March 2026 contribution recap

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

documents formidable forms free stuff gravity forms malaysia ninja forms oop open source php wordpress