Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
- Base: wintercms/winter branch 1.2 (full framework) - Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS - Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication - Partials: hero (offline-first), features, modes (offline/nube toggle), screenshots, pricing (3 planes), comparison, FAQ, CTA - Plugin VivesPOS.Site with ContactForm - Dockerfile: PHP 8.2 Apache, port 80, healthcheck - Added winter/wn-pages, blog, sitemap, seo plugins - Active theme set to vivespos
1380 lines
40 KiB
PHP
1380 lines
40 KiB
PHP
<?php namespace Backend\Widgets;
|
|
|
|
use Str;
|
|
use Lang;
|
|
use Input;
|
|
use Event;
|
|
use Config;
|
|
use Backend;
|
|
use ApplicationException;
|
|
use Backend\Classes\WidgetBase;
|
|
use System\Classes\ImageResizer;
|
|
use System\Classes\MediaLibrary;
|
|
use System\Classes\MediaLibraryItem;
|
|
|
|
/**
|
|
* Media Manager widget.
|
|
*
|
|
* @package winter\wn-backend-module
|
|
* @author Alexey Bobkov, Samuel Georges
|
|
*/
|
|
class MediaManager extends WidgetBase
|
|
{
|
|
use \Backend\Traits\UploadableWidget;
|
|
use \Backend\Traits\PreferenceMaker;
|
|
|
|
const FOLDER_ROOT = '/';
|
|
|
|
const VIEW_MODE_GRID = 'grid';
|
|
const VIEW_MODE_LIST = 'list';
|
|
const VIEW_MODE_TILES = 'tiles';
|
|
|
|
const SELECTION_MODE_NORMAL = 'normal';
|
|
const SELECTION_MODE_FIXED_RATIO = 'fixed-ratio';
|
|
const SELECTION_MODE_FIXED_SIZE = 'fixed-size';
|
|
|
|
const FILTER_ALL = 'all';
|
|
|
|
/**
|
|
* @var boolean Determines whether the widget is in readonly mode or not.
|
|
*/
|
|
public $readOnly = false;
|
|
|
|
/**
|
|
* @var boolean Determines whether the bottom toolbar is visible.
|
|
*/
|
|
public $bottomToolbar = false;
|
|
|
|
/**
|
|
* @var boolean Determines whether the Crop & Insert button is visible.
|
|
*/
|
|
public $cropAndInsertButton = false;
|
|
|
|
/**
|
|
* @var boolean Determines whether the Display filters are visible.
|
|
*/
|
|
public bool $filterDisplay = true;
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct($controller, $alias, $readOnly = false)
|
|
{
|
|
$this->alias = $alias;
|
|
$this->readOnly = $readOnly;
|
|
|
|
parent::__construct($controller, []);
|
|
}
|
|
|
|
/**
|
|
* Adds widget specific asset files. Use $this->addJs() and $this->addCss()
|
|
* to register new assets to include on the page.
|
|
* @return void
|
|
*/
|
|
protected function loadAssets()
|
|
{
|
|
$this->addCss('css/mediamanager.css', 'core');
|
|
|
|
if (Config::get('develop.decompileBackendAssets', false)) {
|
|
$scripts = Backend::decompileAsset($this->getAssetPath('js/mediamanager-browser.js'));
|
|
foreach ($scripts as $script) {
|
|
$this->addJs($script, 'core');
|
|
}
|
|
} else {
|
|
$this->addJs('js/mediamanager-browser-min.js', 'core');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abort the request with an access-denied code if readOnly mode is active
|
|
*/
|
|
protected function abortIfReadOnly(): void
|
|
{
|
|
if ($this->readOnly) {
|
|
abort(403);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders the widget.
|
|
*/
|
|
public function render(): string
|
|
{
|
|
$this->prepareVars();
|
|
|
|
return $this->makePartial('body');
|
|
}
|
|
|
|
//
|
|
// AJAX handlers
|
|
//
|
|
|
|
/**
|
|
* Perform a search with the query specified in the request ("search")
|
|
*/
|
|
public function onSearch(): array
|
|
{
|
|
$this->setSearchTerm(Input::get('search'));
|
|
|
|
$this->prepareVars();
|
|
|
|
return [
|
|
'#' . $this->getId('item-list') => $this->makePartial('item-list'),
|
|
'#' . $this->getId('folder-path') => $this->makePartial('folder-path')
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Go to the path specified in the request ("path")
|
|
*/
|
|
public function onGoToFolder(): array
|
|
{
|
|
$path = Input::get('path');
|
|
|
|
if (Input::get('clearCache')) {
|
|
MediaLibrary::instance()->resetCache();
|
|
}
|
|
|
|
if (Input::get('resetSearch')) {
|
|
$this->setSearchTerm(null);
|
|
}
|
|
|
|
$this->setCurrentFolder($path);
|
|
$this->prepareVars();
|
|
|
|
return [
|
|
'#' . $this->getId('item-list') => $this->makePartial('item-list'),
|
|
'#' . $this->getId('folder-path') => $this->makePartial('folder-path')
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate thumbnails for the provided array of thumbnail info ("batch")
|
|
*/
|
|
public function onGenerateThumbnails(): array
|
|
{
|
|
$batch = Input::get('batch');
|
|
if (!is_array($batch)) {
|
|
return [];
|
|
}
|
|
|
|
$result = [];
|
|
foreach ($batch as $thumbnailInfo) {
|
|
$result[] = $this->generateThumbnail($thumbnailInfo);
|
|
}
|
|
|
|
return [
|
|
'generatedThumbnails' => $result
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get the thumbnail for the provided path ("path") and lastModified date ("lastModified")
|
|
*
|
|
* @throws ApplicationException if the lastModified date is invalid
|
|
*/
|
|
public function onGetSidebarThumbnail(): array
|
|
{
|
|
$path = MediaLibrary::validatePath(Input::get('path'));
|
|
$lastModified = Input::get('lastModified');
|
|
|
|
if (!is_numeric($lastModified)) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
$thumbnailParams = $this->getThumbnailParams();
|
|
$thumbnailParams['width'] = 300;
|
|
$thumbnailParams['height'] = 255;
|
|
$thumbnailParams['mode'] = 'auto';
|
|
|
|
$thumbnailInfo = $thumbnailParams;
|
|
$thumbnailInfo['path'] = $path;
|
|
$thumbnailInfo['lastModified'] = $lastModified;
|
|
$thumbnailInfo['id'] = 'sidebar-thumbnail';
|
|
|
|
return $this->generateThumbnail($thumbnailInfo, $thumbnailParams);
|
|
}
|
|
|
|
/**
|
|
* Render the view for the provided "path" and "view" mode from the request
|
|
*/
|
|
public function onChangeView(): array
|
|
{
|
|
$viewMode = Input::get('view');
|
|
$path = Input::get('path');
|
|
|
|
$this->setViewMode($viewMode);
|
|
$this->setCurrentFolder($path);
|
|
|
|
$this->prepareVars();
|
|
|
|
return [
|
|
'#' . $this->getId('item-list') => $this->makePartial('item-list'),
|
|
'#' . $this->getId('folder-path') => $this->makePartial('folder-path'),
|
|
'#' . $this->getId('view-mode-buttons') => $this->makePartial('view-mode-buttons')
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Set the current filter from the request ("filter")
|
|
*/
|
|
public function onSetFilter(): array
|
|
{
|
|
$filter = Input::get('filter');
|
|
$path = Input::get('path');
|
|
|
|
$this->setFilter($filter);
|
|
$this->setCurrentFolder($path);
|
|
|
|
$this->prepareVars();
|
|
|
|
return [
|
|
'#' . $this->getId('item-list') => $this->makePartial('item-list'),
|
|
'#' . $this->getId('folder-path') => $this->makePartial('folder-path'),
|
|
'#' . $this->getId('filters') => $this->makePartial('filters')
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Set the current sorting configuration from the request ("sortBy", "sortDirection")
|
|
*/
|
|
public function onSetSorting(): array
|
|
{
|
|
$sortBy = Input::get('sortBy', $this->getSortBy());
|
|
$sortDirection = Input::get('sortDirection', $this->getSortDirection());
|
|
$path = Input::get('path');
|
|
|
|
$this->setSortBy($sortBy);
|
|
$this->setSortDirection($sortDirection);
|
|
$this->setCurrentFolder($path);
|
|
|
|
$this->prepareVars();
|
|
|
|
return [
|
|
'#' . $this->getId('item-list') => $this->makePartial('item-list'),
|
|
'#' . $this->getId('folder-path') => $this->makePartial('folder-path')
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Deletes the provided paths from the request ("paths")
|
|
*
|
|
* @throws ApplicationException if the paths input is invalid
|
|
* @todo Move media events to the MediaLibary class instead.
|
|
*/
|
|
public function onDeleteItem(): array
|
|
{
|
|
$this->abortIfReadOnly();
|
|
|
|
$paths = Input::get('paths');
|
|
|
|
if (!is_array($paths)) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
$library = MediaLibrary::instance();
|
|
|
|
$filesToDelete = [];
|
|
foreach ($paths as $pathInfo) {
|
|
$path = array_get($pathInfo, 'path');
|
|
$type = array_get($pathInfo, 'type');
|
|
|
|
if (!$path || !$type) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
if ($type === MediaLibraryItem::TYPE_FILE) {
|
|
/*
|
|
* Add to bulk collection
|
|
*/
|
|
$filesToDelete[] = $path;
|
|
} elseif ($type === MediaLibraryItem::TYPE_FOLDER) {
|
|
/*
|
|
* Delete single folder
|
|
*/
|
|
$library->deleteFolder($path);
|
|
|
|
/**
|
|
* @event media.folder.delete
|
|
* Called after a folder is deleted
|
|
*
|
|
* Example usage:
|
|
*
|
|
* Event::listen('media.folder.delete', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $path) {
|
|
* \Log::info($path . " was deleted");
|
|
* });
|
|
*
|
|
* Or
|
|
*
|
|
* $mediaWidget->bindEvent('folder.delete', function ((string) $path) {
|
|
* \Log::info($path . " was deleted");
|
|
* });
|
|
*
|
|
*/
|
|
$this->fireSystemEvent('media.folder.delete', [$path]);
|
|
}
|
|
}
|
|
|
|
if (count($filesToDelete) > 0) {
|
|
/*
|
|
* Delete collection of files
|
|
*/
|
|
$library->deleteFiles($filesToDelete);
|
|
|
|
/*
|
|
* Extensibility
|
|
*/
|
|
foreach ($filesToDelete as $path) {
|
|
/**
|
|
* @event media.file.delete
|
|
* Called after a file is deleted
|
|
*
|
|
* Example usage:
|
|
*
|
|
* Event::listen('media.file.delete', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $path) {
|
|
* \Log::info($path . " was deleted");
|
|
* });
|
|
*
|
|
* Or
|
|
*
|
|
* $mediaWidget->bindEvent('file.delete', function ((string) $path) {
|
|
* \Log::info($path . " was deleted");
|
|
* });
|
|
*
|
|
*/
|
|
$this->fireSystemEvent('media.file.delete', [$path]);
|
|
}
|
|
}
|
|
|
|
$library->resetCache();
|
|
$this->prepareVars();
|
|
|
|
return [
|
|
'#' . $this->getId('item-list') => $this->makePartial('item-list')
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Render the rename popup for the provided "path" from the request
|
|
*/
|
|
public function onLoadRenamePopup(): string
|
|
{
|
|
$this->abortIfReadOnly();
|
|
|
|
$path = Input::get('path');
|
|
$path = MediaLibrary::validatePath($path);
|
|
|
|
$this->vars['originalPath'] = $path;
|
|
$this->vars['name'] = basename($path);
|
|
$this->vars['listId'] = Input::get('listId');
|
|
$this->vars['type'] = Input::get('type');
|
|
|
|
return $this->makePartial('rename-form');
|
|
}
|
|
|
|
/**
|
|
* Rename the provided path from the request ("originalPath") to the new name ("name")
|
|
*
|
|
* @throws ApplicationException if the new name is invalid
|
|
* @todo Move media events to the MediaLibary class instead.
|
|
*/
|
|
public function onApplyName(): void
|
|
{
|
|
$this->abortIfReadOnly();
|
|
|
|
$newName = trim(Input::get('name'));
|
|
if (!strlen($newName)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.name_cant_be_empty'));
|
|
}
|
|
|
|
if (!$this->validateFileName($newName)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_name'));
|
|
}
|
|
|
|
$originalPath = Input::get('originalPath');
|
|
$originalPath = MediaLibrary::validatePath($originalPath);
|
|
$newPath = dirname($originalPath) . '/' . $newName;
|
|
$type = Input::get('type');
|
|
|
|
if ($type == MediaLibraryItem::TYPE_FILE) {
|
|
/*
|
|
* Validate extension
|
|
*/
|
|
if (!$this->validateFileType($newName)) {
|
|
throw new ApplicationException(Lang::get('backend::lang.media.type_blocked'));
|
|
}
|
|
|
|
/*
|
|
* Move single file
|
|
*/
|
|
MediaLibrary::instance()->moveFile($originalPath, $newPath);
|
|
|
|
/**
|
|
* @event media.file.rename
|
|
* Called after a file is renamed / moved
|
|
*
|
|
* Example usage:
|
|
*
|
|
* Event::listen('media.file.rename', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $originalPath, (string) $newPath) {
|
|
* \Log::info($originalPath . " was moved to " . $path);
|
|
* });
|
|
*
|
|
* Or
|
|
*
|
|
* $mediaWidget->bindEvent('file.rename', function ((string) $originalPath, (string) $newPath) {
|
|
* \Log::info($originalPath . " was moved to " . $path);
|
|
* });
|
|
*
|
|
*/
|
|
$this->fireSystemEvent('media.file.rename', [$originalPath, $newPath]);
|
|
} else {
|
|
/*
|
|
* Move single folder
|
|
*/
|
|
MediaLibrary::instance()->moveFolder($originalPath, $newPath);
|
|
|
|
/**
|
|
* @event media.folder.rename
|
|
* Called after a folder is renamed / moved
|
|
*
|
|
* Example usage:
|
|
*
|
|
* Event::listen('media.folder.rename', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $originalPath, (string) $newPath) {
|
|
* \Log::info($originalPath . " was moved to " . $path);
|
|
* });
|
|
*
|
|
* Or
|
|
*
|
|
* $mediaWidget->bindEvent('folder.rename', function ((string) $originalPath, (string) $newPath) {
|
|
* \Log::info($originalPath . " was moved to " . $path);
|
|
* });
|
|
*
|
|
*/
|
|
$this->fireSystemEvent('media.folder.rename', [$originalPath, $newPath]);
|
|
}
|
|
|
|
MediaLibrary::instance()->resetCache();
|
|
}
|
|
|
|
/**
|
|
* Create a new folder ("name") in the provided "path" from the request
|
|
*
|
|
* @throws ApplicationException If the requested folder already exists or is otherwise invalid
|
|
*/
|
|
public function onCreateFolder(): array
|
|
{
|
|
$this->abortIfReadOnly();
|
|
|
|
$name = trim(Input::get('name'));
|
|
if (!strlen($name)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.name_cant_be_empty'));
|
|
}
|
|
|
|
if (!$this->validateFileName($name)) {
|
|
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_name'));
|
|
}
|
|
|
|
$path = Input::get('path');
|
|
$path = MediaLibrary::validatePath($path);
|
|
|
|
$newFolderPath = $path . '/' . $name;
|
|
|
|
$library = MediaLibrary::instance();
|
|
|
|
if ($library->folderExists($newFolderPath)) {
|
|
throw new ApplicationException(Lang::get('backend::lang.media.folder_or_file_exist'));
|
|
}
|
|
|
|
/*
|
|
* Create the new folder
|
|
*/
|
|
if (!$library->makeFolder($newFolderPath)) {
|
|
throw new ApplicationException(Lang::get('backend::lang.media.error_creating_folder'));
|
|
}
|
|
|
|
/**
|
|
* @event media.folder.create
|
|
* Called after a folder is created
|
|
*
|
|
* Example usage:
|
|
*
|
|
* Event::listen('media.folder.create', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $newFolderPath) {
|
|
* \Log::info($newFolderPath . " was created");
|
|
* });
|
|
*
|
|
* Or
|
|
*
|
|
* $mediaWidget->bindEvent('folder.create', function ((string) $newFolderPath) {
|
|
* \Log::info($newFolderPath . " was created");
|
|
* });
|
|
*
|
|
*/
|
|
$this->fireSystemEvent('media.folder.create', [$newFolderPath]);
|
|
|
|
$library->resetCache();
|
|
|
|
$this->prepareVars();
|
|
|
|
return [
|
|
'#' . $this->getId('item-list') => $this->makePartial('item-list')
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Render the move popup with a list of folders to move the selected items to excluding the provided paths in the request ("exclude")
|
|
*
|
|
* @throws ApplicationException If the exclude input data is not an array
|
|
*/
|
|
public function onLoadMovePopup(): string
|
|
{
|
|
$this->abortIfReadOnly();
|
|
|
|
$exclude = Input::get('exclude', []);
|
|
if (!is_array($exclude)) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
$folders = MediaLibrary::instance()->listAllDirectories($exclude);
|
|
|
|
$folderList = [];
|
|
foreach ($folders as $folder) {
|
|
$path = $folder;
|
|
|
|
if ($folder == '/') {
|
|
$name = Lang::get('backend::lang.media.library');
|
|
} else {
|
|
$segments = explode('/', $folder);
|
|
$name = str_repeat(' ', (count($segments) - 1) * 4) . basename($folder);
|
|
}
|
|
|
|
$folderList[$path] = $name;
|
|
}
|
|
|
|
$this->vars['folders'] = $folderList;
|
|
$this->vars['originalPath'] = Input::get('path');
|
|
|
|
return $this->makePartial('move-form');
|
|
}
|
|
|
|
/**
|
|
* Move the selected items ("files", "folders") to the provided destination path from the request ("dest")
|
|
*
|
|
* @throws ApplicationException if the input data is invalid
|
|
*/
|
|
public function onMoveItems(): array
|
|
{
|
|
$this->abortIfReadOnly();
|
|
|
|
$dest = trim(Input::get('dest'));
|
|
if (!strlen($dest)) {
|
|
throw new ApplicationException(Lang::get('backend::lang.media.please_select_move_dest'));
|
|
}
|
|
|
|
$dest = MediaLibrary::validatePath($dest);
|
|
if ($dest == Input::get('originalPath')) {
|
|
throw new ApplicationException(Lang::get('backend::lang.media.move_dest_src_match'));
|
|
}
|
|
|
|
$files = Input::get('files', []);
|
|
if (!is_array($files)) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
$folders = Input::get('folders', []);
|
|
if (!is_array($folders)) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
$library = MediaLibrary::instance();
|
|
|
|
foreach ($files as $path) {
|
|
/*
|
|
* Move a single file
|
|
*/
|
|
$library->moveFile($path, $dest . '/' . basename($path));
|
|
|
|
/**
|
|
* @event media.file.move
|
|
* Called after a file is moved
|
|
*
|
|
* Example usage:
|
|
*
|
|
* Event::listen('media.file.move', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $path, (string) $dest) {
|
|
* \Log::info($path . " was moved to " . $dest);
|
|
* });
|
|
*
|
|
* Or
|
|
*
|
|
* $mediaWidget->bindEvent('file.rename', function ((string) $path, (string) $dest) {
|
|
* \Log::info($path . " was moved to " . $dest);
|
|
* });
|
|
*
|
|
*/
|
|
$this->fireSystemEvent('media.file.move', [$path, $dest]);
|
|
}
|
|
|
|
foreach ($folders as $path) {
|
|
/*
|
|
* Move a single folder
|
|
*/
|
|
$library->moveFolder($path, $dest . '/' . basename($path));
|
|
|
|
/**
|
|
* @event media.folder.move
|
|
* Called after a folder is moved
|
|
*
|
|
* Example usage:
|
|
*
|
|
* Event::listen('media.folder.move', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $path, (string) $dest) {
|
|
* \Log::info($path . " was moved to " . $dest);
|
|
* });
|
|
*
|
|
* Or
|
|
*
|
|
* $mediaWidget->bindEvent('folder.rename', function ((string) $path, (string) $dest) {
|
|
* \Log::info($path . " was moved to " . $dest);
|
|
* });
|
|
*
|
|
*/
|
|
$this->fireSystemEvent('media.folder.move', [$path, $dest]);
|
|
}
|
|
|
|
$library->resetCache();
|
|
|
|
$this->prepareVars();
|
|
|
|
return [
|
|
'#' . $this->getId('item-list') => $this->makePartial('item-list')
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Sets the sidebar visibility state from the request ("visible")
|
|
*/
|
|
public function onSetSidebarVisible(): void
|
|
{
|
|
$visible = (bool) Input::get('visible');
|
|
|
|
$this->setSidebarVisible($visible);
|
|
}
|
|
|
|
/**
|
|
* Renders the widget in a popup body (options include "bottomToolbar" and "cropAndInsertButton")
|
|
*/
|
|
public function onLoadPopup(): string
|
|
{
|
|
$this->bottomToolbar = Input::get('bottomToolbar', $this->bottomToolbar);
|
|
|
|
$this->cropAndInsertButton = Input::get('cropAndInsertButton', $this->cropAndInsertButton);
|
|
|
|
if ($mode = Input::get('mode')) {
|
|
$this->setFilter($mode);
|
|
if ($mode !== static::FILTER_ALL) {
|
|
$this->setFilterDisplay(false);
|
|
}
|
|
}
|
|
|
|
return $this->makePartial('popup-body');
|
|
}
|
|
|
|
/**
|
|
* Prepares & renders the image crop popup body
|
|
*/
|
|
public function onLoadImageCropPopup(): string
|
|
{
|
|
$this->abortIfReadOnly();
|
|
|
|
$path = Input::get('path');
|
|
$path = MediaLibrary::validatePath($path);
|
|
$selectionParams = $this->getSelectionParams();
|
|
$url = MediaLibrary::url($path);
|
|
|
|
// @TODO: Improve support non-local disks
|
|
if (Str::startsWith($url, '/')) {
|
|
$localPath = base_path(implode("/", array_map("rawurldecode", explode("/", $url))));
|
|
$dimensions = getimagesize($localPath);
|
|
} else {
|
|
$dimensions = getimagesize($url);
|
|
}
|
|
|
|
$width = $dimensions[0];
|
|
$height = $dimensions[1] ?: 1;
|
|
|
|
$this->vars['currentSelectionMode'] = $selectionParams['mode'];
|
|
$this->vars['currentSelectionWidth'] = $selectionParams['width'];
|
|
$this->vars['currentSelectionHeight'] = $selectionParams['height'];
|
|
$this->vars['imageUrl'] = $url;
|
|
$this->vars['dimensions'] = $dimensions;
|
|
$this->vars['originalRatio'] = round($width / $height, 5);
|
|
$this->vars['path'] = $path;
|
|
|
|
return $this->makePartial('image-crop-popup-body');
|
|
}
|
|
|
|
/**
|
|
* Crop image AJAX handler
|
|
*
|
|
* @throws ApplicationException if the input data is invalid
|
|
*/
|
|
public function onCropImage(): array
|
|
{
|
|
$this->abortIfReadOnly();
|
|
|
|
$selectionData = Input::get('selection');
|
|
$sourceImageUrl = Input::get('img');
|
|
$mediaItemPath = Input::get('path');
|
|
|
|
if (!is_array($selectionData)) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
foreach (['x', 'y', 'w', 'h'] as $key) {
|
|
if (!isset($selectionData[$key]) || !is_numeric($selectionData[$key])) {
|
|
throw new ApplicationException('Invalid selection data.');
|
|
}
|
|
|
|
$selectionData[$key] = (int) $selectionData[$key];
|
|
}
|
|
|
|
if ($selectionData['h'] === 0 || $selectionData['w'] === 0) {
|
|
throw new ApplicationException('You must define a crop size before inserting');
|
|
}
|
|
|
|
// Initialize the ImageResizer
|
|
$resizer = new ImageResizer(
|
|
$sourceImageUrl,
|
|
$selectionData['w'],
|
|
$selectionData['h'],
|
|
[
|
|
'mode' => 'exact',
|
|
'offset' => [
|
|
$selectionData['x'],
|
|
$selectionData['y'],
|
|
],
|
|
],
|
|
);
|
|
|
|
// Crop the image
|
|
$resizer->crop();
|
|
|
|
// Get the path to the cropped image
|
|
$croppedPath = $resizer->getPathToResizedImage();
|
|
|
|
// Generate the target path for the cropped image
|
|
$targetPath = $this->deduplicatePath($mediaItemPath, '_cropped');
|
|
|
|
// Move the cropped image to the target path
|
|
MediaLibrary::instance()->put(
|
|
$targetPath,
|
|
ImageResizer::getDefaultDisk()->get($croppedPath)
|
|
);
|
|
|
|
$result = [
|
|
'publicUrl' => MediaLibrary::url($targetPath),
|
|
'documentType' => MediaLibraryItem::FILE_TYPE_IMAGE,
|
|
'itemType' => MediaLibraryItem::TYPE_FILE,
|
|
'path' => $targetPath,
|
|
'title' => basename($targetPath),
|
|
'folder' => dirname($targetPath),
|
|
];
|
|
|
|
$selectionMode = Input::get('selectionMode');
|
|
$selectionWidth = Input::get('selectionWidth');
|
|
$selectionHeight = Input::get('selectionHeight');
|
|
|
|
$this->setSelectionParams($selectionMode, $selectionWidth, $selectionHeight);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Handles resizing the provided image and returns the URL to the resized image
|
|
* Used by the Crop & Insert popup to resize the image being cropped on the canvas
|
|
* before cropping it.
|
|
*
|
|
* @throws ApplicationException if the provided input data is invalid
|
|
*/
|
|
public function onResizeImage(): array
|
|
{
|
|
$this->abortIfReadOnly();
|
|
|
|
$width = trim(Input::get('width'));
|
|
if (!strlen($width) || !ctype_digit($width)) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
$height = trim(Input::get('height'));
|
|
if (!strlen($height) || !ctype_digit($height)) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
$path = Input::get('path');
|
|
$path = MediaLibrary::validatePath($path);
|
|
|
|
// Initialize the ImageResizer
|
|
$resizer = new ImageResizer(
|
|
MediaLibrary::url($path),
|
|
$width,
|
|
$height,
|
|
[
|
|
'mode' => 'exact',
|
|
],
|
|
);
|
|
|
|
// Process the resize
|
|
$resizer->resize();
|
|
|
|
// Get the URL to the resized image
|
|
$resizedUrl = $resizer->getResizedUrl();
|
|
|
|
return [
|
|
'url' => $resizedUrl,
|
|
'dimensions' => [$width, $height]
|
|
];
|
|
}
|
|
|
|
//
|
|
// Methods for internal use
|
|
//
|
|
|
|
/**
|
|
* Internal method to prepare view variables.
|
|
*/
|
|
protected function prepareVars()
|
|
{
|
|
clearstatcache();
|
|
|
|
$folder = $this->getCurrentFolder();
|
|
$viewMode = $this->getViewMode();
|
|
$filter = $this->getFilter();
|
|
$sortBy = $this->getSortBy();
|
|
$sortDirection = $this->getSortDirection();
|
|
$searchTerm = $this->getSearchTerm();
|
|
$searchMode = strlen($searchTerm) > 0;
|
|
|
|
if (!$searchMode) {
|
|
$this->vars['items'] = $this->listFolderItems($folder, $filter, ['by' => $sortBy, 'direction' => $sortDirection]);
|
|
}
|
|
else {
|
|
$this->vars['items'] = $this->findFiles($searchTerm, $filter, ['by' => $sortBy, 'direction' => $sortDirection]);
|
|
}
|
|
|
|
$this->vars['currentFolder'] = $folder;
|
|
$this->vars['isRootFolder'] = $folder == self::FOLDER_ROOT;
|
|
$this->vars['pathSegments'] = $this->splitPathToSegments($folder);
|
|
$this->vars['viewMode'] = $viewMode;
|
|
$this->vars['thumbnailParams'] = $this->getThumbnailParams($viewMode);
|
|
$this->vars['currentFilter'] = $filter;
|
|
$this->vars['sortBy'] = $sortBy;
|
|
$this->vars['sortDirection'] = $sortDirection;
|
|
$this->vars['searchMode'] = $searchMode;
|
|
$this->vars['searchTerm'] = $searchTerm;
|
|
$this->vars['sidebarVisible'] = $this->getSidebarVisible();
|
|
}
|
|
|
|
/**
|
|
* Returns a list of folders and files in a Library folder.
|
|
*
|
|
* @param string $searchTerm
|
|
* @param string $filter
|
|
* @param string $sortBy
|
|
* @return array[System\Classes\MediaLibraryItem]
|
|
*/
|
|
protected function listFolderItems($folder, $filter, $sortBy)
|
|
{
|
|
$filter = $filter !== self::FILTER_ALL ? $filter : null;
|
|
|
|
return MediaLibrary::instance()->listFolderContents($folder, $sortBy, $filter);
|
|
}
|
|
|
|
/**
|
|
* Finds files from within the media library based on supplied criteria,
|
|
* returns an array of MediaLibraryItem objects.
|
|
*
|
|
* @param string $searchTerm
|
|
* @param string $filter
|
|
* @param string $sortBy
|
|
* @return array[System\Classes\MediaLibraryItem]
|
|
*/
|
|
protected function findFiles($searchTerm, $filter, $sortBy)
|
|
{
|
|
$filter = $filter !== self::FILTER_ALL ? $filter : null;
|
|
|
|
return MediaLibrary::instance()->findFiles($searchTerm, $sortBy, $filter);
|
|
}
|
|
|
|
/**
|
|
* Sets the provided path as the current folder in the session
|
|
*/
|
|
protected function setCurrentFolder(string $path): void
|
|
{
|
|
$path = MediaLibrary::validatePath($path);
|
|
|
|
$this->putSession('media_folder', $path);
|
|
}
|
|
|
|
/**
|
|
* Gets the user's current folder from the session
|
|
*/
|
|
protected function getCurrentFolder(): string
|
|
{
|
|
return $this->getSession('media_folder', self::FOLDER_ROOT);
|
|
}
|
|
|
|
/**
|
|
* Sets the user filter from the session
|
|
*/
|
|
protected function setFilter(string $filter): void
|
|
{
|
|
if (!in_array($filter, [
|
|
self::FILTER_ALL,
|
|
MediaLibraryItem::FILE_TYPE_IMAGE,
|
|
MediaLibraryItem::FILE_TYPE_AUDIO,
|
|
MediaLibraryItem::FILE_TYPE_DOCUMENT,
|
|
MediaLibraryItem::FILE_TYPE_VIDEO
|
|
])) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
$this->putSession('media_filter', $filter);
|
|
}
|
|
|
|
/**
|
|
* Sets the filter display option for the request
|
|
*/
|
|
protected function setFilterDisplay(bool $status): void
|
|
{
|
|
$this->filterDisplay = $status;
|
|
}
|
|
|
|
/**
|
|
* Gets the filter display option for the request
|
|
*/
|
|
protected function getFilterDisplay(): bool
|
|
{
|
|
return $this->filterDisplay;
|
|
}
|
|
|
|
/**
|
|
* Gets the user filter from the session state
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function getFilter()
|
|
{
|
|
return $this->getSession('media_filter', self::FILTER_ALL);
|
|
}
|
|
|
|
/**
|
|
* Sets the user search term from the session state
|
|
*
|
|
* @param string $searchTerm
|
|
*/
|
|
protected function setSearchTerm($searchTerm): void
|
|
{
|
|
$this->putSession('media_search', trim($searchTerm));
|
|
}
|
|
|
|
/**
|
|
* Gets the user search term from the session state
|
|
*/
|
|
protected function getSearchTerm(): ?string
|
|
{
|
|
return $this->getSession('media_search', null);
|
|
}
|
|
|
|
/**
|
|
* Sets the sort column
|
|
*/
|
|
protected function setSortBy(string $sortBy): void
|
|
{
|
|
if (!in_array($sortBy, [
|
|
MediaLibrary::SORT_BY_TITLE,
|
|
MediaLibrary::SORT_BY_SIZE,
|
|
MediaLibrary::SORT_BY_MODIFIED
|
|
])) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
$key = 'media_sort_by';
|
|
$this->putUserPreference($key, $sortBy);
|
|
$this->putSession($key, $sortBy);
|
|
}
|
|
|
|
/**
|
|
* Gets the current column to sort by
|
|
*/
|
|
protected function getSortBy(): string
|
|
{
|
|
$key = 'media_sort_by';
|
|
return $this->getSession($key, $this->getUserPreference($key, MediaLibrary::SORT_BY_TITLE));
|
|
}
|
|
|
|
/**
|
|
* Sets the sort direction from the session state
|
|
*
|
|
* @param string $sortDirection
|
|
*/
|
|
protected function setSortDirection($sortDirection): void
|
|
{
|
|
if (!in_array($sortDirection, [
|
|
MediaLibrary::SORT_DIRECTION_ASC,
|
|
MediaLibrary::SORT_DIRECTION_DESC
|
|
])) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
$key = 'media_sort_direction';
|
|
$this->putUserPreference($key, $sortDirection);
|
|
$this->putSession($key, $sortDirection);
|
|
}
|
|
|
|
/**
|
|
* Gets the user sort direction from the session state
|
|
*/
|
|
protected function getSortDirection(): string
|
|
{
|
|
$key = 'media_sort_direction';
|
|
return $this->getSession($key, $this->getUserPreference($key, MediaLibrary::SORT_DIRECTION_ASC));
|
|
}
|
|
|
|
/**
|
|
* Gets the user selection parameters from the session state
|
|
*/
|
|
protected function getSelectionParams(): array
|
|
{
|
|
$result = $this->getSession('media_crop_selection_params');
|
|
|
|
if ($result) {
|
|
if (!isset($result['mode'])) {
|
|
$result['mode'] = self::SELECTION_MODE_NORMAL;
|
|
}
|
|
|
|
if (!isset($result['width'])) {
|
|
$result['width'] = null;
|
|
}
|
|
|
|
if (!isset($result['height'])) {
|
|
$result['height'] = null;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
return [
|
|
'mode' => self::SELECTION_MODE_NORMAL,
|
|
'width' => null,
|
|
'height' => null
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Stores the user selection parameters in the session state
|
|
*
|
|
* @param string $selectionMode
|
|
* @param int $selectionWidth
|
|
* @param int $selectionHeight
|
|
*/
|
|
protected function setSelectionParams($selectionMode, $selectionWidth, $selectionHeight): void
|
|
{
|
|
if (!in_array($selectionMode, [
|
|
self::SELECTION_MODE_NORMAL,
|
|
self::SELECTION_MODE_FIXED_RATIO,
|
|
self::SELECTION_MODE_FIXED_SIZE
|
|
])) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
if (strlen($selectionWidth) && !ctype_digit($selectionWidth)) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
if (strlen($selectionHeight) && !ctype_digit($selectionHeight)) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
$this->putSession('media_crop_selection_params', [
|
|
'mode' => $selectionMode,
|
|
'width' => $selectionWidth,
|
|
'height' => $selectionHeight
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Sets the sidebar visible state
|
|
*/
|
|
protected function setSidebarVisible(bool $visible): void
|
|
{
|
|
$this->putSession('sidebar_visible', $visible);
|
|
}
|
|
|
|
/**
|
|
* Checks if the sidebar is visible
|
|
*/
|
|
protected function getSidebarVisible(): bool
|
|
{
|
|
return $this->getSession('sidebar_visible', true);
|
|
}
|
|
|
|
/**
|
|
* Returns an icon for the item type
|
|
*/
|
|
protected function itemTypeToIconClass(?MediaLibraryItem $item, ?string $itemType): string
|
|
{
|
|
if ($item->type == MediaLibraryItem::TYPE_FOLDER) {
|
|
return 'icon-folder';
|
|
}
|
|
|
|
switch ($itemType) {
|
|
case MediaLibraryItem::FILE_TYPE_IMAGE:
|
|
return "icon-picture-o";
|
|
case MediaLibraryItem::FILE_TYPE_VIDEO:
|
|
return "icon-video-camera";
|
|
case MediaLibraryItem::FILE_TYPE_AUDIO:
|
|
return "icon-volume-up";
|
|
default:
|
|
return "icon-file";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Splits a path in to segments
|
|
*
|
|
* @param string $path
|
|
*/
|
|
protected function splitPathToSegments($path): array
|
|
{
|
|
$path = MediaLibrary::validatePath($path, true);
|
|
$path = explode('/', ltrim($path, '/'));
|
|
|
|
$result = [];
|
|
while (count($path) > 0) {
|
|
$folder = array_pop($path);
|
|
|
|
$result[$folder] = implode('/', $path).'/'.$folder;
|
|
if (substr($result[$folder], 0, 1) != '/') {
|
|
$result[$folder] = '/'.$result[$folder];
|
|
}
|
|
}
|
|
|
|
return array_reverse($result, true);
|
|
}
|
|
|
|
/**
|
|
* Stores a view mode in the session
|
|
*
|
|
* @throws ApplicationException if $viewMode is not a valid VIEW_MODE_* constant
|
|
*/
|
|
protected function setViewMode(string $viewMode): void
|
|
{
|
|
if (!in_array($viewMode, [
|
|
self::VIEW_MODE_GRID,
|
|
self::VIEW_MODE_LIST,
|
|
self::VIEW_MODE_TILES
|
|
])) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
$key = 'view_mode';
|
|
$this->putUserPreference($key, $viewMode);
|
|
$this->putSession($key, $viewMode);
|
|
}
|
|
|
|
/**
|
|
* Returns the current view mode stored in the session
|
|
*/
|
|
protected function getViewMode(): string
|
|
{
|
|
$key = 'view_mode';
|
|
return $this->getSession($key, $this->getUserPreference($key, self::VIEW_MODE_GRID));
|
|
}
|
|
|
|
/**
|
|
* Returns thumbnail parameters
|
|
*/
|
|
protected function getThumbnailParams(?string $viewMode = null): array
|
|
{
|
|
$result = [
|
|
'mode' => 'crop'
|
|
];
|
|
|
|
if (!$viewMode) {
|
|
return $result;
|
|
}
|
|
|
|
if ($viewMode === self::VIEW_MODE_LIST) {
|
|
return array_merge($result, [
|
|
'width' => 75,
|
|
'height' => 75
|
|
]);
|
|
}
|
|
|
|
return array_merge($result, [
|
|
'width' => 165,
|
|
'height' => 165
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get placeholder identifier
|
|
*
|
|
* @param System\Classes\MediaLibraryItem $item
|
|
* @return string
|
|
*/
|
|
protected function getPlaceholderId($item)
|
|
{
|
|
return 'placeholder'.md5($item->path.'-'.$item->lastModified.uniqid(microtime()));
|
|
}
|
|
|
|
/**
|
|
* Generate thumbnail
|
|
*
|
|
* @param array $thumbnailInfo
|
|
* @param array|null $thumbnailParams
|
|
*/
|
|
protected function generateThumbnail($thumbnailInfo, $thumbnailParams = null): array
|
|
{
|
|
$markup = null;
|
|
|
|
$path = $thumbnailInfo['path'];
|
|
|
|
if ($this->isVector($path) && ($id = $thumbnailInfo['id'])) {
|
|
return [
|
|
'id' => $id,
|
|
'markup' => $this->makePartial('thumbnail-image', [
|
|
'imageUrl' => MediaLibrary::url($thumbnailInfo['path']),
|
|
]),
|
|
];
|
|
}
|
|
|
|
try {
|
|
/*
|
|
* Get and validate input data
|
|
*/
|
|
$width = $thumbnailInfo['width'];
|
|
$height = $thumbnailInfo['height'];
|
|
$lastModified = $thumbnailInfo['lastModified'];
|
|
|
|
if (!is_numeric($width) || !is_numeric($height) || !is_numeric($lastModified)) {
|
|
throw new ApplicationException('Invalid input data');
|
|
}
|
|
|
|
if (!$thumbnailParams) {
|
|
$thumbnailParams = $this->getThumbnailParams();
|
|
$thumbnailParams['width'] = $width;
|
|
$thumbnailParams['height'] = $height;
|
|
}
|
|
|
|
/*
|
|
* Resize the thumbnail and save to the thumbnails directory
|
|
*/
|
|
$thumbnailUrl = $this->getResizedImageUrl($path, $thumbnailParams);
|
|
|
|
/*
|
|
* Delete the temporary file
|
|
*/
|
|
$markup = $this->makePartial('thumbnail-image', [
|
|
'imageUrl' => $thumbnailUrl,
|
|
]);
|
|
} catch (\Throwable $ex) {
|
|
$markup = $this->makePartial('thumbnail-image', [
|
|
'imageUrl' => false,
|
|
]);
|
|
|
|
traceLog($ex->getMessage());
|
|
}
|
|
|
|
if ($markup && ($id = $thumbnailInfo['id'])) {
|
|
return [
|
|
'id' => $id,
|
|
'markup' => $markup,
|
|
];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Get the URL to the resized image based on the provided path and parameters
|
|
*/
|
|
protected function getResizedImageUrl(string $path, array $params): string
|
|
{
|
|
return ImageResizer::filterGetUrl(
|
|
MediaLibrary::url($path),
|
|
$params['width'],
|
|
$params['height'],
|
|
array_merge(
|
|
['mode' => 'exact'],
|
|
$params
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Process the provided path and add a suffix of _$int to prevent conflicts
|
|
* with existing paths
|
|
*
|
|
* @todo Consider moving this into the File helper and accepting a $disk instance
|
|
*/
|
|
protected function deduplicatePath(string $path, ?string $suffix = null): string
|
|
{
|
|
$parts = pathinfo($path);
|
|
$i = 1;
|
|
|
|
// Path generation adds a DIRECTORY_SEPARATOR between the dirname and
|
|
// the filename so ensure that the dirname doesn't already end with one
|
|
$parts['dirname'] = rtrim($parts['dirname'], DIRECTORY_SEPARATOR);
|
|
|
|
// Apply the requested suffix to the path
|
|
if (!empty($suffix)) {
|
|
$parts['filename'] = preg_replace(
|
|
// Remove the suffix if it's already there before re-adding it
|
|
'/' . preg_quote($suffix, '/') . '(_\d)?/',
|
|
'',
|
|
$parts['filename']
|
|
) . $suffix;
|
|
|
|
// Regenerate the path so that it can be checked for existance
|
|
$path = sprintf(
|
|
'%s%s%s.%s',
|
|
$parts['dirname'],
|
|
DIRECTORY_SEPARATOR,
|
|
$parts['filename'],
|
|
$parts['extension']
|
|
);
|
|
}
|
|
|
|
while (MediaLibrary::instance()->exists($path)) {
|
|
$path = sprintf(
|
|
'%s%s%s_%d.%s',
|
|
$parts['dirname'],
|
|
DIRECTORY_SEPARATOR,
|
|
$parts['filename'],
|
|
$i++,
|
|
$parts['extension']
|
|
);
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Detect if image is vector graphic (SVG)
|
|
*/
|
|
protected function isVector(string $path): bool
|
|
{
|
|
return (pathinfo($path, PATHINFO_EXTENSION) == 'svg');
|
|
}
|
|
|
|
/**
|
|
* Returns a unique identifier for this widget and controller action for preference storage.
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function getPreferenceKey()
|
|
{
|
|
// User preferences should persist across controller usages for the MediaManager
|
|
return "backend::widgets.media_manager." . strtolower($this->getId());
|
|
}
|
|
}
|