<?php
/**
* @package Gantry5
* @author RocketTheme http://www.rockettheme.com
* @copyright Copyright (C) 2007 - 2021 RocketTheme, LLC
* @license Dual License: MIT or GNU/GPLv2 and later
*
* http://opensource.org/licenses/MIT
* http://www.gnu.org/licenses/gpl-2.0.html
*
* Gantry Framework code that extends GPL code is considered GNU/GPLv2 and later
*/
namespace Gantry\Component\Outline;
use FilesystemIterator;
use Gantry\Component\Collection\Collection;
use Gantry\Component\File\CompiledYamlFile;
use Gantry\Component\Filesystem\Folder;
use Gantry\Component\Layout\Layout;
use Gantry\Framework\Atoms;
use RocketTheme\Toolbox\DI\Container;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceIterator;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
/**
* Class OutlineCollection
* @package Gantry\Component\Outline
*/
class OutlineCollection extends Collection
{
/** @var Container */
protected $container;
/** @var string */
protected $path;
/**
* @param Container $container
* @param array $items
*/
public function __construct(Container $container, $items = [])
{
$this->container = $container;
$this->items = $items;
}
/**
* @param string $id
* @return string|null
*/
public function name($id)
{
return isset($this->items[$id]) ? $this->items[$id] : null;
}
/**
* @param string $id
* @return string
*/
public function title($id)
{
return isset($this->items[$id]) ? $this->items[$id] : $id;
}
/**
* @return $this
*/
public function all()
{
return $this;
}
/**
* @return $this
*/
public function system()
{
foreach ($this->items as $key => $item) {
$key = (string)$key;
if ($key && $key[0] !== '_') {
unset($this->items[$key]);
}
}
return $this;
}
/**
* @return $this
*/
public function user()
{
foreach ($this->items as $key => $item) {
$key = (string)$key;
if ($key === 'default' || ($key && $key[0] === '_')) {
unset($this->items[$key]);
}
}
return $this;
}
/**
* @param string[]|null $include
* @return $this
*/
public function filter(array $include = null)
{
if ($include !== null) {
foreach ($this->items as $key => $item) {
if (!in_array($key, $include, true)) {
unset($this->items[$key]);
}
}
}
return $this;
}
/**
* Returns list of all positions defined in all outlines.
*
* @return array
*/
public function positions()
{
$list = [];
foreach ($this->items as $name => $title) {
try {
$index = Layout::index($name);
$list += $index['positions'];
} catch (\Exception $e) {
// Layout cannot be read. We will just skip it instead of throwing an exception.
}
}
return $list;
}
/**
* @param string $section
* @param bool $includeInherited
* @return array
*/
public function getOutlinesWithSection($section, $includeInherited = true)
{
$list = [];
foreach ($this->items as $name => $title) {
try {
$index = Layout::index($name);
} catch (\Exception $e) {
// Layout cannot be read. We will just skip it instead of throwing an exception.
continue;
}
if (isset($index['sections'][$section])) {
if (!$includeInherited) {
foreach ($index['inherit'] as $outline => $items) {
if (is_array($items) && in_array($section, $items, true)) {
continue 2;
}
}
}
$list[$name] = $title;
}
}
return $list;
}
/**
* @param string $particle
* @param bool $includeInherited
* @return array
*/
public function getOutlinesWithParticle($particle, $includeInherited = true)
{
$list = [];
foreach ($this->items as $name => $title) {
try {
$index = Layout::index($name);
} catch (\Exception $e) {
// Layout cannot be read. We will just skip it instead of throwing an exception.
continue;
}
if (isset($index['particles'][$particle])) {
$ids = $index['particles'][$particle];
if (!$includeInherited && !empty($index['inherit'])) {
foreach ($index['inherit'] as $items) {
foreach ((array) $items as $id => $inheritId) {
unset($ids[$id]);
}
}
}
if ($ids) {
$list[$name] = $title;
}
}
}
return $list;
}
/**
* @param string $type
* @param bool $includeInherited
* @return array
*/
public function getOutlinesWithAtom($type, $includeInherited = true)
{
$list = [];
foreach ($this->items as $name => $title) {
$file = CompiledYamlFile::instance("gantry-theme://config/{$name}/page/head.yaml");
$index = (array)$file->content();
$file->free();
if (isset($index['atoms'])) {
foreach ($index['atoms'] as $atom) {
if (!empty($atom['id']) && $atom['type'] === $type && ($includeInherited || empty($atom['inherit']))) {
$list[$name] = $title;
}
}
}
}
return $list;
}
/**
* @param string $particle
* @param bool $includeInherited
* @return array
*/
public function getAllParticleInstances($particle, $includeInherited = true)
{
$list = [];
foreach ($this->items as $name => $title) {
$list += $this->getParticleInstances($name, $particle, $includeInherited);
}
return $list;
}
/**
* @param string $outline
* @param string $particle
* @param bool $includeInherited
* @return array
*/
public function getParticleInstances($outline, $particle, $includeInherited = true)
{
$list = [];
$index = Layout::index($outline);
if (isset($index['particles'][$particle])) {
$list = $index['particles'][$particle];
if (!$includeInherited && !empty($index['inherit'])) {
foreach ($index['inherit'] as $items) {
foreach ((array) $items as $id => $inheritId) {
unset($list[$id]);
}
}
}
}
$layout = Layout::instance($outline);
foreach ($list as $id => $title) {
$item = clone $layout->find($id);
$block = $layout->block($id);
$item->block = isset($block->attributes) ? $block->attributes : new \stdClass();
$list[$id] = $item;
}
return $list;
}
/**
* @param string $outline
* @param string $type
* @param bool $includeInherited
* @return array
*/
public function getAtomInstances($outline, $type, $includeInherited = true)
{
$list = [];
$file = CompiledYamlFile::instance("gantry-theme://config/{$outline}/page/head.yaml");
$head = (array)$file->content();
$file->free();
if (isset($head['atoms'])) {
foreach ($head['atoms'] as $atom) {
if (!empty($atom['id']) && $atom['type'] === $type && ($includeInherited || empty($atom['inherit']['outline']))) {
$list[$atom['id']] = (object) $atom;
}
}
}
return $list;
}
/**
* Return list of outlines which are inheriting the specified atom.
*
* @param string $outline
* @param string $id
* @return array
*/
public function getInheritingOutlinesWithAtom($outline, $id = null)
{
$list = [];
foreach ($this->items as $name => $title) {
$file = CompiledYamlFile::instance("gantry-theme://config/{$name}/page/head.yaml");
$head = (array)$file->content();
$file->free();
if (isset($head['atoms'])) {
foreach ($head['atoms'] as $atom) {
if (!empty($atom['inherit']['outline']) && $atom['inherit']['outline'] === $outline && (!$id || $atom['inherit']['atom'] === $id)) {
$list[$name] = $title;
}
}
}
}
return $list;
}
/**
* Return list of outlines which are inheriting the specified outline.
*
* You can additionally pass section or particle id to filter the results for only that type.
*
* @param string $outline
* @param string|array $id
* @return array
*/
public function getInheritingOutlines($outline, $id = null)
{
$list = [];
foreach ($this->items as $name => $title) {
try {
$index = Layout::index($name);
} catch (\Exception $e) {
// Layout cannot be read. We will just skip it instead of throwing an exception.
continue;
}
if (!empty($index['inherit'][$outline]) && (!$id || array_intersect((array) $id, $index['inherit'][$outline]))) {
$list[$name] = $title;
}
}
return $list;
}
/**
* Return list of outlines inherited by the specified outline.
*
* You can additionally pass section or particle id to filter the results for only that type.
*
* @param string $outline
* @param string $id
* @return array
*/
public function getInheritedOutlines($outline, $id = null)
{
try {
$index = Layout::index($outline);
} catch (\Exception $e) {
// Layout cannot be read. We will just return nothing instead of throwing an exception.
return [];
}
$list = [];
foreach ($index['inherit'] as $name => $inherited) {
if (!$id || array_intersect_key((array) $id, $inherited[$id])) {
$list[$name] = isset($this->items[$name]) ? $this->items[$name] : $name;
}
}
return $list;
}
/**
* @param int|string $id
* @return int|string
*/
public function preset($id)
{
return $id;
}
/**
* @param int|string $id
* @return Layout
*/
public function layout($id)
{
return Layout::load($id);
}
/**
* @param int|string $id
* @return array
*/
public function layoutPreset($id)
{
$layout = Layout::load($id);
$preset = $layout->preset;
unset($layout);
return $preset;
}
/**
* @param string $path
* @return $this
* @throws \RuntimeException
*/
public function load($path = 'gantry-config://')
{
$this->path = $path;
$iterator = $this->getFilesystemIterator($path);
$files = [];
/** @var FilesystemIterator $info */
foreach ($iterator as $name => $info) {
if (!$info->isDir() || $name[0] === '.' || !is_file($info->getPathname() . '/index.yaml')) {
continue;
}
$files[$name] = ucwords(trim(preg_replace(['|_|', '|/|'], [' ', ' / '], $name)));
}
unset($files['default'], $files['menu']);
asort($files);
$this->items = $this->addDefaults($files);
return $this;
}
/**
* @param string|null $id
* @param string $title
* @param string|array $preset
* @return string
* @throws \RuntimeException
*/
public function create($id, $title = null, $preset = null)
{
$title = $title ?: 'Untitled';
$name = ltrim(strtolower(preg_replace('|[^a-z\d_-]|ui', '_', $id ?: $title)), '_');
if (!$name) {
throw new \RuntimeException('Outline needs a name', 400);
}
if ($name === 'default') {
throw new \RuntimeException("Outline cannot use reserved name '{$name}'", 400);
}
$name = $this->findFreeName($name);
if (!$id) {
$title = ucwords(trim(preg_replace(['|_|', '|/|'], [' ', ' / '], $name)));
}
if (!is_array($preset)) {
// Load preset.
$preset = Layout::preset($preset ?: 'default');
}
// Create layout and index for the new layout.
$layout = new Layout($name, $preset);
$layout->save()->saveIndex();
$this->items[$name] = $title;
return $name;
}
/**
* @param string $id
* @param string $title
* @param bool $inherit
* @return string
* @throws \RuntimeException
*/
public function duplicate($id, $title = null, $inherit = false)
{
if (!$this->canDuplicate($id)) {
throw new \RuntimeException("Outline '{$id}' cannot be duplicated", 400);
}
$layout = Layout::load($id);
if ($inherit) {
$layout->inheritAll()->clean();
}
$new = $this->create(null, $title, $layout->toArray() + ['preset' => $layout->preset]);
if ($id === 'default') {
// For Base Outline we're done.
return $new;
}
/** @var UniformResourceLocator $locator */
$locator = $this->container['locator'];
$path = $locator->findResource("{$this->path}/{$id}");
if (!$path) {
// Nothing to copy.
return $new;
}
$newPath = $locator->findResource("{$this->path}/{$new}", true, true);
try {
// Copy everything over except index, layout and assignments.
Folder::copy($path, $newPath, '/^(index|layout|assignments)\..*$/');
} catch (\Exception $e) {
throw new \RuntimeException(sprintf('Duplicating Outline failed: %s', $e->getMessage()), 500, $e);
}
return $new;
}
/**
* @param string $id
* @param string $title
* @return string
* @throws \RuntimeException
*/
public function rename($id, $title)
{
if (!$this->canDelete($id)) {
throw new \RuntimeException("Outline '{$id}' cannot be renamed", 400);
}
$gantry = $this->container;
/** @var UniformResourceLocator $locator */
$locator = $gantry['locator'];
$path = $locator->findResource("{$this->path}/{$id}", true, true);
if (!$path || !is_dir($path)) {
throw new \RuntimeException('Outline not found', 404);
}
$folder = strtolower(preg_replace('|[^a-z\d_-]|ui', '_', $title));
if ($folder === 'default' || $folder[0] === '_') {
throw new \RuntimeException("Outline cannot use reserved name '{$folder}'", 400);
}
$newPath = $locator->findResource("{$this->path}/{$folder}", true, true);
if (is_dir($newPath)) {
throw new \RuntimeException("Outline '$id' already exists.", 400);
}
try {
foreach ($this->getInheritingOutlines($id) as $outline => $otitle) {
$this->layout($outline)->updateInheritance($id, $folder)->save()->saveIndex();
}
foreach ($this->getInheritingOutlinesWithAtom($id) as $outline => $otitle) {
Atoms::instance($outline)->updateInheritance($id, $folder)->save();
}
Folder::move($path, $newPath);
} catch (\Exception $e) {
throw new \RuntimeException(sprintf('Renaming Outline failed: %s', $e->getMessage()), 500, $e);
}
$this->items[$id] = $title;
return $folder;
}
/**
* @param string $id
* @throws \RuntimeException
*/
public function delete($id)
{
if (!$this->canDelete($id)) {
throw new \RuntimeException("Outline '{$id}' cannot be deleted", 400);
}
$gantry = $this->container;
/** @var UniformResourceLocator $locator */
$locator = $gantry['locator'];
$path = $locator->findResource("{$this->path}/{$id}", true, true);
if (!is_dir($path)) {
throw new \RuntimeException('Outline not found', 404);
}
foreach ($this->getInheritingOutlines($id) as $outline => $title) {
$this->layout($outline)->updateInheritance($id)->save()->saveIndex();
}
foreach ($this->getInheritingOutlinesWithAtom($id) as $outline => $title) {
Atoms::instance($outline)->updateInheritance($id)->save();
}
if (file_exists($path)) {
Folder::delete($path);
}
unset($this->items[$id]);
}
/**
* @param string $id
* @return boolean
*/
public function canDuplicate($id)
{
if (!isset($this->items[$id])) {
return false;
}
return true;
}
/**
* @param string $id
* @return boolean
*/
public function canDelete($id)
{
return !(!$id || $id[0] === '_' || $id === 'default');
}
/**
* @param string $id
* @return boolean
*/
public function isDefault($id)
{
return $id === 'default';
}
/**
* @param array $outlines
* @return array
*/
protected function addDefaults(array $outlines)
{
return [
'default' => 'Base Outline',
'_body_only' => 'Body Only',
'_error' => 'Error',
'_offline' => 'Offline'
] + $outlines;
}
/**
* Find unused name with number appended to it when duplicating an outline.
*
* @param string $id
* @return string
*/
protected function findFreeName($id)
{
if (!isset($this->items[$id])) {
return $id;
}
$name = $id;
$count = 0;
if (preg_match('|^(?:_)?(.*?)(?:_(\d+))?$|u', $id, $matches)) {
$matches += ['', '', ''];
list (, $name, $count) = $matches;
}
$count = max(1, $count);
do {
$count++;
} while (isset($this->items["{$name}_{$count}"]));
return "{$name}_{$count}";
}
/**
* @param string $path
* @return FilesystemIterator|UniformResourceIterator
*/
protected function getFilesystemIterator($path)
{
/** @var UniformResourceLocator $locator */
$locator = $this->container['locator'];
$custom = $locator->findResource($path, true, true);
if (is_dir($custom)) {
/** @var FilesystemIterator $iterator */
$iterator = new FilesystemIterator(
$custom,
FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::KEY_AS_FILENAME |
FilesystemIterator::UNIX_PATHS | FilesystemIterator::SKIP_DOTS
);
} else {
/** @var UniformResourceIterator $iterator */
$iterator = $locator->getIterator(
$path,
UniformResourceIterator::CURRENT_AS_SELF | UniformResourceIterator::KEY_AS_FILENAME |
UniformResourceIterator::UNIX_PATHS | UniformResourceIterator::SKIP_DOTS
);
}
return $iterator;
}
}