1321 lines
40 KiB
PHP
1321 lines
40 KiB
PHP
<?php
|
|
|
|
namespace SilverStripe\Assets;
|
|
|
|
use SilverStripe\ORM\CMSPreviewable;
|
|
use SilverStripe\Assets\Storage\AssetNameGenerator;
|
|
use SilverStripe\Assets\Storage\DBFile;
|
|
use SilverStripe\Assets\Storage\AssetContainer;
|
|
use SilverStripe\CMS\Model\SiteTree;
|
|
use SilverStripe\Core\Convert;
|
|
use SilverStripe\Control\Director;
|
|
use SilverStripe\Core\Injector\Injector;
|
|
use SilverStripe\Dev\Deprecation;
|
|
use SilverStripe\Forms\DatetimeField;
|
|
use SilverStripe\Forms\FieldList;
|
|
use SilverStripe\Forms\HeaderField;
|
|
use SilverStripe\Forms\HiddenField;
|
|
use SilverStripe\Forms\Tab;
|
|
use SilverStripe\Forms\TabSet;
|
|
use SilverStripe\Forms\LiteralField;
|
|
use SilverStripe\Forms\ReadonlyField;
|
|
use SilverStripe\Forms\TextField;
|
|
use SilverStripe\ORM\DataObject;
|
|
use SilverStripe\ORM\DB;
|
|
use SilverStripe\ORM\Hierarchy\Hierarchy;
|
|
use SilverStripe\ORM\ValidationResult;
|
|
use SilverStripe\ORM\Versioning\Versioned;
|
|
use SilverStripe\Security\Member;
|
|
use SilverStripe\Security\Permission;
|
|
use SilverStripe\View\Parsers\ShortcodeHandler;
|
|
use SilverStripe\View\Parsers\ShortcodeParser;
|
|
use InvalidArgumentException;
|
|
|
|
/**
|
|
* This class handles the representation of a file on the filesystem within the framework.
|
|
* Most of the methods also handle the {@link Folder} subclass.
|
|
*
|
|
* Note: The files are stored in the assets/ directory, but SilverStripe
|
|
* looks at the db object to gather information about a file such as URL
|
|
* It then uses this for all processing functions (like image manipulation).
|
|
*
|
|
* <b>Security</b>
|
|
*
|
|
* Caution: It is recommended to disable any script execution in the "assets/"
|
|
* directory in the webserver configuration, to reduce the risk of exploits.
|
|
* See http://doc.silverstripe.org/secure-development#filesystem
|
|
*
|
|
* <b>Asset storage</b>
|
|
*
|
|
* As asset storage is configured separately to any File DataObject records, this class
|
|
* does not make any assumptions about how these records are saved. They could be on
|
|
* a local filesystem, remote filesystem, or a virtual record container (such as in local memory).
|
|
*
|
|
* The File dataobject simply represents an externally facing view of shared resources
|
|
* within this asset store.
|
|
*
|
|
* Internally individual files are referenced by a"Filename" parameter, which represents a File, extension,
|
|
* and is optionally prefixed by a list of custom directories. This path is root-agnostic, so it does not
|
|
* automatically have a direct url mapping (even to the site's base directory).
|
|
*
|
|
* Additionally, individual files may have several versions distinguished by sha1 hash,
|
|
* of which a File DataObject can point to a single one. Files can also be distinguished by
|
|
* variants, which may be resized images or format-shifted documents.
|
|
*
|
|
* <b>Properties</b>
|
|
*
|
|
* - "Title": Optional title of the file (for display purposes only).
|
|
* Defaults to "Name". Note that the Title field of Folder (subclass of File)
|
|
* is linked to Name, so Name and Title will always be the same.
|
|
* -"File": Physical asset backing this DB record. This is a composite DB field with
|
|
* its own list of properties. {@see DBFile} for more information
|
|
* - "Content": Typically unused, but handy for a textual representation of
|
|
* files, e.g. for fulltext indexing of PDF documents.
|
|
* - "ParentID": Points to a {@link Folder} record. Should be in sync with
|
|
* "Filename". A ParentID=0 value points to the "assets/" folder, not the webroot.
|
|
* -"ShowInSearch": True if this file is searchable
|
|
*
|
|
* @property string $Name Basename of the file
|
|
* @property string $Title Title of the file
|
|
* @property DBFile $File asset stored behind this File record
|
|
* @property string $Content
|
|
* @property string $ShowInSearch Boolean that indicates if file is shown in search. Doesn't apply to Folders
|
|
* @property int $ParentID ID of parent File/Folder
|
|
* @property int $OwnerID ID of Member who owns the file
|
|
*
|
|
* @method File Parent() Returns parent File
|
|
* @method Member Owner() Returns Member object of file owner.
|
|
*
|
|
* @mixin Hierarchy
|
|
* @mixin Versioned
|
|
*/
|
|
class File extends DataObject implements ShortcodeHandler, AssetContainer, Thumbnail, CMSPreviewable
|
|
{
|
|
|
|
use ImageManipulation;
|
|
|
|
private static $default_sort = "\"Name\"";
|
|
|
|
/**
|
|
* @config
|
|
* @var string
|
|
*/
|
|
private static $singular_name = "File";
|
|
|
|
private static $plural_name = "Files";
|
|
|
|
/**
|
|
* Permissions necessary to view files outside of the live stage (e.g. archive / draft stage).
|
|
*
|
|
* @config
|
|
* @var array
|
|
*/
|
|
private static $non_live_permissions = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_AssetAdmin', 'VIEW_DRAFT_CONTENT');
|
|
|
|
private static $db = array(
|
|
"Name" => "Varchar(255)",
|
|
"Title" => "Varchar(255)",
|
|
"File" => "DBFile",
|
|
// Only applies to files, doesn't inherit for folder
|
|
'ShowInSearch' => 'Boolean(1)',
|
|
);
|
|
|
|
private static $has_one = array(
|
|
"Parent" => "SilverStripe\\Assets\\File",
|
|
"Owner" => "SilverStripe\\Security\\Member"
|
|
);
|
|
|
|
private static $defaults = array(
|
|
"ShowInSearch" => 1,
|
|
);
|
|
|
|
private static $extensions = array(
|
|
"SilverStripe\\ORM\\Hierarchy\\Hierarchy",
|
|
"SilverStripe\\ORM\\Versioning\\Versioned"
|
|
);
|
|
|
|
private static $casting = array (
|
|
'TreeTitle' => 'HTMLFragment'
|
|
);
|
|
|
|
private static $table_name = 'File';
|
|
|
|
/**
|
|
* @config
|
|
* @var array List of allowed file extensions, enforced through {@link validate()}.
|
|
*
|
|
* Note: if you modify this, you should also change a configuration file in the assets directory.
|
|
* Otherwise, the files will be able to be uploaded but they won't be able to be served by the
|
|
* webserver.
|
|
*
|
|
* - If you are running Apache you will need to change assets/.htaccess
|
|
* - If you are running IIS you will need to change assets/web.config
|
|
*
|
|
* Instructions for the change you need to make are included in a comment in the config file.
|
|
*/
|
|
private static $allowed_extensions = array(
|
|
'', 'ace', 'arc', 'arj', 'asf', 'au', 'avi', 'bmp', 'bz2', 'cab', 'cda', 'css', 'csv', 'dmg', 'doc',
|
|
'docx', 'dotx', 'dotm', 'flv', 'gif', 'gpx', 'gz', 'hqx', 'ico', 'jar', 'jpeg', 'jpg', 'js', 'kml',
|
|
'm4a', 'm4v', 'mid', 'midi', 'mkv', 'mov', 'mp3', 'mp4', 'mpa', 'mpeg', 'mpg', 'ogg', 'ogv', 'pages',
|
|
'pcx', 'pdf', 'png', 'pps', 'ppt', 'pptx', 'potx', 'potm', 'ra', 'ram', 'rm', 'rtf', 'sit', 'sitx',
|
|
'tar', 'tgz', 'tif', 'tiff', 'txt', 'wav', 'webm', 'wma', 'wmv', 'xls', 'xlsx', 'xltx', 'xltm', 'zip',
|
|
'zipx',
|
|
);
|
|
|
|
/**
|
|
* @config
|
|
* @var array Category identifiers mapped to commonly used extensions.
|
|
*/
|
|
private static $app_categories = array(
|
|
'archive' => array(
|
|
'ace', 'arc', 'arj', 'bz', 'bz2', 'cab', 'dmg', 'gz', 'hqx', 'jar', 'rar', 'sit', 'sitx', 'tar', 'tgz',
|
|
'zip', 'zipx',
|
|
),
|
|
'audio' => array(
|
|
'aif', 'aifc', 'aiff', 'apl', 'au', 'avr', 'cda', 'm4a', 'mid', 'midi', 'mp3', 'ogg', 'ra',
|
|
'ram', 'rm', 'snd', 'wav', 'wma',
|
|
),
|
|
'document' => array(
|
|
'css', 'csv', 'doc', 'docx', 'dotm', 'dotx', 'htm', 'html', 'gpx', 'js', 'kml', 'pages', 'pdf',
|
|
'potm', 'potx', 'pps', 'ppt', 'pptx', 'rtf', 'txt', 'xhtml', 'xls', 'xlsx', 'xltm', 'xltx', 'xml',
|
|
),
|
|
'image' => array(
|
|
'alpha', 'als', 'bmp', 'cel', 'gif', 'ico', 'icon', 'jpeg', 'jpg', 'pcx', 'png', 'ps', 'tif', 'tiff',
|
|
),
|
|
'image/supported' => array(
|
|
'gif', 'jpeg', 'jpg', 'png'
|
|
),
|
|
'flash' => array(
|
|
'fla', 'swf'
|
|
),
|
|
'video' => array(
|
|
'asf', 'avi', 'flv', 'ifo', 'm1v', 'm2v', 'm4v', 'mkv', 'mov', 'mp2', 'mp4', 'mpa', 'mpe', 'mpeg',
|
|
'mpg', 'ogv', 'qt', 'vob', 'webm', 'wmv',
|
|
),
|
|
);
|
|
|
|
/**
|
|
* Map of file extensions to class type
|
|
*
|
|
* @config
|
|
* @var
|
|
*/
|
|
private static $class_for_file_extension = array(
|
|
'*' => 'SilverStripe\\Assets\\File',
|
|
'jpg' => 'SilverStripe\\Assets\\Image',
|
|
'jpeg' => 'SilverStripe\\Assets\\Image',
|
|
'png' => 'SilverStripe\\Assets\\Image',
|
|
'gif' => 'SilverStripe\\Assets\\Image',
|
|
);
|
|
|
|
/**
|
|
* @config
|
|
* @var bool If this is true, then restrictions set in {@link $allowed_max_file_size} and
|
|
* {@link $allowed_extensions} will be applied to users with admin privileges as
|
|
* well.
|
|
*/
|
|
private static $apply_restrictions_to_admin = true;
|
|
|
|
/**
|
|
* If enabled, legacy file dataobjects will be automatically imported into the APL
|
|
*
|
|
* @config
|
|
* @var bool
|
|
*/
|
|
private static $migrate_legacy_file = false;
|
|
|
|
/**
|
|
* @config
|
|
* @var boolean
|
|
*/
|
|
private static $update_filesystem = true;
|
|
|
|
public static function get_shortcodes()
|
|
{
|
|
return 'file_link';
|
|
}
|
|
|
|
/**
|
|
* Replace "[file_link id=n]" shortcode with an anchor tag or link to the file.
|
|
*
|
|
* @param array $arguments Arguments passed to the parser
|
|
* @param string $content Raw shortcode
|
|
* @param ShortcodeParser $parser Parser
|
|
* @param string $shortcode Name of shortcode used to register this handler
|
|
* @param array $extra Extra arguments
|
|
* @return string Result of the handled shortcode
|
|
*/
|
|
public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = array())
|
|
{
|
|
// Find appropriate record, with fallback for error handlers
|
|
$record = static::find_shortcode_record($arguments, $errorCode);
|
|
if ($errorCode) {
|
|
$record = static::find_error_record($errorCode);
|
|
}
|
|
if (!$record) {
|
|
return null; // There were no suitable matches at all.
|
|
}
|
|
|
|
// build the HTML tag
|
|
if ($content) {
|
|
// build some useful meta-data (file type and size) as data attributes
|
|
$attrs = ' ';
|
|
if ($record instanceof File) {
|
|
foreach (array(
|
|
'class' => 'file',
|
|
'data-type' => $record->getExtension(),
|
|
'data-size' => $record->getSize()
|
|
) as $name => $value) {
|
|
$attrs .= sprintf('%s="%s" ', $name, $value);
|
|
}
|
|
}
|
|
|
|
return sprintf('<a href="%s"%s>%s</a>', $record->Link(), rtrim($attrs), $parser->parse($content));
|
|
} else {
|
|
return $record->Link();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find the record to use for a given shortcode.
|
|
*
|
|
* @param array $args Array of input shortcode arguments
|
|
* @param int $errorCode If the file is not found, or is inaccessible, this will be assigned to a HTTP error code.
|
|
* @return File|null The File DataObject, if it can be found.
|
|
*/
|
|
public static function find_shortcode_record($args, &$errorCode = null)
|
|
{
|
|
// Validate shortcode
|
|
if (!isset($args['id']) || !is_numeric($args['id'])) {
|
|
return null;
|
|
}
|
|
|
|
// Check if the file is found
|
|
/** @var File $file */
|
|
$file = File::get()->byID($args['id']);
|
|
if (!$file) {
|
|
$errorCode = 404;
|
|
return null;
|
|
}
|
|
|
|
// Check if the file is viewable
|
|
if (!$file->canView()) {
|
|
$errorCode = 403;
|
|
return null;
|
|
}
|
|
|
|
// Success
|
|
return $file;
|
|
}
|
|
|
|
/**
|
|
* Given a HTTP Error, find an appropriate substitute File or SiteTree data object instance.
|
|
*
|
|
* @param int $errorCode HTTP Error value
|
|
* @return File|SiteTree File or SiteTree object to use for the given error
|
|
*/
|
|
protected static function find_error_record($errorCode)
|
|
{
|
|
$result = static::singleton()->invokeWithExtensions('getErrorRecordFor', $errorCode);
|
|
$result = array_filter($result);
|
|
if ($result) {
|
|
return reset($result);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* A file only exists if the file_exists() and is in the DB as a record
|
|
*
|
|
* Use $file->isInDB() to only check for a DB record
|
|
* Use $file->File->exists() to only check if the asset exists
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function exists()
|
|
{
|
|
return parent::exists() && $this->File->exists();
|
|
}
|
|
|
|
/**
|
|
* Find a File object by the given filename.
|
|
*
|
|
* @param string $filename Filename to search for, including any custom parent directories.
|
|
* @return File
|
|
*/
|
|
public static function find($filename)
|
|
{
|
|
// Split to folders and the actual filename, and traverse the structure.
|
|
$parts = explode("/", $filename);
|
|
$parentID = 0;
|
|
/** @var File $item */
|
|
$item = null;
|
|
foreach ($parts as $part) {
|
|
$item = File::get()->filter(array(
|
|
'Name' => $part,
|
|
'ParentID' => $parentID
|
|
))->first();
|
|
if (!$item) {
|
|
break;
|
|
}
|
|
$parentID = $item->ID;
|
|
}
|
|
|
|
return $item;
|
|
}
|
|
|
|
/**
|
|
* Just an alias function to keep a consistent API with SiteTree
|
|
*
|
|
* @return string The link to the file
|
|
*/
|
|
public function Link()
|
|
{
|
|
return $this->getURL();
|
|
}
|
|
|
|
/**
|
|
* @deprecated 4.0
|
|
*/
|
|
public function RelativeLink()
|
|
{
|
|
Deprecation::notice('4.0', 'Use getURL instead, as not all files will be relative to the site root.');
|
|
return Director::makeRelative($this->getURL());
|
|
}
|
|
|
|
/**
|
|
* Just an alias function to keep a consistent API with SiteTree
|
|
*
|
|
* @return string The absolute link to the file
|
|
*/
|
|
public function AbsoluteLink()
|
|
{
|
|
return $this->getAbsoluteURL();
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getTreeTitle()
|
|
{
|
|
return Convert::raw2xml($this->Title);
|
|
}
|
|
|
|
/**
|
|
* @param Member $member
|
|
* @return bool
|
|
*/
|
|
public function canView($member = null)
|
|
{
|
|
if (!$member) {
|
|
$member = Member::currentUser();
|
|
}
|
|
|
|
$result = $this->extendedCan('canView', $member);
|
|
if ($result !== null) {
|
|
return $result;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if this file can be modified
|
|
*
|
|
* @param Member $member
|
|
* @return boolean
|
|
*/
|
|
public function canEdit($member = null)
|
|
{
|
|
if (!$member) {
|
|
$member = Member::currentUser();
|
|
}
|
|
|
|
$result = $this->extendedCan('canEdit', $member);
|
|
if ($result !== null) {
|
|
return $result;
|
|
}
|
|
|
|
return Permission::checkMember($member, array('CMS_ACCESS_AssetAdmin', 'CMS_ACCESS_LeftAndMain'));
|
|
}
|
|
|
|
/**
|
|
* Check if a file can be created
|
|
*
|
|
* @param Member $member
|
|
* @param array $context
|
|
* @return boolean
|
|
*/
|
|
public function canCreate($member = null, $context = array())
|
|
{
|
|
if (!$member) {
|
|
$member = Member::currentUser();
|
|
}
|
|
|
|
$result = $this->extendedCan('canCreate', $member, $context);
|
|
if ($result !== null) {
|
|
return $result;
|
|
}
|
|
|
|
return $this->canEdit($member);
|
|
}
|
|
|
|
/**
|
|
* Check if this file can be deleted
|
|
*
|
|
* @param Member $member
|
|
* @return boolean
|
|
*/
|
|
public function canDelete($member = null)
|
|
{
|
|
if (!$member) {
|
|
$member = Member::currentUser();
|
|
}
|
|
|
|
$result = $this->extendedCan('canDelete', $member);
|
|
if ($result !== null) {
|
|
return $result;
|
|
}
|
|
|
|
return $this->canEdit($member);
|
|
}
|
|
|
|
/**
|
|
* Returns the fields to power the edit screen of files in the CMS.
|
|
* You can modify this FieldList by subclassing folder, or by creating a {@link DataExtension}
|
|
* and implementing updateCMSFields(FieldList $fields) on that extension.
|
|
*
|
|
* @return FieldList
|
|
*/
|
|
public function getCMSFields()
|
|
{
|
|
$path = '/' . dirname($this->getFilename());
|
|
|
|
$previewLink = Convert::raw2att($this->PreviewLink());
|
|
$image = "<img src=\"{$previewLink}\" class=\"editor__thumbnail\" />";
|
|
|
|
$statusTitle = $this->getStatusTitle();
|
|
$statusFlag = ($statusTitle) ? "<span class=\"editor__status-flag\">{$statusTitle}</span>" : '';
|
|
|
|
$content = Tab::create(
|
|
'Main',
|
|
HeaderField::create('TitleHeader', $this->Title, 1)
|
|
->addExtraClass('editor__heading'),
|
|
LiteralField::create('StatusFlag', $statusFlag),
|
|
LiteralField::create("IconFull", $image)
|
|
->addExtraClass('editor__file-preview'),
|
|
TabSet::create(
|
|
'Editor',
|
|
Tab::create(
|
|
'Details',
|
|
TextField::create("Title", $this->fieldLabel('Title')),
|
|
TextField::create("Name", $this->fieldLabel('Filename')),
|
|
ReadonlyField::create(
|
|
"Path",
|
|
_t('AssetTableField.PATH', 'Path'),
|
|
(($path !== '/.') ? $path : '') . '/'
|
|
)
|
|
),
|
|
Tab::create(
|
|
'Usage',
|
|
DatetimeField::create(
|
|
"Created",
|
|
_t('AssetTableField.CREATED', 'First uploaded')
|
|
)->setReadonly(true),
|
|
DatetimeField::create(
|
|
"LastEdited",
|
|
_t('AssetTableField.LASTEDIT', 'Last changed')
|
|
)->setReadonly(true)
|
|
)
|
|
),
|
|
HiddenField::create('ID', $this->ID)
|
|
);
|
|
|
|
$fields = FieldList::create(TabSet::create('Root', $content));
|
|
|
|
$this->extend('updateCMSFields', $fields);
|
|
|
|
return $fields;
|
|
}
|
|
|
|
/**
|
|
* Get title for current file status
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getStatusTitle()
|
|
{
|
|
$statusTitle = '';
|
|
if ($this->isOnDraftOnly()) {
|
|
$statusTitle = _t('File.DRAFT', 'Draft');
|
|
} elseif ($this->isModifiedOnDraft()) {
|
|
$statusTitle = _t('File.MODIFIED', 'Modified');
|
|
}
|
|
return $statusTitle;
|
|
}
|
|
|
|
/**
|
|
* Returns a category based on the file extension.
|
|
* This can be useful when grouping files by type,
|
|
* showing icons on filelinks, etc.
|
|
* Possible group values are: "audio", "mov", "zip", "image".
|
|
*
|
|
* @param string $ext Extension to check
|
|
* @return string
|
|
*/
|
|
public static function get_app_category($ext)
|
|
{
|
|
$ext = strtolower($ext);
|
|
foreach (static::config()->app_categories as $category => $exts) {
|
|
if (in_array($ext, $exts)) {
|
|
return $category;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* For a category or list of categories, get the list of file extensions
|
|
*
|
|
* @param array|string $categories List of categories, or single category
|
|
* @return array
|
|
*/
|
|
public static function get_category_extensions($categories)
|
|
{
|
|
if (empty($categories)) {
|
|
return array();
|
|
}
|
|
|
|
// Fix arguments into a single array
|
|
if (!is_array($categories)) {
|
|
$categories = array($categories);
|
|
} elseif (count($categories) === 1 && is_array(reset($categories))) {
|
|
$categories = reset($categories);
|
|
}
|
|
|
|
// Check configured categories
|
|
$appCategories = self::config()->app_categories;
|
|
|
|
// Merge all categories into list of extensions
|
|
$extensions = array();
|
|
foreach (array_filter($categories) as $category) {
|
|
if (isset($appCategories[$category])) {
|
|
$extensions = array_merge($extensions, $appCategories[$category]);
|
|
} else {
|
|
throw new InvalidArgumentException("Unknown file category: $category");
|
|
}
|
|
}
|
|
$extensions = array_unique($extensions);
|
|
sort($extensions);
|
|
return $extensions;
|
|
}
|
|
|
|
/**
|
|
* Returns a category based on the file extension.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function appCategory()
|
|
{
|
|
return self::get_app_category($this->getExtension());
|
|
}
|
|
|
|
|
|
/**
|
|
* Should be called after the file was uploaded
|
|
*/
|
|
public function onAfterUpload()
|
|
{
|
|
$this->extend('onAfterUpload');
|
|
}
|
|
|
|
/**
|
|
* Make sure the file has a name
|
|
*/
|
|
protected function onBeforeWrite()
|
|
{
|
|
// Set default owner
|
|
if (!$this->isInDB() && !$this->OwnerID) {
|
|
$this->OwnerID = Member::currentUserID();
|
|
}
|
|
|
|
$name = $this->getField('Name');
|
|
$title = $this->getField('Title');
|
|
|
|
$changed = $this->isChanged('Name');
|
|
|
|
// Name can't be blank, default to Title
|
|
if (!$name) {
|
|
$changed = true;
|
|
$name = $title;
|
|
}
|
|
|
|
$filter = FileNameFilter::create();
|
|
if ($name) {
|
|
// Fix illegal characters
|
|
$name = $filter->filter($name);
|
|
} else {
|
|
// Default to file name
|
|
$changed = true;
|
|
$name = $this->i18n_singular_name();
|
|
$name = $filter->filter($name);
|
|
}
|
|
|
|
// Check for duplicates when the name has changed (or is set for the first time)
|
|
if ($changed) {
|
|
$nameGenerator = $this->getNameGenerator($name);
|
|
// Defaults to returning the original filename on first iteration
|
|
foreach ($nameGenerator as $newName) {
|
|
// This logic is also used in the Folder subclass, but we're querying
|
|
// for duplicates on the File base class here (including the Folder subclass).
|
|
|
|
// TODO Add read lock to avoid other processes creating files with the same name
|
|
// before this process has a chance to persist in the database.
|
|
$existingFile = File::get()->filter(array(
|
|
'Name' => $newName,
|
|
'ParentID' => (int) $this->ParentID
|
|
))->exclude(array(
|
|
'ID' => $this->ID
|
|
))->first();
|
|
if (!$existingFile) {
|
|
$name = $newName;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update actual field value
|
|
$this->setField('Name', $name);
|
|
|
|
// Update title
|
|
if (!$title) {
|
|
// Generate a readable title, dashes and underscores replaced by whitespace,
|
|
// and any file extensions removed.
|
|
$this->setField(
|
|
'Title',
|
|
str_replace(array('-','_'), ' ', preg_replace('/\.[^.]+$/', '', $name))
|
|
);
|
|
}
|
|
|
|
// Propagate changes to the AssetStore and update the DBFile field
|
|
$this->updateFilesystem();
|
|
|
|
parent::onBeforeWrite();
|
|
}
|
|
|
|
/**
|
|
* This will check if the parent record and/or name do not match the name on the underlying
|
|
* DBFile record, and if so, copy this file to the new location, and update the record to
|
|
* point to this new file.
|
|
*
|
|
* This method will update the File {@see DBFile} field value on success, so it must be called
|
|
* before writing to the database
|
|
*
|
|
* @return bool True if changed
|
|
*/
|
|
public function updateFilesystem()
|
|
{
|
|
if (!$this->config()->update_filesystem) {
|
|
return false;
|
|
}
|
|
|
|
// Check the file exists
|
|
if (!$this->File->exists()) {
|
|
return false;
|
|
}
|
|
|
|
// Avoid moving files on live; Rely on this being done on stage prior to publish.
|
|
if (Versioned::get_stage() !== Versioned::DRAFT) {
|
|
return false;
|
|
}
|
|
|
|
// Check path updated record will point to
|
|
// If no changes necessary, skip
|
|
$pathBefore = $this->File->getFilename();
|
|
$pathAfter = $this->generateFilename();
|
|
if ($pathAfter === $pathBefore) {
|
|
return false;
|
|
}
|
|
|
|
// Copy record to new location via stream
|
|
$stream = $this->File->getStream();
|
|
$this->File->setFromStream($stream, $pathAfter);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Collate selected descendants of this page.
|
|
* $condition will be evaluated on each descendant, and if it is succeeds, that item will be added
|
|
* to the $collator array.
|
|
*
|
|
* @param string $condition The PHP condition to be evaluated. The page will be called $item
|
|
* @param array $collator An array, passed by reference, to collect all of the matching descendants.
|
|
* @return true|null
|
|
*/
|
|
public function collateDescendants($condition, &$collator)
|
|
{
|
|
if ($children = $this->Children()) {
|
|
foreach ($children as $item) {
|
|
/** @var File $item */
|
|
if (!$condition || eval("return $condition;")) {
|
|
$collator[] = $item;
|
|
}
|
|
$item->collateDescendants($condition, $collator);
|
|
}
|
|
return true;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get an asset renamer for the given filename.
|
|
*
|
|
* @param string $filename Path name
|
|
* @return AssetNameGenerator
|
|
*/
|
|
protected function getNameGenerator($filename)
|
|
{
|
|
return Injector::inst()->createWithArgs('AssetNameGenerator', array($filename));
|
|
}
|
|
|
|
/**
|
|
* Gets the URL of this file
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getAbsoluteURL()
|
|
{
|
|
$url = $this->getURL();
|
|
if ($url) {
|
|
return Director::absoluteURL($url);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Gets the URL of this file
|
|
*
|
|
* @uses Director::baseURL()
|
|
* @param bool $grant Ensures that the url for any protected assets is granted for the current user.
|
|
* @return string
|
|
*/
|
|
public function getURL($grant = true)
|
|
{
|
|
if ($this->File->exists()) {
|
|
return $this->File->getURL($grant);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get URL, but without resampling.
|
|
*
|
|
* @param bool $grant Ensures that the url for any protected assets is granted for the current user.
|
|
* @return string
|
|
*/
|
|
public function getSourceURL($grant = true)
|
|
{
|
|
if ($this->File->exists()) {
|
|
return $this->File->getSourceURL($grant);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get expected value of Filename tuple value. Will be used to trigger
|
|
* a file move on draft stage.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function generateFilename()
|
|
{
|
|
// Check if this file is nested within a folder
|
|
$parent = $this->Parent();
|
|
if ($parent && $parent->exists()) {
|
|
return $this->join_paths($parent->getFilename(), $this->Name);
|
|
}
|
|
return $this->Name;
|
|
}
|
|
|
|
/**
|
|
* Ensure that parent folders are published before this one is published
|
|
*
|
|
* @todo Solve this via triggered publishing / ownership in the future
|
|
*/
|
|
public function onBeforePublish()
|
|
{
|
|
// Publish all parents from the root up
|
|
/** @var Folder $parent */
|
|
foreach ($this->getAncestors()->reverse() as $parent) {
|
|
$parent->publishSingle();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the ParentID and Name for the given filename.
|
|
*
|
|
* On save, the underlying DBFile record will move the underlying file to this location.
|
|
* Thus it will not update the underlying Filename value until this is done.
|
|
*
|
|
* @param string $filename
|
|
* @return $this
|
|
*/
|
|
public function setFilename($filename)
|
|
{
|
|
// Check existing folder path
|
|
$folder = '';
|
|
$parent = $this->Parent();
|
|
if ($parent && $parent->exists()) {
|
|
$folder = $parent->Filename;
|
|
}
|
|
|
|
// Detect change in foldername
|
|
$newFolder = ltrim(dirname(trim($filename, '/')), '.');
|
|
if ($folder !== $newFolder) {
|
|
if (!$newFolder) {
|
|
$this->ParentID = 0;
|
|
} else {
|
|
$parent = Folder::find_or_make($newFolder);
|
|
$this->ParentID = $parent->ID;
|
|
}
|
|
}
|
|
|
|
// Update base name
|
|
$this->Name = basename($filename);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Returns the file extension
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getExtension()
|
|
{
|
|
return self::get_file_extension($this->Name);
|
|
}
|
|
|
|
/**
|
|
* Gets the extension of a filepath or filename,
|
|
* by stripping away everything before the last "dot".
|
|
* Caution: Only returns the last extension in "double-barrelled"
|
|
* extensions (e.g. "gz" for "tar.gz").
|
|
*
|
|
* Examples:
|
|
* - "myfile" returns ""
|
|
* - "myfile.txt" returns "txt"
|
|
* - "myfile.tar.gz" returns "gz"
|
|
*
|
|
* @param string $filename
|
|
* @return string
|
|
*/
|
|
public static function get_file_extension($filename)
|
|
{
|
|
return pathinfo($filename, PATHINFO_EXTENSION);
|
|
}
|
|
|
|
/**
|
|
* Given an extension, determine the icon that should be used
|
|
*
|
|
* @param string $extension
|
|
* @return string Icon filename relative to base url
|
|
*/
|
|
public static function get_icon_for_extension($extension)
|
|
{
|
|
$extension = strtolower($extension);
|
|
|
|
// Check if exact extension has an icon
|
|
if (!file_exists(FRAMEWORK_PATH ."/client/images/app_icons/{$extension}_92.png")) {
|
|
$extension = static::get_app_category($extension);
|
|
|
|
// Fallback to category specific icon
|
|
if (!file_exists(FRAMEWORK_PATH ."/client/images/app_icons/{$extension}_92.png")) {
|
|
$extension ="generic";
|
|
}
|
|
}
|
|
|
|
return FRAMEWORK_DIR ."/client/images/app_icons/{$extension}_92.png";
|
|
}
|
|
|
|
/**
|
|
* Return the type of file for the given extension
|
|
* on the current file name.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getFileType()
|
|
{
|
|
return self::get_file_type($this->getFilename());
|
|
}
|
|
|
|
/**
|
|
* Get descriptive type of file based on filename
|
|
*
|
|
* @param string $filename
|
|
* @return string Description of file
|
|
*/
|
|
public static function get_file_type($filename)
|
|
{
|
|
$types = array(
|
|
'gif' => _t('File.GifType', 'GIF image - good for diagrams'),
|
|
'jpg' => _t('File.JpgType', 'JPEG image - good for photos'),
|
|
'jpeg' => _t('File.JpgType', 'JPEG image - good for photos'),
|
|
'png' => _t('File.PngType', 'PNG image - good general-purpose format'),
|
|
'ico' => _t('File.IcoType', 'Icon image'),
|
|
'tiff' => _t('File.TiffType', 'Tagged image format'),
|
|
'doc' => _t('File.DocType', 'Word document'),
|
|
'xls' => _t('File.XlsType', 'Excel spreadsheet'),
|
|
'zip' => _t('File.ZipType', 'ZIP compressed file'),
|
|
'gz' => _t('File.GzType', 'GZIP compressed file'),
|
|
'dmg' => _t('File.DmgType', 'Apple disk image'),
|
|
'pdf' => _t('File.PdfType', 'Adobe Acrobat PDF file'),
|
|
'mp3' => _t('File.Mp3Type', 'MP3 audio file'),
|
|
'wav' => _t('File.WavType', 'WAV audo file'),
|
|
'avi' => _t('File.AviType', 'AVI video file'),
|
|
'mpg' => _t('File.MpgType', 'MPEG video file'),
|
|
'mpeg' => _t('File.MpgType', 'MPEG video file'),
|
|
'js' => _t('File.JsType', 'Javascript file'),
|
|
'css' => _t('File.CssType', 'CSS file'),
|
|
'html' => _t('File.HtmlType', 'HTML file'),
|
|
'htm' => _t('File.HtmlType', 'HTML file')
|
|
);
|
|
|
|
// Get extension
|
|
$extension = strtolower(self::get_file_extension($filename));
|
|
return isset($types[$extension]) ? $types[$extension] : 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Returns the size of the file type in an appropriate format.
|
|
*
|
|
* @return string|false String value, or false if doesn't exist
|
|
*/
|
|
public function getSize()
|
|
{
|
|
$size = $this->getAbsoluteSize();
|
|
if ($size) {
|
|
return static::format_size($size);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Formats a file size (eg: (int)42 becomes string '42 bytes')
|
|
*
|
|
* @param int $size
|
|
* @return string
|
|
*/
|
|
public static function format_size($size)
|
|
{
|
|
if ($size < 1024) {
|
|
return $size . ' bytes';
|
|
}
|
|
if ($size < 1024*10) {
|
|
return (round($size/1024*10)/10). ' KB';
|
|
}
|
|
if ($size < 1024*1024) {
|
|
return round($size/1024) . ' KB';
|
|
}
|
|
if ($size < 1024*1024*10) {
|
|
return (round(($size/1024)/1024*10)/10) . ' MB';
|
|
}
|
|
if ($size < 1024*1024*1024) {
|
|
return round(($size/1024)/1024) . ' MB';
|
|
}
|
|
return round($size/(1024*1024*1024)*10)/10 . ' GB';
|
|
}
|
|
|
|
/**
|
|
* Convert a php.ini value (eg: 512M) to bytes
|
|
*
|
|
* @param string $iniValue
|
|
* @return int
|
|
*/
|
|
public static function ini2bytes($iniValue)
|
|
{
|
|
$iniValues = str_split(trim($iniValue));
|
|
$unit = strtolower(array_pop($iniValues));
|
|
$quantity = (int) implode($iniValues);
|
|
switch ($unit) {
|
|
case 'g':
|
|
$quantity *= 1024;
|
|
// deliberate no break
|
|
case 'm':
|
|
$quantity *= 1024;
|
|
// deliberate no break
|
|
case 'k':
|
|
$quantity *= 1024;
|
|
// deliberate no break
|
|
default:
|
|
// no-op: pre-existing behaviour
|
|
break;
|
|
}
|
|
return $quantity;
|
|
}
|
|
|
|
/**
|
|
* Return file size in bytes.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getAbsoluteSize()
|
|
{
|
|
return $this->File->getAbsoluteSize();
|
|
}
|
|
|
|
public function validate()
|
|
{
|
|
$result = ValidationResult::create();
|
|
$this->File->validate($result, $this->Name);
|
|
$this->extend('validate', $result);
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Maps a {@link File} subclass to a specific extension.
|
|
* By default, files with common image extensions will be created
|
|
* as {@link Image} instead of {@link File} when using
|
|
* {@link Folder::constructChild}, {@link Folder::addUploadToFolder}),
|
|
* and the {@link Upload} class (either directly or through {@link FileField}).
|
|
* For manually instanciated files please use this mapping getter.
|
|
*
|
|
* Caution: Changes to mapping doesn't apply to existing file records in the database.
|
|
* Also doesn't hook into {@link Object::getCustomClass()}.
|
|
*
|
|
* @param String File extension, without dot prefix. Use an asterisk ('*')
|
|
* to specify a generic fallback if no mapping is found for an extension.
|
|
* @return String Classname for a subclass of {@link File}
|
|
*/
|
|
public static function get_class_for_file_extension($ext)
|
|
{
|
|
$map = array_change_key_case(self::config()->class_for_file_extension, CASE_LOWER);
|
|
return (array_key_exists(strtolower($ext), $map)) ? $map[strtolower($ext)] : $map['*'];
|
|
}
|
|
|
|
/**
|
|
* See {@link get_class_for_file_extension()}.
|
|
*
|
|
* @param String|array
|
|
* @param String
|
|
*/
|
|
public static function set_class_for_file_extension($exts, $class)
|
|
{
|
|
if (!is_array($exts)) {
|
|
$exts = array($exts);
|
|
}
|
|
foreach ($exts as $ext) {
|
|
if (!is_subclass_of($class, 'SilverStripe\\Assets\\File')) {
|
|
throw new InvalidArgumentException(
|
|
sprintf('Class "%s" (for extension "%s") is not a valid subclass of File', $class, $ext)
|
|
);
|
|
}
|
|
self::config()->class_for_file_extension = array($ext => $class);
|
|
}
|
|
}
|
|
|
|
public function getMetaData()
|
|
{
|
|
if (!$this->File->exists()) {
|
|
return null;
|
|
}
|
|
return $this->File->getMetaData();
|
|
}
|
|
|
|
public function getMimeType()
|
|
{
|
|
if (!$this->File->exists()) {
|
|
return null;
|
|
}
|
|
return $this->File->getMimeType();
|
|
}
|
|
|
|
public function getStream()
|
|
{
|
|
if (!$this->File->exists()) {
|
|
return null;
|
|
}
|
|
return $this->File->getStream();
|
|
}
|
|
|
|
public function getString()
|
|
{
|
|
if (!$this->File->exists()) {
|
|
return null;
|
|
}
|
|
return $this->File->getString();
|
|
}
|
|
|
|
public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = array())
|
|
{
|
|
$result = $this->File->setFromLocalFile($path, $filename, $hash, $variant, $config);
|
|
|
|
// Update File record to name of the uploaded asset
|
|
if ($result) {
|
|
$this->setFilename($result['Filename']);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = array())
|
|
{
|
|
$result = $this->File->setFromStream($stream, $filename, $hash, $variant, $config);
|
|
|
|
// Update File record to name of the uploaded asset
|
|
if ($result) {
|
|
$this->setFilename($result['Filename']);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
public function setFromString($data, $filename, $hash = null, $variant = null, $config = array())
|
|
{
|
|
$result = $this->File->setFromString($data, $filename, $hash, $variant, $config);
|
|
|
|
// Update File record to name of the uploaded asset
|
|
if ($result) {
|
|
$this->setFilename($result['Filename']);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
public function getIsImage()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public function getFilename()
|
|
{
|
|
return $this->File->Filename;
|
|
}
|
|
|
|
public function getHash()
|
|
{
|
|
return $this->File->Hash;
|
|
}
|
|
|
|
public function getVariant()
|
|
{
|
|
return $this->File->Variant;
|
|
}
|
|
|
|
/**
|
|
* Return a html5 tag of the appropriate for this file (normally img or a)
|
|
*
|
|
* @return string
|
|
*/
|
|
public function forTemplate()
|
|
{
|
|
return $this->getTag() ?: '';
|
|
}
|
|
|
|
/**
|
|
* Return a html5 tag of the appropriate for this file (normally img or a)
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getTag()
|
|
{
|
|
$template = $this->File->getFrontendTemplate();
|
|
if (empty($template)) {
|
|
return '';
|
|
}
|
|
return (string)$this->renderWith($template);
|
|
}
|
|
|
|
public function requireDefaultRecords()
|
|
{
|
|
parent::requireDefaultRecords();
|
|
|
|
// Check if old file records should be migrated
|
|
if (!$this->config()->migrate_legacy_file) {
|
|
return;
|
|
}
|
|
|
|
$migrated = FileMigrationHelper::singleton()->run();
|
|
if ($migrated) {
|
|
DB::alteration_message("{$migrated} File DataObjects upgraded", "changed");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Joins one or more segments together to build a Filename identifier.
|
|
*
|
|
* Note that the result will not have a leading slash, and should not be used
|
|
* with local file paths.
|
|
*
|
|
* @param string $part,... Parts
|
|
* @return string
|
|
*/
|
|
public static function join_paths($part = null)
|
|
{
|
|
$args = func_get_args();
|
|
if (count($args) === 1 && is_array($args[0])) {
|
|
$args = $args[0];
|
|
}
|
|
|
|
$parts = array();
|
|
foreach ($args as $arg) {
|
|
$part = trim($arg, ' \\/');
|
|
if ($part) {
|
|
$parts[] = $part;
|
|
}
|
|
}
|
|
|
|
return implode('/', $parts);
|
|
}
|
|
|
|
public function deleteFile()
|
|
{
|
|
return $this->File->deleteFile();
|
|
}
|
|
|
|
public function getVisibility()
|
|
{
|
|
return $this->File->getVisibility();
|
|
}
|
|
|
|
public function publishFile()
|
|
{
|
|
$this->File->publishFile();
|
|
}
|
|
|
|
public function protectFile()
|
|
{
|
|
$this->File->protectFile();
|
|
}
|
|
|
|
public function grantFile()
|
|
{
|
|
$this->File->grantFile();
|
|
}
|
|
|
|
public function revokeFile()
|
|
{
|
|
$this->File->revokeFile();
|
|
}
|
|
|
|
public function canViewFile()
|
|
{
|
|
return $this->File->canViewFile();
|
|
}
|
|
|
|
public function CMSEditLink()
|
|
{
|
|
$link = null;
|
|
$this->extend('updateCMSEditLink', $link);
|
|
return $link;
|
|
}
|
|
|
|
public function PreviewLink($action = null)
|
|
{
|
|
// Since AbsoluteURL can whitelist protected assets,
|
|
// do permission check first
|
|
if (!$this->canView()) {
|
|
return null;
|
|
}
|
|
$link = $this->getIcon();
|
|
$this->extend('updatePreviewLink', $link, $action);
|
|
return $link;
|
|
}
|
|
}
|