Initial commit IQParts\Content

This commit is contained in:
2018-03-01 18:41:55 +01:00
commit 349fa9c003
23 changed files with 1535 additions and 0 deletions

86
src/AssetResolver.php Normal file
View File

@@ -0,0 +1,86 @@
<?php
namespace IQParts\Content;
use GuzzleHttp\Psr7\Stream;
use IQParts\Content\Object\CacheControl;
use IQParts\Content\Object\File;
use League\Flysystem\FileNotFoundException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class AssetResolver
* @package IQParts\Framework\Content
*/
final class AssetResolver
{
/**
* @var MimeDetector
*/
private $detector;
/**
* @var Filesystem
*/
private $filesystem;
/**
* AssetResolver constructor.
* @param Filesystem $filesystem
* @internal param string $assetDir
*/
public function __construct(Filesystem $filesystem )
{
$this->detector = new MimeDetector();
$this->filesystem = $filesystem ;
}
/**
* @param string $file
* @return bool
*/
public function exists(string $file): bool
{
// Will never throw as we check if the file exists.
return $this->filesystem->exists($file) && $this->filesystem->get($file) instanceof File;
}
/**
* @param RequestInterface $request
* @param ResponseInterface $response
* @param CacheControl $cacheControl
* @param bool $inline
* @return ResponseInterface|bool
* @throws FileNotFoundException
*/
public function resolve(
RequestInterface $request,
ResponseInterface $response,
CacheControl $cacheControl,
bool $inline = true
)
{
$target = $request->getRequestTarget();
$file = $this->filesystem->get($target);
/** @var ResponseInterface $response */
$response = $response->withHeader('Cache-Control', (string) $cacheControl);
$response = $response->withHeader('Content-Type', $this->detector->detectFromExtension(
$file->getBasename())
);
if ($inline) {
$response = $response->withHeader('Content-Disposition', 'inline; filename="' . $file->getBasename() . '";');
} else {
$response = $response->withHeader('Content-Disposition', 'attachment; filename="' . $file->getBasename() . '";');
}
$response = $response->withHeader('Content-Length', (string)$file->getSize());
/**
* Will always return a stream
*/
$stream = $file->getStream();
return $response->withBody(new Stream($stream))->withStatus(200);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace IQParts\Framework\Content;
interface ContentProviderInterface
{
/**
* @param string $location
* @return bool
*/
public function exists(string $location): bool;
/**
* @param string $location
* @param $data
* @return mixed
*/
public function put(string $location, $data);
/**
* @param string $location
* @return resource
*/
public function get(string $location);
/**
* @param string $location
* @return array
*/
public function getCollection(string $location): array;
/**
* @param string $location
* @return int
*/
public function getSize(string $location): int;
/**
* @param string $location
* @return bool
*/
public function remove(string $location): bool;
/**
* @param string $location
* @return string
*/
public function getPermissions(string $location): string;
/**
* @param string $location
* @return bool
*/
public function createCollection(string $location): bool;
}

197
src/Filesystem.php Normal file
View File

@@ -0,0 +1,197 @@
<?php
namespace IQParts\Content;
use IQParts\Content\Object\File;
use IQParts\Content\Object\Folder;
use League\Flysystem\AdapterInterface;
use League\Flysystem\Config;
use League\Flysystem\Directory;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\Filesystem as FlySystem;
use League\Flysystem\Plugin\ListPaths;
/**
* Class Filesystem
* @package IQParts\Content
*/
final class Filesystem
{
/**
* @var FlySystem
*/
private $flySystem;
/**
* Filesystem constructor.
* @param AdapterInterface $adapter
* @param Config $config
*/
public function __construct(AdapterInterface $adapter, Config $config = null)
{
if (!$config) $config = [];
$flySystem = new FlySystem($adapter, $config);
$flySystem->addPlugin(new ListPaths());
$this->flySystem = $flySystem;
}
/**
* @param string $path
* @return bool
*/
public function exists(string $path): bool
{
if ($path === '/') $path = '';
return $this->flySystem->has($path);
}
/**
* @param string $path
* @return File|Folder|false
* @throws FileNotFoundException
*/
public function get(string $path)
{
/**
* If path = '/' -> create a folder object with all dirs and folders as children
*/
if ($path === '/' || $path === '') {
$dirs = [];
$files = [];
$paths = $this->flySystem->listPaths('', false);
foreach ($paths as $item) {
/** @var File|Directory $i */
$i = $this->flySystem->get($item);
if ($i->getType() === 'dir') {
$dirs[] = base64_encode($i->getPath());
} else {
$files[] = base64_encode($i->getPath());
}
}
return Folder::fromVariables(base64_encode('/'), '/', '', null, $dirs, $files);
} else {
$item = $this->flySystem->get($path);
if ($item instanceof Directory) {
return new Folder($item);
} else {
return new File($item);
}
}
}
/**
* @param string $path
* @return bool|false|resource
*/
public function getStream(string $path)
{
return $this->flySystem->readStream($path);
}
/**
* @param string $path
* @param bool $recursive
* @return \Iterator|Folder[]|File[]
*/
public function listDirContents(string $path, bool $recursive = false): \Iterator
{
if ($path === '/') $path = '';
foreach ($this->flySystem->listPaths($path, $recursive) as $path) {
$item = $this->flySystem->get($path);
if ($item instanceof Directory) {
yield new Folder($item);
} else {
yield new File($item);
}
}
}
/**
* @param string $path
* @param bool $recursive
* @return \Iterator|Folder[]
*/
public function listDirs(string $path, bool $recursive = false): \Iterator
{
if ($path === '/') $path = '';
foreach ($this->flySystem->listPaths($path, $recursive) as $path) {
$item = $this->flySystem->get($path);
if ($item instanceof Directory) {
yield new Folder($item);
}
}
}
/**
* @param string $path
* @param bool $recursive
* @return \Iterator|File[]
*/
public function listFiles(string $path, bool $recursive = false): \Iterator
{
if ($path === '/') $path = '';
foreach ($this->flySystem->listContents($path, $recursive) as $i) {
$item = $this->flySystem->get($i['path']);
if ($item instanceof \League\Flysystem\File) yield new File($item);
}
}
/**
* @param string $path
* @return bool
*/
public function delete(string $path)
{
return $this->flySystem->delete($path);
}
/**
* @param string $path
* @return bool
*/
public function deleteDir(string $path)
{
return $this->flySystem->deleteDir($path);
}
/**
* @param string $path
* @param array $config
* @return bool
*/
public function createDir(string $path, array $config = [])
{
return $this->flySystem->createDir($path, $config);
}
/**
* @param string $path
* @param $contents
* @param array $config
* @return bool
*/
public function put(string $path, $contents, array $config = []): bool
{
return $this->flySystem->put($path, $contents, $config);
}
/**
* @param string $path
* @param resource $contents
* @param array $config
* @return bool
*/
public function putStream(string $path, $contents, array $config = []): bool
{
return $this->flySystem->putStream($path, $contents, $config);
}
/**
* @return FlySystem
*/
public function getFlySystem(): FlySystem
{
return $this->flySystem;
}
}

162
src/MimeDetector.php Normal file
View File

@@ -0,0 +1,162 @@
<?php
namespace IQParts\Content;
/**
* Class MimeDetector
* @package IQParts\Framework\Content
*/
final class MimeDetector
{
/**
* @var
*/
private $mimeTypes;
/**
* Factory constructor.
* @param array $extraMimes
*/
public function __construct(array $extraMimes = [])
{
$this->mimeTypes = array_merge(
[
'doc' => 'application/msword',
'dot' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'docm' => 'application/vnd.ms-word.document.macroEnabled.12',
'dotm' => 'application/vnd.ms-word.template.macroEnabled.12',
'xls' => 'application/vnd.ms-excel',
'xlt' => 'application/vnd.ms-excel',
'xla' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12',
'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'ppt' => 'application/vnd.ms-powerpoint',
'pot' => 'application/vnd.ms-powerpoint',
'pps' => 'application/vnd.ms-powerpoint',
'ppa' => 'application/vnd.ms-powerpoint',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
'potm' => 'application/vnd.ms-powerpoint.template.macroEnabled.12',
'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
'mdb' => 'application/vnd.ms-access',
'aac' => 'audio/aac',
'abw' => 'application/x-abiword',
'arc' => 'application/octet-stream',
'avi' => 'video/x-msvideo',
'azw' => 'application/vnd.amazon.ebook',
'bin' => 'application/octet-stream',
'bz' => 'application/x-bzip',
'bz2' => 'application/x-bzip2',
'csh' => 'application/x-csh',
'css' => 'text/css',
'csv' => 'text/csv',
'eot' => 'application/vnd.ms-fontobject',
'epub' => 'application/epub+zip',
'gif' => 'image/gif',
'htm' => 'text/html',
'html' => 'text/html',
'twig' => 'text/html',
'ico' => 'image/x-icon',
'ics' => 'text/calendar',
'jar' => 'application/java-archive',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpg',
'js' => 'application/javascript',
'json' => 'application/json',
'mid' => 'audio/midi',
'midi' => 'audio/midi',
'mpeg' => 'video/mpeg',
'mpkg' => 'application/vnd.apple.installer+xml',
'odp' => 'application/vnd.oasis.opendocument.presentation',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
'odt' => 'application/vnd.oasis.opendocument.text',
'oga' => 'audio/ogg',
'ogv' => 'video/ogg',
'ogx' => 'application/ogg',
'otf' => 'font/otf',
'png' => 'image/png',
'pdf' => 'application/pdf',
'rar' => 'application/x-rar-compressed',
'rtf' => 'application/rtf',
'sh' => 'application/x-sh',
'svg' => 'image/svg+xml',
'swf' => 'application/x-shockwave-flash',
'tar' => 'application/x-tar',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'ts' => 'application/typescript',
'txt' => 'text/plain',
'ttf' => 'font/ttf',
'vsd' => 'application/vnd.visio',
'wav' => 'audio/x-wav',
'weba' => 'audio/webm',
'webm' => 'video/webm',
'webp' => 'image/webp',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'xhtml' => 'application/xhtml+xml',
'xml' => 'application/xml',
'xul' => 'application/vnd.mozilla.xul+xml',
'zip' => 'application/zip',
'3gp' => 'video/3gpp',
'3g2' => 'video/3gpp2',
'7z' => 'application/x-7z-compressed'
],
$extraMimes
);
}
/**
* @param string $filename
* @return string
*/
public function detectFromExtension(string $filename): string
{
return $this->mimeTypes[$this->getExtension($filename)] ?? 'text/plain';
}
/**
* @param string $filename
* @return bool
*/
public function isText(string $filename): bool
{
$mime = $this->detectFromExtension($filename);
if (fnmatch('text/*', $mime)) {
return true;
}
$text = [
"application/javascript" => true,
"application/rtf" => true,
"application/json" => true,
"application/x-sh" => true,
"application/xml" => true
];
return isset($text[$mime]);
}
/**
* @param $filename
* @return string
*/
private function getExtension($filename): string
{
$file = basename($filename);
$dotPlace = strrpos($file, '.');
if ($dotPlace === false) {
return $file;
}
return substr($file, $dotPlace + 1);
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace IQParts\Content\Object;
final class CacheControl
{
public const MUST_REVALIDATE = 0;
public const NO_TRANSFORM = 1;
public const NO_CACHE = 2;
public const NO_STORE = 3;
public const PUBLIC = 4;
public const PRIVATE = 5;
public const PROXY = 6;
public const MAX_AGE = 7;
public const S_MAX_AGE = 8;
/**
* @var array
*/
private $modes = [
'must-revalidate',
'no-transform',
'no-cache',
'no-store',
'public',
'private',
'proxy-revalidate',
'max-age',
's-max-age'
];
/**
* @var int
*/
private $mode;
/**
* @var int
*/
private $seconds;
/**
* CacheControl constructor.
* @param int $mode
* @param int $seconds
*/
public function __construct(int $mode, int $seconds = -1)
{
if ($mode < 0 || $mode > 7) {
throw new \InvalidArgumentException('Invalid mode.');
}
if ($mode >= 7 && $seconds < 0) {
throw new \InvalidArgumentException('Invalid number of seconds.');
}
$this->mode = $mode;
$this->seconds = $seconds;
}
/**
* @return int|string
*/
public function __toString()
{
$mode = $this->modes[$this->mode];
if ($this->mode >= 7) {
return $mode . '=' . (string)$this->seconds;
}
return $mode;
}
}

252
src/Object/File.php Normal file
View File

@@ -0,0 +1,252 @@
<?php
declare(strict_types=1);
namespace IQParts\Content\Object;
use IQParts\Content\MimeDetector;
use League\Flysystem\File as FlyFile;
/**
* Class File
* @package IQParts\Content\Object
*/
final class File extends Item
{
/**
* @var FlyFile
*/
private $file;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $basename;
/**
* @var int
*/
private $size;
/**
* @var string
*/
private $extension;
/**
* @var string
*/
private $parent;
/**
* @var \DateTime
*/
private $date;
/**
* @var false|int
*/
private $timestamp;
/**
* @var bool
*/
private $isEditable;
/**
* PublicFile constructor.
* @param FlyFile $file
*/
public function __construct(FlyFile $file = null)
{
if ($file !== null) {
$editable = [
"js" => true,
"css" => true,
"html" => true,
"svg" => true
];
$this->file = $file;
$this->path = $file->getPath();
$this->size = $file->getSize();
$this->basename = basename($file->getPath());
$this->date = new \DateTime();
$timestamp = $file->getTimestamp();
$this->date->setTimestamp($timestamp);
$this->timestamp = $timestamp;
$dotPlace = strrpos($this->basename, '.');
if ($dotPlace !== false) $extension = substr($this->basename, $dotPlace + 1);
else $extension = '';
$this->extension = $extension;
$parentSep = strrpos($this->path, '/');
if ($parentSep !== false) {
$parent = substr($this->path, 0, $parentSep);
} else $parent = '/';
$this->parent = base64_encode($parent);
$this->isEditable = $editable[$extension] ?? false;
}
}
/**
* @return string
*/
public function getId(): string
{
return base64_encode($this->path);
}
/**
* @return FlyFile
*/
public function getFile(): FlyFile
{
return $this->file;
}
/**
* @return string
*/
public function getPath(): string
{
return $this->path;
}
/**
* @return int
*/
public function getSize(): int
{
return $this->size;
}
/**
* @return string
*/
public function getBasename(): string
{
return $this->basename;
}
/**
* @return string
*/
public function getExtension()
{
return $this->extension;
}
/**
* @return string
*/
public function getParent()
{
return $this->parent;
}
/**
* @return \DateTime
*/
public function getDate(): \DateTime
{
return $this->date;
}
/**
* @return int
*/
public function getTimestamp()
{
return $this->timestamp;
}
/**
* @return string
*/
public function getMD5(): string
{
return md5($this->file->read());
}
/**
* @return false|string
*/
public function getContents()
{
return $this->file->read();
}
/**
* @return false|resource
*/
public function getStream()
{
return $this->file->readStream();
}
/**
* @return false|string
*/
public function getMimetype()
{
return $this->file->getMimetype();
}
/**
* @return bool
*/
public function isEditable(): bool
{
return (new MimeDetector())->isText($this->basename);
}
/**
* @return array
*/
public function toArray(): array
{
return [
'basename' => $this->getBasename(),
'contents' => $this->isEditable() ? $this->getContents() : null,
'date' => $this->getDate()->format(DATE_ISO8601),
'extension' => $this->getExtension(),
'id' => $this->getId(),
'isEditable' => $this->isEditable(),
'mimetype' => $this->getMimetype(),
'md5' => $this->getMD5(),
'parent' => $this->getParent(),
'path' => $this->getPath(),
'size' => $this->getSize(),
'timestamp' => $this->getTimestamp()
];
}
/**
* @return bool
*/
function isDir(): bool
{
return false;
}
/**
* @return bool
*/
function isFile(): bool
{
return true;
}
/**
* @param string $path
* @param string $parent
* @param int $timestamp
* @return File
*/
public static function fromVariables(string $path, string $parent, int $timestamp): self
{
$file = new self;
$file->path = $path;
$file->parent = $parent;
$file->timestamp = $timestamp;
return $file;
}
}

187
src/Object/Folder.php Normal file
View File

@@ -0,0 +1,187 @@
<?php
namespace IQParts\Content\Object;
use League\Flysystem\Directory;
/**
* Class Folder
* @package IQParts\Content\Object
*/
final class Folder extends Item
{
/**
* @var Directory
*/
private $directory;
/**
* @var string
*/
private $path;
/**
* @var string|null
*/
private $parent;
/**
* @var string
*/
private $basename;
/**
* @var array
*/
private $dirs;
/**
* @var array
*/
private $files;
/**
* Folder constructor.
* @param Directory $directory
*/
public function __construct(Directory $directory = null)
{
if ($directory) {
$this->directory = $directory;
$this->path = $directory->getPath();
$filesystem = $directory->getFilesystem();
$parentSep = strrpos($this->path, '/');
if ($parentSep !== false) {
$parent = substr($this->path, 0, $parentSep);
} else $parent = '/';
$this->parent = base64_encode($parent);
$this->basename = basename($this->path);
$paths = $filesystem->listContents($this->path);
$dirs = [];
$files = [];
foreach ($paths as $path) {
$item = $filesystem->get($path['path']);
if ($item instanceof Directory) {
$dirs[] = base64_encode($item->getPath());
} else {
$files[] = base64_encode($item->getPath());
}
}
$this->dirs = $dirs;
$this->files = $files;
}
}
/**
* @return Directory
*/
public function getDirectory(): Directory
{
return $this->directory;
}
/**
* @return string
*/
public function getId(): string
{
return base64_encode($this->getPath());
}
/**
* @return string
*/
public function getPath(): string
{
return $this->path;
}
/**
* @return string|null
*/
public function getParent()
{
return $this->parent;
}
/**
* @return string
*/
public function getBasename(): string
{
return $this->basename;
}
/**
* @return array
*/
public function getDirs(): array
{
return $this->dirs;
}
/**
* @return array
*/
public function getFiles(): array
{
return $this->files;
}
/**
* @return array
*/
public function toArray(): array
{
return [
'id' => $this->getId(),
'path' => $this->getPath(),
'basename' => $this->getBasename(),
'parent' => $this->getParent(),
'dirs' => $this->getDirs(),
'files' => $this->getFiles()
];
}
/**
* @return bool
*/
function isDir(): bool
{
return true;
}
/**
* @return bool
*/
function isFile(): bool
{
return false;
}
/**
* @param string $id
* @param string $path
* @param string $basename
* @param string|null $parent
* @param array $dirs
* @param array $files
* @return Folder
*/
public static function fromVariables(
string $id,
string $path,
string $basename,
$parent = null,
array $dirs = [],
array $files = []
): Folder
{
$dir = new self();
$dir->id = $id;
$dir->path = $path;
$dir->basename = $basename;
$dir->parent = $parent;
$dir->dirs = $dirs;
$dir->files = $files;
return $dir;
}
}

20
src/Object/Item.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
namespace IQParts\Content\Object;
/**
* Class Item
* @package IQParts\Content\Object
*/
abstract class Item
{
/**
* @return bool
*/
abstract function isDir(): bool;
/**
* @return bool
*/
abstract function isFile(): bool;
}