API / Minn\Ops

Minn\Ops

18 classes, 89 public methods and 58 internals. This page is the public surface; each class name opens the whole class.

Archive final class

public/minn/src/Minn/Ops/Archive.php · 105 lines · Open the class →

A zip unpacked the safe way, for the plugin and theme installer and for the engine's own update alike: every entry stays inside its folder (no absolute path, no "..", no backslash, no NUL), none is a symbolic link, there are no more than 20,000 of them, they unpack to no more than 512 MB, and they all sit in exactly one top folder (macOS's __MACOSX noise aside). Nothing is written until every entry has passed.

constMAX_BYTES = 536870912
staticunpackFolder(string $file, string $stage): string

Unpacks a zip file into a new folder $stage; the path of the one folder the archive held.

staticinspect(string $file): string

Checks every entry of a zip file without writing anything; the one folder it holds.

staticholds(string $file, string $entry): bool

Whether a zip file holds an entry by that name.

staticisFolderName(string $name): bool

A plain folder name: no separators, never "." or "..".

insideisSymlinkEntry()

AutoUpdates final readonly class

public/minn/src/Minn/Ops/AutoUpdates.php · 29 lines · Open the class →

Whether per-item auto-updates apply to plugins or themes, as the reference decides it (contracts/rest/minn-admin-v1.md "Auto-updates"): never when file mods are off (DISALLOW_FILE_MODS through file_mod_allowed, context automatic_updater), never when the updater is disabled (AUTOMATIC_UPDATER_DISABLED through automatic_updater_disabled, which a plugin may turn back), and then the type's own filter has the last word. Only plugins and themes can be on.

new__construct(Closure $filter)
@param Closure(string, mixed, mixed...): mixed $filter applies a filter, as apply_filters does
staticforSite(): self

The gate as the site's plugins see it: through their filters once the runtime's hooks exist, as configured otherwise.

methodenabledFor(string $type): bool

Whether per-item auto-updates apply to a type ("plugin" or "theme"; anything else is never on).

insideupdaterOff()

Changelog final class

public/minn/src/Minn/Ops/Changelog.php · 68 lines · Open the class →

A changelog read from its repository rather than shipped: a Minn release carries no notes, its own or Minn Admin's, for anyone to find on the sites that run it. The update service reads changelog.md from the repository's default branch on GitHub; the engine asks the service at most once a day and keeps the copy in an option (JSON) with the time it came; sections still marked Unreleased are left out, so a site only reads about releases that exist. A fetch GitHub does not answer keeps the last copy; a 404 (no such file, or a private repository) keeps nothing.

constTTL = 86400
constENGINE_SOURCE = 'https://updates.minn.run/v1/minn/changelog'
constADMIN_SOURCE = 'https://updates.minn.run/v1/minn-admin/changelog'
new__construct(Closure $load, Closure $save, string $source, Minn\Http $http = \Minn\Http::__set_state(array( 'within' => array ( ), 'fake' => NULL, 'refusal' => NULL, )))
@param Closure(): ?string $load the stored copy, as JSON
@param Closure(string): void $save keeps the copy, as JSON
staticengine(Minn\Content\Site $site): self

Minn's changelog for a site, kept in its minn_changelog option.

staticadmin(Minn\Content\Site $site): self

Minn Admin's changelog for a site running Minn, kept in its minn_admin_changelog option.

methodmarkdown(): string

The released sections as Markdown, fetched first when the copy is a day old; '' when there is none.

staticreleased(string $markdown): string

The changelog without its Unreleased sections.

insidekept()

CoreStatus final readonly class

public/minn/src/Minn/Ops/CoreStatus.php · 48 lines · Open the class →

The core the app's update banner and chip speak of, which on Minn is Minn: the running engine's version, a newer release on offer (Ops\Releases, asked of the update service once a day), and installing it (Ops\EngineUpdate). WordPress's own offer (the update_core transient a parked copy may write) is not Minn's to act on and is not shown. dbUpgrade is false: the database is WordPress's, and a Minn release never migrates it.

