Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/sitetus/collector-wordpress/archive/refs/heads/main.zipSitetus WordPress plugin
The read-only collector of the Sitetus monitoring panel, packaged as a WordPress plugin. User-facing description and directory listing: readme.txt.
How it fits together
includes/collector/is the vendored core ofsitetus/collector— same files, same namespaces, copied byphp bin/sync-core.phpfrom a sibling checkout (never edited here).CORE_VERSIONrecords the copied version.src/is the WordPress glue:Plugin(hooks),Settings(the pairing inwp_options, plus the last-pull record),PairingCode(decode + validate),NonceDirectory(replay store underwp-content/sitetus, web-denied;wp-content/uploads/sitetusonly when wp-content is not writable),Runner(the core'sCollector::runwith WordPress facts),WordPressChecks(core/plugin/theme updates, config flags, admins, auto-updates, themes, plugin inventory),Rest\CollectEndpoint(GET /wp-json/sitetus/v1/collect),Admin\SettingsPage(Settings → Sitetus).- Identity never ships in the plugin: the panel shows a pairing code (public key, project key, nonce file name, modules — no secret) that the owner pastes into WP admin.
src/Readers/+src/SeoPluginModule.phpis the plugin-nativeseo_pluginmodule (0.2.0):PluginReaderis the contract (slug,isActive,version,supports,read),AbstractReaderholds the shared helpers (plugin list, options, prepared read-only$wpdbqueries under aReadBudgetof 2 s), andYoastReader/RankMathReader/AioseoReaderknow one plugin's storage each (the layout is documented in each class header with the version it was verified on).SeoPluginModulepicks the first active reader inREADERS, counts active SEO plugin families (FAMILIES, a premium add-on counts once), bounds the public post types (20 × 32 chars, attachments excluded) and returns the plugin-independent shape the panel ingests. Adding a plugin = one reader class + one entry inREADERS(+ its slugs inFAMILIES); nothing else changes. The module is listed inSettings::PLUGIN_MODULES, so it is always offered on the settings page (opt-in, off by default) whatever the pairing code's core module list says.src/Readers/Security/+src/SecurityPluginModule.phpis the plugin-nativesecurity_pluginmodule (0.4.0):SecurityPluginReaderis a second contract (sameslug/isActive/version/supports, butread()takes no arguments — a security plugin's facts are site wide, whereas the SEO contract is per post type, soPluginReaderwas left untouched rather than generalised).AbstractSecurityReaderuses the sharedReadsPluginStoragetrait plus a bounded administrator list and usermeta enrolment counts;WordfenceReader,KadenceSecurityReader,AiosReader,ReallySimpleSecurityReader,LimitLoginAttemptsReaderandTwoFactorReaderknow one plugin's storage each. Unlike the SEO module, every supported active reader runs: each block (scan,firewall,two_factor,hardening,login_pressure) comes from the first reader inREADERSthat keeps it and names that plugin;conflictscounts active firewalls and login limiters per family (FAMILIES+FAMILY_ROLES, a reader's ownrolesanswer wins). Never an IP, a user name, a path, a finding text or a key. Tests:php -d zend.assertions=1 tests/security.php.src/CoreIntegrityModule.phpis the plugin-nativecore_integritymodule (0.5.0): the core files on disk against the official MD5 list of the installed release. The list comes from WordPress' ownget_core_checksums($version, $locale)(loaded fromwp-admin/includes/update.phpwhen the REST request has not loaded it) — the one outbound request this plugin makes: the site asks api.wordpress.org once per signed request that includes the module (a second time foren_USwhen the package locale has no list, flaggedlocale_fallback;wp-includes/version.phpis then left out because the language line differs). Version and locale are read fromwp-includes/version.php($wp_version,$wp_local_package), not from the site-language setting. Output:modified/missing/unexpectedas{count, paths}(20 relative paths at most, 200 bytes each),root_extraas{count, names},version,locale,locale_fallback,checked,partial,unreadable,duration_ms— never a hash, content or mtime.wp-content/is skipped; root-levelwp-config-sample.php,*.htmland*.txtare ignored;unexpectedlooks at PHP-family extensions underwp-admin/andwp-includes/only. Budgets:MAX_FILES5,000,MAX_BYTES256 MB,BUDGET_SECONDS5 (the clock starts after the list arrived),MAX_WALK_ENTRIES20,000 →partial: true. The directory walk runs first (it only lists directories), then the comparison walks the sorted official list circularly fromstartOffset()= (UTC day number ×ROTATION_STRIDE150) mod N: stateless, and at the slowest observed rate (172 files per request on a cold disk) every file is reached within ceil(N / 150) days (23 for a 7.x core); a run that finishes is unaffected by the offset. The request also carries WordPress' standard User-Agent (version and site address) — every text that describes the request says so; the panel offers the module unticked. Links are never followed (counted inunreadable). No list →unsupported: checksums_unavailable. Writes nothing and caches nothing (tests/writes.phpruns it with every other module). Tests:php -d zend.assertions=1 tests/core_integrity.php.
What the plugin writes
The plugin never changes content, users, posts, themes or other plugins' data. It writes exactly three things of its own, and uninstall.php removes all three (on every site of a multisite network):
- the option
sitetus_collector(Plugin::OPTION) — the pairing and the module choice; - the option
sitetus_last_pull(Plugin::OPTION_LAST_PULL) — time and status of the last signed request; - the replay-protection record — the nonce file named in the pairing code plus a web-deny
.htaccess, in theNonceDirectoryfolder.
tests/writes.php holds this line: it fails when any other option name is written, when a WordPress write API appears in the source, when a signed request creates any other file, or when uninstall leaves something behind. readme.txt and the settings page say the same thing in the owner's words; keep the three in step.
Develop
php bin/sync-core.php # refresh the vendored core from ../sitetus-collector
php bin/test.php --envelope=/tmp/envelope.json # every tests/*.php with assertions on (what CI runs); smoke's envelope saved
php -d zend.assertions=1 -d assert.exception=1 tests/readers.php # one file on its own
php bin/build-zip.php # dist/sitetus.zip (compiles languages/*.mo first)
bin/test.php discovers the test files, so a new tests/*.php runs locally and in CI without being listed anywhere.
To run the seo_plugin module against a real install (WP-CLI): wp eval 'require_once ABSPATH."wp-admin/includes/plugin.php"; require "/path/to/sitetus-wordpress/includes/autoload.php"; echo json_encode(Sitetus\WP\SeoPluginModule::run());'.
The envelope in /tmp/envelope.json must pass the panel's EnvelopeValidator (checked from the panel repo).
Release
readme.txt Stable tag, sitetus.php Version and SITETUS_PLUGIN_VERSION move together. The zip's single top-level folder is sitetus/.
License
GPLv2 or later — LICENSE. Copyright (C) 2026 Codemarkt Yazılım Sanayi Ticaret A.Ş. The vendored collector core under includes/collector/ is the sitetus/collector package (MIT, Codemarkt), which is GPL-compatible.