Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 80a2202d6e |
18 changed files with 305 additions and 122 deletions
|
|
@ -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_
|
||||
|
|
|
|||
55
readme.txt
55
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
27
update.json
27
update.json
|
|
@ -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": "<p><strong>Documentation Markdown</strong> is a powerful WordPress plugin that enables seamless synchronization of Markdown documentation files from GitHub repositories into your WordPress site.</p><h4>Key Features</h4><ul><li><strong>Automatic Synchronization:</strong> Schedule automatic syncs via WordPress Cron (hourly, twice daily, daily)</li><li><strong>Markdown to HTML:</strong> Convert GitHub Flavored Markdown to clean HTML using CommonMark</li><li><strong>Flexible Mapping:</strong> Map individual MD files to specific WordPress posts or pages</li><li><strong>Title from H1:</strong> Post titles are derived from the first Markdown H1 heading</li><li><strong>Link & Image Translation:</strong> Repo-relative links are rewritten to permalinks; images are sideloaded into the Media Library</li><li><strong>Secure:</strong> Encrypted GitHub token storage (HKDF-SHA256), full input validation & output escaping</li><li><strong>Translatable:</strong> Full internationalization support (i18n/l10n ready)</li><li><strong>Multi-Repository:</strong> Sync from multiple GitHub repos simultaneously</li><li><strong>Manual Sync:</strong> On-demand synchronization from admin interface</li><li><strong>Debug Tools:</strong> Built-in debugging tools (visible when WP_DEBUG is enabled)</li></ul><h4>Use Cases</h4><ul><li>API Documentation - Keep your API docs in sync between GitHub and WordPress</li><li>Technical Documentation - Maintain version-controlled technical docs</li><li>Knowledge Base - Build a knowledge base powered by GitHub</li><li>Blog Posts - Write blog posts in Markdown with Git workflow</li><li>Product Documentation - Sync product documentation from your repository</li></ul><h4>Requirements</h4><ul><li>PHP 8.0 or higher</li><li>WordPress 6.8 or higher</li><li>GitHub Personal Access Token (free)</li></ul>",
|
||||
"changelog": ""
|
||||
},
|
||||
"banners": {
|
||||
"low": "",
|
||||
"high": ""
|
||||
},
|
||||
"icons": {
|
||||
"1x": "",
|
||||
"2x": ""
|
||||
}
|
||||
}
|
||||
14
vendor/composer/installed.json
vendored
14
vendor/composer/installed.json
vendored
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
6
vendor/composer/installed.php
vendored
6
vendor/composer/installed.php
vendored
|
|
@ -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(),
|
||||
|
|
|
|||
2
vendor/nette/schema/readme.md
vendored
2
vendor/nette/schema/readme.md
vendored
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
8
vendor/nette/schema/src/Schema/Context.php
vendored
8
vendor/nette/schema/src/Schema/Context.php
vendored
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
27
vendor/nette/schema/src/Schema/Elements/Base.php
vendored
27
vendor/nette/schema/src/Schema/Elements/Base.php
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
vendor/nette/schema/src/Schema/Elements/Type.php
vendored
29
vendor/nette/schema/src/Schema/Elements/Type.php
vendored
|
|
@ -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<mixed> $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;
|
||||
|
|
|
|||
24
vendor/nette/schema/src/Schema/Expect.php
vendored
24
vendor/nette/schema/src/Schema/Expect.php
vendored
|
|
@ -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<string, Schema> $items */
|
||||
/**
|
||||
* Generates a structure schema from a class instance by reflecting its properties or constructor parameters.
|
||||
* @param array<string, Schema> $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);
|
||||
|
|
|
|||
66
vendor/nette/schema/src/Schema/Helpers.php
vendored
66
vendor/nette/schema/src/Schema/Helpers.php
vendored
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
vendor/nette/schema/src/Schema/Message.php
vendored
12
vendor/nette/schema/src/Schema/Message.php
vendored
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
vendor/nette/schema/src/Schema/Processor.php
vendored
20
vendor/nette/schema/src/Schema/Processor.php
vendored
|
|
@ -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<string> */
|
||||
/**
|
||||
* Returns all deprecation warnings collected during the last processing run.
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getWarnings(): array
|
||||
{
|
||||
$res = [];
|
||||
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);
|
||||
|
|
|
|||
10
vendor/nette/schema/src/Schema/Schema.php
vendored
10
vendor/nette/schema/src/Schema/Schema.php
vendored
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -18,13 +18,16 @@ class ValidationException extends Nette\InvalidStateException
|
|||
public function __construct(
|
||||
?string $message,
|
||||
/** @var list<Message> */
|
||||
private array $messages = [],
|
||||
private readonly array $messages = [],
|
||||
) {
|
||||
parent::__construct($message ?? $messages[0]->toString());
|
||||
}
|
||||
|
||||
|
||||
/** @return list<string> */
|
||||
/**
|
||||
* Returns all validation error messages as formatted strings.
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getMessages(): array
|
||||
{
|
||||
$res = [];
|
||||
|
|
@ -36,7 +39,10 @@ class ValidationException extends Nette\InvalidStateException
|
|||
}
|
||||
|
||||
|
||||
/** @return list<Message> */
|
||||
/**
|
||||
* Returns all validation error messages as Message objects.
|
||||
* @return list<Message>
|
||||
*/
|
||||
public function getMessageObjects(): array
|
||||
{
|
||||
return $this->messages;
|
||||
|
|
|
|||
Loading…
Reference in a new issue