new__construct(Minn\Ops\Releases $releases, Minn\Ops\EngineUpdate $engine, string $version)
methoddata(): array

Minn's version, any offer, and when the update service was last asked.

methoddue(): bool

Whether the update service was last asked a day ago or more.

methodrefresh(): void

Asks the update service now.

methodupdate(): string

Installs the release on offer; the version now in place.

Diagnostics final readonly class

public/minn/src/Minn/Ops/Diagnostics.php · 408 lines · Open the class →

The System view's facts about this install: the engine, PHP, the database, and the server, with the health checks a site owner acts on. Every number is read live; nothing is cached or fetched from outside.

new__construct(Minn\Db $db, Minn\Content\Site $site, Minn\Front\Permalinks $permalinks, Minn\Ops\InstalledSoftware $software, Minn\Ops\Logs $logs, string $engineVersion, string $webroot)
methodpayload(Minn\Http\Request $request): array

The System view: checks, config, logs, and the four info groups.

methodconfig(): array

The wp-config debug constants as they stand; the engine never rewrites the file.

methodcron(): array

The scheduled events: the cron option's hooks and the engine's own scheduled posts, soonest first.

methodautoload(): array

The autoloaded options: the summary and the largest rows.

insidechecks(), engineGroup(), phpGroup(), serverGroup(), opcacheOn(), cronOptionEvents(), humanInterval(), autoloadSummary(), cronSummary(), futurePosts(), databaseGroup(), check(), rows(), bytes(), offsetLabel(), relative()

Directory final class

public/minn/src/Minn/Ops/Directory.php · 35 lines · Open the class →

The Minn update service, https://updates.minn.run: the one place the engine asks about anything it does not have yet, so a site running Minn talks to Minn and never to wordpress.org. The service speaks wordpress.org's own endpoints under /v1/ (plugin and theme update checks, directory search and information, translations) with every address a site would fetch pointed back at itself: packages under /v1/download/, icons, screenshots and emoji under /v1/assets/. It also serves Minn's own releases and changelogs. Every request names the engine and the WordPress version it is compatible with (which judges a plugin's "Requires at least"), never the site's address.

constORIGIN = 'https://updates.minn.run/'
constBASE = 'https://updates.minn.run/v1/'
constPACKAGES = 'https://updates.minn.run/v1/download/'

Where plugin, theme and translation packages download from.

staticurl(string $path): string

An address on the service: a path under /v1/, such as "plugins/info/1.2/".

staticuserAgent(): string

How the engine introduces itself: its version, and nothing about the site.

staticpost(Minn\Http $http, string $path, array $form): array

A form POST to the service (the update checks) over the caller's client, its answer decoded.

@param array<string, string> $form
@return array<string, mixed>

DirectoryApi final readonly class

public/minn/src/Minn/Ops/DirectoryApi.php · 56 lines · Open the class →

plugins_api() and themes_api() as plugins call them and answer them (probes plugins-api, themes-api), one class for both ($kind 'plugins' or 'themes'): the arguments as an object with the reader's locale and the major.minor version beside them, through <kind>_api_args; then whatever a plugin answers through <kind>_api (a seller's own information, or an error) stands, and only when nobody answers is the directory asked, through Ops\Packages (behind the Minn update service); <kind>_api_result is handed what came back either way. A refusal is <kind>_api_failed in the directory's own words.

new__construct(string $kind, Closure $filter, Minn\Ops\Packages $packages, string $locale, string $version)
@param Closure(string, mixed...): mixed $filter applies a filter, as apply_filters does
methodask(string $action, object|array $args): mixed

The answer for an action (plugin_information, query_themes, ...): an object, a WP_Error, or what a plugin returned.

insidedirectory(), unexpected()

EngineChecksums final readonly class

public/minn/src/Minn/Ops/EngineChecksums.php · 157 lines · Open the class →

