winter/modules/cms/classes/CmsObject.php
2015-01-28 11:49:54 +11:00

602 lines
17 KiB
PHP

<?php namespace Cms\Classes;
use File;
use Lang;
use Cache;
use Config;
use Validator;
use ApplicationException;
use ValidationException;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ArrayAccess;
use Exception;
/**
* This is a base class for all CMS objects - content files, pages, partials and layouts.
* The class implements basic operations with file-based templates.
*
* @package october\cms
* @author Alexey Bobkov, Samuel Georges
*/
class CmsObject implements ArrayAccess
{
/**
* @var string Specifies the file name corresponding the CMS object.
*/
protected $fileName;
/**
* @var string Specifies the file name, the CMS object was loaded from.
*/
protected $originalFileName = null;
/**
* @var string The entire file content.
*/
protected $content;
/**
* @var \Cms\Classes\Theme A reference to the CMS theme containing the object.
*/
protected $theme;
/**
* @var boolean Indicated whether the object was loaded from the cache.
*/
protected $loadedFromCache = false;
protected static $fillable = [
'content',
'fileName'
];
protected static $allowedExtensions = ['htm'];
protected static $defaultExtension = 'htm';
/**
* @var integer The template file modification time.
*/
public $mtime;
/**
* Creates an instance of the object and associates it with a CMS theme.
* @param \Cms\Classes\Theme $theme Specifies the theme the object belongs to.
* If the theme is specified as NULL, then a query can be performed on the object directly.
*/
public function __construct(Theme $theme = null)
{
$this->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 ApplicationException(Lang::get('cms::lang.cms_object.invalid_file', ['name'=>$fileName]));
}
if (!strlen(File::extension($fileName))) {
$fileName .= '.'.static::$defaultExtension;
}
$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 ApplicationException(Lang::get('cms::lang.cms_object.invalid_file', ['name'=>$fileName]));
}
if (!strlen(File::extension($fileName))) {
$fileName .= '.'.static::$defaultExtension;
}
$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 unique 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]
));
}
}
$newFullPath = $fullPath;
if (@File::put($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($newFullPath);
$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 ApplicationException(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)
{
if (!$theme) {
throw new ApplicationException(Lang::get('cms::lang.theme.active.not_set'));
}
$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
$it->rewind();
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.
* @param string $name
* @return void
*/
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;
}
/**
* Determine if the given attribute exists.
* @param mixed $offset
* @return bool
*/
public function offsetExists($offset)
{
return isset($this->$offset);
}
/**
* Get the value for a given offset.
* @param mixed $offset
* @return mixed
*/
public function offsetGet($offset)
{
return $this->$offset;
}
/**
* Set the value for a given offset.
* @param mixed $offset
* @param mixed $value
* @return void
*/
public function offsetSet($offset, $value)
{
$this->$offset = $value;
}
/**
* Unset the value for a given offset.
* @param mixed $offset
* @return void
*/
public function offsetUnset($offset)
{
unset($this->$offset);
}
//
// Queries
//
/**
* Get a new query builder for the object
* @return CmsObjectQuery
*/
public function newQuery()
{
$query = new CmsObjectQuery($this, $this->theme);
return $query;
}
/**
* Handle dynamic method calls into the method.
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
// If this object is populated with a theme, then a query
// cannot be performed on it to reduce overhead on populated objects.
if (!$this->theme) {
$query = $this->newQuery();
return call_user_func_array(array($query, $method), $parameters);
}
$className = get_class($this);
throw new \BadMethodCallException("Call to undefined method {$className}::{$method}()");
}
/**
* Handle dynamic static method calls into the method.
* @param string $method
* @param array $parameters
* @return mixed
*/
public static function __callStatic($method, $parameters)
{
$instance = new static;
return call_user_func_array([$instance, $method], $parameters);
}
//
// Overrides
//
/**
* 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()
{
}
}