Security * * 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 * * Asset storage * * 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. * * Properties * * - "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('%s', $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 = ""; $statusTitle = $this->getStatusTitle(); $statusFlag = ($statusTitle) ? "{$statusTitle}" : ''; $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(); } // Set default name if(!$this->getField('Name')) { $this->Name ="new-" . strtolower($this->class); } // Propegate 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; } /** * 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". * * Does not change the filesystem itself, please use {@link write()} for this. * * @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( '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); // Update title if(!$this->Title) { $this->Title = str_replace(array('-','_'),' ', preg_replace('/\.[^.]+$/', '', $name)); } return $this; } /** * 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; } /** * @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() { // 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/dist/images/app_icons/{$extension}_32.png")) { $extension = static::get_app_category($extension); // Fallback to category specific icon if(!file_exists(FRAMEWORK_PATH ."/client/dist/images/app_icons/{$extension}_32.png")) { $extension ="generic"; } } 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'), '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') * * @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'; } /** * Convert a php.ini value (eg: 512M) to bytes * * @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. * * @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; } }