silverstripe-framework/filesystem/File.php
Ingo Schommer b465a46bcc API CHANGErefactored upload functionality from File into newly created Upload class
API CHANGE deprecated some File functions and attributes
API CHANGE moved management function from File to Filesystem and added permission checks: sync(), loadContent(), fixfiles(), moverootfilesto()
API CHANGE deprecated use of File->loadUploaded()
ENHANCEMENT added filesize and extension validation to AssetAdmin and FileField
FEATURE added tests for Upload class

Merged revisions 47617 via svnmerge from 
svn://svn.silverstripe.com/silverstripe/modules/cms/branches/2.2.0-mesq

........
  r47617 | ischommer | 2008-01-04 19:20:29 +1300 (Fri, 04 Jan 2008) | 5 lines

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@52205 467b73ca-7a2a-4603-9d3b-597d59a354a9
2008-04-06 08:20:13 +00:00

582 lines
16 KiB
PHP
Executable File

<?php
/**
* @package sapphire
* @subpackage filesystem
*/
/**
* This class handles the representation of a File within Sapphire
* Note: The files are stored in the "/assets/" directory, but sapphire
* 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)
* @package sapphire
* @subpackage filesystem
*/
class File extends DataObject {
static $default_sort = "Name";
static $singular_name = "File";
static $plural_name = "Files";
static $db = array(
"Name" => "Varchar(255)",
"Title" => "Varchar(255)",
"Filename" => "Varchar(255)",
"Content" => "Text",
"Sort" => "Int"
);
static $indexes = array(
"SearchFields" => "fulltext (Filename,Title,Content)",
);
static $has_one = array(
"Parent" => "File",
"Owner" => "Member"
);
static $extensions = array(
"Hierarchy",
);
static $belongs_many_many = array(
"BackLinkTracking" => "SiteTree",
);
/**
* Cached result of a "SHOW FIELDS" call
* in instance_get() for performance reasons.
*
* @var array
*/
protected static $cache_file_fields = null;
function Link($action = null) {
return Director::baseURL() . $this->RelativeLink($action);
}
function RelativeLink($action = null){
return $this->Filename;
}
function TreeTitle() {
return $this->Title;
}
/**
* Event handler called before deleting from the database.
* You can overload this to clean up or otherwise process data before delete this
* record. Don't forget to call parent::onBeforeDelete(), though!
*/
protected function onBeforeDelete() {
parent::onBeforeDelete();
$this->autosetFilename();
if($this->Filename && $this->Name && file_exists($this->getFullPath()) && !is_dir($this->getFullPath())) {
unlink($this->getFullPath());
}
if($brokenPages = $this->BackLinkTracking()) {
foreach($brokenPages as $brokenPage) {
Notifications::event("BrokenLink", $brokenPage, $brokenPage->OwnerID);
$brokenPage->HasBrokenFile = true;
$brokenPage->write();
}
}
}
/*
* Find the given file
*/
static function find($filename) {
// Get the base file if $filename points to a resampled file
$filename = ereg_replace('_resampled/[^-]+-','',$filename);
$parts = explode("/",$filename);
$parentID = 0;
foreach($parts as $part) {
if($part == "assets" && !$parentID) continue;
$item = DataObject::get_one("File", "Name = '$part' AND ParentID = $parentID");
if(!$item) break;
$parentID = $item->ID;
}
return $item;
}
public function appCategory() {
$ext = $this->Extension;
switch($ext) {
case "aif": case "au": case "mid": case "midi": case "mp3": case "ra": case "ram": case "rm":
case "mp3": case "wav": case "m4a": case "snd": case "aifc": case "aiff": case "wma": case "apl":
case "avr": case "cda": case "mp4": case "ogg":
return "audio";
case "mpeg": case "mpg": case "m1v": case "mp2": case "mpa": case "mpe": case "ifo": case "vob":
case "avi": case "wmv": case "asf": case "m2v": case "qt":
return "mov";
case "arc": case "rar": case "tar": case "gz": case "tgz": case "bz2": case "dmg": case "jar":
case "ace": case "arj": case "bz": case "cab":
return "zip";
case "bmp": case "gif": case "jpg": case "jpeg": case "pcx": case "tif": case "png": case "alpha":
case "als": case "cel": case "icon": case "ico": case "ps":
return "image";
}
}
function CMSThumbnail() {
$filename = $this->Icon();
return "<div style=\"text-align:center;width: 100px;padding-top: 15px;\"><a target=\"_blank\" href=\"$this->URL\" title=\"Download: $this->URL\"><img src=\"$filename\" alt=\"$filename\" /></a><br /><br /><a style=\"color: #0074C6;\"target=\"_blank\" href=\"$this->URL\" title=\"Download: $this->URL\">Download</a><br /><em>$this->Size</e></div>";
}
/**
* Return the URL of an icon for the file type
*/
function Icon() {
$ext = $this->Extension;
if(!Director::fileExists("sapphire/images/app_icons/{$ext}_32.gif")) {
$ext = $this->appCategory();
}
if(!Director::fileExists("sapphire/images/app_icons/{$ext}_32.gif")) {
$ext = "generic";
}
return "sapphire/images/app_icons/{$ext}_32.gif";
}
/**
* Save an file passed from a form post into this object.
* DEPRECATED Please instanciate an Upload-object instead and pass the file
* via {Upload->loadIntoFile()}.
*
* @param $tmpFile array Indexed array that PHP generated for every file it uploads.
* @return Boolean|string Either success or error-message.
*/
function loadUploaded($tmpFile) {
user_error('File::loadUploaded is deprecated, please use the Upload class directly.', E_USER_NOTICE);
$upload = new Upload();
$upload->loadIntoFile($tmpFile, $this);
return $upload->isError();
}
/*
* Help to load the content of different type of files to File Table Content Field
*/
function test() {
Debug::show(get_defined_functions());
}
/**
* Delete the database record (recursively for folders) without touching the filesystem
*/
protected function deleteDatabaseOnly() {
if(is_numeric($this->ID)) DB::query("DELETE FROM File WHERE ID = $this->ID");
}
/**
* Event handler called before deleting from the database.
* You can overload this to clean up or otherwise process data before delete this
* record. Don't forget to call parent::onBeforeDelete(), though!
*/
protected function onBeforeWrite() {
parent::onBeforeWrite();
if(!$this->Name) $this->Name = "new-" . strtolower($this->class);
if($brokenPages = $this->BackLinkTracking()) {
foreach($brokenPages as $brokenPage) {
Notifications::event("BrokenLink", $brokenPage, $brokenPage->OwnerID);
$brokenPage->HasBrokenFile = true;
$brokenPage->write();
}
}
}
/**
* 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.
*/
function setName($name) {
$oldName = $this->Name;
// It can't be blank
if(!$name) $name = $this->Title;
// Fix illegal characters
$name = ereg_replace(' +','-',trim($name));
$name = ereg_replace('[^A-Za-z0-9.+_\-]','',$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) {
if($dotPos = strpos($name, '.')) {
$base = substr($name,0,$dotPos);
$ext = substr($name,$dotPos);
} else {
$base = $name;
$ext = "";
}
$suffix = 1;
while(DataObject::get_one("File", "Name = '" . addslashes($name) . "' AND ParentID = " . (int)$this->ParentID)) {
$suffix++;
$name = "$base-$suffix$ext";
}
}
if(!$this->getField('Title')) $this->__set('Title', str_replace(array('-','_'),' ',ereg_replace('\.[^.]+$','',$name)));
$this->setField('Name', $name);
if($oldName && $oldName != $this->Name) {
$this->resetFilename();
} else {
$this->autosetFilename();
}
return $this->getField('Name');
}
/**
* Change a filename, moving the file if appropriate.
* @param $renamePhysicalFile Set this to false if you don't want to rename the physical file. Used when calling resetFilename() on the children of a folder.
*/
protected function resetFilename($renamePhysicalFile = true) {
$oldFilename = $this->getField('Filename');
$newFilename = $this->getRelativePath();
if($this->Name && $this->Filename && file_exists(Director::getAbsFile($oldFilename)) && strpos($oldFilename, '//') === false) {
if($renamePhysicalFile) {
$from = Director::getAbsFile($oldFilename);
$to = Director::getAbsFile($newFilename);
// Error checking
if(!file_exists($from)) user_error("Cannot move $oldFilename to $newFilename - $oldFilename doesn't exist", E_USER_WARNING);
else if(!file_exists(dirname($to))) user_error("Cannot move $oldFilename to $newFilename - " . dirname($newFilename) . " doesn't exist", E_USER_WARNING);
else if(!rename($from, $to)) user_error("Cannot move $oldFilename to $newFilename", E_USER_WARNING);
else $this->updateLinks($oldFilename, $newFilename);
} else {
$this->updateLinks($oldFilename, $newFilename);
}
} else {
// If the old file doesn't exist, maybe it's already been renamed.
if(file_exists(Director::getAbsFile($newFilename))) $this->updateLinks($oldFilename, $newFilename);
}
$this->setField('Filename', $newFilename);
}
/**
* Set the Filename field without manipulating the filesystem.
*/
protected function autosetFilename() {
$this->setField('Filename', $this->getRelativePath());
}
function setField( $field, $value ) {
parent::setField( $field, $value );
}
/**
* Rewrite links to the $old file to now point to the $new file
*/
protected function updateLinks($old, $new) {
$pages = $this->BackLinkTracking();
if($pages) {
foreach($pages as $page) {
$fieldName = $page->FieldName; // extracted from the many-many join
if($fieldName) {
$text = $page->$fieldName;
$page->$fieldName = str_replace($old, $new, $page->$fieldName);
$page->write();
}
}
}
}
function setParentID($parentID) {
$this->setField('ParentID', $parentID);
if($this->Name) $this->resetFilename();
else $this->autosetFilename();
return $this->getField('ParentID');
}
/**
* Gets the absolute URL accessible through the web.
*
* @uses Director::absoluteBaseURL()
* @return string
*/
function getAbsoluteURL() {
return Director::absoluteBaseURL() . $this->getFilename();
}
/**
* Gets the absolute URL accessible through the web.
*
* @uses Director::absoluteBaseURL()
* @return string
*/
function getURL() {
return Director::absoluteBaseURL() . $this->getFilename();
}
/**
* Return the last 50 characters of the URL
*/
function getLinkedURL() {
return "$this->Name";
}
function getFullPath() {
return Director::baseFolder() . '/' . $this->getFilename();
}
function getRelativePath() {
if($this->ParentID) {
$p = DataObject::get_one('Folder', "ID={$this->ParentID}");
if($p->ID) return $p->getRelativePath() . $this->getField("Name");
else return "assets/" . $this->getField("Name");
} else if($this->getField("Name")) {
return "assets/" . $this->getField("Name");
} else {
return "assets";
}
}
function DeleteLink() {
return Director::absoluteBaseURL()."admin/assets/removefile/".$this->ID;
}
function getFilename() {
if($this->getField('Name')) return $this->getField('Filename');
else return 'assets/';
}
function setFilename($val) {
$this->setField('Filename', $val);
$this->setField('Name', basename($val));
}
/*
* FIXME This overrides getExtension() in DataObject, but it does something completely different.
* This should be renamed to getFileExtension(), but has not been yet as it may break
* legacy code.
*/
function getExtension() {
return strtolower(substr($this->getField('Filename'),strrpos($this->getField('Filename'),'.')+1));
}
function getFileType() {
$types = array(
'gif' => 'GIF Image - good for diagrams',
'jpg' => 'JPEG Image - good for photos',
'jpeg' => 'JPEG Image - good for photos',
'png' => 'PNG Image - good general-purpose format',
'doc' => 'Word Document',
'xls' => 'Excel Spreadsheet',
'zip' => 'ZIP compressed file',
'gz' => 'GZIP compressed file',
'dmg' => 'Apple Disk Image',
'pdf' => 'Adobe Acrobat PDF file',
);
$ext = $this->getExtension();
return isset($types[$ext]) ? $types[$ext] : 'unknown';
}
/**
* Returns the size of the file type in an appropriate format.
*/
function getSize() {
$size = $this->getAbsoluteSize();
return ($size) ? self::format_size($size) : false;
}
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';
}
/**
* returns the size in bytes with no extensions for calculations.
*/
function getAbsoluteSize(){
if(file_exists($this->getFullPath() )) {
$size = filesize($this->getFullPath());
return $size;
}else{
return 0;
}
}
/**
* Select clause for DataObject::get('File') operations/
* Stores an array, suitable for a {@link SQLQuery} object.
*/
private static $dataobject_select;
/**
* We've overridden the DataObject::get function for File so that the very large content field
* is excluded!
*
* @todo Admittedly this is a bit of a hack; but we need a way of ensuring that large
* TEXT fields don't stuff things up for the rest of us. Perhaps a separate search table would
* be a better way of approaching this?
* @deprecated alternative_instance_get()
*/
public function instance_get($filter = "", $sort = "", $join = "", $limit="", $containerClass = "DataObjectSet", $having="") {
$query = $this->extendedSQL($filter, $sort, $limit, $join, $having);
$baseTable = reset($query->from);
$excludeDbColumns = array('Content');
// Work out which columns we're actually going to select
// In short, we select everything except File.Content
$filteredSelect = array();
foreach($query->select as $i => $item) {
if($item == "`File`.*") {
if(!isset(self::$cache_file_fields)) self::$cache_file_fields = DB::query("SHOW FIELDS IN `File`")->column();
$columnsToAdd = array_diff(self::$cache_file_fields, $excludeDbColumns);
foreach($columnsToAdd as $otherItem) {
$filteredSelect[] = '`File`.' . $otherItem;
}
} else {
$filteredSelect[] = $item;
}
}
$query->select = $filteredSelect;
$records = $query->execute();
$ret = $this->buildDataObjectSet($records, $containerClass);
if($ret) $ret->parseQueryLimit($query);
return $ret;
}
/**
* Stub, overridden by Folder
*/
function userCanEdit() {
return false;
}
public function flushCache() {
parent::flushCache();
self::$cache_file_fields = null;
}
/**
* DEPRECATED
*/
static $file_size_restrictions = array();
/**
* DEPRECATED
*/
static $allowed_file_types = array();
/**
* DEPRECATED
*/
static function setMaxFileSize( $maxSize, $warningSize, $extension = '*' ) {
user_error('File::setMaxFileSize is deprecated',E_USER_ERROR);
}
/**
* DEPRECATED
*/
static function getMaxFileSize($extension = '*') {
user_error('File::getMaxFileSize is deprecated',E_USER_ERROR);
}
/**
* DEPRECATED
*/
static function sync() {
user_error('File::sync is deprecated - please use FileSystem::sync',E_USER_ERROR);
}
/**
* DEPRECATED
*/
function moverootfilesto() {
user_error('File::moverootfilesto is deprecated - please use FileSystem::moverootfilesto',E_USER_ERROR);
}
/**
* DEPRECATED
*/
function fixfiles() {
user_error('File::fixfiles is deprecated - please use FileSystem::fixfiles',E_USER_ERROR);
}
/**
* DEPRECATED
*/
function loadContent() {
user_error('File::loadContent deprecated',E_USER_ERROR);
}
/**
* DEPRECATED
*/
public function loadallcontent() {
user_error('File::loadallcontent deprecated',E_USER_ERROR);
}
}
?>