diff --git a/changelog.txt b/changelog.txt index e1013fc..1700450 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,38 @@ == Changelog == += 1.2.2 = + +_Release date: 2026-08-24_ + +**Highlights** + +* Manager (by ROBOTSTXT) detection now uses the ecosystem presence constant ROBOTSTXT_MANAGER_NOTICED (Manager 1.6.2+), with a plugin-list scan fallback for older Manager versions + +**Changed** + +* `robotstxt_docmd_is_manager_active()` now checks the ROBOTSTXT_MANAGER_NOTICED presence constant (Manager 1.6.2+) and falls back to scanning the plugin list (get_plugins() + is_plugin_active()) when the constant is absent; same function name and return type, so callers are unaffected + +**Security** + +* `composer update` (phpstan 2.2.9, nette/schema 1.3.6 — dev tooling only); `composer audit` clean, no known CVEs + +**Compatibility** + +* WordPress: 4.2 - 7.1 (floor re-verified with WP-Compat; smoke-tested on 7.2-alpha) +* PHP: 8.0 - 8.5 (floor re-verified with PHPCompatibility 5.6-8.5 full-range scan) + +**Translations** + +* Spanish (es_ES) and Catalan (ca): verified complete and in sync (156 strings, no changes needed) + +**Tests** + +* PHP Coding Standards: PHPCS with WordPress-Core, WordPress-Docs, WordPress-Extra — 0 errors +* PHPStan: level 9, 0 errors +* PHPCompatibility: PHP 8.0-8.5 validated (full-range scan 5.6-8.5) +* WP-Compat: WordPress 4.2 floor validated +* PHPUnit: 23 tests pass (17 plugin header tests + 6 new Manager-detection tests covering both detection paths) + = 1.2.1 = _Release date: 2026-08-17_ diff --git a/readme.txt b/readme.txt index 5c061f8..ac59063 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: github, documentation, markdown, sync, automation Requires at least: 4.2 Tested up to: 7.1 Requires PHP: 8.0 -Stable tag: 1.2.1 +Stable tag: 1.2.2 License: GPLv3 or later License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -189,6 +189,22 @@ Then go to Documentation → Settings, and you'll see a "Debug Tools" section at For the complete changelog, see [changelog.txt](https://www.robotstxt.software/plugins/robotstxt-documentation-markdown/). += 1.2.2 - 2026-08-24 = + +**Changed** + +* Manager (by ROBOTSTXT) detection now uses the ecosystem presence constant `ROBOTSTXT_MANAGER_NOTICED` (Manager 1.6.2+), falling back to a plugin-list scan for older Manager versions — same function name and behavior for callers +* New unit tests for both detection paths (constant defined / plugin-list fallback) + +**Compatibility** + +* WordPress: 4.2 - 7.1 (floor re-verified with WP-Compat; smoke-tested on 7.2-alpha) +* PHP: 8.0 - 8.5 (floor re-verified with PHPCompatibility 5.6-8.5 full-range scan) + +**Translations** + +* Spanish (es_ES) and Catalan (ca): verified complete and in sync (156 strings, no changes needed) + = 1.2.1 - 2026-08-17 = **Changed** @@ -238,42 +254,11 @@ For the complete changelog, see [changelog.txt](https://www.robotstxt.software/p * Inline `onclick` confirm on Delete replaced with a `data-confirm` attribute + delegated handler (CSP-friendlier, less fragile) * Add Mapping "existing content" dropdown now queries only public post types (excludes attachments and the internal mapping CPT) with `no_found_rows` for better performance on large sites -**Compatibility** - -* Minimum WordPress raised to 6.8 (latest stable + two previous majors) -* Verified compatible with WordPress 7.1 -* Declared PHP minimum made consistent across `readme.txt`, `composer.json`, `phpstan.neon`, and the PHPCompatibility scan range — real minimum remains 8.0 - -= 1.1.1 - 2026-06-08 = - -**Security** - -* CommonMark: disabled raw HTML passthrough (`html_input: strip`, `allow_unsafe_links: false`) — prevents stored XSS via a compromised upstream repository -* Settings option now stored with `autoload = false` — encrypted GitHub token no longer loaded on every WordPress request -* Replaced `serialize()` with `wp_json_encode()` in updater HMAC cache signature -* Added strict base64 length validation before IV extraction in token decryption -* Added `base64_decode()` return-value check in GitHub file content fetch - -**Fixed** - -* Admin notices now display results for all operations: mapping created, updated, deleted, sync complete, and errors — previously only "Settings saved" was shown -* `wp_update_post()` return value now checked during sync — silent failures no longer reported as success -* Wrong textdomain in updater class (`'robotstxt-smtp'` → `'robotstxt-documentation-markdown'`) - -**Changed** - -* Renamed `robotstxt-updater.php` → `class-robotstxt-updater.php` (WordPress file naming convention) -* Minimum PHP version corrected to 8.0 (real minimum confirmed by PHPCompatibility scan — no PHP 8.1 or 8.2 specific features used) - -**Developer** - -* Added full tooling: `composer.json`, `phpstan.neon` (level 9), `.phpcs.xml`, `bin/deploy.sh`, `phpunit.xml.dist` -* PHPCompatibility updated to 10.0.0-alpha2 (PHP 8.x feature detection) -* PHPUnit: 17 plugin header tests added -* PHPCS, PHPStan level 9, PHPUnit all pass with 0 errors - == Upgrade Notice == += 1.2.2 = +Manager (by ROBOTSTXT) detection updated: now uses the ROBOTSTXT_MANAGER_NOTICED presence constant (Manager 1.6.2+) with a fallback for older Manager versions. + = 1.2.1 = Automatic updates now require the Manager (by ROBOTSTXT) plugin. A notice with the download link is shown on the Plugins and Settings pages. diff --git a/robotstxt-documentation-markdown.php b/robotstxt-documentation-markdown.php index e52d538..a6430a2 100644 --- a/robotstxt-documentation-markdown.php +++ b/robotstxt-documentation-markdown.php @@ -3,7 +3,7 @@ * Plugin Name: Documentation Markdown (by ROBOTSTXT) * Plugin URI: https://www.robotstxt.software/plugins/robotstxt-documentation-markdown/ * Description: Synchronizes Markdown documentation from GitHub repositories to WordPress pages and posts automatically. - * Version: 1.2.1 + * Version: 1.2.2 * Requires at least: 4.2 * Requires PHP: 8.0 * Security: robotstxt@robotstxt.es @@ -25,7 +25,7 @@ if ( ! defined( 'ABSPATH' ) ) { } // Define plugin constants. -define( 'ROBOTSTXT_DOCMD_VERSION', '1.2.1' ); +define( 'ROBOTSTXT_DOCMD_VERSION', '1.2.2' ); define( 'ROBOTSTXT_DOCMD_PLUGIN_FILE', __FILE__ ); define( 'ROBOTSTXT_DOCMD_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'ROBOTSTXT_DOCMD_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); @@ -1676,17 +1676,41 @@ function robotstxt_docmd_debug_fix_crons() { } /** - * Check whether the Manager (by ROBOTSTXT) plugin is active + * Check whether the Manager (by ROBOTSTXT) plugin is installed and active * * Updates for this plugin are delivered through the Manager plugin. This - * helper detects whether it is installed and active. + * helper detects whether it is installed and active: it uses the ecosystem + * presence constant (Manager 1.6.2+) and falls back to a plugin-list scan + * for older Manager versions. * * @since 1.2.1 + * @since 1.2.2 Switched to the ROBOTSTXT_MANAGER_NOTICED presence constant + * with a plugin-list scan fallback for Manager < 1.6.2. * * @return bool True if Manager (by ROBOTSTXT) is active, false otherwise. */ function robotstxt_docmd_is_manager_active(): bool { - return defined( 'ROBOTSTXT_MANAGER_VERSION' ); + if ( defined( 'ROBOTSTXT_MANAGER_NOTICED' ) && ROBOTSTXT_MANAGER_NOTICED ) { + return true; + } + + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + foreach ( get_plugins() as $file => $data ) { + $slug = dirname( $file ); + + if ( '.' === $slug ) { + $slug = basename( $file, '.php' ); + } + + if ( 'robotstxt-manager' === $slug ) { + return is_plugin_active( $file ); + } + } + + return false; } /** diff --git a/update.json b/update.json deleted file mode 100644 index 4fa49e4..0000000 --- a/update.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "Documentation Markdown (by ROBOTSTXT)", - "slug": "robotstxt-documentation-markdown", - "version": "1.2.1", - "download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-documentation-markdown/releases/download/1.2.1/robotstxt-documentation-markdown-1.2.1.zip", - "requires": "6.8", - "requires_php": "8.0", - "tested": "7.1", - "last_updated": "2026-08-07", - "author": "ROBOTSTXT", - "author_profile": "https://www.robotstxt.es/", - "homepage": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-documentation-markdown", - "description": "Synchronizes Markdown documentation from GitHub repositories to WordPress pages and posts automatically. Perfect for maintaining technical documentation, API references, knowledge bases, and more with version control.", - "changelog": "", - "sections": { - "description": "

Documentation Markdown is a powerful WordPress plugin that enables seamless synchronization of Markdown documentation files from GitHub repositories into your WordPress site.

Key Features

Use Cases

Requirements

", - "changelog": "" - }, - "banners": { - "low": "", - "high": "" - }, - "icons": { - "1x": "", - "2x": "" - } -} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 6194647..350ecaf 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -275,17 +275,17 @@ }, { "name": "nette/schema", - "version": "v1.3.5", - "version_normalized": "1.3.5.0", + "version": "v1.3.6", + "version_normalized": "1.3.6.0", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + "reference": "c54350438cd6914616f790a49cb424605f421562" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", - "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "url": "https://api.github.com/repos/nette/schema/zipball/c54350438cd6914616f790a49cb424605f421562", + "reference": "c54350438cd6914616f790a49cb424605f421562", "shasum": "" }, "require": { @@ -299,7 +299,7 @@ "phpstan/phpstan": "^2.1.39@stable", "tracy/tracy": "^2.8" }, - "time": "2026-02-23T03:47:12+00:00", + "time": "2026-08-16T21:58:41+00:00", "type": "library", "extra": { "branch-alias": { @@ -339,7 +339,7 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3.5" + "source": "https://github.com/nette/schema/tree/v1.3.6" }, "install-path": "../nette/schema" }, diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index dfced0b..a249c41 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -38,9 +38,9 @@ 'dev_requirement' => false, ), 'nette/schema' => array( - 'pretty_version' => 'v1.3.5', - 'version' => '1.3.5.0', - 'reference' => 'f0ab1a3cda782dbc5da270d28545236aa80c4002', + 'pretty_version' => 'v1.3.6', + 'version' => '1.3.6.0', + 'reference' => 'c54350438cd6914616f790a49cb424605f421562', 'type' => 'library', 'install_path' => __DIR__ . '/../nette/schema', 'aliases' => array(), diff --git a/vendor/nette/schema/readme.md b/vendor/nette/schema/readme.md index ce9d0d1..7e52878 100644 --- a/vendor/nette/schema/readme.md +++ b/vendor/nette/schema/readme.md @@ -126,7 +126,7 @@ Expect::null() Expect::array($default = []) ``` -And then all types [supported by the Validators](https://doc.nette.org/validators#toc-validation-rules) via `Expect::type('scalar')` or abbreviated `Expect::scalar()`. Also class or interface names are accepted, e.g. `Expect::type('AddressEntity')`. +And then all types [supported by the Validators](https://doc.nette.org/validators#toc-expected-types) via `Expect::type('scalar')` or abbreviated `Expect::scalar()`. Also class or interface names are accepted, e.g. `Expect::type('AddressEntity')`. You can also use union notation: diff --git a/vendor/nette/schema/src/Schema/Context.php b/vendor/nette/schema/src/Schema/Context.php index a002d5c..b42b1ed 100644 --- a/vendor/nette/schema/src/Schema/Context.php +++ b/vendor/nette/schema/src/Schema/Context.php @@ -10,6 +10,9 @@ namespace Nette\Schema; use function count; +/** + * Accumulates errors and warnings during schema validation and tracks the current path. + */ final class Context { public bool $skipDefaults = false; @@ -44,7 +47,10 @@ final class Context } - /** @return \Closure(): bool */ + /** + * Returns a closure that returns true as long as no new errors have been added since the call. + * @return \Closure(): bool + */ public function createChecker(): \Closure { $count = count($this->errors); diff --git a/vendor/nette/schema/src/Schema/Elements/AnyOf.php b/vendor/nette/schema/src/Schema/Elements/AnyOf.php index 09e21c9..582cf13 100644 --- a/vendor/nette/schema/src/Schema/Elements/AnyOf.php +++ b/vendor/nette/schema/src/Schema/Elements/AnyOf.php @@ -14,6 +14,9 @@ use Nette\Schema\Schema; use function array_merge, array_unique, implode, is_array; +/** + * Schema that accepts any of a fixed set of values or sub-schemas (union type / enumeration). + */ final class AnyOf implements Schema { use Base; @@ -32,6 +35,9 @@ final class AnyOf implements Schema } + /** + * Sets the first variant as the default value (instead of null). + */ public function firstIsDefault(): self { $this->default = $this->set[0]; @@ -39,6 +45,9 @@ final class AnyOf implements Schema } + /** + * Allows null as an accepted value in addition to the defined variants. + */ public function nullable(): self { $this->set[] = null; @@ -46,6 +55,9 @@ final class AnyOf implements Schema } + /** + * Allows the value to be a DynamicParameter as an accepted variant. + */ public function dynamic(): self { $this->set[] = new Type(Nette\Schema\DynamicParameter::class); @@ -88,10 +100,13 @@ final class AnyOf implements Schema foreach ($this->set as $item) { if ($item instanceof Schema) { $dolly = new Context; + $dolly->skipDefaults = $context->skipDefaults; + $dolly->isKey = $context->isKey; $dolly->path = $context->path; $res = $item->complete($item->normalize($value, $dolly), $dolly); if (!$dolly->errors) { $context->warnings = array_merge($context->warnings, $dolly->warnings); + $context->dynamics = array_merge($context->dynamics, $dolly->dynamics); return $res; } diff --git a/vendor/nette/schema/src/Schema/Elements/Base.php b/vendor/nette/schema/src/Schema/Elements/Base.php index 6e83eb7..c4e3dd6 100644 --- a/vendor/nette/schema/src/Schema/Elements/Base.php +++ b/vendor/nette/schema/src/Schema/Elements/Base.php @@ -43,7 +43,10 @@ trait Base } - /** @param callable(mixed): mixed $handler */ + /** + * Sets a pre-normalization callback applied to the raw input value before any validation. + * @param callable(mixed): mixed $handler + */ public function before(callable $handler): self { $this->before = $handler(...); @@ -51,13 +54,19 @@ trait Base } + /** + * Casts the validated value to a built-in type or instantiates the given class. + */ public function castTo(string $type): self { return $this->transform(Helpers::getCastStrategy($type)); } - /** @param callable(mixed, Context): mixed $handler */ + /** + * Adds a post-validation transformation callback. The handler may also report errors via Context. + * @param callable(mixed, Context): mixed $handler + */ public function transform(callable $handler): self { $this->transforms[] = $handler(...); @@ -65,7 +74,10 @@ trait Base } - /** @param callable(mixed): bool $handler */ + /** + * Adds a custom validation assertion; optionally describe it for error messages. + * @param callable(mixed): bool $handler + */ public function assert(callable $handler, ?string $description = null): self { $expected = $description ?? (is_string($handler) ? "$handler()" : '#' . count($this->transforms)); @@ -78,11 +90,14 @@ trait Base Nette\Schema\Message::FailedAssertion, ['value' => $value, 'assertion' => $expected], ); + return null; }); } - /** Marks as deprecated */ + /** + * Marks the item as deprecated; emits a warning with the given message when the item is used. + */ public function deprecated(string $message = 'The item %path% is deprecated.'): self { $this->deprecated = $message; @@ -138,7 +153,7 @@ trait Base } - /** @deprecated use Nette\Schema\Validators::validateType() */ + /** @deprecated use Nette\Schema\Helpers::validateType() */ private function doValidate(mixed $value, string $expected, Context $context): bool { $isOk = $context->createChecker(); @@ -148,7 +163,7 @@ trait Base /** - * @deprecated use Nette\Schema\Validators::validateRange() + * @deprecated use Nette\Schema\Helpers::validateRange() * @param array{?float, ?float} $range */ private static function doValidateRange(mixed $value, array $range, Context $context, string $types = ''): bool diff --git a/vendor/nette/schema/src/Schema/Elements/Structure.php b/vendor/nette/schema/src/Schema/Elements/Structure.php index 1abf49f..a6cf02d 100644 --- a/vendor/nette/schema/src/Schema/Elements/Structure.php +++ b/vendor/nette/schema/src/Schema/Elements/Structure.php @@ -39,6 +39,9 @@ final class Structure implements Schema } + /** + * Not supported for structures; always throws. + */ public function default(mixed $value): self { throw new Nette\InvalidStateException('Structure cannot have default value.'); @@ -59,6 +62,9 @@ final class Structure implements Schema } + /** + * Allows extra keys not defined in the shape, validating their values against the given type. + */ public function otherItems(string|Schema $type = 'mixed'): self { $this->otherItems = $type instanceof Schema ? $type : new Type($type); @@ -66,6 +72,9 @@ final class Structure implements Schema } + /** + * When enabled, properties whose value equals the default are omitted from the output. + */ public function skipDefaults(bool $state = true): self { $this->skipDefaults = $state; @@ -73,7 +82,10 @@ final class Structure implements Schema } - /** @param Schema[]|self $shape */ + /** + * Creates a new structure by merging this shape with additional properties. + * @param Schema[]|self $shape + */ public function extend(array|self $shape): self { $shape = $shape instanceof self ? $shape->items : $shape; @@ -203,8 +215,17 @@ final class Structure implements Schema public function completeDefault(Context $context): mixed { - return $this->required - ? $this->complete([], $context) - : null; + if (!$this->required) { + return null; + } + + // the item is missing in the input, do not report it as used deprecated + $deprecated = $this->deprecated; + $this->deprecated = null; + try { + return $this->complete([], $context); + } finally { + $this->deprecated = $deprecated; + } } } diff --git a/vendor/nette/schema/src/Schema/Elements/Type.php b/vendor/nette/schema/src/Schema/Elements/Type.php index 4413f69..880f4bb 100644 --- a/vendor/nette/schema/src/Schema/Elements/Type.php +++ b/vendor/nette/schema/src/Schema/Elements/Type.php @@ -11,6 +11,7 @@ use Nette\Schema\Context; use Nette\Schema\DynamicParameter; use Nette\Schema\Helpers; use Nette\Schema\Schema; +use Nette\Utils\Validators; use function array_key_exists, array_pop, implode, is_array, str_replace, strpos; @@ -36,6 +37,9 @@ final class Type implements Schema } + /** + * Allows the value to be null in addition to the declared type. + */ public function nullable(): self { $this->type = 'null|' . $this->type; @@ -43,6 +47,9 @@ final class Type implements Schema } + /** + * Controls whether the default value is merged with the input array (enabled by default). + */ public function mergeDefaults(bool $state = true): self { $this->merge = $state; @@ -50,6 +57,9 @@ final class Type implements Schema } + /** + * Allows the value to be a DynamicParameter, which is recorded for deferred validation. + */ public function dynamic(): self { $this->type = DynamicParameter::class . '|' . $this->type; @@ -86,6 +96,9 @@ final class Type implements Schema } + /** + * Sets a regex pattern the string value must match entirely (anchored to start and end). + */ public function pattern(?string $pattern): self { $this->pattern = $pattern; @@ -162,7 +175,7 @@ final class Type implements Schema $merge = false; } - if ($value === null && is_array($this->default)) { + if ($value === null && is_array($this->default) && !Validators::is(null, $this->type)) { $value = []; // is unable to distinguish null from array in NEON } @@ -173,13 +186,13 @@ final class Type implements Schema $isOk() && Helpers::validateRange($value, $this->range, $context, $this->type); $isOk() && $value !== null && $this->pattern !== null && Helpers::validatePattern($value, $this->pattern, $context); $isOk() && is_array($value) && $this->validateItems($value, $context); - $isOk() && $merge && $value = Helpers::merge($value, $this->default); + $isOk() && $merge && $value !== null && $value = Helpers::merge($value, $this->default); $isOk() && $value = $this->doTransform($value, $context); if (!$isOk()) { return null; } - if ($value instanceof DynamicParameter) { + if ($value instanceof DynamicParameter && $this->type !== DynamicParameter::class) { $expected = $this->type . ($this->range === [null, null] ? '' : ':' . implode('..', $this->range)); $context->dynamics[] = [$value, str_replace(DynamicParameter::class . '|', '', $expected), $context->path]; } @@ -190,7 +203,7 @@ final class Type implements Schema /** @param array $value */ private function validateItems(array &$value, Context $context): void { - if (!$this->itemsValue) { + if (!($itemsValue = $this->itemsValue)) { return; } @@ -198,9 +211,15 @@ final class Type implements Schema foreach ($value as $key => $val) { $context->path[] = $key; $context->isKey = true; + $isKeyOk = $context->createChecker(); $key = $this->itemsKey ? $this->itemsKey->complete($key, $context) : $key; $context->isKey = false; - $res[$key ?? ''] = $this->itemsValue->complete($val, $context); + $keyOk = $isKeyOk(); + $val = $itemsValue->complete($val, $context); + if ($keyOk) { + $res[$key] = $val; + } + array_pop($context->path); } $value = $res; diff --git a/vendor/nette/schema/src/Schema/Expect.php b/vendor/nette/schema/src/Schema/Expect.php index 64e31f2..44cf25b 100644 --- a/vendor/nette/schema/src/Schema/Expect.php +++ b/vendor/nette/schema/src/Schema/Expect.php @@ -42,26 +42,38 @@ final class Expect } + /** + * Creates a schema for a custom type expression (e.g., 'int|string', 'null|float'). + */ public static function type(string $type): Type { return new Type($type); } + /** + * Creates a union schema that accepts any of the given values or sub-schemas. + */ public static function anyOf(mixed ...$set): AnyOf { return new AnyOf(...$set); } - /** @param Schema[] $shape */ + /** + * Creates a structure schema with defined properties; output is stdClass. + * @param Schema[] $shape + */ public static function structure(array $shape): Structure { return new Structure($shape); } - /** @param array $items */ + /** + * Generates a structure schema from a class instance by reflecting its properties or constructor parameters. + * @param array $items Optional overrides for specific properties. + */ public static function from(object $object, array $items = []): Structure { $ro = new \ReflectionObject($object); @@ -95,6 +107,8 @@ final class Expect /** + * Creates an array schema. When passed Schema elements, behaves like structure() but outputs an array. + * Without Schema elements, creates a plain array type with the given default value. * @param mixed[] $shape */ public static function array(?array $shape = []): Structure|Type @@ -106,12 +120,18 @@ final class Expect } + /** + * Creates an associative or indexed array schema where every value matches the given type. + */ public static function arrayOf(string|Schema $valueType, string|Schema|null $keyType = null): Type { return (new Type('array'))->items($valueType, $keyType); } + /** + * Creates a list schema (sequentially indexed from 0) where every element matches the given type. + */ public static function listOf(string|Schema $type): Type { return (new Type('list'))->items($type); diff --git a/vendor/nette/schema/src/Schema/Helpers.php b/vendor/nette/schema/src/Schema/Helpers.php index 79a4a3c..e15bf50 100644 --- a/vendor/nette/schema/src/Schema/Helpers.php +++ b/vendor/nette/schema/src/Schema/Helpers.php @@ -9,7 +9,7 @@ namespace Nette\Schema; use Nette; use Nette\Utils\Reflection; -use function count, explode, get_debug_type, implode, in_array, is_array, is_float, is_int, is_object, is_scalar, is_string, method_exists, preg_match, preg_quote, preg_replace, preg_replace_callback, settype, str_replace, strlen, trim, var_export; +use function array_map, count, explode, get_debug_type, implode, in_array, is_array, is_float, is_int, is_object, is_scalar, is_string, is_subclass_of, method_exists, preg_match, preg_quote, preg_replace, preg_replace_callback, settype, str_replace, strlen, trim, var_export; /** @@ -54,6 +54,9 @@ final class Helpers } + /** + * Returns the type of a property or parameter as a string, or null if not determinable. + */ public static function getPropertyType(\ReflectionProperty|\ReflectionParameter $prop): ?string { if ($type = Nette\Utils\Type::fromReflection($prop)) { @@ -89,6 +92,9 @@ final class Helpers } + /** + * Formats a value for use in error messages (e.g., 'hello', true, object stdClass). + */ public static function formatValue(mixed $value): string { if ($value instanceof DynamicParameter) { @@ -105,6 +111,9 @@ final class Helpers } + /** + * Adds a TypeMismatch error to the context if the value does not match the expected type. + */ public static function validateType(mixed $value, string $expected, Context $context): void { if (!Nette\Utils\Validators::is($value, $expected)) { @@ -119,7 +128,10 @@ final class Helpers } - /** @param array{?float, ?float} $range */ + /** + * Adds a range error to the context if the value (or its length for strings/arrays) is outside the given range. + * @param array{?float, ?float} $range + */ public static function validateRange(mixed $value, array $range, Context $context, string $types = ''): void { if (is_array($value) || is_string($value)) { @@ -146,7 +158,10 @@ final class Helpers } - /** @param array{?float, ?float} $range */ + /** + * Checks whether a value falls within the given [min, max] range (null means no bound). + * @param array{?float, ?float} $range + */ public static function isInRange(mixed $value, array $range): bool { return ($range[0] === null || $value >= $range[0]) @@ -154,6 +169,9 @@ final class Helpers } + /** + * Adds a PatternMismatch error to the context if the value does not match the pattern. + */ public static function validatePattern(string $value, string $pattern, Context $context): void { if (!preg_match("\x01^(?:$pattern)$\x01Du", $value)) { @@ -166,7 +184,10 @@ final class Helpers } - /** @return \Closure(mixed): mixed */ + /** + * Returns a closure that casts a value to the given type (built-in, backed enum, class with constructor, or plain class). + * @return \Closure(mixed, Context): mixed + */ public static function getCastStrategy(string $type): \Closure { if (Nette\Utils\Validators::isBuiltinType($type)) { @@ -174,12 +195,37 @@ final class Helpers settype($value, $type); return $value; }; - } elseif (method_exists($type, '__construct')) { - return static fn($value) => is_array($value) || $value instanceof \stdClass - ? new $type(...(array) $value) - : new $type($value); - } else { - return static fn($value) => Nette\Utils\Arrays::toObject((array) $value, new $type); + + } elseif (is_subclass_of($type, \BackedEnum::class)) { + return static function ($value, Context $context) use ($type) { + try { + return $type::from($value); + } catch (\TypeError | \ValueError) { + $context->addError( + 'The %label% %path% expects to be %expected%, %value% given.', + Message::TypeMismatch, + ['value' => $value, 'expected' => implode('|', array_map(fn(\BackedEnum $case) => self::formatValue($case->value), $type::cases()))], + ); + return null; + } + }; + + } elseif (is_subclass_of($type, \UnitEnum::class)) { + throw new Nette\InvalidStateException("Cannot cast value to pure enum $type."); } + + $factory = method_exists($type, '__construct') + ? static fn($value) => is_array($value) || $value instanceof \stdClass + ? new $type(...(array) $value) + : new $type($value) + : static fn($value) => Nette\Utils\Arrays::toObject((array) $value, new $type); + + return static function ($value) use ($factory, $type) { + try { + return $factory($value); + } catch (\Error $e) { + throw new Nette\InvalidStateException("Unable to cast value to $type: " . $e->getMessage(), 0, $e); + } + }; } } diff --git a/vendor/nette/schema/src/Schema/Message.php b/vendor/nette/schema/src/Schema/Message.php index 4923f14..f06f172 100644 --- a/vendor/nette/schema/src/Schema/Message.php +++ b/vendor/nette/schema/src/Schema/Message.php @@ -7,9 +7,12 @@ namespace Nette\Schema; -use function implode, preg_replace_callback; +use function array_key_exists, implode, preg_replace_callback; +/** + * Represents a single validation error or warning with a message template, error code, path, and variables. + */ final class Message { /** variables: {value: mixed, expected: string} */ @@ -72,6 +75,9 @@ final class Message } + /** + * Formats the message template by substituting %variable% placeholders with their values. + */ public function toString(): string { $vars = $this->variables; @@ -83,7 +89,9 @@ final class Message return preg_replace_callback('~( ?)%(\w+)%~', function ($m) use ($vars) { [, $space, $key] = $m; - return $vars[$key] === null ? '' : $space . $vars[$key]; + return array_key_exists($key, $vars) + ? ($vars[$key] === null ? '' : $space . $vars[$key]) + : $m[0]; }, $this->message); } } diff --git a/vendor/nette/schema/src/Schema/Processor.php b/vendor/nette/schema/src/Schema/Processor.php index 4a2ef7b..3ce576a 100644 --- a/vendor/nette/schema/src/Schema/Processor.php +++ b/vendor/nette/schema/src/Schema/Processor.php @@ -21,6 +21,9 @@ final class Processor private bool $skipDefaults = false; + /** + * When enabled, properties with default values are omitted from the output. + */ public function skipDefaults(bool $value = true): void { $this->skipDefaults = $value; @@ -35,9 +38,9 @@ final class Processor { $this->createContext(); $data = $schema->normalize($data, $this->context); - $this->throwsErrors(); + $this->throwErrors(); $data = $schema->complete($data, $this->context); - $this->throwsErrors(); + $this->throwErrors(); return $data; } @@ -54,30 +57,35 @@ final class Processor $first = true; foreach ($dataset as $data) { $data = $schema->normalize($data, $this->context); - $this->throwsErrors(); + $this->throwErrors(); $flatten = $first ? $data : $schema->merge($data, $flatten); $first = false; } $data = $schema->complete($flatten, $this->context); - $this->throwsErrors(); + $this->throwErrors(); return $data; } - /** @return list */ + /** + * Returns all deprecation warnings collected during the last processing run. + * @return list + */ public function getWarnings(): array { $res = []; - foreach ($this->context->warnings as $message) { - $res[] = $message->toString(); + if (isset($this->context)) { + foreach ($this->context->warnings as $message) { + $res[] = $message->toString(); + } } return $res; } - private function throwsErrors(): void + private function throwErrors(): void { if ($this->context->errors) { throw new ValidationException(null, $this->context->errors); diff --git a/vendor/nette/schema/src/Schema/Schema.php b/vendor/nette/schema/src/Schema/Schema.php index 1616f64..619ebcf 100644 --- a/vendor/nette/schema/src/Schema/Schema.php +++ b/vendor/nette/schema/src/Schema/Schema.php @@ -8,27 +8,31 @@ namespace Nette\Schema; +/** + * Defines the contract for schema elements used in data validation and normalization. + */ interface Schema { /** - * Normalization. + * Applies pre-processing transformations to the raw input value (e.g., via before() hooks). * @return mixed */ function normalize(mixed $value, Context $context); /** - * Merging. + * Merges two normalized values, with $value taking priority over $base. * @return mixed */ function merge(mixed $value, mixed $base); /** - * Validation and finalization. + * Validates the value and applies defaults, transforms, and assertions. * @return mixed */ function complete(mixed $value, Context $context); /** + * Returns the default value, or adds a missing-item error if the field is required. * @return mixed */ function completeDefault(Context $context); diff --git a/vendor/nette/schema/src/Schema/ValidationException.php b/vendor/nette/schema/src/Schema/ValidationException.php index 396320f..cefca43 100644 --- a/vendor/nette/schema/src/Schema/ValidationException.php +++ b/vendor/nette/schema/src/Schema/ValidationException.php @@ -18,13 +18,16 @@ class ValidationException extends Nette\InvalidStateException public function __construct( ?string $message, /** @var list */ - private array $messages = [], + private readonly array $messages = [], ) { parent::__construct($message ?? $messages[0]->toString()); } - /** @return list */ + /** + * Returns all validation error messages as formatted strings. + * @return list + */ public function getMessages(): array { $res = []; @@ -36,7 +39,10 @@ class ValidationException extends Nette\InvalidStateException } - /** @return list */ + /** + * Returns all validation error messages as Message objects. + * @return list + */ public function getMessageObjects(): array { return $this->messages;