Checks a Minn install's files against the release they claim to be, the way `wp core verify-checksums` checks WordPress's against wordpress.org: the update service describes the running version's release, its minn.zip is downloaded from the service alone and must match its published sha256 and carry a signature from a key this engine trusts (EngineUpdate), and then every file of the engine is compared with the archive's, as are the shape files at the webroot with the archive's layout templates. A file that differs, one the archive has and the install lacks, and one the install has beyond the archive are all named. The install record (.install.json) is the site's own and is not compared.

constSOURCE = 'https://updates.minn.run/v1/minn/releases/'

Where a version's release record is asked for.

new__construct(string $engineDir, array $keys = Minn\Ops\EngineUpdate::KEYS, Minn\Http $http = \Minn\Http::__set_state(array( 'within' => array ( ), 'fake' => NULL, 'refusal' => NULL, )))
@param list<string> $keys the release keys to trust (tests pass their own)
methodcheck(string $webroot): array

The comparison: the version checked, and the paths (under minn/, or at the webroot for shape files) that differ, are missing, or are extra.

@return array{version: string, changed: list<string>, missing: list<string>, extra: list<string>}
insiderelease(), archiveHashes(), treeHashes(), layoutOf(), shapeHashes(), compare()

EngineUpdate final readonly class

public/minn/src/Minn/Ops/EngineUpdate.php · 150 lines · Open the class →

Replaces the running engine with a published release. The release's minn.zip comes from the Minn update service and nowhere else (every redirect hop is judged), must match the sha256 its release records, and must carry an Ed25519 signature made with a key this engine was built to trust (KEYS; the secret half never leaves the release maintainer's machine, so neither GitHub nor the service can stand in a build of their own). It is unpacked beside the engine into a folder of its own, where it is checked again: its bootstrap names the version on offer and bin/minn is there. Then the swap, two renames in the folder the engine lives in: the running engine moves aside, the new one moves in, and the old copy is removed; when the second rename fails the first is undone. The webroot's shape files are laid again from the new engine (ShapeFiles::refresh): one still as the old engine wrote it, a stock empty placeholder above all, is replaced, and one the site changed is left alone. The legacy install record (.install.json, from installs that kept it inside the engine) comes along. One update runs at a time. A development checkout (a symbolic link, or a folder under git) is refused: git owns those. The site keeps answering throughout.

constKEYS = array ( 0 => '+Usjnr1ZTudBbWToqYLkZ1Oy2yKN07d9+u62AIJ2FG8=', )

The public halves of the keys that sign Minn releases (base64 Ed25519; scripts/release-key.php). A new key ships here in a release signed by an old one before it signs anything.

new__construct(string $engineDir, array $keys = self::KEYS, Minn\Http $http = \Minn\Http::__set_state(array( 'within' => array ( ), 'fake' => NULL, 'refusal' => NULL, )))
@param list<string> $keys the release keys to trust (tests pass their own)
methodapply(Minn\Ops\Release $release): string

Downloads, checks and installs a release; the version now in place.

methodinstall(string $zip, string $sha256, string $signature, string $version): string

Installs an archive in hand once it matches its checksum and signature, holds the version named, and that version is newer than the running one: an older release signed in its day is still signed, and putting it back would bring back what it was replaced for. The version now in place.

methodverify(string $zip, string $sha256, string $signature, string $version, string $use = 'used'): void

Throws unless an archive matches the checksum its release publishes and carries a signature from a trusted key; $use says what would not happen.

staticversionOf(string $engineDir): string

The version a bootstrap.php names; "0.0.0" when it names none.

insideswap(), signed(), refuseCheckout()

InstalledSoftware final readonly class

public/minn/src/Minn/Ops/InstalledSoftware.php · 59 lines · Open the class →

What is installed, as the System view lists it: every extension and WordPress plugin with whether it runs, the mu-plugins, the themes with their parents, and the active theme's label.

new__construct(Minn\Content\Site $site, Minn\Content\Inventory $inventory, Minn\Extension\Loader $extensions, string $webroot)
methodactiveExtensionCount(): int

How many Minn extensions are active.

methodmanifest(): array

