We ship a WordPress plugin that emails each person who registers for an event a PDF ticket with a QR code on it. Two libraries do the heavy lifting: one draws the PDF, one encodes the QR. Last month the QR one had to go. Our own changelog records why:
Changed: QR codes are now generated with bacon/bacon-qr-code (BSD-2-Clause) instead of chillerlan/php-qrcode. The previous library carries ZXing-derived files under Apache-2.0, which is not compatible with GPLv2.
Apache-2.0 is a perfectly good free licence. It is also, in the Free Software Foundation’s
own words, GPL-compatible. That is what makes the trap worth writing down, so after the swap I
stopped trusting the two names in our composer.json and read the zip we would actually
submit instead. Seven packages are in there, under five different licences. We had
written down two of the seven. And one of the five is still the same problem we had just spent a
day removing.
“GPL-compatible” is a direction, not a property
Apache-2.0 can be combined with the GPL version 3. It cannot be combined with GPL version 2: its patent-termination and indemnity terms count as extra restrictions that GPLv2 does not permit, which is a large part of why GPLv3 was written the way it was. So “is this library GPL-compatible” has no answer on its own. The only answer is compatible with which version, and the version that matters is the one your own plugin claims.
Ours claims this, in the plugin header and again in readme.txt:
* License: GPL-2.0-or-later * License URI: https://www.gnu.org/licenses/gpl-2.0.html
The trap is the words “or later”
Read that header quickly and “or later” looks like the escape hatch: we allow GPLv3, Apache-2.0 is fine under GPLv3, so where is the problem? The problem is who the words are addressed to. “Or later” is not a licence we picked; it is a choice we handed to the recipient. Anybody who downloads the plugin may elect to treat it as GPLv2 and nothing else, and that person now holds a GPLv2 work with Apache-2.0 code inside it, which they cannot legally pass on.
Which means the escape hatch runs the wrong way. Offering a later version adds a permission for the user, so it cannot subtract a requirement from us: every bundled file has to clear the earliest version we offer, not the most convenient one. A GPLv3-only-compatible dependency is incompatible with a GPLv2-or-later plugin, full stop, and that holds for Apache-2.0 and for LGPLv3 alike.
The WordPress.org reviewers do not spell the versions out, which is probably why this keeps biting people. Here is the relevant line verbatim from a review thread on another of our plugins, dated 9 September 2026:
Any included code must be GPL-compatible. This means, among others, that the users have to receive the four essential freedoms: (0) to run the program, (1) to study and change the program in source code form, (2) to redistribute exact copies, and (3) to distribute modified versions. (Guidelines 1 & 4)
“Any included code” is the load-bearing phrase. Not the code you wrote. Not the code you chose.
Read the zip, not the manifest
Our composer.json names two requirements. The build script copies the whole of
vendor/ into the zip, so what we would hand a reviewer is 409 files, 319 of
them PHP, 12 MB — and seven packages, because dependencies have dependencies. One
command over the built zip, no tooling to install:
unzip -q your-plugin.zip -d /tmp/lic && cd /tmp/lic/your-plugin
for f in vendor/*/*/composer.json; do
printf "%-32s %s\n" "${f#vendor/}" "$(grep -m1 -o '"license":[^,}]*' "$f")"
done
Here is what ours printed. I have added the column that is the whole point:
| Package | Declared licence | Clears GPLv2? |
|---|---|---|
| bacon/bacon-qr-code | BSD-2-Clause | Yes |
| dasprid/enum | BSD-2-Clause | Yes |
| masterminds/html5 | MIT | Yes |
| sabberworm/php-css-parser | MIT | Yes |
| dompdf/dompdf | LGPL-2.1 | Yes — LGPL-2.1 §3 lets you convert to GPLv2 or later |
| dompdf/php-font-lib | LGPL-2.1-or-later | Yes |
| dompdf/php-svg-lib 1.0.2 | LGPL-3.0-or-later | No — LGPLv3 goes with GPLv3, not GPLv2 |
The two we declared are the first and the fifth. The problem is the last one.
php-svg-lib appears zero times in our composer.json
and eight times in composer.lock. Nobody on this end ever considered
it. It is a hard requirement of dompdf — "dompdf/php-svg-lib": "^1.0.0", not a
suggestion — so it arrives with the PDF renderer and gets copied into the zip by the same
rsync line that copies everything else. We had removed one GPLv3-only dependency by
hand and shipped another in the same archive, for the same reason, in a package we had never
typed.
The lesson, which is smaller than it sounds
A licence audit that reads composer.json is reading a wish list. The zip is the
distribution, and the only list that matters is the one inside it. Four things follow, and they
take about two minutes:
- Audit the built artifact, every time the build changes, not the manifest.
- Write the version down. “GPL-compatible” in a dependency’s README means nothing until you say compatible with what.
- Treat Apache-2.0 and LGPL-3.0 as the same finding. Both are GPLv3-and-later company.
- Remember that
composer require --no-devpulls a tree, not a package. The licence you have to defend is the strictest leaf on it.
The plugin this came out of
Event Tickets & Registration with QR Check-in publishes an event, takes free RSVPs, emails every attendee a QR-coded PDF ticket, and checks them in when a logged-in phone scans it at the door. No separate app and no external service: the QR is drawn on your own server, which is why whose code draws it was a question in the first place. Paid tickets, seating and the rest are in Pro, a one-time $79. Stated plainly because this article is about reading the small print: the free core is not in the WordPress.org directory yet, so today the way to get it is the Pro download, which contains it.
What I don’t know
Whether a WordPress.org reviewer would actually block on php-svg-lib. I can find
no public case of one doing so, the library is inside a very widely used renderer, and a reviewer
might reasonably read “or later” the generous way round. The argument above says they
would be right to block it, and the argument is mine, not theirs. We have not fixed it
yet either — dompdf hard-requires the package, so this is a fork, a renderer swap
or a conversation, not a line edit, and I would rather publish the finding today than sit on it
until there is a tidy ending. And none of the above is legal advice: I read seven
LICENSE files and the FSF’s compatibility notes, which is enough to know where
to look and not enough to tell you what to do.
