diff --git a/application/controllers/EditController.php b/application/controllers/EditController.php index 92299b8..9360f3c 100644 --- a/application/controllers/EditController.php +++ b/application/controllers/EditController.php @@ -3,9 +3,11 @@ namespace Icinga\Module\Toplevelview\Controllers; +use Icinga\Module\Toplevelview\Model\View; use Icinga\Module\Toplevelview\Forms\EditForm; use Icinga\Module\Toplevelview\ViewConfig; use Icinga\Module\Toplevelview\Web\Controller; + use Icinga\Web\Url; class EditController extends Controller @@ -39,26 +41,30 @@ public function indexAction() { $action = $this->getRequest()->getActionName(); + $c = new ViewConfig(); + $view = null; + if ($action === 'add') { $this->view->title = sprintf('%s Top Level View', $this->translate('Add')); - $view = new ViewConfig(); - $view->setConfigDir(); + $view = new View('', $c::FORMAT_YAML); } elseif ($action === 'clone') { $name = $this->params->getRequired('name'); $this->view->title = sprintf('%s Top Level View', $this->translate('Clone')); - $view = clone ViewConfig::loadByName($name); + // Clone the view and give it to the $config + $view = clone $c->loadByName($name); } else { $this->view->name = $name = $this->params->getRequired('name'); $this->view->title = sprintf('%s Top Level View: %s', $this->translate('Edit'), $this->params->getRequired('name')); - $view = ViewConfig::loadByName($name); + $view = $c->loadByName($name); } + $view->setFormat($c::FORMAT_YAML); + $this->view->form = $form = new EditForm(); + $form->setViewConfig($c); + $form->setViews($view); - $view->setFormat(ViewConfig::FORMAT_YAML); - $form->setViewConfig($view); $form->handleRequest(); - $this->setViewScript('edit/index'); } diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php index eccae95..9525e73 100644 --- a/application/controllers/IndexController.php +++ b/application/controllers/IndexController.php @@ -19,7 +19,10 @@ public function indexAction() ])->activate('index'); // Load add views from the existing YAML files - $this->view->views = ViewConfig::loadAll(); + $c = new ViewConfig(); + $views = $c->loadAll(); + + $this->view->views = $views; $this->setAutorefreshInterval(30); } diff --git a/application/controllers/ShowController.php b/application/controllers/ShowController.php index 2f49166..e416abb 100644 --- a/application/controllers/ShowController.php +++ b/application/controllers/ShowController.php @@ -5,6 +5,7 @@ use Icinga\Module\Toplevelview\ViewConfig; use Icinga\Module\Toplevelview\Web\Controller; + use Icinga\Web\Url; class ShowController extends Controller @@ -43,7 +44,11 @@ public function init() public function indexAction() { $this->view->name = $name = $this->params->getRequired('name'); - $this->view->view = $view = ViewConfig::loadByName($name); + + $c = new ViewConfig(); + $view = $c->loadByName($name); + $this->view->view = $view; + $tree = $view->getTree(); if (($lifetime = $this->getParam('cache')) !== null) { @@ -56,7 +61,10 @@ public function indexAction() public function treeAction() { $this->view->name = $name = $this->params->getRequired('name'); - $this->view->view = $view = ViewConfig::loadByName($name); + + $c = new ViewConfig(); + $view = $c->loadByName($name); + $this->view->view = $view; $tree = $view->getTree(); @@ -72,7 +80,10 @@ public function treeAction() public function sourceAction() { $this->view->name = $name = $this->params->getRequired('name'); - $this->view->view = $view = ViewConfig::loadByName($name); + + $c = new ViewConfig(); + $view = $c->loadByName($name); + $this->view->view = $view; $this->view->text = $view->getText(); $this->setViewScript('index', 'text'); diff --git a/application/forms/EditForm.php b/application/forms/EditForm.php index 96f2c74..6596308 100644 --- a/application/forms/EditForm.php +++ b/application/forms/EditForm.php @@ -3,8 +3,10 @@ namespace Icinga\Module\Toplevelview\Forms; -use Exception; use Icinga\Module\Toplevelview\ViewConfig; +use Icinga\Module\Toplevelview\Model\View; + +use Exception; use Icinga\Web\Form; use Icinga\Web\Notification; use Icinga\Web\Url; @@ -14,7 +16,12 @@ class EditForm extends Form /** * @var ViewConfig */ - protected $viewConfig; + protected $viewconfig; + + /** + * @var View + */ + protected $view; /** * {@inheritdoc} @@ -24,9 +31,15 @@ public function init() $this->setName('form_toplevelview_edit'); } - public function setViewConfig(ViewConfig $viewConfig) + public function setViews(View $view) + { + $this->view = $view; + return $this; + } + + public function setViewConfig(ViewConfig $config) { - $this->viewConfig = $viewConfig; + $this->viewconfig = $config; return $this; } @@ -36,26 +49,27 @@ public function setViewConfig(ViewConfig $viewConfig) public function onSuccess() { try { - $this->viewConfig->setName($this->getValue('name')); - $this->viewConfig->setText($this->getValue('config')); + $this->view->setName($this->getValue('name')); + $this->view->setText($this->getValue('config')); // ensure config can be parsed... - $this->viewConfig->getMetaData(); - $this->viewConfig->getTree(); + $this->view->getMetaData(); + $this->view->getTree(); - $this->viewConfig->storeToSession(); + // Store the view to the session + $this->viewconfig->storeToSession($this->view); $cancel = $this->getElement('btn_submit_cancel'); $delete = $this->getElement('btn_submit_delete'); if ($this->getElement('btn_submit_save_file')->getValue() !== null) { - $this->viewConfig->store(); + $this->viewconfig->storeToFile($this->view); Notification::success($this->translate('Top Level View successfully saved')); } elseif ($cancel !== null && $cancel->getValue() !== null) { - $this->viewConfig->clearSession(); + $this->viewconfig->clearSession($this->view); Notification::success($this->translate('Top Level View restored from disk')); } elseif ($delete != null && $delete->getValue() !== null) { - $this->viewConfig->delete(); + $this->viewconfig->delete($this->view); $this->setRedirectUrl('toplevelview'); Notification::success($this->translate('Top Level View successfully deleted')); } else { @@ -70,8 +84,8 @@ public function onSuccess() public function getRedirectUrl() { - if ($this->redirectUrl === null && ($name = $this->viewConfig->getName()) !== null) { - $this->redirectUrl = Url::fromPath('toplevelview/show', array('name' => $name)); + if ($this->redirectUrl === null && ($name = $this->view->getName()) !== null) { + $this->redirectUrl = Url::fromPath('toplevelview/show', ['name' => $name]); } return parent::getRedirectUrl(); } @@ -84,8 +98,8 @@ public function getRedirectUrl() public function onRequest() { $values = array(); - $values['name'] = $this->viewConfig->getName(); - $values['config'] = $this->viewConfig->getText(); + $values['name'] = $this->view->getName(); + $values['config'] = $this->view->getText(); $this->populate($values); } @@ -95,7 +109,7 @@ public function onRequest() */ public function createElements(array $formData) { - if ($this->viewConfig->hasBeenLoadedFromSession()) { + if ($this->view->hasBeenLoadedFromSession()) { $this->warning( $this->translate( 'This config is only stored in your session!' @@ -117,7 +131,6 @@ public function createElements(array $formData) 'textarea', 'config', array( - //'required' => true, 'label' => $this->translate('YAML Config'), 'class' => 'code-editor codemirror', 'decorators' => array( @@ -149,7 +162,7 @@ public function createElements(array $formData) ) ); - if ($this->viewConfig->hasBeenLoadedFromSession()) { + if ($this->view->hasBeenLoadedFromSession()) { $this->addElement( 'submit', 'btn_submit_cancel', @@ -162,7 +175,7 @@ public function createElements(array $formData) ); } - if ($this->viewConfig->hasBeenLoaded()) { + if ($this->view->hasBeenLoaded()) { $this->addElement( 'submit', 'btn_submit_delete', diff --git a/application/views/helpers/Tiles.php b/application/views/helpers/Tiles.php index 594da28..254edf6 100644 --- a/application/views/helpers/Tiles.php +++ b/application/views/helpers/Tiles.php @@ -35,7 +35,7 @@ public function tiles(TLVTreeNode $node, $levels = 2, $classes = array()) $title . $badges, 'toplevelview/show/tree', array( - 'name' => $node->getRoot()->getConfig()->getName(), + 'name' => $node->getRoot()->getView()->getName(), 'id' => $node->getFullId() ), array( diff --git a/application/views/helpers/Tree.php b/application/views/helpers/Tree.php index bd7c9ea..dd43de9 100644 --- a/application/views/helpers/Tree.php +++ b/application/views/helpers/Tree.php @@ -71,7 +71,7 @@ public function tree(TLVTreeNode $node, $classes = array(), $level = 0) $url = Url::fromPath( 'toplevelview/show/tree', array( - 'name' => $node->getRoot()->getConfig()->getName(), + 'name' => $node->getRoot()->getView()->getName(), 'id' => $node->getFullId() ) ); diff --git a/configuration.php b/configuration.php index 4b41a29..741a933 100644 --- a/configuration.php +++ b/configuration.php @@ -8,6 +8,17 @@ $this->providePermission('toplevelview/edit', $this->translate('Allow the user to edit Top Level Views')); +// TODO Implement these restrictions in the ViewConfig +// $this->provideRestriction( +// 'toplevelview/filter/edit', +// $this->translate('Restrict edit rights to Views that match the filter (comma-separated values)') +// ); + +// $this->provideRestriction( +// 'toplevelview/filter/views', +// $this->translate('Restrict access to Views that match the filter (comma-separated values)') +// ); + /** @var \Icinga\Web\Navigation\NavigationItem $section */ $section = $this->menuSection('toplevelview'); $section @@ -19,7 +30,8 @@ try { if (extension_loaded('yaml')) { /** @var \Icinga\Application\Modules\MenuItemContainer $section */ - $views = ViewConfig::loadAll(); + $c = new ViewConfig(); + $views = $c->loadAll(); foreach ($views as $name => $viewConfig) { $section->add($name, array( diff --git a/library/Toplevelview/Model/View.php b/library/Toplevelview/Model/View.php new file mode 100644 index 0000000..3a2766c --- /dev/null +++ b/library/Toplevelview/Model/View.php @@ -0,0 +1,233 @@ +name = $name; + $this->format = $format; + } + + public function __clone() + { + $this->name = null; + $this->raw = null; + $this->tree = null; + + $this->hasBeenLoaded = false; + $this->hasBeenLoadedFromSession = false; + } + + /** + * getTree loads the Tree for this configuration + * + * @return TLVTree + */ + public function getTree(): TLVTree + { + if ($this->tree === null) { + $this->ensureParsed(); + $this->tree = $tree = TLVTree::fromArray($this->raw); + $tree->setView($this); + } + return $this->tree; + } + + protected function ensureParsed() + { + if ($this->raw === null) { + Benchmark::measure('Begin parsing YAML document'); + + $text = $this->getText(); + if ($text === null) { + // new View + $this->raw = array(); + } elseif ($this->format == self::FORMAT_YAML) { + // TODO: use stdClass instead of Array? + $this->raw = yaml_parse($text); + if (! is_array($this->raw)) { + throw new InvalidPropertyException('Could not parse YAML config!'); + } + } else { + throw new NotImplementedError("Unknown format '%s'", $this->format); + } + + Benchmark::measure('Finished parsing YAML document'); + } + } + + public function getMeta($key) + { + $this->ensureParsed(); + if ($key !== 'children' && array_key_exists($key, $this->raw)) { + return $this->raw[$key]; + } else { + return null; + } + } + + public function setMeta($key, $value) + { + if ($key === 'children') { + throw new ProgrammingError('You can not edit children here!'); + } + $this->raw[$key] = $value; + return $this; + } + + public function getMetaData() + { + $this->ensureParsed(); + $data = array(); + foreach ($this->raw as $key => $value) { + if ($key !== 'children') { + $data[$key] = $value; + } + } + return $data; + } + + /** + * @return bool + */ + public function hasBeenLoadedFromSession() + { + return $this->hasBeenLoadedFromSession; + } + + /** + * @return bool + */ + public function hasBeenLoaded() + { + return $this->hasBeenLoaded; + } + + /** + * getText returns the Views text, which contains the full YAML data + * @return string + */ + public function getText(): ?string + { + return $this->text; + } + + /** + * getTextChecksum returns the textChecksum of this View + * @return string + */ + public function getTextChecksum(): string + { + if ($this->textChecksum === null) { + $this->textChecksum = sha1($this->text); + } + return $this->textChecksum; + } + + /** + * setFormat sets the format for this View + * @param string $format Format for this view (e.g. 'yml') + * @return $this + */ + public function setFormat($format) + { + $this->format = $format; + return $this; + } + + /** + * getFormat returns the View's format + */ + public function getFormat(): string + { + return $this->format; + } + + /** + * setText sets the text (YAML) for this View. + * Hint: This will reset textChecksum, raw, and tree + * @param $text + * @return $this + */ + public function setText($text) + { + $this->text = $text; + $this->textChecksum = null; + $this->raw = null; + $this->tree = null; + return $this; + } + + /** + * getName returns the name of this View + * @return ?string + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * setName sets the name for this View + * @param string $name + * @return $this + */ + public function setName($name) + { + $this->name = $name; + return $this; + } +} diff --git a/library/Toplevelview/Tree/TLVTree.php b/library/Toplevelview/Tree/TLVTree.php index ecfe6e3..f4e1750 100644 --- a/library/Toplevelview/Tree/TLVTree.php +++ b/library/Toplevelview/Tree/TLVTree.php @@ -3,13 +3,14 @@ namespace Icinga\Module\Toplevelview\Tree; +use Icinga\Module\Toplevelview\Util\Json; +use Icinga\Module\Toplevelview\Model\View; + use Icinga\Application\Logger; use Icinga\Exception\IcingaException; use Icinga\Exception\NotFoundError; use Icinga\Exception\ProgrammingError; use Icinga\Module\Icingadb\Common\Database; -use Icinga\Module\Toplevelview\Util\Json; -use Icinga\Module\Toplevelview\ViewConfig; use Icinga\Web\FileCache; use stdClass; @@ -38,9 +39,9 @@ class TLVTree extends TLVTreeNode protected $cacheLifetime = 60; /** - * @var ViewConfig + * @var View */ - protected $config; + protected $view; public function getById($id) { @@ -65,21 +66,21 @@ public function getById($id) } /** - * @return ViewConfig + * @return View */ - public function getConfig() + public function getView() { - return $this->config; + return $this->view; } /** - * @param ViewConfig $config + * @param View $view * * @return $this */ - public function setConfig(ViewConfig $config) + public function setView(View $view) { - $this->config = $config; + $this->view = $view; return $this; } @@ -100,11 +101,11 @@ public function registerObject($type, $name, $class) protected function getCacheName() { - $config = $this->getConfig(); + $view = $this->getView(); return sprintf( '%s-%s.json', - $config->getName(), - $config->getTextChecksum() + $view->getName(), + $view->getTextChecksum() ); } diff --git a/library/Toplevelview/ViewConfig.php b/library/Toplevelview/ViewConfig.php index 3fdc7eb..dfe79ee 100644 --- a/library/Toplevelview/ViewConfig.php +++ b/library/Toplevelview/ViewConfig.php @@ -3,189 +3,139 @@ namespace Icinga\Module\Toplevelview; -use Icinga\Application\Benchmark; +use Icinga\Module\Toplevelview\Model\View; + use Icinga\Application\Icinga; -use Icinga\Exception\InvalidPropertyException; -use Icinga\Exception\NotImplementedError; -use Icinga\Exception\NotReadableError; use Icinga\Exception\NotWritableError; -use Icinga\Exception\ProgrammingError; -use Icinga\Module\Toplevelview\Tree\TLVTree; +use Icinga\Exception\NotReadableError; use Icinga\Util\DirectoryIterator; use Icinga\Web\Session; +/** + * Manages the View's configurations, loads and stores Views. + */ class ViewConfig { const FORMAT_YAML = 'yml'; const SESSION_PREFIX = 'toplevelview_view_'; - protected $config_dir; - - protected $name; - - protected $format; - - protected $file_path; - - protected $view; - - protected $raw; - - protected $tree; - - protected $hasBeenLoaded = false; - protected $hasBeenLoadedFromSession = false; - /** - * Content of the file - * + * The module's configuration directory * @var string */ - protected $text; + protected $config_dir; + + function __construct() { + // Ensure the Views configuration directory exists + $config_dir_module = Icinga::app() + ->getModuleManager() + ->getModule('toplevelview') + ->getConfigDir(); - protected $textChecksum; + $config_dir = $config_dir_module . DIRECTORY_SEPARATOR . 'views'; + $this->ensureDirExists($config_dir_module); + $this->ensureDirExists($config_dir); + // Set the configuration directory + $this->config_dir = $config_dir; + } /** - * @param $name - * @param string|null $config_dir - * @param string $format + * getConfigDir returns the configuration directory * - * @return static + * @return string + * @throws ProgrammingError When dir is not yet set */ - public static function loadByName($name, $config_dir = null, $format = self::FORMAT_YAML) + public function getConfigDir(): string { - $object = new static; - $object - ->setName($name) - ->setConfigDir($config_dir) - ->setFormat($format) - ->load(); - - return $object; + if ($this->config_dir === null) { + throw new ProgrammingError('Configuration directory does not exit'); + } + return $this->config_dir; } /** - * @param string|null $config_dir - * @param string $format + * ensureDirExists checks if a given path exists and creates the path if it doesn't * - * @return static[] + * @param string $path Path to create the directory at + * @param string $mode Mode to create the directory with + * + * @return View */ - public static function loadAll($config_dir = null, $format = self::FORMAT_YAML) + protected function ensureDirExists($path, $mode = '2770'): void { - $suffix = '.' . $format; - - $config_dir = static::configDir($config_dir); - $directory = new DirectoryIterator($config_dir, $suffix); - - $views = array(); - foreach ($directory as $name => $path) { - if (is_dir($path)) { - // no not descend and ignore directories - continue; - } - $name = basename($name, $suffix); - $views[$name] = static::loadByName($name, $config_dir, $format); + if (file_exists($path)) { + return; } - // try to load from session - $len = strlen(self::SESSION_PREFIX); - foreach (static::session()->getAll() as $k => $v) { - if (substr($k, 0, $len) === self::SESSION_PREFIX) { - $name = substr($k, $len); - if (! array_key_exists($name, $views)) { - $views[$name] = static::loadByName($name, $config_dir, $format); - } - } + if (mkdir($path) !== true) { + throw new NotWritableError( + 'Configuration directory does not exit, and it could not be created: %s', + $path + ); } - ksort($views); - - return $views; - } - - /** - * @return string - */ - public function getFilePath() - { - if ($this->file_path === null) { - if ($this->format === null) { - throw new ProgrammingError('format not set!'); - } - $this->file_path = $this->getConfigDir() . DIRECTORY_SEPARATOR . $this->name . '.' . $this->format; + $octalMode = intval($mode, 8); + if ($mode !== null && false === @chmod($path, $octalMode)) { + throw new NotWritableError('Failed to set file mode "%s" on file "%s"', $mode, $path); } - return $this->file_path; } /** - * @param string $file_path + * loadFromSession loads a View stored in the user's session * - * @return $this + * @param string $name name of the View + * @param string $format format of the View + * @return ?View */ - public function setFilePath($file_path) + protected function loadFromSession($name, $format): ?View { - $this->file_path = $file_path; - return $this; - } - - /** - * @return $this - */ - public function load() - { - if ($this->text === null) { - $this->loadFromSession(); - } - if ($this->text === null) { - $this->loadFromFile(); + // Try to load data from the session + $sessionConfig = Session::getSession()->get(self::SESSION_PREFIX . $name); + // If there is none, we return + if ($sessionConfig === null) { + return null; } - return $this; - } + // If there is data, create the View with the data + $view = (new View($name, $format))->setText($sessionConfig); + $view->hasBeenLoadedFromSession = true; + $view->hasBeenLoaded = true; - public function loadFromFile() - { - $file_path = $this->getFilePath(); - $this->text = file_get_contents($file_path); - if ($this->text === false) { - throw new NotReadableError('Could not read file %s', $file_path); - } - $this->view = null; - $this->hasBeenLoadedFromSession = false; - $this->hasBeenLoaded = true; - return $this; + return $view; } /** - * @return string + * loadFromFile loads a View stored in a configuration file + * + * @param string $name name of the View + * @param string $format format of the View + * @return ?View */ - public function getText() - { - return $this->text; - } - - public function getTextChecksum() + protected function loadFromFile($name, $format): View { - if ($this->textChecksum === null) { - $this->textChecksum = sha1($this->text); + // Try to load the data from the file + $file_path = $this->getConfigDir() . DIRECTORY_SEPARATOR . $name . '.' . $format; + $text = file_get_contents($file_path); + // Throw error if we cannot read it + if ($text === false) { + throw new NotReadableError('Could not read file %s', $file_path); } - return $this->textChecksum; + // If there is data, create the View with the data + $view = (new View($name, $format))->setText($text); + $view->hasBeenLoadedFromSession = false; + $view->hasBeenLoaded = true; + + return $view; } /** - * @param $text + * writeFile writes the given content to a given path. + * Used to store the Views YAML content * - * @return $this + * @param $path Path to the file + * @param $content Content of the file + * @param $mode Mode of the new file */ - public function setText($text) - { - $this->text = $text; - $this->textChecksum = null; - $this->raw = null; - $this->tree = null; - return $this; - } - - protected function writeFile($path, $content, $mode = '0660') + protected function writeFile($path, $content, $mode = '0660'): void { $existing = file_exists($path); if (file_put_contents($path, $content) === false) { @@ -200,288 +150,162 @@ protected function writeFile($path, $content, $mode = '0660') } } - protected function storeBackup($force = false) - { - $backupDir = $this->getConfigBackupDir(); - - $this->ensureConfigDir($backupDir); - - $ts = (string) time(); - $backup = $backupDir . DIRECTORY_SEPARATOR . $ts . '.' . $this->format; - - if (file_exists($backup)) { - throw new ProgrammingError('History file with timestamp already present: %s', $backup); - } - - $existingFile = $this->getFilePath(); - $oldText = file_get_contents($existingFile); - if ($oldText === false) { - throw new NotReadableError('Could not read file %s', $existingFile); - } - - // only save backup if changed or forced - if ($force || $oldText !== $this->text) { - $this->writeFile($backup, $oldText); - } - } - - public function store() + /** + * Load a View by its name + * + * @param $name + * @param string|null $config_dir + * @param string $format + * + * @return View + */ + public function loadByName($name, $format = self::FORMAT_YAML): View { - $config_dir = $this->getConfigDir(); - $file_path = $this->getFilePath(); - - $this->ensureConfigDir($config_dir); + // Try to load from session + $view = $this->loadFromSession($name, $format); - // ensure to save history - if (file_exists($file_path)) { - $this->storeBackup(); + if (isset($view)) { + return $view; } - $this->writeFile($file_path, $this->text); + // Try to load the view from the file + $view = $this->loadFromFile($name, $format); - $this->clearSession(); - return $this; + return $view; } /** - * @return string - * @throws ProgrammingError When dir is not yet set + * @param string|null $config_dir + * @param string $format + * + * @return View[] */ - public function getConfigDir() + public function loadAll($format = self::FORMAT_YAML): array { - if ($this->config_dir === null) { - throw new ProgrammingError('config_dir not yet set!'); - } - return $this->config_dir; - } + $suffix = '.' . $format; - /** - * @return string - */ - public function getConfigBackupDir() - { - return $this->getConfigDir() . DIRECTORY_SEPARATOR . $this->name; - } + $views = array(); - /** - * @param string $config_dir - * - * @return $this - * @throws NotReadableError - */ - public function setConfigDir($config_dir = null) - { - $this->config_dir = static::configDir($config_dir); - $this->file_path = null; - return $this; - } + // Load the YAML files for the Views from the config directory + $directory = new DirectoryIterator($this->config_dir, $suffix); - protected static function ensureConfigDir($path, $mode = '2770') - { - if (! file_exists($path)) { - if (mkdir($path) !== true) { - throw new NotWritableError( - 'Config path did not exit, and it could not be created: %s', - $path - ); + foreach ($directory as $name => $path) { + if (is_dir($path)) { + // Do not descend and ignore directories + continue; } + $name = basename($name, $suffix); + $view = $this->loadByName($name, $format); - $octalMode = intval($mode, 8); - if ($mode !== null && false === @chmod($path, $octalMode)) { - throw new NotWritableError('Failed to set file mode "%s" on file "%s"', $mode, $path); + if (isset($view)) { + $views[$name] = $view; } } - } - public static function configDir($config_dir = null) - { - $config_dir_module = Icinga::app()->getModuleManager()->getModule('toplevelview')->getConfigDir(); - if ($config_dir === null) { - $config_dir = $config_dir_module . DIRECTORY_SEPARATOR . 'views'; - } + // Try to load View from the session + $len = strlen(self::SESSION_PREFIX); - static::ensureConfigDir($config_dir_module); - static::ensureConfigDir($config_dir); + foreach (Session::getSession()->getAll() as $k => $v) { + if (substr($k, 0, $len) === self::SESSION_PREFIX) { + $name = substr($k, $len); + if (! array_key_exists($name, $views)) { + $view = $this->loadByName($name, $format); - return $config_dir; - } + if (isset($view)) { + $views[$name] = $view; + } + } + } + } + // Sort and return the views + ksort($views); - /** - * @return mixed - */ - public function getName() - { - return $this->name; + return $views; } /** - * @param mixed $name + * storeToSession stores a View's text to the user's session * - * @return $this + * @param $view */ - public function setName($name) + public function storeToSession($view): void { - $this->name = $name; - $this->file_path = null; - return $this; + Session::getSession()->set(self::SESSION_PREFIX . $view->getName(), $view->getText()); } /** - * @return string + * clearSession removes a view from the user's session + * + * @param $view */ - public function getFormat() + public function clearSession($view): void { - return $this->format; + Session::getSession()->delete(self::SESSION_PREFIX . $view->getName()); } /** - * @param string $format + * storeToFile stores a View to its configuration file * - * @return $this + * @param $view */ - public function setFormat($format) - { - $this->format = $format; - $this->file_path = null; - return $this; - } - - public function getMeta($key) + public function storeToFile($view): void { - $this->ensureParsed(); - if ($key !== 'children' && array_key_exists($key, $this->raw)) { - return $this->raw[$key]; - } else { - return null; - } - } - - public function setMeta($key, $value) - { - if ($key === 'children') { - throw new ProgrammingError('You can not edit children here!'); - } - $this->raw[$key] = $value; - return $this; - } - - public function getMetaData() - { - $this->ensureParsed(); - $data = array(); - foreach ($this->raw as $key => $value) { - if ($key !== 'children') { - $data[$key] = $value; - } - } - return $data; - } - - protected function ensureParsed() - { - if ($this->raw === null) { - Benchmark::measure('Begin parsing YAML document'); - - $text = $this->getText(); - if ($text === null) { - // new ViewConfig - $this->raw = array(); - } elseif ($this->format == self::FORMAT_YAML) { - // TODO: use stdClass instead of Array? - $this->raw = yaml_parse($text); - if (! is_array($this->raw)) { - throw new InvalidPropertyException('Could not parse YAML config!'); - } - } else { - throw new NotImplementedError("Unknown format '%s'", $this->format); - } - - Benchmark::measure('Finished parsing YAML document'); + $file_path = $this->getConfigDir() . DIRECTORY_SEPARATOR . $view->getName() . '.' . $view->getFormat(); + // Store a backup of the existing config + if (file_exists($file_path)) { + $this->storeBackup($view); } + // Write the content to the file and clear the session + $this->writeFile($file_path, $view->getText()); + $this->clearSession($view); } /** - * Loads the Tree for this configuration + * delete removes a Views configuration file * - * @return TLVTree + * @param $view */ - public function getTree() - { - if ($this->tree === null) { - $this->ensureParsed(); - $this->tree = $tree = TLVTree::fromArray($this->raw); - $tree->setConfig($this); - } - return $this->tree; - } - - protected function getSessionVarName() + public function delete($view): void { - return self::SESSION_PREFIX . $this->name; - } + $file_path = $this->getConfigDir() . DIRECTORY_SEPARATOR . $view->getName() . '.' . $view->getFormat(); - public static function session() - { - return Session::getSession(); - } + $this->clearSession($view); - public function loadFromSession() - { - if (($sessionConfig = $this->session()->get($this->getSessionVarName())) !== null) { - $this->text = $sessionConfig; - $this->hasBeenLoadedFromSession = true; - $this->hasBeenLoaded = true; + if (file_exists($file_path)) { + $this->storeBackup($view, true); + unlink($file_path); } - return $this; - } - - public function clearSession() - { - $this->session()->delete($this->getSessionVarName()); - } - - public function storeToSession() - { - $this->session()->set($this->getSessionVarName(), $this->text); } /** - * @return bool - */ - public function hasBeenLoadedFromSession() - { - return $this->hasBeenLoadedFromSession; - } - - /** - * @return bool + * storeBackup stores a timestamped backup file of a View's file, + * if the content has changed + * + * @param $view + * @param $force Stores a backup even if the content hasn't changed */ - public function hasBeenLoaded() + protected function storeBackup($view, $force = false) { - return $this->hasBeenLoaded; - } + $backup_dir = $this->getConfigDir() . DIRECTORY_SEPARATOR . $view->getName(); + $this->ensureDirExists($backup_dir); - public function __clone() - { - $this->name = null; - $this->raw = null; - $this->tree = null; - - $this->hasBeenLoaded = false; - $this->hasBeenLoadedFromSession = false; - } + $ts = (string) time(); + $backup = $backup_dir . DIRECTORY_SEPARATOR . $ts . '.' . $view->getFormat(); - public function delete() - { - $file_path = $this->getFilePath(); + if (file_exists($backup)) { + throw new ProgrammingError('History file with timestamp already present: %s', $backup); + } - $this->clearSession(); + $existing_file = $this->getConfigDir() . DIRECTORY_SEPARATOR . $view->getName() . '.' . $view->getFormat(); + $oldText = file_get_contents($existing_file); - if (file_exists($file_path)) { - $this->storeBackup(true); - unlink($file_path); + if ($oldText === false) { + throw new NotReadableError('Could not read file %s', $existing_file); } - return $this; + // Only store a backup if the text changed or forced is set to true + if ($force || $oldText !== $view->getText()) { + $this->writeFile($backup, $oldText); + } } }