silverstripe-framework/Assets/File.php

1246 lines
34 KiB
PHP
Raw Normal View History

<?php
namespace SilverStripe\Assets;
use SilverStripe\Admin\AdminRootController;
use SilverStripe\Admin\CMSPreviewable;
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\Control\Controller;
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;
2016-06-23 01:37:22 +02:00
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.
2014-08-15 08:53:05 +02:00
*
* 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).
2014-08-15 08:53:05 +02:00
*
* <b>Security</b>
2014-08-15 08:53:05 +02:00
*
* 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
2014-08-15 08:53:05 +02:00
*
* <b>Asset storage</b>
2014-08-15 08:53:05 +02:00
*
* 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).
2014-08-15 08:53:05 +02:00
*
* The File dataobject simply represents an externally facing view of shared resources
* within this asset store.
2014-08-15 08:53:05 +02:00
*
* 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).
2014-08-15 08:53:05 +02:00
*
* 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.
2014-08-15 08:53:05 +02:00
*
* <b>Properties</b>
2014-08-15 08:53:05 +02:00
*
* - "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
2014-08-15 08:53:05 +02:00
*
* @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
*/
2016-04-27 04:26:39 +02:00
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)',
);
2014-08-15 08:53:05 +02:00
private static $has_one = array(
"Parent" => "SilverStripe\\Assets\\File",
2016-06-23 01:37:22 +02:00
"Owner" => "SilverStripe\\Security\\Member"
);
2016-03-08 21:50:18 +01:00
private static $defaults = array(
"ShowInSearch" => 1,
);
2014-08-15 08:53:05 +02:00
private static $extensions = array(
"SilverStripe\\ORM\\Hierarchy\\Hierarchy",
"SilverStripe\\ORM\\Versioning\\Versioned"
);
private static $casting = array (
'TreeTitle' => 'HTMLFragment'
);
2014-08-15 08:53:05 +02:00
private static $table_name = 'File';
/**
* @config
* @var array List of allowed file extensions, enforced through {@link validate()}.
2014-08-15 08:53:05 +02:00
*
* 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.
2014-08-15 08:53:05 +02:00
*
* - If you are running Apache you will need to change assets/.htaccess
2014-08-15 08:53:05 +02:00
* - 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
2016-06-23 01:37:22 +02:00
* @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.
2016-03-08 21:50:18 +01:00
*
* @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()) {
2016-03-08 12:20:51 +01:00
// Find appropriate record, with fallback for error handlers
$record = static::find_shortcode_record($arguments, $errorCode);
if($errorCode) {
2016-03-08 12:20:51 +01:00
$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();
}
}
2016-03-08 12:20:51 +01:00
/**
* 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 */
2016-03-08 12:20:51 +01:00
$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.
2014-08-15 08:53:05 +02:00
*
* @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(
2014-08-15 08:53:05 +02:00
'Name' => $part,
'ParentID' => $parentID
))->first();
if(!$item) break;
$parentID = $item->ID;
}
2014-08-15 08:53:05 +02:00
return $item;
}
2014-08-15 08:53:05 +02:00
/**
* 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();
}
2014-08-15 08:53:05 +02:00
$result = $this->extendedCan('canView', $member);
if($result !== null) {
return $result;
Merge 3 into master # Conflicts: # .travis.yml # admin/css/ie7.css # admin/css/ie7.css.map # admin/css/ie8.css.map # admin/css/screen.css # admin/css/screen.css.map # admin/javascript/LeftAndMain.js # admin/scss/_style.scss # admin/scss/_uitheme.scss # control/HTTPRequest.php # core/Object.php # css/AssetUploadField.css # css/AssetUploadField.css.map # css/ConfirmedPasswordField.css.map # css/Form.css.map # css/GridField.css.map # css/TreeDropdownField.css.map # css/UploadField.css # css/UploadField.css.map # css/debug.css.map # dev/Debug.php # docs/en/00_Getting_Started/00_Server_Requirements.md # docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md # docs/en/02_Developer_Guides/06_Testing/index.md # docs/en/02_Developer_Guides/14_Files/02_Images.md # docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md # filesystem/File.php # filesystem/Folder.php # filesystem/GD.php # filesystem/Upload.php # forms/ToggleField.php # forms/Validator.php # javascript/lang/en_GB.js # javascript/lang/fr.js # javascript/lang/src/en.js # javascript/lang/src/fr.js # model/Image.php # model/UnsavedRelationList.php # model/Versioned.php # model/connect/MySQLDatabase.php # model/fieldtypes/DBField.php # model/fieldtypes/Enum.php # scss/AssetUploadField.scss # scss/UploadField.scss # templates/email/ChangePasswordEmail.ss # templates/forms/DropdownField.ss # tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsFormsContext.php # tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php # tests/forms/EnumFieldTest.php # tests/security/MemberTest.php # tests/security/MemberTest.yml # tests/security/SecurityTest.php
2016-04-29 07:50:55 +02:00
}
2014-08-15 08:53:05 +02:00
return true;
}
2014-08-15 08:53:05 +02:00
/**
* Check if this file can be modified
2014-08-15 08:53:05 +02:00
*
* @param Member $member
* @return boolean
*/
public function canEdit($member = null) {
if(!$member) {
$member = Member::currentUser();
}
2014-08-15 08:53:05 +02:00
$result = $this->extendedCan('canEdit', $member);
if($result !== null) {
return $result;
}
2014-08-15 08:53:05 +02:00
return Permission::checkMember($member, array('CMS_ACCESS_AssetAdmin', 'CMS_ACCESS_LeftAndMain'));
}
2014-08-15 08:53:05 +02:00
/**
* 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();
}
2014-08-15 08:53:05 +02:00
$result = $this->extendedCan('canCreate', $member, $context);
if($result !== null) {
return $result;
}
2014-08-15 08:53:05 +02:00
return $this->canEdit($member);
}
2014-08-15 08:53:05 +02:00
/**
* Check if this file can be deleted
*
* @param Member $member
* @return boolean
*/
public function canDelete($member = null) {
if(!$member) {
$member = Member::currentUser();
}
2014-08-15 08:53:05 +02:00
$result = $this->extendedCan('canDelete', $member);
if($result !== null) {
return $result;
}
2014-08-15 08:53:05 +02:00
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.
2014-08-15 08:53:05 +02:00
*
* @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;
}
protected function getStatusTitle() {
$statusTitle = '';
if ($this->getIsAddedToStage()) {
$statusTitle = _t('File.DRAFT', 'Draft');
} elseif ($this->getIsModifiedOnStage()) {
$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".
2014-08-15 08:53:05 +02:00
*
* @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;
}
2014-08-15 08:53:05 +02:00
/**
* For a category or list of categories, get the list of file extensions
2014-08-15 08:53:05 +02:00
*
* @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;
2016-03-08 21:50:18 +01:00
// 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.
2014-08-15 08:53:05 +02:00
*
* @return string
*/
public function appCategory() {
return self::get_app_category($this->getExtension());
}
2014-08-15 08:53:05 +02:00
/**
2014-08-15 08:53:05 +02:00
* 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();
}
// Set default name
if(!$this->getField('Name')) {
$this->Name ="new-" . strtolower($this->class);
}
2014-08-15 08:53:05 +02:00
// Propegate changes to the AssetStore and update the DBFile field
$this->updateFilesystem();
parent::onBeforeWrite();
}
2014-08-15 08:53:05 +02:00
/**
* 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.
2014-08-15 08:53:05 +02:00
*
* This method will update the File {@see DBFile} field value on success, so it must be called
* before writing to the database
2014-08-15 08:53:05 +02:00
*
* @return bool True if changed
*/
public function updateFilesystem() {
if(!$this->config()->update_filesystem) {
return false;
}
2014-08-15 08:53:05 +02:00
// 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;
2014-08-15 08:53:05 +02:00
}
// Check path updated record will point to
// If no changes necessary, skip
$pathBefore = $this->File->getFilename();
$pathAfter = $this->generateFilename();
if($pathAfter === $pathBefore) {
return false;
}
2014-08-15 08:53:05 +02:00
// Copy record to new location via stream
$stream = $this->File->getStream();
$this->File->setFromStream($stream, $pathAfter);
return true;
}
2014-08-15 08:53:05 +02:00
/**
* 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;
}
/**
* Setter function for Name. Automatically sets a default title,
* and removes characters that might be invalid on the filesystem.
* Also adds a suffix to the name if the filename already exists
* on the filesystem, and is associated to a different {@link File} database record
* in the same folder. This means "myfile.jpg" might become "myfile-1.jpg".
2014-08-15 08:53:05 +02:00
*
* Does not change the filesystem itself, please use {@link write()} for this.
2014-08-15 08:53:05 +02:00
*
* @param string $name
* @return $this
*/
public function setName($name) {
$oldName = $this->Name;
// It can't be blank, default to Title
if(!$name) {
$name = $this->Title;
}
// Fix illegal characters
$filter = FileNameFilter::create();
$name = $filter->filter($name);
// We might have just turned it blank, so check again.
if(!$name) {
$name = 'new-folder';
}
// If it's changed, check for duplicates
if($oldName && $oldName != $name) {
$base = pathinfo($name, PATHINFO_FILENAME);
$ext = self::get_file_extension($name);
$suffix = 1;
while(File::get()->filter(array(
2014-08-15 08:53:05 +02:00
'Name' => $name,
'ParentID' => (int) $this->ParentID
))->exclude(array(
'ID' => $this->ID
))->first()
) {
$suffix++;
$name = "$base-$suffix.$ext";
}
}
// Update actual field value
$this->setField('Name', $name);
2014-08-15 08:53:05 +02:00
// Update title
if(!$this->Title) {
$this->Title = str_replace(array('-','_'),' ', preg_replace('/\.[^.]+$/', '', $name));
}
return $this;
}
/**
* Gets the URL of this file
2014-08-15 08:53:05 +02:00
*
* @return string
*/
public function getAbsoluteURL() {
$url = $this->getURL();
if($url) {
return Director::absoluteURL($url);
}
return null;
}
2014-08-15 08:53:05 +02:00
/**
* Gets the URL of this file
2014-08-15 08:53:05 +02:00
*
* @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);
2016-04-28 01:41:40 +02:00
}
return null;
}
/**
* Get URL, but without resampling.
2014-08-15 08:53:05 +02:00
*
* @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);
2016-04-28 01:41:40 +02:00
}
return null;
}
/**
* @todo Coupling with cms module, remove this method.
*
* @return string
*/
public function DeleteLink() {
return Controller::join_links(
Director::absoluteBaseURL(),
AdminRootController::admin_url(),
"assets/removefile/",
$this->ID
);
}
/**
* 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() {
// Relies on Parent() returning the stage record
$parent = $this->Parent();
if($parent && $parent->exists()) {
2016-04-01 05:27:59 +02:00
$parent->publishRecursive();
}
}
/**
* 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;
}
2016-03-08 21:50:18 +01:00
// 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;
}
}
2014-08-15 08:53:05 +02:00
// Update base name
$this->Name = basename($filename);
return $this;
}
/**
* Returns the file extension
2014-08-15 08:53:05 +02:00
*
* @return string
*/
public function getExtension() {
return self::get_file_extension($this->Name);
}
2014-08-15 08:53:05 +02:00
/**
* 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").
2014-08-15 08:53:05 +02:00
*
* 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);
}
2014-08-15 08:53:05 +02:00
/**
* 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
Merge 3 into master # Conflicts: # .travis.yml # admin/css/ie7.css # admin/css/ie7.css.map # admin/css/ie8.css.map # admin/css/screen.css # admin/css/screen.css.map # admin/javascript/LeftAndMain.js # admin/scss/_style.scss # admin/scss/_uitheme.scss # control/HTTPRequest.php # core/Object.php # css/AssetUploadField.css # css/AssetUploadField.css.map # css/ConfirmedPasswordField.css.map # css/Form.css.map # css/GridField.css.map # css/TreeDropdownField.css.map # css/UploadField.css # css/UploadField.css.map # css/debug.css.map # dev/Debug.php # docs/en/00_Getting_Started/00_Server_Requirements.md # docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md # docs/en/02_Developer_Guides/06_Testing/index.md # docs/en/02_Developer_Guides/14_Files/02_Images.md # docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md # filesystem/File.php # filesystem/Folder.php # filesystem/GD.php # filesystem/Upload.php # forms/ToggleField.php # forms/Validator.php # javascript/lang/en_GB.js # javascript/lang/fr.js # javascript/lang/src/en.js # javascript/lang/src/fr.js # model/Image.php # model/UnsavedRelationList.php # model/Versioned.php # model/connect/MySQLDatabase.php # model/fieldtypes/DBField.php # model/fieldtypes/Enum.php # scss/AssetUploadField.scss # scss/UploadField.scss # templates/email/ChangePasswordEmail.ss # templates/forms/DropdownField.ss # tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsFormsContext.php # tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php # tests/forms/EnumFieldTest.php # tests/security/MemberTest.php # tests/security/MemberTest.yml # tests/security/SecurityTest.php
2016-04-29 07:50:55 +02:00
if(!file_exists(FRAMEWORK_PATH ."/client/dist/images/app_icons/{$extension}_32.png")) {
$extension = static::get_app_category($extension);
// Fallback to category specific icon
Merge 3 into master # Conflicts: # .travis.yml # admin/css/ie7.css # admin/css/ie7.css.map # admin/css/ie8.css.map # admin/css/screen.css # admin/css/screen.css.map # admin/javascript/LeftAndMain.js # admin/scss/_style.scss # admin/scss/_uitheme.scss # control/HTTPRequest.php # core/Object.php # css/AssetUploadField.css # css/AssetUploadField.css.map # css/ConfirmedPasswordField.css.map # css/Form.css.map # css/GridField.css.map # css/TreeDropdownField.css.map # css/UploadField.css # css/UploadField.css.map # css/debug.css.map # dev/Debug.php # docs/en/00_Getting_Started/00_Server_Requirements.md # docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md # docs/en/02_Developer_Guides/06_Testing/index.md # docs/en/02_Developer_Guides/14_Files/02_Images.md # docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md # filesystem/File.php # filesystem/Folder.php # filesystem/GD.php # filesystem/Upload.php # forms/ToggleField.php # forms/Validator.php # javascript/lang/en_GB.js # javascript/lang/fr.js # javascript/lang/src/en.js # javascript/lang/src/fr.js # model/Image.php # model/UnsavedRelationList.php # model/Versioned.php # model/connect/MySQLDatabase.php # model/fieldtypes/DBField.php # model/fieldtypes/Enum.php # scss/AssetUploadField.scss # scss/UploadField.scss # templates/email/ChangePasswordEmail.ss # templates/forms/DropdownField.ss # tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsFormsContext.php # tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php # tests/forms/EnumFieldTest.php # tests/security/MemberTest.php # tests/security/MemberTest.yml # tests/security/SecurityTest.php
2016-04-29 07:50:55 +02:00
if(!file_exists(FRAMEWORK_PATH ."/client/dist/images/app_icons/{$extension}_32.png")) {
$extension ="generic";
}
}
Merge 3 into master # Conflicts: # .travis.yml # admin/css/ie7.css # admin/css/ie7.css.map # admin/css/ie8.css.map # admin/css/screen.css # admin/css/screen.css.map # admin/javascript/LeftAndMain.js # admin/scss/_style.scss # admin/scss/_uitheme.scss # control/HTTPRequest.php # core/Object.php # css/AssetUploadField.css # css/AssetUploadField.css.map # css/ConfirmedPasswordField.css.map # css/Form.css.map # css/GridField.css.map # css/TreeDropdownField.css.map # css/UploadField.css # css/UploadField.css.map # css/debug.css.map # dev/Debug.php # docs/en/00_Getting_Started/00_Server_Requirements.md # docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md # docs/en/02_Developer_Guides/06_Testing/index.md # docs/en/02_Developer_Guides/14_Files/02_Images.md # docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md # filesystem/File.php # filesystem/Folder.php # filesystem/GD.php # filesystem/Upload.php # forms/ToggleField.php # forms/Validator.php # javascript/lang/en_GB.js # javascript/lang/fr.js # javascript/lang/src/en.js # javascript/lang/src/fr.js # model/Image.php # model/UnsavedRelationList.php # model/Versioned.php # model/connect/MySQLDatabase.php # model/fieldtypes/DBField.php # model/fieldtypes/Enum.php # scss/AssetUploadField.scss # scss/UploadField.scss # templates/email/ChangePasswordEmail.ss # templates/forms/DropdownField.ss # tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsFormsContext.php # tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php # tests/forms/EnumFieldTest.php # tests/security/MemberTest.php # tests/security/MemberTest.yml # tests/security/SecurityTest.php
2016-04-29 07:50:55 +02:00
return FRAMEWORK_DIR ."/client/dist/images/app_icons/{$extension}_32.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'),
2015-03-10 11:24:03 +01:00
'htm' => _t('File.HtmlType', 'HTML file')
);
2014-08-15 08:53:05 +02:00
// 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);
Merge 3 into master # Conflicts: # .travis.yml # admin/css/ie7.css # admin/css/ie7.css.map # admin/css/ie8.css.map # admin/css/screen.css # admin/css/screen.css.map # admin/javascript/LeftAndMain.js # admin/scss/_style.scss # admin/scss/_uitheme.scss # control/HTTPRequest.php # core/Object.php # css/AssetUploadField.css # css/AssetUploadField.css.map # css/ConfirmedPasswordField.css.map # css/Form.css.map # css/GridField.css.map # css/TreeDropdownField.css.map # css/UploadField.css # css/UploadField.css.map # css/debug.css.map # dev/Debug.php # docs/en/00_Getting_Started/00_Server_Requirements.md # docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md # docs/en/02_Developer_Guides/06_Testing/index.md # docs/en/02_Developer_Guides/14_Files/02_Images.md # docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md # filesystem/File.php # filesystem/Folder.php # filesystem/GD.php # filesystem/Upload.php # forms/ToggleField.php # forms/Validator.php # javascript/lang/en_GB.js # javascript/lang/fr.js # javascript/lang/src/en.js # javascript/lang/src/fr.js # model/Image.php # model/UnsavedRelationList.php # model/Versioned.php # model/connect/MySQLDatabase.php # model/fieldtypes/DBField.php # model/fieldtypes/Enum.php # scss/AssetUploadField.scss # scss/UploadField.scss # templates/email/ChangePasswordEmail.ss # templates/forms/DropdownField.ss # tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsFormsContext.php # tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php # tests/forms/EnumFieldTest.php # tests/security/MemberTest.php # tests/security/MemberTest.yml # tests/security/SecurityTest.php
2016-04-29 07:50:55 +02:00
}
return false;
}
2014-08-15 08:53:05 +02:00
/**
* Formats a file size (eg: (int)42 becomes string '42 bytes')
*
* @todo unit tests
*
* @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';
}
2016-03-08 21:50:18 +01:00
/**
* Convert a php.ini value (eg: 512M) to bytes
2014-08-15 08:53:05 +02:00
*
* @todo unit tests
*
* @param string $iniValue
* @return int
*/
public static function ini2bytes($iniValue) {
switch(strtolower(substr(trim($iniValue), -1))) {
case 'g':
$iniValue *= 1024;
case 'm':
$iniValue *= 1024;
case 'k':
$iniValue *= 1024;
}
return $iniValue;
}
/**
* Return file size in bytes.
2016-03-08 21:50:18 +01:00
*
* @return int
*/
public function getAbsoluteSize(){
return $this->File->getAbsoluteSize();
}
2014-08-15 08:53:05 +02:00
2015-06-17 05:51:30 +02:00
public function validate() {
$result = ValidationResult::create();
$this->File->validate($result, $this->Name);
$this->extend('validate', $result);
return $result;
}
2014-08-15 08:53:05 +02:00
/**
* Maps a {@link File} subclass to a specific extension.
* By default, files with common image extensions will be created
2014-08-15 08:53:05 +02:00
* 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.
2014-08-15 08:53:05 +02:00
*
* Caution: Changes to mapping doesn't apply to existing file records in the database.
* Also doesn't hook into {@link Object::getCustomClass()}.
2014-08-15 08:53:05 +02:00
*
* @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['*'];
}
2014-08-15 08:53:05 +02:00
/**
* See {@link get_class_for_file_extension()}.
2014-08-15 08:53:05 +02:00
*
* @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);
}
}
2014-08-15 08:53:05 +02:00
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);
2016-03-08 21:50:18 +01:00
// 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();
}
2016-04-27 04:26:39 +02:00
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();
2016-04-27 04:26:39 +02:00
$this->extend('updatePreviewLink', $link, $action);
return $link;
}
}