winter/modules/cms/classes/ComponentPartial.php
Samuel Georges 7902cfa58a Simplify security check
Logic in ComponentPartial was rolled back and moved to the Controller. Since there are issues with throwing exceptions inside the component partial lookup logic (exceptions are conditionally suppressed), it seems like it would be better to bubble up the security logic to the controller level as a simple base dir security check, which is no longer concerned about any suppression logic. This looks to have logic parity with the previous solution

Refs #4652
2019-12-14 12:37:44 +11:00

263 lines
6.9 KiB
PHP

<?php namespace Cms\Classes;
use File;
use Lang;
use Cms\Contracts\CmsObject as CmsObjectContract;
use Cms\Helpers\File as FileHelper;
use October\Rain\Extension\Extendable;
use ApplicationException;
/**
* The CMS component partial class. These objects are read-only.
*
* @package october\cms
* @author Alexey Bobkov, Samuel Georges
*/
class ComponentPartial extends Extendable implements CmsObjectContract
{
/**
* @var \Cms\Classes\ComponentBase A reference to the CMS component containing the object.
*/
protected $component;
/**
* @var string The component partial file name.
*/
public $fileName;
/**
* @var string Last modified time.
*/
public $mtime;
/**
* @var string Partial content.
*/
public $content;
/**
* @var int 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. Set to null if any level is allowed.
*/
protected $maxNesting = 2;
/**
* @var array Allowable file extensions.
*/
protected $allowedExtensions = ['htm'];
/**
* @var string Default file extension.
*/
protected $defaultExtension = 'htm';
/**
* Creates an instance of the object and associates it with a CMS component.
* @param \Cms\Classes\ComponentBase $component Specifies the component the object belongs to.
*/
public function __construct(ComponentBase $component)
{
$this->component = $component;
parent::__construct();
}
/**
* Loads the object from a file.
* This method is used in the CMS back-end. It doesn't use any caching.
* @param \Cms\Classes\ComponentBase $component Specifies the component 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($component, $fileName)
{
return (new static($component))->find($fileName);
}
/**
* There is not much point caching a component partial, so this behavior
* reverts to a regular load call.
* @param \Cms\Classes\ComponentBase $component
* @param string $fileName
* @return mixed
*/
public static function loadCached($component, $fileName)
{
return static::load($component, $fileName);
}
/**
* Checks if a partial override exists in the supplied theme and returns it.
* Since the beginning of time, October inconsistently checked for overrides
* using the component alias exactly, resulting in a folder with uppercase
* characters, subsequently this method checks for both variants.
*
* @param \Cms\Classes\Theme $theme
* @param \Cms\Classes\ComponentBase $component
* @param string $fileName
* @return mixed
*/
public static function loadOverrideCached($theme, $component, $fileName)
{
$partial = Partial::loadCached($theme, strtolower($component->alias) . '/' . $fileName);
if ($partial === null) {
$partial = Partial::loadCached($theme, $component->alias . '/' . $fileName);
}
return $partial;
}
/**
* Find a single template by its file name.
*
* @param string $fileName
* @return mixed|static
*/
public function find($fileName)
{
$fileName = $this->validateFileName($fileName);
$filePath = $this->getFilePath($fileName);
if (!File::isFile($filePath)) {
return null;
}
if (($content = @File::get($filePath)) === false) {
return null;
}
$this->fileName = $fileName;
$this->mtime = File::lastModified($filePath);
$this->content = $content;
return $this;
}
/**
* Returns true if the specific component contains a matching partial.
* @param \Cms\Classes\ComponentBase $component Specifies a component the file belongs to.
* @param string $fileName Specifies the file name to check.
* @return bool
*/
public static function check(ComponentBase $component, $fileName)
{
$partial = new static($component);
$filePath = $partial->getFilePath($fileName);
if (!strlen(File::extension($filePath))) {
$filePath .= '.'.$partial->getDefaultExtension();
}
return File::isFile($filePath);
}
/**
* Checks the supplied file name for validity.
* @param string $fileName
* @return string
*/
protected function validateFileName($fileName)
{
if (!FileHelper::validatePath($fileName, $this->maxNesting)) {
throw new ApplicationException(Lang::get('cms::lang.cms_object.invalid_file', [
'name' => $fileName
]));
}
if (!strlen(File::extension($fileName))) {
$fileName .= '.'.$this->defaultExtension;
}
return $fileName;
}
/**
* Returns the file content.
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* Returns the Twig content string.
* @return string
*/
public function getTwigContent()
{
return $this->content;
}
/**
* Returns the key used by the Twig cache.
* @return string
*/
public function getTwigCacheKey()
{
return $this->getFilePath();
}
/**
* Returns the file name.
* @return string
*/
public function getFileName()
{
return $this->fileName;
}
/**
* Returns the default extension used by this template.
* @return string
*/
public function getDefaultExtension()
{
return $this->defaultExtension;
}
/**
* 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);
}
/**
* Returns the absolute file path.
* @param string $fileName Specifies the file name to return the path to.
* @return string
*/
public function getFilePath($fileName = null)
{
if ($fileName === null) {
$fileName = $this->fileName;
}
$component = $this->component;
$path = $component->getPath().'/'.$fileName;
/*
* Check the shared "/partials" directory for the partial
*/
if (!File::isFile($path)) {
$sharedDir = dirname($component->getPath()).'/partials';
$sharedPath = $sharedDir.'/'.$fileName;
if (File::isFile($sharedPath)) {
return $sharedPath;
}
}
return $path;
}
}