Repository navigation
v8.5.51 — the Shield holds; forms, action arguments, services and the Logger simplified (breaking)
For logging, take v8.5.52 instead. It restores the Logger's query-name and token-path redaction, identity masking as a policy, and
keepingIdentities(),looksSecret()andSender::maskName(), so the callers listed below need no change. Everything else in these notes still applies.
⚠️ Read before upgrading
This release removes public API (see Breaking changes). Code that still calls a removed method fails with Call to undefined method, and an old query tuple fails with a TypeError, once composer update brings it in. Search your code before you update:
grep -rnE "keepingIdentities|maskEmails|looksSecret|addUsernames|getUsernames|addQueryNames|getQueryNames|maskName|CODESAFE_USERNAMES|MASK_(GROUP|MAX)|MASKED_CUT_PATTERN|->as(Text|Number|Integer|Choice|Flag|Collection)\(|addNormalizer|getNormalizers|getGate\(|Normalizer|Validator\\\\Gate|castParam|getCastedParams|bodyIds|Body::ids|->ids\(|->emails?\(|setQueueConnection|(where|andWhere|orWhere|innerJoin|leftJoin|having|select|delete)\(\[" .Known callers that need a change before they take this tag:
CMS\Log\Writer\Codesafein cms-trunk and leadersnet callsLogger::keepingIdentities(),Logger::looksSecret()andSender::maskName(). Both require^8.5, so their nextcomposer updatepicks this tag up.- bo2go's
Service\AuthcallsSender::maskName()inside acatch (Throwable): itsauth_failureandauth_successreports stop and nothing logs an error.
The Shield kernel
src/Codesafe/Shield/Kernel.php and Wordpress.php are codesafe's client-php kernel byte for byte (sha1 33e46314 and 0a9a5a47). Nothing changes for a host: Service\Codesafe\Shield hands the kernel the same inputs, and the switches are as before.
- The stored ruleset is sealed. The kernel keeps an HMAC-SHA256 over every field of its record, under a key derived from the project key (
Kernel::sealKey()). The kernel treats a record that is not sealed under that key as no record at all, and the next pull fetches the whole ruleset. That covers a record planted in the store file or a shared APCu pool, one changed after the write, and one written by an older kernel. After the upgrade every site pulls its full ruleset once. - Padding no longer turns a block into an observe. A condition past a read cap is
unread, and a virtual patch whose other entries pin the handler still blocks, with reasonunread. Decoding shares one budget per request (Facts::DECODED_BUDGET, 131072 bytes). - Form bodies on PUT, PATCH and DELETE are read as WordPress reads them (PHP leaves
$_POSTempty on those methods). The method judged is the one WordPress's REST server runs, after_methodorX-HTTP-Method-Override. - Uploads are read whole for a
file-content:condition, withinFacts::FILE_BUDGET(16 MiB a request). An upload past the budget is unread and blocks on a pinned handler, as every other cut does. - A regex the engine gives up on is unread, never a pass. A module or a test that throws costs its own rule, not the whole request.
- Rules cut past the kernel's caps are reported. Their ids are kept in the sealed record and sent as
X-Shield-Dropped. - XML-RPC parameters are a target, read as WordPress's IXR reads them. The WordPress module adds
wordpress-xmlrpctoX-Shield-Platforms. - A site cut off from codesafe keeps its blocking rules. Once the last good pull is older than
Kernel::CEILING(24 h), match, rate and virtual-patch rules that block hold until their ownexpires_at; observe rules and the threat section lapse. A failed pull holds the next one off forKernel::RETRY(60 s). Reports sent past the ceiling are taggedstalewithpulled_at. codesafe's own JSON 401 (a rotated key, a project switched off) lifts every rule. Service\Codesafe\Shieldkeeps its record on disk even when its folder cannot be created or written, by falling back to the temp dir itself. Past the ceiling the blocking rules hold only while that record exists, and an idle APCu eviction (apc.ttl) empties APCu.
Pair it with codesafe d0f1509a or later, which reads X-Shield-Dropped, stale and pulled_at.
The codesafe Sender
- A rollup sequence that starts over gets a new instance. When APCu dropped the sequence counter but kept the instance (an
apc.ttlidle eviction), the instance sent seq 1 again, and codesafe dropped every following fragment as a retry. The instance id now also carries three random bytes, so a new one made within the same second differs.
⚠️ Breaking changes
Logger: secrets by name, identities raw everywhere
- Local log files no longer mask e-mail addresses or usernames in the GET and POST data, the only place they were masked there. The copy sent to codesafe was already raw; codesafe masks and vaults identities when they arrive.
- Redaction goes by field name only:
Logger::$remove, extended withaddRemove(). A path segment is redacted when the segment before it is a secret name (/token/<value>). - No longer redacted: a token-shaped path segment with no secret name before it (
/invite/<token>), and the query-only nameskey,code,sig,pinandhash(wp-login.php?action=rp&key=…). codesafe's own scrubber still drops these when they arrive. The log file wrote the request URI and argv unredacted up to v8.5.50; from this release it redacts them by secret name. A project that needs one adds a pattern withaddRemove(), which then applies to field names everywhere. - Removed:
keepingIdentities(),maskEmails(),addUsernames(),getUsernames(),looksSecret(),addQueryNames(),getQueryNames(), and the constantsCODESAFE_USERNAMES,MASK_GROUP,MASK_MAXandMASKED_CUT_PATTERN. Sender::maskName()is removed. Keep the username out of areportRefusal()message and pass the account in the context (['userId' => …]), where codesafe can recognise it.
Services: boot() and terminate()
Service::boot(Application $app)runs for every service in the configuration once all of them are registered, for each application, including a reused instance on a warm worker. An override callsparent::boot($app).Service::terminate()runs after the deferred response callbacks, in configuration order, before the collected events are cleared.Eventsno longer finds its own writers: the Logger and the codesafe Sender add themselves inboot().Events::addWriter($writer, $key = null)takes a key, and a keyed writer replaces the previous one under that key. A Logger or Sender that is not in the configured services is never booted and writes nothing. List it there, or calladdWriter()yourself.- The codesafe Sender flushes in
terminate().Applicationno longer handles it as a special case.
Controllers: PHP coerces action arguments
Controller::castParam()andgetCastedParams()are removed. Actions are called through reflection, so PHP's own weak coercion applies."42"still reaches anintparameter as42, but a value PHP cannot coerce ("abc"for anint) now raises aTypeErrorinstead of becoming0.Controller\Api::bodyIds(),Http\Body::ids(),Http\Input::ids(),Input::email()andInput::emails()are removed. Validate ids and addresses with a form;Validator\Eachapplies one validator to every entry of a list.
Forms: input validators, filters, validators, casts
- Removed:
Element::asText(),asNumber(),asInteger(),asChoice(),asFlag(),asCollection(),getGate(),addNormalizer()andgetNormalizers(), plus the classesForm\Normalizer,Form\Normalizer\CallbackandForm\Validator\Gate.Validator\Text,NumberandCollectionextendValidatordirectly and keep their error codes (type_text,type_number). - A value goes through
raw input → input validators → filters → validators → casts:addInputValidator()checks the raw value before any filter. A failure stops the field, and its value reads asnull.addFilter($filter, each: false)hands the filter the whole array. Filters still run per item by default.addValidator($validator, breakOnFailure: true)skips the field's later rules after a failure. Rules run in the order they are declared.addCast()takes a filter or a callable. Casts run only ingetCastValue()andgetPresentValues().
- A filter that returns
nullno longer fails the field. AddValidator\NotNullwherenullis not an acceptable value. - New:
Validator\NotNull,Validator\Each,Filter\Text,Filter\NumberandFilter\Flag, plusFilter\Integer(nullable: true), which keepsnullasnull, andValidator\InArray(nullable:).Element\Text,Number,FlagandCollectionset these steps up in their constructors.
// before
$this->period->asInteger('must be a number of minutes')
->addValidator(new Validator\Range(1, 10080, nullable: false));
// after
$this->period
->addInputValidator((new Validator\Number)->withMessage('must be a number of minutes'))
->addValidator(new Validator\Range(1, 10080, nullable: false), breakOnFailure: true)
->addValidator(new Validator\WholeNumber)
->addCast(new Filter\Integer(nullable: true));Query builder: Fragment instead of tuples
- A clause with bound values is now
new Fragment($sql, ...$values)(Ovos\Store\Mysql\Query\Fragment), wherever a tuple[$sql, ...$values]used to go:select(),where(),andWhere(),orWhere(),innerJoin(),leftJoin(),having()andQueryBuilder::delete(). An array tuple there is now aTypeError. - Any callable opens a nested group, not only a
Closure:where([$conditions, 'active'])calls$conditions->active($group).
Cache, container and HTTP requests
Cache\Store\KeyValue\Redis::setConnection()andsetQueueConnection()are removed. A store keeps the connections its constructor received; create another store for other connections.RedisVersioned::setGroup()with a different group discards the store's local rule state, so one group's rules are never applied to another. Each group keeps its own shared rules in APCu.Containerinjects a value passed by name asnullasnull. Before, thenullwas skipped and the parameter fell back to the container or its default.Stream\Requestcan be invoked again: eachinvoke()starts with a fresh response and measurement.setContent()takes a string body and a content type as well as an array, andgetContent()returnsarray|string|null. A body goes out on every method except GET, where an array is still the query string.setJsonContent()builds onsetContent(), sosetMethod()can turn a JSON POST into a PUT. A read that fails or times out now throws, or leaves a null response undersetBestEffort(); before, a timed-out read kept whatever had arrived.getContextOptions()returns the built options, body and Content-Type included.
Also
Test\Cache\Store\TraitStoredTime(storedTtlMs(),ageStoredValue()): the cache policy tests age a stored value instead of sleeping.- The codesafe Sender's docs name the working-copy pass
php cli.php codesafe integrityand the project switchintegrity_enabled.
Full changelog: v8.5.50...v8.5.51