Plugins, mu-plugins, and themes with their versions and whether each is active.

methodactiveThemeLabel(): string

The active theme's name and version, naming the parent of a child theme; the slug when the headers are missing.

Logs final readonly class

public/minn/src/Minn/Ops/Logs.php · 150 lines · Open the class →

The log files the System view can read and clear: the debug log the engine's failure handler writes, and PHP's own error log when it is a separate file inside the site. Anything outside the site is named but never read; it may be another tenant's.

new__construct(string $webroot)
methodsources(): array

Every log the engine knows about, with its path and group.

@return array<string, array{label: string, group: string, path: string}>
methodlistPayload(): array

The logs list as the System view shows it.

@return list<array> the sources that exist, debug first
methodread(string $id): array

One log's tail, by id.

methodclear(string $id): void

Empties one log.

methoddebugLogPath(): string

Where debug.log lives, from WP_DEBUG_LOG or the default.

methodtail(string $path): array

The last TAIL_BYTES of a file, the partial first line dropped.

methodowned(string $path): bool

True when the path resolves inside the webroot.

statichuman(int $bytes): string

A byte count in KB, MB, or GB.

Packages final readonly class

public/minn/src/Minn/Ops/Packages.php · 481 lines · Open the class →

Putting plugins, themes and extensions on disk. Plugins and themes come from the directory (wordpress.org's, asked through the Minn update service, Ops\Directory) or an uploaded zip, and are installed through WordPress's upgraders (UpgraderRun), so plugins hear every upgrader hook; a Minn extension (a folder with a minn.json) is placed by the engine. Every archive passes Archive's checks first: exactly one top-level folder that is a plain name (never "." or ".."), no absolute or dotted paths, no symbolic links, bounded entry count and size. Removal proves the folder is a direct child of its kind's directory before anything is deleted.

new__construct(Minn\Content\Site $site, string $contentDir, Minn\Http $http = \Minn\Http::__set_state(array( 'within' => array ( ), 'fake' => NULL, 'refusal' => NULL, )))
methodsearchThemes(string $query): array

Directory theme search, or the popular list for an empty query. @return list<array>

@return list<array>
methodsearchPlugins(string $query, int $page): array

Directory plugin search: twelve per page with icons, short descriptions, install counts, and ratings, plus which results are already installed (by folder). @return array{plugins: list<array>, page: int, pages: int, total: int}

@return array{plugins: list<array>, page: int, pages: int, total: int}
methodpluginInfo(string $slug): array

The slim card for one directory plugin, cached twelve hours per slug.

methodinstallPlugin(string $slug, string $version = ''): string

Installs a directory plugin by slug, through the upgrader; returns its folder.

methodreplacePlugin(string $slug, string $version = ''): string

Installs a directory plugin over the folder already there.

methoddirectoryPlugin(string $slug): ?array

One directory plugin record, or null when the slug is unknown.

@return array<string, mixed>|null
methodinfoAction(string $kind, string $action, array $request): ?array

The directory's answer to a plugins_api() or themes_api() action ($kind 'plugins' or 'themes'), its request passed as given (Ops\DirectoryApi), read whatever the status: a refusal is the directory's own words ({"error": ...}); an answer that is not JSON comes back as {"unexpected": body}. Null when the service did not answer.

@param array<string, mixed> $request
@return array<string, mixed>|null
methodqueryThemes(string $search, int $page, int $perPage): array

A page of directory themes for `wp theme search`.

@return array{items: list<array<string, mixed>>, total: int}
methodqueryPlugins(string $search, int $page, int $perPage): array

A page of directory plugins for `wp plugin search`.

@return array{items: list<array<string, mixed>>, total: int}
methoddirectoryTheme(string $slug): ?array

One directory theme record, or null when the slug is unknown.

@return array<string, mixed>|null
methodinstallTheme(string $slug, string $version = ''): string

Installs a directory theme by slug, through the upgrader; returns its stylesheet folder.

methodreplaceTheme(string $slug, string $version = ''): string

Installs a directory theme over the folder already there.

methodunpack(string $zip, string $kind): array

Unpacks an uploaded or downloaded archive into wp-content/themes or wp-content/plugins: a theme or a WordPress plugin through the upgrader, a Minn extension (minn.json) placed by the engine; refused when its folder is taken. @return array{folder: string, name: string, version: string, kind: string}

@return array{folder: string, name: string, version: string, kind: string}
methodunpackReplacing(string $zip, string $kind): array

Unpacks a zip over a folder already there, replacing it whole.

methodremove(string $kind, string $folder): void

Removes a theme or plugin folder that is not in use.

methodidentity(string $dir, string $kind): array

What a folder holds by its headers; kind "unknown" when nothing identifies it. @return array{name: string, version: string, kind: string}

@return array{name: string, version: string, kind: string}
methodfetch(string $url, string ...$hostPrefixes): string

A package over https, every redirect hop included, refusing anything else; when host prefixes are given, every hop must start with one. The request names the engine, never the site's address.

insidepluginPackage(), plain(), themePackage(), upload(), place(), contained(), identify(), ask()

Release final readonly class

public/minn/src/Minn/Ops/Release.php · 58 lines · Open the class →

One published Minn release as the update service describes it (and as the minn_release option keeps it, the same shape): the version, its notes and page, when it went out, and its minn.zip with that file's sha256 and its Ed25519 signature. Anything else is not a release Minn offers: a version that is not major.minor.patch, a package anywhere but Minn's own downloads, a missing checksum, or a missing signature.

constASSET = 'minn.zip'

The archive every release ships: the public/minn/ tree with Minn Admin bundled at minn/admin.

constDOWNLOADS = 'https://updates.minn.run/v1/minn/download/'

Where release packages download from; EngineUpdate fetches nothing else.

new__construct(string $version, string $url, string $published, string $notes, string $package, string $sha256, string $signature)
propreadonly string $version
propreadonly string $url
propreadonly string $published
propreadonly string $notes
propreadonly string $package
propreadonly string $sha256
propreadonly string $signature
staticfromArray(array $row): ?self

A release from the service's answer or the stored option; null when it is not one Minn offers.

@param array<string, mixed> $row
methodtoArray(): array

The release as it is kept in the minn_release option.

@return array{version: string, url: string, published: string, notes: string, package: string, sha256: string, signature: string}
methodnewerThan(string $installed): bool

Whether this release is newer than a version that is installed.

Releases final class

public/minn/src/Minn/Ops/Releases.php · 87 lines · Open the class →

Whether a newer Minn is out, asked of the Minn update service at most once a day: its latest installable release (published on GitHub, read by the service), kept in the minn_release option (JSON) with the time it was asked. A check the service does not answer (offline, GitHub rate limiting it) keeps the last answer and waits a day like any other; no installable release (a 404) offers nothing. This is Minn asking Minn, never wordpress.org, and never GitHub directly.

constOPTION = 'minn_release'
constTTL = 86400
constSOURCE = 'https://updates.minn.run/v1/minn/releases/latest'
new__construct(Closure $load, Closure $save, string $installed, string $source = self::SOURCE, Minn\Http $http = \Minn\Http::__set_state(array( 'within' => array ( ), 'fake' => NULL, 'refusal' => NULL, )))
@param Closure(): ?string $load the stored answer, as JSON
@param Closure(string): void $save keeps the answer, as JSON
staticforSite(Minn\Content\Site $site, string $installed): self

The check for a site, its answer kept in the site's minn_release option.

methodstored(): array

The stored answer: when GitHub was last asked and the latest release then.

@return array{checked: int, latest: ?array<string, string>}
methoddue(): bool

Whether the stored answer is a day old, or there is none.

methodrefresh(): array

Asks the service now and keeps the answer; `answered` says whether it did (when it did not, `latest` is the last answer kept).

@return array{checked: int, latest: ?array<string, string>, answered: bool}
methodoffer(): ?Minn\Ops\Release

The release on offer: the latest one, when it is newer than the running engine.

insideorigin()

ShapeFiles final class

public/minn/src/Minn/Ops/ShapeFiles.php · 104 lines · Open the class →

The files an install lays at the webroot for tooling to find: the shape files from layout/ (index.php, wp-settings.php, version.php and the rest) and one file for each of the reference's wp-includes and wp-admin/includes files, an empty placeholder or, where layout/ ships one, the engine's loader. An engine update lays them again from the new engine: a file still as the replaced engine wrote it is replaced, a missing one is written, and one the site changed is left alone.

constLAYOUT = array ( 0 => 'index.php', 1 => 'wp-login.php', 2 => 'wp-settings.php', 3 => 'wp-cli.yml', 4 => 'wp-includes/version.php', 5 => 'wp-admin/index.php', )

The shape files copied from layout/ as they are.

staticlay(string $engineDir, string $root): array

An install: every layout file copied, and each of the reference's files the site lacks written (an empty placeholder found there becomes the loader layout/ ships for it).

@return array{layout: int, placeholders: int} how many were written
staticrefresh(string $engineDir, string $root, string $previous): int

An update, once the new engine is in place: each file the new engine lays that is missing, or still as the replaced engine ($previous, its folder) laid it, is written from the new one.

staticplaceholder(string $file): string

The empty placeholder for one of the reference's files.

insideboots(), wanted(), referenceFiles(), stale(), write()

Unzip final readonly class

public/minn/src/Minn/Ops/Unzip.php · 112 lines · Open the class →

An archive unpacked as unzip_file() unpacks it (probe unzip-file): into the destination through the filesystem the site set up, every folder made first (the destination and its missing parents included), resource-fork entries (__MACOSX/) and entries whose names climb out or are absolute left out; refused when the disk cannot take twice the unpacked size and a little more. pre_unzip_file may answer first, and unzip_file is handed the result, each with the folders to make and the space needed. An archive the reader cannot open is refused in the words of the library the reference falls back to.

new__construct(object $filesystem, Closure $filter, int $dirMode, int $fileMode)
@param Closure(string, mixed...): mixed $filter applies a filter, as apply_filters does
staticvalid(string $file): bool

Whether a file is a zip archive that opens and passes its consistency check (wp_zip_file_is_valid).

methodinto(string $file, string $to): mixed

The archive's files under the destination: true, a refusal, or what pre_unzip_file answered instead.

insideread(), safe(), write()

Updates final class

public/minn/src/Minn/Ops/Updates.php · 456 lines · Open the class →

Update offers for the site's plugins and themes from the directory, asked through the Minn update service (Ops\Directory), which answers with wordpress.org's own offers and serves their packages: the update-check endpoints asked with the installed headers (each plugin's Name, Version and Update URI, so a plugin that updates from elsewhere is never offered the directory's plugin of the same folder name), the answer kept in the minn_updates option (JSON) for twelve hours, and the offers applied by downloading the release archive through the one package unpacker. A plugin or theme the directory does not know keeps its folder untouched and is never offered anything. The per-item auto-update lists are the site's own auto_update_plugins and auto_update_themes options, in the shape the app already reads.

The directory is not the only source. A plugin that hosts itself answers for its own version through the update transient's filter, which is where WordPress reads it too, so Runtime\PluginUpdates asks the runtime the same question and its answer is merged in. Applying such an offer goes through the publisher: a package outside the directory is unpacked only when `upgrader_pre_download` hands back a copy it verified.

constOPTION = 'minn_updates'
constTTL = 43200
new__construct(Minn\Content\Site $site, Minn\Content\Inventory $inventory, Minn\Ops\Packages $packages, string $contentDir, Minn\Http $http = \Minn\Http::__set_state(array( 'within' => array ( ), 'fake' => NULL, 'refusal' => NULL, )))
staticforSite(Minn\Content\Site $site, string $contentDir): self

The updater over a site's wp-content.

methodcheckForWordPress(string $transient, array $fresh): void

wp_update_plugins and wp_update_themes: the service asked when the stored answer is old (or a fresh one is wanted), and the offers left in the update transients when they are not there already.

methodstate(): array

The stored answer, refreshed when older than the TTL or absent.

methodrefresh(): array

Asks the directory now, whatever the cache says, and keeps the answer.

methodcheck(): array

Asks the directory now and stores the answer.

methodpublish(): void

The offers in WordPress's update transients, update_plugins and update_themes, in the shape wp_update_plugins and wp_update_themes leave there: where plugins, the facade's update helpers and the upgraders read them. Written after every check and after an update.

methodpluginOffers(): array

Plugin file => offered version, only where the installed version is older. @return array<string, string>

@return array<string, string>
methodthemeOffers(): array

Stylesheet => offered version. @return array<string, string>

@return array<string, string>
methodpluginMeta(): array

Plugin file => slug, icon, directory URL, for every plugin the directory knows. @return array<string, array{slug: string, icon: string, url: string}>

@return array<string, array{slug: string, icon: string, url: string}>
methodthemeOnDirectory(string $stylesheet): bool

Whether the directory knows this theme.

methodupdatePlugin(string $file): string

Applies the offer for one plugin file; returns the installed version afterwards.

methodupdateTheme(string $stylesheet): string

Updates one theme to its offer; the new version.

methodauto(string $type): array

The auto-update list for plugins or themes, trimmed to what is installed. @return list<string>

@return list<string>
methodenableAuto(string $type, string $asset): array

Turns auto-updates on or off for one plugin or theme.

@return list<string> the list after the change
methoddisableAuto(string $type, string $asset): array

Takes one plugin or theme off the auto-update list; the list after.

methodrunAuto(Minn\Ops\AutoUpdates $gate): array

Applies every offer on the auto-update lists: what was updated, and what was refused with the reason, by plugin file or theme slug.

@return array{done: list<string>, failed: array<string, string>}
methodpluginVersions(): array

Plugin file => installed version. @return array<string, string>

@return array<string, string>
methodpluginNames(): array

Plugin file => Plugin Name. @return array<string, string>

@return array<string, string>
methodthemeHeaders(): array

Stylesheet => style.css headers. @return array<string, array<string, string>>

@return array<string, array<string, string>>
insidelocales(), supplied(), saveAuto(), pluginHeaders(), apply(), consume(), map(), safeUrl()

UpgraderRun final readonly class

public/minn/src/Minn/Ops/UpgraderRun.php · 113 lines · Open the class →

The engine's own installs and updates, run through the reference's upgraders (Plugin_Upgrader, Theme_Upgrader) so plugins hear every hook a WordPress install or update tells them: the REST plugin route, Minn Admin's install, upload and update routes, the CLI verbs and the automatic updates all come here. The Minn update service's packages are fetched over the engine's own client (Packages::fetch: https, the service's host, a size cap) and handed to the upgrader as a file through upgrader_pre_download, so where Minn gets code is never the plugin-filterable HTTP API's to change. A package from anywhere else is installed only when its publisher answered upgrader_pre_download with a copy it checked (or refused it): nobody's word, no install. A package from the service passes Archive's checks before the upgrader unpacks it; a local file (an upload, checked by Packages) is the upgrader's to read. The upgrader's refusals come back as the engine's REST errors, in the words its routes already use.

new__construct(Minn\Ops\Packages $packages)
methodinstall(string $kind, string $package, array $args = array ( )): array

Installs a package (or, with overwrite_package, replaces the folder it lands on): its folder and the sha256 of a package fetched from the service, '' otherwise.

@param array<string, mixed> $args the upgrader's install arguments
@return array{folder: string, sha256: string}
methodupdate(string $kind, string $item, string $package): string

Applies the offer the update transient holds for one plugin (file) or theme (folder), as Minn Admin does on WordPress: a plugin through bulk_upgrade, a theme through upgrade. The sha256 of the package, '' when it did not come from the service.

insideguarded(), refusal(), taken()