diff --git a/CHANGELOG.md b/CHANGELOG.md index 323774a0c..ee5fd1cb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,39 +4,40 @@ 1. [](#new) * Added `select()` and `unselect()` methods to `CollectionInterface` and its base classes * Added `orderBy()` and `limit()` methods to `ObjectCollectionInterface` and its base classes - * Flex: Added support for custom object index classes (API compatibility break) * Added `user-data://` which is a writable stream (`user://data` is not and should be avoided) * Added support for `/action:{$action}` (like task but used without nonce when only receiving data) * Added `onAction.{$action}` event * Added `Grav\Framework\Form\FormFlash` class to contain AJAX uploaded files in more reliable way * Added `Grav\Framework\Form\FormFlashFile` class which implements `UploadedFileInterface` from PSR-7 * Added `Grav\Framework\Filesystem\Filesystem` class with methods to manipulate stream URLs + * Grav 1.6: Flex: Added support for custom object index classes (API compatibility break) 1. [](#improved) - * Improved Flex storage classes * Improved `Grav\Framework\File\Formatter` classes to have abstract parent class and some useful methods + * Grav 1.6: Improved Flex storage classes + * Grav 1.6: Improved `Grav\Framework\File` classes to use better type hints and the new `Filesystem` class 1. [](#bugfix) * Fixed handling of `append_url_extension` inside of `Page::templateFormat()` [#2264](https://github.com/getgrav/grav/issues/2264) * Fixed a broken language string [#2261](https://github.com/getgrav/grav/issues/2261) * Fixed clearing cache having no effect on Doctrine cache * Fixed `Medium::relativePath()` for streams - * Fixed `FlexObject::update()` call with partial object update * Fixed `Object` serialization breaking if overriding `jsonSerialize()` method + * Grav 1.6: Fixed `FlexObject::update()` call with partial object update # v1.6.0-beta.6 ## 11/12/2018 1. [](#new) - * Added `CsvFormatter` and `CsvFile` classes * Added `$grav->setup()` to simplify CLI and custom access points + * Grav 1.6: Added `CsvFormatter` and `CsvFile` classes 1. [](#improved) * Support negotiated content types set via the Request `Accept:` header * Support negotiated language types set via the Request `Accept-Language:` header - * Allow custom Flex form views * Cleaned up and sorted the Service `idMap` + * Grav 1.6: Allow custom Flex form views 1. [](#bugfix) * Fixed `Uri::hasStandardPort()` to support reverse proxy configurations [#1786](https://github.com/getgrav/grav/issues/1786) * Use `append_url_extension` from page header to set template format if set [#2604](https://github.com/getgrav/grav/pull/2064) - * Fixed some bugs in environment selection + * Fixed some bugs in Grav environment selection logic # v1.6.0-beta.5 ## 11/05/2018 @@ -55,7 +56,7 @@ * Set session name based on `security.salt` rather than `GRAV_ROOT` [#2242](https://github.com/getgrav/grav/issues/2242) * Added option to configure list of `xss_invalid_protocols` in `Security` config [#2250](https://github.com/getgrav/grav/issues/2250) * Smarter `security.salt` checking now we use `security.yaml` for other options - * Merged Grav 1.5.4 fixes in + * Grav 1.6: Merged Grav 1.5.4 fixes in # v1.6.0-beta.4 ## 10/24/2018 @@ -84,15 +85,15 @@ ## 10/09/2018 1. [](#new) - * Added Flex support for custom media tasks + * Grav 1.6: Added Flex support for custom media tasks 1. [](#improved) * Added support for syslog and syslog facility logging (default: 'file') * Improved usability of `System` configuration blueprint with side-tabs 1. [](#bugfix) * Fixed asset manager to not add empty assets when they don't exist in the filesystem - * Regression: Fixed asset manager methods with default legacy attributes * Update `script` and `style` Twig tags to use the new `Assets` classes * Fixed asset pipeline to rewrite remote URLs as well as local [#2216](https://github.com/getgrav/grav/issues/2216) + * Grav 1.6: Regression: Fixed asset manager methods with default legacy attributes # v1.6.0-beta.1 ## 10/01/2018 diff --git a/system/src/Grav/Framework/File/AbstractFile.php b/system/src/Grav/Framework/File/AbstractFile.php index 0023d48ce..9317a6949 100644 --- a/system/src/Grav/Framework/File/AbstractFile.php +++ b/system/src/Grav/Framework/File/AbstractFile.php @@ -12,9 +12,13 @@ declare(strict_types=1); namespace Grav\Framework\File; use Grav\Framework\File\Interfaces\FileInterface; +use Grav\Framework\Filesystem\Filesystem; class AbstractFile implements FileInterface { + /** @var Filesystem */ + private $filesystem; + /** @var string */ private $filepath; @@ -36,8 +40,13 @@ class AbstractFile implements FileInterface /** @var bool */ private $locked = false; - public function __construct($filepath) + /** + * @param string $filepath + * @param Filesystem|null $filesystem + */ + public function __construct(string $filepath, Filesystem $filesystem = null) { + $this->filesystem = $filesystem ?? new Filesystem(); $this->setFilepath($filepath); } @@ -51,11 +60,26 @@ class AbstractFile implements FileInterface } } - /** - * Prevent cloning. - */ - private function __clone() + public function __clone() { + $this->handle = null; + $this->locked = false; + } + + /** + * @return string + */ + public function serialize(): string + { + return serialize($this->doSerialize()); + } + + /** + * @param string $serialized + */ + public function unserialize($serialized): void + { + $this->doUnserialize(unserialize($serialized, ['allowed_classes' => false])); } /** @@ -63,7 +87,7 @@ class AbstractFile implements FileInterface * * @return string */ - public function getFilePath() : string + public function getFilePath(): string { return $this->filepath; } @@ -73,7 +97,7 @@ class AbstractFile implements FileInterface * * @return string */ - public function getPath() : string + public function getPath(): string { if (null === $this->path) { $this->setPathInfo(); @@ -87,7 +111,7 @@ class AbstractFile implements FileInterface * * @return string */ - public function getFilename() : string + public function getFilename(): string { if (null === $this->filename) { $this->setPathInfo(); @@ -101,7 +125,7 @@ class AbstractFile implements FileInterface * * @return string */ - public function getBasename() : string + public function getBasename(): string { if (null === $this->basename) { $this->setPathInfo(); @@ -113,10 +137,10 @@ class AbstractFile implements FileInterface /** * Return file extension. * - * @param $withDot + * @param bool $withDot * @return string */ - public function getExtension($withDot = false) : string + public function getExtension(bool $withDot = false): string { if (null === $this->extension) { $this->setPathInfo(); @@ -130,7 +154,7 @@ class AbstractFile implements FileInterface * * @return bool */ - public function exists() : bool + public function exists(): bool { return is_file($this->filepath); } @@ -138,21 +162,21 @@ class AbstractFile implements FileInterface /** * Return file modification time. * - * @return int|bool Timestamp or false if file doesn't exist. + * @return int Unix timestamp. If file does not exist, method returns current time. */ - public function getCreationTime() + public function getCreationTime(): int { - return is_file($this->filepath) ? filectime($this->filepath) : false; + return is_file($this->filepath) ? filectime($this->filepath) : time(); } /** * Return file modification time. * - * @return int|bool Timestamp or false if file doesn't exist. + * @return int Unix timestamp. If file does not exist, method returns current time. */ - public function getModificationTime() + public function getModificationTime(): int { - return is_file($this->filepath) ? filemtime($this->filepath) : false; + return is_file($this->filepath) ? filemtime($this->filepath) : time(); } /** @@ -162,7 +186,7 @@ class AbstractFile implements FileInterface * @return bool * @throws \RuntimeException */ - public function lock($block = true) : bool + public function lock(bool $block = true): bool { if (!$this->handle) { if (!$this->mkdir($this->getPath())) { @@ -184,7 +208,7 @@ class AbstractFile implements FileInterface * * @return bool */ - public function unlock() : bool + public function unlock(): bool { if (!$this->handle) { return false; @@ -204,25 +228,39 @@ class AbstractFile implements FileInterface * * @return bool True = locked, false = not locked. */ - public function isLocked() : bool + public function isLocked(): bool { return $this->locked; } + /** + * Check if file exists and can be read. + * + * @return bool + */ + public function isReadable(): bool + { + return is_readable($this->filepath) && is_file($this->filepath); + } + /** * Check if file can be written. * * @return bool */ - public function isWritable() : bool + public function isWritable(): bool { - return is_writable($this->filepath) || $this->isWritableDir($this->getPath()); + if (!file_exists($this->filepath)) { + return $this->isWritablePath($this->getPath()); + } + + return is_writable($this->filepath) && is_file($this->filepath); } /** - * (Re)Load a file and return RAW file contents. + * (Re)Load a file and return file contents. * - * @return string + * @return string|array|false */ public function load() { @@ -235,7 +273,7 @@ class AbstractFile implements FileInterface * @param mixed $data * @throws \RuntimeException */ - public function save($data) + public function save($data): void { $lock = false; if (!$this->locked) { @@ -266,7 +304,7 @@ class AbstractFile implements FileInterface * @param string $path * @return bool */ - public function rename($path) : bool + public function rename(string $path): bool { if ($this->exists() && !@rename($this->filepath, $path)) { return false; @@ -282,7 +320,7 @@ class AbstractFile implements FileInterface * * @return bool */ - public function delete() : bool + public function delete(): bool { return @unlink($this->filepath); } @@ -293,7 +331,7 @@ class AbstractFile implements FileInterface * @throws \RuntimeException * @internal */ - protected function mkdir($dir) : bool + protected function mkdir(string $dir): bool { // Silence error for open_basedir; should fail in mkdir instead. if (!@is_dir($dir)) { @@ -310,20 +348,27 @@ class AbstractFile implements FileInterface } /** - * @param string $dir - * @return bool - * @internal + * @return array */ - protected function isWritableDir($dir) : bool + protected function doSerialize(): array { - if ($dir && !file_exists($dir)) { - return $this->isWritableDir(\dirname($dir)); - } - - return $dir && is_dir($dir) && is_writable($dir); + return [ + 'filepath' => $this->filepath + ]; } - protected function setFilepath($filepath) : void + /** + * @param array $serialized + */ + protected function doUnserialize(array $serialized): void + { + $this->setFilepath($serialized['filepath']); + } + + /** + * @param string $filepath + */ + protected function setFilepath(string $filepath): void { $this->filepath = $filepath; $this->filename = null; @@ -332,64 +377,32 @@ class AbstractFile implements FileInterface $this->extension = null; } - protected function setPathInfo() : void + protected function setPathInfo(): void { - $pathInfo = static::pathinfo($this->filepath); - $this->filename = $pathInfo['filename']; - $this->basename = $pathInfo['basename']; - $this->path = $pathInfo['dirname']; - $this->extension = $pathInfo['extension']; + $pathInfo = $this->filesystem->pathinfo($this->filepath); + + $this->filename = $pathInfo['filename'] ?? null; + $this->basename = $pathInfo['basename'] ?? null; + $this->path = $pathInfo['dirname'] ?? null; + $this->extension = $pathInfo['extension'] ?? null; } /** - * Multi-byte-safe pathinfo replacement. - * Replacement for pathinfo(), but stream, multibyte and cross-platform safe. - * - * @see http://www.php.net/manual/en/function.pathinfo.php - * - * @param string $path A filename or path, does not need to exist as a file - * @param int|string $options Either a PATHINFO_* constant, - * or a string name to return only the specified piece - * - * @return string|array + * @param string $dir + * @return bool + * @internal */ - public static function pathinfo($path, $options = null) + protected function isWritablePath(string $dir): bool { - $ret = ['scheme' => '', 'dirname' => '', 'basename' => '', 'extension' => '', 'filename' => '']; - $pathinfo = []; - if (preg_match('#^((.*?)://)?(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$#um', $path, $pathinfo)) { - if (array_key_exists(1, $pathinfo)) { - $ret['scheme'] = $pathinfo[2]; - $ret['dirname'] = $pathinfo[1]; - } - if (array_key_exists(3, $pathinfo)) { - $ret['dirname'] .= $pathinfo[3]; - } - if (array_key_exists(4, $pathinfo)) { - $ret['basename'] = $pathinfo[4]; - } - if (array_key_exists(7, $pathinfo)) { - $ret['extension'] = $pathinfo[7]; - } - if (array_key_exists(5, $pathinfo)) { - $ret['filename'] = $pathinfo[5]; - } + if ($dir === '') { + return false; } - switch ($options) { - case PATHINFO_DIRNAME: - case 'dirname': - return $ret['dirname']; - case PATHINFO_BASENAME: - case 'basename': - return $ret['basename']; - case PATHINFO_EXTENSION: - case 'extension': - return $ret['extension']; - case PATHINFO_FILENAME: - case 'filename': - return $ret['filename']; - default: - return $ret; + + if (!file_exists($dir)) { + // Recursively look up in the directory tree. + return $this->isWritablePath($this->filesystem->parent($dir)); } + + return is_dir($dir) && is_writable($dir); } } diff --git a/system/src/Grav/Framework/File/DataFile.php b/system/src/Grav/Framework/File/DataFile.php index fd6b8957f..cd8fe1c89 100644 --- a/system/src/Grav/Framework/File/DataFile.php +++ b/system/src/Grav/Framework/File/DataFile.php @@ -34,7 +34,7 @@ class DataFile extends AbstractFile /** * (Re)Load a file and return RAW file contents. * - * @return array + * @return array|false * @throws RuntimeException */ public function load() @@ -42,7 +42,7 @@ class DataFile extends AbstractFile $raw = parent::load(); try { - return $this->formatter->decode($raw); + return $raw !== false ? $this->formatter->decode($raw) : false; } catch (RuntimeException $e) { throw new RuntimeException(sprintf("Failed to load file '%s': %s", $this->getFilePath(), $e->getMessage()), $e->getCode(), $e); } @@ -54,9 +54,10 @@ class DataFile extends AbstractFile * @param string|array $data Data to be saved. * @throws RuntimeException */ - public function save($data) + public function save($data): void { if (\is_string($data)) { + // Make sure that the string is valid data. try { $this->formatter->decode($data); } catch (RuntimeException $e) { diff --git a/system/src/Grav/Framework/File/File.php b/system/src/Grav/Framework/File/File.php index bcac43941..31072b43c 100644 --- a/system/src/Grav/Framework/File/File.php +++ b/system/src/Grav/Framework/File/File.php @@ -16,11 +16,11 @@ class File extends AbstractFile /** * Load a file from the filesystem. * - * @return string + * @return string|false */ public function load() { - return (string) parent::load(); + return parent::load(); } /** @@ -29,8 +29,12 @@ class File extends AbstractFile * @param string $data * @throws \RuntimeException */ - public function save($data) + public function save($data): void { + if (!\is_string($data)) { + throw new \RuntimeException('Cannot save data, string required'); + } + parent::save($data); } } diff --git a/system/src/Grav/Framework/File/Interfaces/FileInterface.php b/system/src/Grav/Framework/File/Interfaces/FileInterface.php index 3aa8745ea..9c727398b 100644 --- a/system/src/Grav/Framework/File/Interfaces/FileInterface.php +++ b/system/src/Grav/Framework/File/Interfaces/FileInterface.php @@ -11,64 +11,64 @@ declare(strict_types=1); namespace Grav\Framework\File\Interfaces; -interface FileInterface +interface FileInterface extends \Serializable { /** * Get full path to the file. * * @return string */ - public function getFilePath() : string; + public function getFilePath(): string; /** * Get path to the file. * * @return string */ - public function getPath() : string; + public function getPath(): string; /** * Get filename. * * @return string */ - public function getFilename() : string; + public function getFilename(): string; /** * Return name of the file without extension. * * @return string */ - public function getBasename() : string; + public function getBasename(): string; /** * Return file extension. * - * @param $withDot + * @param bool $withDot * @return string */ - public function getExtension($withDot = false) : string; + public function getExtension(bool $withDot = false): string; /** * Check if file exits. * * @return bool */ - public function exists() : bool; + public function exists(): bool; /** * Return file modification time. * - * @return int|bool Timestamp or false if file doesn't exist. + * @return int Unix timestamp. If file does not exist, method returns current time. */ - public function getCreationTime(); + public function getCreationTime(): int; /** * Return file modification time. * - * @return int|bool Timestamp or false if file doesn't exist. + * @return int Unix timestamp. If file does not exist, method returns current time. */ - public function getModificationTime(); + public function getModificationTime(): int; /** * Lock file for writing. You need to manually unlock(). @@ -77,33 +77,40 @@ interface FileInterface * @return bool * @throws \RuntimeException */ - public function lock($block = true) : bool; + public function lock(bool $block = true): bool; /** * Unlock file. * * @return bool */ - public function unlock() : bool; + public function unlock(): bool; /** * Returns true if file has been locked for writing. * * @return bool True = locked, false = not locked. */ - public function isLocked() : bool; + public function isLocked(): bool; + + /** + * Check if file exists and can be read. + * + * @return bool + */ + public function isReadable(): bool; /** * Check if file can be written. * * @return bool */ - public function isWritable() : bool; + public function isWritable(): bool; /** * (Re)Load a file and return RAW file contents. * - * @return string + * @return string|array|false */ public function load(); @@ -113,7 +120,7 @@ interface FileInterface * @param mixed $data * @throws \RuntimeException */ - public function save($data); + public function save($data): void; /** * Rename file in the filesystem if it exists. @@ -121,26 +128,12 @@ interface FileInterface * @param string $path * @return bool */ - public function rename($path) : bool; + public function rename(string $path): bool; /** * Delete file from filesystem. * * @return bool */ - public function delete() : bool; - - /** - * Multi-byte-safe pathinfo replacement. - * Replacement for pathinfo(), but stream, multibyte and cross-platform safe. - * - * @see http://www.php.net/manual/en/function.pathinfo.php - * - * @param string $path A filename or path, does not need to exist as a file - * @param int|string $options Either a PATHINFO_* constant, - * or a string name to return only the specified piece - * - * @return string|array - */ - public static function pathinfo($path, $options = null); + public function delete(): bool; }