mirror of
https://github.com/wintercms/winter.git
synced 2024-06-28 05:33:29 +02:00
747 lines
22 KiB
PHP
747 lines
22 KiB
PHP
<?php namespace Cms\Widgets;
|
|
|
|
use Str;
|
|
use Url;
|
|
use File;
|
|
use Lang;
|
|
use Input;
|
|
use Request;
|
|
use Response;
|
|
use Cms\Classes\Theme;
|
|
use Cms\Classes\Asset;
|
|
use Backend\Classes\WidgetBase;
|
|
use ApplicationException;
|
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
|
use October\Rain\Filesystem\Definitions as FileDefinitions;
|
|
use RecursiveIteratorIterator;
|
|
use RecursiveDirectoryIterator;
|
|
use DirectoryIterator;
|
|
use Exception;
|
|
|
|
/**
|
|
* CMS asset list widget.
|
|
*
|
|
* @package october\cms
|
|
* @author Alexey Bobkov, Samuel Georges
|
|
*/
|
|
class AssetList extends WidgetBase
|
|
{
|
|
use \Backend\Traits\SelectableWidget;
|
|
|
|
protected $searchTerm = false;
|
|
|
|
protected $theme;
|
|
|
|
/**
|
|
* @var string Message to display when there are no records in the list.
|
|
*/
|
|
public $noRecordsMessage = 'cms::lang.asset.no_list_records';
|
|
|
|
/**
|
|
* @var string Message to display when the Delete button is clicked.
|
|
*/
|
|
public $deleteConfirmation = 'cms::lang.asset.delete_confirm';
|
|
|
|
/**
|
|
* @var array Valid asset file extensions
|
|
*/
|
|
protected $assetExtensions;
|
|
|
|
public function __construct($controller, $alias)
|
|
{
|
|
$this->alias = $alias;
|
|
$this->theme = Theme::getEditTheme();
|
|
$this->selectionInputName = 'file';
|
|
$this->assetExtensions = FileDefinitions::get('assetExtensions');
|
|
|
|
parent::__construct($controller, []);
|
|
|
|
$this->bindToController();
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
protected function loadAssets()
|
|
{
|
|
$this->addCss('css/assetlist.css', 'core');
|
|
$this->addJs('js/assetlist.js', 'core');
|
|
}
|
|
|
|
/**
|
|
* Renders the widget.
|
|
* @return string
|
|
*/
|
|
public function render()
|
|
{
|
|
return $this->makePartial('body', [
|
|
'data' => $this->getData()
|
|
]);
|
|
}
|
|
|
|
//
|
|
// Event handlers
|
|
//
|
|
|
|
public function onOpenDirectory()
|
|
{
|
|
$path = Input::get('path');
|
|
if (!$this->validatePath($path)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
|
|
}
|
|
|
|
$delay = Input::get('delay');
|
|
if ($delay) {
|
|
usleep(1000000*$delay);
|
|
}
|
|
|
|
$this->putSession('currentPath', $path);
|
|
|
|
return [
|
|
'#'.$this->getId('asset-list') => $this->makePartial('items', ['items' => $this->getData()])
|
|
];
|
|
}
|
|
|
|
public function onRefresh()
|
|
{
|
|
return [
|
|
'#'.$this->getId('asset-list') => $this->makePartial('items', ['items' => $this->getData()])
|
|
];
|
|
}
|
|
|
|
public function onUpdate()
|
|
{
|
|
$this->extendSelection();
|
|
|
|
return $this->onRefresh();
|
|
}
|
|
|
|
public function onDeleteFiles()
|
|
{
|
|
$this->validateRequestTheme();
|
|
|
|
$fileList = Request::input('file');
|
|
$error = null;
|
|
$deleted = [];
|
|
|
|
try {
|
|
$assetsPath = $this->getAssetsPath();
|
|
|
|
foreach ($fileList as $path => $selected) {
|
|
if ($selected) {
|
|
if (!$this->validatePath($path)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
|
|
}
|
|
|
|
$fullPath = $assetsPath.'/'.$path;
|
|
if (File::exists($fullPath)) {
|
|
if (!File::isDirectory($fullPath)) {
|
|
if (!@File::delete($fullPath)) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.asset.error_deleting_file',
|
|
['name' => $path]
|
|
));
|
|
}
|
|
}
|
|
else {
|
|
$empty = File::isDirectoryEmpty($fullPath);
|
|
if ($empty === false) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.asset.error_deleting_dir_not_empty',
|
|
['name' => $path]
|
|
));
|
|
}
|
|
|
|
if (!@rmdir($fullPath)) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.asset.error_deleting_dir',
|
|
['name' => $path]
|
|
));
|
|
}
|
|
}
|
|
|
|
$deleted[] = $path;
|
|
$this->removeSelection($path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception $ex) {
|
|
$error = $ex->getMessage();
|
|
}
|
|
|
|
return [
|
|
'deleted' => $deleted,
|
|
'error' => $error,
|
|
'theme' => Request::input('theme')
|
|
];
|
|
}
|
|
|
|
public function onLoadRenamePopup()
|
|
{
|
|
$this->validateRequestTheme();
|
|
|
|
$path = Input::get('renamePath');
|
|
if (!$this->validatePath($path)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
|
|
}
|
|
|
|
$this->vars['originalPath'] = $path;
|
|
$this->vars['name'] = basename($path);
|
|
|
|
return $this->makePartial('rename_form');
|
|
}
|
|
|
|
public function onApplyName()
|
|
{
|
|
$this->validateRequestTheme();
|
|
|
|
$newName = trim(Input::get('name'));
|
|
if (!strlen($newName)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.name_cant_be_empty'));
|
|
}
|
|
|
|
if (!$this->validatePath($newName)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
|
|
}
|
|
|
|
if (!$this->validateName($newName)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_name'));
|
|
}
|
|
|
|
$originalPath = Input::get('originalPath');
|
|
if (!$this->validatePath($originalPath)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
|
|
}
|
|
|
|
$originalFullPath = $this->getFullPath($originalPath);
|
|
if (!file_exists($originalFullPath)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.original_not_found'));
|
|
}
|
|
|
|
if (!is_dir($originalFullPath) && !$this->validateFileType($newName)) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.asset.type_not_allowed',
|
|
['allowed_types' => implode(', ', $this->assetExtensions)]
|
|
));
|
|
}
|
|
|
|
$newFullPath = $this->getFullPath(dirname($originalPath).'/'.$newName);
|
|
if (file_exists($newFullPath) && $newFullPath !== $originalFullPath) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.already_exists'));
|
|
}
|
|
|
|
if (!@rename($originalFullPath, $newFullPath)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.error_renaming'));
|
|
}
|
|
|
|
return [
|
|
'#'.$this->getId('asset-list') => $this->makePartial('items', ['items' => $this->getData()])
|
|
];
|
|
}
|
|
|
|
public function onLoadNewDirPopup()
|
|
{
|
|
$this->validateRequestTheme();
|
|
|
|
return $this->makePartial('new_dir_form');
|
|
}
|
|
|
|
public function onNewDirectory()
|
|
{
|
|
$this->validateRequestTheme();
|
|
|
|
$newName = trim(Input::get('name'));
|
|
if (!strlen($newName)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.name_cant_be_empty'));
|
|
}
|
|
|
|
if (!$this->validatePath($newName)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
|
|
}
|
|
|
|
if (!$this->validateName($newName)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_name'));
|
|
}
|
|
|
|
$newFullPath = $this->getCurrentPath().'/'.$newName;
|
|
if (file_exists($newFullPath)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.already_exists'));
|
|
}
|
|
|
|
if (!File::makeDirectory($newFullPath)) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.cms_object.error_creating_directory',
|
|
['name' => $newName]
|
|
));
|
|
}
|
|
|
|
return [
|
|
'#'.$this->getId('asset-list') => $this->makePartial('items', ['items' => $this->getData()])
|
|
];
|
|
}
|
|
|
|
public function onLoadMovePopup()
|
|
{
|
|
$this->validateRequestTheme();
|
|
|
|
$fileList = Request::input('file');
|
|
$directories = [];
|
|
|
|
$selectedList = array_filter($fileList, function ($value) {
|
|
return $value == 1;
|
|
});
|
|
|
|
$this->listDestinationDirectories($directories, $selectedList);
|
|
|
|
$this->vars['directories'] = $directories;
|
|
$this->vars['selectedList'] = base64_encode(json_encode(array_keys($selectedList)));
|
|
|
|
return $this->makePartial('move_form');
|
|
}
|
|
|
|
public function onMove()
|
|
{
|
|
$this->validateRequestTheme();
|
|
|
|
$selectedList = Input::get('selectedList');
|
|
if (!strlen($selectedList)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.selected_files_not_found'));
|
|
}
|
|
|
|
$destinationDir = Input::get('dest');
|
|
if (!strlen($destinationDir)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.select_destination_dir'));
|
|
}
|
|
|
|
$destinationFullPath = $this->getFullPath($destinationDir);
|
|
if (!file_exists($destinationFullPath) || !is_dir($destinationFullPath)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.destination_not_found'));
|
|
}
|
|
|
|
$list = @json_decode(@base64_decode($selectedList));
|
|
if ($list === false) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.selected_files_not_found'));
|
|
}
|
|
|
|
foreach ($list as $path) {
|
|
if (!$this->validatePath($path)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path'));
|
|
}
|
|
|
|
$basename = basename($path);
|
|
$originalFullPath = $this->getFullPath($path);
|
|
$newFullPath = realpath(rtrim($destinationFullPath, '/')) . '/' . $basename;
|
|
$safeDir = $this->getAssetsPath();
|
|
|
|
if ($originalFullPath == $newFullPath) {
|
|
continue;
|
|
}
|
|
|
|
if (!starts_with($newFullPath, $safeDir)) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.asset.error_moving_file',
|
|
['file' => $basename]
|
|
));
|
|
}
|
|
|
|
if (is_file($originalFullPath)) {
|
|
if (!@File::move($originalFullPath, $newFullPath)) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.asset.error_moving_file',
|
|
['file' => $basename]
|
|
));
|
|
}
|
|
}
|
|
elseif (is_dir($originalFullPath)) {
|
|
if (!@File::copyDirectory($originalFullPath, $newFullPath)) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.asset.error_moving_directory',
|
|
['dir' => $basename]
|
|
));
|
|
}
|
|
|
|
if (strpos($originalFullPath, '../') !== false) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.asset.error_deleting_directory',
|
|
['dir' => $basename]
|
|
));
|
|
}
|
|
|
|
if (strpos($originalFullPath, $safeDir) !== 0) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.asset.error_deleting_directory',
|
|
['dir' => $basename]
|
|
));
|
|
}
|
|
|
|
if (!@File::deleteDirectory($originalFullPath)) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.asset.error_deleting_directory',
|
|
['dir' => $basename]
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
'#'.$this->getId('asset-list') => $this->makePartial('items', ['items' => $this->getData()])
|
|
];
|
|
}
|
|
|
|
public function onSearch()
|
|
{
|
|
$this->setSearchTerm(Input::get('search'));
|
|
$this->extendSelection();
|
|
|
|
return $this->onRefresh();
|
|
}
|
|
|
|
/*
|
|
* Methods for the internal use
|
|
*/
|
|
|
|
protected function getData()
|
|
{
|
|
$assetsPath = $this->getAssetsPath();
|
|
|
|
if (!file_exists($assetsPath) || !is_dir($assetsPath)) {
|
|
if (!File::makeDirectory($assetsPath)) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.cms_object.error_creating_directory',
|
|
['name' => $assetsPath]
|
|
));
|
|
}
|
|
}
|
|
|
|
$searchTerm = Str::lower($this->getSearchTerm());
|
|
|
|
if (!strlen($searchTerm)) {
|
|
$currentPath = $this->getCurrentPath();
|
|
return $this->getDirectoryContents(
|
|
new DirectoryIterator($currentPath)
|
|
);
|
|
}
|
|
|
|
return $this->findFiles();
|
|
}
|
|
|
|
protected function getAssetsPath()
|
|
{
|
|
return $this->theme->getPath().'/assets';
|
|
}
|
|
|
|
protected function getThemeFileUrl($path)
|
|
{
|
|
return Url::to('themes/'.$this->theme->getDirName().'/assets'.$path);
|
|
}
|
|
|
|
public function getCurrentRelativePath()
|
|
{
|
|
$path = $this->getSession('currentPath', '/');
|
|
|
|
if (!$this->validatePath($path)) {
|
|
return null;
|
|
}
|
|
|
|
if ($path == '.') {
|
|
return null;
|
|
}
|
|
|
|
return ltrim($path, '/');
|
|
}
|
|
|
|
protected function getCurrentPath()
|
|
{
|
|
$assetsPath = $this->getAssetsPath();
|
|
|
|
$path = $assetsPath.'/'.$this->getCurrentRelativePath();
|
|
if (!is_dir($path)) {
|
|
return $assetsPath;
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
protected function getRelativePath($path)
|
|
{
|
|
$prefix = $this->getAssetsPath();
|
|
|
|
if (substr($path, 0, strlen($prefix)) == $prefix) {
|
|
$path = substr($path, strlen($prefix));
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
protected function getFullPath($path)
|
|
{
|
|
return $this->getAssetsPath().'/'.ltrim($path, '/');
|
|
}
|
|
|
|
protected function validatePath($path)
|
|
{
|
|
if (!preg_match('/^[0-9a-z\.\s_\-\/]+$/i', $path)) {
|
|
return false;
|
|
}
|
|
|
|
if (strpos($path, '..') !== false || strpos($path, './') !== false) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function validateName($name)
|
|
{
|
|
if (!preg_match('/^[0-9a-z\.\s_\-]+$/i', $name)) {
|
|
return false;
|
|
}
|
|
|
|
if (strpos($name, '..') !== false) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function getDirectoryContents($dir)
|
|
{
|
|
$editableAssetTypes = Asset::getEditableExtensions();
|
|
|
|
$result = [];
|
|
$files = [];
|
|
|
|
foreach ($dir as $node) {
|
|
if (substr($node->getFileName(), 0, 1) == '.') {
|
|
continue;
|
|
}
|
|
|
|
if ($node->isDir() && !$node->isDot()) {
|
|
$result[$node->getFilename()] = (object)[
|
|
'type' => 'directory',
|
|
'path' => File::normalizePath($this->getRelativePath($node->getPathname())),
|
|
'name' => $node->getFilename(),
|
|
'editable' => false
|
|
];
|
|
}
|
|
elseif ($node->isFile()) {
|
|
$files[] = (object)[
|
|
'type' => 'file',
|
|
'path' => File::normalizePath($this->getRelativePath($node->getPathname())),
|
|
'name' => $node->getFilename(),
|
|
'editable' => in_array(strtolower($node->getExtension()), $editableAssetTypes)
|
|
];
|
|
}
|
|
}
|
|
|
|
foreach ($files as $file) {
|
|
$result[] = $file;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected function listDestinationDirectories(&$result, $excludeList, $startDir = null, $level = 0)
|
|
{
|
|
if ($startDir === null) {
|
|
$startDir = $this->getAssetsPath();
|
|
|
|
$result['/'] = 'assets';
|
|
$level = 1;
|
|
}
|
|
|
|
$dirs = new DirectoryIterator($startDir);
|
|
foreach ($dirs as $node) {
|
|
if (substr($node->getFileName(), 0, 1) == '.') {
|
|
continue;
|
|
}
|
|
|
|
if ($node->isDir() && !$node->isDot()) {
|
|
$fullPath = $node->getPathname();
|
|
$relativePath = $this->getRelativePath($fullPath);
|
|
if (array_key_exists($relativePath, $excludeList)) {
|
|
continue;
|
|
}
|
|
|
|
$result[$relativePath] = str_repeat(' ', $level*4).$node->getFilename();
|
|
|
|
$this->listDestinationDirectories($result, $excludeList, $fullPath, $level+1);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function getSearchTerm()
|
|
{
|
|
return $this->searchTerm !== false ? $this->searchTerm : $this->getSession('search');
|
|
}
|
|
|
|
protected function isSearchMode()
|
|
{
|
|
return strlen($this->getSearchTerm());
|
|
}
|
|
|
|
protected function getThemeSessionKey($prefix)
|
|
{
|
|
return $prefix.$this->theme->getDirName();
|
|
}
|
|
|
|
protected function getUpPath()
|
|
{
|
|
$path = $this->getCurrentRelativePath();
|
|
if (!strlen(rtrim(ltrim($path, '/'), '/'))) {
|
|
return null;
|
|
}
|
|
|
|
return dirname($path);
|
|
}
|
|
|
|
protected function validateRequestTheme()
|
|
{
|
|
if ($this->theme->getDirName() != Request::input('theme')) {
|
|
throw new ApplicationException(trans('cms::lang.theme.edit.not_match'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for valid asset file extension
|
|
* @param string
|
|
* @return bool
|
|
*/
|
|
protected function validateFileType($name)
|
|
{
|
|
$extension = strtolower(File::extension($name));
|
|
|
|
if (!in_array($extension, $this->assetExtensions)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Process file uploads submitted via AJAX
|
|
*
|
|
* @return void
|
|
* @throws ApplicationException If the file "file_data" wasn't detected in the request or if the file failed to pass validation / security checks
|
|
*/
|
|
public function onUpload()
|
|
{
|
|
$fileName = null;
|
|
|
|
try {
|
|
$uploadedFile = Input::file('file_data');
|
|
|
|
if (!is_object($uploadedFile)) {
|
|
return;
|
|
}
|
|
|
|
$fileName = $uploadedFile->getClientOriginalName();
|
|
|
|
/*
|
|
* Check valid upload
|
|
*/
|
|
if (!$uploadedFile->isValid()) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.file_not_valid'));
|
|
}
|
|
|
|
/*
|
|
* Check file size
|
|
*/
|
|
$maxSize = UploadedFile::getMaxFilesize();
|
|
if ($uploadedFile->getSize() > $maxSize) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.asset.too_large',
|
|
['max_size' => File::sizeToString($maxSize)]
|
|
));
|
|
}
|
|
|
|
/*
|
|
* Check for valid file extensions
|
|
*/
|
|
if (!$this->validateFileType($fileName)) {
|
|
throw new ApplicationException(Lang::get(
|
|
'cms::lang.asset.type_not_allowed',
|
|
['allowed_types' => implode(', ', $this->assetExtensions)]
|
|
));
|
|
}
|
|
|
|
/*
|
|
* Accept the uploaded file
|
|
*/
|
|
$uploadedFile = $uploadedFile->move($this->getCurrentPath(), $uploadedFile->getClientOriginalName());
|
|
|
|
File::chmod($uploadedFile->getRealPath());
|
|
|
|
$response = Response::make('success');
|
|
}
|
|
catch (Exception $ex) {
|
|
$message = $fileName !== null
|
|
? Lang::get('cms::lang.asset.error_uploading_file', ['name' => $fileName, 'error' => $ex->getMessage()])
|
|
: $ex->getMessage();
|
|
|
|
$response = Response::make($message);
|
|
}
|
|
|
|
// Override the controller response
|
|
$this->controller->setResponse($response);
|
|
}
|
|
|
|
protected function setSearchTerm($term)
|
|
{
|
|
$this->searchTerm = trim($term);
|
|
$this->putSession('search', $this->searchTerm);
|
|
}
|
|
|
|
protected function findFiles()
|
|
{
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($this->getAssetsPath(), RecursiveDirectoryIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST,
|
|
RecursiveIteratorIterator::CATCH_GET_CHILD
|
|
);
|
|
|
|
$editableAssetTypes = Asset::getEditableExtensions();
|
|
$searchTerm = Str::lower($this->getSearchTerm());
|
|
$words = explode(' ', $searchTerm);
|
|
|
|
$result = [];
|
|
foreach ($iterator as $item) {
|
|
if (!$item->isDir()) {
|
|
if (substr($item->getFileName(), 0, 1) == '.') {
|
|
continue;
|
|
}
|
|
|
|
$path = $this->getRelativePath($item->getPathname());
|
|
|
|
if ($this->pathMatchesSearch($words, $path)) {
|
|
$result[] = (object)[
|
|
'type' => 'file',
|
|
'path' => File::normalizePath($path),
|
|
'name' => $item->getFilename(),
|
|
'editable' => in_array(strtolower($item->getExtension()), $editableAssetTypes)
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected function pathMatchesSearch(&$words, $path)
|
|
{
|
|
foreach ($words as $word) {
|
|
$word = trim($word);
|
|
if (!strlen($word)) {
|
|
continue;
|
|
}
|
|
|
|
if (!Str::contains(Str::lower($path), $word)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|