My temporary PHP fix from 2014 has nearly 20M installs. Today I'm deprecating it

My temporary PHP fix from 2014 has nearly 20M installs. Today I’m deprecating it

我在 2014 年写的临时 PHP 补丁安装量已近 2000 万,今天我决定将其弃用

Twelve years ago, I wrote 174 lines of PHP as a stopgap for AOL’s content management system. I put it on Packagist in case anyone else needed the same patch, and somehow it’s been installed nearly 20 million times since. Today I marked it deprecated. 十二年前,我为 AOL 的内容管理系统写了 174 行 PHP 代码作为权宜之计。我把它发布在 Packagist 上,以防其他人也需要同样的补丁,没想到自那以后它竟被安装了近 2000 万次。今天,我将其标记为弃用。

A temporary shim

一个临时的垫片 (Shim)

In 2014, we were in the middle of upgrading AOL’s CMS from PHP 5.2 to 5.3. Part of that upgrade was dropping version 1 of the pecl_http extension, which gave us a function called http_build_url(). A CMS deals with a lot of URLs, and ours called that function in dozens of places. I wasn’t touching those. The function seemed straightforward enough to reproduce, so I wrote my own http_build_url(), defined only if the real one didn’t already exist. The old code never knew anything had changed. Composer was just taking off at the time, which made sharing it easy. I figured it would earn its keep for a year or two, until the PHP community moved on to something better. 2014 年,我们正在将 AOL 的 CMS 从 PHP 5.2 升级到 5.3。升级的一部分工作是弃用 pecl_http 扩展的第 1 版,该版本提供了一个名为 http_build_url() 的函数。CMS 需要处理大量 URL,而我们的系统在几十个地方都调用了这个函数。我不想去动那些代码。这个函数看起来很容易复刻,所以我写了自己的 http_build_url(),并且仅在原版函数不存在时才定义它。旧代码完全没察觉到任何变化。当时 Composer 刚刚兴起,这使得分享代码变得很容易。我原以为它只会发挥一两年的作用,直到 PHP 社区转向更好的方案。

That’s a lot of installs

惊人的安装量

Well, it wasn’t temporary. It’s been installed from Packagist nearly 20 million times, and it still picks up over 400,000 installs a month. And it turns out Composer is only part of the picture. WPML, the market-leading multilingual plugin for WordPress, bundles the polyfill directly in its codebase, and WPML says it’s installed on over 1.5 million sites. The domain-name library idna-convert depends on it too, which is how it ships inside the source of SPIP, a French content management system, and how it ended up packaged in Debian and Ubuntu. Between all of them, there’s a pretty good chance you’ve visited a website that is still running my code. I never imagined it would go this far. 好吧,它并没有成为“临时”的。它在 Packagist 上的安装量已接近 2000 万次,并且每月仍有超过 40 万次的安装。事实证明,Composer 只是冰山一角。WordPress 市场领先的多语言插件 WPML 直接在其代码库中捆绑了这个 polyfill,据 WPML 称,它已安装在超过 150 万个网站上。域名库 idna-convert 也依赖它,这就是为什么它会出现在法国内容管理系统 SPIP 的源码中,并最终被打包进 Debian 和 Ubuntu。综合来看,你很有可能访问过仍在运行我这段代码的网站。我从未想过它会传播得这么广。

Coming back to it

回顾往事

I didn’t grasp how far it had spread until a few months ago, when I looked at the package for the first time in years. I knew it had users. By 2021 I’d been out of PHP for a while, and the downloads were surprising enough that I asked for a new maintainer. Three people offered. Shortly after I asked, we lost a family member unexpectedly, and it turned our world upside down for a while. I never followed up, and that’s on me. By the time things settled, other goals had taken over, and I forgot about the package for years. 直到几个月前,当我多年来第一次查看这个包时,我才意识到它已经传播得如此之广。我知道它有用户。到 2021 年时,我已经离开 PHP 开发一段时间了,下载量之大让我感到惊讶,于是我征求新的维护者。有三个人提出了申请。但在我发出请求后不久,我们的一位家庭成员意外离世,这让我们的生活一度陷入混乱。我没能跟进后续,这是我的责任。等一切平息后,我已转向其他目标,并把这个包遗忘了好几年。

