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 * * @package framework * @subpackage filesystem * * @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. */ class File extends DataObject implements ShortcodeHandler, AssetContainer { use ImageManipulation; private static $default_sort ="\"Name\""; private static $singular_name ="File"; private static $plural_name ="Files"; 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" =>"File", "Owner" =>"Member" ); private static $defaults = array( "ShowInSearch" => 1, ); private static $extensions = array( "Hierarchy", ); private static $casting = array( 'TreeTitle' => 'HTMLText' ); /** * @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( '*' => 'File', 'jpg' => 'Image', 'jpeg' => 'Image', 'png' => 'Image', 'gif' => 'Image', ); /** * @config * @var 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()) { if(!isset($arguments['id']) || !is_numeric($arguments['id'])) return; $record = DataObject::get_by_id('File', $arguments['id']); if (!$record) { if(class_exists('ErrorPage')) { $record = ErrorPage::get()->filter("ErrorCode", 404)->first(); } if (!$record) { return; // 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(); } } /** * 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; $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); } /** * @todo Enforce on filesystem URL level via mod_rewrite * * @param Member $member * @return boolean */ 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 implemeting updateCMSFields(FieldList $fields) on that extension. * * @return FieldList */ public function getCMSFields() { // Preview $filePreview = CompositeField::create( CompositeField::create(new LiteralField("ImageFull", $this->PreviewThumbnail())) ->setName("FilePreviewImage") ->addExtraClass('cms-file-info-preview'), CompositeField::create( CompositeField::create( new ReadonlyField("FileType", _t('AssetTableField.TYPE','File type') . ':'), new ReadonlyField("Size", _t('AssetTableField.SIZE','File size') . ':', $this->getSize()), ReadonlyField::create( 'ClickableURL', _t('AssetTableField.URL','URL'), sprintf('%s', $this->Link(), $this->RelativeLink()) ) ->setDontEscape(true), new DateField_Disabled("Created", _t('AssetTableField.CREATED','First uploaded') . ':'), new DateField_Disabled("LastEdited", _t('AssetTableField.LASTEDIT','Last changed') . ':') ) ) ->setName("FilePreviewData") ->addExtraClass('cms-file-info-data') ) ->setName("FilePreview") ->addExtraClass('cms-file-info'); //get a tree listing with only folder, no files $fields = new FieldList( new TabSet('Root', new Tab('Main', $filePreview, new TextField("Title", _t('AssetTableField.TITLE','Title')), new TextField("Name", _t('AssetTableField.FILENAME','Filename')), DropdownField::create("OwnerID", _t('AssetTableField.OWNER','Owner'), Member::mapInCMSGroups()) ->setHasEmptyDefault(true), new TreeDropdownField("ParentID", _t('AssetTableField.FOLDER','Folder'), 'Folder') ) ) ); $this->extend('updateCMSFields', $fields); return $fields; } /** * 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(Config::inst()->get('File', '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() { parent::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(); } /** * 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 * * @param bool True if changed */ public function updateFilesystem() { if(!$this->config()->update_filesystem) { return false; } // Check the file exists if(!$this->File->exists()) { return false; } // Check path updated record will point to // If no changes necessary, skip $pathBefore = $this->File->getFilename(); $pathAfter = $this->getFilename(); if($pathAfter === $pathBefore) { return false; } // Copy record to new location via stream $stream = $this->File->getStream(); $result = $this->File->setFromStream($stream, $pathAfter); // If the backend chose a new name, update the local record if($result['Filename'] !== $pathAfter) { // Correct saved folder to selected filename $pathAfter = $result['Filename']; $this->setFilename($pathAfter); } // Update any database references $this->updateLinks($pathBefore, $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 condition The PHP condition to be evaluated. The page will be called $item * @param collator An array, passed by reference, to collect all of the matching descendants. */ public function collateDescendants($condition, &$collator) { if($children = $this->Children()) { foreach($children as $item) { if(!$condition || eval("return $condition;")) $collator[] = $item; $item->collateDescendants($condition, $collator); } return true; } } /** * 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 */ 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 $name; } /** * @param String $old File path relative to the webroot * @param String $new File path relative to the webroot */ protected function updateLinks($old, $new) { $this->extend('updateLinks', $old, $new); } /** * Gets the URL of this file * * @return string */ public function getAbsoluteURL() { $url = $this->getURL(); if($url) { return Director::absoluteURL($url); } } /** * Gets the URL of this file * * @uses Director::baseURL() * @return string */ public function getURL() { if($this->File->exists()) { return $this->File->getURL(); } } /** * Get URL, but without resampling. * * @return string */ public function getSourceURL() { if($this->File->exists()) { return $this->File->getSourceURL(); } } /** * @todo Coupling with cms module, remove this method. * * @return string */ public function DeleteLink() { return Director::absoluteBaseURL()."admin/assets/removefile/".$this->ID; } public function getFilename() { // 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; } /** * 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 ."/images/app_icons/{$extension}_32.gif")) { $extension = static::get_app_category($extension); // Fallback to category specific icon if(!file_exists(FRAMEWORK_PATH ."/images/app_icons/{$extension}_32.gif")) { $extension ="generic"; } } return FRAMEWORK_DIR ."/images/app_icons/{$extension}_32.gif"; } /** * 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 = new ValidationResult(); $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, '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 $this->File->getMetaData(); } } public function getMimeType() { if($this->File->exists()) { return $this->File->getMimeType(); } } public function getStream() { if($this->File->exists()) { return $this->File->getStream(); } } public function getString() { if($this->File->exists()) { return $this->File->getString(); } } public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $conflictResolution = null) { $result = $this->File->setFromLocalFile($path, $filename, $hash, $variant, $conflictResolution); // 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, $conflictResolution = null) { $result = $this->File->setFromStream($stream, $filename, $hash, $variant, $conflictResolution); // 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, $conflictResolution = null) { $result = $this->File->setFromString($data, $filename, $hash, $variant, $conflictResolution); // Update File record to name of the uploaded asset if($result) { $this->setFilename($result['Filename']); } return $result; } public function getIsImage() { return false; } 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() { $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); } }