Version: current

      1 <?php
      2 /**
      3  * Represents a folder in the assets/ directory.
      4  *
      5  * @package sapphire
      6  * @subpackage filesystem
      7  */
      8 class Folder extends File {
      9
     10     static $default_sort = "\"Sort\"";
     11
     12     /*
     13      * Find the given folder or create it, recursively.
     14      *
     15      * @param $folderPath string Absolute or relative path to the file
     16      */
     17     static function findOrMake($folderPath) {
     18         // Create assets directory, if it is missing
     19         if(!file_exists(ASSETS_PATH)) mkdir(ASSETS_PATH,Filesystem::$folder_create_mask);
     20
     21         $folderPath = trim(Director::makeRelative($folderPath));
     22         // replace leading and trailing slashes
     23         $folderPath = preg_replace('/^\/?(.*)\/?$/', '$1', $folderPath);
     24
     25         $parts = explode("/",$folderPath);
     26         if ($parts[0] == ASSETS_DIR) array_shift($parts);
     27
     28         $parentID = 0;
     29
     30         foreach($parts as $part) {
     31             if(!$part) continue; // happens for paths with a trailing slash
     32
     33             $item = DataObject::get_one("Folder", "\"Name\" = '$part' AND \"ParentID\" = $parentID");
     34             if(!$item) {
     35                 $item = new Folder();
     36                 $item->ParentID = $parentID;
     37                 $item->Name = $part;
     38                 $item->Title = $part;
     39                 $item->write();
     40             }
     41             if(!file_exists($item->getFullPath())) {
     42                 mkdir($item->getFullPath(),Filesystem::$folder_create_mask);
     43             }
     44             $parentID = $item->ID;
     45         }
     46         return $item;
     47     }
     48
     49     /**
     50      * Syncronise the file database with the actual content of the assets folder
     51      */
     52     function syncChildren() {
     53         $parentID = (int)$this->ID; // parentID = 0 on the singleton, used as the 'root node';
     54         $added = 0;
     55         $deleted = 0;
     56
     57         // First, merge any children that are duplicates
     58         $duplicateChildrenNames = DB::query("SELECT \"Name\" FROM \"File\" WHERE \"ParentID\" = $parentID GROUP BY \"Name\" HAVING count(*) > 1")->column();
     59         if($duplicateChildrenNames) foreach($duplicateChildrenNames as $childName) {
     60             $childName = DB::getConn()->addslashes($childName);
     61             // Note, we do this in the database rather than object-model; otherwise we get all sorts of problems about deleting files
     62             $children = DB::query("SELECT \"ID\" FROM \"File\" WHERE \"Name\" = '$childName' AND \"ParentID\" = $parentID")->column();
     63             if($children) {
     64                 $keptChild = array_shift($children);
     65                 foreach($children as $removedChild) {
     66                     DB::query("UPDATE \"File\" SET \"ParentID\" = $keptChild WHERE \"ParentID\" = $removedChild");
     67                     DB::query("DELETE FROM \"File\" WHERE \"ID\" = $removedChild");
     68                 }
     69             } else {
     70                 user_error("Inconsistent database issue: SELECT ID FROM \"File\" WHERE Name = '$childName' AND ParentID = $parentID should have returned data", E_USER_WARNING);
     71             }
     72         }
     73
     74
     75         // Get index of database content
     76         // We don't use DataObject so that things like subsites doesn't muck with this.
     77         $dbChildren = DB::query("SELECT * FROM \"File\" WHERE \"ParentID\" = $parentID");
     78         $hasDbChild = array();
     79         if($dbChildren) {
     80             foreach($dbChildren as $dbChild) {
     81                 $className = $dbChild['ClassName'];
     82                 if(!$className) $className = "File";
     83                 $hasDbChild[$dbChild['Name']] = new $className($dbChild);
     84             }
     85         }
     86         $unwantedDbChildren = $hasDbChild;
     87
     88
     89         // Iterate through the actual children, correcting the database as necessary
     90         $baseDir = $this->FullPath;
     91
     92         if(!$this->Filename) die($this->ID . " - " . $this->FullPath);
     93
     94
     95         if(file_exists($baseDir)) {
     96             $actualChildren = scandir($baseDir);
     97             foreach($actualChildren as $actualChild) {
     98                 if($actualChild[0] == '.' || $actualChild[0] == '_' || substr($actualChild,0,6) == 'Thumbs') {
     99                     continue;
    100                 }
    101
    102                 // A record with a bad class type doesn't deserve to exist. It must be purged!
    103                 if(isset($hasDbChild[$actualChild])) {
    104                     $child = $hasDbChild[$actualChild];
    105                     if(( !( $child instanceof Folder ) && is_dir($baseDir . $actualChild) )
    106                     || (( $child instanceof Folder ) && !is_dir($baseDir . $actualChild)) ) {
    107                         DB::query("DELETE FROM \"File\" WHERE \"ID\" = $child->ID");
    108                         unset($hasDbChild[$actualChild]);
    109                     }
    110                 }
    111
    112
    113                 if(isset($hasDbChild[$actualChild])) {
    114                     $child = $hasDbChild[$actualChild];
    115                     unset($unwantedDbChildren[$actualChild]);
    116                 } else {
    117                     $added++;
    118                     $childID = $this->constructChild($actualChild);
    119                     $child = DataObject::get_by_id("File", $childID);
    120                 }
    121
    122                 if( $child && is_dir($baseDir . $actualChild)) {
    123                     $childResult = $child->syncChildren();
    124                     $added += $childResult['added'];
    125                     $deleted += $childResult['deleted'];
    126                 }
    127
    128                 // Clean up the child record from memory after use. Important!
    129                 $child->destroy();
    130                 $child = null;
    131             }
    132
    133             // Iterate through the unwanted children, removing them all
    134             if(isset($unwantedDbChildren)) foreach($unwantedDbChildren as $unwantedDbChild) {
    135                 DB::query("DELETE FROM \"File\" WHERE \"ID\" = $unwantedDbChild->ID");
    136                 $deleted++;
    137             }
    138         } else {
    139             DB::query("DELETE FROM \"File\" WHERE \"ID\" = $this->ID");
    140         }
    141
    142         return array('added' => $added, 'deleted' => $deleted);
    143     }
    144
    145     /**
    146      * Construct a child of this Folder with the given name.
    147      * It does this without actually using the object model, as this starts messing
    148      * with all the data.  Rather, it does a direct database insert.
    149      */
    150     function constructChild($name) {
    151         // Determine the class name - File, Folder or Image
    152         $baseDir = $this->FullPath;
    153         if(is_dir($baseDir . $name)) {
    154             $className = "Folder";
    155         } else {
    156             // Could use getimagesize to get the type of the image
    157             $ext = strtolower(substr($name,strrpos($name,'.')+1));
    158             switch($ext) {
    159                 case "gif": case "jpg": case "jpeg": case "png": $className = "Image"; break;
    160                 default: $className = "File";
    161             }
    162         }
    163
    164         if(Member::currentUser()) $ownerID = Member::currentUser()->ID;
    165         else $ownerID = 0;
    166
    167         $filename = DB::getConn()->addslashes($this->Filename . $name);
    168         if($className == 'Folder' ) $filename .= '/';
    169
    170         $name = DB::getConn()->addslashes($name);
    171
    172         DB::query("INSERT INTO \"File\"
    173             (\"ClassName\", \"ParentID\", \"OwnerID\", \"Name\", \"Filename\", \"Created\", \"LastEdited\", \"Title\")
    174             VALUES ('$className', $this->ID, $ownerID, '$name', '$filename', " . DB::getConn()->now() . ',' . DB::getConn()->now() . ", '$name')");
    175
    176         return DB::getGeneratedID("File");
    177     }
    178
    179     /**
    180      * Take a file uploaded via a POST form, and save it inside this folder.
    181      */
    182     function addUploadToFolder($tmpFile) {
    183         if(!is_array($tmpFile)) {
    184             user_error("Folder::addUploadToFolder() Not passed an array.  Most likely, the form hasn't got the right enctype", E_USER_ERROR);
    185         }
    186         if(!isset($tmpFile['size'])) {
    187             return;
    188         }
    189
    190         $base = BASE_PATH;
    191         // $parentFolder = Folder::findOrMake("Uploads");
    192
    193         // Generate default filename
    194         $file = Convert::rus2lat($tmpFile['name']);
    195         $file = str_replace(' ', '-',$file);
    196         $file = ereg_replace('[^A-Za-z0-9+.-]+','',$file);
    197         $file = ereg_replace('-+', '-', $file);
    198         $file = ereg_replace('\.+', '.', $file);
    199
    200         while($file[0] == '_' || $file[0] == '.') {
    201             $file = substr($file, 1);
    202         }
    203
    204         $file = $this->RelativePath . $file;
    205         Filesystem::makeFolder(dirname("$base/$file"));
    206
    207         $doubleBarrelledExts = array('.gz', '.bz', '.bz2');
    208
    209         $ext = "";
    210         if(preg_match('/^(.*)(\.[^.]+)$/', $file, $matches)) {
    211             $file = $matches[1];
    212             $ext = $matches[2];
    213             // Special case for double-barrelled
    214             if(in_array($ext, $doubleBarrelledExts) && preg_match('/^(.*)(\.[^.]+)$/', $file, $matches)) {
    215                 $file = $matches[1];
    216                 $ext = $matches[2] . $ext;
    217             }
    218         }
    219         $origFile = $file;
    220
    221         $i = 1;
    222         while(file_exists("$base/$file$ext")) {
    223             $i++;
    224             $oldFile = $file;
    225
    226             if(strrpos($file, '_') !== false) {
    227                 $file = ereg_replace('_([^_]+$)', '_' . $i, $file);
    228             } else {
    229                 $file .= "_$i";
    230             }
    231
    232             if($oldFile == $file && $i > 2) user_error("Couldn't fix $file$ext with $i", E_USER_ERROR);
    233         }
    234
    235         //if (move_uploaded_file($tmpFile['tmp_name'], "$base/$file$ext")) {
    236                 $success = false;
    237                 if(!ini_get("open_basedir")) $success = move_uploaded_file($tmpFile['tmp_name'], "$base/$file$ext");
    238                 else $success = rename($tmpFile['tmp_name'], "$base/$file$ext");
    239
    240                 if ($success) {
    241                                     // Update with the new image
    242                                     chmod("$base/$file$ext", Filesystem::$file_create_mask);
    243                                     return $this->constructChild(basename($file . $ext));
    244
    245         } else {
    246             if(!file_exists($tmpFile['tmp_name'])) user_error("Folder::addUploadToFolder: '$tmpFile[tmp_name]' doesn't exist", E_USER_ERROR);
    247             else user_error("Folder::addUploadToFolder: Couldn't copy '$tmpFile[tmp_name]' to '$base/$file$ext'", E_USER_ERROR);
    248             return false;
    249         }
    250     }
    251
    252     function validate() {
    253         return new ValidationResult(true);
    254     }
    255
    256     //-------------------------------------------------------------------------------------------------
    257     // Data Model Definition
    258
    259     function getRelativePath() {
    260         return parent::getRelativePath() . "/";
    261     }
    262
    263
    264     function onBeforeDelete() {
    265         if($this->ID && ($children = $this->AllChildren())) {
    266             foreach($children as $child) {
    267                 if(!$this->Filename || !$this->Name || !file_exists($this->getFullPath())) {
    268                     $child->setField('Name',null);
    269                     $child->Filename = null;
    270                 }
    271                 $child->delete();
    272             }
    273         }
    274
    275         // Do this after so a folder's contents are removed before we delete the folder.
    276         if($this->Filename && $this->Name && file_exists($this->getFullPath())) {
    277             $files = glob( $this->getFullPath() . '/*' );
    278
    279             if( !$files || ( count( $files ) == 1 && preg_match( '/\/_resampled$/', $files[0] ) ) )
    280                 Filesystem::removeFolder( $this->getFullPath() );
    281         }
    282         parent::onBeforeDelete();
    283     }
    284
    285     /**
    286      * Delete the database record (recursively for folders) without touching the filesystem
    287      */
    288     function deleteDatabaseOnly() {
    289         if($children = $this->myChildren()) {
    290             foreach($children as $child) $child->deleteDatabaseOnly();
    291         }
    292
    293         parent::deleteDatabaseOnly();
    294     }
    295
    296     public function myChildren() {
    297         // Ugly, but functional.
    298         $ancestors = ClassInfo::ancestry($this->class);
    299         foreach($ancestors as $i => $a) {
    300             if(isset($baseClass) && $baseClass === -1) {
    301                 $baseClass = $a;
    302                 break;
    303             }
    304             if($a == "DataObject") $baseClass = -1;
    305         }
    306
    307         $g = DataObject::get($baseClass, "\"ParentID\" = " . $this->ID);
    308         return $g;
    309     }
    310
    311     /**
    312      * Returns true if this folder has children
    313      */
    314     public function hasChildren() {
    315         return (bool)DB::query("SELECT COUNT(*) FROM \"File\" WHERE ParentID = "
    316             . (int)$this->ID)->value();
    317     }
    318
    319     /**
    320      * Returns true if this folder has children
    321      */
    322     public function hasChildFolders() {
    323         $SQL_folderClasses = Convert::raw2sql(ClassInfo::subclassesFor('Folder'));
    324
    325         return (bool)DB::query("SELECT COUNT(*) FROM \"File\" WHERE \"ParentID\" = " . (int)$this->ID
    326             . " AND \"ClassName\" IN ('" . implode("','", $SQL_folderClasses) . "')")->value();
    327     }
    328
    329     /**
    330      * Overload autosetFilename() to call autosetFilename() on all the children, too
    331      */
    332     public function autosetFilename() {
    333         parent::autosetFilename();
    334
    335         if($this->ID && ($children = $this->AllChildren())) {
    336             $this->write();
    337
    338             foreach($children as $child) {
    339                 $child->autosetFilename();
    340                 $child->write();
    341             }
    342         }
    343     }
    344
    345     /**
    346      * Overload resetFilename() to call resetFilename() on all the children, too.
    347      * Pass renamePhysicalFile = false, since the folder renaming will have taken care of this
    348      */
    349     protected function resetFilename($renamePhysicalFile = true) {
    350         parent::resetFilename($renamePhysicalFile);
    351
    352         if($this->ID && ($children = $this->AllChildren())) {
    353             $this->write();
    354
    355             foreach($children as $child) {
    356                 $child->resetFilename(false);
    357                 $child->write();
    358             }
    359         }
    360     }
    361
    362     /**
    363      * This isn't a decendant of SiteTree, but needs this in case
    364      * the group is "reorganised";
    365      */
    366     function cmsCleanup_parentChanged(){
    367
    368     }
    369
    370     /**
    371      * Return the FieldSet used to edit this folder in the CMS.
    372      * You can modify this fieldset by subclassing folder, or by creating a {@link DataObjectDecorator}
    373      * and implemeting updateCMSFields(FieldSet $fields) on that decorator.
    374      */
    375     function getCMSFields() {
    376         $fileList = new AssetTableField(
    377             $this,
    378             "Files",
    379             "File",
    380             array("Title" => _t('Folder.TITLE', "Title"), "Filename" => _t('Folder.FILENAME', "Filename"), 'Size' => _t('Folder.FILESIZE', 'Size')),
    381             ""
    382         );
    383         $fileList->setFolder($this);
    384         $fileList->setPopupCaption(_t('Folder.VIEWEDITASSET', "View/Edit Asset"));
    385
    386         $titleField = ($this->ID && $this->ID != "root") ? new TextField("Title", _t('Folder.TITLE')) : new HiddenField("Title");
    387         if( $this->canEdit() ) {
    388             $deleteButton = new InlineFormAction('deletemarked',_t('Folder.DELSELECTED','Delete selected files'), 'delete');
    389             $deleteButton->includeDefaultJS(false);
    390         } else {
    391             $deleteButton = new HiddenField('deletemarked');
    392         }
    393
    394         $fields = new FieldSet(
    395             new HiddenField("Name"),
    396             new TabSet("Root",
    397                 new Tab("Files", _t('Folder.FILESTAB', "Files"),
    398                     $titleField,
    399                     $fileList,
    400                     $deleteButton,
    401                     new HiddenField("FileIDs"),
    402                     new HiddenField("DestFolderID")
    403                 ),
    404                 new Tab("Details", _t('Folder.DETAILSTAB', "Details"),
    405                     new ReadonlyField("URL", _t('Folder.URL', 'URL')),
    406                     new ReadonlyField("ClassName", _t('Folder.TYPE','Type')),
    407                     new ReadonlyField("Created", _t('Folder.CREATED','First Uploaded')),
    408                     new ReadonlyField("LastEdited", _t('Folder.LASTEDITED','Last Updated'))
    409                 ),
    410                 new Tab("Upload", _t('Folder.UPLOADTAB', "Upload"),
    411                     new LiteralField("UploadIframe",
    412                         $this->getUploadIframe()
    413                     )
    414                 )
    415                 /* // commenting out unused files tab till bugs are fixed
    416                 new Tab("UnusedFiles", _t('Folder.UNUSEDFILESTAB', "Unused files"),
    417                     new Folder_UnusedAssetsField($this)
    418                 ) */
    419             ),
    420             new HiddenField("ID")
    421         );
    422
    423         if(!$this->canEdit()) {
    424             $fields->removeFieldFromTab("Root", "Upload");
    425         }
    426
    427         $this->extend('updateCMSFields', $fields);
    428
    429         return $fields;
    430     }
    431
    432     /**
    433      * Looks for files used in system and create where clause which contains all ID's of files.
    434      *
    435      * @returns String where clause which will work as filter.
    436      */
    437     public function getUnusedFilesListFilter() {
    438         $result = DB::query("SELECT DISTINCT \"FileID\" FROM \"DataObjectFileTracking\"");
    439         if($result->numRecords() > 0) {
    440             return "\"File\".\"ID\" NOT IN (" . implode(', ', $result->column()) . ") AND (\"ClassName\" = 'File' OR \"ClassName\" = 'Image')";
    441         } else {
    442             return "(\"ClassName\" = 'File' OR \"ClassName\" = 'Image')";
    443         }
    444
    445         $usedFiles = array();
    446         $where = '';
    447         $classes = ClassInfo::subclassesFor('SiteTree');
    448
    449         if($result->numRecords() > 0) {
    450             while($nextResult = $result->next()) {
    451                 $where .= $nextResult['FileID'] . ',';
    452             }
    453         }
    454
    455         foreach($classes as $className) {
    456             $query = singleton($className)->extendedSQL();
    457             $ids = $query->execute()->column();
    458             if(!count($ids)) continue;
    459
    460             foreach(singleton($className)->has_one() as $relName => $joinClass) {
    461                 if($joinClass == 'Image' || $joinClass == 'File') {
    462                     $fieldName = $relName .'ID';
    463                     $query = singleton($className)->extendedSQL("$fieldName > 0");
    464                     $query->distinct = true;
    465                     $query->select = array($fieldName);
    466                     $usedFiles = array_merge($usedFiles, $query->execute()->column());
    467
    468                 } elseif($joinClass == 'Folder') {
    469                     // @todo
    470                 }
    471             }
    472         }
    473
    474         if($usedFiles) {
    475             return "\"File\".\"ID\" NOT IN (" . implode(', ', $usedFiles) . ") AND (\"ClassName\" = 'File' OR \"ClassName\" = 'Image')";
    476
    477         } else {
    478             return "(\"ClassName\" = 'File' OR \"ClassName\" = 'Image')";
    479         }
    480         return $where;
    481     }
    482
    483     /**
    484      * Display the upload form.  Returns an iframe tag that will show admin/assets/uploadiframe.
    485      */
    486     function getUploadIframe() {
    487         return <<<HTML
    488         <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;">
    489         </iframe>
    490 HTML;
    491     }
    492
    493     /**
    494      * Get the children of this folder that are also folders.
    495      */
    496     function ChildFolders() {
    497         return DataObject::get("Folder", "\"ParentID\" = " . (int)$this->ID);
    498     }
    499 }
    500
    501 /**
    502  * @package sapphire
    503  * @subpackage filesystem
    504  */
    505 class Folder_UnusedAssetsField extends CompositeField {
    506     protected $folder;
    507
    508     public function __construct($folder) {
    509         $this->folder = $folder;
    510         parent::__construct(new FieldSet());
    511     }
    512
    513     public function getChildren() {
    514         if($this->children->Count() == 0) {
    515             $inlineFormAction = new InlineFormAction("delete_unused_thumbnails", _t('Folder.DELETEUNUSEDTHUMBNAILS', 'Delete unused thumbnails'));
    516             $inlineFormAction->includeDefaultJS(false) ;
    517
    518             $this->children = new FieldSet(
    519                 new LiteralField( "UnusedAssets", "<h2>"._t('Folder.UNUSEDFILESTITLE', 'Unused files')."</h2>" ),
    520                 $this->getAssetList(),
    521                 new FieldGroup(
    522                     new LiteralField( "UnusedThumbnails", "<h2>"._t('Folder.UNUSEDTHUMBNAILSTITLE', 'Unused thumbnails')."</h2>"),
    523                     $inlineFormAction
    524                 )
    525             );
    526             $this->children->setForm($this->form);
    527         }
    528         return $this->children;
    529     }
    530
    531     public function FieldHolder() {
    532         $output = "";
    533         foreach($this->getChildren() as $child) {
    534             $output .= $child->FieldHolder();
    535         }
    536         return $output;
    537     }
    538
    539
    540     /**
    541      * Creates table for displaying unused files.
    542      *
    543      * @returns AssetTableField
    544      */
    545     protected function getAssetList() {
    546         $where = $this->folder->getUnusedFilesListFilter();
    547         $assetList = new AssetTableField(
    548             $this->folder,
    549             "AssetList",
    550             "File",
    551             array("Title" => _t('Folder.TITLE', "Title"), "LinkedURL" => _t('Folder.FILENAME', "Filename"), 'Size' => _t('Folder.FILESIZE', 'Size')),
    552             "",
    553             $where
    554         );
    555         $assetList->setPopupCaption(_t('Folder.VIEWASSET', "View Asset"));
    556         $assetList->setPermissions(array("show","delete"));
    557         $assetList->Markable = false;
    558         return $assetList;
    559     }
    560 }
    561 ?>
    562

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -