diff --git a/Lib/DownloadHeaderPolicy.php b/Lib/DownloadHeaderPolicy.php new file mode 100644 index 0000000..8cf908b --- /dev/null +++ b/Lib/DownloadHeaderPolicy.php @@ -0,0 +1,23 @@ +moduleDir/bin/report2email.php '$settings->id' > /dev/null 2>&1".PHP_EOL; } } -} \ No newline at end of file +} diff --git a/Lib/RecordingArchiveBuilder.php b/Lib/RecordingArchiveBuilder.php new file mode 100644 index 0000000..7cba846 --- /dev/null +++ b/Lib/RecordingArchiveBuilder.php @@ -0,0 +1,143 @@ +policy = $policy; + $this->tempRoot = rtrim($tempRoot, DIRECTORY_SEPARATOR); + $this->maxRecords = $maxRecords; + $this->maxBytes = $maxBytes; + $this->maxCandidates = $maxCandidates; + } + + /** + * @param array $records + */ + public function build(array $records): RecordingArchiveResult + { + $this->ensureTempRoot(); + $archivePath = $this->tempRoot . DIRECTORY_SEPARATOR . bin2hex(random_bytes(16)) . '.tar'; + $accepted = 0; + $skipped = 0; + $acceptedBytes = 0; + $inspected = 0; + $usedNames = []; + + try { + $archive = new \PharData($archivePath); + foreach ($records as $record) { + $inspected++; + if ($inspected > $this->maxCandidates) { + throw new \RuntimeException('archive_too_large'); + } + if (!isset($record['path'], $record['name']) + || !is_string($record['path']) || !is_string($record['name'])) { + $skipped++; + continue; + } + + $allowed = $this->policy->validate($record['path']); + if (!$allowed->isAllowed() || $allowed->path() === null) { + $skipped++; + continue; + } + + $fileSize = filesize($allowed->path()); + if ($fileSize === false) { + $skipped++; + continue; + } + if ($accepted >= $this->maxRecords || $acceptedBytes + $fileSize > $this->maxBytes) { + throw new \RuntimeException('archive_too_large'); + } + $freeBytes = disk_free_space($this->tempRoot); + if ($freeBytes !== false && $fileSize + self::MIN_FREE_BYTES > $freeBytes) { + throw new \RuntimeException('archive_too_large'); + } + + $extension = strtolower((string) pathinfo((string) $allowed->downloadName(), PATHINFO_EXTENSION)); + $entryName = $this->uniqueEntryName($record['name'], $extension, $usedNames); + $archive->addFile($allowed->path(), $entryName); + $accepted++; + $acceptedBytes += $fileSize; + } + + unset($archive); + if ($accepted === 0) { + @unlink($archivePath); + throw new \RuntimeException('archive_has_no_valid_entries'); + } + + return new RecordingArchiveResult($archivePath, $accepted, $skipped); + } catch (\RuntimeException $exception) { + @unlink($archivePath); + if (in_array($exception->getMessage(), ['archive_has_no_valid_entries', 'archive_too_large'], true)) { + throw $exception; + } + throw new \RuntimeException('archive_build_failed', 0, $exception); + } catch (\Throwable $exception) { + @unlink($archivePath); + throw new \RuntimeException('archive_build_failed', 0, $exception); + } + } + + private function ensureTempRoot(): void + { + if (!is_dir($this->tempRoot) && !mkdir($this->tempRoot, 0700, true) && !is_dir($this->tempRoot)) { + throw new \RuntimeException('archive_build_failed'); + } + @chmod($this->tempRoot, 0700); + } + + /** + * @param array $usedNames + */ + private function uniqueEntryName(string $displayName, string $extension, array &$usedNames): string + { + $stem = preg_replace('/[^\pL\pN._-]+/u', '-', $displayName); + if (!is_string($stem)) { + $stem = ''; + } + $stem = trim($stem, ".-_ \t\n\r\0\x0B"); + if ($stem === '') { + $stem = 'recording'; + } + // POSIX ustar entry names are limited to 100 bytes. Leave room for + // duplicate suffixes and the validated extension. + $stem = function_exists('mb_strcut') ? mb_strcut($stem, 0, 80, 'UTF-8') : substr($stem, 0, 80); + + $candidate = $stem . '.' . $extension; + $suffix = 2; + while (isset($usedNames[$candidate])) { + $candidate = $stem . '-' . $suffix . '.' . $extension; + $suffix++; + } + $usedNames[$candidate] = true; + + return $candidate; + } +} diff --git a/Lib/RecordingArchiveResult.php b/Lib/RecordingArchiveResult.php new file mode 100644 index 0000000..4c97eab --- /dev/null +++ b/Lib/RecordingArchiveResult.php @@ -0,0 +1,34 @@ +path = $path; + $this->acceptedCount = $acceptedCount; + $this->skippedCount = $skippedCount; + } + + public function path(): string + { + return $this->path; + } + + public function acceptedCount(): int + { + return $this->acceptedCount; + } + + public function skippedCount(): int + { + return $this->skippedCount; + } +} diff --git a/Lib/RecordingPathPolicy.php b/Lib/RecordingPathPolicy.php new file mode 100644 index 0000000..5ba608c --- /dev/null +++ b/Lib/RecordingPathPolicy.php @@ -0,0 +1,63 @@ + */ + private array $mimeTypes; + + /** + * @param array $mimeTypes + */ + public function __construct(string $recordingRoot, array $mimeTypes = []) + { + $realRoot = realpath($recordingRoot); + if ($realRoot === false || !is_dir($realRoot)) { + throw new \InvalidArgumentException('Recording root does not exist'); + } + + $this->recordingRoot = rtrim($realRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + $this->mimeTypes = $mimeTypes ?: [ + 'mp3' => 'audio/mpeg', + 'wav' => 'audio/wav', + 'webm' => 'audio/webm', + ]; + } + + public function validate(string $candidate): RecordingPathResult + { + if ($candidate === '' || strpos($candidate, "\0") !== false + || preg_match('~^[a-z][a-z0-9+.-]*://~i', $candidate) === 1) { + return RecordingPathResult::rejected('invalid_recording_path', 404); + } + + $realCandidate = realpath($candidate); + if ($realCandidate === false || !is_file($realCandidate)) { + return RecordingPathResult::rejected('missing_recording', 404); + } + + if (strpos($realCandidate . DIRECTORY_SEPARATOR, $this->recordingRoot) !== 0) { + return RecordingPathResult::rejected('outside_recording_root', 404); + } + + $extension = strtolower((string) pathinfo($realCandidate, PATHINFO_EXTENSION)); + if (!array_key_exists($extension, $this->mimeTypes)) { + return RecordingPathResult::rejected('unsupported_media_type', 415); + } + + if (!is_readable($realCandidate)) { + return RecordingPathResult::rejected('missing_recording', 404); + } + + return RecordingPathResult::allowed( + $realCandidate, + $this->mimeTypes[$extension], + basename($realCandidate) + ); + } +} diff --git a/Lib/RecordingPathResult.php b/Lib/RecordingPathResult.php new file mode 100644 index 0000000..58cbb19 --- /dev/null +++ b/Lib/RecordingPathResult.php @@ -0,0 +1,71 @@ +allowed = $allowed; + $this->reason = $reason; + $this->status = $status; + $this->path = $path; + $this->mimeType = $mimeType; + $this->downloadName = $downloadName; + } + + public static function allowed(string $path, string $mimeType, string $downloadName): self + { + return new self(true, 'allowed', 200, $path, $mimeType, $downloadName); + } + + public static function rejected(string $reason, int $status): self + { + return new self(false, $reason, $status, null, null, null); + } + + public function isAllowed(): bool + { + return $this->allowed; + } + + public function reason(): string + { + return $this->reason; + } + + public function status(): int + { + return $this->status; + } + + public function path(): ?string + { + return $this->path; + } + + public function mimeType(): ?string + { + return $this->mimeType; + } + + public function downloadName(): ?string + { + return $this->downloadName; + } +} diff --git a/Lib/ReportSearchPolicy.php b/Lib/ReportSearchPolicy.php new file mode 100644 index 0000000..18a23bc --- /dev/null +++ b/Lib/ReportSearchPolicy.php @@ -0,0 +1,23 @@ +request->get('CallRecordID'); - $filename = (string)$this->request->get('view'); + $id = (string) $this->request->get('CallRecordID'); + $candidate = ''; + if ($id !== '') { + $resolved = ConnectorDB::invoke('getRecordingPathByID', [$id]); + $candidate = is_array($resolved) ? (string) ($resolved[0] ?? '') : ''; + } else { + $candidate = (string) $this->request->get('view'); + } - if(!file_exists($filename) && !empty($id)){ - [$filename] = ConnectorDB::invoke('getRecordingPathByID', [$id]); + try { + $policy = new RecordingPathPolicy(Directories::getDir(Directories::AST_MONITOR_DIR)); + $result = $policy->validate($candidate); + } catch (\Throwable $exception) { + Util::sysLogMsg('ModuleExtendedCDRs', 'event=recording_rejected reason=policy_unavailable endpoint=records'); + $this->sendError(500); + return; } - if(!file_exists($filename)){ + if (!$result->isAllowed() || $result->path() === null) { + Util::sysLogMsg( + 'ModuleExtendedCDRs', + 'event=recording_rejected reason=' . $result->reason() . ' endpoint=records' + ); + $this->sendError($result->status()); + return; + } + + $fp = fopen($result->path(), 'rb'); + if ($fp === false) { $this->sendError(404); return; } - $size = filesize($filename); - $fp = fopen($filename, 'rb'); - if ($fp) { - // Detect and validate file extension (allowed: wav, webm, mp3) to set correct headers. - $ext = strtolower((string)pathinfo($filename, PATHINFO_EXTENSION)); - $allowedMimeTypes = [ - 'mp3' => 'audio/mpeg', - 'wav' => 'audio/wav', - 'webm' => 'audio/webm', - ]; - if (!array_key_exists($ext, $allowedMimeTypes)) { - fclose($fp); - $this->sendError(415); - return; - } - $this->response->setHeader('Content-Description', $ext . ' file'); - $this->response->setHeader('Content-Disposition', 'attachment; filename=' . basename($filename)); - $this->response->setHeader('Content-type', $allowedMimeTypes[$ext]); + try { + $size = filesize($result->path()); + $this->response->setHeader('Content-Disposition', DownloadHeaderPolicy::attachment((string) $result->downloadName())); + $this->response->setHeader('Content-Type', (string) $result->mimeType()); $this->response->setHeader('Content-Transfer-Encoding', 'binary'); - $this->response->setContentLength($size); + $this->response->setHeader('X-Content-Type-Options', 'nosniff'); + if ($size !== false) { + $this->response->setContentLength($size); + } $this->response->sendHeaders(); fpassthru($fp); - } else { - $this->sendError(404); + } finally { + fclose($fp); } } /** - * curl 'http://127.0.0.1/pbxcore/api/modules/ModuleExtendedCDRs/exportHistory?reportNameID=CdrQueue&type=json&search=%7B%22dateRangeSelector%22%3A%2209%2F11%2F2025%20-%2008%2F12%2F2025%22%2C%22minBilSec%22%3A%226%22%2C%22minBilSecComp%22%3A%22%3E%3D%22%2C%22globalSearch%22%3A%22%22%2C%22typeCall%22%3A%22outgoing-calls%22%2C%22additionalFilter%22%3A%22%22%7D' - * curl 'http://127.0.0.1/pbxcore/api/modules/ModuleExtendedCDRs/exportHistory?reportNameID=CdrQueue&type=json&search=%7B%22dateRangeSelector%22%3A%2201%2F12%2F2025%20-%2002%2F12%2F2025%22%2C%22minBilSec%22%3A%226%22%2C%22minBilSecComp%22%3A%22%3E%3D%22%2C%22globalSearch%22%3A%22%22%2C%22typeCall%22%3A%22outgoing-calls%22%2C%22additionalFilter%22%3A%22%22%7D' - * {"dateRangeSelector":"01/11/2025 - 02/12/2025","minBilSec":"6","minBilSecComp":">=","globalSearch":"","typeCall":"outgoing-calls","additionalFilter":""} + * Export aggregated queue history for an authenticated request. * @return void */ public function exportHistoryQueue(): void @@ -99,8 +108,8 @@ public function exportHistoryQueue(): void ini_set('pcre.backtrack_limit', '10000000'); $type = $this->request->get('type'); $searchPhrase = $this->request->get('search'); - if(!is_string($searchPhrase)){ - $this->response->sendRaw(); + if (!is_string($searchPhrase) || !ReportSearchPolicy::isValid($searchPhrase)) { + $this->sendError(400); return; } $gr = new GetReport(); @@ -180,8 +189,7 @@ public static function aggregateCdrData(array $records): array } /** - * curl 'http://127.0.0.1/pbxcore/api/modules/ModuleExtendedCDRs/exportHistory?reportNameID=OutgoingEmployeeCalls&type=json&search=%7B%22dateRangeSelector%22%3A%2221%2F10%2F2024%20-%2021%2F10%2F2024%22%2C%22minBilSec%22%3A%220%22%2C%22globalSearch%22%3A%22%22%2C%22typeCall%22%3A%22outgoing-calls%22%2C%22additionalFilter%22%3A%22%22%7D' - * curl -H 'Cookie: PHPSESSID=5ada41f50486a5792cb3520f0922b7e9' 'https://boffart.miko.ru/pbxcore/api/modules/ModuleExtendedCDRs/exportHistory?type=json&search=%7B%22dateRangeSelector%22%3A%2201%2F10%2F2024+-+31%2F10%2F2024%22%2C%22globalSearch%22%3A%22%22%2C%22typeCall%22%3A%22all-calls%22%2C%22additionalFilter%22%3A%22%22%7D' + * Export call history for an authenticated request. * @return void */ public function exportHistory() @@ -199,8 +207,8 @@ public function exportHistory() } $type = $this->request->get('type'); $searchPhrase = $this->request->get('search'); - if(!is_string($searchPhrase)){ - $this->response->sendRaw(); + if (!is_string($searchPhrase) || !ReportSearchPolicy::isValid($searchPhrase)) { + $this->sendError(400); return; } $gr = new GetReport(); @@ -218,39 +226,100 @@ public function exportHistory() /** * Скачивание tar архива. - * https://boffart.miko.ru/pbxcore/api/modules/ModuleExtendedCDRs/downloads?search=%7B%22dateRangeSelector%22%3A%2212%2F09%2F2024%2B-%2B11%2F10%2F2024%22%2C%22globalSearch%22%3A%22%22%2C%22typeCall%22%3A%22%22%2C%22additionalFilter%22%3A%22%22%7D + * Returns a tar archive containing validated call recordings. * @return void */ public function downloads():void { $searchPhrase = $this->request->get('search'); + if (!is_string($searchPhrase) || !ReportSearchPolicy::isValid($searchPhrase)) { + $this->sendError(400); + return; + } $gr = new GetReport(); $view = $gr->history($searchPhrase); - $pathLN = Util::which('ln'); - $tmpDir = '/storage/usbdisk1/mikopbx/tmp/ExportCdr/flist-export-'.microtime(true); - shell_exec("mkdir -p $tmpDir"); + $records = []; + $candidateLimitReached = false; foreach ($view->data as $baseItem) { - foreach ($baseItem['4'] as $item){ - if(!file_exists($item['recordingfile'])){ + foreach (($baseItem['4'] ?? []) as $item) { + if (!is_array($item) || !isset($item['recordingfile'])) { continue; } - shell_exec("$pathLN -s {$item['recordingfile']} $tmpDir/{$item['prettyFilename']}.mp3"); + if (count($records) >= self::MAX_ARCHIVE_CANDIDATES) { + $candidateLimitReached = true; + break 2; + } + $records[] = [ + 'path' => (string) $item['recordingfile'], + 'name' => (string) ($item['prettyFilename'] ?? 'recording'), + ]; + } + } + + if ($candidateLimitReached) { + Util::sysLogMsg( + 'ModuleExtendedCDRs', + 'event=archive_rejected reason=archive_too_large endpoint=downloads' + ); + $this->sendError(413); + return; + } + + $archivePath = null; + try { + $di = $this->getDI(); + $config = $di->getShared('config'); + $tempRoot = $config->path('core.tempDir') . '/ModuleExtendedCDRs/archives'; + $policy = new RecordingPathPolicy(Directories::getDir(Directories::AST_MONITOR_DIR)); + $archive = (new RecordingArchiveBuilder($policy, $tempRoot))->build($records); + $archivePath = $archive->path(); + + Util::sysLogMsg( + 'ModuleExtendedCDRs', + 'event=archive_built accepted=' . $archive->acceptedCount() . ' skipped=' . $archive->skippedCount() + ); + + $fp = fopen($archivePath, 'rb'); + if ($fp === false) { + throw new \RuntimeException('archive_build_failed'); + } + try { + $size = filesize($archivePath); + $this->response->setHeader('Content-Type', 'application/x-tar'); + $this->response->setHeader('Content-Disposition', DownloadHeaderPolicy::attachment('download-' . time() . '.tar')); + $this->response->setHeader('Content-Transfer-Encoding', 'binary'); + $this->response->setHeader('X-Content-Type-Options', 'nosniff'); + if ($size !== false) { + $this->response->setContentLength($size); + } + $this->response->sendHeaders(); + fpassthru($fp); + } finally { + fclose($fp); + } + } catch (\RuntimeException $exception) { + if ($exception->getMessage() === 'archive_has_no_valid_entries') { + $reason = 'archive_has_no_valid_entries'; + $status = 404; + } elseif ($exception->getMessage() === 'archive_too_large') { + $reason = 'archive_too_large'; + $status = 413; + } else { + $reason = 'archive_build_failed'; + $status = 500; + } + Util::sysLogMsg('ModuleExtendedCDRs', 'event=archive_rejected reason=' . $reason . ' endpoint=downloads'); + $this->sendError($status); + } finally { + if (is_string($archivePath) && is_file($archivePath)) { + unlink($archivePath); } } - $this->response->setHeader('Content-Description', 'tar file'); - $this->response->setHeader('Content-type', 'application/x-tar'); - $this->response->setHeader('Content-Disposition', "attachment; filename=download-".time().".tar"); - $this->response->setHeader('Content-Transfer-Encoding', 'binary'); - $pathBusybox = Util::which('busybox'); - $this->response->sendRaw(); - passthru("cd $tmpDir; $pathBusybox tar -chf - . 2> /tmp/ar.err" ); - shell_exec($pathBusybox.' rm -rf '.$tmpDir); } /** - * curl -H 'Cookie: PHPSESSID=5ada41f50486a5792cb3520f0922b7e9' 'https://boffart.miko.ru/pbxcore/api/modules/ModuleExtendedCDRs/exportOutgoingEmployeeCalls?type=json&search=%7B%22dateRangeSelector%22%3A%2201%2F10%2F2024+-+31%2F10%2F2024%22%2C%22globalSearch%22%3A%22%22%2C%22typeCall%22%3A%22all-calls%22%2C%22additionalFilter%22%3A%22%22%7D' - * curl 'http://127.0.0.1/pbxcore/api/modules/ModuleExtendedCDRs/exportOutgoingEmployeeCalls?type=json&search=%7B%22dateRangeSelector%22%3A%2201%2F10%2F2024%20-%2031%2F10%2F2024%22%2C%22globalSearch%22%3A%22%22%2C%22typeCall%22%3A%22outgoing-calls%22%2C%22additionalFilter%22%3A%22204%20203%22%7D' + * Export employee call totals for an authenticated request. * @return void */ public function exportOutgoingEmployeeCalls() @@ -259,8 +328,8 @@ public function exportOutgoingEmployeeCalls() ini_set('pcre.backtrack_limit', '10000000'); $type = $this->request->get('type'); $searchPhrase = $this->request->get('search'); - if(!is_string($searchPhrase)){ - $this->response->sendRaw(); + if (!is_string($searchPhrase) || !ReportSearchPolicy::isValid($searchPhrase)) { + $this->sendError(400); return; } $gr = new GetReport(); @@ -297,4 +366,4 @@ private function echoResponse($result):void echo 'Error json encode: '. print_r($result, true); } } -} \ No newline at end of file +} diff --git a/composer.json b/composer.json index 39283b9..204f1cc 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,8 @@ "mk-j/php_xlsxwriter": "^0.39", "monolog/monolog": "2.9.1", "mpdf/mpdf": "8.0.4", - "phpoffice/phpspreadsheet": "^1.29" + "phpoffice/phpspreadsheet": "^1.30.5", + "setasign/fpdi": "^2.6.7" } , "autoload": { diff --git a/composer.lock b/composer.lock index 4aa225a..756a562 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e0736e8f87b5af821fe648f4629df747", + "content-hash": "f8316d193bf6ddbb89cdfdbde49c1ee0", "packages": [ { "name": "cesargb/php-log-rotation", @@ -55,22 +55,98 @@ }, "time": "2022-11-17T19:52:02+00:00" }, + { + "name": "composer/pcre", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-06-07T11:47:49+00:00" + }, { "name": "ezyang/htmlpurifier", - "version": "v4.17.0", + "version": "v4.19.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c" + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/bbc513d79acf6691fa9cf10f192c90dd2957f18c", - "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", "shasum": "" }, "require": { - "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" }, "require-dev": { "cerdic/css-tidy": "^1.7 || ^2.0", @@ -112,9 +188,9 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.17.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" }, - "time": "2023-11-17T15:01:25+00:00" + "time": "2025-10-17T16:34:55+00:00" }, { "name": "james-heinrich/getid3", @@ -652,16 +728,16 @@ }, { "name": "myclabs/php-enum", - "version": "1.8.4", + "version": "1.8.5", "source": { "type": "git", "url": "https://github.com/myclabs/php-enum.git", - "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483" + "reference": "e7be26966b7398204a234f8673fdad5ac6277802" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/php-enum/zipball/a867478eae49c9f59ece437ae7f9506bfaa27483", - "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802", "shasum": "" }, "require": { @@ -671,7 +747,7 @@ "require-dev": { "phpunit/phpunit": "^9.5", "squizlabs/php_codesniffer": "1.*", - "vimeo/psalm": "^4.6.2" + "vimeo/psalm": "^4.6.2 || ^5.2" }, "type": "library", "autoload": { @@ -693,13 +769,13 @@ } ], "description": "PHP Enum implementation", - "homepage": "http://github.com/myclabs/php-enum", + "homepage": "https://github.com/myclabs/php-enum", "keywords": [ "enum" ], "support": { "issues": "https://github.com/myclabs/php-enum/issues", - "source": "https://github.com/myclabs/php-enum/tree/1.8.4" + "source": "https://github.com/myclabs/php-enum/tree/1.8.5" }, "funding": [ { @@ -711,7 +787,7 @@ "type": "tidelift" } ], - "time": "2022-08-04T09:53:51+00:00" + "time": "2025-01-14T11:49:03+00:00" }, { "name": "paragonie/random_compat", @@ -765,19 +841,20 @@ }, { "name": "phpoffice/phpspreadsheet", - "version": "1.29.2", + "version": "1.30.6", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "3a5a818d7d3e4b5bd2e56fb9de44dbded6eae07f" + "reference": "a416375ffc8bf5b661c1bb4e6c60d8f3fddbe5ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/3a5a818d7d3e4b5bd2e56fb9de44dbded6eae07f", - "reference": "3a5a818d7d3e4b5bd2e56fb9de44dbded6eae07f", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/a416375ffc8bf5b661c1bb4e6c60d8f3fddbe5ce", + "reference": "a416375ffc8bf5b661c1bb4e6c60d8f3fddbe5ce", "shasum": "" }, "require": { + "composer/pcre": "^1||^2||^3", "ext-ctype": "*", "ext-dom": "*", "ext-fileinfo": "*", @@ -795,14 +872,13 @@ "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", - "php": "^7.4 || ^8.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", + "php": ">=7.4.0 <8.5.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-main", - "dompdf/dompdf": "^1.0 || ^2.0", + "doctrine/instantiator": "^1.5", + "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", "friendsofphp/php-cs-fixer": "^3.2", "mitoteam/jpgraph": "^10.3", "mpdf/mpdf": "^8.1.1", @@ -848,6 +924,9 @@ }, { "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" } ], "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", @@ -864,116 +943,9 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.2" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.6" }, - "time": "2024-09-29T07:04:47+00:00" - }, - { - "name": "psr/http-client", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-client.git", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", - "shasum": "" - }, - "require": { - "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP clients", - "homepage": "https://github.com/php-fig/http-client", - "keywords": [ - "http", - "http-client", - "psr", - "psr-18" - ], - "support": { - "source": "https://github.com/php-fig/http-client" - }, - "time": "2023-09-23T14:17:50+00:00" - }, - { - "name": "psr/http-factory", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-factory" - }, - "time": "2024-04-15T12:06:14+00:00" + "time": "2026-07-12T19:59:31+00:00" }, { "name": "psr/http-message", @@ -1131,31 +1103,31 @@ }, { "name": "setasign/fpdi", - "version": "v2.6.1", + "version": "v2.6.8", "source": { "type": "git", "url": "https://github.com/Setasign/FPDI.git", - "reference": "09a816004fcee9ed3405bd164147e3fdbb79a56f" + "reference": "881945be29a4996ad3d008eb18ddc01fa3df890c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Setasign/FPDI/zipball/09a816004fcee9ed3405bd164147e3fdbb79a56f", - "reference": "09a816004fcee9ed3405bd164147e3fdbb79a56f", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/881945be29a4996ad3d008eb18ddc01fa3df890c", + "reference": "881945be29a4996ad3d008eb18ddc01fa3df890c", "shasum": "" }, "require": { "ext-zlib": "*", - "php": "^5.6 || ^7.0 || ^8.0" + "php": ">=7.2 <=8.5.99999" }, "conflict": { "setasign/tfpdf": "<1.31" }, "require-dev": { - "phpunit/phpunit": "~5.7", - "setasign/fpdf": "~1.8.6", + "phpunit/phpunit": "^8.5.52", + "setasign/fpdf": "^1.9.0", "setasign/tfpdf": "~1.33", "squizlabs/php_codesniffer": "^3.5", - "tecnickcom/tcpdf": "~6.2" + "tecnickcom/tcpdf": "^6.8" }, "suggest": { "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." @@ -1191,7 +1163,7 @@ ], "support": { "issues": "https://github.com/Setasign/FPDI/issues", - "source": "https://github.com/Setasign/FPDI/tree/v2.6.1" + "source": "https://github.com/Setasign/FPDI/tree/v2.6.8" }, "funding": [ { @@ -1199,23 +1171,24 @@ "type": "tidelift" } ], - "time": "2024-09-02T10:17:15+00:00" + "time": "2026-06-11T10:37:24+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -1227,8 +1200,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1263,7 +1236,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -1274,24 +1247,28 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-27T06:59:30+00:00" } ], "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], + "platform": {}, + "platform-dev": {}, "platform-overrides": { "php": "7.4.6" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/docs/superpowers/plans/2026-07-22-isolated-production-hardening-implementation.md b/docs/superpowers/plans/2026-07-22-isolated-production-hardening-implementation.md new file mode 100644 index 0000000..20dad06 --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-isolated-production-hardening-implementation.md @@ -0,0 +1,399 @@ +# Isolated Production Hardening Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Produce a client-ready ModuleExtendedCDRs release that closes arbitrary recording access, archive command injection, unauthenticated private REST access, and known dependency vulnerabilities without changing CDR synchronization behavior. + +**Architecture:** Move path validation and archive construction out of `ApiController` into small pure-PHP services. Use MikoPBX's canonical monitor directory, `realpath()` containment, and `PharData` tar creation; require authentication on every private route and update the PHP-7.4-compatible dependency lock and bundled vendor tree. + +**Tech Stack:** PHP 7.4.6, MikoPBX module REST API, `MikoPBX\Core\System\Directories`, `PharData`, Composer 2, existing standalone PHP test harness, SSH test server `serber@boffart.miko.ru`. + +## Global Constraints + +- Preserve the transactional CDR batch, quarantine, offset, and trunk-resolution contracts already on `develop`. +- Support platform PHP exactly as resolved by Composer's `config.platform.php` value `7.4.6`. +- Require `phpoffice/phpspreadsheet >=1.30.5,<2.0` and `setasign/fpdi >=2.6.7`. +- Do not interpolate request, database, filename, or path values into a shell command. +- Do not log credentials, cookies, authorization headers, complete queries, recording paths, phone numbers, or report contents. +- Keep legacy `view` compatibility only behind the same recording-root validation as `CallRecordID`. +- Do not push or deploy to a client without explicit user authorization. + +--- + +## File Map + +- Create `Lib/RecordingPathPolicy.php`: canonical path containment, media validation, MIME lookup, and safe download filename. +- Create `Lib/RecordingPathResult.php`: immutable validation result with reason code, HTTP status, resolved path, MIME type, and filename. +- Create `Lib/RecordingArchiveBuilder.php`: validate inputs, assign safe unique entry names, build a tar with `PharData`, and clean partial output. +- Create `Lib/RecordingArchiveResult.php`: archive path plus accepted/skipped counts. +- Modify `Lib/RestAPI/Controllers/ApiController.php`: delegate recording and archive operations, stream controlled responses, and remove shell use. +- Modify `Lib/ExtendedCDRsConf.php`: require authentication for every private module route. +- Modify `composer.json`, `composer.lock`, and `vendor/`: resolve patched dependencies as one reproducible set. +- Create `tests/RecordingPathPolicyTest.php`, `tests/RecordingArchiveBuilderTest.php`, `tests/PrivateRoutesTest.php`, and `tests/DependencyPolicyTest.php`. +- Modify comments in `Lib/RestAPI/Controllers/ApiController.php`: remove concrete sessions, hosts, and filesystem examples. + +--- + +### Task 1: Canonical Recording Path Policy + +**Files:** +- Create: `Lib/RecordingPathResult.php` +- Create: `Lib/RecordingPathPolicy.php` +- Create: `tests/RecordingPathPolicyTest.php` + +**Interfaces:** +- Produces: `RecordingPathPolicy::__construct(string $recordingRoot, array $mimeTypes = [])` +- Produces: `RecordingPathPolicy::validate(string $candidate): RecordingPathResult` +- Produces: `RecordingPathResult::{isAllowed(),reason(),status(),path(),mimeType(),downloadName()}` + +- [ ] **Step 1: Write the failing path-policy test** + +Create a temporary monitor root and sibling directory. Cover valid MP3/WAV/WEBM files, missing file, directory, sibling-prefix path, `../`, an escaping symlink, `phar://`, a NUL byte, and `.txt`. Assertions must include `outside_recording_root`/404 and `unsupported_media_type`/415 without expecting the rejected path in any result field. + +```php +$policy = new RecordingPathPolicy($monitorRoot); +$ok = $policy->validate($monitorRoot . '/2026/07/call.mp3'); +assertSame(true, $ok->isAllowed(), 'valid recording'); +assertSame('audio/mpeg', $ok->mimeType(), 'MP3 MIME'); + +$escape = $policy->validate($monitorRoot . '-old/secret.mp3'); +assertSame(false, $escape->isAllowed(), 'sibling prefix rejected'); +assertSame('outside_recording_root', $escape->reason(), 'safe reason'); +assertSame(null, $escape->path(), 'rejected path is not retained'); +``` + +- [ ] **Step 2: Run the test and verify RED** + +Run: `php tests/RecordingPathPolicyTest.php` + +Expected: failure because `RecordingPathPolicy.php` does not exist. + +- [ ] **Step 3: Implement the immutable result and policy** + +Use `realpath()` for both root and candidate. Reject `\0` and any case-insensitive `^[a-z][a-z0-9+.-]*://` before filesystem calls. Accept only when `is_file($realCandidate)` and: + +```php +$allowedBase = rtrim($realRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; +$insideRoot = strpos($realCandidate . DIRECTORY_SEPARATOR, $allowedBase) === 0; +``` + +Map extensions exactly to `mp3 => audio/mpeg`, `wav => audio/wav`, and `webm => audio/webm`. Return only a sanitized basename from allowed results; rejected results carry no path or candidate value. + +- [ ] **Step 4: Run policy and syntax tests** + +Run: `php tests/RecordingPathPolicyTest.php && php -l Lib/RecordingPathPolicy.php && php -l Lib/RecordingPathResult.php` + +Expected: `RecordingPathPolicyTest: OK` and both files report no syntax errors. + +- [ ] **Step 5: Commit the path boundary** + +```bash +git add Lib/RecordingPathPolicy.php Lib/RecordingPathResult.php tests/RecordingPathPolicyTest.php +git commit -m "security: restrict recording paths to monitor root" +``` + +--- + +### Task 2: Shell-Free Recording Archive Builder + +**Files:** +- Create: `Lib/RecordingArchiveResult.php` +- Create: `Lib/RecordingArchiveBuilder.php` +- Create: `tests/RecordingArchiveBuilderTest.php` + +**Interfaces:** +- Consumes: `RecordingPathPolicy::validate(string): RecordingPathResult` +- Produces: `RecordingArchiveBuilder::__construct(RecordingPathPolicy $policy, string $tempRoot)` +- Produces: `RecordingArchiveBuilder::build(array $records): RecordingArchiveResult` +- Input record: `array{path:string,name:string}` +- Produces: `RecordingArchiveResult::{path(),acceptedCount(),skippedCount()}` + +- [ ] **Step 1: Write the failing archive tests** + +Tests must provide names containing `;`, `$()`, backticks, slashes, Unicode, control characters, and duplicates. Open the resulting tar with `PharData` and assert that entries stay flat, source contents match, duplicate names become `call.mp3`, `call-2.mp3`, and no marker command executes. Also test mixed valid/invalid records, zero valid records, and deletion of a partial tar after an injected exception. + +```php +$result = $builder->build([ + ['path' => $validOne, 'name' => 'call;touch PWNED'], + ['path' => $validTwo, 'name' => 'call;touch PWNED'], +]); +assertSame(2, $result->acceptedCount(), 'two files archived'); +assertSame(false, file_exists($tempRoot . '/PWNED'), 'no shell execution'); +``` + +- [ ] **Step 2: Run the test and verify RED** + +Run: `php tests/RecordingArchiveBuilderTest.php` + +Expected: failure because the builder does not exist. + +- [ ] **Step 3: Implement deterministic names and tar construction** + +Create the temp root with mode `0700`. Generate the tar filename with `bin2hex(random_bytes(16)) . '.tar'`. Normalize entry stems to Unicode letters/numbers plus `._-`, replace other runs with `-`, trim punctuation, fall back to `recording`, cap the stem at 120 bytes without splitting UTF-8, preserve only the validated extension, and add `-2`, `-3`, etc. Use only: + +```php +$archive = new \PharData($archivePath); +$archive->addFile($allowed->path(), $entryName); +``` + +On any exception, unlink the partial archive and rethrow a domain `RuntimeException('archive_build_failed', 0, $previous)`. When no file is accepted, throw `RuntimeException('archive_has_no_valid_entries')`. + +- [ ] **Step 4: Run archive, path, and syntax tests** + +Run: `php tests/RecordingArchiveBuilderTest.php && php tests/RecordingPathPolicyTest.php && php -l Lib/RecordingArchiveBuilder.php && php -l Lib/RecordingArchiveResult.php` + +Expected: both tests report `OK`; syntax checks pass. + +- [ ] **Step 5: Commit archive isolation** + +```bash +git add Lib/RecordingArchiveBuilder.php Lib/RecordingArchiveResult.php tests/RecordingArchiveBuilderTest.php +git commit -m "security: build recording archives without shell commands" +``` + +--- + +### Task 3: Harden Individual Recording Download + +**Files:** +- Modify: `Lib/RestAPI/Controllers/ApiController.php:38-87` +- Create: `tests/RecordingResponsePolicyTest.php` + +**Interfaces:** +- Consumes: `Directories::getDir(Directories::AST_MONITOR_DIR)` +- Consumes: `RecordingPathPolicy::validate()` +- Keeps: `CallRecordID` and deprecated `view` query parameters + +- [ ] **Step 1: Write failing source-level and header-policy tests** + +Assert that the controller imports `Directories` and `RecordingPathPolicy`, obtains `AST_MONITOR_DIR`, never calls `file_exists()` on unvalidated request input, and formats a download header through a pure helper that strips CR/LF/quotes and supplies `filename*` encoding for Unicode. + +- [ ] **Step 2: Run the test and verify RED** + +Run: `php tests/RecordingResponsePolicyTest.php` + +Expected: failure because the controller still opens the raw `view` value. + +- [ ] **Step 3: Delegate lookup and validation** + +Resolve `CallRecordID` first; use `view` only when the ID is empty. Construct the policy from `Directories::getDir(Directories::AST_MONITOR_DIR)`. For rejected results, log only `event=recording_rejected reason= endpoint=records`, return the result status, and do not include the candidate. Open and size only `$result->path()`. + +Set `X-Content-Type-Options: nosniff`, the validated MIME type, and a safe attachment header. Always close the file handle in `finally`. + +- [ ] **Step 4: Run focused checks** + +Run: `php tests/RecordingResponsePolicyTest.php && php tests/RecordingPathPolicyTest.php && php -l Lib/RestAPI/Controllers/ApiController.php` + +Expected: all pass. + +- [ ] **Step 5: Commit endpoint hardening** + +```bash +git add Lib/RestAPI/Controllers/ApiController.php tests/RecordingResponsePolicyTest.php +git commit -m "security: validate recording downloads before streaming" +``` + +--- + +### Task 4: Replace Archive Endpoint Shell Pipeline + +**Files:** +- Modify: `Lib/RestAPI/Controllers/ApiController.php:218-249` +- Modify: `tests/RecordingResponsePolicyTest.php` + +**Interfaces:** +- Consumes: `RecordingArchiveBuilder::build(array): RecordingArchiveResult` +- Produces HTTP tar response and deletes the generated tar in `finally` + +- [ ] **Step 1: Extend the failing controller test** + +Assert that `downloads()` contains no `shell_exec`, `passthru`, `mkdir`, `ln`, `tar`, `rm -rf`, or interpolated command. Assert it converts report rows to `{path,name}`, delegates to the builder, sets `application/x-tar`, streams a server-generated filename, and unlinks the result in `finally`. + +- [ ] **Step 2: Run the test and verify RED** + +Run: `php tests/RecordingResponsePolicyTest.php` + +Expected: failure on the current `shell_exec()`/`passthru()` implementation. + +- [ ] **Step 3: Implement the delegated archive response** + +Use the configured `core.tempDir . '/ModuleExtendedCDRs/archives'` and canonical monitor root. Skip malformed report rows before calling the builder. Map `archive_has_no_valid_entries` to a controlled 404 and other pre-header build failures to 500. Log endpoint, safe reason, accepted count, and skipped count only. Stream with `fopen`/`fpassthru`, close the handle, and unlink the archive in `finally`. + +- [ ] **Step 4: Run endpoint and archive tests** + +Run: `php tests/RecordingResponsePolicyTest.php && php tests/RecordingArchiveBuilderTest.php && php -l Lib/RestAPI/Controllers/ApiController.php && rg -n 'shell_exec|passthru|system\(|exec\(' Lib/RestAPI/Controllers/ApiController.php` + +Expected: tests pass and `rg` returns no matches. + +- [ ] **Step 5: Commit archive endpoint hardening** + +```bash +git add Lib/RestAPI/Controllers/ApiController.php tests/RecordingResponsePolicyTest.php +git commit -m "security: isolate recording archive downloads" +``` + +--- + +### Task 5: Require Authentication and Remove Sensitive Examples + +**Files:** +- Modify: `Lib/ExtendedCDRsConf.php:113-123` +- Modify: `Lib/RestAPI/Controllers/ApiController.php:20-275` +- Create: `tests/PrivateRoutesTest.php` + +**Interfaces:** +- Consumes confirmed core contract: route element index `5` is `$noAuth`; `true` bypasses JWT/session authentication. +- Produces: all five private routes with index `5 === false`. + +- [ ] **Step 1: Write the failing route test** + +Parse the returned route source or instantiate with minimal stubs and assert the `downloads`, `exportHistory`, `exportHistoryDetail`, `recordsAction`, and `exportOutgoingEmployeeCalls` rows all end in `false`. Also scan production PHP for `PHPSESSID=`, `boffart.miko.ru`, `/storage/usbdisk`, bearer/JWT examples, and credential-like query strings. + +- [ ] **Step 2: Run the test and verify RED** + +Run: `php tests/PrivateRoutesTest.php` + +Expected: failure because `exportHistory` currently ends in `true` and comments contain concrete session/server values. + +- [ ] **Step 3: Close unauthenticated access** + +Change only `exportHistory` from `true` to `false`; keep the other four authenticated flags false. Replace concrete curl comments with credential-free localhost examples or remove them. Add a short comment documenting that index 5 means `noAuth`, so `false` is intentional. + +- [ ] **Step 4: Run route and secret scans** + +Run: `php tests/PrivateRoutesTest.php && rg -n 'PHPSESSID=|boffart\.miko\.ru|Authorization: Bearer|/storage/usbdisk' App Lib bin --glob '*.php'` + +Expected: test passes and scan has no production-code matches. + +- [ ] **Step 5: Commit authorization policy** + +```bash +git add Lib/ExtendedCDRsConf.php Lib/RestAPI/Controllers/ApiController.php tests/PrivateRoutesTest.php +git commit -m "security: require authentication for private CDR routes" +``` + +--- + +### Task 6: Upgrade Vulnerable Export Dependencies + +**Files:** +- Modify: `composer.json` +- Modify: `composer.lock` +- Modify: `vendor/` +- Create: `tests/DependencyPolicyTest.php` + +**Interfaces:** +- Requires: `phpoffice/phpspreadsheet:^1.30.5` +- Requires transitive/direct floor: `setasign/fpdi:^2.6.7` +- Preserves: `config.platform.php=7.4.6`, `mpdf/mpdf=8.0.4` unless Composer proves incompatibility. + +- [ ] **Step 1: Write the failing lock-policy test** + +Read `composer.lock`, locate both packages, normalize a leading `v`, and assert `version_compare()` against `1.30.5` and `2.6.7`. Assert `composer.json` still pins platform PHP to `7.4.6`. + +- [ ] **Step 2: Run the test and verify RED** + +Run: `php tests/DependencyPolicyTest.php` + +Expected: failure showing locked PhpSpreadsheet `1.29.2` and FPDI `2.6.1`. + +- [ ] **Step 3: Resolve one compatible dependency set** + +Install Composer 2 in a temporary tool path if the CLI is absent. Change the direct PhpSpreadsheet constraint to `^1.30.5` and add `setasign/fpdi:^2.6.7` as an explicit security floor. Run: + +```bash +composer update phpoffice/phpspreadsheet setasign/fpdi --with-all-dependencies +composer validate --strict +composer audit --locked +``` + +Expected: resolution succeeds for PHP 7.4.6, validation succeeds, and audit reports no known vulnerabilities. Do not use `--ignore-platform-reqs` and do not edit `vendor` manually. + +- [ ] **Step 4: Verify dependency runtime and artifact consistency** + +Run `php tests/DependencyPolicyTest.php`, instantiate `Spreadsheet`, write a minimal XLSX to `/tmp`, instantiate `Mpdf`, write a one-line PDF to `/tmp`, then run Composer's installed-package check. Confirm bundled source versions match the lock and cleanup-vendor did not remove runtime files. + +Expected: XLSX and PDF files are non-empty; policy test passes. + +- [ ] **Step 5: Commit the reproducible dependency update** + +```bash +git add composer.json composer.lock vendor tests/DependencyPolicyTest.php +git commit -m "security: update spreadsheet and PDF dependencies" +``` + +--- + +### Task 7: Full Local Regression and Security Review + +**Files:** +- Modify only files required by findings from this task. + +**Interfaces:** +- Consumes all prior tasks. +- Produces a clean local release candidate and review evidence. + +- [ ] **Step 1: Run every standalone test** + +Run each `tests/*.php` with PHP in sorted order. Expected: every script exits 0 and prints its `OK` marker. + +- [ ] **Step 2: Lint every changed PHP file and check whitespace** + +Run PHP lint over changed `.php` files, `git diff --check`, and `git status --short`. Expected: no lint/whitespace failure and only intentional files plus pre-existing `.DS_Store` remain. + +- [ ] **Step 3: Scan the release diff** + +Search for `shell_exec`, `passthru`, unsafe `exec/system`, `phar://` outside tests, cookies/tokens, server hostnames, absolute customer paths, and SQL/error dumps. Inspect every match and remove unsafe production occurrences without weakening negative tests. + +- [ ] **Step 4: Request independent code review** + +Review against the design release gate: path containment, symlink behavior, archive cleanup, authentication flag semantics, dependency audit, sensitive logging, PHP 7.4 syntax, and unchanged CDR synchronization. Resolve every critical/high finding with a failing regression test first. + +- [ ] **Step 5: Commit review corrections** + +```bash +git add -u Lib tests composer.json composer.lock vendor +git commit -m "fix: address hardening review findings" +``` + +Skip the commit only when the review produced no code change. + +--- + +### Task 8: Packaged Upgrade and Adversarial Test-Server Verification + +**Files:** +- Modify: release/build metadata only if required by the module's existing packaging workflow. +- Create locally or under `/tmp`: release artifact and verification evidence; do not commit server backups or test recordings. + +**Interfaces:** +- Consumes the complete local release candidate. +- Produces a verified installable artifact and go/no-go report. + +- [ ] **Step 1: Capture and back up server state** + +Over SSH, record module version/hash, current CDR offset, source maximum ID, module row/distinct counts, worker PID/count, asset symlink targets, and relevant log tail. Create a timestamped recoverable backup of module code and its two SQLite databases before installation. + +- [ ] **Step 2: Build and inspect the actual module artifact** + +Use the repository's existing CI/package workflow. Inspect the archive to prove it contains the new services, patched `composer.lock`, matching vendor versions, public assets, and no `.DS_Store`, test fixtures, credentials, or server-specific files. + +- [ ] **Step 3: Install the artifact through the normal module lifecycle** + +Install/update on `serber@boffart.miko.ru`, not by overlaying individual files. Verify enable/start succeeds, exactly one ConnectorDB worker runs, and asset symlinks are created automatically. On failure, restore the timestamped backup and stop the release. + +- [ ] **Step 4: Execute functional and adversarial smoke tests** + +Verify module page/CSS/JS are HTTP 200 with correct MIME types; CDR offset catches up and survives restart; existing synthetic CDRs remain exactly-once; authorized MP3/WAV/WEBM downloads work where fixtures exist; unauthenticated requests fail; outside-root, traversal, symlink, `phar://`, unsupported extension, CRLF filename, and hostile archive-name attempts fail without path leakage or command execution. + +- [ ] **Step 5: Verify every report format and diagnostics** + +Exercise history, queue, and employee reports in JSON, XLSX, and PDF where the UI exposes them. Confirm non-empty valid output, no fatal/OOM, no new dependency warnings, and security logs contain safe reason codes and aggregate counts only. + +- [ ] **Step 6: Verify persistence and cleanup** + +Restart the module/PBX service used by the normal lifecycle, recheck the page, worker count, CDR offset, and last structured batch log. Confirm generated archives, temporary recordings, fault fixtures, and synthetic additions created for this hardening test are removed. + +- [ ] **Step 7: Produce the release decision** + +Report exact commit, artifact checksum, dependency versions/audit result, local test count, server scenarios, before/after offset and row counts, backup location, and remaining findings. Mark client-ready only when no unresolved critical/high issue remains and packaged upgrade plus assets passed. Do not push or deploy to the client until explicitly authorized. diff --git a/docs/superpowers/specs/2026-07-22-isolated-production-hardening-design.md b/docs/superpowers/specs/2026-07-22-isolated-production-hardening-design.md new file mode 100644 index 0000000..0222f76 --- /dev/null +++ b/docs/superpowers/specs/2026-07-22-isolated-production-hardening-design.md @@ -0,0 +1,147 @@ +# Isolated Production Hardening Design + +**Date:** 2026-07-22 + +**Status:** Approved for implementation planning + +## Goal + +Prepare one client-ready hardening release of ModuleExtendedCDRs that preserves existing CDR synchronization and report behavior while closing the identified file-access, command-injection, authorization, and dependency vulnerabilities. + +## Scope + +The release covers four connected security boundaries: + +1. downloading an individual call recording; +2. creating and downloading an archive of call recordings; +3. authorization of module REST endpoints; +4. vulnerable Composer dependencies used by XLSX and PDF export. + +It also removes live session identifiers and test-server addresses from source comments and adds enough security diagnostics to investigate rejected requests without logging credentials or sensitive query contents. + +The release does not redesign CDR synchronization, change the transactional batch contract, change trunk resolution, introduce a new public API, or broadly refactor report generation. + +## Architecture + +Security-sensitive filesystem operations are removed from `ApiController` and placed behind two small services: + +- `RecordingPathPolicy` resolves and validates recording paths. +- `RecordingArchiveBuilder` assigns safe archive names, stages links or copies in a private temporary directory, and streams a tar archive without interpolating request or database values into a shell command. + +`ApiController` remains responsible for request parsing and responses. It may only open a recording or archive returned by these services. This keeps path policy independently testable and prevents future endpoints from reimplementing partial checks. + +All externally reachable CDR and recording routes use the standard authenticated module-route mode. Compatibility for trusted on-box consumers is retained only if the MikoPBX route contract proves that loopback calls need a separate mode; that exception must verify the actual peer address and may not trust forwarding headers. There is no unauthenticated remote fallback. + +## Recording Path Policy + +The preferred request input for a single recording is `CallRecordID`. The module resolves it through `ConnectorDB::getRecordingPathByID`. The legacy `view` parameter remains temporarily for compatibility but receives exactly the same validation and must be marked deprecated in code documentation. + +A path is accepted only when all conditions hold: + +- it contains no NUL byte and no PHP stream-wrapper scheme; +- `realpath()` succeeds; +- the resolved path is a regular file, not a directory; +- the resolved path is below an explicitly configured recording root supplied by MikoPBX configuration; +- resolving any symlink cannot escape that root; +- the extension is one of `mp3`, `wav`, or `webm`; +- the file can be opened for binary reading. + +Prefix comparisons operate on normalized paths ending with a directory separator, so `/monitor-old` cannot match `/monitor`. Failure returns a generic 404 for missing or out-of-root paths and 415 for an unsupported media type. Responses must not disclose server paths. + +`Content-Disposition` uses a quoted, sanitized basename plus RFC 5987 encoding where needed. Response headers must not contain raw path data. + +## Archive Construction + +Archive creation must not pass request, CDR, recording path, or display-name values through `shell_exec()`, `exec()`, `system()`, or a shell command string. + +The builder creates a mode-0700 working directory under the configured module temporary root. Each source recording is validated by `RecordingPathPolicy`. Archive entry names are derived from display names using a strict allowlist, receive the validated media extension, and are made unique with deterministic numeric suffixes. Invalid records are skipped and counted; their paths are not logged. + +The preferred implementation uses PHP archive facilities if a tar stream can be produced reliably on PHP 7.4.6. If target-platform verification shows that `PharData` cannot safely stream the expected archive sizes, the fallback may invoke the known BusyBox binary through an argument-array process API with a fixed working directory and fixed arguments. A shell string remains forbidden. + +Temporary data is cleaned in a `finally` path after success, client disconnect, or exception. Existing cron cleanup remains only as crash recovery. The response filename contains only a timestamp generated by the server. + +## REST Authorization + +The following routes expose private call metadata or recordings and must require the platform's authenticated route mode: + +- `downloads`; +- `exportHistory`; +- `exportHistoryDetail`; +- `recordsAction`; +- `exportOutgoingEmployeeCalls`. + +Before changing route flags, implementation must confirm their semantics against the installed MikoPBX core version and an existing authenticated core/module route. Automated or integration tests must demonstrate that a remote unauthenticated request is rejected and an authenticated administrator request succeeds. + +If an internal integration consumes `exportHistoryDetail`, it must use an authenticated internal request or a separately registered loopback-only route. The externally named route may not silently bypass authentication. + +## Dependency Hardening + +Composer resolution remains pinned to platform PHP `7.4.6`. + +Required version floors: + +- `phpoffice/phpspreadsheet` 1.30.5 or newer within the PHP-7.4-compatible 1.x line; +- `setasign/fpdi` 2.6.7 or newer within the version range accepted by the retained mPDF package. + +The current direct constraint `phpoffice/phpspreadsheet: ^1.29` already permits the patched 1.x release, but `composer.lock` and bundled `vendor` must be regenerated from one resolution. FPDI may remain transitive if the solver selects at least 2.6.7. No dependency is patched manually inside `vendor`. + +The update is accepted only if Composer reports no known vulnerabilities for the locked packages and installation succeeds under the declared platform and target extensions. XLSX and PDF exports receive smoke tests because dependency compatibility is part of this release. + +## Diagnostics and Data Handling + +Security rejection logs contain an event name, reason code, endpoint, and request correlation identifier when available. They must not contain session cookies, authorization headers, complete query strings, recording paths, phone numbers, or report results. + +Approved reason codes include `missing_recording`, `outside_recording_root`, `unsupported_media_type`, `invalid_archive_entry`, `unauthenticated`, and `archive_build_failed`. Repeated user-controlled values are not embedded into the message. + +Concrete `PHPSESSID` examples, production/test hostnames, and absolute recording examples are removed from source comments. Generic localhost examples may remain without credentials or customer data. + +## Error Handling + +- A rejected single-recording request returns a controlled 404 or 415 and never emits a PHP warning or path. +- An archive containing some invalid entries succeeds with the valid entries and logs aggregate skipped counts. +- An archive with no valid entries returns a controlled client error rather than an empty tar stream. +- Archive initialization or streaming failures return a controlled server error when headers are not sent; after streaming begins, the connection is closed and cleanup still runs. +- Dependency or export failures must be visible in module logs without including report contents. + +## Testing Strategy + +### Unit tests + +`RecordingPathPolicy` tests cover a valid file, a missing file, a sibling-prefix path, traversal, a symlink escaping the root, a directory, NUL input, `phar://`, unsupported extension, and unreadable file behavior where supported. + +`RecordingArchiveBuilder` tests cover safe names, shell metacharacters, Unicode, duplicate names, mixed valid and invalid inputs, no valid inputs, deterministic suffixes, cleanup after success, and cleanup after an exception. + +Route-policy tests assert that every private route is registered with the confirmed authenticated mode. Response-header tests cover hostile and Unicode basenames. + +### Existing regression tests + +All atomic batch, logging, quarantine, source-status, trunk-resolution, and artifact-source tests must remain green. Changed PHP files must pass syntax checks and `git diff --check`. + +### Test-server verification + +Deploy a built module artifact to `serber@boffart.miko.ru` using a recoverable backup. Verify: + +1. update/install lifecycle and asset-link creation; +2. module page and static assets; +3. CDR catch-up, restart, and offset persistence; +4. one authorized recording download for every supported media type available; +5. rejection of an external path, wrapper path, traversal attempt, and unauthenticated request; +6. archive download with duplicate and hostile display names; +7. history, queue, and employee reports in JSON, XLSX, and PDF where supported; +8. clean dependency audit and absence of new fatal errors or sensitive diagnostics. + +The server is restored from backup if lifecycle or compatibility validation fails. Synthetic test CDRs and temporary recordings are removed after verification. + +## Release Gate + +The hardening version is client-ready only when: + +- every critical and high finding in this design is closed or proven unreachable with documented evidence; +- the locked dependency audit is clean for the packages changed by this release; +- unit, regression, integration, and test-server smoke tests pass; +- an actual packaged upgrade, not only a file overlay, has been tested; +- module UI assets survive the packaged upgrade; +- no credentials, customer identifiers, or server-specific values appear in the diff or release artifact; +- an independent code review reports no unresolved critical or high issue. + +Commits may be created during implementation, but no implementation push or client deployment is performed without explicit user authorization. diff --git a/tests/DependencyPolicyTest.php b/tests/DependencyPolicyTest.php new file mode 100644 index 0000000..b50d9cb --- /dev/null +++ b/tests/DependencyPolicyTest.php @@ -0,0 +1,37 @@ +='), + 'PhpSpreadsheet must be at least 1.30.5; locked ' . $versions['phpoffice/phpspreadsheet'] +); +assertDependencyPolicy(isset($versions['setasign/fpdi']), 'FPDI must be locked'); +assertDependencyPolicy( + version_compare($versions['setasign/fpdi'], '2.6.7', '>='), + 'FPDI must be at least 2.6.7; locked ' . $versions['setasign/fpdi'] +); + +echo "DependencyPolicyTest: OK\n"; diff --git a/tests/PrivateRoutesTest.php b/tests/PrivateRoutesTest.php new file mode 100644 index 0000000..f9aaf46 --- /dev/null +++ b/tests/PrivateRoutesTest.php @@ -0,0 +1,28 @@ +build([ + ['path' => $monitor . '/one.mp3', 'name' => 'call;touch ' . $marker], + ['path' => $monitor . '/two.mp3', 'name' => 'call;touch ' . $marker], + ['path' => $monitor . '/voice.wav', 'name' => '../Голос клиента'], + ['path' => $base . '/outside.mp3', 'name' => 'outside'], + ['path' => $monitor . '/missing.mp3', 'name' => 'missing'], + ]); + + assertArchiveSame(3, $result->acceptedCount(), 'valid files accepted'); + assertArchiveSame(2, $result->skippedCount(), 'invalid files skipped'); + assertArchiveSame(true, is_file($result->path()), 'tar created'); + assertArchiveSame(false, file_exists($marker), 'shell metacharacters not executed'); + + $archive = new PharData($result->path()); + $names = []; + foreach (new RecursiveIteratorIterator($archive) as $file) { + $names[] = $file->getFilename(); + } + sort($names); + assertArchiveSame(3, count($names), 'three flat entries'); + foreach ($names as $name) { + assertArchiveSame(false, strpos($name, '/') !== false, 'entry has no directory'); + assertArchiveSame(false, strpos($name, ';') !== false, 'entry has no shell punctuation'); + } + assertArchiveSame(true, count(array_filter($names, static function (string $name): bool { + return substr($name, -6) === '-2.mp3'; + })) === 1, 'duplicate receives deterministic suffix'); + + unlink($result->path()); + + try { + (new RecordingArchiveBuilder($policy, $temp, 1, 1024))->build([ + ['path' => $monitor . '/one.mp3', 'name' => 'one'], + ['path' => $monitor . '/two.mp3', 'name' => 'two'], + ]); + throw new RuntimeException('record quota did not fail'); + } catch (RuntimeException $exception) { + assertArchiveSame('archive_too_large', $exception->getMessage(), 'record quota reason'); + } + + try { + (new RecordingArchiveBuilder($policy, $temp, 10, 5))->build([ + ['path' => $monitor . '/one.mp3', 'name' => 'one'], + ['path' => $monitor . '/voice.wav', 'name' => 'voice'], + ]); + throw new RuntimeException('byte quota did not fail'); + } catch (RuntimeException $exception) { + assertArchiveSame('archive_too_large', $exception->getMessage(), 'byte quota reason'); + } + + try { + (new RecordingArchiveBuilder($policy, $temp, 10, 1024, 1))->build([ + ['path' => $monitor . '/missing-one.mp3', 'name' => 'missing one'], + ['path' => $monitor . '/missing-two.mp3', 'name' => 'missing two'], + ]); + throw new RuntimeException('candidate quota did not fail'); + } catch (RuntimeException $exception) { + assertArchiveSame('archive_too_large', $exception->getMessage(), 'candidate quota reason'); + } + + try { + $builder->build([ + ['path' => $monitor . '/missing.mp3', 'name' => 'missing'], + ]); + throw new RuntimeException('empty archive did not fail'); + } catch (RuntimeException $exception) { + assertArchiveSame('archive_has_no_valid_entries', $exception->getMessage(), 'empty archive reason'); + } + + assertArchiveSame([], glob($temp . '/*.tar') ?: [], 'no partial archives left'); +} finally { + removeArchiveTree($base); +} + +echo "RecordingArchiveBuilderTest: OK\n"; diff --git a/tests/RecordingPathPolicyTest.php b/tests/RecordingPathPolicyTest.php new file mode 100644 index 0000000..5f7ff94 --- /dev/null +++ b/tests/RecordingPathPolicyTest.php @@ -0,0 +1,90 @@ +validate($monitor . '/2026/07/call.mp3'); + assertPathPolicySame(true, $valid->isAllowed(), 'valid recording allowed'); + assertPathPolicySame('audio/mpeg', $valid->mimeType(), 'MP3 MIME'); + assertPathPolicySame('call.mp3', $valid->downloadName(), 'download basename'); + assertPathPolicySame(realpath($monitor . '/2026/07/call.mp3'), $valid->path(), 'canonical path'); + + assertPathPolicySame('audio/wav', $policy->validate($monitor . '/call.wav')->mimeType(), 'WAV MIME'); + assertPathPolicySame('audio/webm', $policy->validate($monitor . '/call.webm')->mimeType(), 'WEBM MIME'); + + $cases = [ + [$monitor . '/missing.mp3', 'missing_recording', 404], + [$monitor, 'missing_recording', 404], + [$sibling . '/secret.mp3', 'outside_recording_root', 404], + [$monitor . '/../monitor-old/secret.mp3', 'outside_recording_root', 404], + [$monitor . '/escape.mp3', 'outside_recording_root', 404], + ['phar://' . $monitor . '/2026/07/call.mp3', 'invalid_recording_path', 404], + [$monitor . "/bad\0.mp3", 'invalid_recording_path', 404], + [$monitor . '/notes.txt', 'unsupported_media_type', 415], + ]; + + foreach ($cases as $case) { + [$candidate, $reason, $status] = $case; + $result = $policy->validate($candidate); + assertPathPolicySame(false, $result->isAllowed(), 'candidate rejected: ' . $reason); + assertPathPolicySame($reason, $result->reason(), 'safe rejection reason'); + assertPathPolicySame($status, $result->status(), 'rejection status'); + assertPathPolicySame(null, $result->path(), 'rejected path not retained'); + assertPathPolicySame(null, $result->downloadName(), 'rejected basename not retained'); + } +} finally { + removePathPolicyTree($base); +} + +echo "RecordingPathPolicyTest: OK\n"; diff --git a/tests/RecordingResponsePolicyTest.php b/tests/RecordingResponsePolicyTest.php new file mode 100644 index 0000000..edff45c --- /dev/null +++ b/tests/RecordingResponsePolicyTest.php @@ -0,0 +1,48 @@ +}|array{}|null */ private static $installed; + /** + * @var bool + */ + private static $installedIsLocalDir; + /** * @var bool|null */ @@ -309,6 +320,24 @@ public static function reload($data) { self::$installed = $data; self::$installedByVendor = array(); + + // when using reload, we disable the duplicate protection to ensure that self::$installed data is + // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, + // so we have to assume it does not, and that may result in duplicate data being returned when listing + // all installed packages for example + self::$installedIsLocalDir = false; + } + + /** + * @return string + */ + private static function getSelfDir() + { + if (self::$selfDir === null) { + self::$selfDir = strtr(__DIR__, '\\', '/'); + } + + return self::$selfDir; } /** @@ -322,19 +351,27 @@ private static function getInstalled() } $installed = array(); + $copiedLocalDir = false; if (self::$canGetVendors) { + $selfDir = self::getSelfDir(); foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + $vendorDir = strtr($vendorDir, '\\', '/'); if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ $required = require $vendorDir.'/composer/installed.php'; - $installed[] = self::$installedByVendor[$vendorDir] = $required; - if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { - self::$installed = $installed[count($installed) - 1]; + self::$installedByVendor[$vendorDir] = $required; + $installed[] = $required; + if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { + self::$installed = $required; + self::$installedIsLocalDir = true; } } + if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { + $copiedLocalDir = true; + } } } @@ -350,7 +387,7 @@ private static function getInstalled() } } - if (self::$installed !== array()) { + if (self::$installed !== array() && !$copiedLocalDir) { $installed[] = self::$installed; } diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index 76c43ba..734955c 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -11,8 +11,7 @@ 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), 'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'), 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), - 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src', $vendorDir . '/psr/http-factory/src'), - 'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'), + 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'), 'PhpOffice\\PhpSpreadsheet\\' => array($vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet'), 'MyCLabs\\Enum\\' => array($vendorDir . '/myclabs/php-enum/src'), 'Mpdf\\' => array($vendorDir . '/mpdf/mpdf/src'), @@ -20,6 +19,7 @@ 'Modules\\ModuleExtendedCDRs\\' => array('/'), 'Matrix\\' => array($vendorDir . '/markbaker/matrix/classes/src'), 'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'), + 'Composer\\Pcre\\' => array($vendorDir . '/composer/pcre/src'), 'Complex\\' => array($vendorDir . '/markbaker/complex/classes/src'), 'Cesargb\\Log\\' => array($vendorDir . '/cesargb/php-log-rotation/src'), ); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 0f98650..1833514 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -13,27 +13,26 @@ class ComposerStaticInitdb0e7f5f4867f1094b5c4c69668744f9 ); public static $prefixLengthsPsr4 = array ( - 's' => + 's' => array ( 'setasign\\Fpdi\\' => 14, ), - 'Z' => + 'Z' => array ( 'ZipStream\\' => 10, ), - 'S' => + 'S' => array ( 'Symfony\\Polyfill\\Mbstring\\' => 26, ), - 'P' => + 'P' => array ( 'Psr\\SimpleCache\\' => 16, 'Psr\\Log\\' => 8, 'Psr\\Http\\Message\\' => 17, - 'Psr\\Http\\Client\\' => 16, 'PhpOffice\\PhpSpreadsheet\\' => 25, ), - 'M' => + 'M' => array ( 'MyCLabs\\Enum\\' => 13, 'Mpdf\\' => 5, @@ -41,89 +40,89 @@ class ComposerStaticInitdb0e7f5f4867f1094b5c4c69668744f9 'Modules\\ModuleExtendedCDRs\\' => 27, 'Matrix\\' => 7, ), - 'D' => + 'D' => array ( 'DeepCopy\\' => 9, ), - 'C' => + 'C' => array ( + 'Composer\\Pcre\\' => 14, 'Complex\\' => 8, 'Cesargb\\Log\\' => 12, ), ); public static $prefixDirsPsr4 = array ( - 'setasign\\Fpdi\\' => + 'setasign\\Fpdi\\' => array ( 0 => __DIR__ . '/..' . '/setasign/fpdi/src', ), - 'ZipStream\\' => + 'ZipStream\\' => array ( 0 => __DIR__ . '/..' . '/maennchen/zipstream-php/src', ), - 'Symfony\\Polyfill\\Mbstring\\' => + 'Symfony\\Polyfill\\Mbstring\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', ), - 'Psr\\SimpleCache\\' => + 'Psr\\SimpleCache\\' => array ( 0 => __DIR__ . '/..' . '/psr/simple-cache/src', ), - 'Psr\\Log\\' => + 'Psr\\Log\\' => array ( 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', ), - 'Psr\\Http\\Message\\' => + 'Psr\\Http\\Message\\' => array ( 0 => __DIR__ . '/..' . '/psr/http-message/src', - 1 => __DIR__ . '/..' . '/psr/http-factory/src', ), - 'Psr\\Http\\Client\\' => - array ( - 0 => __DIR__ . '/..' . '/psr/http-client/src', - ), - 'PhpOffice\\PhpSpreadsheet\\' => + 'PhpOffice\\PhpSpreadsheet\\' => array ( 0 => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet', ), - 'MyCLabs\\Enum\\' => + 'MyCLabs\\Enum\\' => array ( 0 => __DIR__ . '/..' . '/myclabs/php-enum/src', ), - 'Mpdf\\' => + 'Mpdf\\' => array ( 0 => __DIR__ . '/..' . '/mpdf/mpdf/src', ), - 'Monolog\\' => + 'Monolog\\' => array ( 0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog', ), - 'Modules\\ModuleExtendedCDRs\\' => + 'Modules\\ModuleExtendedCDRs\\' => array ( 0 => '/', ), - 'Matrix\\' => + 'Matrix\\' => array ( 0 => __DIR__ . '/..' . '/markbaker/matrix/classes/src', ), - 'DeepCopy\\' => + 'DeepCopy\\' => array ( 0 => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy', ), - 'Complex\\' => + 'Composer\\Pcre\\' => + array ( + 0 => __DIR__ . '/..' . '/composer/pcre/src', + ), + 'Complex\\' => array ( 0 => __DIR__ . '/..' . '/markbaker/complex/classes/src', ), - 'Cesargb\\Log\\' => + 'Cesargb\\Log\\' => array ( 0 => __DIR__ . '/..' . '/cesargb/php-log-rotation/src', ), ); public static $prefixesPsr0 = array ( - 'H' => + 'H' => array ( - 'HTMLPurifier' => + 'HTMLPurifier' => array ( 0 => __DIR__ . '/..' . '/ezyang/htmlpurifier/library', ), diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 7b92d91..44acded 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -52,23 +52,102 @@ }, "install-path": "../cesargb/php-log-rotation" }, + { + "name": "composer/pcre", + "version": "3.4.0", + "version_normalized": "3.4.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" + }, + "time": "2026-06-07T11:47:49+00:00", + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "install-path": "./pcre" + }, { "name": "ezyang/htmlpurifier", - "version": "v4.17.0", - "version_normalized": "4.17.0.0", + "version": "v4.19.0", + "version_normalized": "4.19.0.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c" + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/bbc513d79acf6691fa9cf10f192c90dd2957f18c", - "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", "shasum": "" }, "require": { - "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" }, "require-dev": { "cerdic/css-tidy": "^1.7 || ^2.0", @@ -80,7 +159,7 @@ "ext-iconv": "Converts text to and from non-UTF-8 encodings", "ext-tidy": "Used for pretty-printing HTML" }, - "time": "2023-11-17T15:01:25+00:00", + "time": "2025-10-17T16:34:55+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -112,7 +191,7 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.17.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" }, "install-path": "../ezyang/htmlpurifier" }, @@ -676,17 +755,17 @@ }, { "name": "myclabs/php-enum", - "version": "1.8.4", - "version_normalized": "1.8.4.0", + "version": "1.8.5", + "version_normalized": "1.8.5.0", "source": { "type": "git", "url": "https://github.com/myclabs/php-enum.git", - "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483" + "reference": "e7be26966b7398204a234f8673fdad5ac6277802" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/php-enum/zipball/a867478eae49c9f59ece437ae7f9506bfaa27483", - "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802", "shasum": "" }, "require": { @@ -696,9 +775,9 @@ "require-dev": { "phpunit/phpunit": "^9.5", "squizlabs/php_codesniffer": "1.*", - "vimeo/psalm": "^4.6.2" + "vimeo/psalm": "^4.6.2 || ^5.2" }, - "time": "2022-08-04T09:53:51+00:00", + "time": "2025-01-14T11:49:03+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -720,13 +799,13 @@ } ], "description": "PHP Enum implementation", - "homepage": "http://github.com/myclabs/php-enum", + "homepage": "https://github.com/myclabs/php-enum", "keywords": [ "enum" ], "support": { "issues": "https://github.com/myclabs/php-enum/issues", - "source": "https://github.com/myclabs/php-enum/tree/1.8.4" + "source": "https://github.com/myclabs/php-enum/tree/1.8.5" }, "funding": [ { @@ -795,20 +874,21 @@ }, { "name": "phpoffice/phpspreadsheet", - "version": "1.29.2", - "version_normalized": "1.29.2.0", + "version": "1.30.6", + "version_normalized": "1.30.6.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "3a5a818d7d3e4b5bd2e56fb9de44dbded6eae07f" + "reference": "a416375ffc8bf5b661c1bb4e6c60d8f3fddbe5ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/3a5a818d7d3e4b5bd2e56fb9de44dbded6eae07f", - "reference": "3a5a818d7d3e4b5bd2e56fb9de44dbded6eae07f", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/a416375ffc8bf5b661c1bb4e6c60d8f3fddbe5ce", + "reference": "a416375ffc8bf5b661c1bb4e6c60d8f3fddbe5ce", "shasum": "" }, "require": { + "composer/pcre": "^1||^2||^3", "ext-ctype": "*", "ext-dom": "*", "ext-fileinfo": "*", @@ -826,14 +906,13 @@ "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", - "php": "^7.4 || ^8.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", + "php": ">=7.4.0 <8.5.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-main", - "dompdf/dompdf": "^1.0 || ^2.0", + "doctrine/instantiator": "^1.5", + "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", "friendsofphp/php-cs-fixer": "^3.2", "mitoteam/jpgraph": "^10.3", "mpdf/mpdf": "^8.1.1", @@ -851,7 +930,7 @@ "mpdf/mpdf": "Option for rendering PDF with PDF Writer", "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" }, - "time": "2024-09-29T07:04:47+00:00", + "time": "2026-07-12T19:59:31+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -881,6 +960,9 @@ }, { "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" } ], "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", @@ -897,123 +979,10 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.2" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.6" }, "install-path": "../phpoffice/phpspreadsheet" }, - { - "name": "psr/http-client", - "version": "1.0.3", - "version_normalized": "1.0.3.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-client.git", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", - "shasum": "" - }, - "require": { - "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "time": "2023-09-23T14:17:50+00:00", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "installation-source": "dist", - "autoload": { - "psr-4": { - "Psr\\Http\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP clients", - "homepage": "https://github.com/php-fig/http-client", - "keywords": [ - "http", - "http-client", - "psr", - "psr-18" - ], - "support": { - "source": "https://github.com/php-fig/http-client" - }, - "install-path": "../psr/http-client" - }, - { - "name": "psr/http-factory", - "version": "1.1.0", - "version_normalized": "1.1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "psr/http-message": "^1.0 || ^2.0" - }, - "time": "2024-04-15T12:06:14+00:00", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "installation-source": "dist", - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-factory" - }, - "install-path": "../psr/http-factory" - }, { "name": "psr/http-message", "version": "1.1", @@ -1179,37 +1148,37 @@ }, { "name": "setasign/fpdi", - "version": "v2.6.1", - "version_normalized": "2.6.1.0", + "version": "v2.6.8", + "version_normalized": "2.6.8.0", "source": { "type": "git", "url": "https://github.com/Setasign/FPDI.git", - "reference": "09a816004fcee9ed3405bd164147e3fdbb79a56f" + "reference": "881945be29a4996ad3d008eb18ddc01fa3df890c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Setasign/FPDI/zipball/09a816004fcee9ed3405bd164147e3fdbb79a56f", - "reference": "09a816004fcee9ed3405bd164147e3fdbb79a56f", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/881945be29a4996ad3d008eb18ddc01fa3df890c", + "reference": "881945be29a4996ad3d008eb18ddc01fa3df890c", "shasum": "" }, "require": { "ext-zlib": "*", - "php": "^5.6 || ^7.0 || ^8.0" + "php": ">=7.2 <=8.5.99999" }, "conflict": { "setasign/tfpdf": "<1.31" }, "require-dev": { - "phpunit/phpunit": "~5.7", - "setasign/fpdf": "~1.8.6", + "phpunit/phpunit": "^8.5.52", + "setasign/fpdf": "^1.9.0", "setasign/tfpdf": "~1.33", "squizlabs/php_codesniffer": "^3.5", - "tecnickcom/tcpdf": "~6.2" + "tecnickcom/tcpdf": "^6.8" }, "suggest": { "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." }, - "time": "2024-09-02T10:17:15+00:00", + "time": "2026-06-11T10:37:24+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1242,7 +1211,7 @@ ], "support": { "issues": "https://github.com/Setasign/FPDI/issues", - "source": "https://github.com/Setasign/FPDI/tree/v2.6.1" + "source": "https://github.com/Setasign/FPDI/tree/v2.6.8" }, "funding": [ { @@ -1254,20 +1223,21 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", - "version_normalized": "1.31.0.0", + "version": "v1.38.2", + "version_normalized": "1.38.2.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -1276,12 +1246,12 @@ "suggest": { "ext-mbstring": "For best performance" }, - "time": "2024-09-09T11:45:10+00:00", + "time": "2026-05-27T06:59:30+00:00", "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "installation-source": "dist", @@ -1317,7 +1287,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -1328,6 +1298,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index f5e21f6..637c5fd 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'mikopbx/moduletemplate', 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => 'ad6dfd31d21beae304ec39f71db4af3029324e93', + 'reference' => '304a4c7610d92ce9d9d334b791529c520b041d75', 'type' => 'application', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -19,10 +19,19 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'composer/pcre' => array( + 'pretty_version' => '3.4.0', + 'version' => '3.4.0.0', + 'reference' => 'd5a341b3fb61f3001970940afb1d332968a183ed', + 'type' => 'library', + 'install_path' => __DIR__ . '/./pcre', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'ezyang/htmlpurifier' => array( - 'pretty_version' => 'v4.17.0', - 'version' => '4.17.0.0', - 'reference' => 'bbc513d79acf6691fa9cf10f192c90dd2957f18c', + 'pretty_version' => 'v4.19.0', + 'version' => '4.19.0.0', + 'reference' => 'b287d2a16aceffbf6e0295559b39662612b77fcf', 'type' => 'library', 'install_path' => __DIR__ . '/../ezyang/htmlpurifier', 'aliases' => array(), @@ -67,7 +76,7 @@ 'mikopbx/moduletemplate' => array( 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => 'ad6dfd31d21beae304ec39f71db4af3029324e93', + 'reference' => '304a4c7610d92ce9d9d334b791529c520b041d75', 'type' => 'application', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -110,9 +119,9 @@ 'dev_requirement' => false, ), 'myclabs/php-enum' => array( - 'pretty_version' => '1.8.4', - 'version' => '1.8.4.0', - 'reference' => 'a867478eae49c9f59ece437ae7f9506bfaa27483', + 'pretty_version' => '1.8.5', + 'version' => '1.8.5.0', + 'reference' => 'e7be26966b7398204a234f8673fdad5ac6277802', 'type' => 'library', 'install_path' => __DIR__ . '/../myclabs/php-enum', 'aliases' => array(), @@ -128,32 +137,14 @@ 'dev_requirement' => false, ), 'phpoffice/phpspreadsheet' => array( - 'pretty_version' => '1.29.2', - 'version' => '1.29.2.0', - 'reference' => '3a5a818d7d3e4b5bd2e56fb9de44dbded6eae07f', + 'pretty_version' => '1.30.6', + 'version' => '1.30.6.0', + 'reference' => 'a416375ffc8bf5b661c1bb4e6c60d8f3fddbe5ce', 'type' => 'library', 'install_path' => __DIR__ . '/../phpoffice/phpspreadsheet', 'aliases' => array(), 'dev_requirement' => false, ), - 'psr/http-client' => array( - 'pretty_version' => '1.0.3', - 'version' => '1.0.3.0', - 'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90', - 'type' => 'library', - 'install_path' => __DIR__ . '/../psr/http-client', - 'aliases' => array(), - 'dev_requirement' => false, - ), - 'psr/http-factory' => array( - 'pretty_version' => '1.1.0', - 'version' => '1.1.0.0', - 'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a', - 'type' => 'library', - 'install_path' => __DIR__ . '/../psr/http-factory', - 'aliases' => array(), - 'dev_requirement' => false, - ), 'psr/http-message' => array( 'pretty_version' => '1.1', 'version' => '1.1.0.0', @@ -188,18 +179,18 @@ 'dev_requirement' => false, ), 'setasign/fpdi' => array( - 'pretty_version' => 'v2.6.1', - 'version' => '2.6.1.0', - 'reference' => '09a816004fcee9ed3405bd164147e3fdbb79a56f', + 'pretty_version' => 'v2.6.8', + 'version' => '2.6.8.0', + 'reference' => '881945be29a4996ad3d008eb18ddc01fa3df890c', 'type' => 'library', 'install_path' => __DIR__ . '/../setasign/fpdi', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/polyfill-mbstring' => array( - 'pretty_version' => 'v1.31.0', - 'version' => '1.31.0.0', - 'reference' => '85181ba99b2345b0ef10ce42ecac37612d9fd341', + 'pretty_version' => 'v1.38.2', + 'version' => '1.38.2.0', + 'reference' => 'd3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', 'aliases' => array(), diff --git a/vendor/psr/http-factory/LICENSE b/vendor/composer/pcre/LICENSE similarity index 61% rename from vendor/psr/http-factory/LICENSE rename to vendor/composer/pcre/LICENSE index 3f1559b..c5a282f 100644 --- a/vendor/psr/http-factory/LICENSE +++ b/vendor/composer/pcre/LICENSE @@ -1,13 +1,11 @@ -MIT License +Copyright (C) 2021 Composer -Copyright (c) 2018 PHP-FIG - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. diff --git a/vendor/composer/pcre/README.md b/vendor/composer/pcre/README.md new file mode 100644 index 0000000..4906514 --- /dev/null +++ b/vendor/composer/pcre/README.md @@ -0,0 +1,189 @@ +composer/pcre +============= + +PCRE wrapping library that offers type-safe `preg_*` replacements. + +This library gives you a way to ensure `preg_*` functions do not fail silently, returning +unexpected `null`s that may not be handled. + +As of 3.0 this library enforces [`PREG_UNMATCHED_AS_NULL`](#preg_unmatched_as_null) usage +for all matching and replaceCallback functions, [read more below](#preg_unmatched_as_null) +to understand the implications. + +It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it +simplifies and reduces the possible return values from all the `preg_*` functions which +are quite packed with edge cases. As of v2.2.0 / v3.2.0 the library also comes with a +[PHPStan extension](#phpstan-extension) for parsing regular expressions and giving you even better output types. + +This library is a thin wrapper around `preg_*` functions with [some limitations](#restrictions--limitations). +If you are looking for a richer API to handle regular expressions have a look at +[rawr/t-regx](https://packagist.org/packages/rawr/t-regx) instead. + +[![Continuous Integration](https://github.com/composer/pcre/workflows/Continuous%20Integration/badge.svg?branch=main)](https://github.com/composer/pcre/actions) + + +Installation +------------ + +Install the latest version with: + +```bash +$ composer require composer/pcre +``` + + +Requirements +------------ + +* PHP 7.4.0 is required for 3.x versions +* PHP 7.2.0 is required for 2.x versions +* PHP 5.3.2 is required for 1.x versions + + +Basic usage +----------- + +Instead of: + +```php +if (preg_match('{fo+}', $string, $matches)) { ... } +if (preg_match('{fo+}', $string, $matches, PREG_OFFSET_CAPTURE)) { ... } +if (preg_match_all('{fo+}', $string, $matches)) { ... } +$newString = preg_replace('{fo+}', 'bar', $string); +$newString = preg_replace_callback('{fo+}', function ($match) { return strtoupper($match[0]); }, $string); +$newString = preg_replace_callback_array(['{fo+}' => fn ($match) => strtoupper($match[0])], $string); +$filtered = preg_grep('{[a-z]}', $elements); +$array = preg_split('{[a-z]+}', $string); +``` + +You can now call these on the `Preg` class: + +```php +use Composer\Pcre\Preg; + +if (Preg::match('{fo+}', $string, $matches)) { ... } +if (Preg::matchWithOffsets('{fo+}', $string, $matches)) { ... } +if (Preg::matchAll('{fo+}', $string, $matches)) { ... } +$newString = Preg::replace('{fo+}', 'bar', $string); +$newString = Preg::replaceCallback('{fo+}', function ($match) { return strtoupper($match[0]); }, $string); +$newString = Preg::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string); +$filtered = Preg::grep('{[a-z]}', $elements); +$array = Preg::split('{[a-z]+}', $string); +``` + +The main difference is if anything fails to match/replace/.., it will throw a `Composer\Pcre\PcreException` +instead of returning `null` (or false in some cases), so you can now use the return values safely relying on +the fact that they can only be strings (for replace), ints (for match) or arrays (for grep/split). + +Additionally the `Preg` class provides match methods that return `bool` rather than `int`, for stricter type safety +when the number of pattern matches is not useful: + +```php +use Composer\Pcre\Preg; + +if (Preg::isMatch('{fo+}', $string, $matches)) // bool +if (Preg::isMatchAll('{fo+}', $string, $matches)) // bool +``` + +Finally the `Preg` class provides a few `*StrictGroups` method variants that ensure match groups +are always present and thus non-nullable, making it easier to write type-safe code: + +```php +use Composer\Pcre\Preg; + +// $matches is guaranteed to be an array of strings, if a subpattern does not match and produces a null it will throw +if (Preg::matchStrictGroups('{fo+}', $string, $matches)) +if (Preg::matchAllStrictGroups('{fo+}', $string, $matches)) +``` + +**Note:** This is generally safe to use as long as you do not have optional subpatterns (i.e. `(something)?` +or `(something)*` or branches with a `|` that result in some groups not being matched at all). +A subpattern that can match an empty string like `(.*)` is **not** optional, it will be present as an +empty string in the matches. A non-matching subpattern, even if optional like `(?:foo)?` will anyway not be present in +matches so it is also not a problem to use these with `*StrictGroups` methods. + +If you would prefer a slightly more verbose usage, replacing by-ref arguments by result objects, you can use the `Regex` class: + +```php +use Composer\Pcre\Regex; + +// this is useful when you are just interested in knowing if something matched +// as it returns a bool instead of int(1/0) for match +$bool = Regex::isMatch('{fo+}', $string); + +$result = Regex::match('{fo+}', $string); +if ($result->matched) { something($result->matches); } + +$result = Regex::matchWithOffsets('{fo+}', $string); +if ($result->matched) { something($result->matches); } + +$result = Regex::matchAll('{fo+}', $string); +if ($result->matched && $result->count > 3) { something($result->matches); } + +$newString = Regex::replace('{fo+}', 'bar', $string)->result; +$newString = Regex::replaceCallback('{fo+}', function ($match) { return strtoupper($match[0]); }, $string)->result; +$newString = Regex::replaceCallbackArray(['{fo+}' => fn ($match) => strtoupper($match[0])], $string)->result; +``` + +Note that `preg_grep` and `preg_split` are only callable via the `Preg` class as they do not have +complex return types warranting a specific result object. + +See the [MatchResult](src/MatchResult.php), [MatchWithOffsetsResult](src/MatchWithOffsetsResult.php), [MatchAllResult](src/MatchAllResult.php), +[MatchAllWithOffsetsResult](src/MatchAllWithOffsetsResult.php), and [ReplaceResult](src/ReplaceResult.php) class sources for more details. + +Restrictions / Limitations +-------------------------- + +Due to type safety requirements a few restrictions are in place. + +- matching using `PREG_OFFSET_CAPTURE` is made available via `matchWithOffsets` and `matchAllWithOffsets`. + You cannot pass the flag to `match`/`matchAll`. +- `Preg::split` will also reject `PREG_SPLIT_OFFSET_CAPTURE` and you should use `splitWithOffsets` + instead. +- `matchAll` rejects `PREG_SET_ORDER` as it also changes the shape of the returned matches. There + is no alternative provided as you can fairly easily code around it. +- `preg_filter` is not supported as it has a rather crazy API, most likely you should rather + use `Preg::grep` in combination with some loop and `Preg::replace`. +- `replace`, `replaceCallback` and `replaceCallbackArray` do not support an array `$subject`, + only simple strings. +- As of 2.0, the library always uses `PREG_UNMATCHED_AS_NULL` for matching, which offers [much + saner/more predictable results](#preg_unmatched_as_null). As of 3.0 the flag is also set for + `replaceCallback` and `replaceCallbackArray`. + +#### PREG_UNMATCHED_AS_NULL + +As of 2.0, this library always uses PREG_UNMATCHED_AS_NULL for all `match*` and `isMatch*` +functions. As of 3.0 it is also done for `replaceCallback` and `replaceCallbackArray`. + +This means your matches will always contain all matching groups, either as null if unmatched +or as string if it matched. + +The advantages in clarity and predictability are clearer if you compare the two outputs of +running this with and without PREG_UNMATCHED_AS_NULL in $flags: + +```php +preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags); +``` + +| no flag | PREG_UNMATCHED_AS_NULL | +| --- | --- | +| array (size=4) | array (size=5) | +| 0 => string 'ac' (length=2) | 0 => string 'ac' (length=2) | +| 1 => string 'a' (length=1) | 1 => string 'a' (length=1) | +| 2 => string '' (length=0) | 2 => null | +| 3 => string 'c' (length=1) | 3 => string 'c' (length=1) | +| | 4 => null | +| group 2 (any unmatched group preceding one that matched) is set to `''`. You cannot tell if it matched an empty string or did not match at all | group 2 is `null` when unmatched and a string if it matched, easy to check for | +| group 4 (any optional group without a matching one following) is missing altogether. So you have to check with `isset()`, but really you want `isset($m[4]) && $m[4] !== ''` for safety unless you are very careful to check that a non-optional group follows it | group 4 is always set, and null in this case as there was no match, easy to check for with `$m[4] !== null` | + +PHPStan Extension +----------------- + +To use the PHPStan extension if you do not use `phpstan/extension-installer` you can include `vendor/composer/pcre/extension.neon` in your PHPStan config. + +The extension provides much better type information for $matches as well as regex validation where possible. + +License +------- + +composer/pcre is licensed under the MIT License, see the LICENSE file for details. diff --git a/vendor/composer/pcre/composer.json b/vendor/composer/pcre/composer.json new file mode 100644 index 0000000..a6e72c3 --- /dev/null +++ b/vendor/composer/pcre/composer.json @@ -0,0 +1,58 @@ +{ + "name": "composer/pcre", + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "type": "library", + "license": "MIT", + "keywords": [ + "pcre", + "regex", + "preg", + "regular expression" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "phpstan/phpstan": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpstan/phpstan-deprecation-rules": "^2" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Composer\\Pcre\\": "tests" + } + }, + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "scripts": { + "test": [ + "@php vendor/bin/phpunit", + "@php vendor/bin/phpunit --testsuite phpstan" + ], + "phpstan": "@php phpstan analyse" + } +} diff --git a/vendor/composer/pcre/extension.neon b/vendor/composer/pcre/extension.neon new file mode 100644 index 0000000..b9cea11 --- /dev/null +++ b/vendor/composer/pcre/extension.neon @@ -0,0 +1,22 @@ +# composer/pcre PHPStan extensions +# +# These can be reused by third party packages by including 'vendor/composer/pcre/extension.neon' +# in your phpstan config + +services: + - + class: Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension + tags: + - phpstan.staticMethodParameterOutTypeExtension + - + class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension + - + class: Composer\Pcre\PHPStan\PregReplaceCallbackClosureTypeExtension + tags: + - phpstan.staticMethodParameterClosureTypeExtension + +rules: + - Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule + - Composer\Pcre\PHPStan\InvalidRegexPatternRule diff --git a/vendor/composer/pcre/src/MatchAllResult.php b/vendor/composer/pcre/src/MatchAllResult.php new file mode 100644 index 0000000..b22b52d --- /dev/null +++ b/vendor/composer/pcre/src/MatchAllResult.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Pcre; + +final class MatchAllResult +{ + /** + * An array of match group => list of matched strings + * + * @readonly + * @var array> + */ + public $matches; + + /** + * @readonly + * @var 0|positive-int + */ + public $count; + + /** + * @readonly + * @var bool + */ + public $matched; + + /** + * @param 0|positive-int $count + * @param array> $matches + */ + public function __construct(int $count, array $matches) + { + $this->matches = $matches; + $this->matched = (bool) $count; + $this->count = $count; + } +} diff --git a/vendor/composer/pcre/src/MatchAllStrictGroupsResult.php b/vendor/composer/pcre/src/MatchAllStrictGroupsResult.php new file mode 100644 index 0000000..b7ec397 --- /dev/null +++ b/vendor/composer/pcre/src/MatchAllStrictGroupsResult.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Pcre; + +final class MatchAllStrictGroupsResult +{ + /** + * An array of match group => list of matched strings + * + * @readonly + * @var array> + */ + public $matches; + + /** + * @readonly + * @var 0|positive-int + */ + public $count; + + /** + * @readonly + * @var bool + */ + public $matched; + + /** + * @param 0|positive-int $count + * @param array> $matches + */ + public function __construct(int $count, array $matches) + { + $this->matches = $matches; + $this->matched = (bool) $count; + $this->count = $count; + } +} diff --git a/vendor/composer/pcre/src/MatchAllWithOffsetsResult.php b/vendor/composer/pcre/src/MatchAllWithOffsetsResult.php new file mode 100644 index 0000000..032a02c --- /dev/null +++ b/vendor/composer/pcre/src/MatchAllWithOffsetsResult.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Pcre; + +final class MatchAllWithOffsetsResult +{ + /** + * An array of match group => list of matches, every match being a pair of string matched + offset in bytes (or -1 if no match) + * + * @readonly + * @var array> + * @phpstan-var array}>> + */ + public $matches; + + /** + * @readonly + * @var 0|positive-int + */ + public $count; + + /** + * @readonly + * @var bool + */ + public $matched; + + /** + * @param 0|positive-int $count + * @param array> $matches + * @phpstan-param array}>> $matches + */ + public function __construct(int $count, array $matches) + { + $this->matches = $matches; + $this->matched = (bool) $count; + $this->count = $count; + } +} diff --git a/vendor/composer/pcre/src/MatchResult.php b/vendor/composer/pcre/src/MatchResult.php new file mode 100644 index 0000000..e951a5e --- /dev/null +++ b/vendor/composer/pcre/src/MatchResult.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Pcre; + +final class MatchResult +{ + /** + * An array of match group => string matched + * + * @readonly + * @var array + */ + public $matches; + + /** + * @readonly + * @var bool + */ + public $matched; + + /** + * @param 0|positive-int $count + * @param array $matches + */ + public function __construct(int $count, array $matches) + { + $this->matches = $matches; + $this->matched = (bool) $count; + } +} diff --git a/vendor/composer/pcre/src/MatchStrictGroupsResult.php b/vendor/composer/pcre/src/MatchStrictGroupsResult.php new file mode 100644 index 0000000..126ee62 --- /dev/null +++ b/vendor/composer/pcre/src/MatchStrictGroupsResult.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Pcre; + +final class MatchStrictGroupsResult +{ + /** + * An array of match group => string matched + * + * @readonly + * @var array + */ + public $matches; + + /** + * @readonly + * @var bool + */ + public $matched; + + /** + * @param 0|positive-int $count + * @param array $matches + */ + public function __construct(int $count, array $matches) + { + $this->matches = $matches; + $this->matched = (bool) $count; + } +} diff --git a/vendor/composer/pcre/src/MatchWithOffsetsResult.php b/vendor/composer/pcre/src/MatchWithOffsetsResult.php new file mode 100644 index 0000000..ba4d4bc --- /dev/null +++ b/vendor/composer/pcre/src/MatchWithOffsetsResult.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Pcre; + +final class MatchWithOffsetsResult +{ + /** + * An array of match group => pair of string matched + offset in bytes (or -1 if no match) + * + * @readonly + * @var array + * @phpstan-var array}> + */ + public $matches; + + /** + * @readonly + * @var bool + */ + public $matched; + + /** + * @param 0|positive-int $count + * @param array $matches + * @phpstan-param array}> $matches + */ + public function __construct(int $count, array $matches) + { + $this->matches = $matches; + $this->matched = (bool) $count; + } +} diff --git a/vendor/composer/pcre/src/PHPStan/InvalidRegexPatternRule.php b/vendor/composer/pcre/src/PHPStan/InvalidRegexPatternRule.php new file mode 100644 index 0000000..8a05fb2 --- /dev/null +++ b/vendor/composer/pcre/src/PHPStan/InvalidRegexPatternRule.php @@ -0,0 +1,142 @@ + + */ +class InvalidRegexPatternRule implements Rule +{ + public function getNodeType(): string + { + return StaticCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $patterns = $this->extractPatterns($node, $scope); + + $errors = []; + foreach ($patterns as $pattern) { + $errorMessage = $this->validatePattern($pattern); + if ($errorMessage === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf('Regex pattern is invalid: %s', $errorMessage))->identifier('regexp.pattern')->build(); + } + + return $errors; + } + + /** + * @return string[] + */ + private function extractPatterns(StaticCall $node, Scope $scope): array + { + if (!$node->class instanceof FullyQualified) { + return []; + } + $isRegex = $node->class->toString() === Regex::class; + $isPreg = $node->class->toString() === Preg::class; + if (!$isRegex && !$isPreg) { + return []; + } + if (!$node->name instanceof Node\Identifier || !Preg::isMatch('{^(match|isMatch|grep|replace|split)}', $node->name->name)) { + return []; + } + + $functionName = $node->name->name; + if (!isset($node->getArgs()[0])) { + return []; + } + + $patternNode = $node->getArgs()[0]->value; + $patternType = $scope->getType($patternNode); + + $patternStrings = []; + + foreach ($patternType->getConstantStrings() as $constantStringType) { + if ($functionName === 'replaceCallbackArray') { + continue; + } + + $patternStrings[] = $constantStringType->getValue(); + } + + foreach ($patternType->getConstantArrays() as $constantArrayType) { + if ( + in_array($functionName, [ + 'replace', + 'replaceCallback', + ], true) + ) { + foreach ($constantArrayType->getValueTypes() as $arrayKeyType) { + foreach ($arrayKeyType->getConstantStrings() as $constantString) { + $patternStrings[] = $constantString->getValue(); + } + } + } + + if ($functionName !== 'replaceCallbackArray') { + continue; + } + + foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) { + foreach ($arrayKeyType->getConstantStrings() as $constantString) { + $patternStrings[] = $constantString->getValue(); + } + } + } + + return $patternStrings; + } + + private function validatePattern(string $pattern): ?string + { + try { + $msg = null; + $prev = set_error_handler(function (int $severity, string $message, string $file) use (&$msg): bool { + $msg = preg_replace("#^preg_match(_all)?\\(.*?\\): #", '', $message); + + return true; + }); + + if ($pattern === '') { + return 'Empty string is not a valid regular expression'; + } + + Preg::match($pattern, ''); + if ($msg !== null) { + return $msg; + } + } catch (PcreException $e) { + if ($e->getCode() === PREG_INTERNAL_ERROR && $msg !== null) { + return $msg; + } + + return preg_replace('{.*? failed executing ".*": }', '', $e->getMessage()); + } finally { + restore_error_handler(); + } + + return null; + } + +} diff --git a/vendor/composer/pcre/src/PHPStan/PregMatchFlags.php b/vendor/composer/pcre/src/PHPStan/PregMatchFlags.php new file mode 100644 index 0000000..aa30ab3 --- /dev/null +++ b/vendor/composer/pcre/src/PHPStan/PregMatchFlags.php @@ -0,0 +1,70 @@ +getType($flagsArg->value); + + $constantScalars = $flagsType->getConstantScalarValues(); + if ($constantScalars === []) { + return null; + } + + $internalFlagsTypes = []; + foreach ($flagsType->getConstantScalarValues() as $constantScalarValue) { + if (!is_int($constantScalarValue)) { + return null; + } + + $internalFlagsTypes[] = new ConstantIntegerType($constantScalarValue | PREG_UNMATCHED_AS_NULL); + } + return TypeCombinator::union(...$internalFlagsTypes); + } + + static public function removeNullFromMatches(Type $matchesType): Type + { + return TypeTraverser::map($matchesType, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof ConstantArrayType) { + return new ConstantArrayType( + $type->getKeyTypes(), + array_map(static function (Type $valueType) use ($traverse): Type { + return $traverse($valueType); + }, $type->getValueTypes()), + $type->getNextAutoIndexes(), + [], + $type->isList() + ); + } + + if ($type instanceof ArrayType) { + return new ArrayType($type->getKeyType(), $traverse($type->getItemType())); + } + + return TypeCombinator::removeNull($type); + }); + } + +} diff --git a/vendor/composer/pcre/src/PHPStan/PregMatchParameterOutTypeExtension.php b/vendor/composer/pcre/src/PHPStan/PregMatchParameterOutTypeExtension.php new file mode 100644 index 0000000..e0d6020 --- /dev/null +++ b/vendor/composer/pcre/src/PHPStan/PregMatchParameterOutTypeExtension.php @@ -0,0 +1,65 @@ +regexShapeMatcher = $regexShapeMatcher; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return + $methodReflection->getDeclaringClass()->getName() === Preg::class + && in_array($methodReflection->getName(), [ + 'match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups', + 'matchAll', 'isMatchAll', 'matchAllStrictGroups', 'isMatchAllStrictGroups' + ], true) + && $parameter->getName() === 'matches'; + } + + public function getParameterOutTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $methodCall->getArgs(); + $patternArg = $args[0] ?? null; + $matchesArg = $args[2] ?? null; + $flagsArg = $args[3] ?? null; + + if ( + $patternArg === null || $matchesArg === null + ) { + return null; + } + + $flagsType = PregMatchFlags::getType($flagsArg, $scope); + if ($flagsType === null) { + return null; + } + + if (stripos($methodReflection->getName(), 'matchAll') !== false) { + return $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope); + } + + return $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope); + } + +} diff --git a/vendor/composer/pcre/src/PHPStan/PregMatchTypeSpecifyingExtension.php b/vendor/composer/pcre/src/PHPStan/PregMatchTypeSpecifyingExtension.php new file mode 100644 index 0000000..e492c79 --- /dev/null +++ b/vendor/composer/pcre/src/PHPStan/PregMatchTypeSpecifyingExtension.php @@ -0,0 +1,125 @@ +regexShapeMatcher = $regexShapeMatcher; + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return Preg::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context): bool + { + return in_array($methodReflection->getName(), [ + 'match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups', + 'matchAll', 'isMatchAll', 'matchAllStrictGroups', 'isMatchAllStrictGroups' + ], true) + && !$context->null(); + } + + public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $args = $node->getArgs(); + $patternArg = $args[0] ?? null; + $subjectArg = $args[1] ?? null; + $matchesArg = $args[2] ?? null; + $flagsArg = $args[3] ?? null; + + $subjectTypes = new SpecifiedTypes(); + if ($patternArg === null) { + return $subjectTypes; + } + + if ( + $subjectArg !== null + && $context->true() + && $scope->getType($subjectArg->value)->isString()->yes() + ) { + $subjectType = $this->regexShapeMatcher->matchSubjectExpr($patternArg->value, $scope); + if ($subjectType !== null) { + $subjectTypes = $this->typeSpecifier->create( + $subjectArg->value, + $subjectType, + $context, + $scope, + )->setRootExpr($node); + } + } + + if ($matchesArg === null) { + return $subjectTypes; + } + + $flagsType = PregMatchFlags::getType($flagsArg, $scope); + if ($flagsType === null) { + return $subjectTypes; + } + + if (stripos($methodReflection->getName(), 'matchAll') !== false) { + $matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope); + } else { + $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope); + } + + if ($matchedType === null) { + return $subjectTypes; + } + + if ( + in_array($methodReflection->getName(), ['matchStrictGroups', 'isMatchStrictGroups', 'matchAllStrictGroups', 'isMatchAllStrictGroups'], true) + ) { + $matchedType = PregMatchFlags::removeNullFromMatches($matchedType); + } + + $overwrite = false; + if ($context->false()) { + $overwrite = true; + $context = $context->negate(); + } + + $specifiedTypes = $this->typeSpecifier->create( + $matchesArg->value, + $matchedType, + $context, + $scope + )->setRootExpr($node); + + return $subjectTypes->unionWith($overwrite ? $specifiedTypes->setAlwaysOverwriteTypes() : $specifiedTypes); + } +} diff --git a/vendor/composer/pcre/src/PHPStan/PregReplaceCallbackClosureTypeExtension.php b/vendor/composer/pcre/src/PHPStan/PregReplaceCallbackClosureTypeExtension.php new file mode 100644 index 0000000..7b95367 --- /dev/null +++ b/vendor/composer/pcre/src/PHPStan/PregReplaceCallbackClosureTypeExtension.php @@ -0,0 +1,91 @@ +regexShapeMatcher = $regexShapeMatcher; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return in_array($methodReflection->getDeclaringClass()->getName(), [Preg::class, Regex::class], true) + && in_array($methodReflection->getName(), ['replaceCallback', 'replaceCallbackStrictGroups'], true) + && $parameter->getName() === 'replacement'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $methodCall->getArgs(); + $patternArg = $args[0] ?? null; + $flagsArg = $args[5] ?? null; + + if ( + $patternArg === null + ) { + return null; + } + + $flagsType = PregMatchFlags::getType($flagsArg, $scope); + + $matchesType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope); + if ($matchesType === null) { + return null; + } + + if ($methodReflection->getName() === 'replaceCallbackStrictGroups' && count($matchesType->getConstantArrays()) === 1) { + $matchesType = $matchesType->getConstantArrays()[0]; + $matchesType = new ConstantArrayType( + $matchesType->getKeyTypes(), + array_map(static function (Type $valueType): Type { + if (count($valueType->getConstantArrays()) === 1) { + $valueTypeArray = $valueType->getConstantArrays()[0]; + return new ConstantArrayType( + $valueTypeArray->getKeyTypes(), + array_map(static function (Type $valueType): Type { + return TypeCombinator::removeNull($valueType); + }, $valueTypeArray->getValueTypes()), + $valueTypeArray->getNextAutoIndexes(), + [], + $valueTypeArray->isList() + ); + } + return TypeCombinator::removeNull($valueType); + }, $matchesType->getValueTypes()), + $matchesType->getNextAutoIndexes(), + [], + $matchesType->isList() + ); + } + + return new ClosureType( + [ + new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), $matchesType, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()), + ], + new StringType() + ); + } +} diff --git a/vendor/composer/pcre/src/PHPStan/UnsafeStrictGroupsCallRule.php b/vendor/composer/pcre/src/PHPStan/UnsafeStrictGroupsCallRule.php new file mode 100644 index 0000000..5bced50 --- /dev/null +++ b/vendor/composer/pcre/src/PHPStan/UnsafeStrictGroupsCallRule.php @@ -0,0 +1,112 @@ + + */ +final class UnsafeStrictGroupsCallRule implements Rule +{ + /** + * @var RegexArrayShapeMatcher + */ + private $regexShapeMatcher; + + public function __construct(RegexArrayShapeMatcher $regexShapeMatcher) + { + $this->regexShapeMatcher = $regexShapeMatcher; + } + + public function getNodeType(): string + { + return StaticCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->class instanceof FullyQualified) { + return []; + } + $isRegex = $node->class->toString() === Regex::class; + $isPreg = $node->class->toString() === Preg::class; + if (!$isRegex && !$isPreg) { + return []; + } + if (!$node->name instanceof Node\Identifier || !in_array($node->name->name, ['matchStrictGroups', 'isMatchStrictGroups', 'matchAllStrictGroups', 'isMatchAllStrictGroups'], true)) { + return []; + } + + $args = $node->getArgs(); + if (!isset($args[0])) { + return []; + } + + $patternArg = $args[0] ?? null; + if ($isPreg) { + if (!isset($args[2])) { // no matches set, skip as the matches won't be used anyway + return []; + } + $flagsArg = $args[3] ?? null; + } else { + $flagsArg = $args[2] ?? null; + } + + if ($patternArg === null) { + return []; + } + + $flagsType = PregMatchFlags::getType($flagsArg, $scope); + if ($flagsType === null) { + return []; + } + + $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope); + if ($matchedType === null) { + return [ + RuleErrorBuilder::message(sprintf('The %s call is potentially unsafe as $matches\' type could not be inferred.', $node->name->name)) + ->identifier('composerPcre.maybeUnsafeStrictGroups') + ->build(), + ]; + } + + if (count($matchedType->getConstantArrays()) === 1) { + $matchedType = $matchedType->getConstantArrays()[0]; + $nullableGroups = []; + foreach ($matchedType->getValueTypes() as $index => $type) { + if (TypeCombinator::containsNull($type)) { + $nullableGroups[] = $matchedType->getKeyTypes()[$index]->getValue(); + } + } + + if (\count($nullableGroups) > 0) { + return [ + RuleErrorBuilder::message(sprintf( + 'The %s call is unsafe as match group%s "%s" %s optional and may be null.', + $node->name->name, + \count($nullableGroups) > 1 ? 's' : '', + implode('", "', $nullableGroups), + \count($nullableGroups) > 1 ? 'are' : 'is' + ))->identifier('composerPcre.unsafeStrictGroups')->build(), + ]; + } + } + + return []; + } +} diff --git a/vendor/composer/pcre/src/PcreException.php b/vendor/composer/pcre/src/PcreException.php new file mode 100644 index 0000000..23d9327 --- /dev/null +++ b/vendor/composer/pcre/src/PcreException.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Pcre; + +class PcreException extends \RuntimeException +{ + /** + * @param string $function + * @param string|string[] $pattern + * @return self + */ + public static function fromFunction($function, $pattern) + { + $code = preg_last_error(); + + if (is_array($pattern)) { + $pattern = implode(', ', $pattern); + } + + return new PcreException($function.'(): failed executing "'.$pattern.'": '.self::pcreLastErrorMessage($code), $code); + } + + /** + * @param int $code + * @return string + */ + private static function pcreLastErrorMessage($code) + { + if (function_exists('preg_last_error_msg')) { + return preg_last_error_msg(); + } + + $constants = get_defined_constants(true); + if (!isset($constants['pcre']) || !is_array($constants['pcre'])) { + return 'UNDEFINED_ERROR'; + } + + foreach ($constants['pcre'] as $const => $val) { + if ($val === $code && substr($const, -6) === '_ERROR') { + return $const; + } + } + + return 'UNDEFINED_ERROR'; + } +} diff --git a/vendor/composer/pcre/src/Preg.php b/vendor/composer/pcre/src/Preg.php new file mode 100644 index 0000000..98b4d29 --- /dev/null +++ b/vendor/composer/pcre/src/Preg.php @@ -0,0 +1,430 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Pcre; + +class Preg +{ + /** @internal */ + public const ARRAY_MSG = '$subject as an array is not supported. You can use \'foreach\' instead.'; + /** @internal */ + public const INVALID_TYPE_MSG = '$subject must be a string, %s given.'; + + /** + * @param non-empty-string $pattern + * @param array $matches Set by method + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + * @return 0|1 + * + * @param-out array $matches + */ + public static function match(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int + { + self::checkOffsetCapture($flags, 'matchWithOffsets'); + + $result = preg_match($pattern, $subject, $matches, $flags | PREG_UNMATCHED_AS_NULL, $offset); + if ($result === false) { + throw PcreException::fromFunction('preg_match', $pattern); + } + + return $result; + } + + /** + * Variant of `match()` which outputs non-null matches (or throws) + * + * @param non-empty-string $pattern + * @param array $matches Set by method + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + * @return 0|1 + * @throws UnexpectedNullMatchException + * + * @param-out array $matches + */ + public static function matchStrictGroups(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int + { + $result = self::match($pattern, $subject, $matchesInternal, $flags, $offset); + $matches = self::enforceNonNullMatches($pattern, $matchesInternal, 'match'); + + return $result; + } + + /** + * Runs preg_match with PREG_OFFSET_CAPTURE + * + * @param non-empty-string $pattern + * @param array $matches Set by method + * @param int-mask $flags PREG_UNMATCHED_AS_NULL and PREG_OFFSET_CAPTURE are always set, no other flags are supported + * @return 0|1 + * + * @param-out array}> $matches + */ + public static function matchWithOffsets(string $pattern, string $subject, ?array &$matches, int $flags = 0, int $offset = 0): int + { + $result = preg_match($pattern, $subject, $matches, $flags | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE, $offset); + if ($result === false) { + throw PcreException::fromFunction('preg_match', $pattern); + } + + return $result; + } + + /** + * @param non-empty-string $pattern + * @param array $matches Set by method + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + * @return 0|positive-int + * + * @param-out array> $matches + */ + public static function matchAll(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int + { + self::checkOffsetCapture($flags, 'matchAllWithOffsets'); + self::checkSetOrder($flags); + + $result = preg_match_all($pattern, $subject, $matches, $flags | PREG_UNMATCHED_AS_NULL, $offset); + if (!is_int($result)) { // PHP < 8 may return null, 8+ returns int|false + throw PcreException::fromFunction('preg_match_all', $pattern); + } + + return $result; + } + + /** + * Variant of `match()` which outputs non-null matches (or throws) + * + * @param non-empty-string $pattern + * @param array $matches Set by method + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + * @return 0|positive-int + * @throws UnexpectedNullMatchException + * + * @param-out array> $matches + */ + public static function matchAllStrictGroups(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int + { + $result = self::matchAll($pattern, $subject, $matchesInternal, $flags, $offset); + $matches = self::enforceNonNullMatchAll($pattern, $matchesInternal, 'matchAll'); + + return $result; + } + + /** + * Runs preg_match_all with PREG_OFFSET_CAPTURE + * + * @param non-empty-string $pattern + * @param array $matches Set by method + * @param int-mask $flags PREG_UNMATCHED_AS_NULL and PREG_MATCH_OFFSET are always set, no other flags are supported + * @return 0|positive-int + * + * @param-out array}>> $matches + */ + public static function matchAllWithOffsets(string $pattern, string $subject, ?array &$matches, int $flags = 0, int $offset = 0): int + { + self::checkSetOrder($flags); + + $result = preg_match_all($pattern, $subject, $matches, $flags | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE, $offset); + if (!is_int($result)) { // PHP < 8 may return null, 8+ returns int|false + throw PcreException::fromFunction('preg_match_all', $pattern); + } + + return $result; + } + + /** + * @param string|string[] $pattern + * @param string|string[] $replacement + * @param string $subject + * @param int $count Set by method + * + * @param-out int<0, max> $count + */ + public static function replace($pattern, $replacement, $subject, int $limit = -1, ?int &$count = null): string + { + if (!is_scalar($subject)) { + if (is_array($subject)) { + throw new \InvalidArgumentException(static::ARRAY_MSG); + } + + throw new \TypeError(sprintf(static::INVALID_TYPE_MSG, gettype($subject))); + } + + $result = preg_replace($pattern, $replacement, $subject, $limit, $count); + if ($result === null) { + throw PcreException::fromFunction('preg_replace', $pattern); + } + + return $result; + } + + /** + * @param string|string[] $pattern + * @param ($flags is PREG_OFFSET_CAPTURE ? (callable(array}>): string) : callable(array): string) $replacement + * @param string $subject + * @param int $count Set by method + * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set + * + * @param-out int<0, max> $count + */ + public static function replaceCallback($pattern, callable $replacement, $subject, int $limit = -1, ?int &$count = null, int $flags = 0): string + { + if (!is_scalar($subject)) { + if (is_array($subject)) { + throw new \InvalidArgumentException(static::ARRAY_MSG); + } + + throw new \TypeError(sprintf(static::INVALID_TYPE_MSG, gettype($subject))); + } + + $result = preg_replace_callback($pattern, $replacement, $subject, $limit, $count, $flags | PREG_UNMATCHED_AS_NULL); + if ($result === null) { + throw PcreException::fromFunction('preg_replace_callback', $pattern); + } + + return $result; + } + + /** + * Variant of `replaceCallback()` which outputs non-null matches (or throws) + * + * @param string $pattern + * @param ($flags is PREG_OFFSET_CAPTURE ? (callable(array}>): string) : callable(array): string) $replacement + * @param string $subject + * @param int $count Set by method + * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set + * + * @param-out int<0, max> $count + */ + public static function replaceCallbackStrictGroups(string $pattern, callable $replacement, $subject, int $limit = -1, ?int &$count = null, int $flags = 0): string + { + return self::replaceCallback($pattern, function (array $matches) use ($pattern, $replacement) { + return $replacement(self::enforceNonNullMatches($pattern, $matches, 'replaceCallback')); + }, $subject, $limit, $count, $flags); + } + + /** + * @param ($flags is PREG_OFFSET_CAPTURE ? (array}>): string>) : array): string>) $pattern + * @param string $subject + * @param int $count Set by method + * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set + * + * @param-out int<0, max> $count + */ + public static function replaceCallbackArray(array $pattern, $subject, int $limit = -1, ?int &$count = null, int $flags = 0): string + { + if (!is_scalar($subject)) { + if (is_array($subject)) { + throw new \InvalidArgumentException(static::ARRAY_MSG); + } + + throw new \TypeError(sprintf(static::INVALID_TYPE_MSG, gettype($subject))); + } + + $result = preg_replace_callback_array($pattern, $subject, $limit, $count, $flags | PREG_UNMATCHED_AS_NULL); + if ($result === null) { + $pattern = array_keys($pattern); + throw PcreException::fromFunction('preg_replace_callback_array', $pattern); + } + + return $result; + } + + /** + * @param int-mask $flags PREG_SPLIT_NO_EMPTY or PREG_SPLIT_DELIM_CAPTURE + * @return list + */ + public static function split(string $pattern, string $subject, int $limit = -1, int $flags = 0): array + { + if (($flags & PREG_SPLIT_OFFSET_CAPTURE) !== 0) { + throw new \InvalidArgumentException('PREG_SPLIT_OFFSET_CAPTURE is not supported as it changes the type of $matches, use splitWithOffsets() instead'); + } + + $result = preg_split($pattern, $subject, $limit, $flags); + if ($result === false) { + throw PcreException::fromFunction('preg_split', $pattern); + } + + return $result; + } + + /** + * @param int-mask $flags PREG_SPLIT_NO_EMPTY or PREG_SPLIT_DELIM_CAPTURE, PREG_SPLIT_OFFSET_CAPTURE is always set + * @return list + * @phpstan-return list}> + */ + public static function splitWithOffsets(string $pattern, string $subject, int $limit = -1, int $flags = 0): array + { + $result = preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE); + if ($result === false) { + throw PcreException::fromFunction('preg_split', $pattern); + } + + return $result; + } + + /** + * @template T of string|\Stringable + * @param string $pattern + * @param array $array + * @param int-mask $flags PREG_GREP_INVERT + * @return array + */ + public static function grep(string $pattern, array $array, int $flags = 0): array + { + $result = preg_grep($pattern, $array, $flags); + if ($result === false) { + throw PcreException::fromFunction('preg_grep', $pattern); + } + + return $result; + } + + /** + * Variant of match() which returns a bool instead of int + * + * @param non-empty-string $pattern + * @param array $matches Set by method + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + * + * @param-out array $matches + */ + public static function isMatch(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool + { + return (bool) static::match($pattern, $subject, $matches, $flags, $offset); + } + + /** + * Variant of `isMatch()` which outputs non-null matches (or throws) + * + * @param non-empty-string $pattern + * @param array $matches Set by method + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + * @throws UnexpectedNullMatchException + * + * @param-out array $matches + */ + public static function isMatchStrictGroups(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool + { + return (bool) self::matchStrictGroups($pattern, $subject, $matches, $flags, $offset); + } + + /** + * Variant of matchAll() which returns a bool instead of int + * + * @param non-empty-string $pattern + * @param array $matches Set by method + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + * + * @param-out array> $matches + */ + public static function isMatchAll(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool + { + return (bool) static::matchAll($pattern, $subject, $matches, $flags, $offset); + } + + /** + * Variant of `isMatchAll()` which outputs non-null matches (or throws) + * + * @param non-empty-string $pattern + * @param array $matches Set by method + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + * + * @param-out array> $matches + */ + public static function isMatchAllStrictGroups(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool + { + return (bool) self::matchAllStrictGroups($pattern, $subject, $matches, $flags, $offset); + } + + /** + * Variant of matchWithOffsets() which returns a bool instead of int + * + * Runs preg_match with PREG_OFFSET_CAPTURE + * + * @param non-empty-string $pattern + * @param array $matches Set by method + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + * + * @param-out array}> $matches + */ + public static function isMatchWithOffsets(string $pattern, string $subject, ?array &$matches, int $flags = 0, int $offset = 0): bool + { + return (bool) static::matchWithOffsets($pattern, $subject, $matches, $flags, $offset); + } + + /** + * Variant of matchAllWithOffsets() which returns a bool instead of int + * + * Runs preg_match_all with PREG_OFFSET_CAPTURE + * + * @param non-empty-string $pattern + * @param array $matches Set by method + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + * + * @param-out array}>> $matches + */ + public static function isMatchAllWithOffsets(string $pattern, string $subject, ?array &$matches, int $flags = 0, int $offset = 0): bool + { + return (bool) static::matchAllWithOffsets($pattern, $subject, $matches, $flags, $offset); + } + + private static function checkOffsetCapture(int $flags, string $useFunctionName): void + { + if (($flags & PREG_OFFSET_CAPTURE) !== 0) { + throw new \InvalidArgumentException('PREG_OFFSET_CAPTURE is not supported as it changes the type of $matches, use ' . $useFunctionName . '() instead'); + } + } + + private static function checkSetOrder(int $flags): void + { + if (($flags & PREG_SET_ORDER) !== 0) { + throw new \InvalidArgumentException('PREG_SET_ORDER is not supported as it changes the type of $matches'); + } + } + + /** + * @param array $matches + * @return array + * @throws UnexpectedNullMatchException + */ + private static function enforceNonNullMatches(string $pattern, array $matches, string $variantMethod): array + { + foreach ($matches as $group => $match) { + if (is_string($match) || (is_array($match) && is_string($match[0]))) { + continue; + } + + throw new UnexpectedNullMatchException('Pattern "'.$pattern.'" had an unexpected unmatched group "'.$group.'", make sure the pattern always matches or use '.$variantMethod.'() instead.'); + } + + /** @var array */ + return $matches; + } + + /** + * @param array> $matches + * @return array> + * @throws UnexpectedNullMatchException + */ + private static function enforceNonNullMatchAll(string $pattern, array $matches, string $variantMethod): array + { + foreach ($matches as $group => $groupMatches) { + foreach ($groupMatches as $match) { + if (null === $match) { + throw new UnexpectedNullMatchException('Pattern "'.$pattern.'" had an unexpected unmatched group "'.$group.'", make sure the pattern always matches or use '.$variantMethod.'() instead.'); + } + } + } + + /** @var array> */ + return $matches; + } +} diff --git a/vendor/composer/pcre/src/Regex.php b/vendor/composer/pcre/src/Regex.php new file mode 100644 index 0000000..038cf06 --- /dev/null +++ b/vendor/composer/pcre/src/Regex.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Pcre; + +class Regex +{ + /** + * @param non-empty-string $pattern + */ + public static function isMatch(string $pattern, string $subject, int $offset = 0): bool + { + return (bool) Preg::match($pattern, $subject, $matches, 0, $offset); + } + + /** + * @param non-empty-string $pattern + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + */ + public static function match(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchResult + { + self::checkOffsetCapture($flags, 'matchWithOffsets'); + + $count = Preg::match($pattern, $subject, $matches, $flags, $offset); + + return new MatchResult($count, $matches); + } + + /** + * Variant of `match()` which returns non-null matches (or throws) + * + * @param non-empty-string $pattern + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + * @throws UnexpectedNullMatchException + */ + public static function matchStrictGroups(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchStrictGroupsResult + { + // @phpstan-ignore composerPcre.maybeUnsafeStrictGroups + $count = Preg::matchStrictGroups($pattern, $subject, $matches, $flags, $offset); + + return new MatchStrictGroupsResult($count, $matches); + } + + /** + * Runs preg_match with PREG_OFFSET_CAPTURE + * + * @param non-empty-string $pattern + * @param int-mask $flags PREG_UNMATCHED_AS_NULL and PREG_MATCH_OFFSET are always set, no other flags are supported + */ + public static function matchWithOffsets(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchWithOffsetsResult + { + $count = Preg::matchWithOffsets($pattern, $subject, $matches, $flags, $offset); + + return new MatchWithOffsetsResult($count, $matches); + } + + /** + * @param non-empty-string $pattern + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + */ + public static function matchAll(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchAllResult + { + self::checkOffsetCapture($flags, 'matchAllWithOffsets'); + self::checkSetOrder($flags); + + $count = Preg::matchAll($pattern, $subject, $matches, $flags, $offset); + + return new MatchAllResult($count, $matches); + } + + /** + * Variant of `matchAll()` which returns non-null matches (or throws) + * + * @param non-empty-string $pattern + * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported + * @throws UnexpectedNullMatchException + */ + public static function matchAllStrictGroups(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchAllStrictGroupsResult + { + self::checkOffsetCapture($flags, 'matchAllWithOffsets'); + self::checkSetOrder($flags); + + // @phpstan-ignore composerPcre.maybeUnsafeStrictGroups + $count = Preg::matchAllStrictGroups($pattern, $subject, $matches, $flags, $offset); + + return new MatchAllStrictGroupsResult($count, $matches); + } + + /** + * Runs preg_match_all with PREG_OFFSET_CAPTURE + * + * @param non-empty-string $pattern + * @param int-mask $flags PREG_UNMATCHED_AS_NULL and PREG_MATCH_OFFSET are always set, no other flags are supported + */ + public static function matchAllWithOffsets(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchAllWithOffsetsResult + { + self::checkSetOrder($flags); + + $count = Preg::matchAllWithOffsets($pattern, $subject, $matches, $flags, $offset); + + return new MatchAllWithOffsetsResult($count, $matches); + } + /** + * @param string|string[] $pattern + * @param string|string[] $replacement + * @param string $subject + */ + public static function replace($pattern, $replacement, $subject, int $limit = -1): ReplaceResult + { + $result = Preg::replace($pattern, $replacement, $subject, $limit, $count); + + return new ReplaceResult($count, $result); + } + + /** + * @param string|string[] $pattern + * @param ($flags is PREG_OFFSET_CAPTURE ? (callable(array}>): string) : callable(array): string) $replacement + * @param string $subject + * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set + */ + public static function replaceCallback($pattern, callable $replacement, $subject, int $limit = -1, int $flags = 0): ReplaceResult + { + $result = Preg::replaceCallback($pattern, $replacement, $subject, $limit, $count, $flags); + + return new ReplaceResult($count, $result); + } + + /** + * Variant of `replaceCallback()` which outputs non-null matches (or throws) + * + * @param string $pattern + * @param ($flags is PREG_OFFSET_CAPTURE ? (callable(array}>): string) : callable(array): string) $replacement + * @param string $subject + * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set + */ + public static function replaceCallbackStrictGroups($pattern, callable $replacement, $subject, int $limit = -1, int $flags = 0): ReplaceResult + { + $result = Preg::replaceCallbackStrictGroups($pattern, $replacement, $subject, $limit, $count, $flags); + + return new ReplaceResult($count, $result); + } + + /** + * @param ($flags is PREG_OFFSET_CAPTURE ? (array}>): string>) : array): string>) $pattern + * @param string $subject + * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set + */ + public static function replaceCallbackArray(array $pattern, $subject, int $limit = -1, int $flags = 0): ReplaceResult + { + $result = Preg::replaceCallbackArray($pattern, $subject, $limit, $count, $flags); + + return new ReplaceResult($count, $result); + } + + private static function checkOffsetCapture(int $flags, string $useFunctionName): void + { + if (($flags & PREG_OFFSET_CAPTURE) !== 0) { + throw new \InvalidArgumentException('PREG_OFFSET_CAPTURE is not supported as it changes the return type, use '.$useFunctionName.'() instead'); + } + } + + private static function checkSetOrder(int $flags): void + { + if (($flags & PREG_SET_ORDER) !== 0) { + throw new \InvalidArgumentException('PREG_SET_ORDER is not supported as it changes the return type'); + } + } +} diff --git a/vendor/composer/pcre/src/ReplaceResult.php b/vendor/composer/pcre/src/ReplaceResult.php new file mode 100644 index 0000000..3384771 --- /dev/null +++ b/vendor/composer/pcre/src/ReplaceResult.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Pcre; + +final class ReplaceResult +{ + /** + * @readonly + * @var string + */ + public $result; + + /** + * @readonly + * @var 0|positive-int + */ + public $count; + + /** + * @readonly + * @var bool + */ + public $matched; + + /** + * @param 0|positive-int $count + */ + public function __construct(int $count, string $result) + { + $this->count = $count; + $this->matched = (bool) $count; + $this->result = $result; + } +} diff --git a/vendor/composer/pcre/src/UnexpectedNullMatchException.php b/vendor/composer/pcre/src/UnexpectedNullMatchException.php new file mode 100644 index 0000000..f123828 --- /dev/null +++ b/vendor/composer/pcre/src/UnexpectedNullMatchException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Pcre; + +class UnexpectedNullMatchException extends PcreException +{ + public static function fromFunction($function, $pattern) + { + throw new \LogicException('fromFunction should not be called on '.self::class.', use '.PcreException::class); + } +} diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php index 580fa96..d2225c7 100644 --- a/vendor/composer/platform_check.php +++ b/vendor/composer/platform_check.php @@ -19,8 +19,7 @@ echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } - trigger_error( - 'Composer detected issues in your platform: ' . implode(' ', $issues), - E_USER_ERROR + throw new \RuntimeException( + 'Composer detected issues in your platform: ' . implode(' ', $issues) ); } diff --git a/vendor/ezyang/htmlpurifier/VERSION b/vendor/ezyang/htmlpurifier/VERSION index 8643e72..35e3d1b 100644 --- a/vendor/ezyang/htmlpurifier/VERSION +++ b/vendor/ezyang/htmlpurifier/VERSION @@ -1 +1 @@ -4.17.0 \ No newline at end of file +4.19.0 \ No newline at end of file diff --git a/vendor/ezyang/htmlpurifier/composer.json b/vendor/ezyang/htmlpurifier/composer.json index ed46bd5..cfb7151 100644 --- a/vendor/ezyang/htmlpurifier/composer.json +++ b/vendor/ezyang/htmlpurifier/composer.json @@ -13,7 +13,7 @@ } ], "require": { - "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" }, "require-dev": { "cerdic/css-tidy": "^1.7 || ^2.0", diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.includes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.includes.php index 77ebf2d..1f99a4a 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.includes.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.includes.php @@ -7,7 +7,7 @@ * primary concern and you are using an opcode cache. PLEASE DO NOT EDIT THIS * FILE, changes will be overwritten the next time the script is run. * - * @version 4.17.0 + * @version 4.19.0 * * @warning * You must *not* include any other HTML Purifier files before this file, @@ -101,6 +101,7 @@ require 'HTMLPurifier/AttrDef/CSS/ListStyle.php'; require 'HTMLPurifier/AttrDef/CSS/Multiple.php'; require 'HTMLPurifier/AttrDef/CSS/Percentage.php'; +require 'HTMLPurifier/AttrDef/CSS/Ratio.php'; require 'HTMLPurifier/AttrDef/CSS/TextDecoration.php'; require 'HTMLPurifier/AttrDef/CSS/URI.php'; require 'HTMLPurifier/AttrDef/HTML/Bool.php'; diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.php index 5c14a33..31b5a0f 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.php @@ -19,7 +19,7 @@ */ /* - HTML Purifier 4.17.0 - Standards Compliant HTML Filtering + HTML Purifier 4.19.0 - Standards Compliant HTML Filtering Copyright (C) 2006-2008 Edward Z. Yang This library is free software; you can redistribute it and/or @@ -58,12 +58,12 @@ class HTMLPurifier * Version of HTML Purifier. * @type string */ - public $version = '4.17.0'; + public $version = '4.19.0'; /** * Constant with version of HTML Purifier. */ - const VERSION = '4.17.0'; + const VERSION = '4.19.0'; /** * Global configuration object. diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.safe-includes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.safe-includes.php index 94543f5..8a417d2 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.safe-includes.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.safe-includes.php @@ -95,6 +95,7 @@ require_once $__dir . '/HTMLPurifier/AttrDef/CSS/ListStyle.php'; require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Multiple.php'; require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Percentage.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Ratio.php'; require_once $__dir . '/HTMLPurifier/AttrDef/CSS/TextDecoration.php'; require_once $__dir . '/HTMLPurifier/AttrDef/CSS/URI.php'; require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Bool.php'; diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php index ad2cb90..af6b8a0 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php @@ -27,6 +27,13 @@ public function validate($css, $config, $context) $definition = $config->getCSSDefinition(); $allow_duplicates = $config->get("CSS.AllowDuplicates"); + $universal_attrdef = new HTMLPurifier_AttrDef_Enum( + array( + 'initial', + 'inherit', + 'unset', + ) + ); // According to the CSS2.1 spec, the places where a // non-delimiting semicolon can appear are in strings @@ -96,16 +103,13 @@ public function validate($css, $config, $context) if (!$ok) { continue; } - // inefficient call, since the validator will do this again - if (strtolower(trim($value)) !== 'inherit') { - // inherit works for everything (but only on the base property) + $result = $universal_attrdef->validate($value, $config, $context); + if ($result === false) { $result = $definition->info[$property]->validate( $value, $config, $context ); - } else { - $result = 'inherit'; } if ($result === false) { continue; diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php index f1ff116..799166b 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php @@ -195,7 +195,7 @@ public function validate($string, $config, $context) // transforms don't pose a security risk (as \\ and \" // might--these escapes are not supported by most browsers). // We could try to be clever and use single-quote wrapping - // when there is a double quote present, but I have choosen + // when there is a double quote present, but I have chosen // not to implement that. (NOTE: you can reduce the amount // of escapes by one depending on what quoting style you use) // $font = str_replace('\\', '\\5C ', $font); diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ratio.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ratio.php new file mode 100644 index 0000000..e08e2c4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ratio.php @@ -0,0 +1,46 @@ +parseCDATA($ratio); + + $parts = explode('/', $ratio, 2); + $length = count($parts); + + if ($length < 1 || $length > 2) { + return false; + } + + $num = new \HTMLPurifier_AttrDef_CSS_Number(); + + if ($length === 1) { + return $num->validate($parts[0], $config, $context); + } + + $num1 = $num->validate($parts[0], $config, $context); + $num2 = $num->validate($parts[1], $config, $context); + + if ($num1 === false || $num2 === false) { + return false; + } + + return $num1 . '/' . $num2; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php index 63fa04c..3decf0c 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php @@ -25,12 +25,7 @@ public function __construct($name) 'rev' => 'AllowedRev' ); if (!isset($configLookup[$name])) { - trigger_error( - 'Unrecognized attribute name for link ' . - 'relationship.', - E_USER_ERROR - ); - return; + throw new Exception('Unrecognized attribute name for link relationship.'); } $this->name = $configLookup[$name]; } diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php index ddc5dfb..17a97c1 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php @@ -63,24 +63,18 @@ public function validate($string, $config, $context) // This doesn't match I18N domain names, but we don't have proper IRI support, // so force users to insert Punycode. - // There is not a good sense in which underscores should be - // allowed, since it's technically not! (And if you go as - // far to allow everything as specified by the DNS spec... - // well, that's literally everything, modulo some space limits - // for the components and the overall name (which, by the way, - // we are NOT checking!). So we (arbitrarily) decide this: - // let's allow underscores wherever we would have allowed - // hyphens, if they are enabled. This is a pretty good match - // for browser behavior, for example, a large number of browsers - // cannot handle foo_.example.com, but foo_bar.example.com is - // fairly well supported. + // Underscores defined as Unreserved Characters in RFC 3986 are + // allowed in a URI. There are cases where we want to consider a + // URI containing "_" such as "_dmarc.example.com". + // Underscores are not allowed in the default. If you want to + // allow it, set Core.AllowHostnameUnderscore to true. $underscore = $config->get('Core.AllowHostnameUnderscore') ? '_' : ''; // Based off of RFC 1738, but amended so that // as per RFC 3696, the top label need only not be all numeric. // The productions describing this are: $a = '[a-z]'; // alpha - $an = '[a-z0-9]'; // alphanum + $an = "[a-z0-9$underscore]"; // alphanum $and = "[a-z0-9-$underscore]"; // alphanum | "-" // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum $domainlabel = "$an(?:$and*$an)?"; diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php index f243793..dc4ef62 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php @@ -37,7 +37,7 @@ public function validate($aIP, $config, $context) } } - // IPv4-compatiblity check + // IPv4-compatibility check if (preg_match('#(?<=:' . ')' . $this->ip4 . '$#s', $aIP, $find)) { $aIP = substr($aIP, 0, 0 - strlen($find[0])); $ip = explode('.', $find[0]); diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php index d66c04a..d769c6f 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php @@ -3,7 +3,7 @@ // this MUST be placed in post, as it assumes that any value in dir is valid /** - * Post-trasnform that ensures that bdo tags have the dir attribute set. + * Post-transform that ensures that bdo tags have the dir attribute set. */ class HTMLPurifier_AttrTransform_BdoDir extends HTMLPurifier_AttrTransform { diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php index e4429e8..62575ca 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php @@ -77,7 +77,7 @@ public function get($type) } if (!isset($this->info[$type])) { - trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR); + throw new Exception('Cannot retrieve undefined attribute type ' . $type); return; } return $this->info[$type]->make($string); diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php index f97dc93..350330b 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php @@ -135,7 +135,7 @@ public function validateToken($token, $config, $context) // we'd also want slightly more complicated substitution // involving an array as the return value, // although we're not sure how colliding attributes would - // resolve (certain ones would be completely overriden, + // resolve (certain ones would be completely overridden, // others would prepend themselves). } diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php index bd8f998..8805ecc 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php @@ -5,7 +5,7 @@ define('HTMLPURIFIER_PREFIX', realpath(dirname(__FILE__) . '/..')); } -// accomodations for versions earlier than 5.0.2 +// accommodations for versions earlier than 5.0.2 // borrowed from PHP_Compat, LGPL licensed, by Aidan Lister if (!defined('PHP_EOL')) { switch (strtoupper(substr(PHP_OS, 0, 3))) { diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php index 1bc419c..923d6f3 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php @@ -26,6 +26,11 @@ protected function doSetup($config) false ); + $this->info['direction'] = new HTMLPurifier_AttrDef_Enum( + ['ltr', 'rtl'], + false + ); + $border_style = $this->info['border-bottom-style'] = $this->info['border-right-style'] = @@ -116,8 +121,6 @@ protected function doSetup($config) 'auto', 'cover', 'contain', - 'initial', - 'inherit', ] ), new HTMLPurifier_AttrDef_CSS_Percentage(), @@ -236,21 +239,20 @@ protected function doSetup($config) [ new HTMLPurifier_AttrDef_CSS_Length('0'), new HTMLPurifier_AttrDef_CSS_Percentage(true), - new HTMLPurifier_AttrDef_Enum(['auto', 'initial', 'inherit']) + new HTMLPurifier_AttrDef_Enum(['auto']) ] ); $trusted_min_wh = new HTMLPurifier_AttrDef_CSS_Composite( [ new HTMLPurifier_AttrDef_CSS_Length('0'), new HTMLPurifier_AttrDef_CSS_Percentage(true), - new HTMLPurifier_AttrDef_Enum(['initial', 'inherit']) ] ); $trusted_max_wh = new HTMLPurifier_AttrDef_CSS_Composite( [ new HTMLPurifier_AttrDef_CSS_Length('0'), new HTMLPurifier_AttrDef_CSS_Percentage(true), - new HTMLPurifier_AttrDef_Enum(['none', 'initial', 'inherit']) + new HTMLPurifier_AttrDef_Enum(['none']) ] ); $max = $config->get('CSS.MaxImgLength'); @@ -278,12 +280,7 @@ protected function doSetup($config) new HTMLPurifier_AttrDef_Switch( 'img', // For img tags: - new HTMLPurifier_AttrDef_CSS_Composite( - [ - new HTMLPurifier_AttrDef_CSS_Length('0', $max), - new HTMLPurifier_AttrDef_Enum(['initial', 'inherit']) - ] - ), + new HTMLPurifier_AttrDef_CSS_Length('0', $max), // For everyone else: $trusted_min_wh ); @@ -297,22 +294,29 @@ protected function doSetup($config) new HTMLPurifier_AttrDef_CSS_Composite( [ new HTMLPurifier_AttrDef_CSS_Length('0', $max), - new HTMLPurifier_AttrDef_Enum(['none', 'initial', 'inherit']) + new HTMLPurifier_AttrDef_Enum(['none']) ] ), // For everyone else: $trusted_max_wh ); + $this->info['aspect-ratio'] = new HTMLPurifier_AttrDef_CSS_Multiple( + new HTMLPurifier_AttrDef_CSS_Composite([ + new HTMLPurifier_AttrDef_CSS_Ratio(), + new HTMLPurifier_AttrDef_Enum(['auto']), + ]) + ); + // text-decoration and related shorthands $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration(); $this->info['text-decoration-line'] = new HTMLPurifier_AttrDef_Enum( - ['none', 'underline', 'overline', 'line-through', 'initial', 'inherit'] + ['none', 'underline', 'overline', 'line-through'] ); $this->info['text-decoration-style'] = new HTMLPurifier_AttrDef_Enum( - ['solid', 'double', 'dotted', 'dashed', 'wavy', 'initial', 'inherit'] + ['solid', 'double', 'dotted', 'dashed', 'wavy'] ); $this->info['text-decoration-color'] = new HTMLPurifier_AttrDef_CSS_Color(); @@ -320,7 +324,7 @@ protected function doSetup($config) $this->info['text-decoration-thickness'] = new HTMLPurifier_AttrDef_CSS_Composite([ new HTMLPurifier_AttrDef_CSS_Length(), new HTMLPurifier_AttrDef_CSS_Percentage(), - new HTMLPurifier_AttrDef_Enum(['auto', 'from-font', 'initial', 'inherit']) + new HTMLPurifier_AttrDef_Enum(['auto', 'from-font']) ]); $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily(); diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php index 67c7e95..d92205b 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php @@ -190,6 +190,9 @@ public function validateChildren($children, $config, $context) $current_tr_tbody = null; foreach($content as $node) { + if (!isset($node->name)) { + continue; + } switch ($node->name) { case 'tbody': $current_tr_tbody = null; diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Config.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Config.php index f7511ca..256408e 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Config.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Config.php @@ -21,7 +21,7 @@ class HTMLPurifier_Config * HTML Purifier's version * @type string */ - public $version = '4.17.0'; + public $version = '4.19.0'; /** * Whether or not to automatically finalize @@ -898,7 +898,11 @@ protected function triggerError($msg, $no) break; } } - trigger_error($msg . $extra, $no); + if ($no == E_USER_ERROR) { + throw new Exception($msg . $extra); + } else { + trigger_error($msg . $extra, $no); + } } /** diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php index c3fe8cd..42f6604 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php @@ -72,7 +72,7 @@ public static function makeFromSerial() $r = unserialize($contents); if (!$r) { $hash = sha1($contents); - trigger_error("Unserialization of configuration schema failed, sha1 of file was $hash", E_USER_ERROR); + throw new Exception("Unserialization of configuration schema failed, sha1 of file was $hash"); } return $r; } diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php index 127a39a..4902a56 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php @@ -66,7 +66,7 @@ class HTMLPurifier_ConfigSchema_Interchange_Directive public $version; /** - * ID of directive that supercedes this old directive. + * ID of directive that supersedes this old directive. * Null if not deprecated. * @type HTMLPurifier_ConfigSchema_Interchange_Id */ diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema.ser b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema.ser index a5426c7..34ea683 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema.ser +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema.ser @@ -1 +1 @@ -O:25:"HTMLPurifier_ConfigSchema":3:{s:8:"defaults";a:127:{s:19:"Attr.AllowedClasses";N;s:24:"Attr.AllowedFrameTargets";a:0:{}s:15:"Attr.AllowedRel";a:0:{}s:15:"Attr.AllowedRev";a:0:{}s:18:"Attr.ClassUseCDATA";N;s:20:"Attr.DefaultImageAlt";N;s:24:"Attr.DefaultInvalidImage";s:0:"";s:27:"Attr.DefaultInvalidImageAlt";s:13:"Invalid image";s:19:"Attr.DefaultTextDir";s:3:"ltr";s:13:"Attr.EnableID";b:0;s:21:"Attr.ForbiddenClasses";a:0:{}s:13:"Attr.ID.HTML5";N;s:16:"Attr.IDBlacklist";a:0:{}s:22:"Attr.IDBlacklistRegexp";N;s:13:"Attr.IDPrefix";s:0:"";s:18:"Attr.IDPrefixLocal";s:0:"";s:24:"AutoFormat.AutoParagraph";b:0;s:17:"AutoFormat.Custom";a:0:{}s:25:"AutoFormat.DisplayLinkURI";b:0;s:18:"AutoFormat.Linkify";b:0;s:33:"AutoFormat.PurifierLinkify.DocURL";s:3:"#%s";s:26:"AutoFormat.PurifierLinkify";b:0;s:32:"AutoFormat.RemoveEmpty.Predicate";a:4:{s:8:"colgroup";a:0:{}s:2:"th";a:0:{}s:2:"td";a:0:{}s:6:"iframe";a:1:{i:0;s:3:"src";}}s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";a:2:{s:2:"td";b:1;s:2:"th";b:1;}s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";b:0;s:22:"AutoFormat.RemoveEmpty";b:0;s:39:"AutoFormat.RemoveSpansWithoutAttributes";b:0;s:19:"CSS.AllowDuplicates";b:0;s:18:"CSS.AllowImportant";b:0;s:15:"CSS.AllowTricky";b:0;s:16:"CSS.AllowedFonts";N;s:21:"CSS.AllowedProperties";N;s:17:"CSS.DefinitionRev";i:1;s:23:"CSS.ForbiddenProperties";a:0:{}s:16:"CSS.MaxImgLength";s:6:"1200px";s:15:"CSS.Proprietary";b:0;s:11:"CSS.Trusted";b:0;s:20:"Cache.DefinitionImpl";s:10:"Serializer";s:20:"Cache.SerializerPath";N;s:27:"Cache.SerializerPermissions";i:493;s:22:"Core.AggressivelyFixLt";b:1;s:29:"Core.AggressivelyRemoveScript";b:1;s:28:"Core.AllowHostnameUnderscore";b:0;s:23:"Core.AllowParseManyTags";b:0;s:18:"Core.CollectErrors";b:0;s:18:"Core.ColorKeywords";a:148:{s:9:"aliceblue";s:7:"#F0F8FF";s:12:"antiquewhite";s:7:"#FAEBD7";s:4:"aqua";s:7:"#00FFFF";s:10:"aquamarine";s:7:"#7FFFD4";s:5:"azure";s:7:"#F0FFFF";s:5:"beige";s:7:"#F5F5DC";s:6:"bisque";s:7:"#FFE4C4";s:5:"black";s:7:"#000000";s:14:"blanchedalmond";s:7:"#FFEBCD";s:4:"blue";s:7:"#0000FF";s:10:"blueviolet";s:7:"#8A2BE2";s:5:"brown";s:7:"#A52A2A";s:9:"burlywood";s:7:"#DEB887";s:9:"cadetblue";s:7:"#5F9EA0";s:10:"chartreuse";s:7:"#7FFF00";s:9:"chocolate";s:7:"#D2691E";s:5:"coral";s:7:"#FF7F50";s:14:"cornflowerblue";s:7:"#6495ED";s:8:"cornsilk";s:7:"#FFF8DC";s:7:"crimson";s:7:"#DC143C";s:4:"cyan";s:7:"#00FFFF";s:8:"darkblue";s:7:"#00008B";s:8:"darkcyan";s:7:"#008B8B";s:13:"darkgoldenrod";s:7:"#B8860B";s:8:"darkgray";s:7:"#A9A9A9";s:8:"darkgrey";s:7:"#A9A9A9";s:9:"darkgreen";s:7:"#006400";s:9:"darkkhaki";s:7:"#BDB76B";s:11:"darkmagenta";s:7:"#8B008B";s:14:"darkolivegreen";s:7:"#556B2F";s:10:"darkorange";s:7:"#FF8C00";s:10:"darkorchid";s:7:"#9932CC";s:7:"darkred";s:7:"#8B0000";s:10:"darksalmon";s:7:"#E9967A";s:12:"darkseagreen";s:7:"#8FBC8F";s:13:"darkslateblue";s:7:"#483D8B";s:13:"darkslategray";s:7:"#2F4F4F";s:13:"darkslategrey";s:7:"#2F4F4F";s:13:"darkturquoise";s:7:"#00CED1";s:10:"darkviolet";s:7:"#9400D3";s:8:"deeppink";s:7:"#FF1493";s:11:"deepskyblue";s:7:"#00BFFF";s:7:"dimgray";s:7:"#696969";s:7:"dimgrey";s:7:"#696969";s:10:"dodgerblue";s:7:"#1E90FF";s:9:"firebrick";s:7:"#B22222";s:11:"floralwhite";s:7:"#FFFAF0";s:11:"forestgreen";s:7:"#228B22";s:7:"fuchsia";s:7:"#FF00FF";s:9:"gainsboro";s:7:"#DCDCDC";s:10:"ghostwhite";s:7:"#F8F8FF";s:4:"gold";s:7:"#FFD700";s:9:"goldenrod";s:7:"#DAA520";s:4:"gray";s:7:"#808080";s:4:"grey";s:7:"#808080";s:5:"green";s:7:"#008000";s:11:"greenyellow";s:7:"#ADFF2F";s:8:"honeydew";s:7:"#F0FFF0";s:7:"hotpink";s:7:"#FF69B4";s:9:"indianred";s:7:"#CD5C5C";s:6:"indigo";s:7:"#4B0082";s:5:"ivory";s:7:"#FFFFF0";s:5:"khaki";s:7:"#F0E68C";s:8:"lavender";s:7:"#E6E6FA";s:13:"lavenderblush";s:7:"#FFF0F5";s:9:"lawngreen";s:7:"#7CFC00";s:12:"lemonchiffon";s:7:"#FFFACD";s:9:"lightblue";s:7:"#ADD8E6";s:10:"lightcoral";s:7:"#F08080";s:9:"lightcyan";s:7:"#E0FFFF";s:20:"lightgoldenrodyellow";s:7:"#FAFAD2";s:9:"lightgray";s:7:"#D3D3D3";s:9:"lightgrey";s:7:"#D3D3D3";s:10:"lightgreen";s:7:"#90EE90";s:9:"lightpink";s:7:"#FFB6C1";s:11:"lightsalmon";s:7:"#FFA07A";s:13:"lightseagreen";s:7:"#20B2AA";s:12:"lightskyblue";s:7:"#87CEFA";s:14:"lightslategray";s:7:"#778899";s:14:"lightslategrey";s:7:"#778899";s:14:"lightsteelblue";s:7:"#B0C4DE";s:11:"lightyellow";s:7:"#FFFFE0";s:4:"lime";s:7:"#00FF00";s:9:"limegreen";s:7:"#32CD32";s:5:"linen";s:7:"#FAF0E6";s:7:"magenta";s:7:"#FF00FF";s:6:"maroon";s:7:"#800000";s:16:"mediumaquamarine";s:7:"#66CDAA";s:10:"mediumblue";s:7:"#0000CD";s:12:"mediumorchid";s:7:"#BA55D3";s:12:"mediumpurple";s:7:"#9370DB";s:14:"mediumseagreen";s:7:"#3CB371";s:15:"mediumslateblue";s:7:"#7B68EE";s:17:"mediumspringgreen";s:7:"#00FA9A";s:15:"mediumturquoise";s:7:"#48D1CC";s:15:"mediumvioletred";s:7:"#C71585";s:12:"midnightblue";s:7:"#191970";s:9:"mintcream";s:7:"#F5FFFA";s:9:"mistyrose";s:7:"#FFE4E1";s:8:"moccasin";s:7:"#FFE4B5";s:11:"navajowhite";s:7:"#FFDEAD";s:4:"navy";s:7:"#000080";s:7:"oldlace";s:7:"#FDF5E6";s:5:"olive";s:7:"#808000";s:9:"olivedrab";s:7:"#6B8E23";s:6:"orange";s:7:"#FFA500";s:9:"orangered";s:7:"#FF4500";s:6:"orchid";s:7:"#DA70D6";s:13:"palegoldenrod";s:7:"#EEE8AA";s:9:"palegreen";s:7:"#98FB98";s:13:"paleturquoise";s:7:"#AFEEEE";s:13:"palevioletred";s:7:"#DB7093";s:10:"papayawhip";s:7:"#FFEFD5";s:9:"peachpuff";s:7:"#FFDAB9";s:4:"peru";s:7:"#CD853F";s:4:"pink";s:7:"#FFC0CB";s:4:"plum";s:7:"#DDA0DD";s:10:"powderblue";s:7:"#B0E0E6";s:6:"purple";s:7:"#800080";s:13:"rebeccapurple";s:7:"#663399";s:3:"red";s:7:"#FF0000";s:9:"rosybrown";s:7:"#BC8F8F";s:9:"royalblue";s:7:"#4169E1";s:11:"saddlebrown";s:7:"#8B4513";s:6:"salmon";s:7:"#FA8072";s:10:"sandybrown";s:7:"#F4A460";s:8:"seagreen";s:7:"#2E8B57";s:8:"seashell";s:7:"#FFF5EE";s:6:"sienna";s:7:"#A0522D";s:6:"silver";s:7:"#C0C0C0";s:7:"skyblue";s:7:"#87CEEB";s:9:"slateblue";s:7:"#6A5ACD";s:9:"slategray";s:7:"#708090";s:9:"slategrey";s:7:"#708090";s:4:"snow";s:7:"#FFFAFA";s:11:"springgreen";s:7:"#00FF7F";s:9:"steelblue";s:7:"#4682B4";s:3:"tan";s:7:"#D2B48C";s:4:"teal";s:7:"#008080";s:7:"thistle";s:7:"#D8BFD8";s:6:"tomato";s:7:"#FF6347";s:9:"turquoise";s:7:"#40E0D0";s:6:"violet";s:7:"#EE82EE";s:5:"wheat";s:7:"#F5DEB3";s:5:"white";s:7:"#FFFFFF";s:10:"whitesmoke";s:7:"#F5F5F5";s:6:"yellow";s:7:"#FFFF00";s:11:"yellowgreen";s:7:"#9ACD32";}s:30:"Core.ConvertDocumentToFragment";b:1;s:36:"Core.DirectLexLineNumberSyncInterval";i:0;s:20:"Core.DisableExcludes";b:0;s:15:"Core.EnableIDNA";b:0;s:13:"Core.Encoding";s:5:"utf-8";s:26:"Core.EscapeInvalidChildren";b:0;s:22:"Core.EscapeInvalidTags";b:0;s:29:"Core.EscapeNonASCIICharacters";b:0;s:19:"Core.HiddenElements";a:2:{s:6:"script";b:1;s:5:"style";b:1;}s:13:"Core.Language";s:2:"en";s:24:"Core.LegacyEntityDecoder";b:0;s:14:"Core.LexerImpl";N;s:24:"Core.MaintainLineNumbers";N;s:22:"Core.NormalizeNewlines";b:1;s:21:"Core.RemoveInvalidImg";b:1;s:33:"Core.RemoveProcessingInstructions";b:0;s:25:"Core.RemoveScriptContents";N;s:13:"Filter.Custom";a:0:{}s:34:"Filter.ExtractStyleBlocks.Escaping";b:1;s:31:"Filter.ExtractStyleBlocks.Scope";N;s:34:"Filter.ExtractStyleBlocks.TidyImpl";N;s:25:"Filter.ExtractStyleBlocks";b:0;s:14:"Filter.YouTube";b:0;s:12:"HTML.Allowed";N;s:22:"HTML.AllowedAttributes";N;s:20:"HTML.AllowedComments";a:0:{}s:26:"HTML.AllowedCommentsRegexp";N;s:20:"HTML.AllowedElements";N;s:19:"HTML.AllowedModules";N;s:23:"HTML.Attr.Name.UseCDATA";b:0;s:17:"HTML.BlockWrapper";s:1:"p";s:16:"HTML.CoreModules";a:7:{s:9:"Structure";b:1;s:4:"Text";b:1;s:9:"Hypertext";b:1;s:4:"List";b:1;s:22:"NonXMLCommonAttributes";b:1;s:19:"XMLCommonAttributes";b:1;s:16:"CommonAttributes";b:1;}s:18:"HTML.CustomDoctype";N;s:17:"HTML.DefinitionID";N;s:18:"HTML.DefinitionRev";i:1;s:12:"HTML.Doctype";N;s:25:"HTML.FlashAllowFullScreen";b:0;s:24:"HTML.ForbiddenAttributes";a:0:{}s:22:"HTML.ForbiddenElements";a:0:{}s:10:"HTML.Forms";b:0;s:17:"HTML.MaxImgLength";i:1200;s:13:"HTML.Nofollow";b:0;s:11:"HTML.Parent";s:3:"div";s:16:"HTML.Proprietary";b:0;s:14:"HTML.SafeEmbed";b:0;s:15:"HTML.SafeIframe";b:0;s:15:"HTML.SafeObject";b:0;s:18:"HTML.SafeScripting";a:0:{}s:11:"HTML.Strict";b:0;s:16:"HTML.TargetBlank";b:0;s:19:"HTML.TargetNoopener";b:1;s:21:"HTML.TargetNoreferrer";b:1;s:12:"HTML.TidyAdd";a:0:{}s:14:"HTML.TidyLevel";s:6:"medium";s:15:"HTML.TidyRemove";a:0:{}s:12:"HTML.Trusted";b:0;s:10:"HTML.XHTML";b:1;s:28:"Output.CommentScriptContents";b:1;s:19:"Output.FixInnerHTML";b:1;s:18:"Output.FlashCompat";b:0;s:14:"Output.Newline";N;s:15:"Output.SortAttr";b:0;s:17:"Output.TidyFormat";b:0;s:17:"Test.ForceNoIconv";b:0;s:18:"URI.AllowedSchemes";a:7:{s:4:"http";b:1;s:5:"https";b:1;s:6:"mailto";b:1;s:3:"ftp";b:1;s:4:"nntp";b:1;s:4:"news";b:1;s:3:"tel";b:1;}s:8:"URI.Base";N;s:17:"URI.DefaultScheme";s:4:"http";s:16:"URI.DefinitionID";N;s:17:"URI.DefinitionRev";i:1;s:11:"URI.Disable";b:0;s:19:"URI.DisableExternal";b:0;s:28:"URI.DisableExternalResources";b:0;s:20:"URI.DisableResources";b:0;s:8:"URI.Host";N;s:17:"URI.HostBlacklist";a:0:{}s:16:"URI.MakeAbsolute";b:0;s:9:"URI.Munge";N;s:18:"URI.MungeResources";b:0;s:18:"URI.MungeSecretKey";N;s:26:"URI.OverrideAllowedSchemes";b:1;s:20:"URI.SafeIframeRegexp";N;}s:12:"defaultPlist";O:25:"HTMLPurifier_PropertyList":3:{s:7:"*data";a:127:{s:19:"Attr.AllowedClasses";N;s:24:"Attr.AllowedFrameTargets";a:0:{}s:15:"Attr.AllowedRel";a:0:{}s:15:"Attr.AllowedRev";a:0:{}s:18:"Attr.ClassUseCDATA";N;s:20:"Attr.DefaultImageAlt";N;s:24:"Attr.DefaultInvalidImage";s:0:"";s:27:"Attr.DefaultInvalidImageAlt";s:13:"Invalid image";s:19:"Attr.DefaultTextDir";s:3:"ltr";s:13:"Attr.EnableID";b:0;s:21:"Attr.ForbiddenClasses";a:0:{}s:13:"Attr.ID.HTML5";N;s:16:"Attr.IDBlacklist";a:0:{}s:22:"Attr.IDBlacklistRegexp";N;s:13:"Attr.IDPrefix";s:0:"";s:18:"Attr.IDPrefixLocal";s:0:"";s:24:"AutoFormat.AutoParagraph";b:0;s:17:"AutoFormat.Custom";a:0:{}s:25:"AutoFormat.DisplayLinkURI";b:0;s:18:"AutoFormat.Linkify";b:0;s:33:"AutoFormat.PurifierLinkify.DocURL";s:3:"#%s";s:26:"AutoFormat.PurifierLinkify";b:0;s:32:"AutoFormat.RemoveEmpty.Predicate";a:4:{s:8:"colgroup";a:0:{}s:2:"th";a:0:{}s:2:"td";a:0:{}s:6:"iframe";a:1:{i:0;s:3:"src";}}s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";a:2:{s:2:"td";b:1;s:2:"th";b:1;}s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";b:0;s:22:"AutoFormat.RemoveEmpty";b:0;s:39:"AutoFormat.RemoveSpansWithoutAttributes";b:0;s:19:"CSS.AllowDuplicates";b:0;s:18:"CSS.AllowImportant";b:0;s:15:"CSS.AllowTricky";b:0;s:16:"CSS.AllowedFonts";N;s:21:"CSS.AllowedProperties";N;s:17:"CSS.DefinitionRev";i:1;s:23:"CSS.ForbiddenProperties";a:0:{}s:16:"CSS.MaxImgLength";s:6:"1200px";s:15:"CSS.Proprietary";b:0;s:11:"CSS.Trusted";b:0;s:20:"Cache.DefinitionImpl";s:10:"Serializer";s:20:"Cache.SerializerPath";N;s:27:"Cache.SerializerPermissions";i:493;s:22:"Core.AggressivelyFixLt";b:1;s:29:"Core.AggressivelyRemoveScript";b:1;s:28:"Core.AllowHostnameUnderscore";b:0;s:23:"Core.AllowParseManyTags";b:0;s:18:"Core.CollectErrors";b:0;s:18:"Core.ColorKeywords";a:148:{s:9:"aliceblue";s:7:"#F0F8FF";s:12:"antiquewhite";s:7:"#FAEBD7";s:4:"aqua";s:7:"#00FFFF";s:10:"aquamarine";s:7:"#7FFFD4";s:5:"azure";s:7:"#F0FFFF";s:5:"beige";s:7:"#F5F5DC";s:6:"bisque";s:7:"#FFE4C4";s:5:"black";s:7:"#000000";s:14:"blanchedalmond";s:7:"#FFEBCD";s:4:"blue";s:7:"#0000FF";s:10:"blueviolet";s:7:"#8A2BE2";s:5:"brown";s:7:"#A52A2A";s:9:"burlywood";s:7:"#DEB887";s:9:"cadetblue";s:7:"#5F9EA0";s:10:"chartreuse";s:7:"#7FFF00";s:9:"chocolate";s:7:"#D2691E";s:5:"coral";s:7:"#FF7F50";s:14:"cornflowerblue";s:7:"#6495ED";s:8:"cornsilk";s:7:"#FFF8DC";s:7:"crimson";s:7:"#DC143C";s:4:"cyan";s:7:"#00FFFF";s:8:"darkblue";s:7:"#00008B";s:8:"darkcyan";s:7:"#008B8B";s:13:"darkgoldenrod";s:7:"#B8860B";s:8:"darkgray";s:7:"#A9A9A9";s:8:"darkgrey";s:7:"#A9A9A9";s:9:"darkgreen";s:7:"#006400";s:9:"darkkhaki";s:7:"#BDB76B";s:11:"darkmagenta";s:7:"#8B008B";s:14:"darkolivegreen";s:7:"#556B2F";s:10:"darkorange";s:7:"#FF8C00";s:10:"darkorchid";s:7:"#9932CC";s:7:"darkred";s:7:"#8B0000";s:10:"darksalmon";s:7:"#E9967A";s:12:"darkseagreen";s:7:"#8FBC8F";s:13:"darkslateblue";s:7:"#483D8B";s:13:"darkslategray";s:7:"#2F4F4F";s:13:"darkslategrey";s:7:"#2F4F4F";s:13:"darkturquoise";s:7:"#00CED1";s:10:"darkviolet";s:7:"#9400D3";s:8:"deeppink";s:7:"#FF1493";s:11:"deepskyblue";s:7:"#00BFFF";s:7:"dimgray";s:7:"#696969";s:7:"dimgrey";s:7:"#696969";s:10:"dodgerblue";s:7:"#1E90FF";s:9:"firebrick";s:7:"#B22222";s:11:"floralwhite";s:7:"#FFFAF0";s:11:"forestgreen";s:7:"#228B22";s:7:"fuchsia";s:7:"#FF00FF";s:9:"gainsboro";s:7:"#DCDCDC";s:10:"ghostwhite";s:7:"#F8F8FF";s:4:"gold";s:7:"#FFD700";s:9:"goldenrod";s:7:"#DAA520";s:4:"gray";s:7:"#808080";s:4:"grey";s:7:"#808080";s:5:"green";s:7:"#008000";s:11:"greenyellow";s:7:"#ADFF2F";s:8:"honeydew";s:7:"#F0FFF0";s:7:"hotpink";s:7:"#FF69B4";s:9:"indianred";s:7:"#CD5C5C";s:6:"indigo";s:7:"#4B0082";s:5:"ivory";s:7:"#FFFFF0";s:5:"khaki";s:7:"#F0E68C";s:8:"lavender";s:7:"#E6E6FA";s:13:"lavenderblush";s:7:"#FFF0F5";s:9:"lawngreen";s:7:"#7CFC00";s:12:"lemonchiffon";s:7:"#FFFACD";s:9:"lightblue";s:7:"#ADD8E6";s:10:"lightcoral";s:7:"#F08080";s:9:"lightcyan";s:7:"#E0FFFF";s:20:"lightgoldenrodyellow";s:7:"#FAFAD2";s:9:"lightgray";s:7:"#D3D3D3";s:9:"lightgrey";s:7:"#D3D3D3";s:10:"lightgreen";s:7:"#90EE90";s:9:"lightpink";s:7:"#FFB6C1";s:11:"lightsalmon";s:7:"#FFA07A";s:13:"lightseagreen";s:7:"#20B2AA";s:12:"lightskyblue";s:7:"#87CEFA";s:14:"lightslategray";s:7:"#778899";s:14:"lightslategrey";s:7:"#778899";s:14:"lightsteelblue";s:7:"#B0C4DE";s:11:"lightyellow";s:7:"#FFFFE0";s:4:"lime";s:7:"#00FF00";s:9:"limegreen";s:7:"#32CD32";s:5:"linen";s:7:"#FAF0E6";s:7:"magenta";s:7:"#FF00FF";s:6:"maroon";s:7:"#800000";s:16:"mediumaquamarine";s:7:"#66CDAA";s:10:"mediumblue";s:7:"#0000CD";s:12:"mediumorchid";s:7:"#BA55D3";s:12:"mediumpurple";s:7:"#9370DB";s:14:"mediumseagreen";s:7:"#3CB371";s:15:"mediumslateblue";s:7:"#7B68EE";s:17:"mediumspringgreen";s:7:"#00FA9A";s:15:"mediumturquoise";s:7:"#48D1CC";s:15:"mediumvioletred";s:7:"#C71585";s:12:"midnightblue";s:7:"#191970";s:9:"mintcream";s:7:"#F5FFFA";s:9:"mistyrose";s:7:"#FFE4E1";s:8:"moccasin";s:7:"#FFE4B5";s:11:"navajowhite";s:7:"#FFDEAD";s:4:"navy";s:7:"#000080";s:7:"oldlace";s:7:"#FDF5E6";s:5:"olive";s:7:"#808000";s:9:"olivedrab";s:7:"#6B8E23";s:6:"orange";s:7:"#FFA500";s:9:"orangered";s:7:"#FF4500";s:6:"orchid";s:7:"#DA70D6";s:13:"palegoldenrod";s:7:"#EEE8AA";s:9:"palegreen";s:7:"#98FB98";s:13:"paleturquoise";s:7:"#AFEEEE";s:13:"palevioletred";s:7:"#DB7093";s:10:"papayawhip";s:7:"#FFEFD5";s:9:"peachpuff";s:7:"#FFDAB9";s:4:"peru";s:7:"#CD853F";s:4:"pink";s:7:"#FFC0CB";s:4:"plum";s:7:"#DDA0DD";s:10:"powderblue";s:7:"#B0E0E6";s:6:"purple";s:7:"#800080";s:13:"rebeccapurple";s:7:"#663399";s:3:"red";s:7:"#FF0000";s:9:"rosybrown";s:7:"#BC8F8F";s:9:"royalblue";s:7:"#4169E1";s:11:"saddlebrown";s:7:"#8B4513";s:6:"salmon";s:7:"#FA8072";s:10:"sandybrown";s:7:"#F4A460";s:8:"seagreen";s:7:"#2E8B57";s:8:"seashell";s:7:"#FFF5EE";s:6:"sienna";s:7:"#A0522D";s:6:"silver";s:7:"#C0C0C0";s:7:"skyblue";s:7:"#87CEEB";s:9:"slateblue";s:7:"#6A5ACD";s:9:"slategray";s:7:"#708090";s:9:"slategrey";s:7:"#708090";s:4:"snow";s:7:"#FFFAFA";s:11:"springgreen";s:7:"#00FF7F";s:9:"steelblue";s:7:"#4682B4";s:3:"tan";s:7:"#D2B48C";s:4:"teal";s:7:"#008080";s:7:"thistle";s:7:"#D8BFD8";s:6:"tomato";s:7:"#FF6347";s:9:"turquoise";s:7:"#40E0D0";s:6:"violet";s:7:"#EE82EE";s:5:"wheat";s:7:"#F5DEB3";s:5:"white";s:7:"#FFFFFF";s:10:"whitesmoke";s:7:"#F5F5F5";s:6:"yellow";s:7:"#FFFF00";s:11:"yellowgreen";s:7:"#9ACD32";}s:30:"Core.ConvertDocumentToFragment";b:1;s:36:"Core.DirectLexLineNumberSyncInterval";i:0;s:20:"Core.DisableExcludes";b:0;s:15:"Core.EnableIDNA";b:0;s:13:"Core.Encoding";s:5:"utf-8";s:26:"Core.EscapeInvalidChildren";b:0;s:22:"Core.EscapeInvalidTags";b:0;s:29:"Core.EscapeNonASCIICharacters";b:0;s:19:"Core.HiddenElements";a:2:{s:6:"script";b:1;s:5:"style";b:1;}s:13:"Core.Language";s:2:"en";s:24:"Core.LegacyEntityDecoder";b:0;s:14:"Core.LexerImpl";N;s:24:"Core.MaintainLineNumbers";N;s:22:"Core.NormalizeNewlines";b:1;s:21:"Core.RemoveInvalidImg";b:1;s:33:"Core.RemoveProcessingInstructions";b:0;s:25:"Core.RemoveScriptContents";N;s:13:"Filter.Custom";a:0:{}s:34:"Filter.ExtractStyleBlocks.Escaping";b:1;s:31:"Filter.ExtractStyleBlocks.Scope";N;s:34:"Filter.ExtractStyleBlocks.TidyImpl";N;s:25:"Filter.ExtractStyleBlocks";b:0;s:14:"Filter.YouTube";b:0;s:12:"HTML.Allowed";N;s:22:"HTML.AllowedAttributes";N;s:20:"HTML.AllowedComments";a:0:{}s:26:"HTML.AllowedCommentsRegexp";N;s:20:"HTML.AllowedElements";N;s:19:"HTML.AllowedModules";N;s:23:"HTML.Attr.Name.UseCDATA";b:0;s:17:"HTML.BlockWrapper";s:1:"p";s:16:"HTML.CoreModules";a:7:{s:9:"Structure";b:1;s:4:"Text";b:1;s:9:"Hypertext";b:1;s:4:"List";b:1;s:22:"NonXMLCommonAttributes";b:1;s:19:"XMLCommonAttributes";b:1;s:16:"CommonAttributes";b:1;}s:18:"HTML.CustomDoctype";N;s:17:"HTML.DefinitionID";N;s:18:"HTML.DefinitionRev";i:1;s:12:"HTML.Doctype";N;s:25:"HTML.FlashAllowFullScreen";b:0;s:24:"HTML.ForbiddenAttributes";a:0:{}s:22:"HTML.ForbiddenElements";a:0:{}s:10:"HTML.Forms";b:0;s:17:"HTML.MaxImgLength";i:1200;s:13:"HTML.Nofollow";b:0;s:11:"HTML.Parent";s:3:"div";s:16:"HTML.Proprietary";b:0;s:14:"HTML.SafeEmbed";b:0;s:15:"HTML.SafeIframe";b:0;s:15:"HTML.SafeObject";b:0;s:18:"HTML.SafeScripting";a:0:{}s:11:"HTML.Strict";b:0;s:16:"HTML.TargetBlank";b:0;s:19:"HTML.TargetNoopener";b:1;s:21:"HTML.TargetNoreferrer";b:1;s:12:"HTML.TidyAdd";a:0:{}s:14:"HTML.TidyLevel";s:6:"medium";s:15:"HTML.TidyRemove";a:0:{}s:12:"HTML.Trusted";b:0;s:10:"HTML.XHTML";b:1;s:28:"Output.CommentScriptContents";b:1;s:19:"Output.FixInnerHTML";b:1;s:18:"Output.FlashCompat";b:0;s:14:"Output.Newline";N;s:15:"Output.SortAttr";b:0;s:17:"Output.TidyFormat";b:0;s:17:"Test.ForceNoIconv";b:0;s:18:"URI.AllowedSchemes";a:7:{s:4:"http";b:1;s:5:"https";b:1;s:6:"mailto";b:1;s:3:"ftp";b:1;s:4:"nntp";b:1;s:4:"news";b:1;s:3:"tel";b:1;}s:8:"URI.Base";N;s:17:"URI.DefaultScheme";s:4:"http";s:16:"URI.DefinitionID";N;s:17:"URI.DefinitionRev";i:1;s:11:"URI.Disable";b:0;s:19:"URI.DisableExternal";b:0;s:28:"URI.DisableExternalResources";b:0;s:20:"URI.DisableResources";b:0;s:8:"URI.Host";N;s:17:"URI.HostBlacklist";a:0:{}s:16:"URI.MakeAbsolute";b:0;s:9:"URI.Munge";N;s:18:"URI.MungeResources";b:0;s:18:"URI.MungeSecretKey";N;s:26:"URI.OverrideAllowedSchemes";b:1;s:20:"URI.SafeIframeRegexp";N;}s:9:"*parent";N;s:8:"*cache";N;}s:4:"info";a:140:{s:19:"Attr.AllowedClasses";i:-8;s:24:"Attr.AllowedFrameTargets";i:8;s:15:"Attr.AllowedRel";i:8;s:15:"Attr.AllowedRev";i:8;s:18:"Attr.ClassUseCDATA";i:-7;s:20:"Attr.DefaultImageAlt";i:-1;s:24:"Attr.DefaultInvalidImage";i:1;s:27:"Attr.DefaultInvalidImageAlt";i:1;s:19:"Attr.DefaultTextDir";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:2:{s:3:"ltr";b:1;s:3:"rtl";b:1;}}s:13:"Attr.EnableID";i:7;s:17:"HTML.EnableAttrID";O:8:"stdClass":2:{s:3:"key";s:13:"Attr.EnableID";s:7:"isAlias";b:1;}s:21:"Attr.ForbiddenClasses";i:8;s:13:"Attr.ID.HTML5";i:-7;s:16:"Attr.IDBlacklist";i:9;s:22:"Attr.IDBlacklistRegexp";i:-1;s:13:"Attr.IDPrefix";i:1;s:18:"Attr.IDPrefixLocal";i:1;s:24:"AutoFormat.AutoParagraph";i:7;s:17:"AutoFormat.Custom";i:9;s:25:"AutoFormat.DisplayLinkURI";i:7;s:18:"AutoFormat.Linkify";i:7;s:33:"AutoFormat.PurifierLinkify.DocURL";i:1;s:37:"AutoFormatParam.PurifierLinkifyDocURL";O:8:"stdClass":2:{s:3:"key";s:33:"AutoFormat.PurifierLinkify.DocURL";s:7:"isAlias";b:1;}s:26:"AutoFormat.PurifierLinkify";i:7;s:32:"AutoFormat.RemoveEmpty.Predicate";i:10;s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";i:8;s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";i:7;s:22:"AutoFormat.RemoveEmpty";i:7;s:39:"AutoFormat.RemoveSpansWithoutAttributes";i:7;s:19:"CSS.AllowDuplicates";i:7;s:18:"CSS.AllowImportant";i:7;s:15:"CSS.AllowTricky";i:7;s:16:"CSS.AllowedFonts";i:-8;s:21:"CSS.AllowedProperties";i:-8;s:17:"CSS.DefinitionRev";i:5;s:23:"CSS.ForbiddenProperties";i:8;s:16:"CSS.MaxImgLength";i:-1;s:15:"CSS.Proprietary";i:7;s:11:"CSS.Trusted";i:7;s:20:"Cache.DefinitionImpl";i:-1;s:20:"Core.DefinitionCache";O:8:"stdClass":2:{s:3:"key";s:20:"Cache.DefinitionImpl";s:7:"isAlias";b:1;}s:20:"Cache.SerializerPath";i:-1;s:27:"Cache.SerializerPermissions";i:-5;s:22:"Core.AggressivelyFixLt";i:7;s:29:"Core.AggressivelyRemoveScript";i:7;s:28:"Core.AllowHostnameUnderscore";i:7;s:23:"Core.AllowParseManyTags";i:7;s:18:"Core.CollectErrors";i:7;s:18:"Core.ColorKeywords";i:10;s:30:"Core.ConvertDocumentToFragment";i:7;s:24:"Core.AcceptFullDocuments";O:8:"stdClass":2:{s:3:"key";s:30:"Core.ConvertDocumentToFragment";s:7:"isAlias";b:1;}s:36:"Core.DirectLexLineNumberSyncInterval";i:5;s:20:"Core.DisableExcludes";i:7;s:15:"Core.EnableIDNA";i:7;s:13:"Core.Encoding";i:2;s:26:"Core.EscapeInvalidChildren";i:7;s:22:"Core.EscapeInvalidTags";i:7;s:29:"Core.EscapeNonASCIICharacters";i:7;s:19:"Core.HiddenElements";i:8;s:13:"Core.Language";i:1;s:24:"Core.LegacyEntityDecoder";i:7;s:14:"Core.LexerImpl";i:-11;s:24:"Core.MaintainLineNumbers";i:-7;s:22:"Core.NormalizeNewlines";i:7;s:21:"Core.RemoveInvalidImg";i:7;s:33:"Core.RemoveProcessingInstructions";i:7;s:25:"Core.RemoveScriptContents";i:-7;s:13:"Filter.Custom";i:9;s:34:"Filter.ExtractStyleBlocks.Escaping";i:7;s:33:"Filter.ExtractStyleBlocksEscaping";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.Escaping";s:7:"isAlias";b:1;}s:38:"FilterParam.ExtractStyleBlocksEscaping";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.Escaping";s:7:"isAlias";b:1;}s:31:"Filter.ExtractStyleBlocks.Scope";i:-1;s:30:"Filter.ExtractStyleBlocksScope";O:8:"stdClass":2:{s:3:"key";s:31:"Filter.ExtractStyleBlocks.Scope";s:7:"isAlias";b:1;}s:35:"FilterParam.ExtractStyleBlocksScope";O:8:"stdClass":2:{s:3:"key";s:31:"Filter.ExtractStyleBlocks.Scope";s:7:"isAlias";b:1;}s:34:"Filter.ExtractStyleBlocks.TidyImpl";i:-11;s:38:"FilterParam.ExtractStyleBlocksTidyImpl";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.TidyImpl";s:7:"isAlias";b:1;}s:25:"Filter.ExtractStyleBlocks";i:7;s:14:"Filter.YouTube";i:7;s:12:"HTML.Allowed";i:-4;s:22:"HTML.AllowedAttributes";i:-8;s:20:"HTML.AllowedComments";i:8;s:26:"HTML.AllowedCommentsRegexp";i:-1;s:20:"HTML.AllowedElements";i:-8;s:19:"HTML.AllowedModules";i:-8;s:23:"HTML.Attr.Name.UseCDATA";i:7;s:17:"HTML.BlockWrapper";i:1;s:16:"HTML.CoreModules";i:8;s:18:"HTML.CustomDoctype";i:-1;s:17:"HTML.DefinitionID";i:-1;s:18:"HTML.DefinitionRev";i:5;s:12:"HTML.Doctype";O:8:"stdClass":3:{s:4:"type";i:1;s:10:"allow_null";b:1;s:7:"allowed";a:5:{s:22:"HTML 4.01 Transitional";b:1;s:16:"HTML 4.01 Strict";b:1;s:22:"XHTML 1.0 Transitional";b:1;s:16:"XHTML 1.0 Strict";b:1;s:9:"XHTML 1.1";b:1;}}s:25:"HTML.FlashAllowFullScreen";i:7;s:24:"HTML.ForbiddenAttributes";i:8;s:22:"HTML.ForbiddenElements";i:8;s:10:"HTML.Forms";i:7;s:17:"HTML.MaxImgLength";i:-5;s:13:"HTML.Nofollow";i:7;s:11:"HTML.Parent";i:1;s:16:"HTML.Proprietary";i:7;s:14:"HTML.SafeEmbed";i:7;s:15:"HTML.SafeIframe";i:7;s:15:"HTML.SafeObject";i:7;s:18:"HTML.SafeScripting";i:8;s:11:"HTML.Strict";i:7;s:16:"HTML.TargetBlank";i:7;s:19:"HTML.TargetNoopener";i:7;s:21:"HTML.TargetNoreferrer";i:7;s:12:"HTML.TidyAdd";i:8;s:14:"HTML.TidyLevel";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:4:{s:4:"none";b:1;s:5:"light";b:1;s:6:"medium";b:1;s:5:"heavy";b:1;}}s:15:"HTML.TidyRemove";i:8;s:12:"HTML.Trusted";i:7;s:10:"HTML.XHTML";i:7;s:10:"Core.XHTML";O:8:"stdClass":2:{s:3:"key";s:10:"HTML.XHTML";s:7:"isAlias";b:1;}s:28:"Output.CommentScriptContents";i:7;s:26:"Core.CommentScriptContents";O:8:"stdClass":2:{s:3:"key";s:28:"Output.CommentScriptContents";s:7:"isAlias";b:1;}s:19:"Output.FixInnerHTML";i:7;s:18:"Output.FlashCompat";i:7;s:14:"Output.Newline";i:-1;s:15:"Output.SortAttr";i:7;s:17:"Output.TidyFormat";i:7;s:15:"Core.TidyFormat";O:8:"stdClass":2:{s:3:"key";s:17:"Output.TidyFormat";s:7:"isAlias";b:1;}s:17:"Test.ForceNoIconv";i:7;s:18:"URI.AllowedSchemes";i:8;s:8:"URI.Base";i:-1;s:17:"URI.DefaultScheme";i:-1;s:16:"URI.DefinitionID";i:-1;s:17:"URI.DefinitionRev";i:5;s:11:"URI.Disable";i:7;s:15:"Attr.DisableURI";O:8:"stdClass":2:{s:3:"key";s:11:"URI.Disable";s:7:"isAlias";b:1;}s:19:"URI.DisableExternal";i:7;s:28:"URI.DisableExternalResources";i:7;s:20:"URI.DisableResources";i:7;s:8:"URI.Host";i:-1;s:17:"URI.HostBlacklist";i:9;s:16:"URI.MakeAbsolute";i:7;s:9:"URI.Munge";i:-1;s:18:"URI.MungeResources";i:7;s:18:"URI.MungeSecretKey";i:-1;s:26:"URI.OverrideAllowedSchemes";i:7;s:20:"URI.SafeIframeRegexp";i:-1;}} \ No newline at end of file +O:25:"HTMLPurifier_ConfigSchema":3:{s:8:"defaults";a:130:{s:19:"Attr.AllowedClasses";N;s:24:"Attr.AllowedFrameTargets";a:0:{}s:15:"Attr.AllowedRel";a:0:{}s:15:"Attr.AllowedRev";a:0:{}s:18:"Attr.ClassUseCDATA";N;s:20:"Attr.DefaultImageAlt";N;s:24:"Attr.DefaultInvalidImage";s:0:"";s:27:"Attr.DefaultInvalidImageAlt";s:13:"Invalid image";s:19:"Attr.DefaultTextDir";s:3:"ltr";s:13:"Attr.EnableID";b:0;s:21:"Attr.ForbiddenClasses";a:0:{}s:13:"Attr.ID.HTML5";N;s:16:"Attr.IDBlacklist";a:0:{}s:22:"Attr.IDBlacklistRegexp";N;s:13:"Attr.IDPrefix";s:0:"";s:18:"Attr.IDPrefixLocal";s:0:"";s:24:"AutoFormat.AutoParagraph";b:0;s:17:"AutoFormat.Custom";a:0:{}s:25:"AutoFormat.DisplayLinkURI";b:0;s:18:"AutoFormat.Linkify";b:0;s:33:"AutoFormat.PurifierLinkify.DocURL";s:3:"#%s";s:26:"AutoFormat.PurifierLinkify";b:0;s:32:"AutoFormat.RemoveEmpty.Predicate";a:4:{s:8:"colgroup";a:0:{}s:2:"th";a:0:{}s:2:"td";a:0:{}s:6:"iframe";a:1:{i:0;s:3:"src";}}s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";a:2:{s:2:"td";b:1;s:2:"th";b:1;}s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";b:0;s:22:"AutoFormat.RemoveEmpty";b:0;s:39:"AutoFormat.RemoveSpansWithoutAttributes";b:0;s:19:"CSS.AllowDuplicates";b:0;s:18:"CSS.AllowImportant";b:0;s:15:"CSS.AllowTricky";b:0;s:16:"CSS.AllowedFonts";N;s:21:"CSS.AllowedProperties";N;s:17:"CSS.DefinitionRev";i:1;s:23:"CSS.ForbiddenProperties";a:0:{}s:16:"CSS.MaxImgLength";N;s:15:"CSS.Proprietary";b:0;s:11:"CSS.Trusted";b:0;s:20:"Cache.DefinitionImpl";s:10:"Serializer";s:20:"Cache.SerializerPath";N;s:27:"Cache.SerializerPermissions";i:493;s:22:"Core.AggressivelyFixLt";b:1;s:29:"Core.AggressivelyRemoveScript";b:1;s:28:"Core.AllowHostnameUnderscore";b:0;s:23:"Core.AllowParseManyTags";b:0;s:18:"Core.CollectErrors";b:0;s:18:"Core.ColorKeywords";a:148:{s:9:"aliceblue";s:7:"#F0F8FF";s:12:"antiquewhite";s:7:"#FAEBD7";s:4:"aqua";s:7:"#00FFFF";s:10:"aquamarine";s:7:"#7FFFD4";s:5:"azure";s:7:"#F0FFFF";s:5:"beige";s:7:"#F5F5DC";s:6:"bisque";s:7:"#FFE4C4";s:5:"black";s:7:"#000000";s:14:"blanchedalmond";s:7:"#FFEBCD";s:4:"blue";s:7:"#0000FF";s:10:"blueviolet";s:7:"#8A2BE2";s:5:"brown";s:7:"#A52A2A";s:9:"burlywood";s:7:"#DEB887";s:9:"cadetblue";s:7:"#5F9EA0";s:10:"chartreuse";s:7:"#7FFF00";s:9:"chocolate";s:7:"#D2691E";s:5:"coral";s:7:"#FF7F50";s:14:"cornflowerblue";s:7:"#6495ED";s:8:"cornsilk";s:7:"#FFF8DC";s:7:"crimson";s:7:"#DC143C";s:4:"cyan";s:7:"#00FFFF";s:8:"darkblue";s:7:"#00008B";s:8:"darkcyan";s:7:"#008B8B";s:13:"darkgoldenrod";s:7:"#B8860B";s:8:"darkgray";s:7:"#A9A9A9";s:8:"darkgrey";s:7:"#A9A9A9";s:9:"darkgreen";s:7:"#006400";s:9:"darkkhaki";s:7:"#BDB76B";s:11:"darkmagenta";s:7:"#8B008B";s:14:"darkolivegreen";s:7:"#556B2F";s:10:"darkorange";s:7:"#FF8C00";s:10:"darkorchid";s:7:"#9932CC";s:7:"darkred";s:7:"#8B0000";s:10:"darksalmon";s:7:"#E9967A";s:12:"darkseagreen";s:7:"#8FBC8F";s:13:"darkslateblue";s:7:"#483D8B";s:13:"darkslategray";s:7:"#2F4F4F";s:13:"darkslategrey";s:7:"#2F4F4F";s:13:"darkturquoise";s:7:"#00CED1";s:10:"darkviolet";s:7:"#9400D3";s:8:"deeppink";s:7:"#FF1493";s:11:"deepskyblue";s:7:"#00BFFF";s:7:"dimgray";s:7:"#696969";s:7:"dimgrey";s:7:"#696969";s:10:"dodgerblue";s:7:"#1E90FF";s:9:"firebrick";s:7:"#B22222";s:11:"floralwhite";s:7:"#FFFAF0";s:11:"forestgreen";s:7:"#228B22";s:7:"fuchsia";s:7:"#FF00FF";s:9:"gainsboro";s:7:"#DCDCDC";s:10:"ghostwhite";s:7:"#F8F8FF";s:4:"gold";s:7:"#FFD700";s:9:"goldenrod";s:7:"#DAA520";s:4:"gray";s:7:"#808080";s:4:"grey";s:7:"#808080";s:5:"green";s:7:"#008000";s:11:"greenyellow";s:7:"#ADFF2F";s:8:"honeydew";s:7:"#F0FFF0";s:7:"hotpink";s:7:"#FF69B4";s:9:"indianred";s:7:"#CD5C5C";s:6:"indigo";s:7:"#4B0082";s:5:"ivory";s:7:"#FFFFF0";s:5:"khaki";s:7:"#F0E68C";s:8:"lavender";s:7:"#E6E6FA";s:13:"lavenderblush";s:7:"#FFF0F5";s:9:"lawngreen";s:7:"#7CFC00";s:12:"lemonchiffon";s:7:"#FFFACD";s:9:"lightblue";s:7:"#ADD8E6";s:10:"lightcoral";s:7:"#F08080";s:9:"lightcyan";s:7:"#E0FFFF";s:20:"lightgoldenrodyellow";s:7:"#FAFAD2";s:9:"lightgray";s:7:"#D3D3D3";s:9:"lightgrey";s:7:"#D3D3D3";s:10:"lightgreen";s:7:"#90EE90";s:9:"lightpink";s:7:"#FFB6C1";s:11:"lightsalmon";s:7:"#FFA07A";s:13:"lightseagreen";s:7:"#20B2AA";s:12:"lightskyblue";s:7:"#87CEFA";s:14:"lightslategray";s:7:"#778899";s:14:"lightslategrey";s:7:"#778899";s:14:"lightsteelblue";s:7:"#B0C4DE";s:11:"lightyellow";s:7:"#FFFFE0";s:4:"lime";s:7:"#00FF00";s:9:"limegreen";s:7:"#32CD32";s:5:"linen";s:7:"#FAF0E6";s:7:"magenta";s:7:"#FF00FF";s:6:"maroon";s:7:"#800000";s:16:"mediumaquamarine";s:7:"#66CDAA";s:10:"mediumblue";s:7:"#0000CD";s:12:"mediumorchid";s:7:"#BA55D3";s:12:"mediumpurple";s:7:"#9370DB";s:14:"mediumseagreen";s:7:"#3CB371";s:15:"mediumslateblue";s:7:"#7B68EE";s:17:"mediumspringgreen";s:7:"#00FA9A";s:15:"mediumturquoise";s:7:"#48D1CC";s:15:"mediumvioletred";s:7:"#C71585";s:12:"midnightblue";s:7:"#191970";s:9:"mintcream";s:7:"#F5FFFA";s:9:"mistyrose";s:7:"#FFE4E1";s:8:"moccasin";s:7:"#FFE4B5";s:11:"navajowhite";s:7:"#FFDEAD";s:4:"navy";s:7:"#000080";s:7:"oldlace";s:7:"#FDF5E6";s:5:"olive";s:7:"#808000";s:9:"olivedrab";s:7:"#6B8E23";s:6:"orange";s:7:"#FFA500";s:9:"orangered";s:7:"#FF4500";s:6:"orchid";s:7:"#DA70D6";s:13:"palegoldenrod";s:7:"#EEE8AA";s:9:"palegreen";s:7:"#98FB98";s:13:"paleturquoise";s:7:"#AFEEEE";s:13:"palevioletred";s:7:"#DB7093";s:10:"papayawhip";s:7:"#FFEFD5";s:9:"peachpuff";s:7:"#FFDAB9";s:4:"peru";s:7:"#CD853F";s:4:"pink";s:7:"#FFC0CB";s:4:"plum";s:7:"#DDA0DD";s:10:"powderblue";s:7:"#B0E0E6";s:6:"purple";s:7:"#800080";s:13:"rebeccapurple";s:7:"#663399";s:3:"red";s:7:"#FF0000";s:9:"rosybrown";s:7:"#BC8F8F";s:9:"royalblue";s:7:"#4169E1";s:11:"saddlebrown";s:7:"#8B4513";s:6:"salmon";s:7:"#FA8072";s:10:"sandybrown";s:7:"#F4A460";s:8:"seagreen";s:7:"#2E8B57";s:8:"seashell";s:7:"#FFF5EE";s:6:"sienna";s:7:"#A0522D";s:6:"silver";s:7:"#C0C0C0";s:7:"skyblue";s:7:"#87CEEB";s:9:"slateblue";s:7:"#6A5ACD";s:9:"slategray";s:7:"#708090";s:9:"slategrey";s:7:"#708090";s:4:"snow";s:7:"#FFFAFA";s:11:"springgreen";s:7:"#00FF7F";s:9:"steelblue";s:7:"#4682B4";s:3:"tan";s:7:"#D2B48C";s:4:"teal";s:7:"#008080";s:7:"thistle";s:7:"#D8BFD8";s:6:"tomato";s:7:"#FF6347";s:9:"turquoise";s:7:"#40E0D0";s:6:"violet";s:7:"#EE82EE";s:5:"wheat";s:7:"#F5DEB3";s:5:"white";s:7:"#FFFFFF";s:10:"whitesmoke";s:7:"#F5F5F5";s:6:"yellow";s:7:"#FFFF00";s:11:"yellowgreen";s:7:"#9ACD32";}s:30:"Core.ConvertDocumentToFragment";b:1;s:36:"Core.DirectLexLineNumberSyncInterval";i:0;s:20:"Core.DisableExcludes";b:0;s:15:"Core.EnableIDNA";b:0;s:13:"Core.Encoding";s:5:"utf-8";s:26:"Core.EscapeInvalidChildren";b:0;s:22:"Core.EscapeInvalidTags";b:0;s:29:"Core.EscapeNonASCIICharacters";b:0;s:19:"Core.HiddenElements";a:2:{s:6:"script";b:1;s:5:"style";b:1;}s:13:"Core.Language";s:2:"en";s:24:"Core.LegacyEntityDecoder";b:0;s:14:"Core.LexerImpl";N;s:24:"Core.MaintainLineNumbers";N;s:22:"Core.NormalizeNewlines";b:1;s:17:"Core.RemoveBlanks";b:0;s:21:"Core.RemoveInvalidImg";b:1;s:33:"Core.RemoveProcessingInstructions";b:0;s:25:"Core.RemoveScriptContents";N;s:13:"Filter.Custom";a:0:{}s:34:"Filter.ExtractStyleBlocks.Escaping";b:1;s:31:"Filter.ExtractStyleBlocks.Scope";N;s:34:"Filter.ExtractStyleBlocks.TidyImpl";N;s:25:"Filter.ExtractStyleBlocks";b:0;s:14:"Filter.YouTube";b:0;s:12:"HTML.Allowed";N;s:22:"HTML.AllowedAttributes";N;s:20:"HTML.AllowedComments";a:0:{}s:26:"HTML.AllowedCommentsRegexp";N;s:20:"HTML.AllowedElements";N;s:19:"HTML.AllowedModules";N;s:23:"HTML.Attr.Name.UseCDATA";b:0;s:17:"HTML.BlockWrapper";s:1:"p";s:16:"HTML.CoreModules";a:7:{s:9:"Structure";b:1;s:4:"Text";b:1;s:9:"Hypertext";b:1;s:4:"List";b:1;s:22:"NonXMLCommonAttributes";b:1;s:19:"XMLCommonAttributes";b:1;s:16:"CommonAttributes";b:1;}s:18:"HTML.CustomDoctype";N;s:17:"HTML.DefinitionID";N;s:18:"HTML.DefinitionRev";i:1;s:12:"HTML.Doctype";N;s:25:"HTML.FlashAllowFullScreen";b:0;s:24:"HTML.ForbiddenAttributes";a:0:{}s:22:"HTML.ForbiddenElements";a:0:{}s:10:"HTML.Forms";b:0;s:17:"HTML.MaxImgLength";N;s:13:"HTML.Nofollow";b:0;s:11:"HTML.Parent";s:3:"div";s:16:"HTML.Proprietary";b:0;s:14:"HTML.SafeEmbed";b:0;s:15:"HTML.SafeIframe";b:0;s:15:"HTML.SafeObject";b:0;s:18:"HTML.SafeScripting";a:0:{}s:11:"HTML.Strict";b:0;s:16:"HTML.TargetBlank";b:0;s:19:"HTML.TargetNoopener";b:1;s:21:"HTML.TargetNoreferrer";b:1;s:12:"HTML.TidyAdd";a:0:{}s:14:"HTML.TidyLevel";s:6:"medium";s:15:"HTML.TidyRemove";a:0:{}s:12:"HTML.Trusted";b:0;s:10:"HTML.XHTML";b:1;s:28:"Output.CommentScriptContents";b:1;s:19:"Output.FixInnerHTML";b:1;s:18:"Output.FlashCompat";b:0;s:14:"Output.Newline";N;s:15:"Output.SortAttr";b:0;s:17:"Output.TidyFormat";b:0;s:17:"Test.ForceNoIconv";b:0;s:18:"URI.AllowedSchemes";a:7:{s:4:"http";b:1;s:5:"https";b:1;s:6:"mailto";b:1;s:3:"ftp";b:1;s:4:"nntp";b:1;s:4:"news";b:1;s:3:"tel";b:1;}s:18:"URI.AllowedSymbols";s:11:"!$&'()*+,;=";s:8:"URI.Base";N;s:17:"URI.DefaultScheme";s:4:"http";s:16:"URI.DefinitionID";N;s:17:"URI.DefinitionRev";i:1;s:11:"URI.Disable";b:0;s:19:"URI.DisableExternal";b:0;s:28:"URI.DisableExternalResources";b:0;s:20:"URI.DisableResources";b:0;s:8:"URI.Host";N;s:17:"URI.HostBlacklist";a:0:{}s:16:"URI.MakeAbsolute";b:0;s:9:"URI.Munge";N;s:18:"URI.MungeResources";b:0;s:18:"URI.MungeSecretKey";N;s:26:"URI.OverrideAllowedSchemes";b:1;s:19:"URI.SafeIframeHosts";N;s:20:"URI.SafeIframeRegexp";N;}s:12:"defaultPlist";O:25:"HTMLPurifier_PropertyList":3:{s:7:"*data";a:130:{s:19:"Attr.AllowedClasses";N;s:24:"Attr.AllowedFrameTargets";a:0:{}s:15:"Attr.AllowedRel";a:0:{}s:15:"Attr.AllowedRev";a:0:{}s:18:"Attr.ClassUseCDATA";N;s:20:"Attr.DefaultImageAlt";N;s:24:"Attr.DefaultInvalidImage";s:0:"";s:27:"Attr.DefaultInvalidImageAlt";s:13:"Invalid image";s:19:"Attr.DefaultTextDir";s:3:"ltr";s:13:"Attr.EnableID";b:0;s:21:"Attr.ForbiddenClasses";a:0:{}s:13:"Attr.ID.HTML5";N;s:16:"Attr.IDBlacklist";a:0:{}s:22:"Attr.IDBlacklistRegexp";N;s:13:"Attr.IDPrefix";s:0:"";s:18:"Attr.IDPrefixLocal";s:0:"";s:24:"AutoFormat.AutoParagraph";b:0;s:17:"AutoFormat.Custom";a:0:{}s:25:"AutoFormat.DisplayLinkURI";b:0;s:18:"AutoFormat.Linkify";b:0;s:33:"AutoFormat.PurifierLinkify.DocURL";s:3:"#%s";s:26:"AutoFormat.PurifierLinkify";b:0;s:32:"AutoFormat.RemoveEmpty.Predicate";a:4:{s:8:"colgroup";a:0:{}s:2:"th";a:0:{}s:2:"td";a:0:{}s:6:"iframe";a:1:{i:0;s:3:"src";}}s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";a:2:{s:2:"td";b:1;s:2:"th";b:1;}s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";b:0;s:22:"AutoFormat.RemoveEmpty";b:0;s:39:"AutoFormat.RemoveSpansWithoutAttributes";b:0;s:19:"CSS.AllowDuplicates";b:0;s:18:"CSS.AllowImportant";b:0;s:15:"CSS.AllowTricky";b:0;s:16:"CSS.AllowedFonts";N;s:21:"CSS.AllowedProperties";N;s:17:"CSS.DefinitionRev";i:1;s:23:"CSS.ForbiddenProperties";a:0:{}s:16:"CSS.MaxImgLength";N;s:15:"CSS.Proprietary";b:0;s:11:"CSS.Trusted";b:0;s:20:"Cache.DefinitionImpl";s:10:"Serializer";s:20:"Cache.SerializerPath";N;s:27:"Cache.SerializerPermissions";i:493;s:22:"Core.AggressivelyFixLt";b:1;s:29:"Core.AggressivelyRemoveScript";b:1;s:28:"Core.AllowHostnameUnderscore";b:0;s:23:"Core.AllowParseManyTags";b:0;s:18:"Core.CollectErrors";b:0;s:18:"Core.ColorKeywords";a:148:{s:9:"aliceblue";s:7:"#F0F8FF";s:12:"antiquewhite";s:7:"#FAEBD7";s:4:"aqua";s:7:"#00FFFF";s:10:"aquamarine";s:7:"#7FFFD4";s:5:"azure";s:7:"#F0FFFF";s:5:"beige";s:7:"#F5F5DC";s:6:"bisque";s:7:"#FFE4C4";s:5:"black";s:7:"#000000";s:14:"blanchedalmond";s:7:"#FFEBCD";s:4:"blue";s:7:"#0000FF";s:10:"blueviolet";s:7:"#8A2BE2";s:5:"brown";s:7:"#A52A2A";s:9:"burlywood";s:7:"#DEB887";s:9:"cadetblue";s:7:"#5F9EA0";s:10:"chartreuse";s:7:"#7FFF00";s:9:"chocolate";s:7:"#D2691E";s:5:"coral";s:7:"#FF7F50";s:14:"cornflowerblue";s:7:"#6495ED";s:8:"cornsilk";s:7:"#FFF8DC";s:7:"crimson";s:7:"#DC143C";s:4:"cyan";s:7:"#00FFFF";s:8:"darkblue";s:7:"#00008B";s:8:"darkcyan";s:7:"#008B8B";s:13:"darkgoldenrod";s:7:"#B8860B";s:8:"darkgray";s:7:"#A9A9A9";s:8:"darkgrey";s:7:"#A9A9A9";s:9:"darkgreen";s:7:"#006400";s:9:"darkkhaki";s:7:"#BDB76B";s:11:"darkmagenta";s:7:"#8B008B";s:14:"darkolivegreen";s:7:"#556B2F";s:10:"darkorange";s:7:"#FF8C00";s:10:"darkorchid";s:7:"#9932CC";s:7:"darkred";s:7:"#8B0000";s:10:"darksalmon";s:7:"#E9967A";s:12:"darkseagreen";s:7:"#8FBC8F";s:13:"darkslateblue";s:7:"#483D8B";s:13:"darkslategray";s:7:"#2F4F4F";s:13:"darkslategrey";s:7:"#2F4F4F";s:13:"darkturquoise";s:7:"#00CED1";s:10:"darkviolet";s:7:"#9400D3";s:8:"deeppink";s:7:"#FF1493";s:11:"deepskyblue";s:7:"#00BFFF";s:7:"dimgray";s:7:"#696969";s:7:"dimgrey";s:7:"#696969";s:10:"dodgerblue";s:7:"#1E90FF";s:9:"firebrick";s:7:"#B22222";s:11:"floralwhite";s:7:"#FFFAF0";s:11:"forestgreen";s:7:"#228B22";s:7:"fuchsia";s:7:"#FF00FF";s:9:"gainsboro";s:7:"#DCDCDC";s:10:"ghostwhite";s:7:"#F8F8FF";s:4:"gold";s:7:"#FFD700";s:9:"goldenrod";s:7:"#DAA520";s:4:"gray";s:7:"#808080";s:4:"grey";s:7:"#808080";s:5:"green";s:7:"#008000";s:11:"greenyellow";s:7:"#ADFF2F";s:8:"honeydew";s:7:"#F0FFF0";s:7:"hotpink";s:7:"#FF69B4";s:9:"indianred";s:7:"#CD5C5C";s:6:"indigo";s:7:"#4B0082";s:5:"ivory";s:7:"#FFFFF0";s:5:"khaki";s:7:"#F0E68C";s:8:"lavender";s:7:"#E6E6FA";s:13:"lavenderblush";s:7:"#FFF0F5";s:9:"lawngreen";s:7:"#7CFC00";s:12:"lemonchiffon";s:7:"#FFFACD";s:9:"lightblue";s:7:"#ADD8E6";s:10:"lightcoral";s:7:"#F08080";s:9:"lightcyan";s:7:"#E0FFFF";s:20:"lightgoldenrodyellow";s:7:"#FAFAD2";s:9:"lightgray";s:7:"#D3D3D3";s:9:"lightgrey";s:7:"#D3D3D3";s:10:"lightgreen";s:7:"#90EE90";s:9:"lightpink";s:7:"#FFB6C1";s:11:"lightsalmon";s:7:"#FFA07A";s:13:"lightseagreen";s:7:"#20B2AA";s:12:"lightskyblue";s:7:"#87CEFA";s:14:"lightslategray";s:7:"#778899";s:14:"lightslategrey";s:7:"#778899";s:14:"lightsteelblue";s:7:"#B0C4DE";s:11:"lightyellow";s:7:"#FFFFE0";s:4:"lime";s:7:"#00FF00";s:9:"limegreen";s:7:"#32CD32";s:5:"linen";s:7:"#FAF0E6";s:7:"magenta";s:7:"#FF00FF";s:6:"maroon";s:7:"#800000";s:16:"mediumaquamarine";s:7:"#66CDAA";s:10:"mediumblue";s:7:"#0000CD";s:12:"mediumorchid";s:7:"#BA55D3";s:12:"mediumpurple";s:7:"#9370DB";s:14:"mediumseagreen";s:7:"#3CB371";s:15:"mediumslateblue";s:7:"#7B68EE";s:17:"mediumspringgreen";s:7:"#00FA9A";s:15:"mediumturquoise";s:7:"#48D1CC";s:15:"mediumvioletred";s:7:"#C71585";s:12:"midnightblue";s:7:"#191970";s:9:"mintcream";s:7:"#F5FFFA";s:9:"mistyrose";s:7:"#FFE4E1";s:8:"moccasin";s:7:"#FFE4B5";s:11:"navajowhite";s:7:"#FFDEAD";s:4:"navy";s:7:"#000080";s:7:"oldlace";s:7:"#FDF5E6";s:5:"olive";s:7:"#808000";s:9:"olivedrab";s:7:"#6B8E23";s:6:"orange";s:7:"#FFA500";s:9:"orangered";s:7:"#FF4500";s:6:"orchid";s:7:"#DA70D6";s:13:"palegoldenrod";s:7:"#EEE8AA";s:9:"palegreen";s:7:"#98FB98";s:13:"paleturquoise";s:7:"#AFEEEE";s:13:"palevioletred";s:7:"#DB7093";s:10:"papayawhip";s:7:"#FFEFD5";s:9:"peachpuff";s:7:"#FFDAB9";s:4:"peru";s:7:"#CD853F";s:4:"pink";s:7:"#FFC0CB";s:4:"plum";s:7:"#DDA0DD";s:10:"powderblue";s:7:"#B0E0E6";s:6:"purple";s:7:"#800080";s:13:"rebeccapurple";s:7:"#663399";s:3:"red";s:7:"#FF0000";s:9:"rosybrown";s:7:"#BC8F8F";s:9:"royalblue";s:7:"#4169E1";s:11:"saddlebrown";s:7:"#8B4513";s:6:"salmon";s:7:"#FA8072";s:10:"sandybrown";s:7:"#F4A460";s:8:"seagreen";s:7:"#2E8B57";s:8:"seashell";s:7:"#FFF5EE";s:6:"sienna";s:7:"#A0522D";s:6:"silver";s:7:"#C0C0C0";s:7:"skyblue";s:7:"#87CEEB";s:9:"slateblue";s:7:"#6A5ACD";s:9:"slategray";s:7:"#708090";s:9:"slategrey";s:7:"#708090";s:4:"snow";s:7:"#FFFAFA";s:11:"springgreen";s:7:"#00FF7F";s:9:"steelblue";s:7:"#4682B4";s:3:"tan";s:7:"#D2B48C";s:4:"teal";s:7:"#008080";s:7:"thistle";s:7:"#D8BFD8";s:6:"tomato";s:7:"#FF6347";s:9:"turquoise";s:7:"#40E0D0";s:6:"violet";s:7:"#EE82EE";s:5:"wheat";s:7:"#F5DEB3";s:5:"white";s:7:"#FFFFFF";s:10:"whitesmoke";s:7:"#F5F5F5";s:6:"yellow";s:7:"#FFFF00";s:11:"yellowgreen";s:7:"#9ACD32";}s:30:"Core.ConvertDocumentToFragment";b:1;s:36:"Core.DirectLexLineNumberSyncInterval";i:0;s:20:"Core.DisableExcludes";b:0;s:15:"Core.EnableIDNA";b:0;s:13:"Core.Encoding";s:5:"utf-8";s:26:"Core.EscapeInvalidChildren";b:0;s:22:"Core.EscapeInvalidTags";b:0;s:29:"Core.EscapeNonASCIICharacters";b:0;s:19:"Core.HiddenElements";a:2:{s:6:"script";b:1;s:5:"style";b:1;}s:13:"Core.Language";s:2:"en";s:24:"Core.LegacyEntityDecoder";b:0;s:14:"Core.LexerImpl";N;s:24:"Core.MaintainLineNumbers";N;s:22:"Core.NormalizeNewlines";b:1;s:17:"Core.RemoveBlanks";b:0;s:21:"Core.RemoveInvalidImg";b:1;s:33:"Core.RemoveProcessingInstructions";b:0;s:25:"Core.RemoveScriptContents";N;s:13:"Filter.Custom";a:0:{}s:34:"Filter.ExtractStyleBlocks.Escaping";b:1;s:31:"Filter.ExtractStyleBlocks.Scope";N;s:34:"Filter.ExtractStyleBlocks.TidyImpl";N;s:25:"Filter.ExtractStyleBlocks";b:0;s:14:"Filter.YouTube";b:0;s:12:"HTML.Allowed";N;s:22:"HTML.AllowedAttributes";N;s:20:"HTML.AllowedComments";a:0:{}s:26:"HTML.AllowedCommentsRegexp";N;s:20:"HTML.AllowedElements";N;s:19:"HTML.AllowedModules";N;s:23:"HTML.Attr.Name.UseCDATA";b:0;s:17:"HTML.BlockWrapper";s:1:"p";s:16:"HTML.CoreModules";a:7:{s:9:"Structure";b:1;s:4:"Text";b:1;s:9:"Hypertext";b:1;s:4:"List";b:1;s:22:"NonXMLCommonAttributes";b:1;s:19:"XMLCommonAttributes";b:1;s:16:"CommonAttributes";b:1;}s:18:"HTML.CustomDoctype";N;s:17:"HTML.DefinitionID";N;s:18:"HTML.DefinitionRev";i:1;s:12:"HTML.Doctype";N;s:25:"HTML.FlashAllowFullScreen";b:0;s:24:"HTML.ForbiddenAttributes";a:0:{}s:22:"HTML.ForbiddenElements";a:0:{}s:10:"HTML.Forms";b:0;s:17:"HTML.MaxImgLength";N;s:13:"HTML.Nofollow";b:0;s:11:"HTML.Parent";s:3:"div";s:16:"HTML.Proprietary";b:0;s:14:"HTML.SafeEmbed";b:0;s:15:"HTML.SafeIframe";b:0;s:15:"HTML.SafeObject";b:0;s:18:"HTML.SafeScripting";a:0:{}s:11:"HTML.Strict";b:0;s:16:"HTML.TargetBlank";b:0;s:19:"HTML.TargetNoopener";b:1;s:21:"HTML.TargetNoreferrer";b:1;s:12:"HTML.TidyAdd";a:0:{}s:14:"HTML.TidyLevel";s:6:"medium";s:15:"HTML.TidyRemove";a:0:{}s:12:"HTML.Trusted";b:0;s:10:"HTML.XHTML";b:1;s:28:"Output.CommentScriptContents";b:1;s:19:"Output.FixInnerHTML";b:1;s:18:"Output.FlashCompat";b:0;s:14:"Output.Newline";N;s:15:"Output.SortAttr";b:0;s:17:"Output.TidyFormat";b:0;s:17:"Test.ForceNoIconv";b:0;s:18:"URI.AllowedSchemes";a:7:{s:4:"http";b:1;s:5:"https";b:1;s:6:"mailto";b:1;s:3:"ftp";b:1;s:4:"nntp";b:1;s:4:"news";b:1;s:3:"tel";b:1;}s:18:"URI.AllowedSymbols";s:11:"!$&'()*+,;=";s:8:"URI.Base";N;s:17:"URI.DefaultScheme";s:4:"http";s:16:"URI.DefinitionID";N;s:17:"URI.DefinitionRev";i:1;s:11:"URI.Disable";b:0;s:19:"URI.DisableExternal";b:0;s:28:"URI.DisableExternalResources";b:0;s:20:"URI.DisableResources";b:0;s:8:"URI.Host";N;s:17:"URI.HostBlacklist";a:0:{}s:16:"URI.MakeAbsolute";b:0;s:9:"URI.Munge";N;s:18:"URI.MungeResources";b:0;s:18:"URI.MungeSecretKey";N;s:26:"URI.OverrideAllowedSchemes";b:1;s:19:"URI.SafeIframeHosts";N;s:20:"URI.SafeIframeRegexp";N;}s:9:"*parent";N;s:8:"*cache";N;}s:4:"info";a:143:{s:19:"Attr.AllowedClasses";i:-8;s:24:"Attr.AllowedFrameTargets";i:8;s:15:"Attr.AllowedRel";i:8;s:15:"Attr.AllowedRev";i:8;s:18:"Attr.ClassUseCDATA";i:-7;s:20:"Attr.DefaultImageAlt";i:-1;s:24:"Attr.DefaultInvalidImage";i:1;s:27:"Attr.DefaultInvalidImageAlt";i:1;s:19:"Attr.DefaultTextDir";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:2:{s:3:"ltr";b:1;s:3:"rtl";b:1;}}s:13:"Attr.EnableID";i:7;s:17:"HTML.EnableAttrID";O:8:"stdClass":2:{s:3:"key";s:13:"Attr.EnableID";s:7:"isAlias";b:1;}s:21:"Attr.ForbiddenClasses";i:8;s:13:"Attr.ID.HTML5";i:-7;s:16:"Attr.IDBlacklist";i:9;s:22:"Attr.IDBlacklistRegexp";i:-1;s:13:"Attr.IDPrefix";i:1;s:18:"Attr.IDPrefixLocal";i:1;s:24:"AutoFormat.AutoParagraph";i:7;s:17:"AutoFormat.Custom";i:9;s:25:"AutoFormat.DisplayLinkURI";i:7;s:18:"AutoFormat.Linkify";i:7;s:33:"AutoFormat.PurifierLinkify.DocURL";i:1;s:37:"AutoFormatParam.PurifierLinkifyDocURL";O:8:"stdClass":2:{s:3:"key";s:33:"AutoFormat.PurifierLinkify.DocURL";s:7:"isAlias";b:1;}s:26:"AutoFormat.PurifierLinkify";i:7;s:32:"AutoFormat.RemoveEmpty.Predicate";i:10;s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";i:8;s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";i:7;s:22:"AutoFormat.RemoveEmpty";i:7;s:39:"AutoFormat.RemoveSpansWithoutAttributes";i:7;s:19:"CSS.AllowDuplicates";i:7;s:18:"CSS.AllowImportant";i:7;s:15:"CSS.AllowTricky";i:7;s:16:"CSS.AllowedFonts";i:-8;s:21:"CSS.AllowedProperties";i:-8;s:17:"CSS.DefinitionRev";i:5;s:23:"CSS.ForbiddenProperties";i:8;s:16:"CSS.MaxImgLength";i:-1;s:15:"CSS.Proprietary";i:7;s:11:"CSS.Trusted";i:7;s:20:"Cache.DefinitionImpl";i:-1;s:20:"Core.DefinitionCache";O:8:"stdClass":2:{s:3:"key";s:20:"Cache.DefinitionImpl";s:7:"isAlias";b:1;}s:20:"Cache.SerializerPath";i:-1;s:27:"Cache.SerializerPermissions";i:-5;s:22:"Core.AggressivelyFixLt";i:7;s:29:"Core.AggressivelyRemoveScript";i:7;s:28:"Core.AllowHostnameUnderscore";i:7;s:23:"Core.AllowParseManyTags";i:7;s:18:"Core.CollectErrors";i:7;s:18:"Core.ColorKeywords";i:10;s:30:"Core.ConvertDocumentToFragment";i:7;s:24:"Core.AcceptFullDocuments";O:8:"stdClass":2:{s:3:"key";s:30:"Core.ConvertDocumentToFragment";s:7:"isAlias";b:1;}s:36:"Core.DirectLexLineNumberSyncInterval";i:5;s:20:"Core.DisableExcludes";i:7;s:15:"Core.EnableIDNA";i:7;s:13:"Core.Encoding";i:2;s:26:"Core.EscapeInvalidChildren";i:7;s:22:"Core.EscapeInvalidTags";i:7;s:29:"Core.EscapeNonASCIICharacters";i:7;s:19:"Core.HiddenElements";i:8;s:13:"Core.Language";i:1;s:24:"Core.LegacyEntityDecoder";i:7;s:14:"Core.LexerImpl";i:-11;s:24:"Core.MaintainLineNumbers";i:-7;s:22:"Core.NormalizeNewlines";i:7;s:17:"Core.RemoveBlanks";i:7;s:21:"Core.RemoveInvalidImg";i:7;s:33:"Core.RemoveProcessingInstructions";i:7;s:25:"Core.RemoveScriptContents";i:-7;s:13:"Filter.Custom";i:9;s:34:"Filter.ExtractStyleBlocks.Escaping";i:7;s:33:"Filter.ExtractStyleBlocksEscaping";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.Escaping";s:7:"isAlias";b:1;}s:38:"FilterParam.ExtractStyleBlocksEscaping";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.Escaping";s:7:"isAlias";b:1;}s:31:"Filter.ExtractStyleBlocks.Scope";i:-1;s:30:"Filter.ExtractStyleBlocksScope";O:8:"stdClass":2:{s:3:"key";s:31:"Filter.ExtractStyleBlocks.Scope";s:7:"isAlias";b:1;}s:35:"FilterParam.ExtractStyleBlocksScope";O:8:"stdClass":2:{s:3:"key";s:31:"Filter.ExtractStyleBlocks.Scope";s:7:"isAlias";b:1;}s:34:"Filter.ExtractStyleBlocks.TidyImpl";i:-11;s:38:"FilterParam.ExtractStyleBlocksTidyImpl";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.TidyImpl";s:7:"isAlias";b:1;}s:25:"Filter.ExtractStyleBlocks";i:7;s:14:"Filter.YouTube";i:7;s:12:"HTML.Allowed";i:-4;s:22:"HTML.AllowedAttributes";i:-8;s:20:"HTML.AllowedComments";i:8;s:26:"HTML.AllowedCommentsRegexp";i:-1;s:20:"HTML.AllowedElements";i:-8;s:19:"HTML.AllowedModules";i:-8;s:23:"HTML.Attr.Name.UseCDATA";i:7;s:17:"HTML.BlockWrapper";i:1;s:16:"HTML.CoreModules";i:8;s:18:"HTML.CustomDoctype";i:-1;s:17:"HTML.DefinitionID";i:-1;s:18:"HTML.DefinitionRev";i:5;s:12:"HTML.Doctype";O:8:"stdClass":3:{s:4:"type";i:1;s:10:"allow_null";b:1;s:7:"allowed";a:5:{s:22:"HTML 4.01 Transitional";b:1;s:16:"HTML 4.01 Strict";b:1;s:22:"XHTML 1.0 Transitional";b:1;s:16:"XHTML 1.0 Strict";b:1;s:9:"XHTML 1.1";b:1;}}s:25:"HTML.FlashAllowFullScreen";i:7;s:24:"HTML.ForbiddenAttributes";i:8;s:22:"HTML.ForbiddenElements";i:8;s:10:"HTML.Forms";i:7;s:17:"HTML.MaxImgLength";i:-5;s:13:"HTML.Nofollow";i:7;s:11:"HTML.Parent";i:1;s:16:"HTML.Proprietary";i:7;s:14:"HTML.SafeEmbed";i:7;s:15:"HTML.SafeIframe";i:7;s:15:"HTML.SafeObject";i:7;s:18:"HTML.SafeScripting";i:8;s:11:"HTML.Strict";i:7;s:16:"HTML.TargetBlank";i:7;s:19:"HTML.TargetNoopener";i:7;s:21:"HTML.TargetNoreferrer";i:7;s:12:"HTML.TidyAdd";i:8;s:14:"HTML.TidyLevel";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:4:{s:4:"none";b:1;s:5:"light";b:1;s:6:"medium";b:1;s:5:"heavy";b:1;}}s:15:"HTML.TidyRemove";i:8;s:12:"HTML.Trusted";i:7;s:10:"HTML.XHTML";i:7;s:10:"Core.XHTML";O:8:"stdClass":2:{s:3:"key";s:10:"HTML.XHTML";s:7:"isAlias";b:1;}s:28:"Output.CommentScriptContents";i:7;s:26:"Core.CommentScriptContents";O:8:"stdClass":2:{s:3:"key";s:28:"Output.CommentScriptContents";s:7:"isAlias";b:1;}s:19:"Output.FixInnerHTML";i:7;s:18:"Output.FlashCompat";i:7;s:14:"Output.Newline";i:-1;s:15:"Output.SortAttr";i:7;s:17:"Output.TidyFormat";i:7;s:15:"Core.TidyFormat";O:8:"stdClass":2:{s:3:"key";s:17:"Output.TidyFormat";s:7:"isAlias";b:1;}s:17:"Test.ForceNoIconv";i:7;s:18:"URI.AllowedSchemes";i:8;s:18:"URI.AllowedSymbols";i:-1;s:8:"URI.Base";i:-1;s:17:"URI.DefaultScheme";i:-1;s:16:"URI.DefinitionID";i:-1;s:17:"URI.DefinitionRev";i:5;s:11:"URI.Disable";i:7;s:15:"Attr.DisableURI";O:8:"stdClass":2:{s:3:"key";s:11:"URI.Disable";s:7:"isAlias";b:1;}s:19:"URI.DisableExternal";i:7;s:28:"URI.DisableExternalResources";i:7;s:20:"URI.DisableResources";i:7;s:8:"URI.Host";i:-1;s:17:"URI.HostBlacklist";i:9;s:16:"URI.MakeAbsolute";i:7;s:9:"URI.Munge";i:-1;s:18:"URI.MungeResources";i:7;s:18:"URI.MungeSecretKey";i:-1;s:26:"URI.OverrideAllowedSchemes";i:7;s:19:"URI.SafeIframeHosts";i:-8;s:20:"URI.SafeIframeRegexp";i:-1;}} \ No newline at end of file diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt index 2c5924a..dc6e30f 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt @@ -5,10 +5,10 @@ DEFAULT: '' --DESCRIPTION-- Temporary prefix for IDs used in conjunction with %Attr.IDPrefix. If you need to allow multiple sets of user content on web page, you may need to -have a seperate prefix that changes with each iteration. This way, -seperately submitted user content displayed on the same page doesn't +have a separate prefix that changes with each iteration. This way, +separately submitted user content displayed on the same page doesn't clobber each other. Ideal values are unique identifiers for the content it represents (i.e. the id of the row in the database). Be sure to add a -seperator (like an underscore) at the end. Warning: this directive will +separator (like an underscore) at the end. Warning: this directive will not work unless %Attr.IDPrefix is set to a non-empty value! --# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt index 7a32914..63c2730 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt @@ -1,6 +1,6 @@ CSS.MaxImgLength TYPE: string/null -DEFAULT: '1200px' +DEFAULT: null VERSION: 3.1.1 --DESCRIPTION--

diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt index 64b114f..60cb409 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt @@ -7,7 +7,8 @@ This parameter determines whether or not the filter should convert input that is a full document with html and body tags to a fragment of just the contents of a body tag. This parameter is simply something HTML Purifier can do during an edge-case: for most inputs, this -processing is not necessary. +processing is not necessary. Warning: Full HTML purification has not +been implemented. See GitHub issue #7. --ALIASES-- Core.AcceptFullDocuments diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt index abb4999..4eedb34 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt @@ -8,6 +8,6 @@ converting all non-ASCII characters into decimal numeric entities before converting it to its native encoding. This means that even characters that can be expressed in the non-UTF-8 encoding will be entity-ized, which can be a real downer for encodings like Big5. It also assumes that the ASCII -repetoire is available, although this is the case for almost all encodings. +repertoire is available, although this is the case for almost all encodings. Anyway, use UTF-8! --# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt index 8983e2c..e469b88 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt @@ -16,7 +16,7 @@ DEFAULT: NULL

string lexer identifier
- This is a slim way of manually overridding the implementation. + This is a slim way of manually overriding the implementation. Currently recognized values are: DOMLex (the default PHP5 implementation) and DirectLex (the default PHP4 implementation). Only use this if diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveBlanks.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveBlanks.txt new file mode 100644 index 0000000..95e5285 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveBlanks.txt @@ -0,0 +1,10 @@ +Core.RemoveBlanks +TYPE: bool +DEFAULT: false +VERSION: 4.18 +--DESCRIPTION-- +

+ If set to true, blank nodes will be removed. This can be useful for maintaining + backwards compatibility when upgrading from previous versions of PHP. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt index e424c38..b2591e4 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt @@ -1,6 +1,6 @@ HTML.MaxImgLength TYPE: int/null -DEFAULT: 1200 +DEFAULT: null VERSION: 3.1.1 --DESCRIPTION--

diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt index 5eb6ec2..5a5c103 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt @@ -6,7 +6,7 @@ DEFAULT: false

Whether or not to permit iframe tags in untrusted documents. This directive must be accompanied by a whitelist of permitted iframes, - such as %URI.SafeIframeRegexp, otherwise it will fatally error. + such as %URI.SafeIframeRegexp or %URI.SafeIframeHosts, otherwise it will fatally error. This directive has no effect on strict doctypes, as iframes are not valid.

diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSymbols.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSymbols.txt new file mode 100644 index 0000000..d89a5f6 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSymbols.txt @@ -0,0 +1,7 @@ +URI.AllowedSymbols +TYPE: string/null +DEFAULT: '!$&\'()*+,;=' +--DESCRIPTION-- +If a system permits templated URLs, then the URI encoder may need extra +hints about which symbols to preserve. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeHosts.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeHosts.txt new file mode 100644 index 0000000..c32b52c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeHosts.txt @@ -0,0 +1,14 @@ +URI.SafeIframeHosts +TYPE: lookup/null +DEFAULT: null +--DESCRIPTION-- +

+ A whitelist which indicates what explicit hosts should be + allowed to embed iframe. See also %HTML.SafeIframeRegexp, + it has precedence over this config. Here are some example values: +

+
    +
  • www.youtube.com - Allow YouTube videos
  • +
  • maps.google.com - Allow Embedding a Google map
  • +
+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php index 543e3f8..d342995 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php @@ -142,12 +142,11 @@ public function getChildDef($def, $module) if ($return !== false) { return $return; } - // error-out - trigger_error( + + throw new Exception( 'Could not determine which ChildDef class to instantiate', E_USER_ERROR ); - return false; } /** diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Context.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Context.php index 00e509c..5a0e7b9 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Context.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Context.php @@ -24,11 +24,7 @@ class HTMLPurifier_Context public function register($name, &$ref) { if (array_key_exists($name, $this->_storage)) { - trigger_error( - "Name $name produces collision, cannot re-register", - E_USER_ERROR - ); - return; + throw new Exception("Name $name produces collision, cannot re-register"); } $this->_storage[$name] =& $ref; } @@ -43,10 +39,7 @@ public function &get($name, $ignore_error = false) { if (!array_key_exists($name, $this->_storage)) { if (!$ignore_error) { - trigger_error( - "Attempted to retrieve non-existent variable $name", - E_USER_ERROR - ); + throw new Exception("Attempted to retrieve non-existent variable $name"); } $var = null; // so we can return by reference return $var; @@ -61,11 +54,7 @@ public function &get($name, $ignore_error = false) public function destroy($name) { if (!array_key_exists($name, $this->_storage)) { - trigger_error( - "Attempted to destroy non-existent variable $name", - E_USER_ERROR - ); - return; + throw new Exception("Attempted to destroy non-existent variable $name"); } unset($this->_storage[$name]); } diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer.php index bfad967..6ba9ad2 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer.php @@ -139,8 +139,9 @@ public function cleanup($config) continue; } $key = substr($filename, 0, strlen($filename) - 4); - if ($this->isOld($key, $config)) { - unlink($dir . '/' . $filename); + $file = $dir . '/' . $filename; + if ($this->isOld($key, $config) && file_exists($file)) { + unlink($file); } } closedir($dh); diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php index acc1d64..9ad7b4b 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php @@ -86,7 +86,7 @@ public function get($doctype) $doctype = $this->aliases[$doctype]; } if (!isset($this->doctypes[$doctype])) { - trigger_error('Doctype ' . htmlspecialchars($doctype) . ' does not exist', E_USER_ERROR); + throw new Exception('Doctype ' . htmlspecialchars($doctype) . ' does not exist'); $anon = new HTMLPurifier_Doctype($doctype); return $anon; } diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php index d4791cc..910181b 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php @@ -12,7 +12,7 @@ class HTMLPurifier_Encoder */ private function __construct() { - trigger_error('Cannot instantiate encoder, call methods statically', E_USER_ERROR); + throw new Exception('Cannot instantiate encoder, call methods statically'); } /** @@ -390,7 +390,7 @@ public static function convertToUTF8($str, $config, $context) $str = self::unsafeIconv($encoding, 'utf-8//IGNORE', $str); if ($str === false) { // $encoding is not a valid encoding - trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR); + throw new Exception('Invalid encoding ' . $encoding); return ''; } // If the string is bjorked by Shift_JIS or a similar encoding @@ -404,12 +404,11 @@ public static function convertToUTF8($str, $config, $context) } $bug = HTMLPurifier_Encoder::testIconvTruncateBug(); if ($bug == self::ICONV_OK) { - trigger_error('Encoding not supported, please install iconv', E_USER_ERROR); + throw new Exception('Encoding not supported, please install iconv'); } else { - trigger_error( + throw new Exception( 'You have a buggy version of iconv, see https://bugs.php.net/bug.php?id=48147 ' . - 'and http://sourceware.org/bugzilla/show_bug.cgi?id=13541', - E_USER_ERROR + 'and http://sourceware.org/bugzilla/show_bug.cgi?id=13541' ); } } @@ -454,7 +453,7 @@ public static function convertFromUTF8($str, $config, $context) $str = mb_convert_encoding($str, 'ISO-8859-1', 'UTF-8'); return $str; } - trigger_error('Encoding not supported', E_USER_ERROR); + throw new Exception('Encoding not supported'); // You might be tempted to assume that the ASCII representation // might be OK, however, this is *not* universally true over all // encodings. So we take the conservative route here, rather @@ -545,10 +544,9 @@ public static function testIconvTruncateBug() } elseif (($c = strlen($r)) < 9000) { $code = self::ICONV_TRUNCATES; } elseif ($c > 9000) { - trigger_error( + throw new Exception( 'Your copy of iconv is extremely buggy. Please notify HTML Purifier maintainers: ' . - 'include your iconv version as per phpversion()', - E_USER_ERROR + 'include your iconv version as per phpversion()' ); } else { $code = self::ICONV_OK; diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php index 3ef2d09..1dcd10c 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php @@ -5,7 +5,7 @@ // $config or $context to the callback functions. /** - * Handles referencing and derefencing character entities + * Handles referencing and dereferencing character entities */ class HTMLPurifier_EntityParser { @@ -116,8 +116,8 @@ public function substituteAttrEntities($string) protected function entityCallback($matches) { $entity = $matches[0]; - $hex_part = @$matches[1]; - $dec_part = @$matches[2]; + $hex_part = isset($matches[1]) ? $matches[1] : null; + $dec_part = isset($matches[2]) ? $matches[2] : null; $named_part = empty($matches[3]) ? (empty($matches[4]) ? "" : $matches[4]) : $matches[3]; if ($hex_part !== NULL && $hex_part !== "") { return HTMLPurifier_Encoder::unichr(hexdec($hex_part)); diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter.php index c1f41ee..d52ae08 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter.php @@ -4,7 +4,7 @@ * Represents a pre or post processing filter on HTML Purifier's output * * Sometimes, a little ad-hoc fixing of HTML has to be done before - * it gets sent through HTML Purifier: you can use filters to acheive + * it gets sent through HTML Purifier: you can use filters to achieve * this effect. For instance, YouTube videos can be preserved using * this manner. You could have used a decorator for this task, but * PHP's support for them is not terribly robust, so we're going diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php index 6f8e779..e7e3cac 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php @@ -54,6 +54,11 @@ class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter */ private $_enum_attrdef; + /** + * @type HTMLPurifier_AttrDef_Enum + */ + private $_universal_attrdef; + public function __construct() { $this->_tidy = new csstidy(); @@ -70,6 +75,13 @@ public function __construct() 'focus' ) ); + $this->_universal_attrdef = new HTMLPurifier_AttrDef_Enum( + array( + 'initial', + 'inherit', + 'unset', + ) + ); } /** @@ -307,6 +319,11 @@ public function cleanCSS($css, $config, $context) unset($style[$name]); continue; } + $uni_ret = $this->_universal_attrdef->validate($value, $config, $context); + if ($uni_ret !== false) { + $style[$name] = $uni_ret; + continue; + } $def = $css_definition->info[$name]; $ret = $def->validate($value, $config, $context); if ($ret === false) { diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php index 276d836..d86509c 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php @@ -19,7 +19,7 @@ public function preFilter($html, $config, $context) $pre_regex = '#]+>.+?' . '(?:http:)?//www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+).+?#s'; $pre_replace = '\1'; - return preg_replace($pre_regex, $pre_replace, $html); + return preg_replace($pre_regex, $pre_replace, (string)$html); } /** @@ -31,7 +31,7 @@ public function preFilter($html, $config, $context) public function postFilter($html, $config, $context) { $post_regex = '#((?:v|cp)/[A-Za-z0-9\-_=]+)#'; - return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html); + return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), (string)$html); } /** diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php index eb56e2d..457fa90 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php @@ -244,7 +244,7 @@ public function generateAttributes($assoc_array_of_attributes, $element = '') // whitespace (in fact, most don't, at least for attributes // like alt, but an extra space at the end is barely // noticeable). Still, we have a configuration knob for - // this, since this transformation is not necesary if you + // this, since this transformation is not necessary if you // don't process user input with innerHTML or you don't plan // on supporting Internet Explorer. if ($this->_innerHTMLFix) { diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php index 9b7b334..dc2c33c 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php @@ -264,9 +264,8 @@ protected function setupConfigStuff($config) if (isset($this->info_content_sets['Block'][$block_wrapper])) { $this->info_block_wrapper = $block_wrapper; } else { - trigger_error( - 'Cannot use non-block element as block wrapper', - E_USER_ERROR + throw new Exception( + 'Cannot use non-block element as block wrapper' ); } @@ -276,11 +275,7 @@ protected function setupConfigStuff($config) $this->info_parent = $parent; $this->info_parent_def = $def; } else { - trigger_error( - 'Cannot use unrecognized element as parent', - E_USER_ERROR - ); - $this->info_parent_def = $this->manager->getElement($this->info_parent, true); + throw new Exception('Cannot use unrecognized element as parent'); } // support template text diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php index a9042a3..f02a563 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php @@ -28,7 +28,7 @@ public function setup($config) // HTML 4.01 specifies that ins/del must not contain block // elements when used in an inline context, chameleon is - // a complicated workaround to acheive this effect + // a complicated workaround to achieve this effect // Inline context ! Block context (exclamation mark is // separator, see getChildDef for parsing) diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php index f7e7c91..71dfc77 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php @@ -28,22 +28,28 @@ public function setup($config) if ($config->get('HTML.SafeIframe')) { $this->safe = true; } + $attrs = array( + 'src' => 'URI#embedded', + 'width' => 'Length', + 'height' => 'Length', + 'name' => 'ID', + 'scrolling' => 'Enum#yes,no,auto', + 'frameborder' => 'Enum#0,1', + 'longdesc' => 'URI', + 'marginheight' => 'Pixels', + 'marginwidth' => 'Pixels', + ); + + if ($config->get('HTML.Trusted')) { + $attrs['allowfullscreen'] = 'Bool#allowfullscreen'; + } + $this->addElement( 'iframe', 'Inline', 'Flow', 'Common', - array( - 'src' => 'URI#embedded', - 'width' => 'Length', - 'height' => 'Length', - 'name' => 'ID', - 'scrolling' => 'Enum#yes,no,auto', - 'frameborder' => 'Enum#0,1', - 'longdesc' => 'URI', - 'marginheight' => 'Pixels', - 'marginwidth' => 'Pixels', - ) + $attrs ); } } diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php index a0d4892..d1afde0 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php @@ -2,7 +2,7 @@ /** * XHTML 1.1 Ruby Annotation Module, defines elements that indicate - * short runs of text alongside base text for annotation or pronounciation. + * short runs of text alongside base text for annotation or pronunciation. */ class HTMLPurifier_HTMLModule_Ruby extends HTMLPurifier_HTMLModule { diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php index 76fd93a..bd926dc 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php @@ -112,9 +112,8 @@ public function makeFixesForLevel($fixes) return; } if (!isset($this->fixesForLevel[$this->defaultLevel])) { - trigger_error( - 'Default level ' . $this->defaultLevel . ' does not exist', - E_USER_ERROR + throw new Exception( + 'Default level ' . $this->defaultLevel . ' does not exist' ); return; } @@ -162,8 +161,7 @@ public function populate($fixes) $e->$type = $fix; break; default: - trigger_error("Fix type $type not supported", E_USER_ERROR); - break; + throw new Exception("Fix type $type not supported"); } } } diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php index a995161..5b3f3d5 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php @@ -1,7 +1,7 @@ loadLanguage($fallback); diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php index 1f552a1..793edc8 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php @@ -238,7 +238,7 @@ public function parseData($string, $is_attr, $config) */ public function tokenizeHTML($string, $config, $context) { - trigger_error('Call to abstract class', E_USER_ERROR); + throw new Exception('Call to abstract class'); } /** @@ -269,20 +269,6 @@ protected static function escapeCommentedCDATA($string) ); } - /** - * Special Internet Explorer conditional comments should be removed. - * @param string $string HTML string to process. - * @return string HTML with conditional comments removed. - */ - protected static function removeIEConditional($string) - { - return preg_replace( - '##si', // probably should generalize for all strings - '', - $string - ); - } - /** * Callback function for escapeCDATA() that does the work. * @@ -323,8 +309,6 @@ public function normalize($html, $config, $context) // escape CDATA $html = $this->escapeCDATA($html); - $html = $this->removeIEConditional($html); - // extract body from document if applicable if ($config->get('Core.ConvertDocumentToFragment')) { $e = false; diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php index 5e8104b..de79aaa 100644 --- a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php @@ -52,14 +52,7 @@ public function tokenizeHTML($html, $config, $context) // attempt to armor stray angled brackets that cannot possibly // form tags and thus are probably being used as emoticons if ($config->get('Core.AggressivelyFixLt')) { - $char = '[^a-z!\/]'; - $comment = "/|\z)/is"; - $html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html); - do { - $old = $html; - $html = preg_replace("/<($char)/i", '<\\1', $html); - } while ($html !== $old); - $html = preg_replace_callback($comment, array($this, 'callbackUndoCommentSubst'), $html); // fix comments + $html = $this->aggressivelyFixLt($html); } // preprocess html, essential for UTF-8 @@ -72,6 +65,9 @@ public function tokenizeHTML($html, $config, $context) if ($config->get('Core.AllowParseManyTags') && defined('LIBXML_PARSEHUGE')) { $options |= LIBXML_PARSEHUGE; } + if ($config->get('Core.RemoveBlanks') && defined('LIBXML_NOBLANKS')) { + $options |= LIBXML_NOBLANKS; + } set_error_handler(array($this, 'muteErrorHandler')); // loadHTML() fails on PHP 5.3 when second parameter is given @@ -285,7 +281,7 @@ public function muteErrorHandler($errno, $errstr) */ public function callbackUndoCommentSubst($matches) { - return ''; + + while (($startPos = strpos($html, $startTag, $offset)) !== false) { + $startPos += strlen($startTag); // Move past ` - - - - diff --git a/vendor/phpoffice/phpspreadsheet/.readthedocs.yaml b/vendor/phpoffice/phpspreadsheet/.readthedocs.yaml new file mode 100644 index 0000000..c671015 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/.readthedocs.yaml @@ -0,0 +1,12 @@ +# Read the Docs configuration file for MkDocs projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3" + +mkdocs: + configuration: mkdocs.yml diff --git a/vendor/phpoffice/phpspreadsheet/CHANGELOG.md b/vendor/phpoffice/phpspreadsheet/CHANGELOG.md index 170398c..400a8a5 100644 --- a/vendor/phpoffice/phpspreadsheet/CHANGELOG.md +++ b/vendor/phpoffice/phpspreadsheet/CHANGELOG.md @@ -3,7 +3,162 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) -and this project adheres to [Semantic Versioning](https://semver.org). +and this project adheres to [Semantic Versioning](https://semver.org). This is always true of the master branch. Some earlier branches, including the branch from which you are reading this file, remain supported and security fixes are applied to them; if the security fix represents a breaking change, it may have to be applied as a minor or patch version. + +## 2026-07-12 - 1.30.6 + +### Fixed + +- Security patches. + +## 2026-05-30 - 1.30.5 + +### Security Note + +- File::prohibitWrappers and Drawing::setPath now reject phar paths with extra leading slashes (e.g. phar:///…) that escaped the prior parse_url-based filter. + +### Fixed + +- Third-party security patches. + +## 2026-04-19 - 1.30.4 + +### Fixed + +- Security patches. + +## 2026-04-09 - 1.30.3 + +### Fixed + +- Security patches. +- Option to whitelist external images. Security-related backport of [PR #4793](https://github.com/PHPOffice/PhpSpreadsheet/pull/4793) + +## 2026-01-10 - 1.30.2 + +### Changed + +- Evaluation of WEBSERVICE no longer requires external client, but will use oldCalculatedValue unless the request is for a domain in a user-supplied whitelist. Security-related backport of [PR #4751](https://github.com/PHPOffice/PhpSpreadsheet/pull/4751) + +### Deprecated + +- Settings methods setHttpClient, unsetHttpClient, getHttpClient, and getRequestFactory are no longer used. No replacement. + +### Fixed + +- Changes to WEBSERVICE. Backport of [PR #4751](https://github.com/PHPOffice/PhpSpreadsheet/pull/4751) + +## 2025-10-25 - 1.30.1 + +### Functionally Frozen + +- Except for security changes, no further maintenance will be applied to this branch. +You are encouraged to upgrade to a maintained branch as soon as possible. +Maintained branches are master (preferred - version is 5.4.0 as of the date when this is being written), 3.10.x (current version is 3.10.3), and 2.4.x (2.4.3). +- Of particular note is that this branch should not run under Php 8.5+, and will *not* be updated to avoid deprecation notices introduced with Php 8.5. + +## 2025-08-10 - 1.30.0 + +### Breaking Changes + +- Images will be loaded from an external source (e.g. http://example.com/img.png) only if the reader is explicitly set to allow it via `$reader->setAllowExternalImages(true)`. We do not believe that loading of external images is a widely used feature. This is a necessary change for security purposes. It unfortunately breaks Semantic Versioning for reasons described above; there is no way to start a new major version for this branch. + +# 2025-07-23 - 1.29.12 + +### Added + +- Add to all readers the option to allow or forbid fetching external images. This is unconditionally allowed now. The default will be set to "allow", so no code changes are necessary. However, we are giving consideration to changing the default.[PR #4545](https://github.com/PHPOffice/PhpSpreadsheet/pull/4545) + +# 2025-06-22 - 1.29.11 + +### Changed + +- Allow php-cs-fixer to Handle Implicit Backslashes. + +### Added + +- Allow spreadsheet to be serialized. [PR #4405](https://github.com/PHPOffice/PhpSpreadsheet/pull/4405) + +### Fixed + +- TEXT and TIMEVALUE functions. [Issue #4249](https://github.com/PHPOffice/PhpSpreadsheet/issues/4249) [PR #4352](https://github.com/PHPOffice/PhpSpreadsheet/pull/4352) +- Removing Columns/Rows Containing Merged Cells. Backport of [PR #4465](https://github.com/PHPOffice/PhpSpreadsheet/pull/4465) +- Allow Xlsx Reader to Specify ParseHuge. [Issue #4260](https://github.com/PHPOffice/PhpSpreadsheet/issues/4260) [PR #4515](https://github.com/PHPOffice/PhpSpreadsheet/pull/4515) + +# 2025-02-07 - 1.29.10 + +### Changed + +- Allow version 1 and 2 of `composer/pcre`. + +### Fixed + +- Xls writer Parser Mishandling True/False Argument. Backport of [PR #4333](https://github.com/PHPOffice/PhpSpreadsheet/pull/4333) +- Xls writer Parser Parse By Character Not Byte. Backport of [PR #4344](https://github.com/PHPOffice/PhpSpreadsheet/pull/4344) + +# 2025-01-26 - 1.29.9 + +### Fixed + +- Backported security patch for control characters in protocol. +- Use Composer\Pcre in Xls/Parser. Partial backport of [PR #4203](https://github.com/PHPOffice/PhpSpreadsheet/pull/4203) + +# 2025-01-11 - 1.29.8 + +### Deprecated + +- Worksheet::getHashCode is no longer needed. + +### Fixed + +- Backported security patch for Html navigation. +- Change hash code for worksheet. Backport of [PR #4207](https://github.com/PHPOffice/PhpSpreadsheet/pull/4207) +- Retitling cloned worksheets. Backport of [PR #4302](https://github.com/PHPOffice/PhpSpreadsheet/pull/4302) + +# 2024-12-26 - 1.29.7 + +### Deprecated + +- Drawing::setIsUrl is unneeded. The property is set when setPath determines whether path is a url. + +### Fixed + +- More context options may be needed for http(s) image. Backport of [PR #4276](https://github.com/PHPOffice/PhpSpreadsheet/pull/4276) +- Backported security patches for Samples. +- Backported security patches for Html Writer. + +## 1.29.6 - 2024-12-08 + +### Fixed + +- Fix Minor Break Handling Drawings. Backport of [PR #4244](https://github.com/PHPOffice/PhpSpreadsheet/pull/4244) +- Upgrade locked version of Tcpdf (security advisory). +- Upgrade locked version of Dompdf (Php8.4 compatibility). +- Remove unnecessary files from Composer package. + +## 1.29.5 - 2024-11-22 + +### Changed + +- Settings::libXmlLoaderOptions is ignored. Backport of [PR #4233](https://github.com/PHPOffice/PhpSpreadsheet/pull/4233) + +### Deprecated + +- Settings::setLibXmlLoaderOptions() and Settings::getLibXmlLoaderOptions() are no longer needed - no replacement. + +## 1.29.4 - 2024-11-10 + +### Fixed + +- 1.29.3 omitted +- Backported security patches. +- Write ignoredErrors Tag Before Drawings. Backport of [PR #4212](https://github.com/PHPOffice/PhpSpreadsheet/pull/4212) intended for 3.4.0. +- Changes to ROUNDDOWN/ROUNDUP/TRUNC. Backport of [PR #4214](https://github.com/PHPOffice/PhpSpreadsheet/pull/4214) intended for 3.4.0. +- Replace str_starts_with in Drawing. [Issue #4215](https://github.com/PHPOffice/PhpSpreadsheet/issues/4215) + +### Added + +- Method to Test Whether Csv Will Be Affected by Php9. Backport of [PR #4189](https://github.com/PHPOffice/PhpSpreadsheet/pull/4189) intended for 3.4.0. ## 1.29.2 - 2024-09-29 diff --git a/vendor/phpoffice/phpspreadsheet/LICENSE b/vendor/phpoffice/phpspreadsheet/LICENSE index 3ec5723..e4bc4a3 100644 --- a/vendor/phpoffice/phpspreadsheet/LICENSE +++ b/vendor/phpoffice/phpspreadsheet/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 PhpSpreadsheet Authors +Copyright (c) 2019-2026 PhpSpreadsheet Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/phpoffice/phpspreadsheet/README.md b/vendor/phpoffice/phpspreadsheet/README.md index a69c3af..af83bbc 100644 --- a/vendor/phpoffice/phpspreadsheet/README.md +++ b/vendor/phpoffice/phpspreadsheet/README.md @@ -1,22 +1,33 @@ # PhpSpreadsheet [![Build Status](https://github.com/PHPOffice/PhpSpreadsheet/workflows/main/badge.svg)](https://github.com/PHPOffice/PhpSpreadsheet/actions) -[![Code Quality](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/?branch=master) -[![Code Coverage](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/?branch=master) +[![Code Coverage](https://coveralls.io/repos/github/PHPOffice/PhpSpreadsheet/badge.svg?branch=master)](https://coveralls.io/github/PHPOffice/PhpSpreadsheet?branch=master) [![Total Downloads](https://img.shields.io/packagist/dt/PHPOffice/PhpSpreadsheet)](https://packagist.org/packages/phpoffice/phpspreadsheet) [![Latest Stable Version](https://img.shields.io/github/v/release/PHPOffice/PhpSpreadsheet)](https://packagist.org/packages/phpoffice/phpspreadsheet) -[![License](https://img.shields.io/github/license/PHPOffice/PhpSpreadsheet)](https://packagist.org/packages/phpoffice/phpspreadsheet) +[![License](https://poser.pugx.org/phpoffice/phpspreadsheet/license)](https://packagist.org/packages/phpoffice/phpspreadsheet) [![Join the chat at https://gitter.im/PHPOffice/PhpSpreadsheet](https://img.shields.io/badge/GITTER-join%20chat-green.svg)](https://gitter.im/PHPOffice/PhpSpreadsheet) PhpSpreadsheet is a library written in pure PHP and offers a set of classes that allow you to read and write various spreadsheet file formats such as Excel and LibreOffice Calc. +This branch (1.30.x) is *not* the latest version of PhpSpreadsheet, and may therefore lack features and bug fixes found in the latest version. + +## Security Changes Only + +Except for security changes, no further maintenance will be applied to this branch. +You are encouraged to upgrade to a maintained branch as soon as possible. +Maintained branches are master (preferred - version is 5.4.0 as of the date when this is being written), 3.10.x (current version is 3.10.3), and 2.4.x (2.4.3). + +Of particular note is that this branch should not run under Php 8.5, and will *not* be updated to avoid deprecation notices which will be introduced with Php 8.5. + ## PHP Version Support -LTS: Support for PHP versions will only be maintained for a period of six months beyond the +LTS: For maintained branches, support for PHP versions will only be maintained for a period of six months beyond the [end of life](https://www.php.net/supported-versions) of that PHP version. Currently the required PHP minimum version is PHP __7.4__, and we [will support that version](https://www.php.net/eol.php) until 28th June 2023. +However, since this branch is no longer maintained, that policy is not enforced. +The PHP maximum version supported by this release is PHP __8.4__. See the `composer.json` for other requirements. diff --git a/vendor/phpoffice/phpspreadsheet/composer.json b/vendor/phpoffice/phpspreadsheet/composer.json index aabddd3..d935d37 100644 --- a/vendor/phpoffice/phpspreadsheet/composer.json +++ b/vendor/phpoffice/phpspreadsheet/composer.json @@ -12,6 +12,7 @@ "spreadsheet" ], "config": { + "process-timeout": 600, "sort-packages": true, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true @@ -38,6 +39,9 @@ }, { "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" } ], "scripts": { @@ -61,7 +65,7 @@ ] }, "require": { - "php": "^7.4 || ^8.0", + "php": ">=7.4.0 <8.5.0", "ext-ctype": "*", "ext-dom": "*", "ext-fileinfo": "*", @@ -75,17 +79,17 @@ "ext-xmlwriter": "*", "ext-zip": "*", "ext-zlib": "*", + "composer/pcre": "^1||^2||^3", "ezyang/htmlpurifier": "^4.15", "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-main", - "dompdf/dompdf": "^1.0 || ^2.0", + "doctrine/instantiator": "^1.5", + "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", "friendsofphp/php-cs-fixer": "^3.2", "mitoteam/jpgraph": "^10.3", "mpdf/mpdf": "^8.1.1", diff --git a/vendor/phpoffice/phpspreadsheet/phpstan-baseline.neon b/vendor/phpoffice/phpspreadsheet/phpstan-baseline.neon deleted file mode 100644 index e107b26..0000000 --- a/vendor/phpoffice/phpspreadsheet/phpstan-baseline.neon +++ /dev/null @@ -1,351 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Offset 1 does not exist on array\\{0\\?\\: string, 1\\?\\: ''\\|numeric\\-string, 2\\: '\\+', 3\\?\\: ''\\|numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Offset 1 does not exist on array\\{0\\?\\: string, 1\\?\\: ''\\|numeric\\-string, 2\\: ',', 3\\?\\: ''\\|numeric\\-string\\}\\.$#" - count: 2 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Offset 1 does not exist on array\\{0\\?\\: string, 1\\?\\: ''\\|numeric\\-string, 2\\: '\\-', 3\\?\\: ''\\|numeric\\-string\\}\\.$#" - count: 2 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Offset 2 does not exist on array\\{0\\?\\: string, 1\\?\\: string, 2\\?\\: string, 3\\?\\: string, 4\\?\\: string, 5\\?\\: string, 6\\?\\: non\\-empty\\-string, 7\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Offset 3 does not exist on array\\{0\\?\\: string, 1\\: ''\\|numeric\\-string, 2\\: '\\-', 3\\?\\: ''\\|numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Offset 3 does not exist on array\\{0\\?\\: string, 1\\?\\: ''\\|numeric\\-string, 2\\: ',', 3\\?\\: ''\\|numeric\\-string\\}\\.$#" - count: 2 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Offset 3 does not exist on array\\{0\\?\\: string, 1\\?\\: ''\\|numeric\\-string, 2\\: '\\-', 3\\?\\: ''\\|numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Offset 8 on array\\{string, string, string, string, string, string, non\\-empty\\-string, numeric\\-string\\} in isset\\(\\) does not exist\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Strict comparison using \\=\\=\\= between mixed and null will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Binary operation \"%%\" between int\\|string and 100 results in an error\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php - - - - message: "#^Binary operation \"%%\" between int\\|string and 4 results in an error\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php - - - - message: "#^Binary operation \"%%\" between int\\|string and 400 results in an error\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php - - - - message: "#^Binary operation \"%%\" between string and 24 results in an error\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php - - - - message: "#^Binary operation \"\\-\" between 1 and array\\|float\\|string results in an error\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Engineering/Erf.php - - - - message: "#^Cannot call method getTokenSubType\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken\\|null\\.$#" - count: 4 - path: src/PhpSpreadsheet/Calculation/FormulaParser.php - - - - message: "#^Cannot call method getTokenType\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken\\|null\\.$#" - count: 9 - path: src/PhpSpreadsheet/Calculation/FormulaParser.php - - - - message: "#^Cannot call method setTokenSubType\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken\\|null\\.$#" - count: 5 - path: src/PhpSpreadsheet/Calculation/FormulaParser.php - - - - message: "#^Cannot call method setValue\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken\\|null\\.$#" - count: 5 - path: src/PhpSpreadsheet/Calculation/FormulaParser.php - - - - message: "#^Strict comparison using \\=\\=\\= between PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken and null will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/FormulaParser.php - - - - message: "#^Offset 1 does not exist on array\\{0\\?\\: string, 1\\?\\: non\\-falsy\\-string, 2\\?\\: string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Functions.php - - - - message: "#^Offset 2 does not exist on array\\{0\\?\\: string, 1\\?\\: non\\-falsy\\-string, 2\\?\\: string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Functions.php - - - - message: "#^Offset 2 does not exist on array\\{0\\?\\: string, 1\\?\\: string, 2\\?\\: string, 3\\?\\: string, 4\\?\\: string, 5\\?\\: string, 6\\?\\: non\\-empty\\-string, 7\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Information/Value.php - - - - message: "#^Offset 6 does not exist on array\\{0\\?\\: string, 1\\?\\: string, 2\\?\\: string, 3\\?\\: string, 4\\?\\: string, 5\\?\\: string, 6\\?\\: non\\-empty\\-string, 7\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Information/Value.php - - - - message: "#^Offset 7 does not exist on array\\{0\\?\\: string, 1\\?\\: string, 2\\?\\: string, 3\\?\\: string, 4\\?\\: string, 5\\?\\: string, 6\\?\\: non\\-empty\\-string, 7\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Information/Value.php - - - - message: "#^Offset 3 does not exist on array\\{0\\?\\: string, 1\\?\\: string, 2\\?\\: string, 3\\?\\: string, 4\\?\\: string, 5\\?\\: string, 6\\?\\: non\\-empty\\-string, 7\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/LookupRef/Formula.php - - - - message: "#^Offset 6 does not exist on array\\{0\\?\\: string, 1\\?\\: string, 2\\?\\: string, 3\\?\\: string, 4\\?\\: string, 5\\?\\: string, 6\\?\\: non\\-empty\\-string, 7\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/LookupRef/Formula.php - - - - message: "#^Offset 7 does not exist on array\\{0\\?\\: string, 1\\?\\: string, 2\\?\\: string, 3\\?\\: string, 4\\?\\: string, 5\\?\\: string, 6\\?\\: non\\-empty\\-string, 7\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/LookupRef/Formula.php - - - - message: "#^Binary operation \"/\" between float\\|int and float\\|string results in an error\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Statistical/Deviations.php - - - - message: "#^Binary operation \"\\-\" between 1 and array\\|float\\|string results in an error\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php - - - - message: "#^Binary operation \"\\-\" between 1 and array\\|float\\|string results in an error\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php - - - - message: "#^Offset 'col' does not exist on array\\{0\\?\\: string, col\\?\\: non\\-empty\\-string, 1\\?\\: non\\-empty\\-string, row\\?\\: non\\-empty\\-string, 2\\?\\: non\\-empty\\-string\\}\\.$#" - count: 2 - path: src/PhpSpreadsheet/Cell/AddressHelper.php - - - - message: "#^Offset 'row' does not exist on array\\{0\\?\\: string, col\\?\\: non\\-empty\\-string, 1\\?\\: non\\-empty\\-string, row\\?\\: non\\-empty\\-string, 2\\?\\: non\\-empty\\-string\\}\\.$#" - count: 2 - path: src/PhpSpreadsheet/Cell/AddressHelper.php - - - - message: "#^Parameter \\#1 \\$num of function dechex expects int, string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Offset 'size' does not exist on array\\{0\\?\\: string, size\\?\\: non\\-empty\\-string, 1\\?\\: non\\-empty\\-string, unit\\?\\: non\\-falsy\\-string, 2\\?\\: non\\-falsy\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Size.php - - - - message: "#^Parameter \\#2 \\$length of function fgetcsv expects int\\<0, max\\>\\|null, int\\|null given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Csv.php - - - - message: "#^Parameter \\#1 \\$namespace of method DOMDocument\\:\\:getElementsByTagNameNS\\(\\) expects string, string\\|null given\\.$#" - count: 3 - path: src/PhpSpreadsheet/Reader/Ods.php - - - - message: "#^Parameter \\#1 \\$namespace of method DOMElement\\:\\:getElementsByTagNameNS\\(\\) expects string, string\\|null given\\.$#" - count: 7 - path: src/PhpSpreadsheet/Reader/Ods.php - - - - message: "#^Parameter \\#2 \\$tableNs of class PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\AutoFilter constructor expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods.php - - - - message: "#^Parameter \\#2 \\$tableNs of class PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\DefinedNames constructor expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods.php - - - - message: "#^Parameter \\#2 \\$tableNs of method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\:\\:processMergedCells\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods.php - - - - message: "#^Parameter \\#3 \\$configNs of method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\:\\:lookForActiveSheet\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods.php - - - - message: "#^Parameter \\#3 \\$configNs of method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\:\\:lookForSelectedCells\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$officeNs \\(string\\) does not accept string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$stylesFo \\(string\\) does not accept string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$stylesNs \\(string\\) does not accept string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$tableNs \\(string\\) does not accept string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - - message: "#^Parameter \\#1 \\$column of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\:\\:getColumnDimension\\(\\) expects string, \\(float\\|int\\) given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Slk.php - - - - message: "#^Offset 4 does not exist on array\\{0\\: string, 1\\: non\\-empty\\-string, 2\\: numeric\\-string, 3\\: string, 4\\?\\: string, 5\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Offset 5 does not exist on array\\{0\\: string, 1\\: non\\-empty\\-string, 2\\: numeric\\-string, 3\\: string, 4\\?\\: string, 5\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Variable \\$column in empty\\(\\) always exists and is not falsy\\.$#" - count: 1 - path: src/PhpSpreadsheet/ReferenceHelper.php - - - - message: "#^Offset 1 does not exist on array\\{0\\?\\: string, 1\\?\\: non\\-falsy\\-string, 2\\?\\: non\\-empty\\-string\\|numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php - - - - message: "#^Offset 2 does not exist on array\\{0\\?\\: string, 1\\?\\: non\\-falsy\\-string, 2\\?\\: non\\-empty\\-string\\|numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php - - - - message: "#^Variable \\$language on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/Wizard/NumberBase.php - - - - message: "#^Offset 'mime' on array\\{0\\: int\\<0, max\\>, 1\\: int\\<0, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\} on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/MemoryDrawing.php - - - - message: "#^Variable \\$rgb in empty\\(\\) always exists and is not falsy\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/MemoryDrawing.php - - - - message: "#^Parameter \\#1 \\$filename of function fopen expects string, resource\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/BaseWriter.php - - - - message: "#^Parameter \\#1 \\$url of function parse_url expects string, resource\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/BaseWriter.php - - - - message: "#^Offset 'mime' does not exist on array\\{\\}\\|array\\{0\\: int\\<0, max\\>, 1\\: int\\<0, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Offset 0 does not exist on array\\{\\}\\|array\\{0\\: int\\<0, max\\>, 1\\: int\\<0, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Offset 1 does not exist on array\\{\\}\\|array\\{0\\: int\\<0, max\\>, 1\\: int\\<0, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Variable \\$column in empty\\(\\) always exists and is not falsy\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Ods/Formula.php - - - - message: "#^Variable \\$column in empty\\(\\) always exists and is not falsy\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php - - - - message: "#^Offset 2 on array\\{0\\: int\\<0, max\\>, 1\\: int\\<0, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\} on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls.php - - - - message: "#^Offset 2 does not exist on array\\{0\\?\\: string, 1\\?\\: ''\\|'\\$', 2\\?\\: non\\-falsy\\-string, 3\\?\\: ''\\|'\\$', 4\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls/Parser.php - - - - message: "#^Offset 2 does not exist on array\\{0\\?\\: string, 1\\?\\: ''\\|'\\$', 2\\?\\: numeric\\-string, 3\\?\\: ''\\|'\\$', 4\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls/Parser.php - - - - message: "#^Offset 4 does not exist on array\\{0\\?\\: string, 1\\?\\: ''\\|'\\$', 2\\?\\: non\\-falsy\\-string, 3\\?\\: ''\\|'\\$', 4\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls/Parser.php - - - - message: "#^Offset 4 does not exist on array\\{0\\?\\: string, 1\\?\\: ''\\|'\\$', 2\\?\\: numeric\\-string, 3\\?\\: ''\\|'\\$', 4\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls/Parser.php - - - - message: "#^Parameter \\#2 \\$length of function fread expects int\\<1, max\\>, int\\<0, max\\> given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - - - - message: "#^Comparison operation \"\\>\\=\" between int\\<5, 7\\> and 3 is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php - - - - message: "#^Offset 1 does not exist on array\\{0\\?\\: string, 1\\?\\: numeric\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php diff --git a/vendor/phpoffice/phpspreadsheet/phpstan-conditional.php b/vendor/phpoffice/phpspreadsheet/phpstan-conditional.php deleted file mode 100644 index 9b1150b..0000000 --- a/vendor/phpoffice/phpspreadsheet/phpstan-conditional.php +++ /dev/null @@ -1,86 +0,0 @@ - '~^Method .* has invalid return type GdImage\.$~', - 'path' => __DIR__ . '/src/PhpSpreadsheet/Shared/Drawing.php', - 'count' => 1, - ]; - $config['parameters']['ignoreErrors'][] = [ - 'message' => '~^Property .* has unknown class GdImage as its type\.$~', - 'path' => __DIR__ . '/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php', - 'count' => 1, - ]; - $config['parameters']['ignoreErrors'][] = [ - 'message' => '~^Method .* has invalid return type GdImage\.$~', - 'path' => __DIR__ . '/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php', - 'count' => 1, - ]; - $config['parameters']['ignoreErrors'][] = [ - 'message' => '~^Parameter .* of method .* has invalid type GdImage\.$~', - 'path' => __DIR__ . '/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php', - 'count' => 1, - ]; - $config['parameters']['ignoreErrors'][] = [ - 'message' => '~^Class GdImage not found\.$~', - 'path' => __DIR__ . '/src/PhpSpreadsheet/Writer/Xls/Worksheet.php', - 'count' => 1, - ]; - $config['parameters']['ignoreErrors'][] = [ - 'message' => '~^Parameter .* of method .* has invalid type GdImage\.$~', - 'path' => __DIR__ . '/src/PhpSpreadsheet/Writer/Xls/Worksheet.php', - 'count' => 1, - ]; - // GdImage with Phpstan 1.9.2 - $config['parameters']['ignoreErrors'][] = [ - 'message' => '~Class GdImage not found.*$~', - 'path' => __DIR__ . '/tests/PhpSpreadsheetTests/Worksheet/MemoryDrawingTest.php', - 'count' => 3, - ]; - // Erroneous analysis by Phpstan before PHP8 - 3rd parameter is nullable - // Fixed for Php7 with Phpstan 1.9. - //$config['parameters']['ignoreErrors'][] = [ - // 'message' => '#^Parameter \\#3 \\$namespace of method XMLWriter\\:\\:startElementNs\\(\\) expects string, null given\\.$#', - // 'path' => __DIR__ . '/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php', - // 'count' => 8, - //]; - // Erroneous analysis by Phpstan before PHP8 - mb_strlen does not return false - $config['parameters']['ignoreErrors'][] = [ - 'message' => '#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:countCharacters\\(\\) should return int but returns int(<0, max>)?\\|false\\.$#', - 'path' => __DIR__ . '/src/PhpSpreadsheet/Shared/StringHelper.php', - 'count' => 1, - ]; - // New with Phpstan 1.9.2 for Php7 only - $config['parameters']['ignoreErrors'][] = [ - 'message' => '#^Parameter \\#2 \\.\\.\\.\\$args of function array_merge expects array, array\\|false given.$#', - 'path' => __DIR__ . '/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php', - 'count' => 1, - ]; - $config['parameters']['ignoreErrors'][] = [ - 'message' => '#^Parameter \\#1 \\$input of function array_chunk expects array, array\\|false given.$#', - 'path' => __DIR__ . '/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php', - 'count' => 1, - ]; - $config['parameters']['ignoreErrors'][] = [ - 'message' => '#^Parameter \\#2 \\$array of function array_map expects array, array\\|false given.$#', - 'path' => __DIR__ . '/src/PhpSpreadsheet/Calculation/MathTrig/Random.php', - 'count' => 1, - ]; - $config['parameters']['ignoreErrors'][] = [ - 'message' => '#^Parameter \\#2 \\.\\.\\.\\$args of function array_merge expects array, array\\|false given.$#', - 'path' => __DIR__ . '/src/PhpSpreadsheet/Calculation/TextData/Text.php', - 'count' => 1, - ]; -} else { - // Flagged in Php8+ - unsure how to correct code - $config['parameters']['ignoreErrors'][] = [ - 'message' => '#^Binary operation "/" between float and array[|]float[|]int[|]string results in an error.#', - 'path' => __DIR__ . '/src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php', - 'count' => 1, - ]; -} - -return $config; diff --git a/vendor/phpoffice/phpspreadsheet/phpstan.neon.dist b/vendor/phpoffice/phpspreadsheet/phpstan.neon.dist deleted file mode 100644 index f49ab2e..0000000 --- a/vendor/phpoffice/phpspreadsheet/phpstan.neon.dist +++ /dev/null @@ -1,27 +0,0 @@ -includes: - - phpstan-baseline.neon - - phpstan-conditional.php - - vendor/phpstan/phpstan-phpunit/extension.neon - - vendor/phpstan/phpstan-phpunit/rules.neon - -parameters: - level: 8 - paths: - - src/ - - tests/ - excludePaths: - - src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php - - src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php - - src/PhpSpreadsheet/Collection/Memory/SimpleCache3.php - - src/PhpSpreadsheet/Writer/ZipStream2.php - - src/PhpSpreadsheet/Writer/ZipStream3.php - parallel: - processTimeout: 300.0 - ignoreErrors: - - identifier: missingType.iterableValue - - '~^Parameter \#1 \$im(age)? of function (imagedestroy|imageistruecolor|imagealphablending|imagesavealpha|imagecolortransparent|imagecolorsforindex|imagesavealpha|imagesx|imagesy|imagepng) expects (GdImage|resource), GdImage\|resource given\.$~' - - '~^Parameter \#2 \$src_im(age)? of function imagecopy expects (GdImage|resource), GdImage\|resource given\.$~' - # Accept a bit anything for assert methods - - '~^Parameter \#2 .* of static method PHPUnit\\Framework\\Assert\:\:assert\w+\(\) expects .*, .* given\.$~' - - '~^Method PhpOffice\\PhpSpreadsheetTests\\.*\:\:test.*\(\) has parameter \$args with no type specified\.$~' diff --git a/vendor/phpoffice/phpspreadsheet/phpunit10.xml.dist b/vendor/phpoffice/phpspreadsheet/phpunit10.xml.dist deleted file mode 100644 index 207d8ec..0000000 --- a/vendor/phpoffice/phpspreadsheet/phpunit10.xml.dist +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - ./tests/PhpSpreadsheetTests - - - - ./src - - - diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php index 5061233..c7a97b6 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php @@ -46,7 +46,7 @@ class Calculation // Defined Names: Named Range of cells, or Named Formulae const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)'; // Structured Reference (Fully Qualified and Unqualified) - const CALCULATION_REGEXP_STRUCTURED_REFERENCE = '([\p{L}_\\\\][\p{L}\p{N}\._]+)?(\[(?:[^\d\]+-])?)'; + const CALCULATION_REGEXP_STRUCTURED_REFERENCE = '([\p{L}_\\\][\p{L}\p{N}\._]+)?(\[(?:[^\d\]+-])?)'; // Error const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?'; @@ -152,13 +152,6 @@ class Calculation */ public $formulaError; - /** - * Reference Helper. - * - * @var ReferenceHelper - */ - private static $referenceHelper; - /** * An array of the nested cell references accessed by the calculation engine, used for the debug log. * @@ -2796,6 +2789,7 @@ public static function getExcelConstants(string $key) 'category' => Category::CATEGORY_WEB, 'functionCall' => [Web\Service::class, 'webService'], 'argumentCount' => '1', + 'passCellReference' => true, ], 'WEEKDAY' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, @@ -2925,7 +2919,6 @@ public function __construct(?Spreadsheet $spreadsheet = null) $this->cyclicReferenceStack = new CyclicReferenceStack(); $this->debugLog = new Logger($this->cyclicReferenceStack); $this->branchPruner = new BranchPruner($this->branchPruningEnabled); - self::$referenceHelper = ReferenceHelper::getInstance(); } private static function loadLocales(): void @@ -5730,11 +5723,14 @@ private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksh $recursiveCalculationCellAddress = $recursiveCalculationCell->getCoordinate(); // Adjust relative references in ranges and formulae so that we execute the calculation for the correct rows and columns - $definedNameValue = self::$referenceHelper->updateFormulaReferencesAnyWorksheet( - $definedNameValue, - Coordinate::columnIndexFromString($cell->getColumn()) - 1, - $cell->getRow() - 1 - ); + $definedNameValue = ReferenceHelper::getInstance() + ->updateFormulaReferencesAnyWorksheet( + $definedNameValue, + Coordinate::columnIndexFromString( + $cell->getColumn() + ) - 1, + $cell->getRow() - 1 + ); $this->debugLog->writeDebugLog('Value adjusted for relative references is %s', $definedNameValue); diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php index 1d59988..e9e1a9b 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php @@ -46,7 +46,7 @@ public static function fromString($dateValue) } // try to parse as date iff there is at least one digit - if (is_string($dateValue) && preg_match('/\\d/', $dateValue) !== 1) { + if (is_string($dateValue) && preg_match('/\d/', $dateValue) !== 1) { return ExcelError::VALUE(); } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php index 78d67b8..17629ae 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; +use Composer\Pcre\Preg; use Datetime; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Functions; @@ -12,6 +13,19 @@ class TimeValue { use ArrayEnabled; + private const EXTRACT_TIME = '/\b' + . '(\d+)' // match[1] - hour + . '(:' // start of match[2] (rest of string) - colon + . '(\d+' // start of match[3] - minute + . '(:\d+' // start of match[4] - colon and seconds + . '([.]\d+)?' // match[5] - optional decimal point followed by fractional seconds + . ')?' // end of match[4], which is optional + . ')' // end of match 3 + // Excel does not require 'm' to trail 'a' or 'p'; Php does + . '(\s*(a|p))?' // match[6] optional whitespace followed by optional match[7] a or p + . ')' // end of match[2] + . '/i'; + /** * TIMEVALUE. * @@ -43,17 +57,20 @@ public static function fromString($timeValue) } // try to parse as time iff there is at least one digit - if (is_string($timeValue) && preg_match('/\\d/', $timeValue) !== 1) { + if (is_string($timeValue) && preg_match('/\d/', $timeValue) !== 1) { return ExcelError::VALUE(); } $timeValue = trim($timeValue ?? '', '"'); - $timeValue = str_replace(['/', '.'], '-', $timeValue); - - $arraySplit = preg_split('/[\/:\-\s]/', $timeValue) ?: []; - if ((count($arraySplit) == 2 || count($arraySplit) == 3) && $arraySplit[0] > 24) { - $arraySplit[0] = ($arraySplit[0] % 24); - $timeValue = implode(':', $arraySplit); + if (Preg::isMatch(self::EXTRACT_TIME, $timeValue, $matches)) { + if (empty($matches[6])) { // am/pm + $hour = (int) $matches[1]; + $timeValue = ($hour % 24) . $matches[2]; + } elseif ($matches[6] === $matches[7]) { // Excel wants space before am/pm + return ExcelError::VALUE(); + } else { + $timeValue = $matches[0] . 'm'; + } } $PHPDateArray = Helpers::dateParse($timeValue); diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php index 59cc3e3..d7c2ffd 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php @@ -28,7 +28,7 @@ final class StructuredReference implements Operand self::ITEM_SPECIFIER_TOTALS, ]; - private const TABLE_REFERENCE = '/([\p{L}_\\\\][\p{L}\p{N}\._]+)?(\[(?:[^\]\[]+|(?R))*+\])/miu'; + private const TABLE_REFERENCE = '/([\p{L}_\\\][\p{L}\p{N}\._]+)?(\[(?:[^\]\[]+|(?R))*+\])/miu'; private string $value; diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php index 14c5ae5..e3bae6b 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php @@ -219,7 +219,7 @@ private function parseToTokens(): void // scientific notation check if (strpos(self::OPERATORS_SN, $this->formula[$index]) !== false) { if (strlen($value) > 1) { - if (preg_match('/^[1-9]{1}(\\.\\d+)?E{1}$/', $this->formula[$index]) != 0) { + if (preg_match('/^[1-9]{1}(\.\d+)?E{1}$/', $this->formula[$index]) != 0) { $value .= $this->formula[$index]; ++$index; diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php index 0ec396f..7889276 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php @@ -26,6 +26,8 @@ class Functions const RETURNDATE_PHP_DATETIME_OBJECT = 'O'; const RETURNDATE_EXCEL = 'E'; + public const NOT_YET_IMPLEMENTED = '#Not Yet Implemented'; + /** * Compatibility mode to use for error checking and responses. * @@ -663,7 +665,7 @@ public static function expandDefinedName(string $coordinate, Cell $cell): string public static function trimTrailingRange(string $coordinate): string { - return (string) preg_replace('/:[\\w\$]+$/', '', $coordinate); + return (string) preg_replace('/:[\w\$]+$/', '', $coordinate); } public static function trimSheetFromCellReference(string $coordinate): string diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php index 371ad8b..8282ea2 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php @@ -6,10 +6,10 @@ class WildcardMatch { private const SEARCH_SET = [ '~~', // convert double tilde to unprintable value - '~\\*', // convert tilde backslash asterisk to [*] (matches literal asterisk in regexp) - '\\*', // convert backslash asterisk to .* (matches string of any length in regexp) - '~\\?', // convert tilde backslash question to [?] (matches literal question mark in regexp) - '\\?', // convert backslash question to . (matches one character in regexp) + '~\*', // convert tilde backslash asterisk to [*] (matches literal asterisk in regexp) + '\*', // convert backslash asterisk to .* (matches string of any length in regexp) + '~\?', // convert tilde backslash question to [?] (matches literal question mark in regexp) + '\?', // convert backslash question to . (matches one character in regexp) "\x1c", // convert original double tilde to single tilde ]; diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Round.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Round.php index 94b6341..0014413 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Round.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Round.php @@ -5,6 +5,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +// following added in Php8.4 +use RoundingMode; class Round { @@ -67,22 +69,19 @@ public static function up($number, $digits) return 0.0; } - $digitsPlus1 = $digits + 1; - if ($number < 0.0) { - if ($digitsPlus1 < 0) { - return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); - } - $result = sprintf("%.{$digitsPlus1}f", $number - 0.5 * 0.1 ** $digits); - - return round((float) $result, $digits, PHP_ROUND_HALF_DOWN); + if (PHP_VERSION_ID >= 80400) { + return round( + (float) (string) $number, + $digits, + RoundingMode::AwayFromZero //* @phpstan-ignore-line + ); } - if ($digitsPlus1 < 0) { - return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); + if ($number < 0.0) { + return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); } - $result = sprintf("%.{$digitsPlus1}f", $number + 0.5 * 0.1 ** $digits); - return round((float) $result, $digits, PHP_ROUND_HALF_DOWN); + return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); } /** @@ -90,8 +89,8 @@ public static function up($number, $digits) * * Rounds a number down to a specified number of decimal places * - * @param array|float $number Number to round, or can be an array of numbers - * @param array|int $digits Number of digits to which you want to round $number, or can be an array of numbers + * @param null|array|float|string $number Number to round, or can be an array of numbers + * @param array|float|int|string $digits Number of digits to which you want to round $number, or can be an array of numbers * * @return array|float|string Rounded Number, or a string containing an error * If an array of numbers is passed as the argument, then the returned result will also be an array @@ -114,23 +113,19 @@ public static function down($number, $digits) return 0.0; } - $digitsPlus1 = $digits + 1; - if ($number < 0.0) { - if ($digitsPlus1 < 0) { - return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); - } - $result = sprintf("%.{$digitsPlus1}f", $number + 0.5 * 0.1 ** $digits); - - return round((float) $result, $digits, PHP_ROUND_HALF_UP); + if (PHP_VERSION_ID >= 80400) { + return round( + (float) (string) $number, + $digits, + RoundingMode::TowardsZero //* @phpstan-ignore-line + ); } - if ($digitsPlus1 < 0) { - return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); + if ($number < 0.0) { + return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); } - $result = sprintf("%.{$digitsPlus1}f", $number - 0.5 * 0.1 ** $digits); - - return round((float) $result, $digits, PHP_ROUND_HALF_UP); + return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); } /** @@ -141,7 +136,7 @@ public static function down($number, $digits) * @param mixed $number Expect float. Number to round, or can be an array of numbers * @param mixed $multiple Expect int. Multiple to which you want to round, or can be an array of numbers. * - * @return array|float|string Rounded Number, or a string containing an error + * @return array|float|int|string Rounded Number, or a string containing an error * If an array of numbers is passed as the argument, then the returned result will also be an array * with the same dimensions */ @@ -210,7 +205,7 @@ public static function even($number) * * @param array|float $number Number to round, or can be an array of numbers * - * @return array|float|string Rounded Number, or a string containing an error + * @return array|float|int|string Rounded Number, or a string containing an error * If an array of numbers is passed as the argument, then the returned result will also be an array * with the same dimensions */ diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php index ee60105..bd128ba 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php @@ -3,7 +3,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; -use PhpOffice\PhpSpreadsheet\Calculation\Exception; class Trunc { @@ -13,11 +12,14 @@ class Trunc * TRUNC. * * Truncates value to the number of fractional digits by number_digits. + * This will probably not be the precise result in the unlikely + * event that the number of digits to the left of the decimal + * plus the number of digits to the right exceeds PHP_FLOAT_DIG + * (or possibly that value minus 1). + * Excel is unlikely to do any better. * - * @param array|float $value - * Or can be an array of values - * @param array|int $digits - * Or can be an array of values + * @param null|array|float|string $value Or can be an array of values + * @param array|float|int|string $digits Or can be an array of values * * @return array|float|string Truncated value, or a string containing an error * If an array of numbers is passed as an argument, then the returned result will also be an array @@ -29,33 +31,6 @@ public static function evaluate($value = 0, $digits = 0) return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $digits); } - try { - $value = Helpers::validateNumericNullBool($value); - $digits = Helpers::validateNumericNullSubstitution($digits, null); - } catch (Exception $e) { - return $e->getMessage(); - } - - if ($value == 0) { - return $value; - } - - if ($value >= 0) { - $minusSign = ''; - } else { - $minusSign = '-'; - $value = -$value; - } - - $digits = (int) floor($digits); - if ($digits < 0) { - $power = (int) (10 ** -$digits); - $result = intdiv((int) floor($value), $power) * $power; - return ($minusSign === '') ? $result : -$result; - } - $digitsPlus1 = $digits + 1; - $result = substr($minusSign . sprintf("%.{$digitsPlus1}f", $value), 0, -1); - - return (float) $result; + return Round::down($value, $digits); } } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php index 57d3316..5352b6d 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; +use Composer\Pcre\Preg; use DateTimeInterface; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; @@ -128,8 +129,11 @@ public static function TEXTFORMAT($value, $format) $value = Helpers::extractString($value); $format = Helpers::extractString($format); - if (!is_numeric($value) && Date::isDateTimeFormatCode($format)) { - $value = DateTimeExcel\DateValue::fromString($value) + DateTimeExcel\TimeValue::fromString($value); + if (!is_numeric($value) && Date::isDateTimeFormatCode($format) && !Preg::isMatch('/^\s*\d+(\s+\d+)+\s*$/', $value)) { + $value1 = DateTimeExcel\DateValue::fromString($value); + $value2 = DateTimeExcel\TimeValue::fromString($value); + /** @var float|int|string */ + $value = (is_numeric($value1) && is_numeric($value2)) ? ($value1 + $value2) : (is_numeric($value1) ? $value1 : (is_numeric($value2) ? $value2 : $value)); } return (string) NumberFormat::toFormattedString($value, $format); diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Trim.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Trim.php index 27eceb9..c4808cc 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Trim.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Trim.php @@ -26,7 +26,7 @@ public static function nonPrintable($stringValue = '') $stringValue = Helpers::extractString($stringValue); - return (string) preg_replace('/[\\x00-\\x1f]/', '', "$stringValue"); + return (string) preg_replace('/[\x00-\x1f]/', '', "$stringValue"); } /** diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web.php index f4dd40e..99809d3 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web.php @@ -21,7 +21,7 @@ class Web * Use the webService() method in the Web\Service class instead * @see Web\Service::webService() * - * @return string the output resulting from a call to the webservice + * @return ?string the output resulting from a call to the webservice */ public static function WEBSERVICE(string $url) { diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web/Service.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web/Service.php index 697d3a6..c832631 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web/Service.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web/Service.php @@ -2,9 +2,9 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Web; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; -use PhpOffice\PhpSpreadsheet\Settings; -use Psr\Http\Client\ClientExceptionInterface; +use PhpOffice\PhpSpreadsheet\Cell\Cell; class Service { @@ -16,40 +16,56 @@ class Service * Excel Function: * Webservice(url) * - * @return string the output resulting from a call to the webservice + * @param mixed $url + * + * @return ?string the output resulting from a call to the webservice */ - public static function webService(string $url) + public static function webService($url, ?Cell $cell = null) { + if (is_array($url)) { + $url = Functions::flattenSingleValue($url); + } + if (!is_string($url)) { + return ExcelError::VALUE(); // Invalid URL length + } $url = trim($url); - if (strlen($url) > 2048) { + if (mb_strlen($url) > 2048) { return ExcelError::VALUE(); // Invalid URL length } - - if (!preg_match('/^http[s]?:\/\//', $url)) { + $parsed = parse_url($url); + $scheme = $parsed['scheme'] ?? ''; + if ($scheme !== 'http' && $scheme !== 'https') { return ExcelError::VALUE(); // Invalid protocol } - - // Get results from the the webservice - $client = Settings::getHttpClient(); - $requestFactory = Settings::getRequestFactory(); - $request = $requestFactory->createRequest('GET', $url); - - try { - $response = $client->sendRequest($request); - } catch (ClientExceptionInterface $e) { - return ExcelError::VALUE(); // cURL error + $domainWhiteList = []; + if ($cell !== null) { + $parent = $cell->getWorksheet()->getParent(); + if ($parent !== null) { + $domainWhiteList = $parent->getDomainWhiteList(); + } } - - if ($response->getStatusCode() != 200) { - return ExcelError::VALUE(); // cURL error + $host = $parsed['host'] ?? ''; + if (!in_array($host, $domainWhiteList, true)) { + return ($cell === null) ? null : Functions::NOT_YET_IMPLEMENTED; // will be converted to oldCalculatedValue or null } - $output = $response->getBody()->getContents(); - if (strlen($output) > 32767) { + // Get results from the webservice + $ctxArray = [ + 'http' => [ + 'follow_location' => 0, + 'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + ], + ]; + if ($scheme === 'https') { + $ctxArray['ssl'] = ['crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT]; + } + $ctx = stream_context_create($ctxArray); + $output = @file_get_contents($url, false, $ctx); + if ($output === false || mb_strlen($output) > 32767) { return ExcelError::VALUE(); // Output not a string or too long } - return $output; + return ($output === '') ? Functions::NOT_YET_IMPLEMENTED : $output; } /** diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php index e9e41d7..bbfbd26 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php @@ -67,7 +67,7 @@ class Cell /** * Attributes of the formula. * - * @var mixed + * @var ?array */ private $formulaAttributes; @@ -767,26 +767,14 @@ public function setXfIndex(int $indexValue): self return $this->updateInCollection(); } - /** - * Set the formula attributes. - * - * @param mixed $attributes - * - * @return $this - */ - public function setFormulaAttributes($attributes): self + public function setFormulaAttributes(?array $attributes): self { $this->formulaAttributes = $attributes; return $this; } - /** - * Get the formula attributes. - * - * @return mixed - */ - public function getFormulaAttributes() + public function getFormulaAttributes(): ?array { return $this->formulaAttributes; } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DefaultValueBinder.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DefaultValueBinder.php index 92e1d25..33d7133 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DefaultValueBinder.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DefaultValueBinder.php @@ -60,7 +60,7 @@ public static function dataTypeForValue($value) return DataType::TYPE_INLINE; } elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=') { return DataType::TYPE_FORMULA; - } elseif (preg_match('/^[\+\-]?(\d+\\.?\d*|\d*\\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) { + } elseif (preg_match('/^[\+\-]?(\d+\.?\d*|\d*\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) { $tValue = ltrim($value, '+-'); if (is_string($value) && strlen($tValue) > 1 && $tValue[0] === '0' && $tValue[1] !== '.') { return DataType::TYPE_STRING; diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Document/Properties.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Document/Properties.php index 302afee..9dd44ce 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Document/Properties.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Document/Properties.php @@ -174,8 +174,8 @@ private static function intOrFloatTimestamp($timestamp) $timestamp = (float) $timestamp; } else { $timestamp = (string) preg_replace('/[.][0-9]*$/', '', $timestamp); - $timestamp = (string) preg_replace('/^(\\d{4})- (\\d)/', '$1-0$2', $timestamp); - $timestamp = (string) preg_replace('/^(\\d{4}-\\d{2})- (\\d)/', '$1-0$2', $timestamp); + $timestamp = (string) preg_replace('/^(\d{4})- (\d)/', '$1-0$2', $timestamp); + $timestamp = (string) preg_replace('/^(\d{4}-\d{2})- (\d)/', '$1-0$2', $timestamp); $timestamp = (float) (new DateTime($timestamp))->format('U'); } } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Downloader.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Downloader.php index e66ae42..e591143 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Downloader.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Downloader.php @@ -23,19 +23,17 @@ class Downloader public function __construct(string $folder, string $filename, ?string $filetype = null) { - if ((is_dir($folder) === false) || (is_readable($folder) === false)) { - throw new Exception("Folder {$folder} is not accessable"); - } - $filepath = "{$folder}/{$filename}"; - $this->filepath = (string) realpath($filepath); - $this->filename = basename($filepath); - if ((file_exists($this->filepath) === false) || (is_readable($this->filepath) === false)) { - throw new Exception("{$this->filename} not found, or cannot be read"); + clearstatcache(); + $filepath = realpath("{$folder}/{$filename}"); + if ($filepath === false || !is_file($filepath) || !is_readable($filepath)) { + throw new Exception('File not found, or cannot be read'); } + $this->filepath = $filepath; + $this->filename = basename($this->filepath); - $filetype ??= pathinfo($filename, PATHINFO_EXTENSION); + $filetype ??= pathinfo($this->filename, PATHINFO_EXTENSION); if (array_key_exists(strtolower($filetype), self::CONTENT_TYPES) === false) { - throw new Exception("Invalid filetype: {$filetype} cannot be downloaded"); + throw new Exception('Invalid filetype: cannot be downloaded'); } $this->filetype = strtolower($filetype); } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Sample.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Sample.php index 034d79b..6244375 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Sample.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Sample.php @@ -8,7 +8,6 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Writer\IWriter; -use PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use RecursiveRegexIterator; @@ -138,11 +137,7 @@ public function write(Spreadsheet $spreadsheet, $filename, array $writers = ['Xl $writerCallback($writer); } $callStartTime = microtime(true); - if (PHP_VERSION_ID >= 80400 && $writer instanceof Dompdf) { - @$writer->save($path); - } else { - $writer->save($path); - } + $writer->save($path); $this->logWrite($writer, $path, /** @scrutinizer ignore-type */ $callStartTime); if ($this->isCli() === false) { echo 'Download ' . basename($path) . '
'; diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/IOFactory.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/IOFactory.php index c088397..901d901 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/IOFactory.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/IOFactory.php @@ -30,7 +30,7 @@ abstract class IOFactory public const WRITER_CSV = 'Csv'; public const WRITER_HTML = 'Html'; - /** @var string[] */ + /** @var array> */ private static $readers = [ self::READER_XLSX => Reader\Xlsx::class, self::READER_XLS => Reader\Xls::class, @@ -236,4 +236,16 @@ public static function registerReader(string $readerType, string $readerClass): self::$readers[$readerType] = $readerClass; } + + /** + * @return array> + * + * @internal + * + * @codeCoverageIgnore + */ + public static function getReaders(): array + { + return self::$readers; + } } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/BaseReader.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/BaseReader.php index aa380aa..e51f47e 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/BaseReader.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/BaseReader.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader; +use Closure; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; @@ -44,6 +45,15 @@ abstract class BaseReader implements IReader */ protected $loadSheetsOnly; + /** + * Allow external images. Use with caution. + * Improper specification of these within a spreadsheet + * can subject the caller to security exploits. + * + * @var bool + */ + protected $allowExternalImages = false; + /** * IReadFilter instance. * @@ -59,6 +69,9 @@ abstract class BaseReader implements IReader */ protected $securityScanner; + /** @var null|Closure(string):bool function to return whether image path is okay */ + protected ?Closure $isWhitelisted = null; + public function __construct() { $this->readFilter = new DefaultReadFilter(); @@ -160,6 +173,12 @@ protected function processFlags(int $flags): void if (((bool) ($flags & self::SKIP_EMPTY_CELLS) || (bool) ($flags & self::IGNORE_EMPTY_CELLS)) === true) { $this->setReadEmptyCells(false); } + if (((bool) ($flags & self::ALLOW_EXTERNAL_IMAGES)) === true) { + $this->setAllowExternalImages(true); + } + if (((bool) ($flags & self::DONT_ALLOW_EXTERNAL_IMAGES)) === true) { + $this->setAllowExternalImages(false); + } } protected function loadSpreadsheetFromFile(string $filename): Spreadsheet @@ -203,4 +222,38 @@ protected function openFile(string $filename): void $this->fileHandle = $fileHandle; } + + /** + * USE WITH CAUTION (and in conjunction with setIsWhiteListed)! + * Allow external images; + * these can be specified within a spreadsheet + * in a way that can subject the caller to security exploits. + */ + public function setAllowExternalImages(bool $allowExternalImages) + { + $this->allowExternalImages = $allowExternalImages; + + return $this; + } + + public function getAllowExternalImages() + { + return $this->allowExternalImages; + } + + /** + * USE WITH CAUTION! + * Supply a callback to determine whether a path should be whitelisted, + * used in conjunction with setAllowExternalImages; + * supplying a method which might return true + * can subject the caller to security exploits. + * + * @param Closure(string):bool $isWhitelisted + */ + public function setIsWhitelisted(Closure $isWhitelisted): self + { + $this->isWhitelisted = $isWhitelisted; + + return $this; + } } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv.php index 3feec8e..b866892 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv.php @@ -10,6 +10,7 @@ use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; +use Throwable; class Csv extends BaseReader { @@ -85,7 +86,7 @@ class Csv extends BaseReader * It is anticipated that it will conditionally be set * to null-string for Php9 and above. */ - private static string $defaultEscapeCharacter = '\\'; + private static string $defaultEscapeCharacter = PHP_VERSION_ID < 90000 ? '\\' : ''; /** * Callback for setting defaults in construction. @@ -295,6 +296,12 @@ private function openFileOrMemory(string $filename): void if (!$fhandle) { throw new Exception($filename . ' is an Invalid Spreadsheet file.'); } + if ($this->inputEncoding === 'UTF-8') { + $encoding = self::guessEncodingBom($filename); + if ($encoding !== '') { + $this->inputEncoding = $encoding; + } + } if ($this->inputEncoding === self::GUESS_ENCODING) { $this->inputEncoding = self::guessEncoding($filename, $this->fallbackEncoding); } @@ -322,7 +329,7 @@ public function setTestAutoDetect(bool $value): self private function setAutoDetect(?string $value): ?string { $retVal = null; - if ($value !== null && $this->testAutodetect) { + if ($value !== null && $this->testAutodetect && PHP_VERSION_ID < 90000) { $retVal2 = @ini_set('auto_detect_line_endings', $value); if (is_string($retVal2)) { $retVal = $retVal2; @@ -371,6 +378,20 @@ private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bo // Deprecated in Php8.1 $iniset = $this->setAutoDetect('1'); + try { + $this->loadStringOrFile2($filename, $spreadsheet, $dataUri); + $this->setAutoDetect($iniset); + } catch (Throwable $e) { + $this->setAutoDetect($iniset); + + throw $e; + } + + return $spreadsheet; + } + + private function loadStringOrFile2(string $filename, Spreadsheet $spreadsheet, bool $dataUri): void + { // Open file if ($dataUri) { $this->openDataUri($filename); @@ -428,11 +449,6 @@ private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bo // Close file fclose($fileHandle); - - $this->setAutoDetect($iniset); - - // Return - return $spreadsheet; } /** @@ -544,6 +560,10 @@ public function getContiguous(): bool */ public function setEscapeCharacter(string $escapeCharacter): self { + if (PHP_VERSION_ID >= 90000 && $escapeCharacter !== '') { + throw new ReaderException('Escape character must be null string for Php9+'); + } + $this->escapeCharacter = $escapeCharacter; return $this; @@ -620,17 +640,15 @@ private static function guessEncodingTestBom(string &$encoding, string $first4, } } - private static function guessEncodingBom(string $filename): string + public static function guessEncodingBom(string $filename, ?string $convertString = null): string { $encoding = ''; - $first4 = file_get_contents($filename, false, null, 0, 4); - if ($first4 !== false) { - self::guessEncodingTestBom($encoding, $first4, self::UTF8_BOM, 'UTF-8'); - self::guessEncodingTestBom($encoding, $first4, self::UTF16BE_BOM, 'UTF-16BE'); - self::guessEncodingTestBom($encoding, $first4, self::UTF32BE_BOM, 'UTF-32BE'); - self::guessEncodingTestBom($encoding, $first4, self::UTF32LE_BOM, 'UTF-32LE'); - self::guessEncodingTestBom($encoding, $first4, self::UTF16LE_BOM, 'UTF-16LE'); - } + $first4 = $convertString ?? (string) file_get_contents($filename, false, null, 0, 4); + self::guessEncodingTestBom($encoding, $first4, self::UTF8_BOM, 'UTF-8'); + self::guessEncodingTestBom($encoding, $first4, self::UTF16BE_BOM, 'UTF-16BE'); + self::guessEncodingTestBom($encoding, $first4, self::UTF32BE_BOM, 'UTF-32BE'); + self::guessEncodingTestBom($encoding, $first4, self::UTF32LE_BOM, 'UTF-32LE'); + self::guessEncodingTestBom($encoding, $first4, self::UTF16LE_BOM, 'UTF-16LE'); return $encoding; } @@ -679,4 +697,39 @@ private static function getCsv( return fgetcsv($stream, $length, $separator, $enclosure, $escape); } + + public static function affectedByPhp9( + string $filename, + string $inputEncoding = 'UTF-8', + ?string $delimiter = null, + string $enclosure = '"', + string $escapeCharacter = '\\' + ): bool { + if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 90000) { + throw new ReaderException('Function valid only for Php7.4 or Php8'); // @codeCoverageIgnore + } + $reader1 = new self(); + $reader1->setInputEncoding($inputEncoding) + ->setTestAutoDetect(true) + ->setEscapeCharacter($escapeCharacter) + ->setDelimiter($delimiter) + ->setEnclosure($enclosure); + $spreadsheet1 = $reader1->load($filename); + $sheet1 = $spreadsheet1->getActiveSheet(); + $array1 = $sheet1->toArray(null, false, false); + $spreadsheet1->disconnectWorksheets(); + + $reader2 = new self(); + $reader2->setInputEncoding($inputEncoding) + ->setTestAutoDetect(false) + ->setEscapeCharacter('') + ->setDelimiter($delimiter) + ->setEnclosure($enclosure); + $spreadsheet2 = $reader2->load($filename); + $sheet2 = $spreadsheet2->getActiveSheet(); + $array2 = $sheet2->toArray(null, false, false); + $spreadsheet2->disconnectWorksheets(); + + return $array1 !== $array2; + } } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php index 99e4d6a..2fd1fd9 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -11,7 +11,6 @@ use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\RichText\RichText; -use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -65,6 +64,14 @@ class Gnumeric extends BaseReader ], ]; + protected int $maxLength; + + private const LENGTH_MULTIPLIER = [ + 'G' => 1024 * 1024 * 1024, + 'M' => 1024 * 1024, + 'K' => 1024, + ]; + /** * Create a new Gnumeric. */ @@ -73,6 +80,20 @@ public function __construct() parent::__construct(); $this->referenceHelper = ReferenceHelper::getInstance(); $this->securityScanner = XmlScanner::getInstance($this); + $limit = ini_get('memory_limit') ?: '128M'; + $limit = trim(str_replace('-1', '128M', $limit)); + $unit = strtoupper(substr($limit, -1)); + $limit = (int) $limit; + $multiplier = self::LENGTH_MULTIPLIER[$unit] ?? 1; + $limit *= $multiplier; + $this->maxLength = intdiv($limit, 4); + } + + public function setMaxLength(int $maxLength): self + { + $this->maxLength = $maxLength; + + return $this; } /** @@ -114,7 +135,7 @@ public function listWorksheetNames($filename) $xml = new XMLReader(); $contents = $this->gzfileGetContents($filename); - $xml->xml($contents, null, Settings::getLibXmlLoaderOptions()); + $xml->xml($contents); $xml->setParserProperty(2, true); $worksheetNames = []; @@ -147,7 +168,7 @@ public function listWorksheetInfo($filename) $xml = new XMLReader(); $contents = $this->gzfileGetContents($filename); - $xml->xml($contents, null, Settings::getLibXmlLoaderOptions()); + $xml->xml($contents); $xml->setParserProperty(2, true); $worksheetInfo = []; @@ -197,7 +218,7 @@ private function gzfileGetContents($filename) if (substr($contents, 0, 2) === "\x1f\x8b") { // Check if gzlib functions are available if (function_exists('gzdecode')) { - $contents = @gzdecode($contents); + $contents = @gzdecode($contents, $this->maxLength); if ($contents !== false) { $data = $contents; } @@ -267,7 +288,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp $gFileData = $this->gzfileGetContents($filename); - $xml2 = simplexml_load_string($gFileData, 'SimpleXMLElement', Settings::getLibXmlLoaderOptions()); + $xml2 = simplexml_load_string($gFileData); $xml = self::testSimpleXml($xml2); $gnmXML = $xml->children(self::NAMESPACE_GNM); diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php index 19bf67b..0505912 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php @@ -168,7 +168,7 @@ private function readBeginning(): string private function readEnding(): string { $meta = stream_get_meta_data($this->fileHandle); - $filename = $meta['uri']; // @phpstan-ignore-line + $filename = $meta['uri']; $size = (int) filesize($filename); if ($size === 0) { @@ -1084,7 +1084,10 @@ private function insertImage(Worksheet $sheet, $column, $row, array $attributes) $name = $attributes['alt'] ?? null; $drawing = new Drawing(); - $drawing->setPath($src); + $drawing->setPath($src, false, null, $this->allowExternalImages, $this->isWhitelisted); + if ($drawing->getPath() === '') { + return; + } $drawing->setWorksheet($sheet); $drawing->setCoordinates($column . $row); $drawing->setOffsetX(0); diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/IReader.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/IReader.php index 97afe3c..8b51153 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/IReader.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/IReader.php @@ -11,6 +11,14 @@ interface IReader public const SKIP_EMPTY_CELLS = 4; public const IGNORE_EMPTY_CELLS = 4; + /** + * Allow external images. Use with caution. + * Improper specification of these within a spreadsheet + * can subject the caller to security exploits. + */ + public const ALLOW_EXTERNAL_IMAGES = 16; + public const DONT_ALLOW_EXTERNAL_IMAGES = 32; + /** * IReader constructor. */ @@ -127,6 +135,22 @@ public function getReadFilter(); */ public function setReadFilter(IReadFilter $readFilter); + /** + * Allow external images. Use with caution. + * Improper specification of these within a spreadsheet + * can subject the caller to security exploits. + * + * @param bool $allowExternalImages + * + * @return IReader + */ + public function setAllowExternalImages(bool $allowExternalImages); + + /** + * @return bool + */ + public function getAllowExternalImages(); + /** * Loads PhpSpreadsheet from file. * @@ -136,6 +160,8 @@ public function setReadFilter(IReadFilter $readFilter); * self::READ_DATA_ONLY Read only data, not style or structure information, from the file * self::SKIP_EMPTY_CELLS Don't read empty cells (cells that contain a null value, * empty string, or a string containing only whitespace characters) + * self::ALLOW_EXTERNAL_IMAGES Attempt to fetch images stored outside the spreadsheet. + * self::DONT_ALLOW_EXTERNAL_IMAGES Don't attempt to fetch images stored outside the spreadsheet. * * @return \PhpOffice\PhpSpreadsheet\Spreadsheet */ diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php index 9913f33..e688859 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php @@ -16,7 +16,6 @@ use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; use PhpOffice\PhpSpreadsheet\RichText\RichText; -use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -57,9 +56,12 @@ public function canRead(string $filename): bool $mimeType = $zip->getFromName($stat['name']); } elseif ($zip->statName('META-INF/manifest.xml')) { $xml = simplexml_load_string( - $this->getSecurityScannerOrThrow()->scan($zip->getFromName('META-INF/manifest.xml')), - 'SimpleXMLElement', - Settings::getLibXmlLoaderOptions() + $this->getSecurityScannerOrThrow() + ->scan( + $zip->getFromName( + 'META-INF/manifest.xml' + ) + ) ); if ($xml !== false) { $namespacesContent = $xml->getNamespaces(true); @@ -100,9 +102,8 @@ public function listWorksheetNames($filename) $xml = new XMLReader(); $xml->xml( - $this->getSecurityScannerOrThrow()->scanFile('zip://' . realpath($filename) . '#' . self::INITIAL_FILE), - null, - Settings::getLibXmlLoaderOptions() + $this->getSecurityScannerOrThrow() + ->scanFile('zip://' . realpath($filename) . '#' . self::INITIAL_FILE) ); $xml->setParserProperty(2, true); @@ -151,9 +152,8 @@ public function listWorksheetInfo($filename) $xml = new XMLReader(); $xml->xml( - $this->getSecurityScannerOrThrow()->scanFile('zip://' . realpath($filename) . '#' . self::INITIAL_FILE), - null, - Settings::getLibXmlLoaderOptions() + $this->getSecurityScannerOrThrow() + ->scanFile('zip://' . realpath($filename) . '#' . self::INITIAL_FILE) ); $xml->setParserProperty(2, true); @@ -262,9 +262,8 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet) // Meta $xml = @simplexml_load_string( - $this->getSecurityScannerOrThrow()->scan($zip->getFromName('meta.xml')), - 'SimpleXMLElement', - Settings::getLibXmlLoaderOptions() + $this->getSecurityScannerOrThrow() + ->scan($zip->getFromName('meta.xml')) ); if ($xml === false) { throw new Exception('Unable to read data from {$pFilename}'); @@ -278,8 +277,8 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet) $dom = new DOMDocument('1.01', 'UTF-8'); $dom->loadXML( - $this->getSecurityScannerOrThrow()->scan($zip->getFromName('styles.xml')), - Settings::getLibXmlLoaderOptions() + $this->getSecurityScannerOrThrow() + ->scan($zip->getFromName('styles.xml')) ); $pageSettings = new PageSettings($dom); @@ -288,8 +287,8 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet) $dom = new DOMDocument('1.01', 'UTF-8'); $dom->loadXML( - $this->getSecurityScannerOrThrow()->scan($zip->getFromName(self::INITIAL_FILE)), - Settings::getLibXmlLoaderOptions() + $this->getSecurityScannerOrThrow() + ->scan($zip->getFromName(self::INITIAL_FILE)) ); $officeNs = $dom->lookupNamespaceUri('office'); @@ -664,8 +663,8 @@ private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): voi { $dom = new DOMDocument('1.01', 'UTF-8'); $dom->loadXML( - $this->getSecurityScannerOrThrow()->scan($zip->getFromName('settings.xml')), - Settings::getLibXmlLoaderOptions() + $this->getSecurityScannerOrThrow() + ->scan($zip->getFromName('settings.xml')) ); //$xlinkNs = $dom->lookupNamespaceUri('xlink'); $configNs = $dom->lookupNamespaceUri('config'); diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Security/XmlScanner.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Security/XmlScanner.php index 9ac5e95..46b24c6 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Security/XmlScanner.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Security/XmlScanner.php @@ -6,6 +6,9 @@ class XmlScanner { + private const ENCODING_PATTERN = '/encoding\s*=\s*(["\'])(.+?)\1/s'; + private const ENCODING_UTF7 = '/encoding\s*=\s*(["\'])UTF-7\1/si'; + /** * String used to identify risky xml elements. * @@ -114,13 +117,24 @@ private static function forceString($arg): string private function toUtf8($xml) { $charset = $this->findCharSet($xml); + $foundUtf7 = $charset === 'UTF-7'; if ($charset !== 'UTF-8') { + $testStart = '/^.{0,4}\s*findCharSet($xml); - if ($charset !== 'UTF-8') { - throw new Reader\Exception('Suspicious Double-encoded XML, spreadsheet file load() aborted to prevent XXE/XEE attacks'); + if ($startWithXml1 === 1 && preg_match($testStart, $xml) !== 1) { + throw new Reader\Exception('Double encoding not permitted'); } + $foundUtf7 = $foundUtf7 || (preg_match(self::ENCODING_UTF7, $xml) === 1); + $xml = preg_replace(self::ENCODING_PATTERN, '', $xml) ?? $xml; + } else { + $foundUtf7 = $foundUtf7 || (preg_match(self::ENCODING_UTF7, $xml) === 1); + } + if ($foundUtf7) { + throw new Reader\Exception('UTF-7 encoding not permitted'); + } + if (substr($xml, 0, Reader\Csv::UTF8_BOM_LEN) === Reader\Csv::UTF8_BOM) { + $xml = substr($xml, Reader\Csv::UTF8_BOM_LEN); } return $xml; @@ -128,15 +142,16 @@ private function toUtf8($xml) private function findCharSet(string $xml): string { - $patterns = [ - '/encoding\\s*=\\s*"([^"]*]?)"/', - "/encoding\\s*=\\s*'([^']*?)'/", - ]; - - foreach ($patterns as $pattern) { - if (preg_match($pattern, $xml, $matches)) { - return strtoupper($matches[1]); - } + if (substr($xml, 0, 4) === "\x4c\x6f\xa7\x94") { + throw new Reader\Exception('EBCDIC encoding not permitted'); + } + $encoding = Reader\Csv::guessEncodingBom('', $xml); + if ($encoding !== '') { + return $encoding; + } + $xml = str_replace("\0", '', $xml); + if (preg_match(self::ENCODING_PATTERN, $xml, $matches)) { + return strtoupper($matches[2]); } return 'UTF-8'; @@ -151,13 +166,16 @@ private function findCharSet(string $xml): string */ public function scan($xml) { - $xml = "$xml"; $this->disableEntityLoaderCheck(); + // Don't rely purely on libxml_disable_entity_loader() + $pattern = '/\0*' . implode('\0*', /** @scrutinizer ignore-type */ str_split($this->pattern)) . '\0*/'; - $xml = $this->toUtf8($xml); + $xml = "$xml"; + if (preg_match($pattern, $xml)) { + throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks'); + } - // Don't rely purely on libxml_disable_entity_loader() - $pattern = '/\\0?' . implode('\\0?', /** @scrutinizer ignore-type */ str_split($this->pattern)) . '\\0?/'; + $xml = $this->toUtf8($xml); if (preg_match($pattern, $xml)) { throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks'); @@ -171,7 +189,7 @@ public function scan($xml) } /** - * Scan theXML for use of parseHuge = $parseHuge; + } + /** * Create a new Xlsx Reader instance. */ @@ -136,8 +148,8 @@ private function loadZip(string $filename, string $ns = '', bool $replaceUnclose } $rels = @simplexml_load_string( $this->getSecurityScannerOrThrow()->scan($contents), - 'SimpleXMLElement', - Settings::getLibXmlLoaderOptions(), + SimpleXMLElement::class, + $this->parseHuge ? LIBXML_PARSEHUGE : 0, $ns ); @@ -151,8 +163,8 @@ private function loadZipNonamespace(string $filename, string $ns): SimpleXMLElem $contents = $this->getFromZipArchive($this->zip, $filename); $rels = simplexml_load_string( $this->getSecurityScannerOrThrow()->scan($contents), - 'SimpleXMLElement', - Settings::getLibXmlLoaderOptions(), + SimpleXMLElement::class, + $this->parseHuge ? LIBXML_PARSEHUGE : 0, ($ns === '' ? $ns : '') ); @@ -268,11 +280,15 @@ public function listWorksheetInfo($filename) $xml = new XMLReader(); $xml->xml( - $this->getSecurityScannerOrThrow()->scan( - $this->getFromZipArchive($this->zip, $fileWorksheetPath) - ), + $this->getSecurityScannerOrThrow() + ->scan( + $this->getFromZipArchive( + $this->zip, + $fileWorksheetPath + ) + ), null, - Settings::getLibXmlLoaderOptions() + $this->parseHuge ? LIBXML_PARSEHUGE : 0, ); $xml->setParserProperty(2, true); @@ -857,6 +873,9 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet } // Read cell! + $useFormula = isset($c->f) + && ((string) $c->f !== '' || (isset($c->f->attributes()['t']) + && strtolower((string) $c->f->attributes()['t']) === 'shared')); switch ($cellDataType) { case 's': if ((string) $c->v != '') { @@ -881,10 +900,16 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet } else { // Formula $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToBoolean'); - if (isset($c->f['t'])) { - $att = $c->f; - $docSheet->getCell($r)->setFormulaAttributes($att); - } + self::storeFormulaAttributes($c->f, $docSheet, $r); + } + + break; + case 'str': + if ($useFormula) { + $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString'); + self::storeFormulaAttributes($c->f, $docSheet, $r); + } else { + $value = self::castToString($c); } break; @@ -911,10 +936,10 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet } else { // Formula $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString'); - if (isset($c->f['t'])) { - $attributes = $c->f['t']; - $docSheet->getCell($r)->setFormulaAttributes(['t' => (string) $attributes]); + if (is_numeric($calculatedValue)) { + $calculatedValue += 0; } + self::storeFormulaAttributes($c->f, $docSheet, $r); } break; @@ -1411,7 +1436,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet ); if (isset($images[$linkImageKey])) { $url = str_replace('xl/drawings/', '', $images[$linkImageKey]); - $objDrawing->setPath($url, false); + $objDrawing->setPath($url, false, null, $this->allowExternalImages, $this->isWhitelisted); } if ($objDrawing->getPath() === '') { continue; @@ -1500,7 +1525,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet ); if (isset($images[$linkImageKey])) { $url = str_replace('xl/drawings/', '', $images[$linkImageKey]); - $objDrawing->setPath($url, false); + $objDrawing->setPath($url, false, null, $this->allowExternalImages, $this->isWhitelisted); } if ($objDrawing->getPath() === '') { continue; @@ -1940,9 +1965,10 @@ private function readRibbon(Spreadsheet $excel, string $customUITarget, ZipArchi if ($dataRels) { // exists and not empty if the ribbon have some pictures (other than internal MSO) $UIRels = simplexml_load_string( - $this->getSecurityScannerOrThrow()->scan($dataRels), - 'SimpleXMLElement', - Settings::getLibXmlLoaderOptions() + $this->getSecurityScannerOrThrow() + ->scan($dataRels), + SimpleXMLElement::class, + $this->parseHuge ? LIBXML_PARSEHUGE : 0 ); if (false !== $UIRels) { // we need to save id and target to avoid parsing customUI.xml and "guess" if it's a pseudo callback who load the image @@ -2325,4 +2351,19 @@ private function processIgnoredErrors(SimpleXMLElement $xml, Worksheet $sheet): } } } + + private static function storeFormulaAttributes(SimpleXMLElement $f, Worksheet $docSheet, string $r): void + { + $formulaAttributes = []; + $attributes = $f->attributes(); + if (isset($attributes['t'])) { + $formulaAttributes['t'] = (string) $attributes['t']; + } + if (isset($attributes['ref'])) { + $formulaAttributes['ref'] = (string) $attributes['ref']; + } + if (!empty($formulaAttributes)) { + $docSheet->getCell($r)->setFormulaAttributes($formulaAttributes); + } + } } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php index 2b14eab..55462f7 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; +use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Reader\DefaultReadFilter; use PhpOffice\PhpSpreadsheet\Reader\IReadFilter; @@ -196,24 +197,31 @@ private function readRowAttributes(SimpleXMLElement $worksheetRow, bool $readDat { $rowAttributes = []; + $rowIndex = 0; foreach ($worksheetRow as $rowx) { - /** @scrutinizer ignore-call */ $row = $rowx->attributes(); + ++$rowIndex; if ($row !== null) { + if (isset($row['r'])) { + $rowIndex = (int) $row['r']; + } + if ($rowIndex < 1 || $rowIndex > AddressRange::MAX_ROW) { + continue; + } if (isset($row['ht']) && !$readDataOnly) { - $rowAttributes[(int) $row['r']]['rowHeight'] = (float) $row['ht']; + $rowAttributes[$rowIndex]['rowHeight'] = (float) $row['ht']; } if (isset($row['hidden']) && self::boolean($row['hidden'])) { - $rowAttributes[(int) $row['r']]['visible'] = false; + $rowAttributes[$rowIndex]['visible'] = false; } if (isset($row['collapsed']) && self::boolean($row['collapsed'])) { - $rowAttributes[(int) $row['r']]['collapsed'] = true; + $rowAttributes[$rowIndex]['collapsed'] = true; } if (isset($row['outlineLevel']) && (int) $row['outlineLevel'] > 0) { - $rowAttributes[(int) $row['r']]['outlineLevel'] = (int) $row['outlineLevel']; + $rowAttributes[$rowIndex]['outlineLevel'] = (int) $row['outlineLevel']; } if (isset($row['s']) && !$readDataOnly) { - $rowAttributes[(int) $row['r']]['xfIndex'] = (int) $row['s']; + $rowAttributes[$rowIndex]['xfIndex'] = (int) $row['s']; } } } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php index 210c322..126db1d 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php @@ -27,7 +27,7 @@ public function load(): void $range = strtoupper((string) $dataValidation['sqref']); $rangeSet = explode(' ', $range); foreach ($rangeSet as $range) { - if (preg_match('/^[A-Z]{1,3}\\d{1,7}/', $range, $matches) === 1) { + if (preg_match('/^[A-Z]{1,3}\d{1,7}/', $range, $matches) === 1) { // Ensure left/top row of range exists, thereby // adjusting high row/column. $this->worksheet->getCell($matches[0]); diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Properties.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Properties.php index 0d4701a..6b23422 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Properties.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Properties.php @@ -4,7 +4,6 @@ use PhpOffice\PhpSpreadsheet\Document\Properties as DocumentProperties; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; -use PhpOffice\PhpSpreadsheet\Settings; use SimpleXMLElement; class Properties @@ -33,9 +32,7 @@ private function extractPropertyData(string $propertyData): ?SimpleXMLElement { // okay to omit namespace because everything will be processed by xpath $obj = simplexml_load_string( - $this->securityScanner->scan($propertyData), - 'SimpleXMLElement', - Settings::getLibXmlLoaderOptions() + $this->securityScanner->scan($propertyData) ); return self::nullOrSimple($obj); diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml.php index 6be26fc..7e0c257 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml.php @@ -5,6 +5,7 @@ use DateTime; use DateTimeZone; use PhpOffice\PhpSpreadsheet\Cell\AddressHelper; +use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\DefinedName; @@ -14,10 +15,8 @@ use PhpOffice\PhpSpreadsheet\Reader\Xml\Properties; use PhpOffice\PhpSpreadsheet\Reader\Xml\Style; use PhpOffice\PhpSpreadsheet\RichText\RichText; -use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; -use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; @@ -77,10 +76,9 @@ public function canRead(string $filename): bool ]; // Open file - $data = file_get_contents($filename) ?: ''; - - // Why? - //$data = str_replace("'", '"', $data); // fix headers with single quote + File::assertFile($filename); + $data = (string) file_get_contents($filename); + $data = $this->getSecurityScannerOrThrow()->scan($data); $valid = true; foreach ($signature as $match) { @@ -92,14 +90,6 @@ public function canRead(string $filename): bool } } - // Retrieve charset encoding - if (preg_match('//m', $data, $matches)) { - $charSet = strtoupper($matches[1]); - if (preg_match('/^ISO-8859-\d[\dL]?$/i', $charSet) === 1) { - $data = StringHelper::convertEncoding($data, 'UTF-8', $charSet); - $data = (string) preg_replace('/()/um', '$1' . 'UTF-8' . '$2', $data, 1); - } - } $this->fileContents = $data; return $valid; @@ -116,9 +106,8 @@ public function trySimpleXMLLoadString($filename) { try { $xml = simplexml_load_string( - $this->getSecurityScannerOrThrow()->scan($this->fileContents ?: file_get_contents($filename)), - 'SimpleXMLElement', - Settings::getLibXmlLoaderOptions() + $this->getSecurityScannerOrThrow() + ->scan($this->fileContents ?: file_get_contents($filename)) ); } catch (\Exception $e) { throw new Exception('Cannot load invalid XML file: ' . $filename, 0, $e); @@ -366,15 +355,19 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo } } - $rowID = 1; + $rowID = 0; if (isset($worksheet->Table->Row)) { $additionalMergedCells = 0; foreach ($worksheet->Table->Row as $rowData) { $rowHasData = false; + ++$rowID; $row_ss = self::getAttributes($rowData, self::NAMESPACES_SS); if (isset($row_ss['Index'])) { $rowID = (int) $row_ss['Index']; } + if ($rowID < 1 || $rowID > AddressRange::MAX_ROW) { + continue; + } if (isset($row_ss['Hidden'])) { $rowVisible = ((string) $row_ss['Hidden']) !== '1'; $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setVisible($rowVisible); @@ -508,8 +501,6 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setRowHeight((float) $rowHeight); } } - - ++$rowID; } } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/ReferenceHelper.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/ReferenceHelper.php index 90eee53..8cd0c6d 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/ReferenceHelper.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/ReferenceHelper.php @@ -913,7 +913,7 @@ private function updateNamedRange(DefinedName $definedName, Worksheet $worksheet { $cellAddress = $definedName->getValue(); $asFormula = ($cellAddress[0] === '='); - if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashInt() === $worksheet->getHashInt()) { /** * If we delete the entire range that is referenced by a Named Range, MS Excel sets the value to #REF! * PhpSpreadsheet still only does a basic adjustment, so the Named Range will still reference Cells. @@ -932,7 +932,7 @@ private function updateNamedRange(DefinedName $definedName, Worksheet $worksheet private function updateNamedFormula(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void { - if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashInt() === $worksheet->getHashInt()) { /** * If we delete the entire range that is referenced by a Named Formula, MS Excel sets the value to #REF! * PhpSpreadsheet still only does a basic adjustment, so the Named Formula will still reference Cells. diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Settings.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Settings.php index d8007fd..edd9ca2 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Settings.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Settings.php @@ -5,8 +5,6 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Chart\Renderer\IRenderer; use PhpOffice\PhpSpreadsheet\Collection\Memory; -use Psr\Http\Client\ClientInterface; -use Psr\Http\Message\RequestFactoryInterface; use Psr\SimpleCache\CacheInterface; use ReflectionClass; @@ -37,12 +35,12 @@ class Settings /** * The HTTP client implementation to be used for network request. * - * @var null|ClientInterface + * @var mixed */ private static $httpClient; /** - * @var null|RequestFactoryInterface + * @var mixed */ private static $requestFactory; @@ -98,6 +96,8 @@ public static function htmlEntityFlags(): int * Set default options for libxml loader. * * @param ?int $options Default options for libxml loader + * + * @deprecated 3.5.0 no longer needed */ public static function setLibXmlLoaderOptions($options): int { @@ -114,14 +114,12 @@ public static function setLibXmlLoaderOptions($options): int * Defaults to LIBXML_DTDLOAD | LIBXML_DTDATTR when not set explicitly. * * @return int Default options for libxml loader + * + * @deprecated 3.5.0 no longer needed */ public static function getLibXmlLoaderOptions(): int { - if (self::$libXmlLoaderOptions === null) { - return self::setLibXmlLoaderOptions(null); - } - - return self::$libXmlLoaderOptions; + return self::$libXmlLoaderOptions ?? (defined('LIBXML_DTDLOAD') ? (LIBXML_DTDLOAD | LIBXML_DTDATTR) : 0); } /** @@ -181,8 +179,13 @@ public static function useSimpleCacheVersion3(): bool /** * Set the HTTP client implementation to be used for network request. + * + * @param mixed $httpClient + * @param mixed $requestFactory + * + * @deprecated 1.30.2 No replacement. */ - public static function setHttpClient(ClientInterface $httpClient, RequestFactoryInterface $requestFactory): void + public static function setHttpClient($httpClient, $requestFactory): void { self::$httpClient = $httpClient; self::$requestFactory = $requestFactory; @@ -190,6 +193,8 @@ public static function setHttpClient(ClientInterface $httpClient, RequestFactory /** * Unset the HTTP client configuration. + * + * @deprecated 1.30.2 No replacement. */ public static function unsetHttpClient(): void { @@ -199,25 +204,25 @@ public static function unsetHttpClient(): void /** * Get the HTTP client implementation to be used for network request. + * + * @return mixed + * + * @deprecated 1.30.2 No replacement. */ - public static function getHttpClient(): ClientInterface + public static function getHttpClient() { - if (!self::$httpClient || !self::$requestFactory) { - throw new Exception('HTTP client must be configured via Settings::setHttpClient() to be able to use WEBSERVICE function.'); - } - return self::$httpClient; } /** * Get the HTTP request factory. + * + * @return mixed + * + * @deprecated 1.30.2 No replacement. */ - public static function getRequestFactory(): RequestFactoryInterface + public static function getRequestFactory() { - if (!self::$httpClient || !self::$requestFactory) { - throw new Exception('HTTP client must be configured via Settings::setHttpClient() to be able to use WEBSERVICE function.'); - } - return self::$requestFactory; } } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Date.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Date.php index 4f19673..6ded02a 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Date.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Date.php @@ -184,7 +184,7 @@ public static function convertIsoDate($value) throw new Exception("Invalid string $value supplied for datatype Date"); } - if (preg_match('/^\\s*\\d?\\d:\\d\\d(:\\d\\d([.]\\d+)?)?\\s*(am|pm)?\\s*$/i', $value) == 1) { + if (preg_match('/^\s*\d?\d:\d\d(:\d\d([.]\d+)?)?\s*(am|pm)?\s*$/i', $value) == 1) { $newValue = fmod($newValue, 1.0); } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php index 737a6eb..c68156c 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use ZipArchive; @@ -140,11 +141,33 @@ public static function temporaryFilename(): string return $filename; } + /** + * Blocks phar:// and similar RCE-bearing wrappers. + * Note that many protocols, including http and zip, will already + * return false for is_file. + * A whitelist of protocols may be added if needed in future. + * data: is intentionally allowed; callers needing strict + * on-disk-only semantics must validate $filename themselves. + */ + public static function prohibitWrappers(string $filename): void + { + if ( + Preg::IsMatch('~^phar://~i', $filename) + || (Preg::isMatch('/^([\w.\s\x00-\x1f]+):/', $filename) && !Preg::isMatch('/^([\w.]+):/', $filename)) + || Preg::isMatch('~^[\w.]+://.*phar:~is', $filename) + ) { + throw new Exception( + "Disallowed stream wrapper: {$filename}" + ); + } + } + /** * Assert that given path is an existing file and is readable, otherwise throw exception. */ public static function assertFile(string $filename, string $zipMember = ''): void { + self::prohibitWrappers($filename); if (!is_file($filename)) { throw new ReaderException('File "' . $filename . '" does not exist.'); } @@ -167,9 +190,11 @@ public static function assertFile(string $filename, string $zipMember = ''): voi /** * Same as assertFile, except return true/false and don't throw Exception. + * Will nevertheless throw if filename uses invalid protocol, e.g. phar. */ public static function testFileNoThrow(string $filename, ?string $zipMember = null): bool { + self::prohibitWrappers($filename); if (!is_file($filename)) { return false; } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php index 90c1992..b329107 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php @@ -561,7 +561,7 @@ public static function getTrueTypeFontFileFromFont(FontStyle $font, bool $checkP if (mb_strlen(self::$trueTypeFontPath) > 1 && mb_substr(self::$trueTypeFontPath, -1) !== '/' && mb_substr(self::$trueTypeFontPath, -1) !== '\\') { $separator = DIRECTORY_SEPARATOR; } - $fontFileAbsolute = preg_match('~^([A-Za-z]:)?[/\\\\]~', $fontFile) === 1; + $fontFileAbsolute = preg_match('~^([A-Za-z]:)?[/\\\]~', $fontFile) === 1; if (!$fontFileAbsolute) { $fontFile = self::$trueTypeFontPath . $separator . $fontFile; } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLERead.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLERead.php index fcc9639..d92bceb 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLERead.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLERead.php @@ -94,6 +94,9 @@ class OLERead */ private $props = []; + /** @var int[] */ + private array $possibleLoop = []; + /** * Read the file. */ @@ -170,7 +173,9 @@ public function read(string $filename): void $sbdBlock = $this->sbdStartBlock; $this->smallBlockChain = ''; + $this->possibleLoop = []; while ($sbdBlock != -2) { + $this->catchLoop($sbdBlock); $pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE; $this->smallBlockChain .= substr($this->data, $pos, 4 * $bbs); @@ -186,6 +191,14 @@ public function read(string $filename): void $this->readPropertySets(); } + private function catchLoop(int $sbdBlock): void + { + if (in_array($sbdBlock, $this->possibleLoop, true)) { + throw new ReaderException('Detected loop while iterating blocks'); + } + $this->possibleLoop[] = $sbdBlock; + } + /** * Extract binary stream data. * @@ -206,7 +219,9 @@ public function getStream($stream) $block = $this->props[$stream]['startBlock']; + $this->possibleLoop = []; while ($block != -2) { + $this->catchLoop($block); $pos = $block * self::SMALL_BLOCK_SIZE; $streamData .= substr($rootdata, $pos, self::SMALL_BLOCK_SIZE); @@ -226,7 +241,9 @@ public function getStream($stream) $block = $this->props[$stream]['startBlock']; + $this->possibleLoop = []; while ($block != -2) { + $this->catchLoop($block); $pos = ($block + 1) * self::BIG_BLOCK_SIZE; $streamData .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); $block = self::getInt4d($this->bigBlockChain, $block * 4); @@ -246,7 +263,9 @@ private function readData($block) { $data = ''; + $this->possibleLoop = []; while ($block != -2) { + $this->catchLoop($block); $pos = ($block + 1) * self::BIG_BLOCK_SIZE; $data .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); $block = self::getInt4d($this->bigBlockChain, $block * 4); diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Spreadsheet.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Spreadsheet.php index 1109085..9b1e1e3 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Spreadsheet.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Spreadsheet.php @@ -4,13 +4,10 @@ use JsonSerializable; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; -use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Iterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter; class Spreadsheet implements JsonSerializable { @@ -599,7 +596,7 @@ public function getActiveSheet() public function createSheet($sheetIndex = null) { $newSheet = new Worksheet($this); - $this->addSheet($newSheet, $sheetIndex); + $this->addSheet($newSheet, $sheetIndex, true); return $newSheet; } @@ -621,11 +618,24 @@ public function sheetNameExists($worksheetName) * * @param Worksheet $worksheet The worksheet to add * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last) + * @param bool $retitleIfNeeded add suffix if title exists in spreadsheet * * @return Worksheet */ - public function addSheet(Worksheet $worksheet, $sheetIndex = null) + public function addSheet(Worksheet $worksheet, $sheetIndex = null, $retitleIfNeeded = false) { + if ($retitleIfNeeded) { + $title = $worksheet->getTitle(); + if ($this->sheetNameExists($title)) { + $i = 1; + $newTitle = "$title $i"; + while ($this->sheetNameExists($newTitle)) { + ++$i; + $newTitle = "$title $i"; + } + $worksheet->setTitle($newTitle); + } + } if ($this->sheetNameExists($worksheet->getTitle())) { throw new Exception( "Workbook already contains a worksheet named '{$worksheet->getTitle()}'. Rename this worksheet first." @@ -750,13 +760,17 @@ public function getSheetByNameOrThrow(string $worksheetName): Worksheet * * @return int index */ - public function getIndex(Worksheet $worksheet) + public function getIndex(Worksheet $worksheet, bool $noThrow = false) { + $wsHash = $worksheet->getHashInt(); foreach ($this->workSheetCollection as $key => $value) { - if ($value->getHashCode() === $worksheet->getHashCode()) { + if ($value->getHashInt() === $wsHash) { return $key; } } + if ($noThrow) { + return -1; + } throw new Exception('Sheet does not exist.'); } @@ -1144,17 +1158,7 @@ public function getWorksheetIterator() */ public function copy() { - $filename = File::temporaryFilename(); - $writer = new XlsxWriter($this); - $writer->setIncludeCharts(true); - $writer->save($filename); - - $reader = new XlsxReader(); - $reader->setIncludeCharts(true); - $reloadedSpreadsheet = $reader->load($filename); - unlink($filename); - - return $reloadedSpreadsheet; + return unserialize(serialize($this)); } public function __clone() @@ -1646,16 +1650,6 @@ public function getSharedComponent(): Style return new Style(); } - /** - * @throws Exception - * - * @return mixed - */ - public function __serialize() - { - throw new Exception('Spreadsheet objects cannot be serialized'); - } - /** * @throws Exception */ @@ -1685,4 +1679,25 @@ public function resetThemeFonts(): void } } } + + /** @var string[] */ + private $domainWhiteList = []; + + /** + * Currently used only by WEBSERVICE function. + * + * @param string[] $domainWhiteList + */ + public function setDomainWhiteList(array $domainWhiteList): self + { + $this->domainWhiteList = $domainWhiteList; + + return $this; + } + + /** @return string[] */ + public function getDomainWhiteList(): array + { + return $this->domainWhiteList; + } } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php index ba54b53..aeecf12 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php @@ -165,7 +165,7 @@ public static function format($value, string $format): string // If the colon preceding minute had been quoted, as happens in // Excel 2003 XML formats, m will not have been changed to i above. // Change it now. - $format = (string) \preg_replace('/\\\\:m/', ':i', $format); + $format = (string) \preg_replace('/\\\:m/', ':i', $format); return $dateObj->format($format); } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php index 41a1715..7fc3049 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -14,6 +14,7 @@ class Formatter * Matches any @ symbol that isn't enclosed in quotes. */ private const SYMBOL_AT = '/@(?=(?:[^"]*"[^"]*")*[^"]*\Z)/miu'; + private const QUOTE_REPLACEMENT = "\u{fffe}"; // invalid Unicode character /** * Matches any ; symbol that isn't enclosed in quotes, for a "section" split. @@ -69,8 +70,8 @@ private static function splitFormatForSectionSelection(array $sections, $value): // 4 sections: [POSITIVE] [NEGATIVE] [ZERO] [TEXT] $sectionCount = count($sections); // Colour could be a named colour, or a numeric index entry in the colour-palette - $color_regex = '/\\[(' . implode('|', Color::NAMED_COLORS) . '|color\\s*(\\d+))\\]/mui'; - $cond_regex = '/\\[(>|>=|<|<=|=|<>)([+-]?\\d+([.]\\d+)?)\\]/'; + $color_regex = '/\[(' . implode('|', Color::NAMED_COLORS) . '|color\s*(\d+))\]/mui'; + $cond_regex = '/\[(>|>=|<|<=|=|<>)([+-]?\d+([.]\d+)?)\]/'; $colors = ['', '', '', '', '']; $conditionOperations = ['', '', '', '', '']; $conditionComparisonValues = [0, 0, 0, 0, 0]; @@ -137,8 +138,33 @@ public static function toFormattedString($value, $format, $callBack = null) } // For now we do not treat strings in sections, although section 4 of a format code affects strings // Process a single block format code containing @ for text substitution - if (preg_match(self::SECTION_SPLIT, $format) === 0 && preg_match(self::SYMBOL_AT, $format) === 1) { - return str_replace('"', '', preg_replace(self::SYMBOL_AT, (string) $value, $format) ?? ''); + $formatx = str_replace('\"', self::QUOTE_REPLACEMENT, $format); + if (preg_match(self::SECTION_SPLIT, $format) === 0 && preg_match(self::SYMBOL_AT, $formatx) === 1) { + if (strpos($format, '"') === false) { + $temp = str_replace('@', "$value", $format); + if (is_callable($callBack)) { + $temp = $callBack($temp, $format); + } + + return $temp; + } + //escape any dollar signs on the string, so they are not replaced with an empty value + $value = str_replace( + ['$', '"'], + ['\$', self::QUOTE_REPLACEMENT], + (string) $value + ); + $temp = preg_replace(self::SYMBOL_AT, $value, $formatx) ?? $value; + if (is_callable($callBack)) { + $temp = $callBack($temp, $formatx); + } + /** @var string $temp */ + + return str_replace( + ['"', self::QUOTE_REPLACEMENT], + ['', '"'], + $temp + ); } // If we have a text value, return it "as is" @@ -156,7 +182,7 @@ public static function toFormattedString($value, $format, $callBack = null) $format = (string) preg_replace('/^\[\$-[^\]]*\]/', '', $format); $format = (string) preg_replace_callback( - '/(["])(?:(?=(\\\\?))\\2.)*?\\1/u', + '/(["])(?:(?=(\\\?))\2.)*?\1/u', function ($matches) { return str_replace('.', chr(0x00), $matches[0]); }, diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php index fdcf983..807832f 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php @@ -63,7 +63,7 @@ public static function format($value, string $format): string private static function getDecimal(string $value): string { $decimalPart = '0'; - if (preg_match('/^\\d*[.](\\d*[1-9])0*$/', $value, $matches) === 1) { + if (preg_match('/^\d*[.](\d*[1-9])0*$/', $value, $matches) === 1) { $decimalPart = $matches[1]; } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php index c035345..19c6b19 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php @@ -7,7 +7,7 @@ class NumberFormatter { - private const NUMBER_REGEX = '/(0+)(\\.?)(0*)/'; + private const NUMBER_REGEX = '/(0+)(\.?)(0*)/'; private static function mergeComplexNumberFormatMasks(array $numbers, array $masks): array { @@ -223,11 +223,11 @@ public static function format($value, string $format): string $paddingPlaceholder = (strpos($format, '?') !== false); // Replace # or ? with 0 - $format = self::pregReplace('/[\\#\?](?=(?:[^"]*"[^"]*")*[^"]*\Z)/', '0', $format); + $format = self::pregReplace('/[\#\?](?=(?:[^"]*"[^"]*")*[^"]*\Z)/', '0', $format); // Remove locale code [$-###] for an LCID $format = self::pregReplace('/\[\$\-.*\]/', '', $format); - $n = '/\\[[^\\]]+\\]/'; + $n = '/\[[^\]]+\]/'; $m = self::pregReplace($n, '', $format); // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php index cbc4ff6..c7eeff3 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -146,7 +146,7 @@ public function setRangeToMaxRow(): self $this->evaluated = false; if ($this->workSheet !== null) { $thisrange = $this->range; - $range = (string) preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange); + $range = (string) preg_replace('/\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange); if ($range !== $thisrange) { $this->setRange($range); } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php index 49e2eff..6396ac7 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php @@ -219,15 +219,18 @@ public function getWorksheet(): ?Worksheet public function setWorksheet(?Worksheet $worksheet = null, bool $overrideOld = false): self { if ($this->worksheet === null) { - // Add drawing to \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet - if ($worksheet !== null && !($this instanceof Drawing && $this->getPath() === '')) { + // Add drawing to Worksheet + if ($worksheet !== null) { $this->worksheet = $worksheet; - $this->worksheet->getCell($this->coordinates); - $this->worksheet->getDrawingCollection()->append($this); + if (!($this instanceof Drawing && $this->getPath() === '')) { + $this->worksheet->getCell($this->coordinates); + } + $this->worksheet->getDrawingCollection() + ->append($this); } } else { if ($overrideOld) { - // Remove drawing from old \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet + // Remove drawing from old Worksheet $iterator = $this->worksheet->getDrawingCollection()->getIterator(); while ($iterator->valid()) { @@ -239,10 +242,10 @@ public function setWorksheet(?Worksheet $worksheet = null, bool $overrideOld = f } } - // Set new \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet + // Set new Worksheet $this->setWorksheet($worksheet); } else { - throw new PhpSpreadsheetException('A Worksheet has already been assigned. Drawings can only exist on one \\PhpOffice\\PhpSpreadsheet\\Worksheet.'); + throw new PhpSpreadsheetException('A Worksheet has already been assigned. Drawings can only exist on one Worksheet.'); } } @@ -257,6 +260,11 @@ public function getCoordinates(): string public function setCoordinates(string $coordinates): self { $this->coordinates = $coordinates; + if ($this->worksheet !== null) { + if (!($this instanceof Drawing && $this->getPath() === '')) { + $this->worksheet->getCell($this->coordinates); + } + } return $this; } @@ -436,7 +444,7 @@ public function getHashCode() return md5( $this->name . $this->description . - (($this->worksheet === null) ? '' : $this->worksheet->getHashCode()) . + (($this->worksheet === null) ? '' : (string) $this->worksheet->getHashInt()) . $this->coordinates . $this->offsetX . $this->offsetY . diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing.php index aec5426..5ed5376 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use ZipArchive; @@ -101,30 +102,64 @@ public function getPath() * @param string $path File path * @param bool $verifyFile Verify file * @param ZipArchive $zip Zip archive instance + * @param bool $allowExternal + * @param null|callable(string):bool $isWhitelisted * * @return $this */ - public function setPath($path, $verifyFile = true, $zip = null) + public function setPath($path, $verifyFile = true, $zip = null, $allowExternal = true, ?callable $isWhitelisted = null) { $this->isUrl = false; - if (preg_match('~^data:image/[a-z]+;base64,~', $path) === 1) { + if (Preg::isMatch('~^data:image/[a-z]+;base64,~', $path)) { $this->path = $path; return $this; } $this->path = ''; + if ($zip instanceof ZipArchive) { + $zipPath = explode('#', $path)[1]; + $locate = @$zip->locateName($zipPath); + if ($locate !== false) { + if ($this->isImage($path)) { + $this->path = $path; + $this->setSizesAndType($path); + } + } // Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979 - if (filter_var($path, FILTER_VALIDATE_URL)) { - if (!preg_match('/^(http|https|file|ftp|s3):/', $path)) { + } elseif ( + filter_var($path, FILTER_VALIDATE_URL) + || Preg::isMatch('~^phar://~i', $path) + || (Preg::isMatch('/^([\w.\s\x00-\x1f]+):/', $path) && !Preg::isMatch('/^([\w.]+):/', $path)) + ) { + if (!Preg::isMatch('/^(http|https|file|ftp|s3):/', $path)) { throw new PhpSpreadsheetException('Invalid protocol for linked drawing'); } + if (!$allowExternal) { + return $this; + } + if ($isWhitelisted !== null && !$isWhitelisted($path)) { + return $this; + } // Implicit that it is a URL, rather store info than running check above on value in other places. $this->isUrl = true; $ctx = null; // https://github.com/php/php-src/issues/16023 - if (substr($path, 0, 6) === 'https:') { - $ctx = stream_context_create(['ssl' => ['crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT]]); + // https://github.com/php/php-src/issues/17121 + if (Preg::isMatch('/^https?:/', $path)) { + $ctxArray = [ + 'http' => [ + 'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'header' => [ + //'Connection: keep-alive', // unacceptable performance + 'Accept: image/*;q=0.9,*/*;q=0.8', + ], + ], + ]; + if (Preg::isMatch('/^https:/', $path)) { + $ctxArray['ssl'] = ['crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT]; + } + $ctx = stream_context_create($ctxArray); } $imageContents = @file_get_contents($path, false, $ctx); if ($imageContents !== false) { @@ -140,15 +175,6 @@ public function setPath($path, $verifyFile = true, $zip = null) } } } - } elseif ($zip instanceof ZipArchive) { - $zipPath = explode('#', $path)[1]; - $locate = @$zip->locateName($zipPath); - if ($locate !== false) { - if ($this->isImage($path)) { - $this->path = $path; - $this->setSizesAndType($path); - } - } } else { $exists = @file_exists($path); if ($exists !== false && $this->isImage($path)) { @@ -160,6 +186,12 @@ public function setPath($path, $verifyFile = true, $zip = null) throw new PhpSpreadsheetException("File $path not found!"); } + if ($this->worksheet !== null) { + if ($this->path !== '') { + $this->worksheet->getCell($this->coordinates); + } + } + return $this; } @@ -167,7 +199,7 @@ private function isImage(string $path): bool { $mime = (string) @mime_content_type($path); $retVal = false; - if (str_starts_with($mime, 'image/')) { + if (strpos($mime, 'image/') === 0) { $retVal = true; } elseif ($mime === 'application/octet-stream') { $extension = pathinfo($path, PATHINFO_EXTENSION); @@ -189,6 +221,8 @@ public function getIsURL(): bool * Set isURL. * * @return $this + * + * @deprecated 3.7.0 not needed, property is set by setPath */ public function setIsURL(bool $isUrl): self { diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table.php index 1bc8dff..bab83d3 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table.php @@ -123,10 +123,10 @@ public function setName(string $name): self ) { throw new PhpSpreadsheetException('The table name can\'t be the same as a cell reference'); } - if (!preg_match('/^[\p{L}_\\\\]/iu', $name)) { + if (!preg_match('/^[\p{L}_\\\]/iu', $name)) { throw new PhpSpreadsheetException('The table name must begin a name with a letter, an underscore character (_), or a backslash (\)'); } - if (!preg_match('/^[\p{L}_\\\\][\p{L}\p{M}0-9\._]+$/iu', $name)) { + if (!preg_match('/^[\p{L}_\\\][\p{L}\p{M}0-9\._]+$/iu', $name)) { throw new PhpSpreadsheetException('The table name contains invalid characters'); } @@ -324,7 +324,7 @@ public function setRangeToMaxRow(): self { if ($this->workSheet !== null) { $thisrange = $this->range; - $range = (string) preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange); + $range = (string) preg_replace('/\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange); if ($range !== $thisrange) { $this->setRange($range); } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Validations.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Validations.php index 42ba566..c9a8f28 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Validations.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Validations.php @@ -71,7 +71,7 @@ public static function validateCellRange($cellRange): string // Convert Column ranges like 'A:C' to 'A1:C1048576' // or Row ranges like '1:3' to 'A1:XFD3' $addressRange = (string) preg_replace( - ['/^([A-Z]+):([A-Z]+)$/i', '/^(\\d+):(\\d+)$/'], + ['/^([A-Z]+):([A-Z]+)$/i', '/^(\d+):(\d+)$/'], [self::SETMAXROW, self::SETMAXCOL], $addressRange ); diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Worksheet.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Worksheet.php index 29221e9..0a875d6 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; use ArrayObject; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Cell\AddressRange; @@ -20,7 +21,6 @@ use PhpOffice\PhpSpreadsheet\Comment; use PhpOffice\PhpSpreadsheet\DefinedName; use PhpOffice\PhpSpreadsheet\Exception; -use PhpOffice\PhpSpreadsheet\IComparable; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared; @@ -31,7 +31,7 @@ use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; -class Worksheet implements IComparable +class Worksheet { // Break types public const BREAK_NONE = 0; @@ -338,17 +338,10 @@ class Worksheet implements IComparable */ private $tabColor; - /** - * Dirty flag. - * - * @var bool - */ - private $dirty = true; - /** * Hash. * - * @var string + * @var int */ private $hash; @@ -368,6 +361,7 @@ public function __construct(?Spreadsheet $parent = null, $title = 'Worksheet') { // Set parent and title $this->parent = $parent; + $this->hash = spl_object_id($this); $this->setTitle($title, false); // setTitle can change $pTitle $this->setCodeName($this->getTitle()); @@ -424,6 +418,11 @@ public function __destruct() $this->rowDimensions = []; } + public function __wakeup(): void + { + $this->hash = spl_object_id($this); + } + /** * Return the cell collection. * @@ -914,7 +913,7 @@ public function setTitle($title, $updateFormulaCellReferences = true, $validate // Syntax check self::checkSheetTitle($title); - if ($this->parent) { + if ($this->parent && $this->parent->getIndex($this, true) >= 0) { // Is there already such sheet name? if ($this->parent->sheetNameExists($title)) { // Use name, but append with lowest possible integer @@ -943,9 +942,8 @@ public function setTitle($title, $updateFormulaCellReferences = true, $validate // Set title $this->title = $title; - $this->dirty = true; - if ($this->parent && $this->parent->getCalculationEngine()) { + if ($this->parent && $this->parent->getIndex($this, true) >= 0 && $this->parent->getCalculationEngine()) { // New title $newTitle = $this->getTitle(); $this->parent->getCalculationEngine() @@ -1088,7 +1086,6 @@ public function getProtection() public function setProtection(Protection $protection) { $this->protection = $protection; - $this->dirty = true; return $this; } @@ -1311,8 +1308,8 @@ private function getWorksheetAndCoordinate(string $coordinate): array throw new Exception('Sheet not found for name: ' . $worksheetReference[0]); } } elseif ( - !preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate) && - preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate) + !Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate) && + Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate) ) { // Named range? $namedRange = $this->validateNamedRange($coordinate, true); @@ -1897,7 +1894,7 @@ public function mergeCells($range, $behaviour = self::MERGE_CELL_CONTENT_EMPTY) $range .= ":{$range}"; } - if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) !== 1) { + if (!Preg::isMatch('/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/', $range, $matches)) { throw new Exception('Merge must be on a valid range of cells.'); } @@ -2554,6 +2551,42 @@ public function removeRow(int $row, int $numberOfRows = 1) if ($row < 1) { throw new Exception('Rows to be deleted should at least start from row 1.'); } + $startRow = $row; + $endRow = $startRow + $numberOfRows - 1; + $removeKeys = []; + $addKeys = []; + foreach ($this->mergeCells as $key => $value) { + if ( + Preg::isMatch( + '/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i', + $key, + $matches + ) + ) { + $startMergeInt = (int) $matches[2]; + $endMergeInt = (int) $matches[4]; + if ($startMergeInt >= $startRow) { + if ($startMergeInt <= $endRow) { + $removeKeys[] = $key; + } + } elseif ($endMergeInt >= $startRow) { + if ($endMergeInt <= $endRow) { + $temp = $endMergeInt - 1; + $removeKeys[] = $key; + if ($temp !== $startMergeInt) { + $temp3 = $matches[1] . $matches[2] . ':' . $matches[3] . $temp; + $addKeys[] = $temp3; + } + } + } + } + } + foreach ($removeKeys as $key) { + unset($this->mergeCells[$key]); + } + foreach ($addKeys as $key) { + $this->mergeCells[$key] = $key; + } $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows); $highestRow = $this->getHighestDataRow(); @@ -2610,6 +2643,43 @@ public function removeColumn(string $column, int $numberOfColumns = 1) if (is_numeric($column)) { throw new Exception('Column references should not be numeric.'); } + $startColumnInt = Coordinate::columnIndexFromString($column); + $endColumnInt = $startColumnInt + $numberOfColumns - 1; + $removeKeys = []; + $addKeys = []; + foreach ($this->mergeCells as $key => $value) { + if ( + Preg::isMatch( + '/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i', + $key, + $matches + ) + ) { + $startMergeInt = Coordinate::columnIndexFromString($matches[1]); + $endMergeInt = Coordinate::columnIndexFromString($matches[3]); + if ($startMergeInt >= $startColumnInt) { + if ($startMergeInt <= $endColumnInt) { + $removeKeys[] = $key; + } + } elseif ($endMergeInt >= $startColumnInt) { + if ($endMergeInt <= $endColumnInt) { + $temp = Coordinate::columnIndexFromString($matches[3]) - 1; + $temp2 = Coordinate::stringFromColumnIndex($temp); + $removeKeys[] = $key; + if ($temp2 !== $matches[1]) { + $temp3 = $matches[1] . $matches[2] . ':' . $temp2 . $matches[4]; + $addKeys[] = $temp3; + } + } + } + } + } + foreach ($removeKeys as $key) { + unset($this->mergeCells[$key]); + } + foreach ($addKeys as $key) { + $this->mergeCells[$key] = $key; + } $highestColumn = $this->getHighestDataColumn(); $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); @@ -3137,7 +3207,7 @@ private function validateNamedRange(string $definedName, bool $returnNullIfInval if ($namedRange->getLocalOnly()) { $worksheet = $namedRange->getWorksheet(); - if ($worksheet === null || $this->getHashCode() !== $worksheet->getHashCode()) { + if ($worksheet === null || $this->getHashInt() !== $worksheet->getHashInt()) { if ($returnNullIfInvalid) { return null; } @@ -3278,17 +3348,20 @@ public function garbageCollect() } /** - * Get hash code. + * @deprecated 3.5.0 use getHashInt instead. * * @return string Hash code */ public function getHashCode() { - if ($this->dirty) { - $this->hash = md5($this->title . $this->autoFilter . ($this->protection->isProtectionEnabled() ? 't' : 'f') . __CLASS__); - $this->dirty = false; - } + return (string) $this->hash; + } + /** + * @return int Hash code + */ + public function getHashInt() + { return $this->hash; } @@ -3620,6 +3693,7 @@ public function __clone() } } } + $this->hash = spl_object_id($this); } /** @@ -3702,6 +3776,6 @@ public function hasCodeName() public static function nameRequiresQuotes(string $sheetName): bool { - return preg_match(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName) !== 1; + return !Preg::isMatch(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName); } } diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Html.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Html.php index 66a0ec8..5da3a1f 100644 --- a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Html.php +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Html.php @@ -407,12 +407,12 @@ public function generateHTMLHeader($includeStyles = false) } else { $propertyValue = (string) $propertyValue; } - $html .= self::generateMeta($propertyValue, "custom.$propertyQualifier.$customProperty"); + $html .= self::generateMeta($propertyValue, htmlspecialchars("custom.$propertyQualifier.$customProperty")); } } if (!empty($properties->getHyperlinkBase())) { - $html .= ' ' . PHP_EOL; + $html .= ' ' . PHP_EOL; } $html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true); @@ -563,7 +563,7 @@ public function generateNavigation() $html .= '