1 <?php
2 3 4 5 6 7
8 class Folder extends File {
9
10 static $default_sort = "\"Sort\"";
11
12 13 14 15 16
17 static function findOrMake($folderPath) {
18
19 if(!file_exists(ASSETS_PATH)) mkdir(ASSETS_PATH,Filesystem::$folder_create_mask);
20
21 $folderPath = trim(Director::makeRelative($folderPath));
22
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;
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 51
52 function syncChildren() {
53 $parentID = (int)$this->ID;
54 $added = 0;
55 $deleted = 0;
56
57
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
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
76
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
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
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
129 $child->destroy();
130 $child = null;
131 }
132
133
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 147 148 149
150 function constructChild($name) {
151
152 $baseDir = $this->FullPath;
153 if(is_dir($baseDir . $name)) {
154 $className = "Folder";
155 } else {
156
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 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
192
193
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
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
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
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
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
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 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
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 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 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 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 347 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 364 365
366 function cmsCleanup_parentChanged(){
367
368 }
369
370 371 372 373 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 416 417 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 434 435 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
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 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 495
496 function ChildFolders() {
497 return DataObject::get("Folder", "\"ParentID\" = " . (int)$this->ID);
498 }
499 }
500
501 502 503 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 542 543 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.
-