2007-07-19 12:40:28 +02:00
< ? php
2008-02-25 03:10:37 +01:00
/**
* Represents a folder in the assets directory .
* @ package sapphire
* @ subpackage filesystem
*/
2007-07-19 12:40:28 +02:00
class Folder extends File {
/*
2008-03-17 23:51:25 +01:00
* Find the given folder or create it , recursively .
*
* @ param $folderPath string Absolute or relative path to the file
2007-07-19 12:40:28 +02:00
*/
2008-03-17 23:51:25 +01:00
static function findOrMake ( $folderPath ) {
$folderPath = trim ( Director :: makeRelative ( $folderPath ));
// replace leading and trailing slashes
$folderPath = preg_replace ( '/^\/?(.*)\/?$/' , '$1' , $folderPath );
$parts = explode ( " / " , $folderPath );
2007-07-19 12:40:28 +02:00
$parentID = 0 ;
foreach ( $parts as $part ) {
2008-11-24 10:31:14 +01:00
$item = DataObject :: get_one ( " Folder " , " \" Name \" = ' $part ' AND \" ParentID \" = $parentID " );
2007-07-19 12:40:28 +02:00
if ( ! $item ) {
$item = new Folder ();
$item -> ParentID = $parentID ;
$item -> Name = $part ;
2008-11-10 04:51:35 +01:00
$item -> Title = $part ;
2007-07-19 12:40:28 +02:00
$item -> write ();
2007-10-26 01:07:24 +02:00
if ( ! file_exists ( $item -> getFullPath ())) mkdir ( $item -> getFullPath (), Filesystem :: $folder_create_mask );
2007-07-19 12:40:28 +02:00
}
$parentID = $item -> ID ;
}
return $item ;
}
/**
* Syncronise the file database with the actual content of the assets folder
*/
function syncChildren () {
$parentID = ( int ) $this -> ID ; // parentID = 0 on the singleton, used as the 'root node';
$added = 0 ;
$deleted = 0 ;
// First, merge any children that are duplicates
2008-11-24 10:31:14 +01:00
$duplicateChildrenNames = DB :: query ( " SELECT \" Name \" FROM \" File \" WHERE \" ParentID \" = $parentID GROUP BY \" Name \" HAVING count(*) > 1 " ) -> column ();
2007-07-19 12:40:28 +02:00
if ( $duplicateChildrenNames ) foreach ( $duplicateChildrenNames as $childName ) {
2009-04-06 01:24:56 +02:00
$childName = DB :: getConn () -> addslashes ( $childName );
2007-07-19 12:40:28 +02:00
// Note, we do this in the database rather than object-model; otherwise we get all sorts of problems about deleting files
2008-11-24 10:31:14 +01:00
$children = DB :: query ( " SELECT \" ID \" FROM \" File \" WHERE \" Name \" = ' $childName ' AND \" ParentID \" = $parentID " ) -> column ();
2008-08-15 00:20:14 +02:00
if ( $children ) {
$keptChild = array_shift ( $children );
foreach ( $children as $removedChild ) {
2008-11-24 10:31:14 +01:00
DB :: query ( " UPDATE \" File \" SET \" ParentID \" = $keptChild WHERE \" ParentID \" = $removedChild " );
2008-11-24 00:28:16 +01:00
DB :: query ( " DELETE FROM \" File \" WHERE \" ID \" = $removedChild " );
2008-08-15 00:20:14 +02:00
}
} else {
2008-11-23 01:31:06 +01:00
user_error ( " Inconsistent database issue: SELECT ID FROM \" File \" WHERE Name = ' $childName ' AND ParentID = $parentID should have returned data " , E_USER_WARNING );
2007-07-19 12:40:28 +02:00
}
}
// Get index of database content
2008-02-25 03:10:37 +01:00
// We don't use DataObject so that things like subsites doesn't muck with this.
2008-11-24 10:31:14 +01:00
$dbChildren = DB :: query ( " SELECT * FROM \" File \" WHERE \" ParentID \" = $parentID " );
2008-02-25 03:10:37 +01:00
$hasDbChild = array ();
if ( $dbChildren ) {
2007-07-19 12:40:28 +02:00
foreach ( $dbChildren as $dbChild ) {
2008-02-25 03:10:37 +01:00
$className = $dbChild [ 'ClassName' ];
2009-08-24 09:21:08 +02:00
if ( ! $className ) $className = " File " ;
2008-02-25 03:10:37 +01:00
$hasDbChild [ $dbChild [ 'Name' ]] = new $className ( $dbChild );
2007-07-19 12:40:28 +02:00
}
}
2008-02-25 03:10:37 +01:00
$unwantedDbChildren = $hasDbChild ;
2007-07-19 12:40:28 +02:00
// Iterate through the actual children, correcting the database as necessary
$baseDir = $this -> FullPath ;
if ( ! $this -> Filename ) die ( $this -> ID . " - " . $this -> FullPath );
if ( file_exists ( $baseDir )) {
$actualChildren = scandir ( $baseDir );
foreach ( $actualChildren as $actualChild ) {
if ( $actualChild [ 0 ] == '.' ) continue ; // ignore hidden files
if ( substr ( $actualChild , 0 , 6 ) == 'Thumbs' ) continue ; // ignore windows cache stuff
if ( $actualChild == '_resampled' ) continue ; // ignore the resampled copies of images
2007-09-14 04:10:14 +02:00
if ( $actualChild == '_tmp' ) continue ; // ignore tmp folder for PhotoEditor.
2007-07-19 12:40:28 +02:00
// A record with a bad class type doesn't deserve to exist. It must be purged!
if ( isset ( $hasDbChild [ $actualChild ])) {
$child = $hasDbChild [ $actualChild ];
2009-03-10 23:08:52 +01:00
if (( ! ( $child instanceof Folder ) && is_dir ( $baseDir . $actualChild ) )
|| (( $child instanceof Folder ) && ! is_dir ( $baseDir . $actualChild )) ) {
2008-11-24 00:28:16 +01:00
DB :: query ( " DELETE FROM \" File \" WHERE \" ID \" = $child->ID " );
2007-07-19 12:40:28 +02:00
unset ( $hasDbChild [ $actualChild ]);
}
}
if ( isset ( $hasDbChild [ $actualChild ])) {
$child = $hasDbChild [ $actualChild ];
unset ( $unwantedDbChildren [ $actualChild ]);
} else {
$added ++ ;
$childID = $this -> constructChild ( $actualChild );
$child = DataObject :: get_by_id ( " File " , $childID );
}
if ( $child && is_dir ( $baseDir . $actualChild )) {
$childResult = $child -> syncChildren ();
$added += $childResult [ 'added' ];
$deleted += $childResult [ 'deleted' ];
}
2009-10-21 04:30:48 +02:00
// Clean up the child record from memory after use. Important!
$child -> destroy ();
2009-10-21 04:32:11 +02:00
$child = null ;
2007-07-19 12:40:28 +02:00
}
// Iterate through the unwanted children, removing them all
if ( isset ( $unwantedDbChildren )) foreach ( $unwantedDbChildren as $unwantedDbChild ) {
2008-11-24 00:28:16 +01:00
DB :: query ( " DELETE FROM \" File \" WHERE \" ID \" = $unwantedDbChild->ID " );
2007-07-19 12:40:28 +02:00
$deleted ++ ;
}
} else {
2008-11-24 00:28:16 +01:00
DB :: query ( " DELETE FROM \" File \" WHERE \" ID \" = $this->ID " );
2007-07-19 12:40:28 +02:00
}
return array ( 'added' => $added , 'deleted' => $deleted );
}
/**
* Construct a child of this Folder with the given name .
* It does this without actually using the object model , as this starts messing
* with all the data . Rather , it does a direct database insert .
*/
function constructChild ( $name ) {
// Determine the class name - File, Folder or Image
$baseDir = $this -> FullPath ;
if ( is_dir ( $baseDir . $name )) {
$className = " Folder " ;
} else {
// Could use getimagesize to get the type of the image
$ext = strtolower ( substr ( $name , strrpos ( $name , '.' ) + 1 ));
switch ( $ext ) {
case " gif " : case " jpg " : case " jpeg " : case " png " : $className = " Image " ; break ;
default : $className = " File " ;
}
}
if ( Member :: currentUser ()) $ownerID = Member :: currentUser () -> ID ;
2007-10-02 06:56:43 +02:00
else $ownerID = 0 ;
2007-07-19 12:40:28 +02:00
2009-04-06 01:24:56 +02:00
$filename = DB :: getConn () -> addslashes ( $this -> Filename . $name );
2007-07-19 12:40:28 +02:00
if ( $className == 'Folder' ) $filename .= '/' ;
2009-04-06 01:24:56 +02:00
$name = DB :: getConn () -> addslashes ( $name );
2007-07-19 12:40:28 +02:00
2008-11-24 10:31:14 +01:00
DB :: query ( " INSERT INTO \" File \"
( \ " ClassName \" , \" ParentID \" , \" OwnerID \" , \" Name \" , \" Filename \" , \" Created \" , \" LastEdited \" , \" Title \" )
2009-03-11 22:50:03 +01:00
VALUES ( '$className' , $this -> ID , $ownerID , '$name' , '$filename' , " . DB::getConn()->now() . ',' . DB::getConn()->now() . " , '$name' ) " );
2007-07-19 12:40:28 +02:00
2007-09-14 03:36:32 +02:00
return DB :: getGeneratedID ( " File " );
2007-07-19 12:40:28 +02:00
}
/**
* Take a file uploaded via a POST form , and save it inside this folder .
*/
function addUploadToFolder ( $tmpFile ) {
if ( ! is_array ( $tmpFile )) {
2008-04-06 06:10:51 +02:00
user_error ( " Folder::addUploadToFolder() Not passed an array. Most likely, the form hasn't got the right enctype " , E_USER_ERROR );
2007-07-19 12:40:28 +02:00
}
if ( ! $tmpFile [ 'size' ]) {
return ;
}
ENHANCEMENT Introduced constants for system paths like /sapphire in preparation for a more flexible directory reorganisation. Instead of hardcoding your path, please use the following constants: BASE_PATH, BASE_URL, SAPPHIRE_DIR, SAPPHIRE_PATH, CMS_DIR, CMS_PATH, THIRDPARTY_DIR, THIRDPARTY_PATH, ASSETS_DIR, ASSETS_PATH, THEMES_DIR, THEMES_PATH
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@63154 467b73ca-7a2a-4603-9d3b-597d59a354a9
2008-09-27 18:02:38 +02:00
$base = BASE_PATH ;
2007-07-19 12:40:28 +02:00
// $parentFolder = Folder::findOrMake("Uploads");
// Generate default filename
$file = str_replace ( ' ' , '-' , $tmpFile [ 'name' ]);
$file = ereg_replace ( '[^A-Za-z0-9+.-]+' , '' , $file );
$file = ereg_replace ( '-+' , '-' , $file );
$file = $this -> RelativePath . $file ;
Filesystem :: makeFolder ( dirname ( " $base / $file " ));
while ( file_exists ( " $base / $file " )) {
$i = isset ( $i ) ? ( $i + 1 ) : 2 ;
$oldFile = $file ;
$file = ereg_replace ( '[0-9]*(\.[^.]+$)' , $i . '\\1' , $file );
if ( $oldFile == $file && $i > 2 ) user_error ( " Couldn't fix $file with $i " , E_USER_ERROR );
}
2009-09-04 00:45:15 +02:00
if ( move_uploaded_file ( $tmpFile [ 'tmp_name' ], " $base / $file " )) {
2007-07-19 12:40:28 +02:00
// Update with the new image
return $this -> constructChild ( basename ( $file ));
} else {
2009-07-02 00:27:18 +02:00
if ( ! file_exists ( $tmpFile [ 'tmp_name' ])) user_error ( " Folder::addUploadToFolder: ' $tmpFile[tmp_name] ' doesn't exist " , E_USER_ERROR );
else user_error ( " Folder::addUploadToFolder: Couldn't copy ' $tmpFile[tmp_name] ' to ' $fullFilename ' " , E_USER_ERROR );
2007-07-19 12:40:28 +02:00
return false ;
}
}
//-------------------------------------------------------------------------------------------------
// Data Model Definition
function getRelativePath () {
return parent :: getRelativePath () . " / " ;
}
function onBeforeDelete () {
2009-01-10 12:35:50 +01:00
if ( $this -> ID && ( $children = $this -> AllChildren ())) {
2007-07-19 12:40:28 +02:00
foreach ( $children as $child ) {
if ( ! $this -> Filename || ! $this -> Name || ! file_exists ( $this -> getFullPath ())) {
$child -> setField ( 'Name' , null );
$child -> Filename = null ;
}
$child -> delete ();
}
}
// Do this after so a folder's contents are removed before we delete the folder.
if ( $this -> Filename && $this -> Name && file_exists ( $this -> getFullPath ())) {
$files = glob ( $this -> getFullPath () . '/*' );
if ( ! $files || ( count ( $files ) == 1 && preg_match ( '/\/_resampled$/' , $files [ 0 ] ) ) )
Filesystem :: removeFolder ( $this -> getFullPath () );
}
parent :: onBeforeDelete ();
}
/**
* Delete the database record ( recursively for folders ) without touching the filesystem
*/
function deleteDatabaseOnly () {
if ( $children = $this -> myChildren ()) {
foreach ( $children as $child ) $child -> deleteDatabaseOnly ();
}
parent :: deleteDatabaseOnly ();
}
public function myChildren () {
// Ugly, but functional.
$ancestors = ClassInfo :: ancestry ( $this -> class );
foreach ( $ancestors as $i => $a ) {
if ( isset ( $baseClass ) && $baseClass === - 1 ) {
$baseClass = $a ;
break ;
}
if ( $a == " DataObject " ) $baseClass = - 1 ;
}
2008-11-24 10:31:14 +01:00
$g = DataObject :: get ( $baseClass , " \" ParentID \" = " . $this -> ID );
2007-07-19 12:40:28 +02:00
return $g ;
}
/**
* Returns true if this folder has children
*/
public function hasChildren () {
2009-07-02 00:27:18 +02:00
return ( bool ) DB :: query ( " SELECT COUNT(*) FROM \" File \" WHERE ParentID = "
. ( int ) $this -> ID ) -> value ();
}
/**
* Returns true if this folder has children
*/
public function hasChildFolders () {
$SQL_folderClasses = Convert :: raw2sql ( ClassInfo :: subclassesFor ( 'Folder' ));
return ( bool ) DB :: query ( " SELECT COUNT(*) FROM \" File \" WHERE ParentID = " . ( int ) $this -> ID
. " AND \" ClassName \" IN (' " . implode ( " ',' " , $SQL_folderClasses ) . " ') " ) -> value ();
2007-07-19 12:40:28 +02:00
}
/**
* Overload autosetFilename () to call autosetFilename () on all the children , too
*/
public function autosetFilename () {
parent :: autosetFilename ();
2009-01-10 12:35:50 +01:00
if ( $this -> ID && ( $children = $this -> AllChildren ())) {
2007-07-19 12:40:28 +02:00
$this -> write ();
foreach ( $children as $child ) {
$child -> autosetFilename ();
$child -> write ();
}
}
}
/**
* Overload resetFilename () to call resetFilename () on all the children , too .
* Pass renamePhysicalFile = false , since the folder renaming will have taken care of this
*/
protected function resetFilename ( $renamePhysicalFile = true ) {
parent :: resetFilename ( $renamePhysicalFile );
2009-01-10 12:35:50 +01:00
if ( $this -> ID && ( $children = $this -> AllChildren ())) {
2007-07-19 12:40:28 +02:00
$this -> write ();
foreach ( $children as $child ) {
$child -> resetFilename ( false );
$child -> write ();
}
}
}
/**
* This isn ' t a decendant of SiteTree , but needs this in case
* the group is " reorganised " ;
*/
function cmsCleanup_parentChanged (){
}
2008-02-25 03:10:37 +01:00
/**
* Return the FieldSet used to edit this folder in the CMS .
* You can modify this fieldset by subclassing folder , or by creating a { @ link DataObjectDecorator }
* and implemeting updateCMSFields ( FieldSet $fields ) on that decorator .
*/
function getCMSFields () {
$nameField = ( $this -> ID > 0 ) ? new TextField ( " Name " ) : new HiddenField ( " Name " );
$fileList = new AssetTableField (
$this ,
" Files " ,
" File " ,
2009-10-16 00:19:25 +02:00
array ( " Title " => _t ( 'Folder.TITLE' , " Title " ), " Filename " => _t ( 'Folder.FILENAME' , " Filename " )),
2008-02-25 03:10:37 +01:00
" "
);
$fileList -> setFolder ( $this );
2008-10-10 19:35:24 +02:00
$fileList -> setPopupCaption ( _t ( 'Folder.VIEWEDITASSET' , " View/Edit Asset " ));
2008-02-25 03:10:37 +01:00
2009-01-05 07:19:48 +01:00
$nameField = ( $this -> ID && $this -> ID != " root " ) ? new TextField ( " Name " , _t ( 'Folder.TITLE' )) : new HiddenField ( " Name " );
2008-11-07 13:17:42 +01:00
if ( $this -> canEdit () ) {
2008-10-10 19:35:24 +02:00
$deleteButton = new InlineFormAction ( 'deletemarked' , _t ( 'Folder.DELSELECTED' , 'Delete selected files' ), 'delete' );
2008-02-25 03:10:37 +01:00
$deleteButton -> includeDefaultJS ( false );
} else {
$deleteButton = new HiddenField ( 'deletemarked' );
}
$fields = new FieldSet (
new HiddenField ( " Title " ),
new TabSet ( " Root " ,
2008-10-10 19:35:24 +02:00
new Tab ( " Files " , _t ( 'Folder.FILESTAB' , " Files " ),
2008-02-25 03:10:37 +01:00
$nameField ,
$fileList ,
$deleteButton ,
new HiddenField ( " FileIDs " ),
new HiddenField ( " DestFolderID " )
),
2008-10-10 19:35:24 +02:00
new Tab ( " Details " , _t ( 'Folder.DETAILSTAB' , " Details " ),
new ReadonlyField ( " URL " , _t ( 'Folder.URL' , 'URL' )),
new ReadonlyField ( " ClassName " , _t ( 'Folder.TYPE' , 'Type' )),
new ReadonlyField ( " Created " , _t ( 'Folder.CREATED' , 'First Uploaded' )),
new ReadonlyField ( " LastEdited " , _t ( 'Folder.LASTEDITED' , 'Last Updated' ))
2008-02-25 03:10:37 +01:00
),
2008-10-10 19:35:24 +02:00
new Tab ( " Upload " , _t ( 'Folder.UPLOADTAB' , " Upload " ),
2008-02-25 03:10:37 +01:00
new LiteralField ( " UploadIframe " ,
$this -> getUploadIframe ()
)
2009-07-02 00:27:18 +02:00
) /* ,
2008-10-10 19:35:24 +02:00
new Tab ( " UnusedFiles " , _t ( 'Folder.UNUSEDFILESTAB' , " Unused files " ),
2009-04-29 03:20:24 +02:00
new Folder_UnusedAssetsField ( $this )
2009-07-02 00:27:18 +02:00
) */
2009-04-29 01:52:15 +02:00
),
2008-02-25 03:10:37 +01:00
new HiddenField ( " ID " )
);
2009-05-14 07:26:47 +02:00
if ( ! $this -> canEdit ()) {
$fields -> removeFieldFromTab ( " Root " , " Upload " );
}
2008-02-25 03:10:37 +01:00
$this -> extend ( 'updateCMSFields' , $fields );
return $fields ;
}
2008-05-24 07:19:42 +02:00
/**
* Looks for files used in system and create where clause which contains all ID ' s of files .
*
* @ returns String where clause which will work as filter .
*/
2009-04-29 03:20:24 +02:00
public function getUsedFilesList () {
2009-05-11 07:30:29 +02:00
$result = DB :: query ( " SELECT DISTINCT \" FileID \" FROM \" SiteTree_ImageTracking \" " );
$usedFiles = array ();
$where = '' ;
$classes = ClassInfo :: subclassesFor ( 'SiteTree' );
if ( $result -> numRecords () > 0 ) {
while ( $nextResult = $result -> next ()) {
$where .= $nextResult [ 'FileID' ] . ',' ;
}
}
foreach ( $classes as $className ) {
$query = singleton ( $className ) -> extendedSQL ();
$ids = $query -> execute () -> column ();
if ( ! count ( $ids )) continue ;
foreach ( singleton ( $className ) -> has_one () as $fieldName => $joinClass ) {
if ( $joinClass == 'Image' || $joinClass == 'File' ) {
foreach ( $ids as $id ) {
$object = DataObject :: get_by_id ( $className , ( int ) $id );
if ( $object && $object -> $fieldName != NULL ) $usedFiles [] = $object -> $fieldName ;
unset ( $object );
}
} elseif ( $joinClass == 'Folder' ) {
// @todo
}
}
}
foreach ( $usedFiles as $file ) {
$where .= $file -> ID . ',' ;
}
if ( $where == " " ) return " ( \" ClassName \" = 'File' OR \" ClassName \" = 'Image') " ;
$where = substr ( $where , 0 , strlen ( $where ) - 1 );
$where = " \" File \" . \" ID \" NOT IN ( " . $where . " ) AND ( \" ClassName \" = 'File' OR \" ClassName \" = 'Image') " ;
return $where ;
2008-05-24 07:19:42 +02:00
}
2008-02-25 03:10:37 +01:00
/**
* Display the upload form . Returns an iframe tag that will show admin / assets / uploadiframe .
*/
function getUploadIframe () {
return <<< HTML
2009-02-02 00:49:53 +01:00
< iframe name = " AssetAdmin_upload " src = " admin/assets/uploadiframe/ { $this -> ID } " id = " AssetAdmin_upload " border = " 0 " style = " border-style none !important; width: 97%; min-height: 300px; height: 100%; height: expression(document.body.clientHeight) !important; " >
2008-02-25 03:10:37 +01:00
</ iframe >
HTML ;
}
2007-12-13 22:47:07 +01:00
2009-07-02 00:27:18 +02:00
/**
* Get the children of this folder that are also folders .
*/
function ChildFolders () {
2009-08-31 08:06:44 +02:00
return DataObject :: get ( " Folder " , " \" ParentID \" = " . ( int ) $this -> ID );
2009-07-02 00:27:18 +02:00
}
2007-07-19 12:40:28 +02:00
}
2009-04-29 03:20:24 +02:00
class Folder_UnusedAssetsField extends CompositeField {
protected $folder ;
public function __construct ( $folder ) {
$this -> folder = $folder ;
parent :: __construct ( new FieldSet ());
}
public function getChildren () {
if ( $this -> children -> Count () == 0 ) {
$inlineFormAction = new InlineFormAction ( " delete_unused_thumbnails " , _t ( 'Folder.DELETEUNUSEDTHUMBNAILS' , 'Delete unused thumbnails' ));
$inlineFormAction -> includeDefaultJS ( false ) ;
$this -> children = new FieldSet (
new LiteralField ( " UnusedAssets " , " <h2> " . _t ( 'Folder.UNUSEDFILESTITLE' , 'Unused files' ) . " </h2> " ),
$this -> getAssetList (),
new FieldGroup (
new LiteralField ( " UnusedThumbnails " , " <h2> " . _t ( 'Folder.UNUSEDTHUMBNAILSTITLE' , 'Unused thumbnails' ) . " </h2> " ),
$inlineFormAction
)
);
$this -> children -> setForm ( $this -> form );
}
return $this -> children ;
}
public function FieldHolder () {
$output = " " ;
foreach ( $this -> getChildren () as $child ) {
$output .= $child -> FieldHolder ();
}
return $output ;
}
/**
* Creates table for displaying unused files .
*
* @ returns AssetTableField
*/
protected function getAssetList () {
$where = $this -> folder -> getUsedFilesList ();
$assetList = new AssetTableField (
$this -> folder ,
" AssetList " ,
" File " ,
array ( " Title " => _t ( 'Folder.TITLE' , " Title " ), " LinkedURL " => _t ( 'Folder.FILENAME' , " Filename " )),
" " ,
$where
);
$assetList -> setPopupCaption ( _t ( 'Folder.VIEWASSET' , " View Asset " ));
$assetList -> setPermissions ( array ( " show " , " delete " ));
$assetList -> Markable = false ;
return $assetList ;
}
}
2009-10-16 00:19:25 +02:00
?>