mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
API CHANGE Don't reflect changes in File and Folder property setters on filesystem before write() is called, to ensure that validate() applies in all cases. This fixes a problem where File->setName() would circumvent restrictions in File::$allowed_extensions (fixes #5693)
API CHANGE Removed File->resetFilename(), use File->updateFilesystem() to update the filesystem, and File->getRelativePath() to just update the "Filename" property without any filesystem changes (emulating the old $renamePhysicalFile method argument in resetFilename()) API CHANGE Removed File->autosetFilename(), please set the "Filename" property via File->getRelativePath() MINOR Added unit tests to FileTest and FolderTest (some of them copied from FileTest, to test Folder behaviour separately) ENHANCEMENT Added File::$allowed_extensions (backport from 2.4 to enable File->validate() security fix) git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/branches/2.3@108062 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
parent
c1295753b9
commit
bdd30fa4fd
@ -47,6 +47,26 @@ class File extends DataObject {
|
||||
"Hierarchy",
|
||||
);
|
||||
|
||||
/**
|
||||
* @var array List of allowed file extensions, enforced through {@link validate()}.
|
||||
*/
|
||||
public static $allowed_extensions = array(
|
||||
'','html','htm','xhtml','js','css',
|
||||
'bmp','png','gif','jpg','jpeg','ico','pcx','tif','tiff',
|
||||
'au','mid','midi','mpa','mp3','ogg','m4a','ra','wma','wav','cda',
|
||||
'avi','mpg','mpeg','asf','wmv','m4v','mov','mkv','mp4','swf','flv','ram','rm',
|
||||
'doc','docx','txt','rtf','xls','xlsx','pages',
|
||||
'ppt','pptx','pps','csv',
|
||||
'cab','arj','tar','zip','zipx','sit','sitx','gz','tgz','bz2','ace','arc','pkg','dmg','hqx','jar',
|
||||
'xml','pdf',
|
||||
);
|
||||
|
||||
/**
|
||||
* @var If this is true, then restrictions set in {@link $allowed_max_file_size} and
|
||||
* {@link $allowed_extensions} will be applied to users with admin privileges as
|
||||
* well.
|
||||
*/
|
||||
public static $apply_restrictions_to_admin = true;
|
||||
|
||||
/**
|
||||
* Cached result of a "SHOW FIELDS" call
|
||||
@ -99,7 +119,9 @@ class File extends DataObject {
|
||||
protected function onBeforeDelete() {
|
||||
parent::onBeforeDelete();
|
||||
|
||||
$this->autosetFilename();
|
||||
// ensure that the record is synced with the filesystem before deleting
|
||||
$this->updateFilesystem();
|
||||
|
||||
if($this->Filename && $this->Name && file_exists($this->getFullPath()) && !is_dir($this->getFullPath())) {
|
||||
unlink($this->getFullPath());
|
||||
}
|
||||
@ -245,12 +267,17 @@ class File extends DataObject {
|
||||
/**
|
||||
* 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!
|
||||
* record.
|
||||
*/
|
||||
protected function onBeforeWrite() {
|
||||
parent::onBeforeWrite();
|
||||
|
||||
if(!$this->Name) $this->Name = "new-" . strtolower($this->class);
|
||||
// Set default name
|
||||
if(!$this->getField('Name')) $this->Name = "new-" . strtolower($this->class);
|
||||
|
||||
// Set name on filesystem. If the current object is a "Folder", will also update references
|
||||
// to subfolders and contained file records (both in database and filesystem)
|
||||
$this->updateFilesystem();
|
||||
|
||||
if($brokenPages = $this->BackLinkTracking()) {
|
||||
foreach($brokenPages as $brokenPage) {
|
||||
@ -311,56 +338,70 @@ class File extends DataObject {
|
||||
}
|
||||
}
|
||||
|
||||
// Update title
|
||||
if(!$this->getField('Title')) $this->__set('Title', str_replace(array('-','_'),' ',ereg_replace('\.[^.]+$','',$name)));
|
||||
|
||||
// Update actual field value
|
||||
$this->setField('Name', $name);
|
||||
|
||||
|
||||
if($oldName && $oldName != $this->Name) {
|
||||
$this->resetFilename();
|
||||
} else {
|
||||
$this->autosetFilename();
|
||||
}
|
||||
|
||||
|
||||
// Ensure that the filename is updated as well (only in-memory)
|
||||
// Important: Circumvent the getter to avoid infinite loops
|
||||
$this->setField('Filename', $this->getRelativePath());
|
||||
|
||||
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.
|
||||
* Moving the file if appropriate according to updated database content.
|
||||
* Throws an Exception if the new file already exists.
|
||||
*
|
||||
* Caution: This method should just be called during a {@link write()} invocation,
|
||||
* as it relies on {@link DataObject->getChangedFields()}, which is reset after a {@link write()} call.
|
||||
* Might be called as {@link File->updateFilesystem()} from within {@link Folder->updateFilesystem()},
|
||||
* so it has to handle both files and folders.
|
||||
*
|
||||
* Assumes that the "Filename" property was previously updated, either directly or indirectly.
|
||||
* (it might have been influenced by {@link setName()} or {@link setParentID()} before).
|
||||
*/
|
||||
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() {
|
||||
public function updateFilesystem() {
|
||||
$changedFields = $this->getChangedFields();
|
||||
|
||||
// Regenerate "Filename", just to be sure
|
||||
$this->setField('Filename', $this->getRelativePath());
|
||||
|
||||
// If certain elements are changed, update the filesystem reference
|
||||
if(!isset($changedFields['Filename'])) return false;
|
||||
|
||||
$pathBefore = $changedFields['Filename']['before'];
|
||||
$pathAfter = $changedFields['Filename']['after'];
|
||||
|
||||
// If the file or folder didn't exist before, don't rename - its created
|
||||
if(!$pathBefore) return;
|
||||
|
||||
$pathBeforeAbs = Director::getAbsFile($pathBefore);
|
||||
$pathAfterAbs = Director::getAbsFile($pathAfter);
|
||||
|
||||
// Check that original file or folder exists, and rename on filesystem if required.
|
||||
// The folder of the path might've already been renamed by Folder->updateFilesystem()
|
||||
// before any filesystem update on contained file or subfolder records is triggered.
|
||||
if(!file_exists($pathAfterAbs)) {
|
||||
if(!is_a($this, 'Folder')) {
|
||||
// Only throw a fatal error if *both* before and after paths don't exist.
|
||||
if(!file_exists($pathBeforeAbs)) throw new Exception("Cannot move $pathBefore to $pathAfter - $pathBefore doesn't exist");
|
||||
|
||||
// Check that target directory (not the file itself) exists.
|
||||
// Only check if we're dealing with a file, otherwise the folder will need to be created
|
||||
if(!file_exists(dirname($pathAfterAbs))) throw new Exception("Cannot move $pathBefore to $pathAfter - Directory " . dirname($pathAfter) . " doesn't exist");
|
||||
}
|
||||
|
||||
// Rename file or folder
|
||||
$success = rename($pathBeforeAbs, $pathAfterAbs);
|
||||
if(!$success) throw new Exception("Cannot move $pathBeforeAbs to $pathAfterAbs");
|
||||
}
|
||||
|
||||
|
||||
// Update any database references
|
||||
$this->updateLinks($pathBeforeAbs, $pathAfterAbs);
|
||||
}
|
||||
|
||||
function setField( $field, $value ) {
|
||||
@ -385,11 +426,14 @@ class File extends DataObject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does not change the filesystem itself, please use {@link write()} for this.
|
||||
*/
|
||||
function setParentID($parentID) {
|
||||
$this->setField('ParentID', $parentID);
|
||||
|
||||
if($this->Name) $this->resetFilename();
|
||||
else $this->autosetFilename();
|
||||
// Don't change on the filesystem, we'll handle that in onBeforeWrite()
|
||||
$this->setField('Filename', $this->getRelativePath());
|
||||
|
||||
return $this->getField('ParentID');
|
||||
}
|
||||
@ -436,7 +480,7 @@ class File extends DataObject {
|
||||
function getRelativePath() {
|
||||
|
||||
if($this->ParentID) {
|
||||
$p = DataObject::get_one('Folder', "ID={$this->ParentID}");
|
||||
$p = DataObject::get_by_id('Folder', $this->ParentID);
|
||||
|
||||
if($p && $p->ID) return $p->getRelativePath() . $this->getField("Name");
|
||||
else return ASSETS_DIR . "/" . $this->getField("Name");
|
||||
@ -454,6 +498,7 @@ class File extends DataObject {
|
||||
}
|
||||
|
||||
function getFilename() {
|
||||
// Default behaviour: Return field if its set
|
||||
if($this->getField('Filename')) {
|
||||
return $this->getField('Filename');
|
||||
} else {
|
||||
@ -461,6 +506,9 @@ class File extends DataObject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does not change the filesystem itself, please use {@link write()} for this.
|
||||
*/
|
||||
function setFilename($val) {
|
||||
$this->setField('Filename', $val);
|
||||
$this->setField('Name', basename($val));
|
||||
@ -478,12 +526,19 @@ class File extends DataObject {
|
||||
/**
|
||||
* Gets the extension of a filepath or filename,
|
||||
* by stripping away everything before the last "dot".
|
||||
* Caution: Only returns the last extension in "double-barrelled"
|
||||
* extensions (e.g. "gz" for "tar.gz").
|
||||
*
|
||||
* Examples:
|
||||
* - "myfile" returns ""
|
||||
* - "myfile.txt" returns "txt"
|
||||
* - "myfile.tar.gz" returns "gz"
|
||||
*
|
||||
* @param string $filename
|
||||
* @return string
|
||||
*/
|
||||
public static function get_file_extension($filename) {
|
||||
return strtolower(substr($filename,strrpos($filename,'.')+1));
|
||||
return pathinfo($filename, PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -613,6 +668,33 @@ class File extends DataObject {
|
||||
return $labels;
|
||||
}
|
||||
|
||||
function validate() {
|
||||
if(File::$apply_restrictions_to_admin || !Permission::check('ADMIN')) {
|
||||
// Extension validation
|
||||
// TODO Merge this with Upload_Validator
|
||||
$extension = $this->getExtension();
|
||||
if($extension && !in_array($extension, self::$allowed_extensions)) {
|
||||
$exts = self::$allowed_extensions;
|
||||
sort($exts);
|
||||
$message = sprintf(
|
||||
_t(
|
||||
'File.INVALIDEXTENSION',
|
||||
'Extension is not allowed (valid: %s)',
|
||||
PR_MEDIUM,
|
||||
'Argument 1: Comma-separated list of valid extensions'
|
||||
),
|
||||
implode(', ',$exts)
|
||||
);
|
||||
return new ValidationResult(false, $message);
|
||||
}
|
||||
}
|
||||
|
||||
// We aren't validating for an existing "Filename" on the filesystem.
|
||||
// A record should still be saveable even if the underlying record has been removed.
|
||||
|
||||
return new ValidationResult(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
@ -51,7 +51,7 @@ class Filesystem extends Object {
|
||||
|
||||
$files = DataObject::get("File");
|
||||
foreach($files as $file) {
|
||||
$file->resetFilename();
|
||||
$file->updateFilesystem();
|
||||
echo "<li>", $file->Filename;
|
||||
$file->write();
|
||||
}
|
||||
|
@ -6,31 +6,41 @@
|
||||
*/
|
||||
class Folder extends File {
|
||||
|
||||
/*
|
||||
* Find the given folder or create it, recursively.
|
||||
/**
|
||||
* Find the given folder or create it both as {@link Folder} database records
|
||||
* and on the filesystem. If necessary, creates parent folders as well.
|
||||
*
|
||||
* @param $folderPath string Absolute or relative path to the file
|
||||
* @param $folderPath string Absolute or relative path to the file.
|
||||
* If path is relative, its interpreted relative to the "assets/" directory.
|
||||
* @return Folder
|
||||
*/
|
||||
static function findOrMake($folderPath) {
|
||||
// Create assets directory, if it is missing
|
||||
if(!file_exists(ASSETS_PATH)) Filesystem::makeFolder(ASSETS_PATH);
|
||||
|
||||
$folderPath = trim(Director::makeRelative($folderPath));
|
||||
// replace leading and trailing slashes
|
||||
$folderPath = preg_replace('/^\/?(.*)\/?$/', '$1', $folderPath);
|
||||
|
||||
$parts = explode("/",$folderPath);
|
||||
$parentID = 0;
|
||||
|
||||
$parentID = 0;
|
||||
$item = null;
|
||||
foreach($parts as $part) {
|
||||
$item = DataObject::get_one("Folder", "Name = '$part' AND ParentID = $parentID");
|
||||
if(!$part) continue; // happens for paths with a trailing slash
|
||||
$item = DataObject::get_one("Folder", "`Name` = '$part' AND `ParentID` = $parentID");
|
||||
if(!$item) {
|
||||
$item = new Folder();
|
||||
$item->ParentID = $parentID;
|
||||
$item->Name = $part;
|
||||
$item->Title = $part;
|
||||
$item->write();
|
||||
if(!file_exists($item->getFullPath())) mkdir($item->getFullPath(),Filesystem::$folder_create_mask);
|
||||
}
|
||||
if(!file_exists($item->getFullPath())) {
|
||||
Filesystem::makeFolder($item->getFullPath());
|
||||
}
|
||||
$parentID = $item->ID;
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
@ -201,6 +211,9 @@ class Folder extends File {
|
||||
}
|
||||
}
|
||||
|
||||
function validate() {
|
||||
return new ValidationResult(true);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------------------------------
|
||||
// Data Model Definition
|
||||
@ -261,37 +274,31 @@ class Folder extends File {
|
||||
* Returns true if this folder has children
|
||||
*/
|
||||
public function hasChildren() {
|
||||
return $this->ID && $this->myChildren() && $this->myChildren()->Count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload autosetFilename() to call autosetFilename() on all the children, too
|
||||
*/
|
||||
public function autosetFilename() {
|
||||
parent::autosetFilename();
|
||||
|
||||
if($this->ID && ($children = $this->AllChildren())) {
|
||||
$this->write();
|
||||
|
||||
foreach($children as $child) {
|
||||
$child->autosetFilename();
|
||||
$child->write();
|
||||
}
|
||||
}
|
||||
return (bool)DB::query("SELECT COUNT(*) FROM `File` WHERE `ParentID` = "
|
||||
. (int)$this->ID)->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload resetFilename() to call resetFilename() on all the children, too.
|
||||
* Pass renamePhysicalFile = false, since the folder renaming will have taken care of this
|
||||
* Returns true if this folder has children
|
||||
*/
|
||||
protected function resetFilename($renamePhysicalFile = true) {
|
||||
parent::resetFilename($renamePhysicalFile);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloaded to call recursively on all contained {@link File} records.
|
||||
*/
|
||||
public function updateFilesystem() {
|
||||
parent::updateFilesystem();
|
||||
|
||||
// Note: Folders will have been renamed on the filesystem already at this point,
|
||||
// File->updateFilesystem() needs to take this into account.
|
||||
if($this->ID && ($children = $this->AllChildren())) {
|
||||
$this->write();
|
||||
|
||||
foreach($children as $child) {
|
||||
$child->resetFilename(false);
|
||||
$child->updateFilesystem();
|
||||
$child->write();
|
||||
}
|
||||
}
|
||||
|
@ -4,57 +4,158 @@
|
||||
* Tests for the File class
|
||||
*/
|
||||
class FileTest extends SapphireTest {
|
||||
|
||||
static $fixture_file = 'sapphire/tests/filesystem/FileTest.yml';
|
||||
|
||||
function testCreateWithFilenameWithSubfolder() {
|
||||
// Note: We can't use fixtures/setUp() for this, as we want to create the db record manually.
|
||||
// Creating the folder is necessary to avoid having "Filename" overwritten by setName()/setRelativePath(),
|
||||
// because the parent folders don't exist in the database
|
||||
$folder = Folder::findOrMake('/FileTest/');
|
||||
$testfilePath = 'assets/FileTest/CreateWithFilenameHasCorrectPath.txt'; // Important: No leading slash
|
||||
$fh = fopen(BASE_PATH . '/' . $testfilePath, "w");
|
||||
fwrite($fh, str_repeat('x',1000000));
|
||||
fclose($fh);
|
||||
|
||||
$file = new File();
|
||||
$file->Filename = $testfilePath;
|
||||
// TODO This should be auto-detected
|
||||
$file->ParentID = $folder->ID;
|
||||
$file->write();
|
||||
|
||||
$this->assertEquals('CreateWithFilenameHasCorrectPath.txt', $file->Name, '"Name" property is automatically set from "Filename"');
|
||||
$this->assertEquals($testfilePath, $file->Filename, '"Filename" property remains unchanged');
|
||||
|
||||
// TODO This should be auto-detected, see File->updateFilesystem()
|
||||
// $this->assertType('Folder', $file->Parent(), 'Parent folder is created in database');
|
||||
// $this->assertFileExists($file->Parent()->getFullPath(), 'Parent folder is created on filesystem');
|
||||
// $this->assertEquals('FileTest', $file->Parent()->Name);
|
||||
// $this->assertType('Folder', $file->Parent()->Parent(), 'Grandparent folder is created in database');
|
||||
// $this->assertFileExists($file->Parent()->Parent()->getFullPath(), 'Grandparent folder is created on filesystem');
|
||||
// $this->assertEquals('assets', $file->Parent()->Parent()->Name);
|
||||
}
|
||||
|
||||
function testGetExtension() {
|
||||
$this->assertEquals('', File::get_file_extension('myfile'), 'No extension');
|
||||
$this->assertEquals('txt', File::get_file_extension('myfile.txt'), 'Simple extension');
|
||||
$this->assertEquals('gz', File::get_file_extension('myfile.tar.gz'), 'Double-barrelled extension only returns last bit');
|
||||
}
|
||||
|
||||
function testValidateExtension() {
|
||||
Session::set('loggedInAs', null);
|
||||
|
||||
$origExts = File::$allowed_extensions;
|
||||
File::$allowed_extensions = array('txt');
|
||||
|
||||
$file = $this->objFromFixture('File', 'asdf');
|
||||
|
||||
// Invalid ext
|
||||
$file->Name = 'asdf.php';
|
||||
$v = $file->validate();
|
||||
$this->assertFalse($v->valid());
|
||||
$this->assertContains('Extension is not allowed', $v->message());
|
||||
|
||||
// Valid ext
|
||||
$file->Name = 'asdf.txt';
|
||||
$v = $file->validate();
|
||||
$this->assertTrue($v->valid());
|
||||
|
||||
File::$allowed_extensions = $origExts;
|
||||
}
|
||||
|
||||
function testSetNameChangesFilesystemOnWrite() {
|
||||
$file = $this->objFromFixture('File', 'asdf');
|
||||
$oldPath = $file->getFullPath();
|
||||
|
||||
// Before write()
|
||||
$file->Name = 'renamed.txt';
|
||||
$this->assertFileExists($oldPath, 'Old path is still present');
|
||||
$this->assertFileNotExists($file->getFullPath(), 'New path is updated in memory, not written before write() is called');
|
||||
|
||||
$file->write();
|
||||
|
||||
// After write()
|
||||
clearstatcache();
|
||||
$this->assertFileNotExists($oldPath, 'Old path is removed after write()');
|
||||
$this->assertFileExists($file->getFullPath(), 'New path is created after write()');
|
||||
}
|
||||
|
||||
function testSetParentIDChangesFilesystemOnWrite() {
|
||||
$file = $this->objFromFixture('File', 'asdf');
|
||||
$subfolder = $this->objFromFixture('Folder', 'subfolder');
|
||||
$oldPath = $file->getFullPath();
|
||||
|
||||
// set ParentID
|
||||
$file->ParentID = $subfolder->ID;
|
||||
|
||||
// Before write()
|
||||
$this->assertFileExists($oldPath, 'Old path is still present');
|
||||
$this->assertFileNotExists($file->getFullPath(), 'New path is updated in memory, not written before write() is called');
|
||||
|
||||
$file->write();
|
||||
|
||||
// After write()
|
||||
clearstatcache();
|
||||
$this->assertFileNotExists($oldPath, 'Old path is removed after write()');
|
||||
$this->assertFileExists($file->getFullPath(), 'New path is created after write()');
|
||||
}
|
||||
|
||||
/**
|
||||
* @see http://open.silverstripe.org/ticket/5693
|
||||
*/
|
||||
function testSetNameWithInvalidExtensionDoesntChangeFilesystem() {
|
||||
$origExts = File::$allowed_extensions;
|
||||
File::$allowed_extensions = array('txt');
|
||||
|
||||
$file = $this->objFromFixture('File', 'asdf');
|
||||
$oldPath = $file->getFullPath();
|
||||
|
||||
$file->Name = 'renamed.php'; // evil extension
|
||||
try {
|
||||
$file->write();
|
||||
} catch(ValidationException $e) {
|
||||
File::$allowed_extensions = $origExts;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->fail('Expected ValidationException not raised');
|
||||
File::$allowed_extensions = $origExts;
|
||||
}
|
||||
|
||||
function testLinkAndRelativeLink() {
|
||||
$file = $this->objFromFixture('File', 'asdf');
|
||||
$this->assertEquals(ASSETS_DIR . '/asdfjkl.txt', $file->RelativeLink());
|
||||
$this->assertEquals(Director::baseURL() . ASSETS_DIR . '/asdfjkl.txt', $file->Link());
|
||||
$this->assertEquals(ASSETS_DIR . '/FileTest.txt', $file->RelativeLink());
|
||||
$this->assertEquals(Director::baseURL() . ASSETS_DIR . '/FileTest.txt', $file->Link());
|
||||
}
|
||||
|
||||
function testGetRelativePath() {
|
||||
$rootfile = $this->objFromFixture('File', 'asdf');
|
||||
$this->assertEquals('assets/FileTest.txt', $rootfile->getRelativePath(), 'File in assets/ folder');
|
||||
|
||||
$subfolderfile = $this->objFromFixture('File', 'subfolderfile');
|
||||
$this->assertEquals('assets/FileTest-subfolder/FileTestSubfolder.txt', $subfolderfile->getRelativePath(), 'File in subfolder within assets/ folder, with existing Filename');
|
||||
|
||||
$subfolderfilesetfromname = $this->objFromFixture('File', 'subfolderfile-setfromname');
|
||||
$this->assertEquals('assets/FileTest-subfolder/FileTestSubfolder2.txt', $subfolderfilesetfromname->getRelativePath(), 'File in subfolder within assets/ folder, with Filename generated through setName()');
|
||||
}
|
||||
|
||||
function testGetFullPath() {
|
||||
$rootfile = $this->objFromFixture('File', 'asdf');
|
||||
$this->assertEquals(ASSETS_PATH . '/FileTest.txt', $rootfile->getFullPath(), 'File in assets/ folder');
|
||||
}
|
||||
|
||||
function testNameAndTitleGeneration() {
|
||||
/* If objects are loaded into the system with just a Filename, then Name is generated but Title isn't */
|
||||
$file = $this->objFromFixture('File', 'asdf');
|
||||
$this->assertEquals('asdfjkl.txt', $file->Name);
|
||||
$this->assertEquals('FileTest.txt', $file->Name);
|
||||
$this->assertNull($file->Title);
|
||||
|
||||
/* However, if Name is set instead of Filename, then Title is set */
|
||||
$file = $this->objFromFixture('File', 'setfromname');
|
||||
$this->assertEquals(ASSETS_DIR . '/asdfjkl.png', $file->Filename);
|
||||
$this->assertEquals('asdfjkl', $file->Title);
|
||||
$this->assertEquals(ASSETS_DIR . '/FileTest.png', $file->Filename);
|
||||
$this->assertEquals('FileTest', $file->Title);
|
||||
}
|
||||
|
||||
function testChangingNameAndFilenameAndParentID() {
|
||||
$file = $this->objFromFixture('File', 'asdf');
|
||||
|
||||
/* If you alter the Name attribute of a file, then the filesystem is also affected */
|
||||
$file->Name = 'asdfjkl2.txt';
|
||||
clearstatcache();
|
||||
$this->assertFileNotExists(ASSETS_PATH . "/asdfjkl.txt");
|
||||
$this->assertFileExists(ASSETS_PATH . "/asdfjkl2.txt");
|
||||
/* The Filename field is also updated */
|
||||
$this->assertEquals(ASSETS_DIR . '/asdfjkl2.txt', $file->Filename);
|
||||
|
||||
/* However, if you alter the Filename attribute, the the filesystem isn't affected. Altering Filename directly isn't
|
||||
recommended */
|
||||
$file->Filename = ASSETS_DIR . '/asdfjkl3.txt';
|
||||
clearstatcache();
|
||||
$this->assertFileExists(ASSETS_PATH . "/asdfjkl2.txt");
|
||||
$this->assertFileNotExists(ASSETS_PATH . "/asdfjkl3.txt");
|
||||
|
||||
$file->Filename = ASSETS_DIR . '/asdfjkl2.txt';
|
||||
$file->write();
|
||||
|
||||
/* Instead, altering Name and ParentID is the recommended way of changing the name and location of a file */
|
||||
$file->ParentID = $this->idFromFixture('Folder', 'subfolder');
|
||||
clearstatcache();
|
||||
$this->assertFileExists(ASSETS_PATH . "/subfolder/asdfjkl2.txt");
|
||||
$this->assertFileNotExists(ASSETS_PATH . "/asdfjkl2.txt");
|
||||
$this->assertEquals(ASSETS_DIR . '/subfolder/asdfjkl2.txt', $file->Filename);
|
||||
$file->write();
|
||||
|
||||
}
|
||||
|
||||
function testSizeAndAbsoluteSizeParameters() {
|
||||
$file = $this->objFromFixture('File', 'asdf');
|
||||
|
||||
@ -67,10 +168,10 @@ class FileTest extends SapphireTest {
|
||||
function testFileType() {
|
||||
$file = $this->objFromFixture('File', 'gif');
|
||||
$this->assertEquals("GIF image - good for diagrams", $file->FileType);
|
||||
|
||||
|
||||
$file = $this->objFromFixture('File', 'pdf');
|
||||
$this->assertEquals("Adobe Acrobat PDF file", $file->FileType);
|
||||
|
||||
|
||||
/* Only a few file types are given special descriptions; the rest are unknown */
|
||||
$file = $this->objFromFixture('File', 'asdf');
|
||||
$this->assertEquals("unknown", $file->FileType);
|
||||
@ -94,6 +195,19 @@ class FileTest extends SapphireTest {
|
||||
$this->assertEquals("93132.3 GB", File::format_size(100000000000000));
|
||||
}
|
||||
|
||||
function testDeleteDatabaseOnly() {
|
||||
$file = $this->objFromFixture('File', 'asdf');
|
||||
$fileID = $file->ID;
|
||||
$filePath = $file->getFullPath();
|
||||
|
||||
$file->deleteDatabaseOnly();
|
||||
|
||||
DataObject::flush_and_destroy_cache();
|
||||
|
||||
$this->assertFileExists($filePath);
|
||||
$this->assertFalse(DataObject::get_by_id('File', $fileID));
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function setUp() {
|
||||
@ -102,17 +216,17 @@ class FileTest extends SapphireTest {
|
||||
if(!file_exists(ASSETS_PATH)) mkdir(ASSETS_PATH);
|
||||
|
||||
/* Create a test folders for each of the fixture references */
|
||||
$fileIDs = $this->allFixtureIDs('Folder');
|
||||
foreach($fileIDs as $fileID) {
|
||||
$file = DataObject::get_by_id('Folder', $fileID);
|
||||
if(!file_exists("../$file->Filename")) mkdir("../$file->Filename");
|
||||
$folderIDs = $this->allFixtureIDs('Folder');
|
||||
foreach($folderIDs as $folderID) {
|
||||
$folder = DataObject::get_by_id('Folder', $folderID);
|
||||
if(!file_exists(BASE_PATH."/$folder->Filename")) mkdir(BASE_PATH."/$folder->Filename");
|
||||
}
|
||||
|
||||
/* Create a test files for each of the fixture references */
|
||||
$fileIDs = $this->allFixtureIDs('File');
|
||||
foreach($fileIDs as $fileID) {
|
||||
$file = DataObject::get_by_id('File', $fileID);
|
||||
$fh = fopen("../$file->Filename", "w");
|
||||
$fh = fopen(BASE_PATH."/$file->Filename", "w");
|
||||
fwrite($fh, str_repeat('x',1000000));
|
||||
fclose($fh);
|
||||
}
|
||||
@ -123,14 +237,14 @@ class FileTest extends SapphireTest {
|
||||
$fileIDs = $this->allFixtureIDs('File');
|
||||
foreach($fileIDs as $fileID) {
|
||||
$file = DataObject::get_by_id('File', $fileID);
|
||||
if(file_exists("../$file->Filename")) unlink("../$file->Filename");
|
||||
if($file && file_exists(BASE_PATH."/$file->Filename")) unlink(BASE_PATH."/$file->Filename");
|
||||
}
|
||||
|
||||
/* Remove the test folders that we've crated */
|
||||
$fileIDs = $this->allFixtureIDs('Folder');
|
||||
foreach($fileIDs as $fileID) {
|
||||
$file = DataObject::get_by_id('Folder', $fileID);
|
||||
if(file_exists("../$file->Filename")) rmdir("../$file->Filename");
|
||||
$folderIDs = $this->allFixtureIDs('Folder');
|
||||
foreach($folderIDs as $folderID) {
|
||||
$folder = DataObject::get_by_id('Folder', $folderID);
|
||||
if($folder && file_exists(BASE_PATH."/$folder->Filename")) Filesystem::removeFolder(BASE_PATH."/$folder->Filename");
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
|
@ -1,14 +1,30 @@
|
||||
File:
|
||||
asdf:
|
||||
Filename: assets/asdfjkl.txt
|
||||
gif:
|
||||
Filename: assets/asdfjkl.gif
|
||||
pdf:
|
||||
Filename: assets/asdfjkl.pdf
|
||||
setfromname:
|
||||
Name: asdfjkl.png
|
||||
ParentID: 0
|
||||
|
||||
Folder:
|
||||
subfolder:
|
||||
Name: subfolder
|
||||
Name: FileTest-subfolder
|
||||
folder1:
|
||||
Name: FileTest-folder1
|
||||
folder2:
|
||||
Name: FileTest-folder2
|
||||
folder1-subfolder1:
|
||||
Name: FileTest-folder1-subfolder1
|
||||
ParentID: =>Folder.folder1
|
||||
File:
|
||||
asdf:
|
||||
Filename: assets/FileTest.txt
|
||||
gif:
|
||||
Filename: assets/FileTest.gif
|
||||
pdf:
|
||||
Filename: assets/FileTest.pdf
|
||||
setfromname:
|
||||
Name: FileTest.png
|
||||
ParentID: 0
|
||||
subfolderfile:
|
||||
Filename: assets/FileTest-subfolder/FileTestSubfolder.txt
|
||||
ParentID: =>Folder.subfolder
|
||||
subfolderfile-setfromname:
|
||||
Name: FileTestSubfolder2.txt
|
||||
ParentID: =>Folder.subfolder
|
||||
file1-folder1:
|
||||
Filename: assets/FileTest-folder1/File1.txt
|
||||
Name: File1.txt
|
||||
ParentID: =>Folder.folder1
|
255
tests/filesystem/FolderTest.php
Normal file
255
tests/filesystem/FolderTest.php
Normal file
@ -0,0 +1,255 @@
|
||||
<?php
|
||||
/**
|
||||
* @author Ingo Schommer (ingo at silverstripe dot com)
|
||||
* @todo There's currently no way to save outside of assets/ folder
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage tests
|
||||
*/
|
||||
class FolderTest extends SapphireTest {
|
||||
|
||||
static $fixture_file = 'sapphire/tests/filesystem/FileTest.yml';
|
||||
|
||||
function testCreateFromNameAndParentIDSetsFilename() {
|
||||
$folder1 = $this->objFromFixture('Folder', 'folder1');
|
||||
$newFolder = new Folder();
|
||||
$newFolder->Name = 'CreateFromNameAndParentID';
|
||||
$newFolder->ParentID = $folder1->ID;
|
||||
$newFolder->write();
|
||||
|
||||
$this->assertEquals($folder1->Filename . 'CreateFromNameAndParentID/', $newFolder->Filename);
|
||||
}
|
||||
|
||||
function testAllChildrenIncludesFolders() {
|
||||
$folder1 = $this->objFromFixture('Folder', 'folder1');
|
||||
$subfolder1 = $this->objFromFixture('Folder', 'folder1-subfolder1');
|
||||
$file1 = $this->objFromFixture('File', 'file1-folder1');
|
||||
|
||||
$children = $folder1->allChildren();
|
||||
$this->assertEquals(2, $children->Count());
|
||||
$this->assertContains($subfolder1->ID, $children->column('ID'));
|
||||
$this->assertContains($file1->ID, $children->column('ID'));
|
||||
}
|
||||
|
||||
function testFindOrMake() {
|
||||
$path = '/FolderTest/testFindOrMake/';
|
||||
$folder = Folder::findOrMake($path);
|
||||
$this->assertEquals(ASSETS_DIR . $path,$folder->getRelativePath(),
|
||||
'Nested path information is correctly saved to database (with trailing slash)'
|
||||
);
|
||||
|
||||
$this->assertTrue(file_exists(ASSETS_PATH . $path), 'File');
|
||||
$parentFolder = DataObject::get_one('Folder', '`Name` = \'FolderTest\'');
|
||||
$this->assertNotNull($parentFolder);
|
||||
$this->assertEquals($parentFolder->ID, $folder->ParentID);
|
||||
|
||||
$path = '/FolderTest/testFindOrMake'; // no trailing slash
|
||||
$folder = Folder::findOrMake($path);
|
||||
$this->assertEquals(ASSETS_DIR . $path . '/',$folder->getRelativePath(),
|
||||
'Path information is correctly saved to database (without trailing slash)'
|
||||
);
|
||||
|
||||
$path = '/assets/'; // relative to "assets/" folder, should produce "assets/assets/"
|
||||
$folder = Folder::findOrMake($path);
|
||||
$this->assertEquals(ASSETS_DIR . $path,$folder->getRelativePath(),
|
||||
'A folder named "assets/" within "assets/" is allowed'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see FileTest->testSetNameChangesFilesystemOnWrite()
|
||||
*/
|
||||
function testSetNameChangesFilesystemOnWrite() {
|
||||
$folder1 = $this->objFromFixture('Folder', 'folder1');
|
||||
$subfolder1 = $this->objFromFixture('Folder', 'folder1-subfolder1');
|
||||
$file1 = $this->objFromFixture('File', 'file1-folder1');
|
||||
$oldPathFolder1 = $folder1->getFullPath();
|
||||
$oldPathSubfolder1 = $subfolder1->getFullPath();
|
||||
$oldPathFile1 = $file1->getFullPath();
|
||||
|
||||
// Before write()
|
||||
$folder1->Name = 'FileTest-folder1-renamed';
|
||||
$this->assertFileExists($oldPathFolder1, 'Old path is still present');
|
||||
$this->assertFileNotExists($folder1->getFullPath(), 'New path is updated in memory, not written before write() is called');
|
||||
$this->assertFileExists($oldPathFile1, 'Old file is still present');
|
||||
// TODO setters currently can't update in-memory
|
||||
// $this->assertFileNotExists($file1->getFullPath(), 'New path on contained files is updated in memory, not written before write() is called');
|
||||
// $this->assertFileNotExists($subfolder1->getFullPath(), 'New path on subfolders is updated in memory, not written before write() is called');
|
||||
|
||||
$folder1->write();
|
||||
|
||||
// After write()
|
||||
|
||||
// Reload state
|
||||
clearstatcache();
|
||||
DataObject::flush_and_destroy_cache();
|
||||
$folder1 = DataObject::get_by_id('Folder', $folder1->ID);
|
||||
$file1 = DataObject::get_by_id('File', $file1->ID);
|
||||
$subfolder1 = DataObject::get_by_id('Folder', $subfolder1->ID);
|
||||
|
||||
$this->assertFileNotExists($oldPathFolder1, 'Old path is removed after write()');
|
||||
$this->assertFileExists($folder1->getFullPath(), 'New path is created after write()');
|
||||
$this->assertFileNotExists($oldPathFile1, 'Old file is removed after write()');
|
||||
$this->assertFileExists($file1->getFullPath(), 'New file path is created after write()');
|
||||
$this->assertFileNotExists($oldPathSubfolder1, 'Subfolder is removed after write()');
|
||||
$this->assertFileExists($subfolder1->getFullPath(), 'New subfolder path is created after write()');
|
||||
|
||||
// Clean up after ourselves - tearDown() doesn't like renamed fixtures
|
||||
$folder1->delete(); // implicitly deletes subfolder as well
|
||||
}
|
||||
|
||||
/**
|
||||
* @see FileTest->testSetParentIDChangesFilesystemOnWrite()
|
||||
*/
|
||||
function testSetParentIDChangesFilesystemOnWrite() {
|
||||
$folder1 = $this->objFromFixture('Folder', 'folder1');
|
||||
$folder2 = $this->objFromFixture('Folder', 'folder2');
|
||||
$oldPathFolder1 = $folder1->getFullPath();
|
||||
|
||||
// set ParentID
|
||||
$folder1->ParentID = $folder2->ID;
|
||||
|
||||
// Before write()
|
||||
$this->assertFileExists($oldPathFolder1, 'Old path is still present');
|
||||
$this->assertFileNotExists($folder1->getFullPath(), 'New path is updated in memory, not written before write() is called');
|
||||
|
||||
$folder1->write();
|
||||
|
||||
// After write()
|
||||
clearstatcache();
|
||||
$this->assertFileNotExists($oldPathFolder1, 'Old path is removed after write()');
|
||||
$this->assertFileExists($folder1->getFullPath(), 'New path is created after write()');
|
||||
}
|
||||
|
||||
/**
|
||||
* @see FileTest->testLinkAndRelativeLink()
|
||||
*/
|
||||
function testLinkAndRelativeLink() {
|
||||
$folder = $this->objFromFixture('Folder', 'folder1');
|
||||
$this->assertEquals(ASSETS_DIR . '/FileTest-folder1/', $folder->RelativeLink());
|
||||
$this->assertEquals(Director::baseURL() . ASSETS_DIR . '/FileTest-folder1/', $folder->Link());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see FileTest->testGetRelativePath()
|
||||
*/
|
||||
function testGetRelativePath() {
|
||||
$rootfolder = $this->objFromFixture('Folder', 'folder1');
|
||||
$this->assertEquals('assets/FileTest-folder1/', $rootfolder->getRelativePath(), 'Folder in assets/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @see FileTest->testGetFullPath()
|
||||
*/
|
||||
function testGetFullPath() {
|
||||
$rootfolder = $this->objFromFixture('Folder', 'folder1');
|
||||
$this->assertEquals(ASSETS_PATH . '/FileTest-folder1/', $rootfolder->getFullPath(), 'File in assets/ folder');
|
||||
}
|
||||
|
||||
function testDeleteAlsoRemovesFilesystem() {
|
||||
$path = '/FolderTest/DeleteAlsoRemovesFilesystemAndChildren';
|
||||
$folder = Folder::findOrMake($path);
|
||||
$this->assertFileExists(ASSETS_PATH . $path);
|
||||
|
||||
$folder->delete();
|
||||
|
||||
$this->assertFileNotExists(ASSETS_PATH . $path);
|
||||
}
|
||||
|
||||
function testDeleteAlsoRemovesSubfoldersInDatabaseAndFilesystem() {
|
||||
$path = '/FolderTest/DeleteAlsoRemovesSubfoldersInDatabaseAndFilesystem';
|
||||
$subfolderPath = $path . '/subfolder';
|
||||
$folder = Folder::findOrMake($path);
|
||||
$subfolder = Folder::findOrMake($subfolderPath);
|
||||
$subfolderID = $subfolder->ID;
|
||||
|
||||
$folder->delete();
|
||||
|
||||
$this->assertFileNotExists(ASSETS_PATH . $path);
|
||||
$this->assertFileNotExists(ASSETS_PATH . $subfolderPath, 'Subfolder removed from filesystem');
|
||||
$this->assertFalse(DataObject::get_by_id('Folder', $subfolderID), 'Subfolder removed from database');
|
||||
}
|
||||
|
||||
function testDeleteAlsoRemovesContainedFilesInDatabaseAndFilesystem() {
|
||||
$path = '/FolderTest/DeleteAlsoRemovesContainedFilesInDatabaseAndFilesystem';
|
||||
$folder = Folder::findOrMake($path);
|
||||
|
||||
$file = $this->objFromFixture('File', 'gif');
|
||||
$file->ParentID = $folder->ID;
|
||||
$file->write();
|
||||
$fileID = $file->ID;
|
||||
$fileAbsPath = $file->getFullPath();
|
||||
$this->assertFileExists($fileAbsPath);
|
||||
|
||||
$folder->delete();
|
||||
|
||||
$this->assertFileNotExists($fileAbsPath, 'Contained files removed from filesystem');
|
||||
$this->assertFalse(DataObject::get_by_id('File', $fileID), 'Contained files removed from database');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @see FileTest->testDeleteDatabaseOnly()
|
||||
*/
|
||||
function testDeleteDatabaseOnly() {
|
||||
$subfolder = $this->objFromFixture('Folder', 'subfolder');
|
||||
$subfolderID = $subfolder->ID;
|
||||
$subfolderFile = $this->objFromFixture('File', 'subfolderfile');
|
||||
$subfolderFileID = $subfolderFile->ID;
|
||||
|
||||
$subfolder->deleteDatabaseOnly();
|
||||
|
||||
DataObject::flush_and_destroy_cache();
|
||||
|
||||
$this->assertFileExists($subfolder->getFullPath());
|
||||
$this->assertFalse(DataObject::get_by_id('Folder', $subfolderID));
|
||||
|
||||
$this->assertFileExists($subfolderFile->getFullPath());
|
||||
$this->assertFalse(DataObject::get_by_id('File', $subfolderFileID));
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
if(!file_exists(ASSETS_PATH)) mkdir(ASSETS_PATH);
|
||||
|
||||
// Create a test folders for each of the fixture references
|
||||
$folderIDs = $this->allFixtureIDs('Folder');
|
||||
foreach($folderIDs as $folderID) {
|
||||
$folder = DataObject::get_by_id('Folder', $folderID);
|
||||
if(!file_exists(BASE_PATH."/$folder->Filename")) mkdir(BASE_PATH."/$folder->Filename");
|
||||
}
|
||||
|
||||
// Create a test files for each of the fixture references
|
||||
$fileIDs = $this->allFixtureIDs('File');
|
||||
foreach($fileIDs as $fileID) {
|
||||
$file = DataObject::get_by_id('File', $fileID);
|
||||
$fh = fopen(BASE_PATH."/$file->Filename", "w");
|
||||
fwrite($fh, str_repeat('x',1000000));
|
||||
fclose($fh);
|
||||
}
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
$testPath = ASSETS_PATH . '/FolderTest';
|
||||
if(file_exists($testPath)) Filesystem::removeFolder($testPath);
|
||||
|
||||
/* Remove the test files that we've created */
|
||||
$fileIDs = $this->allFixtureIDs('File');
|
||||
foreach($fileIDs as $fileID) {
|
||||
$file = DataObject::get_by_id('File', $fileID);
|
||||
if($file && file_exists(BASE_PATH."/$file->Filename")) unlink(BASE_PATH."/$file->Filename");
|
||||
}
|
||||
|
||||
// Remove the test folders that we've crated
|
||||
$folderIDs = $this->allFixtureIDs('Folder');
|
||||
foreach($folderIDs as $folderID) {
|
||||
$folder = DataObject::get_by_id('Folder', $folderID);
|
||||
// Might have been removed during test
|
||||
if($folder && file_exists(BASE_PATH."/$folder->Filename")) Filesystem::removeFolder(BASE_PATH."/$folder->Filename");
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user