External Repositories self-updates
WordPress plugin to install and update private plugins and themes from external sources (wp-content.io or any compatible update server).
by Alexandre Chastan · github.com/wp-content-io/external-repositories · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/wp-content-io/external-repositories/archive/refs/heads/develop.zipShips its own WordPress updater (Update URI header), so new versions show up under Dashboard → Updates.
A WordPress plugin that installs and updates private plugins and themes from external sources instead of the wordpress.org directory.
A source is either:
- a wp-content.io account, or
- the URL of any compatible update server (see Compatible server API).
Each source gets its own tab in Plugins → Add New and Themes → Add New, and the plugins and themes it serves receive updates through the standard WordPress update screens, WP-CLI and auto-updates.
User documentation: https://docs.wp-content.io/.
Requirements
- WordPress 6.4 or later
- PHP 7.1 or later, with the OpenSSL extension (API keys are encrypted in the database)
- HTTPS for the sources that use an API key
Installation
[!IMPORTANT] Install from the release asset
external-repositories.zip, not from the green Code → Download ZIP button nor the Source code archives of a release. Those archives unpack asexternal-repositories-main/orexternal-repositories-1.3.1/: the plugin then installs under the wrong folder name, and its self-update and some settings stop working.
- Download
external-repositories.zipfrom the latest release. - In WordPress, go to Plugins → Add New → Upload Plugin, choose the zip and activate the plugin. On multisite, network activate it.
With WP-CLI:
wp plugin install https://github.com/wp-content-io/external-repositories/releases/latest/download/external-repositories.zip --activate
# Multisite: --activate-network instead of --activate
The plugin then keeps itself up to date (see Self-update).
Usage
Add a source in the admin
Go to Settings → External Repositories. On multisite, sources are managed by the network admin only, in Network Admin → Settings → External Repositories (capability manage_network_options), and they apply to the whole network.
For each source, set a name, choose wp-content.io or a custom URL, enter the API key and choose whether it serves plugins, themes or both. API keys are encrypted in the database with the SECURE_AUTH_KEY constant of wp-config.php: if that key changes, save the source again with its API key.
Declare a source in code
Sources can also be declared with the external_repositories filter, for example from a mu-plugin. These sources are not stored in the database and do not appear on the settings screen.
add_filter( 'external_repositories', function ( $repositories ) {
$repositories[] = array(
'name' => 'My company', // Required. Also used (sanitized) as the tab slug: keep it unique.
'url' => 'https://updates.example.com/', // Defaults to https://registry.wp-content.io/
'api_key' => getenv( 'MY_COMPANY_API_KEY' ), // Stored nowhere: read it from an environment variable or a constant.
'supports_plugins' => true, // Default true.
'supports_themes' => false, // Default true.
);
return $repositories;
} );
API keys are only sent over HTTPS. For a local development server on http://, allow it explicitly (never in production):
add_filter( 'external_repositories_allow_insecure_url', '__return_true' );
How a plugin or theme is linked to a source
A plugin or theme is updated by a source when the host of its Update URI header equals the host of the source URL. The slug is the plugin folder name, or the theme stylesheet (folder) name.
/**
* Plugin Name: My Private Plugin
* Version: 1.0.0
* Update URI: https://updates.example.com/
*/
For a theme, add Update URI: https://updates.example.com/ to the header of style.css. If several sources share the same host, the first one that answers wins.
Other hooks
| Hook | Type | Purpose |
|---|---|---|
external_repositories |
filter | Add sources from code (see above). Receives the sources saved in the database. |
external_repositories_api_key |
filter | Change the API key of a source at runtime. Arguments: $api_key, $config. |
external_repositories_allow_insecure_url |
filter | Allow an http:// source URL (local development only). Arguments: $allowed, $url. |
external_repositories_allowed_package |
filter | Allow or refuse the package URL announced by the self-update manifest. Arguments: $allowed, $package, $update_uri. |
hide_external_repositories_plugin |
filter | Hide this plugin from the plugins list. |
Compatible server API
Any server implementing these endpoints can be used as a source. All requests are GET, with a 10 second timeout. When the source has an API key, it is sent in the x-api-key header. {url} is the source URL with a trailing slash. WordPress adds its own query arguments (for example fields, locale): servers must ignore the arguments they do not know.
| Request | Response (JSON) |
|---|---|
GET {url}plugins?browse={source-slug}&page={n}&per_page=36 |
{ "info": { "page", "pages", "results" }, "plugins": [ … ] }. Each plugin follows the WordPress query_plugins shape: name, slug, version, author, short_description, icons, requires, requires_php, tested, last_updated, download_link… |
GET {url}plugins/{slug}?installed=1&active=1 |
The WordPress plugin_information shape: name, slug, version, download_link, tested, requires, requires_php, icons, banners, sections (description, changelog…). active is 1 when the plugin is active, empty otherwise. |
GET {url}themes?browse={source-slug}&page={n}&per_page=100 |
{ "info": { "page", "pages", "results" }, "themes": [ … ] }. Each theme: name, slug, version, screenshot_url, preview_url (optional), author, description… |
GET {url}themes/{slug}?installed=1 |
The WordPress theme_information shape: name, slug, version, download_link, requires, requires_php, sections… |
{source-slug} is the sanitized name of the source. version and download_link are required for updates. download_link may be a short-lived URL: the plugin requests the item again right before downloading an update.
Errors: return a non-2xx status with a JSON body { "messages": "…" } or { "messages": { "key": "…" } }; the messages are shown to the admin. In particular:
401/402: the messages of the body are shown (for example an invalid key or an expired subscription);403: the API key lacks read permission (a fixed message is shown);404: unknown item or source; the messages of the body are shown when present;5xx, timeouts and network errors: the source is paused for 5 minutes, so a down server never slows down every admin page.
A successful response must be a JSON object. A slow or failing server never breaks the site: the plugin falls back to the data WordPress already has.
Self-update
This plugin updates itself from its own Update URI (https://downloads.wp-content.io/), not from wordpress.org:
- It reads the manifest at https://downloads.wp-content.io/plugins/external-repositories/manifest.json.
Versionis the latest version, without avprefix (1.3.1, neverv1.3.1: a leadingvis stripped defensively, becauseversion_compare()would rank it below any number).PackageURIis the zip to download. It is used only when it is an HTTPS URL on theUpdate URIhost or a GitHub release asset of thewp-content-ioorganization (https://github.com/wp-content-io/*/releases/download/…); otherwise the plugin falls back tohttps://downloads.wp-content.io/plugins/external-repositories/external-repositories.{Version}.zip. Theexternal_repositories_allowed_packagefilter can change this allow-list.- When the manifest has a
Sha256field, the downloaded zip is checked against it and the update is refused on mismatch. - Optional fields:
Tested(otherwise read from the localreadme.txt),Name,Author,AuthorURI,RequiresWP,RequiresPHP.
The details modal ("View details") is built from the bundled readme.txt, including its changelog.
Releases are tagged vX.Y.Z on GitHub and published by the maintainers; the zip of each release is attached to its GitHub Release. A fork that wants to distribute its own build must change the Update URI header (and host its own manifest), otherwise it receives the official updates.
Development
See CONTRIBUTING.md: branch model, DCO sign-off, local checks (PHP syntax, WordPress Coding Standards, PHP 7.1 compatibility), testing with a local WordPress and translations.
Security
Please do not open public issues for vulnerabilities. See SECURITY.md for how to report them privately.
Support
- Bugs and feature requests for this plugin: GitHub issues.
- wp-content.io accounts, billing and the service itself: support@wp-content.io.
License
External Repositories is free software, released under the GNU General Public License v2 or later. Copyright (C) Alexandre Chastan.
It bundles a modified copy of Slimdown (MIT License, Johnny Broadway). See NOTICE.txt.