check($value); if ($error !== null) { $fail("The :attribute is not allowed: {$error}"); } } /** * Returns the reason the value is not allowed, or null when it is safe. */ public function check(mixed $value): ?string { if (blank($value)) { return null; } if (! is_string($value)) { return 'it must be a string.'; } $allowedSections = $this->parsersOnly ? self::PARSER_SECTIONS : self::CONFIG_SECTIONS; $section = null; foreach (preg_split('/\r\n|\r|\n/', $value) as $number => $rawLine) { $line = trim($rawLine); $lineNumber = $number + 1; if ($line === '' || str_starts_with($line, '#')) { continue; } if (str_starts_with($line, '@')) { $directive = strtolower(preg_split('/\s+/', $line)[0]); if ($this->parsersOnly || $directive !== '@set') { return "line {$lineNumber} uses the {$directive} directive."; } continue; } if (str_starts_with($line, '[')) { if (! preg_match('/^\[\s*([A-Za-z_]+)\s*\]$/', $line, $matches)) { return "line {$lineNumber} is not a valid section header."; } $section = strtoupper($matches[1]); if (! in_array($section, $allowedSections, true)) { return "line {$lineNumber} uses the [{$section}] section. Allowed sections: ".implode(', ', $allowedSections).'.'; } continue; } if ($section === null) { return "line {$lineNumber} is outside a section."; } $parts = preg_split('/\s+/', $line, 2); $key = strtolower($parts[0]); if (! preg_match('/^[a-z_][a-z0-9_.\-]*$/', $key)) { return "line {$lineNumber} has an invalid key."; } if (in_array($key, self::DENIED_KEYS, true)) { return "line {$lineNumber} uses the {$key} key."; } if ($key === 'name' && in_array($section, self::PLUGIN_SECTIONS, true)) { $plugin = strtolower(explode('://', trim($parts[1] ?? ''), 2)[0]); if (! preg_match('/^[a-z0-9_]+$/', $plugin)) { return "line {$lineNumber} must name a plugin literally."; } if (in_array($plugin, self::DENIED_PLUGINS, true)) { return "line {$lineNumber} uses the {$plugin} plugin."; } } } return null; } }