silverstripe-framework/src/Assets/File.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;
}
}