2007-07-19 12:40:28 +02:00
< ? php
2008-02-25 03:10:37 +01:00
/**
2010-10-13 01:02:10 +02:00
* Represents a folder in the assets / directory .
2010-10-15 05:12:39 +02:00
* The folder path is stored in the " Filename " property .
*
* Updating the " Name " or " Filename " properties on
* a folder object also updates all associated children
* ( both { @ link File } and { @ link Folder } records ) .
*
2010-10-15 05:20:47 +02:00
* Deleting a folder will also remove the folder from the filesystem ,
* including any subfolders and contained files . Use { @ link deleteDatabaseOnly ()}
* to avoid touching the filesystem .
*
2010-10-15 05:12:39 +02:00
* See { @ link File } documentation for more details about the
2012-03-24 04:38:57 +01:00
* relationship between the database and filesystem in the SilverStripe file APIs .
2010-10-13 01:02:10 +02:00
*
2012-04-12 08:02:46 +02:00
* @ package framework
2008-02-25 03:10:37 +01:00
* @ subpackage filesystem
*/
2007-07-19 12:40:28 +02:00
class Folder extends File {
2009-11-21 03:32:40 +01:00
static $singular_name = " Folder " ;
static $plural_name = " Folders " ;
2010-10-13 03:01:12 +02:00
2012-04-11 08:12:54 +02:00
static $default_sort = " \" Name \" " ;
2007-07-19 12:40:28 +02:00
2011-12-06 01:56:24 +01:00
/**
*
*/
public function populateDefaults () {
2011-03-15 10:30:31 +01:00
parent :: populateDefaults ();
if ( ! $this -> Name ) $this -> Name = _t ( 'AssetAdmin.NEWFOLDER' , " NewFolder " );
}
2011-12-06 01:56:24 +01:00
/**
* @ param $folderPath string Absolute or relative path to the file .
* If path is relative , its interpreted relative to the " assets/ " directory .
* @ return Folder
* @ deprecated in favor of the correct name find_or_make
*/
public static function findOrMake ( $folderPath ) {
2012-07-13 11:37:35 +02:00
Deprecation :: notice ( '3.0' , " Use Folder::find_or_make() instead. " );
2011-12-06 01:56:24 +01:00
return self :: find_or_make ( $folderPath );
}
2010-10-15 05:12:39 +02:00
/**
* Find the given folder or create it both as { @ link Folder } database records
* and on the filesystem . If necessary , creates parent folders as well .
2008-03-17 23:51:25 +01:00
*
2010-10-15 05:21:41 +02:00
* @ param $folderPath string Absolute or relative path to the file .
* If path is relative , its interpreted relative to the " assets/ " directory .
2010-10-15 05:12:39 +02:00
* @ return Folder
2007-07-19 12:40:28 +02:00
*/
2011-12-06 01:56:24 +01:00
public static function find_or_make ( $folderPath ) {
2010-04-14 06:42:59 +02:00
// Create assets directory, if it is missing
2010-10-15 05:21:23 +02:00
if ( ! file_exists ( ASSETS_PATH )) Filesystem :: makeFolder ( ASSETS_PATH );
2010-04-14 06:42:59 +02:00
2008-03-17 23:51:25 +01:00
$folderPath = trim ( Director :: makeRelative ( $folderPath ));
// replace leading and trailing slashes
$folderPath = preg_replace ( '/^\/?(.*)\/?$/' , '$1' , $folderPath );
$parts = explode ( " / " , $folderPath );
2010-10-13 01:02:10 +02:00
2007-07-19 12:40:28 +02:00
$parentID = 0 ;
2010-10-15 05:21:41 +02:00
$item = null ;
2007-07-19 12:40:28 +02:00
foreach ( $parts as $part ) {
2010-10-13 01:02:10 +02:00
if ( ! $part ) continue ; // happens for paths with a trailing slash
2011-03-21 04:12:37 +01:00
$item = DataObject :: get_one (
" Folder " ,
sprintf (
" \" Name \" = '%s' AND \" ParentID \" = %d " ,
Convert :: raw2sql ( $part ),
( int ) $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 ();
2010-04-13 01:45:53 +02:00
}
if ( ! file_exists ( $item -> getFullPath ())) {
2010-10-15 05:21:23 +02:00
Filesystem :: makeFolder ( $item -> getFullPath ());
2007-07-19 12:40:28 +02:00
}
$parentID = $item -> ID ;
}
2010-10-15 05:21:41 +02:00
2007-07-19 12:40:28 +02:00
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 ) {
2011-09-15 14:15:41 +02:00
$childName = Convert :: raw2sql ( $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 ;
2012-06-15 01:28:29 +02:00
// if we're syncing a folder with no ID, we assume we're syncing the root assets folder
// however the Filename field is populated with "NewFolder", so we need to set this to empty
// to satisfy the baseDir variable below, which is the root folder to scan for new files in
if ( ! $parentID ) $this -> Filename = '' ;
2007-07-19 12:40:28 +02:00
// Iterate through the actual children, correcting the database as necessary
$baseDir = $this -> FullPath ;
2012-06-15 01:28:29 +02:00
// @todo this shouldn't call die() but log instead
if ( $parentID && ! $this -> Filename ) die ( $this -> ID . " - " . $this -> FullPath );
2007-07-19 12:40:28 +02:00
if ( file_exists ( $baseDir )) {
$actualChildren = scandir ( $baseDir );
foreach ( $actualChildren as $actualChild ) {
2010-12-05 01:05:05 +01:00
if ( $actualChild [ 0 ] == '.' || $actualChild [ 0 ] == '_' || substr ( $actualChild , 0 , 6 ) == 'Thumbs' || $actualChild == 'web.config' ) {
2010-01-21 23:59:19 +01:00
continue ;
}
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 {
2011-08-22 13:04:15 +02:00
$className = File :: get_class_for_file_extension ( pathinfo ( $name , PATHINFO_EXTENSION ));
2007-07-19 12:40:28 +02:00
}
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
2011-09-15 14:15:41 +02:00
$filename = Convert :: raw2sql ( $this -> Filename . $name );
2007-07-19 12:40:28 +02:00
if ( $className == 'Folder' ) $filename .= '/' ;
2011-09-15 14:15:41 +02:00
$name = Convert :: raw2sql ( $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 .
2011-08-26 17:57:05 +02:00
* File names are filtered through { @ link FileNameFilter }, see class documentation
* on how to influence this behaviour .
2007-07-19 12:40:28 +02:00
*/
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
}
2010-05-25 05:04:55 +02:00
if ( ! isset ( $tmpFile [ 'size' ])) {
2007-07-19 12:40:28 +02:00
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
2012-04-04 16:59:30 +02:00
$nameFilter = FileNameFilter :: create ();
2011-08-26 17:57:05 +02:00
$file = $nameFilter -> filter ( $tmpFile [ 'name' ]);
2010-01-21 23:59:19 +01:00
while ( $file [ 0 ] == '_' || $file [ 0 ] == '.' ) {
$file = substr ( $file , 1 );
}
2007-07-19 12:40:28 +02:00
$file = $this -> RelativePath . $file ;
Filesystem :: makeFolder ( dirname ( " $base / $file " ));
2010-05-25 05:04:55 +02:00
$doubleBarrelledExts = array ( '.gz' , '.bz' , '.bz2' );
$ext = " " ;
if ( preg_match ( '/^(.*)(\.[^.]+)$/' , $file , $matches )) {
$file = $matches [ 1 ];
$ext = $matches [ 2 ];
// Special case for double-barrelled
if ( in_array ( $ext , $doubleBarrelledExts ) && preg_match ( '/^(.*)(\.[^.]+)$/' , $file , $matches )) {
$file = $matches [ 1 ];
$ext = $matches [ 2 ] . $ext ;
}
}
$origFile = $file ;
$i = 1 ;
while ( file_exists ( " $base / $file $ext " )) {
$i ++ ;
2007-07-19 12:40:28 +02:00
$oldFile = $file ;
2010-10-04 06:42:13 +02:00
if ( strpos ( $file , '.' ) !== false ) {
2012-02-27 22:14:02 +01:00
$file = preg_replace ( '/[0-9]*(\.[^.]+$)/' , $i . '\\1' , $file );
2010-10-04 06:44:14 +02:00
} elseif ( strpos ( $file , '_' ) !== false ) {
2012-02-27 22:14:02 +01:00
$file = preg_replace ( '/_([^_]+$)/' , '_' . $i , $file );
2010-10-04 06:42:13 +02:00
} else {
2012-02-27 22:14:02 +01:00
$file .= '_' . $i ;
2010-10-04 06:42:13 +02:00
}
2012-02-27 22:14:02 +01:00
2010-05-25 05:04:55 +02:00
if ( $oldFile == $file && $i > 2 ) user_error ( " Couldn't fix $file $ext with $i " , E_USER_ERROR );
2007-07-19 12:40:28 +02:00
}
2010-05-25 05:04:55 +02:00
if ( move_uploaded_file ( $tmpFile [ 'tmp_name' ], " $base / $file $ext " )) {
2007-07-19 12:40:28 +02:00
// Update with the new image
2010-05-25 05:04:55 +02:00
return $this -> constructChild ( basename ( $file . $ext ));
2007-07-19 12:40:28 +02:00
} 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 );
2010-05-25 05:04:55 +02:00
else user_error ( " Folder::addUploadToFolder: Couldn't copy ' $tmpFile[tmp_name] ' to ' $base / $file $ext ' " , E_USER_ERROR );
2007-07-19 12:40:28 +02:00
return false ;
}
}
2010-10-13 01:02:10 +02:00
function validate () {
return new ValidationResult ( true );
}
2007-07-19 12:40:28 +02:00
//-------------------------------------------------------------------------------------------------
// Data Model Definition
function getRelativePath () {
return parent :: getRelativePath () . " / " ;
}
2010-10-15 05:20:47 +02:00
2007-07-19 12:40:28 +02:00
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 () );
}
2010-10-15 05:20:47 +02:00
2007-07-19 12:40:28 +02:00
parent :: onBeforeDelete ();
}
2011-03-23 01:23:43 +01:00
/** Override setting the Title of Folders to that Name , Filename and Title are always in sync .
* Note that this is not appropriate for files , because someone might want to create a human - readable name
* of a file that is different from its name on disk . But folders should always match their name on disk . */
function setTitle ( $title ) {
$this -> setField ( 'Title' , $title );
parent :: setName ( $title ); //set the name and filename to match the title
}
function setName ( $name ) {
$this -> setField ( 'Title' , $name );
parent :: setName ( $name );
}
function setFilename ( $filename ) {
2011-03-30 01:05:05 +02:00
$this -> setField ( 'Title' , pathinfo ( $filename , PATHINFO_BASENAME ));
2011-03-23 01:23:43 +01:00
parent :: setFilename ( $filename );
}
2012-02-15 19:33:14 +01:00
/**
* A folder doesn ' t have a ( meaningful ) file size .
*
* @ return Null
*/
function getSize () {
return null ;
}
2007-07-19 12:40:28 +02:00
/**
* 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' ));
2010-10-12 23:55:17 +02:00
return ( bool ) DB :: query ( " SELECT COUNT(*) FROM \" File \" WHERE \" ParentID \" = " . ( int ) $this -> ID
2009-07-02 00:27:18 +02:00
. " AND \" ClassName \" IN (' " . implode ( " ',' " , $SQL_folderClasses ) . " ') " ) -> value ();
2007-07-19 12:40:28 +02:00
}
/**
2010-10-15 05:20:47 +02:00
* Overloaded to call recursively on all contained { @ link File } records .
2007-07-19 12:40:28 +02:00
*/
2010-10-15 05:20:47 +02:00
public function updateFilesystem () {
parent :: updateFilesystem ();
2007-07-19 12:40:28 +02:00
2010-10-15 05:20:47 +02:00
// Note: Folders will have been renamed on the filesystem already at this point,
// File->updateFilesystem() needs to take this into account.
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 ) {
2010-10-15 05:20:47 +02:00
$child -> updateFilesystem ();
2007-07-19 12:40:28 +02:00
$child -> write ();
}
}
}
2008-02-25 03:10:37 +01:00
/**
2011-10-28 03:37:27 +02:00
* Return the FieldList used to edit this folder in the CMS .
* You can modify this FieldList by subclassing folder , or by creating a { @ link DataExtension }
2011-05-11 09:51:54 +02:00
* and implemeting updateCMSFields ( FieldList $fields ) on that extension .
2008-02-25 03:10:37 +01:00
*/
2012-04-13 15:46:47 +02:00
function getCMSFields () {
2012-02-21 23:19:49 +01:00
// Hide field on root level, which can't be renamed
2012-03-02 17:30:00 +01:00
if ( ! $this -> ID || $this -> ID === " root " ) {
$titleField = new HiddenField ( " Name " );
} else {
$titleField = new TextField ( " Name " , $this -> fieldLabel ( 'Name' ));
}
2011-05-11 09:51:54 +02:00
$fields = new FieldList (
2012-02-21 23:19:49 +01:00
$titleField ,
new HiddenField ( 'ParentID' )
2008-02-25 03:10:37 +01:00
);
$this -> extend ( 'updateCMSFields' , $fields );
return $fields ;
}
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
}
2009-11-21 03:33:42 +01:00
/**
* @ return String
*/
2012-04-17 22:29:45 +02:00
function CMSTreeClasses () {
2009-11-21 03:33:42 +01:00
$classes = sprintf ( 'class-%s' , $this -> class );
if ( ! $this -> canDelete ())
$classes .= " nodelete " ;
if ( ! $this -> canEdit ())
$classes .= " disabled " ;
$classes .= $this -> markingClasses ();
return $classes ;
}
2012-04-26 01:42:56 +02:00
/**
* @ return string
*/
function getTreeTitle () {
return $treeTitle = sprintf (
" <span class= \" jstree-foldericon \" ></span><span class= \" item \" >%s</span> " ,
Convert :: raw2xml ( str_replace ( array ( " \n " , " \r " ), " " , $this -> Title ))
);
}
2011-03-23 01:23:43 +01:00
}