Along with the numbers, there were a handful of GitHub issues, including one where joining a path onto a URL with a trailing slash strips every letter “a” out of the path. So much for straightforward. Under a comment that reads // Workaround for trailing slashes, my code tacks an “a” onto the path so there’s always a last segment to cut off, then cuts it off with a find-and-replace. When the path ends in a slash, that last segment is just the “a”, and the find-and-replace takes every other “a” in the path with it. I can’t believe the bug went unnoticed for as long as it did. 除了数据之外,GitHub 上还有一些 Issue,其中一个指出:当将路径连接到带有尾部斜杠的 URL 时,路径中所有的字母“a”都会被删掉。看来它并不像我想象的那么简单。在一段注释为 // Workaround for trailing slashes 的代码下,我的代码会在路径末尾添加一个“a”,以便总有一个最后的部分可以被切除,然后通过查找替换将其切掉。当路径以斜杠结尾时,那最后的部分正好就是“a”,而查找替换操作会把路径中所有的“a”一并删掉。我简直不敢相信这个 Bug 竟然存在了这么久都没被发现。

So I had a decision to make. I could dive back into PHP after almost a decade away, hand the package to one of the people who’d offered, or let it keep sitting there. 所以我必须做出决定。我可以时隔近十年后重拾 PHP,将这个包交给当初申请维护的人之一,或者就让它继续放在那里。

None of the above

以上皆非

It was always meant to be temporary, so I’m retiring it. The PHP League’s URI library has been the community’s answer for years, and PHP 8.5 now ships a standards-compliant URI API in the language itself (thanks to jawira for pointing me at it). Both are better than a 174-line shim from 2014. 它本意就是临时的,所以我决定将其退役。PHP League 的 URI 库多年来一直是社区的标准答案,而 PHP 8.5 现在已经在语言本身中内置了符合标准的 URI API(感谢 jawira 指出这一点)。这两者都比 2014 年那 174 行的垫片要好得多。

Maintaining the package would only delay the move everyone should be making, and handing it over would add a risk on top of that. I don’t doubt anyone who offered, and ozh has kept a fork going for YOURLS. But a widely installed package with a new maintainer nobody downstream has vetted is exactly what attackers look for. Veritasium’s video on the xz Utils backdoor is the best telling I’ve seen of how that plays out. 继续维护这个包只会延缓大家本应进行的迁移,而转交维护权则会增加额外的风险。我并不怀疑任何申请者,ozh 也确实为 YOURLS 维护了一个分支。但一个被广泛安装、且由下游用户未经审查的新维护者接手的包,正是攻击者所寻找的目标。Veritasium 关于 xz Utils 后门的视频是我看过的关于此类事件如何演变的最好说明。

The package will keep installing, but it won’t get new fixes, including for the missing-”a” bug. After this long without a change, even a one-line fix could have unintended consequences for someone, with no one around to support it. The README shows how to switch. I wrote this code to ease a painful migration, for myself and anyone else going through the same one. Thank you to everyone who sent a pull request or offered to take it over, and to the people who kept filing issues long after I’d stopped reading them. It was a good run for a temporary fix. 这个包仍可安装,但不会再有新的修复,包括那个丢失“a”的 Bug。在这么长时间没有变动之后,即使是一行代码的修复也可能给某些人带来意想不到的后果,且无人提供支持。README 中说明了如何切换。我写这段代码是为了减轻自己以及其他经历同样迁移过程的人的痛苦。感谢所有发送 Pull Request 或提出接手维护的人,也感谢那些在我停止阅读后仍坚持提交 Issue 的人们。作为一项临时补丁,它已经完成了它的使命。

P.S. We never migrated AOL’s CMS off the “temporary” polyfill. It ran there until the whole platform was shut down around 2020. 附注:我们最终从未将 AOL 的 CMS 从这个“临时” polyfill 中迁移出来。它一直运行到 2020 年左右整个平台关闭为止。