theme = $theme; } /** * Loads the object from a cache. * This method is used by the CMS in the runtime. If the cache is not found, it is created. * @param \Cms\Classes\Theme $theme Specifies the theme the object belongs to. * @param string $fileName Specifies the file name, with the extension. * @return mixed Returns a CMS object instance or null if the object wasn't found. */ public static function loadCached($theme, $fileName) { if (!FileHelper::validatePath($fileName, static::getMaxAllowedPathNesting())) throw new SystemException(Lang::get('cms::lang.cms_object.invalid_file', ['name'=>$fileName])); if (!strlen(File::extension($fileName))) $fileName .= '.htm'; $filePath = static::getFilePath($theme, $fileName); if (array_key_exists($filePath, ObjectMemoryCache::$cache)) return ObjectMemoryCache::$cache[$filePath]; $key = self::getObjectTypeDirName().crc32($filePath); clearstatcache($filePath); $cached = Cache::get($key, false); if ($cached !== false && ($cached = @unserialize($cached)) !== false) { if ($cached['mtime'] != @File::lastModified($filePath)) $cached = false; } if ($cached && !File::isFile($filePath)) $cached = false; if ($cached !== false) { /* * The cached item exists and successfully unserialized. * Initialize the object from the cached data. */ $obj = new static($theme); $obj->content = $cached['content']; $obj->fileName = $fileName; $obj->mtime = File::lastModified($filePath); $obj->loadedFromCache = true; $obj->initFromCache($cached); return ObjectMemoryCache::$cache[$filePath] = $obj; } /* * The cached item doesn't exists. * Load the object from the file and create the cache. */ if (($obj = static::load($theme, $fileName)) === null) { /* * If the object cannot be loaded from the disk, delete the cache item. */ Cache::forget($key); return null; } $cached = [ 'mtime' => @File::lastModified($filePath), 'content' => $obj->content ]; $obj->loadedFromCache = false; $obj->initCacheItem($cached); Cache::put($key, serialize($cached), Config::get('cms.parsedPageCacheTTL', 1440)); return ObjectMemoryCache::$cache[$filePath] = $obj; } /** * Loads the object from a file. * This method is used in the CMS back-end. It doesn't use any caching. * @param \Cms\Classes\Theme $theme Specifies the theme the object belongs to. * @param string $fileName Specifies the file name, with the extension. * The file name can contain only alphanumeric symbols, dashes and dots. * @return mixed Returns a CMS object instance or null if the object wasn't found. */ public static function load($theme, $fileName) { if (!FileHelper::validatePath($fileName, static::getMaxAllowedPathNesting())) throw new SystemException(Lang::get('cms::lang.cms_object.invalid_file', ['name'=>$fileName])); $fullPath = static::getFilePath($theme, $fileName); if (!File::isFile($fullPath)) return null; if (($content = @File::get($fullPath)) === false) return null; $obj = new static($theme); $obj->fileName = $fileName; $obj->originalFileName = $fileName; $obj->mtime = File::lastModified($fullPath); $obj->content = $content; return $obj; } /** * Returns the maximum allowed path nesting level. * The default value is 2, meaning that files * can only exist in the root directory, or in a subdirectory. * @return mixed Returns the maximum nesting level or null if any level is allowed. */ protected static function getMaxAllowedPathNesting() { return 2; } /** * Returns the file content. * @return string */ public function getContent() { return $this->content; } /** * Returns the file name. * @return string */ public function getFileName() { return $this->fileName; } /** * Returns the file name without the extension. * @return string */ public function getBaseFileName() { $pos = strrpos($this->fileName, '.'); if ($pos === false) return $this->fileName; return substr($this->fileName, 0, $pos); } /** * Helper for {{ page.id }} or {{ layout.id }} twig vars * Returns a unqiue string for this object. * @return string */ public function getId() { return str_replace('/', '-', $this->getBaseFileName()); } /** * Sets the object file name. * @param string $fileName Specifies the file name. * @return \Cms\Classes\CmsObject Returns the object instance. */ public function setFileName($fileName) { $fileName = trim($fileName); if (!strlen($fileName)) { throw new ValidationException(['fileName' => Lang::get('cms::lang.cms_object.file_name_required', [ 'allowed' => implode(', ', static::$allowedExtensions), 'invalid' => pathinfo($fileName, PATHINFO_EXTENSION) ]) ]); } if (!FileHelper::validateExtension($fileName, static::$allowedExtensions)) { throw new ValidationException(['fileName' => Lang::get('cms::lang.cms_object.invalid_file_extension', [ 'allowed' => implode(', ', static::$allowedExtensions), 'invalid' => pathinfo($fileName, PATHINFO_EXTENSION) ]) ]); } if (!FileHelper::validatePath($fileName, static::getMaxAllowedPathNesting())) { throw new ValidationException([ 'fileName' => Lang::get('cms::lang.cms_object.invalid_file', ['name'=>$fileName]) ]); } if (!strlen(File::extension($fileName))) $fileName .= '.htm'; $this->fileName = $fileName; return $this; } /** * Returns the full path to the template file corresponding to this object. * @return string */ public function getFullPath() { return static::getFilePath($this->theme, $this->fileName); } /** * Returns true if the object was loaded from the cache. * This method is used by the CMS internally. * @return boolean */ public function isLoadedFromCache() { return $this->loadedFromCache; } /** * Returns the Twig content string. */ public function getTwigContent() { return $this->content; } /** * Sets the object attributes. * @param array $attributes A list of attributes to set. */ public function fill(array $attributes) { foreach ($attributes as $key=>$value) { if (!in_array($key, static::$fillable)) throw new ApplicationException(Lang::get('cms::lang.cms_object.invalid_property', ['name'=>$key])); $methodName = 'set'.ucfirst($key); if (method_exists($this, $methodName)) $this->$methodName($value); else $this->$key = $value; } } /** * Saves the object to the disk. */ public function save() { $fullPath = static::getFilePath($this->theme, $this->fileName); if (File::isFile($fullPath) && $this->originalFileName !== $this->fileName) throw new ApplicationException(Lang::get('cms::lang.cms_object.file_already_exists', ['name'=>$this->fileName])); $dirPath = rtrim(static::getFilePath($this->theme, ''), '/'); if (!file_exists($dirPath) || !is_dir($dirPath)) { if (!File::makeDirectory($dirPath, 0777, true, true)) throw new ApplicationException(Lang::get('cms::lang.cms_object.error_creating_directory', ['name'=>$dirPath])); } if (($pos = strpos($this->fileName, '/')) !== false) { $dirPath = static::getFilePath($this->theme, dirname($this->fileName)); if (!is_dir($dirPath) && !File::makeDirectory($dirPath, 0777, true, true)) throw new ApplicationException(Lang::get('cms::lang.cms_object.error_creating_directory', ['name'=>$dirPath])); } if (@file_put_contents($fullPath, $this->content) === false) throw new ApplicationException(Lang::get('cms::lang.cms_object.error_saving', ['name'=>$this->fileName])); if (strlen($this->originalFileName) && $this->originalFileName !== $this->fileName) { $fullPath = static::getFilePath($this->theme, $this->originalFileName); if (File::isFile($fullPath)) @unlink($fullPath); } clearstatcache(); $this->mtime = @File::lastModified($fullPath); $this->originalFileName = $this->fileName; } /** * Deletes the object from the disk. */ public function delete() { $fullPath = static::getFilePath($this->theme, $this->fileName); if (File::isFile($fullPath) && !is_dir($fullPath) && !@unlink($fullPath)) { throw new SystemException(Lang::get('cms::lang.cms_object.error_deleting', ['name'=>$this->fileName])); } } /** * Clears the internal request-level object cache. */ public static function clearInternalCache() { ObjectMemoryCache::$cache = []; } /** * Returns the list of objects in the specified theme. * This method is used internally by the system. * @param \Cms\Classes\Theme $theme Specifies a parent theme. * @param boolean $skipCache Indicates if objects should be reloaded from the disk bypassing the cache. * @return array Returns an array of CMS objects. */ public static function listInTheme($theme, $skipCache = false) { $dirPath = $theme->getPath().'/'.static::getObjectTypeDirName(); $result = []; if (!File::isDirectory($dirPath)) return $result; $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dirPath)); $it->setMaxDepth(1); // Support only a single level of subdirectories while($it->valid()) { if ($it->isFile() && in_array($it->getExtension(), static::$allowedExtensions)) { $filePath = $it->getBasename(); if ($it->getDepth() > 0) $filePath = basename($it->getPath()).'/'.$filePath; $page = $skipCache ? static::load($theme, $filePath) : static::loadCached($theme, $filePath); $result[] = $page; } $it->next(); } return $result; } /** * Returns the absolute file path. * @param \Cms\Classes\Theme $theme Specifies a theme the file belongs to. * @param string$fileName Specifies the file name to return the path to. * @return string */ protected static function getFilePath($theme, $fileName) { return $theme->getPath().'/'.static::getObjectTypeDirName().'/'.$fileName; } /** * Implements the getter functionality. */ public function __get($name) { $methodName = 'get'.ucfirst($name); if (method_exists($this, $methodName)) return $this->$methodName(); return null; } /** * Determine if an attribute exists on the object. * * @param string $key * @return void */ public function __isset($key) { $methodName = 'get'.ucfirst($key); if (method_exists($this, $methodName)) return true; return false; } /** * Initializes the object properties from the cached data. * @param array $cached The cached data array. */ protected function initFromCache($cached) {} /** * Initializes a cache item. * @param array &$item The cached item array. */ protected function initCacheItem(&$item) {} /** * Returns the directory name corresponding to the object type. * For pages the directory name is "pages", for layouts - "layouts", etc. * @return string */ public static function getObjectTypeDirName() {} }