API Refactor of File / Folder to use DBFile

API Remove filesystem sync
API to handle file manipulations
This commit is contained in:
Damian Mooyman 2015-09-15 14:52:02 +12:00
parent 88fe25a5d1
commit be239896d3
71 changed files with 5540 additions and 5037 deletions

View File

@ -31,8 +31,11 @@ Object::useCustomClass('Datetime', 'SS_Datetime', true);
*/
define('MCE_ROOT', FRAMEWORK_DIR . '/thirdparty/tinymce/');
ShortcodeParser::get('default')->register('file_link', array('File', 'link_shortcode_handler'));
ShortcodeParser::get('default')->register('embed', array('Oembed', 'handle_shortcode'));
ShortcodeParser::get('default')
->register('file_link', array('File', 'handle_shortcode'))
->register('embed', array('Oembed', 'handle_shortcode'));
// @todo
// ->register('dbfile_link', array('DBFile', 'handle_shortcode'))
// Zend_Cache temp directory setting
$_ENV['TMPDIR'] = TEMP_FOLDER; // for *nix

View File

@ -23,3 +23,5 @@ Injector:
AssetNameGenerator:
class: SilverStripe\Filesystem\Storage\DefaultAssetNameGenerator
type: prototype
# Image mechanism
Image_Backend: GDBackend

View File

@ -161,12 +161,15 @@ class SS_Backtrace {
/**
* Render a backtrace array into an appropriate plain-text or HTML string.
*
* @param string $bt The trace array, as returned by debug_backtrace() or Exception::getTrace()
* @param array $bt The trace array, as returned by debug_backtrace() or Exception::getTrace()
* @param boolean $plainText Set to false for HTML output, or true for plain-text output
* @param array List of functions that should be ignored. If not set, a default is provided
* @return string The rendered backtrace
*/
public static function get_rendered_backtrace($bt, $plainText = false, $ignoredFunctions = null) {
if(empty($bt)) {
return '';
}
$bt = self::filter_backtrace($bt, $ignoredFunctions);
$result = ($plainText) ? '' : '<ul>';
foreach($bt as $item) {

View File

@ -70,8 +70,10 @@ class FixtureBlueprint {
public function createObject($identifier, $data = null, $fixtures = null) {
// We have to disable validation while we import the fixtures, as the order in
// which they are imported doesnt guarantee valid relations until after the import is complete.
$validationenabled = Config::inst()->get('DataObject', 'validation_enabled');
// Also disable filesystem manipulations
Config::nest();
Config::inst()->update('DataObject', 'validation_enabled', false);
Config::inst()->update('File', 'update_filesystem', false);
$this->invokeCallbacks('beforeCreate', array($identifier, &$data, &$fixtures));
@ -193,12 +195,11 @@ class FixtureBlueprint {
$this->overrideField($obj, 'LastEdited', $data['LastEdited'], $fixtures);
}
} catch(Exception $e) {
Config::inst()->update('DataObject', 'validation_enabled', $validationenabled);
Config::unnest();
throw $e;
}
Config::inst()->update('DataObject', 'validation_enabled', $validationenabled);
Config::unnest();
$this->invokeCallbacks('afterCreate', array($obj, $identifier, &$data, &$fixtures));
return $obj;

View File

@ -1,14 +1,24 @@
# UploadField
## Introduction
The UploadField will let you upload one or multiple files of all types, including images. But that's not all it does - it will also link the uploaded file(s) to an existing relation and let you edit the linked files as well. That makes it flexible enough to sometimes even replace the GridField, like for instance in creating and managing a simple gallery.
The UploadField will let you upload one or multiple files of all types, including images.
But that's not all it does - it will also link the uploaded file(s) to an existing relation
and let you edit the linked files as well. That makes it flexible enough to sometimes even
replace the GridField, like for instance in creating and managing a simple gallery.
## Usage
The field can be used in three ways: To upload a single file into a `has_one` relationship,or allow multiple files into a `has_many` or `many_many` relationship, or to act as a stand
The field can be used in three ways: To upload a single file into a `has_one` relationship,
or allow multiple files into a `has_many` or `many_many` relationship, or to act as a stand
alone uploader into a folder with no underlying relation.
## Validation
Although images are uploaded and stored on the filesystem immediately after selection,the value (or values) of this field will not be written to any related record until the record is saved and successfully validated. However, any invalid records will still persist across form submissions until explicitly removed or replaced by the user.
Although images are uploaded and stored on the filesystem immediately after selection,
the value (or values) of this field will not be written to any related record until the
record is saved and successfully validated. However, any invalid records will still
persist across form submissions until explicitly removed or replaced by the user.
Care should be taken as invalid files may remain within the filesystem until explicitly removed.
@ -32,19 +42,27 @@ The following example adds an UploadField to a page for single fileupload, based
$uploadField = new UploadField(
$name = 'SingleImage',
$title = 'Upload a single image'
)
)
);
// Restrict validator to include only supported image formats
$uploadField->setAllowedFileCategories('image/supported');
return $fields;
}
}
```
The UploadField will auto-detect the relation based on it's `name` property, and save it into the GalleyPages' `SingleImageID` field. Setting the `setAllowedMaxFileNumber` to 1 will make sure that only one image can ever be uploaded and linked to the relation.
The UploadField will auto-detect the relation based on its `name` property, and save
it into the GalleyPages' `SingleImageID` field. Setting the `setAllowedMaxFileNumber`
to 1 will make sure that only one image can ever be uploaded and linked to the relation.
### Multiple fileupload
Enable multiple fileuploads by using a many_many (or has_many) relation. Again, the `UploadField` will detect the relation based on its $name property value:
```php
Enable multiple fileuploads by using a many_many (or has_many) relation. Again,
the `UploadField` will detect the relation based on its $name property value:
:::php
class GalleryPage extends Page {
private static $many_many = array(
@ -60,52 +78,62 @@ Enable multiple fileuploads by using a many_many (or has_many) relation. Again,
$uploadField = new UploadField(
$name = 'GalleryImages',
$title = 'Upload one or more images (max 10 in total)'
)
)
);
$uploadField->setAllowedFileCategories('image/supported');
$uploadField->setAllowedMaxFileNumber(10);
return $fields;
}
}
class GalleryPage_Controller extends Page_Controller {
}
```
```php
:::php
class GalleryImageExtension extends DataExtension {
private static $belongs_many_many = array('Galleries' => 'GalleryPage);
private static $belongs_many_many = array(
'Galleries' => 'GalleryPage
);
}
```
```yml
Image:
extensions:
- GalleryImageExtension
```
:::yaml
Image:
extensions:
- GalleryImageExtension
<div class="notice" markdown='1'>
In order to link both ends of the relationship together it's usually advisable to extend Image with the necessary $has_one, $belongs_to, $has_many or $belongs_many_many. In particular, a DataObject with $has_many Images will not work without this specified explicitly.
In order to link both ends of the relationship together it's usually advisable to extend
File with the necessary $has_one, $belongs_to, $has_many or $belongs_many_many.
In particular, a DataObject with $has_many File will not work without this specified explicitly.
</div>
## Configuration
### Overview
The field can either be configured on an instance level with the various getProperty and setProperty functions, or globally by overriding the YAML defaults.
UploadField can either be configured on an instance level with the various getProperty
and setProperty functions, or globally by overriding the YAML defaults.
See the [Configuration Reference](uploadfield#configuration-reference) section for possible values.
Example: mysite/_config/uploadfield.yml
```yml
:::yaml
after: framework#uploadfield
---
UploadField:
defaultConfig:
canUpload: false
```
### Set a custom folder
This example will save all uploads in the `/assets/customfolder/` folder. If the folder doesn't exist, it will be created.
```php
This example will save all uploads in the `customfolder` in the configured assets store root (normally under 'assets')
If the folder doesn't exist, it will be created.
:::php
$fields->addFieldToTab(
'Root.Upload',
$uploadField = new UploadField(
@ -113,96 +141,145 @@ This example will save all uploads in the `/assets/customfolder/` folder. If the
$title = 'Please upload one or more images' )
);
$uploadField->setFolderName('customfolder');
```
### Limit the allowed filetypes
`AllowedExtensions` defaults to the `File.allowed_extensions` configuration setting, but can be overwritten for each UploadField:
```php
### Limit the allowed filetypes
`AllowedExtensions` defaults to the `File.allowed_extensions` configuration setting,
but can be overwritten for each UploadField:
:::php
$uploadField->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif'));
```
Entire groups of file extensions can be specified in order to quickly limit types to known file categories.
This can be done by using file category names, which are defined via the `File.app_categories` config. This
list could be extended with any custom categories.
```php
$uploadField->setAllowedFileCategories('image', 'doc');
```
This will limit files to the following extensions: bmp gif jpg jpeg pcx tif png alpha als cel icon ico ps doc docx txt rtf xls xlsx pages ppt pptx pps csv html htm xhtml xml pdf.
The built in categories are:
`AllowedExtensions` can also be set globally via the [YAML configuration](/developer_guides/configuration/configuration/#configuration-yaml-syntax-and-rules), for example you may add the following into your mysite/_config/config.yml:
| File category | Example extensions |
|-----------------|--------------------|
| archive | zip, gz, rar |
| audio | mp3, wav, ogg |
| document | doc, txt, pdf |
| flash | fla, swf |
| image | jpg, tiff, ps |
| image/supported | jpg, gif, png |
| video | mkv, avi, mp4 |
```yaml
Note that although all image types are included in the 'image' category, only images that are in the
'images/supported' list are compatible with the SilverStripe image manipulations API. Other types
can be uploaded, but cannot be resized.
:::php
$uploadField->setAllowedFileCategories('image/supported');
This will limit files to the the compatible image formats: jpg, jpeg, gif, and png.
`AllowedExtensions` can also be set globally via the
[YAML configuration](/developer_guides/configuration/configuration/#configuration-yaml-syntax-and-rules),
for example you may add the following into your mysite/_config/config.yml:
:::yaml
File:
allowed_extensions:
- 7zip
- xzip
```
### Limit the maximum file size
`AllowedMaxFileSize` is by default set to the lower value of the 2 php.ini configurations: `upload_max_filesize` and `post_max_size`. The value is set as bytes.
NOTE: this only sets the configuration for your UploadField, this does NOT change your server upload settings, so if your server is set to only allow 1 MB and you set the UploadField to 2 MB, uploads will not work.
`AllowedMaxFileSize` is by default set to the lower value of the 2 php.ini configurations:
`upload_max_filesize` and `post_max_size`. The value is set as bytes.
```php
NOTE: this only sets the configuration for your UploadField, this does NOT change your
server upload settings, so if your server is set to only allow 1 MB and you set the
UploadField to 2 MB, uploads will not work.
:::php
$sizeMB = 2; // 2 MB
$size = $sizeMB * 1024 * 1024; // 2 MB in bytes
$this->getValidator()->setAllowedMaxFileSize($size);
```
You can also specify a default global maximum file size setting in your config for different file types. This is overridden when specifying the max allowed file size on the UploadField instance.
```yaml
You can also specify a default global maximum file size setting in your config for different file types.
This is overridden when specifying the max allowed file size on the UploadField instance.
:::yaml
Upload_Validator:
default_max_file_size:
'[image]': '1m'
'[doc]': '5m'
'[document]': '5m'
'jpeg': 2000
```
### Preview dimensions
Set the dimensions of the image preview. By default the max width is set to 80 and the max height is set to 60.
```php
:::php
$uploadField->setPreviewMaxWidth(100);
$uploadField->setPreviewMaxHeight(100);
```
### Disable attachment of existing files
This can force the user to upload a new file, rather than link to the already existing file library
```php
:::php
$uploadField->setCanAttachExisting(false);
```
### Disable uploading of new files
Alternatively, you can force the user to only specify already existing files in the file library
```php
:::php
$uploadField->setCanUpload(false);
```
### Automatic or manual upload
By default, the UploadField will try to automatically upload all selected files. Setting the `autoUpload` property to false, will present you with a list of selected files that you can then upload manually one by one:
```php
By default, the UploadField will try to automatically upload all selected files. Setting the `autoUpload`
property to false, will present you with a list of selected files that you can then upload manually one by one:
:::php
$uploadField->setAutoUpload(false);
```
### Change Detection
The CMS interface will automatically notify the form containing
an UploadField instance of changes, such as a new upload,
or the removal of an existing upload (through a `dirty` event).
The UI can then choose an appropriate response (e.g. highlighting the "save" button). If the UploadField doesn't save into a relation, there's technically no saveable change (the upload has already happened), which is why this feature can be disabled on demand.
The UI can then choose an appropriate response (e.g. highlighting the "save" button).
If the UploadField doesn't save into a relation, there's technically no saveable change
(the upload has already happened), which is why this feature can be disabled on demand.
```php
:::php
$uploadField->setConfig('changeDetection', false);
```
### Build a simple gallery
A gallery most times needs more then simple images. You might want to add a description, or maybe some settings to define a transition effect for each slide.
A gallery most times needs more then simple images. You might want to add a description, or
maybe some settings to define a transition effect for each slide.
First create a [DataExtension](/developer_guides/extending/extensions) like this:
```php
:::php
class GalleryImage extends DataExtension {
private static $db = array(
@ -213,142 +290,152 @@ First create a [DataExtension](/developer_guides/extending/extensions) like this
'GalleryPage' => 'GalleryPage'
);
}
```
Now register the DataExtension for the Image class in your mysite/_config/config.yml:
```yml
Image:
extensions:
- GalleryImage
```
:::yaml
Image:
extensions:
- GalleryImage
<div class="notice" markdown='1'>
Note: Although you can subclass the Image class instead of using a DataExtension, this is not advisable. For instance: when using a subclass, the 'From files' button will only return files that were uploaded for that subclass, it won't recognize any other images!
Note: Although you can subclass the Image class instead of using a DataExtension, this is not advisable.
For instance: when using a subclass, the 'From files' button will only return files that were uploaded
for that subclass, it won't recognize any other images!
</div>
### Edit uploaded images
By default the UploadField will let you edit the following fields: *Title, Filename, Owner and Folder*. The fileEditFields` configuration setting allows you you alter these settings. One way to go about this is create a `getCustomFields` function in your GalleryImage object like this:
```php
By default the UploadField will let you edit the following fields: *Title, Filename, Owner and Folder*.
The fileEditFields` configuration setting allows you you alter these settings. One way to go about this
is create a `getCustomFields` function in your GalleryImage object like this:
:::php
class GalleryImage extends DataExtension {
...
function getCustomFields() {
public function getCustomFields() {
$fields = new FieldList();
$fields->push(new TextField('Title', 'Title'));
$fields->push(new TextareaField('Description', 'Description'));
return $fields;
}
}
```
Then, in your GalleryPage, tell the UploadField to use this function:
```php
$uploadField->setFileEditFields('getCustomFields');
```
In a similar fashion you can use 'setFileEditActions' to set the actions for the editform, or 'fileEditValidator' to determine the validator (e.g. RequiredFields).
:::php
$uploadField->setFileEditFields('getCustomFields');
In a similar fashion you can use 'setFileEditActions' to set the actions for the editform, or
'fileEditValidator' to determine the validator (e.g. RequiredFields).
### Configuration Reference
- `setAllowedMaxFileNumber`: (int) php validation of allowedMaxFileNumber only works when a db relation is available, set to null to allow unlimited if record has a has_one and allowedMaxFileNumber is null, it will be set to 1.
- `setAllowedFileExtensions`: (array) List of file extensions allowed.
- `setAllowedFileCategories`: (array|string) List of types of files allowed. May be any of 'image', 'audio', 'mov', 'zip', 'flash', or 'doc'.
- `setAutoUpload`: (boolean) Should the field automatically trigger an upload once a file is selected?
- `setCanAttachExisting`: (boolean|string) Can the user attach existing files from the library. String values are interpreted as permission codes.
- `setCanPreviewFolder`: (boolean|string) Can the user preview the folder files will be saved into? String values are interpreted as permission codes.
- `setCanUpload`: (boolean|string) Can the user upload new files, or just select from existing files. String values are interpreted as permission codes.
- `setDownloadTemplateName`: (string) javascript template used to display already uploaded files, see javascript/UploadField_downloadtemplate.js.
- `setFileEditFields`: (FieldList|string) FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm (Example: 'getCMSFields').
- `setFileEditActions`: (FieldList|string) FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm (Example: 'getCMSActions').
- `setFileEditValidator`: (string) Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm (Example: 'getCMSValidator').
- `setOverwriteWarning`: (boolean) Show a warning when overwriting a file.
- `setPreviewMaxWidth`: (int).
- `setPreviewMaxHeight`: (int).
- `setTemplateFileButtons`: (string) Template name to use for the file buttons.
- `setTemplateFileEdit`: (string) Template name to use for the file edit form.
- `setUploadTemplateName`: (string) javascript template used to display uploading files, see javascript/UploadField_uploadtemplate.js.
- `setCanPreviewFolder`: (boolean|string) Is the upload folder visible to uploading users? String values are interpreted as permission codes.
* `setAllowedMaxFileNumber`: (int) php validation of allowedMaxFileNumber only works when a db
relation is available, set to null to allow unlimited if record has a has_one and
allowedMaxFileNumber is null, it will be set to 1.
* `setAllowedFileExtensions`: (array) List of file extensions allowed.
* `setAllowedFileCategories`: (array|string) List of types of files allowed. May be any number of
categories as defined in `File.app_categories` config.
* `setAutoUpload`: (boolean) Should the field automatically trigger an upload once a file is selected?
* `setCanAttachExisting`: (boolean|string) Can the user attach existing files from the library. String
values are interpreted as permission codes.
* `setCanPreviewFolder`: (boolean|string) Can the user preview the folder files will be saved into?
String values are interpreted as permission codes.
* `setCanUpload`: (boolean|string) Can the user upload new files, or just select from existing files.
String values are interpreted as permission codes.
* `setDownloadTemplateName`: (string) javascript template used to display already uploaded files, see
javascript/UploadField_downloadtemplate.js.
* `setFileEditFields`: (FieldList|string) FieldList $fields or string $name (of a method on File to
provide a fields) for the EditForm (Example: 'getCMSFields').
* `setFileEditActions`: (FieldList|string) FieldList $actions or string $name (of a method on File to
provide a actions) for the EditForm (Example: 'getCMSActions').
* `setFileEditValidator`: (string) Validator (eg RequiredFields) or string $name (of a method on File
to provide a Validator) for the EditForm (Example: 'getCMSValidator').
* `setOverwriteWarning`: (boolean) Show a warning when overwriting a file.
* `setPreviewMaxWidth`: (int).
* `setPreviewMaxHeight`: (int).
* `setTemplateFileButtons`: (string) Template name to use for the file buttons.
* `setTemplateFileEdit`: (string) Template name to use for the file edit form.
* `setUploadTemplateName`: (string) javascript template used to display uploading files, see
javascript/UploadField_uploadtemplate.js.
* `setCanPreviewFolder`: (boolean|string) Is the upload folder visible to uploading users? String values
are interpreted as permission codes.
Certain default values for the above can be configured using the YAML config system.
```yaml
:::yaml
UploadField:
defaultConfig:
autoUpload: true
allowedMaxFileNumber:
canUpload: true
canAttachExisting: 'CMS_ACCESS_AssetAdmin'
canPreviewFolder: true
previewMaxWidth: 80
previewMaxHeight: 60
uploadTemplateName: 'ss-uploadfield-uploadtemplate'
downloadTemplateName: 'ss-uploadfield-downloadtemplate'
overwriteWarning: true # Warning before overwriting existing file (only relevant when Upload: replaceFile is true)
```
autoUpload: true
allowedMaxFileNumber:
canUpload: true
canAttachExisting: 'CMS_ACCESS_AssetAdmin'
canPreviewFolder: true
previewMaxWidth: 80
previewMaxHeight: 60
uploadTemplateName: 'ss-uploadfield-uploadtemplate'
downloadTemplateName: 'ss-uploadfield-downloadtemplate'
overwriteWarning: true # Warning before overwriting existing file (only relevant when Upload: replaceFile is true)
The above settings can also be set on a per-instance basis by using `setConfig` with the appropriate key.
The `Upload_Validator` class has configuration options for setting the `default_max_file_size`.
```yaml
:::yaml
Upload_Validator:
default_max_file_size:
'[image]': '1m'
'[doc]': '5m'
'jpeg': 2000
```
You can specify the file extension or the app category (as specified in the `File` class) in square brackets. It supports setting the file size in bytes or using the syntax supported by `File::ini2bytes()`.
You can also configure the underlying `[api:Upload]` class, by using the YAML config system.
```yaml
:::yaml
Upload:
# Globally disables automatic renaming of files and displays a warning before overwriting an existing file
replaceFile: true
uploads_folder: 'Uploads'
```
## Using the UploadField in a frontend form
The UploadField can be used in a frontend form, given that sufficient attention is given to the permissions granted to non-authorised users.
By default Image::canDelete and Image::canEdit do not require admin privileges, so make sure you override the methods in your Image extension class.
The UploadField can be used in a frontend form, given that sufficient attention is given to the permissions
granted to non-authorised users.
For instance, to generate an upload form suitable for saving images into a user-defined gallery the below code could be used:
Make sure that, for any dataobjects being exposed to the frontend, appropriate `canEdit`, `canDelete` and `canCreate`
are set appropriately, either via extensions on that dataobject or directly in subclasses.
For instance, to generate an upload form suitable for saving images into a user-defined gallery the below
code could be used:
*In GalleryPage.php:*
```php
<?php
class GalleryPage extends Page {}
class GalleryPage_Controller extends Page_Controller {
:::php
class GalleryPage extends Page {}
class GalleryPage_Controller extends Page_Controller {
private static $allowed_actions = array('Form');
public function Form() {
$fields = new FieldList(
new TextField('Title', 'Title', null, 255),
$field = new UploadField('Images', 'Upload Images')
);
$field->setAllowedFileCategories('image/supported'); // Allow images only
$field->setCanAttachExisting(false); // Block access to SilverStripe assets library
$field->setCanPreviewFolder(false); // Don't show target filesystem folder on upload field
$field->relationAutoSetting = false; // Prevents the form thinking the GalleryPage is the underlying object
@ -363,45 +450,43 @@ class GalleryPage_Controller extends Page_Controller {
return $this;
}
}
```
*Gallery.php:*
```php
<?php
:::php
class Gallery extends DataObject {
private static $db = array(
'Title' => 'Varchar(255)'
);
private static $many_many = array(
'Images' => 'Image'
);
}
```
*ImageExtension.php:*
```php
<?php
class ImageExtension extends DataExtension {
:::php
class ImageExtension extends DataExtension {
private static $belongs_many_many = array(
'Gallery' => 'Gallery'
);
function canEdit($member) {
public function canEdit($member) {
// WARNING! This affects permissions on ALL images. Setting this incorrectly can restrict
// access to authorised users or unintentionally give access to unauthorised users if set incorrectly.
return Permission::check('CMS_ACCESS_AssetAdmin');
}
}
```
*mysite/_config/config.yml*
```yml
Image:
extensions:
- ImageExtension
```
:::yaml
Image:
extensions:
- ImageExtension

View File

@ -168,8 +168,9 @@ extension. The `CMS` provides a `updateCMSFields` Extension Hook to tie into.
);
public function updateCMSFields(FieldList $fields) {
$fields->push(new TextField('Position'));
$fields->push(new UploadField('Image', 'Profile Image'));
$fields->push(new TextField('Position'));
$fields->push($upload = new UploadField('Image', 'Profile Image'));
$upload->setAllowedFileCategories('image/supported');
}
}

View File

@ -122,16 +122,15 @@ Other than files stored exclusively via DBFile, files can also exist as subclass
Each record has the following database fields:
| Field name | Description |
| ---------- | ----------- |
| `ClassName` | The class name of the file (e.g. File, Image or Folder). |
| `Name` | The 'basename' of the file, or the folder name. For example 'my-image.jpg', or 'images' for a folder. |
| `Title` | The optional, human-readable title of the file for display only (doesn't apply to folders). |
| `Filename` | The path to the file/folder, relative to the webroot. For example 'assets/images/my-image.jpg', or 'assets/images/' for a folder. |
| `Content` | Typically unused, but handy for a textual representation of files. For example for fulltext indexing of PDF documents. |
| `ShowInSearch` | Whether the file should be shown in search results, defaults to '1'. See ["Tutorial 4 - Site Search"](/tutorials/site_search) for enabling search. |
| `ParentID` | The ID of the parent Folder that this File/Folder is in. A ParentID of '0' indicates that the File/Folder is in the 'assets' directory. |
| `OwnerID` | The ID of the Member that 'owns' the File/Folder (not related to filesystem permissions). |
| Field name | Description |
| ---------- | ----------- |
| `ClassName` | The class name of the file (e.g. File, Image or Folder). |
| `Name` | The 'basename' of the file, or the folder name. For example 'my-image.jpg', or 'images' for a folder. |
| `Title` | The optional, human-readable title of the file for display only (doesn't apply to folders). |
| `File` | The `[api:DBFile]` field (see above) which stores the underlying asset content. |
| `ShowInSearch` | Whether the file should be shown in search results, defaults to '1'. See ["Tutorial 4 - Site Search"](/tutorials/site_search) for enabling search. |
| `ParentID` | The ID of the parent Folder that this File/Folder is in. A ParentID of '0' indicates that this is a top level record. |
| `OwnerID` | The ID of the Member that 'owns' the File/Folder (not related to filesystem permissions). |
## Management through the "Files" section of the CMS

View File

@ -2,7 +2,9 @@ summary: Learn how to crop and resize images in templates and PHP code
# Image
Represents an image object through the `[api:Image]` class, inheriting all base functionality from the `[api:File]` class with extra functionality including resizing.
Image files can be stored either through the `[api:Image]` dataobject, or though `[api:DBFile]` fields.
In either case, the same image resizing and manipulation functionality is available though the common
`[api:ImageManipulation]` trait.
## Usage
@ -38,6 +40,9 @@ Here are some examples, assuming the `$Image` object has dimensions of 200x100px
$Image.ScaleMaxHeight(150) // Returns a 200x100px image (like ScaleHeight but prevents up-sampling)
$Image.Fit(300,300) // Returns an image that fits within a 300x300px boundary, resulting in a 300x150px image (up-sampled)
$Image.FitMax(300,300) // Returns a 200x100px image (like Fit but prevents up-sampling)
// Warning: This method can distort images that are not the correct aspect ratio
$Image.ResizedImage(200, 300) // Forces dimensions of this image to the given values.
// Cropping functions
$Image.Fill(150,150) // Returns a 150x150px image resized and cropped to fill specified dimensions (up-sampled)
@ -70,11 +75,11 @@ The image manipulation functions can be used in your code with the same names, e
Some of the MetaData functions need to be prefixed with 'get', example `getHeight()`, `getOrientation()` etc.
Please refer to the `[api:Image]` API documentation for specific functions.
Please refer to the `[api:ImageManipulation]` API documentation for specific functions.
### Creating custom image functions
You can also create your own functions by extending the image class, for example
You can also create your own functions by decorating the `Image` class.
:::php
class MyImage extends DataExtension {
@ -88,23 +93,31 @@ You can also create your own functions by extending the image class, for example
}
public function PerfectSquare() {
return $this->owner->getFormattedImage('PerfectSquare');
}
public function generatePerfectSquare(Image_Backend $backend) {
return $backend->croppedResize(100,100);
$variant = $this->owner->variantName(__FUNCTION__);
return $this->owner->manipulateImage($variant, function(Image_Backend $backend) {
return $backend->croppedResize(100,100);
});
}
public function Exif(){
//http://www.v-nessa.net/2010/08/02/using-php-to-extract-image-exif-data
$image = $this->owner->AbsoluteLink();
$d=new ArrayList();
$mime = $this->owner->getMimeType();
$content = $this->owner->getAsString();
$image = "data://{$mime};base64," . base64_encode($content);
$d = new ArrayList();
$exif = exif_read_data($image, 0, true);
foreach ($exif as $key => $section) {
$a=new ArrayList();
foreach ($section as $name => $val)
$a->push(new ArrayData(array("Title"=>$name,"Content"=>$val)));
$d->push(new ArrayData(array("Title"=>strtolower($key),"Content"=>$a)));
$a = new ArrayList();
foreach ($section as $name => $val) {
$a->push(new ArrayData(array(
"Title"=>$name,
"Content"=>$val
)));
}
$d->push(new ArrayData(array(
"Title"=>strtolower($key),
"Content"=>$a
)));
}
return $d;
}
@ -114,6 +127,9 @@ You can also create your own functions by extending the image class, for example
Image:
extensions:
- MyImage
DBFile:
extensions:
- MyImage
### Form Upload
@ -140,7 +156,11 @@ always produce resampled output by adding this to your
mysite/config/config.yml file:
:::yml
Image:
# Configure resampling for File dataobject
File:
force_resample: true
# DBFile can be configured independently
DBFile:
force_resample: true
If you are intending to resample images with SilverStripe it is good practice
@ -148,16 +168,9 @@ to upload high quality (minimal compression) images as these will produce
better results when resampled. Very high resolution images may cause GD to
crash so a good size for website images is around 2000px on the longest edge.
### Clearing Thumbnail Cache
Images are (like all other Files) synchronized with the SilverStripe database.
This syncing happens whenever you load the "Files & Images" interface,
and whenever you upload or modify an Image through SilverStripe.
If you encounter problems with images not appearing, or have mysteriously
disappeared, you can try manually flushing the image cache.
http://localhost/dev/tasks/FlushGeneratedImagesTask
## API Documentation
`[api:Image]`
* `[api:File]`
* `[api:Image]`
* `[api:DBFile]`
* `[api:ImageManipulation]`

View File

@ -8,4 +8,5 @@ introduction: Upload, manage and manipulate files and images.
* [api:File]
* [api:Image]
* [api:Folder]
* [api:DBFile]
* [api:Folder]

View File

@ -9,7 +9,64 @@
* `DataObject::database_fields` now returns all fields on that table.
* `DataObject::db` now returns composite fields.
* `DataObject::ClassName` field has been refactored into a `DBClassName` type field.
* Image manipulations have been moved into a new `[api:ImageManipulation]` trait.
* `CMSFileAddController` removed.
* UploadField::setAllowedFileCategories('image') now excludes non-resizeable images. 'unresizeable_image' is
can be used to validate these types.
* `Image_Backend` API now loads and saves from `AssetContainer` instances rather than local files.
* The following File categories have been renamed: 'zip' to 'archive', 'doc' to 'document', and 'mov' to 'video'
## New API
* New filesystem abstraction including new `DBFile` database field to hold file references.
* `ShortcodeHandler` interface to help generate standard handlers for HTML shortcodes in the editor.
* `AssetNameGenerator` interface, including a `DefaultAssetNameGenerator` implementation, which is used to generate
renaming suggestions based on an original given filename in order to resolve file duplication issues.
## Deprecated classes/methods
The following image manipulations previously deprecated has been removed:
* `Image::SetRatioSize` superceded by `Fit`
* `Image::SetWidth` superceded by `ScaleWidth`
* `Image::SetHeight` superceded by `ScaleHeight`
* `Image::SetSize` superceded by `Pad`
* `Image::PaddedImage` superceded by `Pad`
* `Image::CroppedImage` superceded by `Fill`
* `Image::AssetLibraryPreview` superceded by `PreviewThumbnail`
* `Image::AssetLibraryThumbnail` superceded by `CMSThumbnail`
The following `File` methods have been removed. Since there is no longer any assumed local path for any file,
methods which dealt with such paths may no longer be relied on.
* `File::deletedatabaseOnly`
* `File::link_shortcode_handler` renamed to `handle_shortcode`
* `File::setParentID`
* `File::getFullPath`
* `File::getRelativePath`
* `File::Content` database field is removed
Image manipulations have been moved out of Image.php and now available to any File or DBFile which has the
appropriate mime types. The following file manipulations classes and methods have been removed:
* `CleanImageManipulationCache` class
* `Image_Cached` class
* `Image::regenerateFormattedImages`
* `Image::getGeneratedImages`
* `Image::deleteFormattedImages`
* `AssetAdmin::deleteunusedthumbnails`
* `AssetAdmin::getUnusedThumbnails`
Many `Folder` methods have also been removed:
* `Folder::syncChildren`
* `Folder::constructChild`
* `Folder::addUploadToFolder`
The following filesystem synchronisation methods are also removed
* `Filesystem::sync`
* `AssetAdmin::doSync`
## Upgrading
@ -28,11 +85,187 @@ Note that this will not allow you to utilise certain file versioning features in
:::yaml
\SilverStripe\Filesystem\Flysystem\FlysystemAssetStore:
legacy_paths: true
legacy_paths: true
See [/developer_guides/files/file_management] for more information on how the new system works.
### Migrating File DataObject from 3.x to 4.0
Since the structure of `File` dataobjects has changed, a new task `MigrateFileTask` has been added to assist
in migration of legacy files. Migration can be invoked by either this task, or can be configured to automatically
run during dev build by setting the `File.migrate_legacy_file` config to true. However, it's recommended that
this task is run manually during an explicit migration process, as this process could potentially consume
large amounts of memory and run for an extended time.
:::yaml
File:
migrate_legacy_file: true
### Upgrade code which acts on `Image`
As all image-specific manipulations has been refactored from `Image` into an `ImageManipulations` trait, which
is applied to both `File` and `DBFile`. These both implement a common interface `AssetContainer`, which
has the `getIsImage()` method. In some cases, it may be preferable to invoke this method to detect
if the asset is an image or not, rather than checking the subclass, as the asset may also be a `DBFile` with
an image filter applied, rather than an instance of the `Image` dataobject.
In addition, a new file category `image/supported` has been added, which is a subset of the `image` category.
This is the subset of all image types which may be assigned to the `[api:Image]` dataobject, and may have
manipulations applied to it. This should be used as the file type restriction on any `[api:UploadField]` which
is intended to upload images for manipulation.
Before:
:::php
if($file instanceof Image) {
$upload = new UploadField();
$upload->setAllowedFileCategories('image');
}
After:
:::php
if($file->getIsImage()) {
$upload = new UploadField();
$upload->setAllowedFileCategories('image/supported');
}
In cases where image-only assets may be assigned to relationships then your datamodel should specify explicitly
an `Image` datatype, or refer to `DBFile('image/supported')`.
E.g.
:::php
class MyObject extends DataObject {
private static $has_one = array(
"ImageObject" => "Image"
);
private static $db = array(
"ImageField" => "DBFile('image/supported')"
);
}
### Upgrading code that writes to `File` dataobjects, or writes files to the 'assets' folder
In the past all that was necessary to write a `File` dataobject to the database was to ensure a physical file
existed in the assets folder, and that the Filename of the dataobject was set to the same location.
Since the storage of physical files is no longer a standard location, it's necessary to delegate the writing of such
files to the asset persistence layer. As a wrapper for an individual file, you can use any of the `setFrom*`
methods to assign content from a local (e.g. temporary) file, a stream, or a string of content.
You would need to upgrade your code as below.
Before:
:::php
function importTempFile($tmp) {
copy($tmp, ASSETS_PATH . '/imported/' . basename($tmp));
$file = new File();
$file->setFilename('assets/imported/'.basename($tmp));
$file->write();
}
After:
:::php
function importTempFile($tmp) {
$file = new File();
$file->setFromLocalFile($tmp, 'imported/'.basename($tmp));
$file->write();
}
Note that 'assets' is no longer present in the new code, and the path beneath what was once assets is now
used to generate the 'filename' value. This is because there is no longer an assumption that files are
stored in the assets folder.
There are other important considerations in working with File dataobjects which differ from legacy:
* Deleting File dataobjects no longer removes the physical file directly. This is because any file could be referenced
from DBFile fields, and deleting these could be a potentially unsafe operation.
* File synchronisation is no longer automatic. This is due to the fact that there is no longer a 1-to-1 relationship
between physical files and File dataobjects.
* Moving files now performs a file copy rather than moving the underlying file, although only a single DataObject
will exist, and will reference the destination path.
* Folder dataobjects are now purely logical dataobjects, and perform no actual filesystem folder creation on write.
### Upgrading code performs custom image manipulations
As file storage and handling has been refactored into the abstract interface, many other components which were
once specific to Image.php have now been moved into a shared `ImageManipulation` trait. Manipulations of file content,
which are used to generate what are now called "variants" of assets, is now a generic api available to both `File`
and `DBFile` classes through this trait.
Custom manipulations, applied via extensions, must be modified to use the new API.
For instance, code which sizes images to a fixed width should be updated as below:
Before:
:::php
// in MyImageExtension.php
class MyImageExtension extends DataExtension {
public function GalleryThumbnail($height) {
return $this->getFormattedImage('GalleryThumbnail', $height);
}
public function generateGalleryThumbnail(Image_Backend $backend, $height) {
return $backend->paddedResize(300, $height);
}
}
// in _config.php
Image::add_extension('MyImageExtension');
Now image manipulations are implemented with a single method via a callback generator.
After:
:::php
// in MyImageExtension.php
class MyImageExtension extends Extension {
public function GalleryThumbnail($height) {
// Generates the manipulation key
$variant = $this->owner->variantName(__FUNCTION__, $height);
// Instruct the backend to search for an existing variant with this key,
// and include a callback used to generate this image if it doesn't exist
return $this->owner->manipulateImage($variant, function(Image_Backend $backend) use ($height) {
return $backend->paddedResize(300, $height);
});
}
}
// in _config.php
File::add_extension('MyImageExtension');
DBFile::add_extension('MyImageExtension');
There are a few differences in this new API:
* The extension is no longer specific to dataobjects, so it uses the generic 'Extension' class instead of 'DataExtension'
* This extension is added to both `DBFile` and `File`, or order to make this manipulation available to non-dataobject
file references as well, but it could be applied to either independently.
* A helper method `variantName` is invoked in order to help generate a unique variant key. Custom code may use another
generation mechanism.
* Non-image files may also have manipulations, however the specific `manipulateImage` should not be used in this case.
A generic `manipulate` method may be used, although the callback for this method both is given, and should return,
an `AssetStore` instance and file tuple (Filename, Hash, and Variant) rather than an Image_Backend.
### Upgrading code that uses composite db fields.
@ -115,5 +348,3 @@ After:
));
}
}

View File

@ -1,4 +1,9 @@
<?php
use SilverStripe\Filesystem\ImageManipulation;
use SilverStripe\Filesystem\Storage\AssetContainer;
use SilverStripe\Filesystem\Storage\AssetStore;
/**
* This class handles the representation of a file on the filesystem within the framework.
* Most of the methods also handle the {@link Folder} subclass.
@ -9,97 +14,77 @@
*
* <b>Security</b>
*
* Caution: It is recommended to disable any script execution in the "assets/"
* Caution: It is recommended to disable any script execution in the"assets/"
* directory in the webserver configuration, to reduce the risk of exploits.
* See http://doc.silverstripe.org/secure-development#filesystem
*
* <b>Asset storage</b>
*
* As asset storage is configured separately to any File DataObject records, this class
* does not make any assumptions about how these records are saved. They could be on
* a local filesystem, remote filesystem, or a virtual record container (such as in local memory).
*
* The File dataobject simply represents an externally facing view of shared resources
* within this asset store.
*
* Internally individual files are referenced by a"Filename" parameter, which represents a File, extension,
* and is optionally prefixed by a list of custom directories. This path is root-agnostic, so it does not
* automatically have a direct url mapping (even to the site's base directory).
*
* Additionally, individual files may have several versions distinguished by sha1 hash,
* of which a File DataObject can point to a single one. Files can also be distinguished by
* variants, which may be resized images or format-shifted documents.
*
* <b>Properties</b>
*
* - "Name": File name (including extension) or folder name.
* Should be the same as the actual filesystem.
* - "Title": Optional title of the file (for display purposes only).
* Defaults to "Name". Note that the Title field of Folder (subclass of File)
* -"Title": Optional title of the file (for display purposes only).
* Defaults to"Name". Note that the Title field of Folder (subclass of File)
* is linked to Name, so Name and Title will always be the same.
* - "Filename": Path of the file or folder, relative to the webroot.
* Usually starts with the "assets/" directory, and has no trailing slash.
* Defaults to the "assets/" directory plus "Name" property if not set.
* Setting the "Filename" property will override the "Name" property.
* The value should be in sync with "ParentID".
* - "Content": Typically unused, but handy for a textual representation of
* -"File": Physical asset backing this DB record. This is a composite DB field with
* its own list of properties. {@see DBFile} for more information
* -"Content": Typically unused, but handy for a textual representation of
* files, e.g. for fulltext indexing of PDF documents.
* - "ParentID": Points to a {@link Folder} record. Should be in sync with
* "Filename". A ParentID=0 value points to the "assets/" folder, not the webroot.
*
* <b>Synchronization</b>
*
* Changes to a File database record can change the filesystem entry,
* but not the other way around. If the filesystem path is renamed outside
* of SilverStripe, there's no way for the database to recover this linkage.
* New physical files on the filesystem can be "discovered" via {@link Filesystem::sync()},
* the equivalent {@link File} and {@link Folder} records are automatically
* created by this method.
*
* Certain property changes within the File API that can cause a "delayed" filesystem change:
* The change is enforced in {@link onBeforeWrite()} later on.
* - setParentID()
* - setFilename()
* - setName()
* It is recommended that you use {@link write()} directly after setting any of these properties,
* otherwise getters like {@link getFullPath()} and {@link getRelativePath()}
* will result paths that are inconsistent with the filesystem.
*
* Caution: Calling {@link delete()} will also delete from the filesystem.
* Call {@link deleteDatabaseOnly()} if you want to avoid this.
*
* <b>Creating Files and Folders</b>
*
* Typically both files and folders should be created first on the filesystem,
* and then reflected in as database records. Folders can be created recursively
* from SilverStripe both in the database and filesystem through {@link Folder::findOrMake()}.
* Ensure that you always set a "Filename" property when writing to the database,
* leaving it out can lead to unexpected results.
* -"ParentID": Points to a {@link Folder} record. Should be in sync with
* "Filename". A ParentID=0 value points to the"assets/" folder, not the webroot.
* -"ShowInSearch": True if this file is searchable
*
* @package framework
* @subpackage filesystem
*
* @property string Name Basename of the file
* @property string Title Title of the file
* @property string Filename Filename including path
* @property string Content
* @property string ShowInSearch Boolean that indicates if file is shown in search. Doesn't apply to Folder
*
* @property int ParentID ID of parent File/Folder
* @property int OwnerID ID of Member who owns the file
* @property string $Name Basename of the file
* @property string $Title Title of the file
* @property DBFile $File asset stored behind this File record
* @property string $Content
* @property string $ShowInSearch Boolean that indicates if file is shown in search. Doesn't apply to Folders
* @property int $ParentID ID of parent File/Folder
* @property int $OwnerID ID of Member who owns the file
*
* @method File Parent() Returns parent File
* @method Member Owner() Returns Member object of file owner.
*/
class File extends DataObject {
class File extends DataObject implements ShortcodeHandler, AssetContainer {
private static $default_sort = "\"Name\"";
use ImageManipulation;
private static $singular_name = "File";
private static $default_sort ="\"Name\"";
private static $plural_name = "Files";
private static $singular_name ="File";
private static $plural_name ="Files";
private static $db = array(
"Name" => "Varchar(255)",
"Title" => "Varchar(255)",
"Filename" => "Text",
"Content" => "Text",
"Name" =>"Varchar(255)",
"Title" =>"Varchar(255)",
"File" =>"DBFile",
// Only applies to files, doesn't inherit for folder
'ShowInSearch' => 'Boolean(1)',
);
private static $has_one = array(
"Parent" => "File",
"Owner" => "Member"
"Parent" =>"File",
"Owner" =>"Member"
);
private static $has_many = array();
private static $many_many = array();
private static $defaults = array(
"ShowInSearch" => 1,
);
@ -108,7 +93,7 @@ class File extends DataObject {
"Hierarchy",
);
private static $casting = array (
private static $casting = array(
'TreeTitle' => 'HTMLText'
);
@ -126,11 +111,12 @@ class File extends DataObject {
* Instructions for the change you need to make are included in a comment in the config file.
*/
private static $allowed_extensions = array(
'','ace','arc','arj','asf','au','avi','bmp','bz2','cab','cda','css','csv','dmg','doc','docx','dotx','dotm',
'flv','gif','gpx','gz','hqx','ico','jar','jpeg','jpg','js','kml', 'm4a','m4v',
'mid','midi','mkv','mov','mp3','mp4','mpa','mpeg','mpg','ogg','ogv','pages','pcx','pdf','pkg',
'png','pps','ppt','pptx','potx','potm','ra','ram','rm','rtf','sit','sitx','tar','tgz','tif','tiff',
'txt','wav','webm','wma','wmv','xls','xlsx','xltx','xltm','zip','zipx',
'', 'ace', 'arc', 'arj', 'asf', 'au', 'avi', 'bmp', 'bz2', 'cab', 'cda', 'css', 'csv', 'dmg', 'doc',
'docx', 'dotx', 'dotm', 'flv', 'gif', 'gpx', 'gz', 'hqx', 'ico', 'jar', 'jpeg', 'jpg', 'js', 'kml',
'm4a', 'm4v', 'mid', 'midi', 'mkv', 'mov', 'mp3', 'mp4', 'mpa', 'mpeg', 'mpg', 'ogg', 'ogv', 'pages',
'pcx', 'pdf', 'png', 'pps', 'ppt', 'pptx', 'potx', 'potm', 'ra', 'ram', 'rm', 'rtf', 'sit', 'sitx',
'tar', 'tgz', 'tif', 'tiff', 'txt', 'wav', 'webm', 'wma', 'wmv', 'xls', 'xlsx', 'xltx', 'xltm', 'zip',
'zipx',
);
/**
@ -138,25 +124,45 @@ class File extends DataObject {
* @var array Category identifiers mapped to commonly used extensions.
*/
private static $app_categories = array(
'archive' => array(
'ace', 'arc', 'arj', 'bz', 'bz2', 'cab', 'dmg', 'gz', 'hqx', 'jar', 'rar', 'sit', 'sitx', 'tar', 'tgz',
'zip', 'zipx',
),
'audio' => array(
"aif" ,"au" ,"mid" ,"midi" ,"mp3" ,"ra" ,"ram" ,"rm","mp3" ,"wav" ,"m4a" ,"snd" ,"aifc" ,"aiff" ,"wma",
"apl", "avr" ,"cda" ,"ogg"
'aif', 'aifc', 'aiff', 'apl', 'au', 'avr', 'cda', 'm4a', 'mid', 'midi', 'mp3', 'ogg', 'ra',
'ram', 'rm', 'snd', 'wav', 'wma',
),
'mov' => array(
"mpeg" ,"mpg" ,"mp4" ,"m1v" ,"mp2" ,"mpa" ,"mpe" ,"ifo" ,"vob","avi" ,"wmv" ,"asf" ,"m2v" ,"qt", "ogv", "webm"
),
'zip' => array(
"arc" ,"rar" ,"tar" ,"gz" ,"tgz" ,"bz2" ,"dmg" ,"jar","ace" ,"arj" ,"bz" ,"cab"
'document' => array(
'css', 'csv', 'doc', 'docx', 'dotm', 'dotx', 'htm', 'html', 'gpx', 'js', 'kml', 'pages', 'pdf',
'potm', 'potx', 'pps', 'ppt', 'pptx', 'rtf', 'txt', 'xhtml', 'xls', 'xlsx', 'xltm', 'xltx', 'xml',
),
'image' => array(
"bmp" ,"gif" ,"jpg" ,"jpeg" ,"pcx" ,"tif" ,"png" ,"alpha","als" ,"cel" ,"icon" ,"ico" ,"ps"
'alpha', 'als', 'bmp', 'cel', 'gif', 'ico', 'icon', 'jpeg', 'jpg', 'pcx', 'png', 'ps', 'tif', 'tiff',
),
'image/supported' => array(
'gif', 'jpeg', 'jpg', 'png'
),
'flash' => array(
'swf', 'fla'
'fla', 'swf'
),
'doc' => array(
'doc','docx','txt','rtf','xls','xlsx','pages', 'ppt','pptx','pps','csv', 'html','htm','xhtml', 'xml','pdf'
)
'video' => array(
'asf', 'avi', 'flv', 'ifo', 'm1v', 'm2v', 'm4v', 'mkv', 'mov', 'mp2', 'mp4', 'mpa', 'mpe', 'mpeg',
'mpg', 'ogv', 'qt', 'vob', 'webm', 'wmv',
),
);
/**
* Map of file extensions to class type
*
* @config
* @var
*/
private static $class_for_file_extension = array(
'*' => 'File',
'jpg' => 'Image',
'jpeg' => 'Image',
'png' => 'Image',
'gif' => 'Image',
);
/**
@ -167,28 +173,35 @@ class File extends DataObject {
*/
private static $apply_restrictions_to_admin = true;
/**
* If enabled, legacy file dataobjects will be automatically imported into the APL
*
* @config
* @var bool
*/
private static $migrate_legacy_file = false;
/**
* @config
* @var boolean
*/
private static $update_filesystem = true;
/**
* Cached result of a "SHOW FIELDS" call
* in instance_get() for performance reasons.
*
* @var array
*/
protected static $cache_file_fields = null;
public static function get_shortcodes() {
return 'file_link';
}
/**
* Replace "[file_link id=n]" shortcode with an anchor tag or link to the file.
* @param $arguments array Arguments to the shortcode
* @param $content string Content of the returned link (optional)
* @param $parser object Specify a parser to parse the content (see {@link ShortCodeParser})
* @return string anchor HTML tag if content argument given, otherwise file path link
* Replace"[file_link id=n]" shortcode with an anchor tag or link to the file.
*
* @param array $arguments Arguments passed to the parser
* @param string $content Raw shortcode
* @param ShortcodeParser $parser Parser
* @param string $shortcode Name of shortcode used to register this handler
* @param array $extra Extra arguments
* @return string Result of the handled shortcode
*/
public static function link_shortcode_handler($arguments, $content = null, $parser = null) {
public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = array()) {
if(!isset($arguments['id']) || !is_numeric($arguments['id'])) return;
$record = DataObject::get_by_id('File', $arguments['id']);
@ -198,7 +211,9 @@ class File extends DataObject {
$record = ErrorPage::get()->filter("ErrorCode", 404)->first();
}
if (!$record) return; // There were no suitable matches at all.
if (!$record) {
return; // There were no suitable matches at all.
}
}
// build the HTML tag
@ -224,28 +239,27 @@ class File extends DataObject {
/**
* A file only exists if the file_exists() and is in the DB as a record
*
* Use $file->isInDB() to only check for a DB record
* Use $file->File->exists() to only check if the asset exists
*
* @return bool
*/
public function exists() {
return parent::exists() && file_exists($this->getFullPath());
return parent::exists() && $this->File->exists();
}
/**
* Find a File object by the given filename.
*
* @param String $filename Matched against the "Name" property.
* @return mixed null if not found, File object of found file
* @param string $filename Filename to search for, including any custom parent directories.
* @return File
*/
public static function find($filename) {
// Get the base file if $filename points to a resampled file
$filename = Image::strip_resampled_prefix($filename);
// Split to folders and the actual filename, and traverse the structure.
$parts = explode("/", $filename);
$parentID = 0;
$item = null;
foreach($parts as $part) {
if($part == ASSETS_DIR && !$parentID) continue;
$item = File::get()->filter(array(
'Name' => $part,
'ParentID' => $parentID
@ -267,12 +281,11 @@ class File extends DataObject {
}
/**
* Just an alias function to keep a consistent API with SiteTree
*
* @return string The relative link to the file
* @deprecated 4.0
*/
public function RelativeLink() {
return $this->getFilename();
Deprecation::notice('4.0', 'Use getURL instead, as not all files will be relative to the site root.');
return Director::makeRelative($this->getURL());
}
/**
@ -291,54 +304,49 @@ class File extends DataObject {
return Convert::raw2xml($this->Title);
}
/**
* 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 {@link parent::onBeforeDelete()}, though!
*/
protected function onBeforeDelete() {
parent::onBeforeDelete();
// ensure that the record is synced with the filesystem before deleting
$this->updateFilesystem();
if($this->exists() && !is_dir($this->getFullPath())) {
unlink($this->getFullPath());
}
}
/**
* @todo Enforce on filesystem URL level via mod_rewrite
*
* @param Member $member
* @return boolean
*/
public function canView($member = null) {
if(!$member) $member = Member::currentUser();
if(!$member) {
$member = Member::currentUser();
}
$results = $this->extend('canView', $member);
if($results && is_array($results)) if(!min($results)) return false;
$result = $this->extendedCan('canView', $member);
if($result !== null) {
return $result;
}
return true;
}
/**
* Returns true if the following conditions are met:
* - CMS_ACCESS_AssetAdmin
*
* @todo Decouple from CMS view access
* Check if this file can be modified
*
* @param Member $member
* @return boolean
*/
public function canEdit($member = null) {
if(!$member) $member = Member::currentUser();
if(!$member) {
$member = Member::currentUser();
}
$result = $this->extendedCan('canEdit', $member);
if($result !== null) return $result;
if($result !== null) {
return $result;
}
return Permission::checkMember($member, array('CMS_ACCESS_AssetAdmin', 'CMS_ACCESS_LeftAndMain'));
}
/**
* Check if a file can be created
*
* @param Member $member
* @param array $context
* @return boolean
*/
public function canCreate($member = null, $context = array()) {
@ -355,13 +363,20 @@ class File extends DataObject {
}
/**
* Check if this file can be deleted
*
* @param Member $member
* @return boolean
*/
public function canDelete($member = null) {
if(!$member) $member = Member::currentUser();
if(!$member) {
$member = Member::currentUser();
}
$results = $this->extend('canDelete', $member);
if($results && is_array($results)) if(!min($results)) return false;
$result = $this->extendedCan('canDelete', $member);
if($result !== null) {
return $result;
}
return $this->canEdit($member);
}
@ -375,75 +390,45 @@ class File extends DataObject {
*/
public function getCMSFields() {
// Preview
if($this instanceof Image) {
$formattedImage = $this->getFormattedImage(
'ScaleWidth',
Config::inst()->get('Image', 'asset_preview_width')
);
$thumbnail = $formattedImage ? $formattedImage->URL : '';
$previewField = new LiteralField("ImageFull",
"<img id='thumbnailImage' class='thumbnail-preview' src='{$thumbnail}?r="
. rand(1,100000) . "' alt='{$this->Name}' />\n"
);
} else {
$previewField = new LiteralField("ImageFull", $this->CMSThumbnail());
}
// Upload
$uploadField = UploadField::create('UploadField','Upload Field')
->setPreviewMaxWidth(40)
->setPreviewMaxHeight(30)
->setAllowedMaxFileNumber(1);
//$uploadField->setTemplate('FileEditUploadField');
if ($this->ParentID) {
$parent = $this->Parent();
if ($parent) { //set the parent that the Upload field should use for uploads
$uploadField->setFolderName($parent->getFilename());
$uploadField->setRecord($parent);
}
}
//create the file attributes in a FieldGroup
$filePreview = CompositeField::create(
CompositeField::create(
$previewField
)->setName("FilePreviewImage")->addExtraClass('cms-file-info-preview'),
CompositeField::create(new LiteralField("ImageFull", $this->PreviewThumbnail()))
->setName("FilePreviewImage")
->addExtraClass('cms-file-info-preview'),
CompositeField::create(
CompositeField::create(
new ReadonlyField("FileType", _t('AssetTableField.TYPE','File type') . ':'),
new ReadonlyField("Size", _t('AssetTableField.SIZE','File size') . ':', $this->getSize()),
$urlField = new ReadonlyField('ClickableURL', _t('AssetTableField.URL','URL'),
ReadonlyField::create(
'ClickableURL',
_t('AssetTableField.URL','URL'),
sprintf('<a href="%s" target="_blank">%s</a>', $this->Link(), $this->RelativeLink())
),
)
->setDontEscape(true),
new DateField_Disabled("Created", _t('AssetTableField.CREATED','First uploaded') . ':'),
new DateField_Disabled("LastEdited", _t('AssetTableField.LASTEDIT','Last changed') . ':')
)
)->setName("FilePreviewData")->addExtraClass('cms-file-info-data')
)->setName("FilePreview")->addExtraClass('cms-file-info');
$urlField->dontEscape = true;
)
->setName("FilePreviewData")
->addExtraClass('cms-file-info-data')
)
->setName("FilePreview")
->addExtraClass('cms-file-info');
//get a tree listing with only folder, no files
$folderTree = new TreeDropdownField("ParentID", _t('AssetTableField.FOLDER','Folder'), 'Folder');
$fields = new FieldList(
new TabSet('Root',
new Tab('Main',
$filePreview,
//TODO: make the uploadField replace the existing file
// $uploadField,
new TextField("Title", _t('AssetTableField.TITLE','Title')),
new TextField("Name", _t('AssetTableField.FILENAME','Filename')),
$ownerField
= new DropdownField("OwnerID", _t('AssetTableField.OWNER','Owner'), Member::mapInCMSGroups()),
$folderTree
DropdownField::create("OwnerID", _t('AssetTableField.OWNER','Owner'), Member::mapInCMSGroups())
->setHasEmptyDefault(true),
new TreeDropdownField("ParentID", _t('AssetTableField.FOLDER','Folder'), 'Folder')
)
)
);
$ownerField->setHasEmptyDefault(true);
// Folder has its own updateCMSFields hook
if(!($this instanceof Folder)) $this->extend('updateCMSFields', $fields);
$this->extend('updateCMSFields', $fields);
return $fields;
}
@ -451,9 +436,10 @@ class File extends DataObject {
* Returns a category based on the file extension.
* This can be useful when grouping files by type,
* showing icons on filelinks, etc.
* Possible group values are: "audio", "mov", "zip", "image".
* Possible group values are:"audio","mov","zip","image".
*
* @return String
* @param string $ext Extension to check
* @return string
*/
public static function get_app_category($ext) {
$ext = strtolower($ext);
@ -463,38 +449,50 @@ class File extends DataObject {
return false;
}
/**
* For a category or list of categories, get the list of file extensions
*
* @param array|string $categories List of categories, or single category
* @return array
*/
public static function get_category_extensions($categories) {
if(empty($categories)) {
return array();
}
// Fix arguments into a single array
if(!is_array($categories)) {
$categories = array($categories);
} elseif(count($categories) === 1 && is_array(reset($categories))) {
$categories = reset($categories);
}
// Check configured categories
$appCategories = self::config()->app_categories;
// Merge all categories into list of extensions
$extensions = array();
foreach(array_filter($categories) as $category) {
if(isset($appCategories[$category])) {
$extensions = array_merge($extensions, $appCategories[$category]);
} else {
throw new InvalidArgumentException("Unknown file category: $category");
}
}
$extensions = array_unique($extensions);
sort($extensions);
return $extensions;
}
/**
* Returns a category based on the file extension.
*
* @return String
* @return string
*/
public function appCategory() {
return self::get_app_category($this->getExtension());
}
public function CMSThumbnail() {
return '<img src="' . $this->Icon() . '" />';
}
/**
* Return the relative URL of an icon for the file type,
* based on the {@link appCategory()} value.
* Images are searched for in "framework/images/app_icons/".
*
* @return String
*/
public function Icon() {
$ext = strtolower($this->getExtension());
if(!Director::fileExists(FRAMEWORK_DIR . "/images/app_icons/{$ext}_32.gif")) {
$ext = $this->appCategory();
}
if(!Director::fileExists(FRAMEWORK_DIR . "/images/app_icons/{$ext}_32.gif")) {
$ext = "generic";
}
return FRAMEWORK_DIR . "/images/app_icons/{$ext}_32.gif";
}
/**
* Should be called after the file was uploaded
@ -503,15 +501,6 @@ class File extends DataObject {
$this->extend('onAfterUpload');
}
/**
* Delete the database record (recursively for folders) without touching the filesystem
*/
public function deleteDatabaseOnly() {
if(is_numeric($this->ID)) {
DB::prepared_query('DELETE FROM "File" WHERE "ID" = ?', array($this->ID));
}
}
/**
* Make sure the file has a name
*/
@ -519,88 +508,61 @@ class File extends DataObject {
parent::onBeforeWrite();
// Set default owner
if(!$this->ID && !$this->OwnerID) {
$this->OwnerID = (Member::currentUser() ? Member::currentUser()->ID : 0);
if(!$this->isInDB() && !$this->OwnerID) {
$this->OwnerID = Member::currentUserID();
}
// 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)
*/
protected function onAfterWrite() {
parent::onAfterWrite();
if(!$this->getField('Name')) {
$this->Name ="new-" . strtolower($this->class);
}
// Propegate changes to the AssetStore and update the DBFile field
$this->updateFilesystem();
}
/**
* Moving the file if appropriate according to updated database content.
* Throws an Exception if the new file already exists.
* This will check if the parent record and/or name do not match the name on the underlying
* DBFile record, and if so, copy this file to the new location, and update the record to
* point to this new file.
*
* Caution: This method should just be called during a {@link write()} invocation,
* as it relies on {@link DataObject->isChanged()}, 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.
* This method will update the File {@see DBFile} field value on success, so it must be called
* before writing to the database
*
* Assumes that the "Filename" property was previously updated, either directly or indirectly.
* (it might have been influenced by {@link setName()} or {@link setParentID()} before).
* @param bool True if changed
*/
public function updateFilesystem() {
if(!$this->config()->update_filesystem) return false;
// Regenerate "Filename", just to be sure
$this->setField('Filename', $this->getRelativePath());
// If certain elements are changed, update the filesystem reference
if(!$this->isChanged('Filename')) return false;
$changedFields = $this->getChangedFields();
$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);
// TODO Fix Filetest->testCreateWithFilenameWithSubfolder() to enable this
// // Create parent folders recursively in database and filesystem
// if(!is_a($this, 'Folder')) {
// $folder = Folder::findOrMake(dirname($pathAfterAbs));
// if($folder) $this->ParentID = $folder->ID;
// }
// 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 $pathBeforeAbs to $pathAfterAbs - $pathBeforeAbs 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 $pathBeforeAbs to $pathAfterAbs - Directory " . dirname($pathAfter)
. " doesn't exist");
}
}
// Rename file or folder
$success = rename($pathBeforeAbs, $pathAfterAbs);
if(!$success) throw new Exception("Cannot move $pathBeforeAbs to $pathAfterAbs");
if(!$this->config()->update_filesystem) {
return false;
}
// Check the file exists
if(!$this->File->exists()) {
return false;
}
// Check path updated record will point to
// If no changes necessary, skip
$pathBefore = $this->File->getFilename();
$pathAfter = $this->getFilename();
if($pathAfter === $pathBefore) {
return false;
}
// Copy record to new location via stream
$stream = $this->File->getStream();
$result = $this->File->setFromStream($stream, $pathAfter);
// If the backend chose a new name, update the local record
if($result['Filename'] !== $pathAfter) {
// Correct saved folder to selected filename
$pathAfter = $result['Filename'];
$this->setFilename($pathAfter);
}
// Update any database references
$this->updateLinks($pathBefore, $pathAfter);
return true;
}
/**
@ -625,7 +587,7 @@ class File extends DataObject {
* and removes characters that might be invalid on the filesystem.
* Also adds a suffix to the name if the filename already exists
* on the filesystem, and is associated to a different {@link File} database record
* in the same folder. This means "myfile.jpg" might become "myfile-1.jpg".
* in the same folder. This means"myfile.jpg" might become"myfile-1.jpg".
*
* Does not change the filesystem itself, please use {@link write()} for this.
*
@ -635,14 +597,18 @@ class File extends DataObject {
$oldName = $this->Name;
// It can't be blank, default to Title
if(!$name) $name = $this->Title;
if(!$name) {
$name = $this->Title;
}
// Fix illegal characters
$filter = FileNameFilter::create();
$name = $filter->filter($name);
// We might have just turned it blank, so check again.
if(!$name) $name = 'new-folder';
if(!$name) {
$name = 'new-folder';
}
// If it's changed, check for duplicates
if($oldName && $oldName != $name) {
@ -658,17 +624,13 @@ class File extends DataObject {
))->first()
) {
$suffix++;
$name = "$base-$suffix.$ext";
$name ="$base-$suffix.$ext";
}
}
// Update actual field value
$this->setField('Name', $name);
// Ensure that the filename is updated as well (only in-memory)
// Important: Circumvent the getter to avoid infinite loops
$this->setField('Filename', $this->getRelativePath());
// Update title
if(!$this->Title) {
$this->Title = str_replace(array('-','_'),' ', preg_replace('/\.[^.]+$/', '', $name));
@ -686,126 +648,111 @@ class File extends DataObject {
}
/**
* Does not change the filesystem itself, please use {@link write()} for this.
*/
public function setParentID($parentID) {
$this->setField('ParentID', (int)$parentID);
// Don't change on the filesystem, we'll handle that in onBeforeWrite()
$this->setField('Filename', $this->getRelativePath());
return $this->getField('ParentID');
}
/**
* Gets the absolute URL accessible through the web.
* Gets the URL of this file
*
* @uses Director::absoluteBaseURL()
* @return string
*/
public function getAbsoluteURL() {
return Director::absoluteURL($this->getURL());
$url = $this->getURL();
if($url) {
return Director::absoluteURL($url);
}
}
/**
* Gets the relative URL accessible through the web.
* Gets the URL of this file
*
* @uses Director::baseURL()
* @return string
*/
public function getURL() {
return Controller::join_links(Director::baseURL(), $this->getFilename());
}
/**
* Returns an absolute filesystem path to the file.
* Use {@link getRelativePath()} to get the same path relative to the webroot.
*
* @return String
*/
public function getFullPath() {
$baseFolder = Director::baseFolder();
if(strpos($this->getFilename(), $baseFolder) === 0) {
// if path is absolute already, just return
return $this->getFilename();
} else {
// otherwise assume silverstripe-basefolder
return Director::baseFolder() . '/' . $this->getFilename();
if($this->File->exists()) {
return $this->File->getURL();
}
}
/**
* Returns path relative to webroot.
* Serves as a "fallback" method to create the "Filename" property if it isn't set.
* If no {@link Folder} is set ("ParentID" property),
* defaults to a filename relative to the ASSETS_DIR (usually "assets/").
* Get URL, but without resampling.
*
* @return String
* @return string
*/
public function getRelativePath() {
if($this->ParentID) {
// Don't use the cache, the parent has just been changed
$p = DataObject::get_by_id('Folder', $this->ParentID, false);
if($p && $p->exists()) return $p->getRelativePath() . $this->getField("Name");
else return ASSETS_DIR . "/" . $this->getField("Name");
} else if($this->getField("Name")) {
return ASSETS_DIR . "/" . $this->getField("Name");
} else {
return ASSETS_DIR;
public function getSourceURL() {
if($this->File->exists()) {
return $this->File->getSourceURL();
}
}
/**
* @todo Coupling with cms module, remove this method.
*
* @return string
*/
public function DeleteLink() {
return Director::absoluteBaseURL()."admin/assets/removefile/".$this->ID;
}
public function getFilename() {
// Default behaviour: Return field if its set
if($this->getField('Filename')) {
return $this->getField('Filename');
} else {
return ASSETS_DIR . '/';
// Check if this file is nested within a folder
$parent = $this->Parent();
if($parent && $parent->exists()) {
return $this->join_paths($parent->getFilename(), $this->Name);
}
return $this->Name;
}
/**
* Caution: this does not change the location of the file on the filesystem.
* Update the ParentID and Name for the given filename.
*
* On save, the underlying DBFile record will move the underlying file to this location.
* Thus it will not update the underlying Filename value until this is done.
*
* @param string $filename
* @return $this
*/
public function setFilename($val) {
$this->setField('Filename', $val);
public function setFilename($filename) {
// Check existing folder path
$folder = '';
$parent = $this->Parent();
if($parent && $parent->exists()) {
$folder = $parent->Filename;
}
// Detect change in foldername
$newFolder = ltrim(dirname(trim($filename, '/')), '.');
if($folder !== $newFolder) {
if(!$newFolder) {
$this->ParentID = 0;
} else {
$parent = Folder::find_or_make($newFolder);
$this->ParentID = $parent->ID;
}
}
// "Filename" is the "master record" (existing on the filesystem),
// meaning we have to adjust the "Name" property in the database as well.
$this->setField('Name', basename($val));
// Update base name
$this->Name = basename($filename);
return $this;
}
/**
* Returns the file extension
*
* @todo This overrides getExtension() in DataObject, but it does something completely different.
* This should be renamed to getFileExtension(), but has not been yet as it may break
* legacy code.
*
* @return String
* @return string
*/
public function getExtension() {
return self::get_file_extension($this->getField('Filename'));
return self::get_file_extension($this->Name);
}
/**
* 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").
* 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"
* -"myfile" returns""
* -"myfile.txt" returns"txt"
* -"myfile.tar.gz" returns"gz"
*
* @param string $filename
* @return string
@ -814,6 +761,28 @@ class File extends DataObject {
return pathinfo($filename, PATHINFO_EXTENSION);
}
/**
* Given an extension, determine the icon that should be used
*
* @param string $extension
* @return string Icon filename relative to base url
*/
public static function get_icon_for_extension($extension) {
$extension = strtolower($extension);
// Check if exact extension has an icon
if(!file_exists(FRAMEWORK_PATH ."/images/app_icons/{$extension}_32.gif")) {
$extension = static::get_app_category($extension);
// Fallback to category specific icon
if(!file_exists(FRAMEWORK_PATH ."/images/app_icons/{$extension}_32.gif")) {
$extension ="generic";
}
}
return FRAMEWORK_DIR ."/images/app_icons/{$extension}_32.gif";
}
/**
* Return the type of file for the given extension
* on the current file name.
@ -821,6 +790,16 @@ class File extends DataObject {
* @return string
*/
public function getFileType() {
return self::get_file_type($this->getFilename());
}
/**
* Get descriptive type of file based on filename
*
* @param string $filename
* @return string Description of file
*/
public static function get_file_type($filename) {
$types = array(
'gif' => _t('File.GifType', 'GIF image - good for diagrams'),
'jpg' => _t('File.JpgType', 'JPEG image - good for photos'),
@ -845,126 +824,87 @@ class File extends DataObject {
'htm' => _t('File.HtmlType', 'HTML file')
);
$ext = strtolower($this->getExtension());
return isset($types[$ext]) ? $types[$ext] : 'unknown';
// Get extension
$extension = strtolower(self::get_file_extension($filename));
return isset($types[$extension]) ? $types[$extension] : 'unknown';
}
/**
* Returns the size of the file type in an appropriate format.
*
* @return string|false String value, or false if doesn't exist
*/
public function getSize() {
$size = $this->getAbsoluteSize();
return ($size) ? self::format_size($size) : false;
if($size) {
return static::format_size($size);
}
return false;
}
/**
* Formats a file size (eg: (int)42 becomes string '42 bytes')
*
* @todo unit tests
*
* @param int $size
* @return string
*/
public static function format_size($size) {
if($size < 1024) return $size . ' bytes';
if($size < 1024*10) return (round($size/1024*10)/10). ' KB';
if($size < 1024*1024) return round($size/1024) . ' KB';
if($size < 1024*1024*10) return (round(($size/1024)/1024*10)/10) . ' MB';
if($size < 1024*1024*1024) return round(($size/1024)/1024) . ' MB';
if($size < 1024) {
return $size . ' bytes';
}
if($size < 1024*10) {
return (round($size/1024*10)/10). ' KB';
}
if($size < 1024*1024) {
return round($size/1024) . ' KB';
}
if($size < 1024*1024*10) {
return (round(($size/1024)/1024*10)/10) . ' MB';
}
if($size < 1024*1024*1024) {
return round(($size/1024)/1024) . ' MB';
}
return round($size/(1024*1024*1024)*10)/10 . ' GB';
}
/**
* Convert a php.ini value (eg: 512M) to bytes
*
* @param string $phpIniValue
* @todo unit tests
*
* @param string $iniValue
* @return int
*/
public static function ini2bytes($PHPiniValue) {
switch(strtolower(substr(trim($PHPiniValue), -1))) {
public static function ini2bytes($iniValue) {
switch(strtolower(substr(trim($iniValue), -1))) {
case 'g':
$PHPiniValue *= 1024;
$iniValue *= 1024;
case 'm':
$PHPiniValue *= 1024;
$iniValue *= 1024;
case 'k':
$PHPiniValue *= 1024;
$iniValue *= 1024;
}
return $PHPiniValue;
return $iniValue;
}
/**
* Return file size in bytes.
*
* @return int
*/
public function getAbsoluteSize(){
if(file_exists($this->getFullPath())) {
$size = filesize($this->getFullPath());
return $size;
} else {
return 0;
}
}
public function flushCache($persistant = true) {
parent::flushCache($persistant);
self::$cache_file_fields = null;
}
/**
*
* @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
*
*/
public function fieldLabels($includerelations = true) {
$labels = parent::fieldLabels($includerelations);
$labels['Name'] = _t('File.Name', 'Name');
$labels['Title'] = _t('File.Title', 'Title');
$labels['Filename'] = _t('File.Filename', 'Filename');
$labels['Filename'] = _t('File.Filename', 'Filename');
$labels['Content'] = _t('File.Content', 'Content');
return $labels;
return $this->File->getAbsoluteSize();
}
public function validate() {
if($this->config()->apply_restrictions_to_admin || !Permission::check('ADMIN')) {
// Extension validation
// TODO Merge this with Upload_Validator
$extension = $this->getExtension();
$allowed = array_map('strtolower', $this->config()->allowed_extensions);
if(!in_array(strtolower($extension), $allowed)) {
$exts = $allowed;
sort($exts);
$message = _t(
'File.INVALIDEXTENSION',
'Extension is not allowed (valid: {extensions})',
'Argument 1: Comma-separated list of valid extensions',
array('extensions' => wordwrap(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.
$result = new ValidationResult(true);
$result = new ValidationResult();
$this->File->validate($result, $this->Name);
$this->extend('validate', $result);
return $result;
}
/**
* @config
* @var Array Only use lowercase extensions in here.
*/
private static $class_for_file_extension = array(
'*' => 'File',
'jpg' => 'Image',
'jpeg' => 'Image',
'png' => 'Image',
'gif' => 'Image',
);
/**
* Maps a {@link File} subclass to a specific extension.
* By default, files with common image extensions will be created
@ -993,15 +933,142 @@ class File extends DataObject {
*/
public static function set_class_for_file_extension($exts, $class) {
if(!is_array($exts)) $exts = array($exts);
foreach($exts as $ext) {
if(!is_subclass_of($class, 'File')) {
throw new InvalidArgumentException(
sprintf('Class "%s" (for extension "%s") is not a valid subclass of File', $class, $ext)
sprintf('Class"%s" (for extension"%s") is not a valid subclass of File', $class, $ext)
);
}
self::config()->class_for_file_extension = array($ext => $class);
}
}
public function getMetaData() {
if($this->File->exists()) {
return $this->File->getMetaData();
}
}
public function getMimeType() {
if($this->File->exists()) {
return $this->File->getMimeType();
}
}
public function getStream() {
if($this->File->exists()) {
return $this->File->getStream();
}
}
public function getString() {
if($this->File->exists()) {
return $this->File->getString();
}
}
public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $conflictResolution = null) {
$result = $this->File->setFromLocalFile($path, $filename, $hash, $variant, $conflictResolution);
// Update File record to name of the uploaded asset
if($result) {
$this->setFilename($result['Filename']);
}
return $result;
}
public function setFromStream($stream, $filename, $hash = null, $variant = null, $conflictResolution = null) {
$result = $this->File->setFromStream($stream, $filename, $hash, $variant, $conflictResolution);
// Update File record to name of the uploaded asset
if($result) {
$this->setFilename($result['Filename']);
}
return $result;
}
public function setFromString($data, $filename, $hash = null, $variant = null, $conflictResolution = null) {
$result = $this->File->setFromString($data, $filename, $hash, $variant, $conflictResolution);
// Update File record to name of the uploaded asset
if($result) {
$this->setFilename($result['Filename']);
}
return $result;
}
public function getIsImage() {
return false;
}
public function getHash() {
return $this->File->Hash;
}
public function getVariant() {
return $this->File->Variant;
}
/**
* Return a html5 tag of the appropriate for this file (normally img or a)
*
* @return string
*/
public function forTemplate() {
return $this->getTag() ?: '';
}
/**
* Return a html5 tag of the appropriate for this file (normally img or a)
*
* @return string
*/
public function getTag() {
$template = $this->File->getFrontendTemplate();
if(empty($template)) {
return '';
}
return (string)$this->renderWith($template);
}
public function requireDefaultRecords() {
parent::requireDefaultRecords();
// Check if old file records should be migrated
if(!$this->config()->migrate_legacy_file) {
return;
}
$migrated = FileMigrationHelper::singleton()->run();
if($migrated) {
DB::alteration_message("{$migrated} File DataObjects upgraded","changed");
}
}
/**
* Joins one or more segments together to build a Filename identifier.
*
* Note that the result will not have a leading slash, and should not be used
* with local file paths.
*
* @param string $part,... Parts
* @return string
*/
public static function join_paths() {
$args = func_get_args();
if(count($args) === 1 && is_array($args[0])) {
$args = $args[0];
}
$parts = array();
foreach($args as $arg) {
$part = trim($arg, ' \\/');
if($part) {
$parts[] = $part;
}
}
return implode('/', $parts);
}
}

View File

@ -0,0 +1,105 @@
<?php
use SilverStripe\Filesystem\Storage\AssetStore;
/**
* Service to help migrate File dataobjects to the new APL.
*
* This service does not alter these records in such a way that prevents downgrading back to 3.x
*
* @package framework
* @subpackage filesystem
*/
class FileMigrationHelper extends Object {
/**
* Perform migration
*
* @param string $base Absolute base path (parent of assets folder). Will default to BASE_PATH
* @return int Number of files successfully migrated
*/
public function run($base = null) {
if(empty($base)) {
$base = BASE_PATH;
}
// Check if the File dataobject has a "Filename" field.
// If not, cannot migrate
if(!DB::get_schema()->hasField('File', 'Filename')) {
return 0;
}
// Set max time and memory limit
increase_time_limit_to();
increase_memory_limit_to();
// Loop over all files
$count = 0;
$filenameMap = $this->getFilenameArray();
foreach($this->getFileQuery() as $file) {
// Get the name of the file to import
$filename = $filenameMap[$file->ID];
$success = $this->migrateFile($base, $file, $filename);
if($success) {
$count++;
}
}
return $count;
}
/**
* Migrate a single file
*
* @param string $base Absolute base path (parent of assets folder)
* @param File $file
* @param type $legacyFilename
* @return bool True if this file is imported successfully
*/
protected function migrateFile($base, File $file, $legacyFilename) {
// Make sure this legacy file actually exists
$path = $base . '/' . $legacyFilename;
if(!file_exists($path)) {
return false;
}
// Copy local file into this filesystem
$filename = $file->getFilename();
$result = $file->setFromLocalFile($path, $filename, null, null, AssetStore::CONFLICT_OVERWRITE);
// Move file if the APL changes filename value
if($result['Filename'] !== $filename) {
$this->setFilename($result['Filename']);
}
// Save
$file->write();
return true;
}
/**
* Get list of File dataobjects to import
*
* @return DataList
*/
protected function getFileQuery() {
// Select all records which have a Filename value, but not FileFilename.
return File::get()
->exclude('ClassName', 'Folder')
->filter('FileFilename', array('', null))
->where('"File"."Filename" IS NOT NULL AND "File"."Filename" != \'\''); // Non-orm field
}
/**
* Get map of File IDs to legacy filenames
*
* @return array
*/
protected function getFilenameArray() {
// Convert original query, ensuring the legacy "Filename" is included in the result
return $this
->getFileQuery()
->dataQuery()
->selectFromTable('File', array('ID', 'Filename'))
->execute()
->map(); // map ID to Filename
}
}

View File

@ -24,22 +24,6 @@ class Filesystem extends Object {
*/
protected static $cache_folderModTime;
/**
* @config
*
* Array of file / folder regex expressions to exclude from the
* {@link Filesystem::sync()}
*
* @var array
*/
private static $sync_blacklisted_patterns = array(
"/^\./",
"/^_combinedfiles$/i",
"/^_resampled$/i",
"/^web.config/i",
"/^Thumbs(.)/"
);
/**
* Create a folder on the filesystem, recursively.
* Uses {@link Filesystem::$folder_create_mask} to set filesystem permissions.
@ -162,82 +146,4 @@ class Filesystem extends Object {
if($_ENV['OS'] == "Windows_NT" || $_SERVER['WINDIR']) return $filename[1] == ':' && $filename[2] == '/';
else return $filename[0] == '/';
}
/**
* This function ensures the file table is correct with the files in the assets folder.
*
* If a Folder record ID is given, all of that folder's children will be synchronised.
* If the given Folder ID isn't found, or not specified at all, then everything will
* be synchronised from the root folder (singleton Folder).
*
* See {@link File->updateFilesystem()} to sync properties of a single database record
* back to the equivalent filesystem record.
*
* @param int $folderID Folder ID to sync along with all it's children
* @param Boolean $syncLinkTracking Determines if the link tracking data should also
* be updated via {@link SiteTree->syncLinkTracking()}. Setting this to FALSE
* means that broken links inside page content are not noticed, at least until the next
* call to {@link SiteTree->write()} on this page.
* @return string Localized status message
*/
public static function sync($folderID = null, $syncLinkTracking = true) {
$folder = DataObject::get_by_id('Folder', (int) $folderID);
if(!($folder && $folder->exists())) $folder = singleton('Folder');
$results = $folder->syncChildren();
$finished = false;
while(!$finished) {
$orphans = DB::query('SELECT "ChildFile"."ID" FROM "File" AS "ChildFile"
LEFT JOIN "File" AS "ParentFile" ON "ChildFile"."ParentID" = "ParentFile"."ID"
WHERE "ParentFile"."ID" IS NULL AND "ChildFile"."ParentID" > 0');
$finished = true;
if($orphans) foreach($orphans as $orphan) {
$finished = false;
// Delete the database record but leave the filesystem alone
$file = DataObject::get_by_id("File", $orphan['ID']);
$file->deleteDatabaseOnly();
unset($file);
}
}
// Update the image tracking of all pages
if($syncLinkTracking) {
if(class_exists('SiteTree')) {
// if subsites exist, go through each subsite and sync each subsite's pages.
// disabling the filter doesn't work reliably, because writing pages that share
// the same URLSegment between subsites will break, e.g. "home" between two
// sites will modify one of them to "home-2", thinking it's a duplicate. The
// check before a write is done in SiteTree::validURLSegment()
if(class_exists('Subsite')) {
// loop through each subsite ID, changing the subsite, then query it's pages
foreach(Subsite::get()->getIDList() as $id) {
Subsite::changeSubsite($id);
foreach(SiteTree::get() as $page) {
// syncLinkTracking is called by SiteTree::onBeforeWrite().
// Call it without affecting the page version, as this is an internal change.
$page->writeWithoutVersion();
}
}
// change back to the main site so the foreach below works
Subsite::changeSubsite(0);
}
foreach(SiteTree::get() as $page) {
// syncLinkTracking is called by SiteTree::onBeforeWrite().
// Call it without affecting the page version, as this is an internal change.
$page->writeWithoutVersion();
}
}
}
return _t(
'Filesystem.SYNCRESULTS',
'Sync complete: {createdcount} items created, {deletedcount} items deleted',
array('createdcount' => (int)$results['added'], 'deletedcount' => (int)$results['deleted'])
);
}
}

View File

@ -1,15 +1,18 @@
<?php
/**
* Represents a folder in the assets/ directory.
* The folder path is stored in the "Filename" property.
* Represents a logical folder, which may be used to organise assets
* stored in the configured backend.
*
* Updating the "Name" or "Filename" properties on
* a folder object also updates all associated children
* (both {@link File} and {@link Folder} records).
* Unlike {@see File} dataobjects, there is not necessarily a physical filesystem entite which
* represents a Folder, and it may be purely logical. However, a physical folder may exist
* if the backend creates one.
*
* 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.
* Additionally, folders do not have URLs (relative or absolute), nor do they have paths.
*
* When a folder is moved or renamed, records within it will automatically be copied to the updated
* location.
*
* Deleting a folder will remove all child records, but not any physical files.
*
* See {@link File} documentation for more details about the
* relationship between the database and filesystem in the SilverStripe file APIs.
@ -23,7 +26,9 @@ class Folder extends File {
private static $plural_name = "Folders";
private static $default_sort = "\"Name\"";
public function exists() {
return $this->isInDB();
}
/**
*
@ -31,33 +36,29 @@ class Folder extends File {
public function populateDefaults() {
parent::populateDefaults();
if(!$this->Name) $this->Name = _t('AssetAdmin.NEWFOLDER',"NewFolder");
if(!$this->Name) {
$this->Name = _t('AssetAdmin.NEWFOLDER', "NewFolder");
}
}
/**
* Find the given folder or create it both as {@link Folder} database records
* and on the filesystem. If necessary, creates parent folders as well. If it's
* unable to find or make the folder, it will return null (as /assets is unable
* to be represented by a Folder DataObject)
* Find the given folder or create it as a database record
*
* @param $folderPath string Absolute or relative path to the file.
* If path is relative, its interpreted relative to the "assets/" directory.
* @param string $folderPath Directory path relative to assets root
* @return Folder|null
*/
public static function find_or_make($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);
$folderPath = preg_replace('/^\/?(.*)\/?$/', '$1', trim($folderPath));
$parts = explode("/",$folderPath);
$parentID = 0;
$item = null;
$filter = FileNameFilter::create();
foreach($parts as $part) {
if(!$part) continue; // happens for paths with a trailing slash
if(!$part) {
continue; // happens for paths with a trailing slash
}
// Ensure search includes folders with illegal characters removed, but
// err in favour of matching existing folders if $folderPath
@ -75,331 +76,65 @@ class Folder extends File {
$item->Title = $part;
$item->write();
}
if(!file_exists($item->getFullPath())) {
Filesystem::makeFolder($item->getFullPath());
}
$parentID = $item->ID;
}
return $item;
}
/**
* Synchronize the file database with the actual content of the assets
* folder.
*/
public function syncChildren() {
$parentID = (int)$this->ID; // parentID = 0 on the singleton, used as the 'root node';
$added = 0;
$deleted = 0;
$skipped = 0;
// First, merge any children that are duplicates
$duplicateChildrenNames = DB::prepared_query(
'SELECT "Name" FROM "File" WHERE "ParentID" = ? GROUP BY "Name" HAVING count(*) > 1',
array($parentID)
)->column();
if($duplicateChildrenNames) foreach($duplicateChildrenNames as $childName) {
// Note, we do this in the database rather than object-model; otherwise we get all sorts of problems
// about deleting files
$children = DB::prepared_query(
'SELECT "ID" FROM "File" WHERE "Name" = ? AND "ParentID" = ?',
array($childName, $parentID)
)->column();
if($children) {
$keptChild = array_shift($children);
foreach($children as $removedChild) {
DB::prepared_query('UPDATE "File" SET "ParentID" = ? WHERE "ParentID" = ?',
array($keptChild, $removedChild));
DB::prepared_query('DELETE FROM "File" WHERE "ID" = ?', array($removedChild));
}
} else {
user_error("Inconsistent database issue: SELECT ID FROM \"File\" WHERE Name = '$childName'"
. " AND ParentID = $parentID should have returned data", E_USER_WARNING);
}
}
// Get index of database content
// We don't use DataObject so that things like subsites doesn't muck with this.
$dbChildren = DB::prepared_query('SELECT * FROM "File" WHERE "ParentID" = ?', array($parentID));
$hasDbChild = array();
if($dbChildren) {
foreach($dbChildren as $dbChild) {
$className = $dbChild['ClassName'];
if(!$className) $className = "File";
$hasDbChild[$dbChild['Name']] = new $className($dbChild);
}
}
$unwantedDbChildren = $hasDbChild;
// 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 = '';
// Iterate through the actual children, correcting the database as necessary
$baseDir = $this->FullPath;
// @todo this shouldn't call die() but log instead
if($parentID && !$this->Filename) die($this->ID . " - " . $this->FullPath);
if(file_exists($baseDir)) {
$actualChildren = scandir($baseDir);
$ignoreRules = Filesystem::config()->sync_blacklisted_patterns;
$allowedExtensions = File::config()->allowed_extensions;
$checkExtensions = $this->config()->apply_restrictions_to_admin || !Permission::check('ADMIN');
foreach($actualChildren as $actualChild) {
$skip = false;
// Check ignore patterns
if($ignoreRules) foreach($ignoreRules as $rule) {
if(preg_match($rule, $actualChild)) {
$skip = true;
break;
}
}
// Check allowed extensions, unless admin users are allowed to bypass these exclusions
if($checkExtensions
&& ($extension = self::get_file_extension($actualChild))
&& !in_array(strtolower($extension), $allowedExtensions)
) {
$skip = true;
}
if($skip) {
$skipped++;
continue;
}
// A record with a bad class type doesn't deserve to exist. It must be purged!
if(isset($hasDbChild[$actualChild])) {
$child = $hasDbChild[$actualChild];
if(( !( $child instanceof Folder ) && is_dir($baseDir . $actualChild) )
|| (( $child instanceof Folder ) && !is_dir($baseDir . $actualChild)) ) {
DB::prepared_query('DELETE FROM "File" WHERE "ID" = ?', array($child->ID));
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'];
$skipped += $childResult['skipped'];
}
// Clean up the child record from memory after use. Important!
$child->destroy();
$child = null;
}
// Iterate through the unwanted children, removing them all
if(isset($unwantedDbChildren)) foreach($unwantedDbChildren as $unwantedDbChild) {
DB::prepared_query('DELETE FROM "File" WHERE "ID" = ?', array($unwantedDbChild->ID));
$deleted++;
}
} else {
DB::prepared_query('DELETE FROM "File" WHERE "ID" = ?', array($this->ID));
}
return array(
'added' => $added,
'deleted' => $deleted,
'skipped' => $skipped
);
}
/**
* 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.
*
* @param string $name Name of the file or folder
* @return integer the ID of the newly saved File record
*/
public function constructChild($name) {
// Determine the class name - File, Folder or Image
$baseDir = $this->FullPath;
if(is_dir($baseDir . $name)) {
$className = "Folder";
} else {
$className = File::get_class_for_file_extension(pathinfo($name, PATHINFO_EXTENSION));
}
$ownerID = Member::currentUserID();
$filename = $this->Filename . $name;
if($className == 'Folder' ) $filename .= '/';
$nowExpression = DB::get_conn()->now();
DB::prepared_query("INSERT INTO \"File\"
(\"ClassName\", \"ParentID\", \"OwnerID\", \"Name\", \"Filename\", \"Created\", \"LastEdited\", \"Title\")
VALUES (?, ?, ?, ?, ?, $nowExpression, $nowExpression, ?)",
array($className, $this->ID, $ownerID, $name, $filename, $name)
);
return DB::get_generated_id("File");
}
/**
* Take a file uploaded via a POST form, and save it inside this folder.
* File names are filtered through {@link FileNameFilter}, see class documentation
* on how to influence this behaviour.
*/
public function addUploadToFolder($tmpFile) {
if(!is_array($tmpFile)) {
user_error("Folder::addUploadToFolder() Not passed an array."
. " Most likely, the form hasn't got the right enctype", E_USER_ERROR);
}
if(!isset($tmpFile['size'])) {
return;
}
$base = BASE_PATH;
// $parentFolder = Folder::findOrMake("Uploads");
// Generate default filename
$nameFilter = FileNameFilter::create();
$file = $nameFilter->filter($tmpFile['name']);
while($file[0] == '_' || $file[0] == '.') {
$file = substr($file, 1);
}
$file = $this->RelativePath . $file;
Filesystem::makeFolder(dirname("$base/$file"));
$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++;
$oldFile = $file;
if(strpos($file, '.') !== false) {
$file = preg_replace('/[0-9]*(\.[^.]+$)/', $i . '\\1', $file);
} elseif(strpos($file, '_') !== false) {
$file = preg_replace('/_([^_]+$)/', '_' . $i, $file);
} else {
$file .= '_'.$i;
}
if($oldFile == $file && $i > 2) user_error("Couldn't fix $file$ext with $i", E_USER_ERROR);
}
if (move_uploaded_file($tmpFile['tmp_name'], "$base/$file$ext")) {
// Update with the new image
return $this->constructChild(basename($file . $ext));
} else {
if(!file_exists($tmpFile['tmp_name'])) {
user_error("Folder::addUploadToFolder: '$tmpFile[tmp_name]' doesn't exist", E_USER_ERROR);
} else {
user_error("Folder::addUploadToFolder: Couldn't copy '$tmpFile[tmp_name]' to '$base/$file$ext'",
E_USER_ERROR);
}
return false;
}
}
public function validate() {
return new ValidationResult(true);
}
//-------------------------------------------------------------------------------------------------
// Data Model Definition
public function getRelativePath() {
return parent::getRelativePath() . "/";
}
public function onBeforeDelete() {
if($this->ID && ($children = $this->AllChildren())) {
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() );
foreach($this->AllChildren() as $child) {
$child->delete();
}
parent::onBeforeDelete();
}
/** Override setting the Title of Folders to that Name, Filename and Title are always in sync.
/**
* Override setting the Title of Folders to that Name 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. */
* of a file that is different from its name on disk. But folders should always match their name on disk.
*
* @param string $title
* @return $this
*/
public function setTitle($title) {
$this->setName($title);
return $this;
}
/**
* Get the folder title
*
* @return string
*/
public function getTitle() {
return $this->Name;
}
/**
* Override setting the Title of Folders to that Name 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.
*
* @param string $name
* @return $this
*/
public function setName($name) {
parent::setName($name);
$this->setField('Title', $this->Name);
}
public function setFilename($filename) {
$this->setField('Title',pathinfo($filename, PATHINFO_BASENAME));
parent::setFilename($filename);
return $this;
}
/**
* A folder doesn't have a (meaningful) file size.
*
* @return Null
* @return null
*/
public function getSize() {
return null;
}
/**
* Delete the database record (recursively for folders) without touching the filesystem
*/
public function deleteDatabaseOnly() {
if($children = $this->myChildren()) {
foreach($children as $child) $child->deleteDatabaseOnly();
}
parent::deleteDatabaseOnly();
}
/**
* Returns all children of this folder
*
@ -427,26 +162,12 @@ class Folder extends File {
return $this->ChildFolders()->exists();
}
/**
* 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())) {
foreach($children as $child) {
$child->updateFilesystem();
$child->write();
}
}
}
/**
* 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}
* and implemeting updateCMSFields(FieldList $fields) on that extension.
*
* @return FieldList
*/
public function getCMSFields() {
// Hide field on root level, which can't be renamed
@ -476,21 +197,25 @@ class Folder extends File {
/**
* Get the number of children of this folder that are also folders.
*
* @return int
*/
public function numChildFolders() {
return $this->ChildFolders()->count();
}
/**
* @return String
* @return string
*/
public function CMSTreeClasses() {
$classes = sprintf('class-%s', $this->class);
if(!$this->canDelete())
if(!$this->canDelete()) {
$classes .= " nodelete";
}
if(!$this->canEdit())
if(!$this->canEdit()) {
$classes .= " disabled";
}
$classes .= $this->markingClasses('numChildFolders');
@ -501,9 +226,64 @@ class Folder extends File {
* @return string
*/
public function getTreeTitle() {
return $treeTitle = sprintf(
return sprintf(
"<span class=\"jstree-foldericon\"></span><span class=\"item\">%s</span>",
Convert::raw2xml(str_replace(array("\n","\r"),"",$this->Title))
Convert::raw2att(preg_replace('~\R~u', ' ', $this->Title))
);
}
public function getFilename() {
return parent::getFilename() . '/';
}
/**
* Folders do not have public URLs
*
* @return null
*/
public function getURL() {
return null;
}
/**
* Folders do not have public URLs
*
* @return string
*/
public function getAbsoluteURL() {
return null;
}
public function onAfterWrite() {
parent::onAfterWrite();
// Ensure that children loading $this->Parent() load the refreshed record
$this->flushCache();
$this->updateChildFilesystem();
}
public function updateFilesystem() {
// No filesystem changes to update
}
/**
* If a write is skipped due to no changes, ensure that nested records still get asked to update
*/
public function onAfterSkippedWrite() {
$this->updateChildFilesystem();
}
/**
* Update filesystem of all children
*/
public function updateChildFilesystem() {
// Writing this record should trigger a write (and potential updateFilesystem) on each child
foreach($this->AllChildren() as $child) {
$child->write();
}
}
public function StripThumbnail() {
return null;
}
}

View File

@ -1,578 +0,0 @@
<?php
/**
* A wrapper class for GD-based images, with lots of manipulation functions.
* @package framework
* @subpackage filesystem
*/
class GDBackend extends Object implements Image_Backend {
protected $gd, $width, $height;
protected $quality;
protected $interlace;
protected $cache, $cacheKey, $manipulation;
/**
* @config
* @var integer
*/
private static $default_quality = 75;
/**
* @config
* @var integer
*/
private static $image_interlace = 0;
/**
* Set the default image quality.
*
* @deprecated 4.0 Use the "GDBackend.default_quality" config setting instead
* @param quality int A number from 0 to 100, 100 being the best quality.
*/
public static function set_default_quality($quality) {
Deprecation::notice('4.0', 'Use the "GDBackend.default_quality" config setting instead');
if(is_numeric($quality) && (int) $quality >= 0 && (int) $quality <= 100) {
config::inst()->update('GDBackend', 'default_quality', (int) $quality);
}
}
/**
* __construct
*
* @param string $filename = null
* @param array $args = array()
* @return void
*/
public function __construct($filename = null, $args = array()) {
// If we're working with image resampling, things could take a while. Bump up the time-limit
increase_time_limit_to(300);
$this->cache = SS_Cache::factory('GDBackend_Manipulations');
if($filename && is_readable($filename)) {
$this->cacheKey = md5(implode('_', array($filename, filemtime($filename))));
$this->manipulation = implode('|', $args);
$cacheData = unserialize($this->cache->load($this->cacheKey));
$cacheData = ($cacheData !== false) ? $cacheData : array();
if ($this->imageAvailable($filename, $this->manipulation)) {
$cacheData[$this->manipulation] = true;
$this->cache->save(serialize($cacheData), $this->cacheKey);
// We use getimagesize instead of extension checking, because sometimes extensions are wrong.
list($width, $height, $type, $attr) = getimagesize($filename);
switch($type) {
case 1:
if(function_exists('imagecreatefromgif'))
$this->setImageResource(imagecreatefromgif($filename));
break;
case 2:
if(function_exists('imagecreatefromjpeg'))
$this->setImageResource(imagecreatefromjpeg($filename));
break;
case 3:
if(function_exists('imagecreatefrompng')) {
$img = imagecreatefrompng($filename);
imagesavealpha($img, true); // save alphablending setting (important)
$this->setImageResource($img);
}
break;
}
}
}
parent::__construct();
$this->quality = $this->config()->default_quality;
$this->interlace = $this->config()->image_interlace;
}
public function setImageResource($resource) {
$this->gd = $resource;
$this->width = imagesx($resource);
$this->height = imagesy($resource);
}
public function getImageResource() {
return $this->gd;
}
/**
* @param string $filename
* @return boolean
*/
public function imageAvailable($filename, $manipulation) {
return ($this->checkAvailableMemory($filename) && ! $this->failedResample($filename, $manipulation));
}
/**
* Check if we've got enough memory available for resampling this image. This check is rough,
* so it will not catch all images that are too large - it also won't work accurately on large,
* animated GIFs as bits per pixel can't be calculated for an animated GIF with a global color
* table.
*
* @param string $filename
* @return boolean
*/
public function checkAvailableMemory($filename) {
$limit = translate_memstring(ini_get('memory_limit'));
if ($limit < 0) return true; // memory_limit == -1
$imageInfo = getimagesize($filename);
// bits per channel (rounded up, default to 1)
$bits = isset($imageInfo['bits']) ? ($imageInfo['bits'] + 7) / 8 : 1;
// channels (default 4 rgba)
$channels = isset($imageInfo['channels']) ? $imageInfo['channels'] : 4;
$bytesPerPixel = $bits * $channels;
// width * height * bytes per pixel
$memoryRequired = $imageInfo[0] * $imageInfo[1] * $bytesPerPixel;
return $memoryRequired + memory_get_usage() < $limit;
}
/**
* Check if this image has previously crashed GD when attempting to open it - if it's opened
* successfully, the manipulation's cache key is removed.
*
* @param string $filename
* @return boolean
*/
public function failedResample($filename, $manipulation) {
$cacheData = unserialize($this->cache->load($this->cacheKey));
return ($cacheData && array_key_exists($manipulation, $cacheData));
}
/**
* Set the image quality, used when saving JPEGs.
*/
public function setQuality($quality) {
$this->quality = $quality;
}
/**
* Resize an image to cover the given width/height completely, and crop off any overhanging edges.
*/
public function croppedResize($width, $height) {
if(!$this->gd) return;
$width = round($width);
$height = round($height);
// Check that a resize is actually necessary.
if ($width == $this->width && $height == $this->height) {
return $this;
}
$newGD = imagecreatetruecolor($width, $height);
// Preserves transparency between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
$destAR = $width / $height;
if ($this->width > 0 && $this->height > 0 ){
// We can't divide by zero theres something wrong.
$srcAR = $this->width / $this->height;
// Destination narrower than the source
if($destAR < $srcAR) {
$srcY = 0;
$srcHeight = $this->height;
$srcWidth = round( $this->height * $destAR );
$srcX = round( ($this->width - $srcWidth) / 2 );
// Destination shorter than the source
} else {
$srcX = 0;
$srcWidth = $this->width;
$srcHeight = round( $this->width / $destAR );
$srcY = round( ($this->height - $srcHeight) / 2 );
}
imagecopyresampled($newGD, $this->gd, 0,0, $srcX, $srcY, $width, $height, $srcWidth, $srcHeight);
}
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Resizes the image to fit within the given region.
* Behaves similarly to paddedResize but without the padding.
* @todo This method isn't very efficent
*/
public function fittedResize($width, $height) {
$gd = $this->resizeByHeight($height);
if($gd->width > $width) $gd = $gd->resizeByWidth($width);
return $gd;
}
/**
* hasImageResource
*
* @return boolean
*/
public function hasImageResource() {
return $this->gd ? true : false;
}
/**
* Resize an image, skewing it as necessary.
*/
public function resize($width, $height) {
if(!$this->gd) return;
if($width < 0 || $height < 0) throw new InvalidArgumentException("Image resizing dimensions cannot be negative");
if(!$width && !$height) throw new InvalidArgumentException("No dimensions given when resizing image");
if(!$width) throw new InvalidArgumentException("Width not given when resizing image");
if(!$height) throw new InvalidArgumentException("Height not given when resizing image");
//use whole numbers, ensuring that size is at least 1x1
$width = max(1, round($width));
$height = max(1, round($height));
// Check that a resize is actually necessary.
if ($width == $this->width && $height == $this->height) {
return $this;
}
$newGD = imagecreatetruecolor($width, $height);
// Preserves transparency between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
imagecopyresampled($newGD, $this->gd, 0,0, 0, 0, $width, $height, $this->width, $this->height);
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Rotates image by given angle.
*
* @param angle
*
* @return GD
*/
public function rotate($angle) {
if(!$this->gd) return;
if(function_exists("imagerotate")) {
$newGD = imagerotate($this->gd, $angle,0);
} else {
//imagerotate is not included in PHP included in Ubuntu
$newGD = $this->rotatePixelByPixel($angle);
}
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Rotates image by given angle. It's slow because makes it pixel by pixel rather than
* using built-in function. Used when imagerotate function is not available(i.e. Ubuntu)
*
* @param angle
*
* @return GD
*/
public function rotatePixelByPixel($angle) {
$sourceWidth = imagesx($this->gd);
$sourceHeight = imagesy($this->gd);
if ($angle == 180) {
$destWidth = $sourceWidth;
$destHeight = $sourceHeight;
} else {
$destWidth = $sourceHeight;
$destHeight = $sourceWidth;
}
$rotate=imagecreatetruecolor($destWidth,$destHeight);
imagealphablending($rotate, false);
for ($x = 0; $x < ($sourceWidth); $x++) {
for ($y = 0; $y < ($sourceHeight); $y++) {
$color = imagecolorat($this->gd, $x, $y);
switch ($angle) {
case 90:
imagesetpixel($rotate, $y, $destHeight - $x - 1, $color);
break;
case 180:
imagesetpixel($rotate, $destWidth - $x - 1, $destHeight - $y - 1, $color);
break;
case 270:
imagesetpixel($rotate, $destWidth - $y - 1, $x, $color);
break;
default: $rotate = $this->gd;
};
}
}
return $rotate;
}
/**
* Crop's part of image.
*
* @param top y position of left upper corner of crop rectangle
* @param left x position of left upper corner of crop rectangle
* @param width rectangle width
* @param height rectangle height
*
* @return GD
*/
public function crop($top, $left, $width, $height) {
$newGD = imagecreatetruecolor($width, $height);
// Preserve alpha channel between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
imagecopyresampled($newGD, $this->gd, 0, 0, $left, $top, $width, $height, $width, $height);
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Method return width of image.
*
* @return integer width.
*/
public function getWidth() {
return $this->width;
}
/**
* Method return height of image.
*
* @return integer height
*/
public function getHeight() {
return $this->height;
}
/**
* Resize an image by width. Preserves aspect ratio.
*/
public function resizeByWidth( $width ) {
$heightScale = $width / $this->width;
return $this->resize( $width, $heightScale * $this->height );
}
/**
* Resize an image by height. Preserves aspect ratio
*/
public function resizeByHeight( $height ) {
$scale = $height / $this->height;
return $this->resize( $scale * $this->width, $height );
}
/**
* Resize the image by preserving aspect ratio. By default, it will keep the image inside the maxWidth
* and maxHeight. Passing useAsMinimum will make the smaller dimension equal to the maximum corresponding dimension
*/
public function resizeRatio( $maxWidth, $maxHeight, $useAsMinimum = false ) {
$widthRatio = $maxWidth / $this->width;
$heightRatio = $maxHeight / $this->height;
if( $widthRatio < $heightRatio )
return $useAsMinimum ? $this->resizeByHeight( $maxHeight ) : $this->resizeByWidth( $maxWidth );
else
return $useAsMinimum ? $this->resizeByWidth( $maxWidth ) : $this->resizeByHeight( $maxHeight );
}
public static function color_web2gd($image, $webColor) {
if(substr($webColor,0,1) == "#") $webColor = substr($webColor,1);
$r = hexdec(substr($webColor,0,2));
$g = hexdec(substr($webColor,2,2));
$b = hexdec(substr($webColor,4,2));
return imagecolorallocate($image, $r, $g, $b);
}
/**
* Resize to fit fully within the given box, without resizing. Extra space left around
* the image will be padded with the background color.
* @param width
* @param height
* @param backgroundColour
*/
public function paddedResize($width, $height, $backgroundColor = "FFFFFF") {
if(!$this->gd) return;
$width = round($width);
$height = round($height);
// Check that a resize is actually necessary.
if ($width == $this->width && $height == $this->height) {
return $this;
}
$newGD = imagecreatetruecolor($width, $height);
// Preserves transparency between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
$bg = GD::color_web2gd($newGD, $backgroundColor);
imagefilledrectangle($newGD, 0, 0, $width, $height, $bg);
$destAR = $width / $height;
if ($this->width > 0 && $this->height > 0) {
// We can't divide by zero theres something wrong.
$srcAR = $this->width / $this->height;
// Destination narrower than the source
if($destAR > $srcAR) {
$destY = 0;
$destHeight = $height;
$destWidth = round( $height * $srcAR );
$destX = round( ($width - $destWidth) / 2 );
// Destination shorter than the source
} else {
$destX = 0;
$destWidth = $width;
$destHeight = round( $width / $srcAR );
$destY = round( ($height - $destHeight) / 2 );
}
imagecopyresampled($newGD, $this->gd,
$destX, $destY, 0, 0,
$destWidth, $destHeight, $this->width, $this->height);
}
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Make the image greyscale
* $rv = red value, defaults to 38
* $gv = green value, defaults to 36
* $bv = blue value, defaults to 26
* Based (more or less entirely, with changes for readability) on code from
* http://www.teckis.com/scriptix/thumbnails/teck.html
*/
public function greyscale($rv=38, $gv=36, $bv=26) {
$width = $this->width;
$height = $this->height;
$newGD = imagecreatetruecolor($this->width, $this->height);
// Preserves transparency between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
$rt = $rv + $bv + $gv;
$rr = ($rv == 0) ? 0 : 1/($rt/$rv);
$br = ($bv == 0) ? 0 : 1/($rt/$bv);
$gr = ($gv == 0) ? 0 : 1/($rt/$gv);
for($dy = 0; $dy < $height; $dy++) {
for($dx = 0; $dx < $width; $dx++) {
$pxrgb = imagecolorat($this->gd, $dx, $dy);
$heightgb = ImageColorsforIndex($this->gd, $pxrgb);
$newcol = ($rr*$heightgb['red']) + ($br*$heightgb['blue']) + ($gr*$heightgb['green']);
$setcol = ImageColorAllocateAlpha($newGD, $newcol, $newcol, $newcol, $heightgb['alpha']);
imagesetpixel($newGD, $dx, $dy, $setcol);
}
}
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
public function makeDir($dirname) {
if(!file_exists(dirname($dirname))) $this->makeDir(dirname($dirname));
if(!file_exists($dirname)) mkdir($dirname, Config::inst()->get('Filesystem', 'folder_create_mask'));
}
public function writeTo($filename) {
$this->makeDir(dirname($filename));
if($filename) {
if(file_exists($filename)) list($width, $height, $type, $attr) = getimagesize($filename);
if(file_exists($filename)) unlink($filename);
$ext = strtolower(substr($filename, strrpos($filename,'.')+1));
if(!isset($type)) switch($ext) {
case "gif": $type = IMAGETYPE_GIF; break;
case "jpeg": case "jpg": case "jpe": $type = IMAGETYPE_JPEG; break;
default: $type = IMAGETYPE_PNG; break;
}
// if $this->interlace != 0, the output image will be interlaced
imageinterlace ($this->gd, $this->interlace);
// if the extension does not exist, the file will not be created!
switch($type) {
case IMAGETYPE_GIF: imagegif($this->gd, $filename); break;
case IMAGETYPE_JPEG: imagejpeg($this->gd, $filename, $this->quality); break;
// case 3, and everything else
default:
// Save them as 8-bit images
// imagetruecolortopalette($this->gd, false, 256);
imagepng($this->gd, $filename); break;
}
if(file_exists($filename)) @chmod($filename,0664);
// Remove image manipulation from cache now that it's complete
$cacheData = unserialize($this->cache->load($this->cacheKey));
if(isset($cacheData[$this->manipulation])) unset($cacheData[$this->manipulation]);
$this->cache->save(serialize($cacheData), $this->cacheKey);
}
}
/**
* @param Image $frontend
* @return void
*/
public function onBeforeDelete($frontend) {
$file = Director::baseFolder() . "/" . $frontend->Filename;
if (file_exists($file)) {
$key = md5(implode('_', array($file, filemtime($file))));
$this->cache->remove($key);
}
}
}
/**
* This class is maintained for backwards-compatibility only. Please use the {@link GDBackend} class instead.
*
* @package framework
* @subpackage filesystem
*/
class GD extends GDBackend {
/**
* @deprecated 4.0 Use the "GDBackend.default_quality" config setting instead
*/
public static function set_default_quality($quality) {
Deprecation::notice('4.0', 'Use the "GDBackend.default_quality" config setting instead');
GDBackend::set_default_quality($quality);
}
}

642
filesystem/GDBackend.php Normal file
View File

@ -0,0 +1,642 @@
<?php
use SilverStripe\Filesystem\Storage\AssetContainer;
use SilverStripe\Filesystem\Storage\AssetStore;
/**
* A wrapper class for GD-based images, with lots of manipulation functions.
* @package framework
* @subpackage filesystem
*/
class GDBackend extends Object implements Image_Backend, Flushable {
/**
* GD Resource
*
* @var resource
*/
protected $gd;
/**
* @var Zend_Cache_Core
*/
protected $cache;
/**
* @var int
*/
protected $width;
/**
* @var height
*/
protected $height;
/**
* @var int
*/
protected $quality;
/**
*
* @var int
*/
protected $interlace;
/**
* @config
* @var integer
*/
private static $default_quality = 75;
/**
* @config
* @var integer
*/
private static $image_interlace = 0;
public function __construct(AssetContainer $assetContainer = null) {
parent::__construct();
$this->cache = SS_Cache::factory('GDBackend_Manipulations');
if($assetContainer) {
$this->loadFromContainer($assetContainer);
}
}
public function loadFrom($path) {
// If we're working with image resampling, things could take a while. Bump up the time-limit
increase_time_limit_to(300);
$this->resetResource();
// Skip if path is unavailable
if(!file_exists($path)) {
return;
}
$mtime = filemtime($path);
// Skip if load failed before
if($this->failedResample($path, $mtime)) {
return;
}
// We use getimagesize instead of extension checking, because sometimes extensions are wrong.
$meta = getimagesize($path);
if($meta === false) {
$this->markFailed($path, $mtime);
return;
}
$gd = null;
switch($meta[2]) {
case 1:
if(function_exists('imagecreatefromgif')) {
$gd = imagecreatefromgif($path);
}
break;
case 2:
if(function_exists('imagecreatefromjpeg')) {
$gd = imagecreatefromjpeg($path);
}
break;
case 3:
if(function_exists('imagecreatefrompng')) {
$gd = imagecreatefrompng($path);
if($gd) {
imagesavealpha($gd, true); // save alphablending setting (important)
}
}
break;
}
// image failed
if($gd === false) {
$this->markFailed($path, $mtime);
return;
}
// Save
$this->setImageResource($gd);
}
public function loadFromContainer(AssetContainer $assetContainer) {
// If we're working with image resampling, things could take a while. Bump up the time-limit
increase_time_limit_to(300);
$this->resetResource();
// Skip non-existant files
if(!$assetContainer->exists()) {
return;
}
// Skip if failed before
$filename = $assetContainer->getFilename();
$hash = $assetContainer->getHash();
$variant = $assetContainer->getVariant();
if($this->failedResample($filename, $hash, $variant)) {
return;
}
// Mark as potentially failed prior to creation, resetting this on success
$content = $assetContainer->getString();
$image = imagecreatefromstring($content);
if($image === false) {
$this->markFailed($filename, $hash, $variant);
return;
}
imagealphablending($image, false);
imagesavealpha($image, true); // save alphablending setting (important)
$this->setImageResource($image);
}
/**
* Clear GD resource
*/
protected function resetResource(){
// Set defaults and clear resource
$this->setImageResource(null);
$this->quality = $this->config()->default_quality;
$this->interlace = $this->config()->image_interlace;
}
/**
* Assign or clear GD resource
*
* @param resource|null $resource
*/
public function setImageResource($resource) {
$this->gd = $resource;
$this->width = $resource ? imagesx($resource) : 0;
$this->height = $resource ? imagesy($resource) : 0;
}
/**
* Get the currently assigned GD resource
*
* @return resource
*/
public function getImageResource() {
return $this->gd;
}
/**
* Check if this image has previously crashed GD when attempting to open it - if it's opened
* successfully, the manipulation's cache key is removed.
*
* @param string $args,... Any number of args that identify this image
* @return bool True if failed
*/
public function failedResample() {
$key = sha1(implode('|', func_get_args()));
return (bool)$this->cache->load($key);
}
/**
* Mark a file as failed
*
* @param string $args,... Any number of args that identify this image
*/
protected function markFailed() {
$key = sha1(implode('|', func_get_args()));
$this->cache->save('1', $key);
}
/**
* Mark a file as succeeded
*
* @param string $args,... Any number of args that identify this image
*/
protected function markSucceeded() {
$key = sha1(implode('|', func_get_args()));
$this->cache->save('0', $key);
}
public function setQuality($quality) {
$this->quality = $quality;
}
public function croppedResize($width, $height) {
if(!$this->gd) {
return null;
}
$width = round($width);
$height = round($height);
// Check that a resize is actually necessary.
if ($width == $this->width && $height == $this->height) {
return $this;
}
$newGD = imagecreatetruecolor($width, $height);
// Preserves transparency between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
$destAR = $width / $height;
if ($this->width > 0 && $this->height > 0 ){
// We can't divide by zero theres something wrong.
$srcAR = $this->width / $this->height;
// Destination narrower than the source
if($destAR < $srcAR) {
$srcY = 0;
$srcHeight = $this->height;
$srcWidth = round( $this->height * $destAR );
$srcX = round( ($this->width - $srcWidth) / 2 );
// Destination shorter than the source
} else {
$srcX = 0;
$srcWidth = $this->width;
$srcHeight = round( $this->width / $destAR );
$srcY = round( ($this->height - $srcHeight) / 2 );
}
imagecopyresampled($newGD, $this->gd, 0,0, $srcX, $srcY, $width, $height, $srcWidth, $srcHeight);
}
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Resizes the image to fit within the given region.
* Behaves similarly to paddedResize but without the padding.
* @todo This method isn't very efficent
*/
public function fittedResize($width, $height) {
$gd = $this->resizeByHeight($height);
if($gd->width > $width) {
$gd = $gd->resizeByWidth($width);
}
return $gd;
}
public function resize($width, $height) {
if(!$this->gd) {
return null;
}
if($width < 0 || $height < 0) {
throw new InvalidArgumentException("Image resizing dimensions cannot be negative");
}
if(!$width && !$height) {
throw new InvalidArgumentException("No dimensions given when resizing image");
}
if(!$width) {
throw new InvalidArgumentException("Width not given when resizing image");
}
if(!$height) {
throw new InvalidArgumentException("Height not given when resizing image");
}
//use whole numbers, ensuring that size is at least 1x1
$width = max(1, round($width));
$height = max(1, round($height));
// Check that a resize is actually necessary.
if ($width == $this->width && $height == $this->height) {
return $this;
}
$newGD = imagecreatetruecolor($width, $height);
// Preserves transparency between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
imagecopyresampled($newGD, $this->gd, 0,0, 0, 0, $width, $height, $this->width, $this->height);
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Rotates image by given angle.
*
* @param float angle Angle in degrees
* @return static
*/
public function rotate($angle) {
if(!$this->gd) {
return null;
}
if(function_exists("imagerotate")) {
$newGD = imagerotate($this->gd, $angle, 0);
} else {
//imagerotate is not included in PHP included in Ubuntu
$newGD = $this->rotatePixelByPixel($angle);
}
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Rotates image by given angle. It's slow because makes it pixel by pixel rather than
* using built-in function. Used when imagerotate function is not available(i.e. Ubuntu)
*
* @param float $angle Angle in degrees
* @return static
*/
public function rotatePixelByPixel($angle) {
if(!$this->gd) {
return null;
}
$sourceWidth = imagesx($this->gd);
$sourceHeight = imagesy($this->gd);
if ($angle == 180) {
$destWidth = $sourceWidth;
$destHeight = $sourceHeight;
} else {
$destWidth = $sourceHeight;
$destHeight = $sourceWidth;
}
$rotate=imagecreatetruecolor($destWidth,$destHeight);
imagealphablending($rotate, false);
for ($x = 0; $x < ($sourceWidth); $x++) {
for ($y = 0; $y < ($sourceHeight); $y++) {
$color = imagecolorat($this->gd, $x, $y);
switch ($angle) {
case 90:
imagesetpixel($rotate, $y, $destHeight - $x - 1, $color);
break;
case 180:
imagesetpixel($rotate, $destWidth - $x - 1, $destHeight - $y - 1, $color);
break;
case 270:
imagesetpixel($rotate, $destWidth - $y - 1, $x, $color);
break;
default: $rotate = $this->gd;
};
}
}
return $rotate;
}
/**
* Crop's part of image.
*
* @param int $top y position of left upper corner of crop rectangle
* @param int $left x position of left upper corner of crop rectangle
* @param int $width rectangle width
* @param int $height rectangle height
* @return static
*/
public function crop($top, $left, $width, $height) {
if(!$this->gd) {
return null;
}
$newGD = imagecreatetruecolor($width, $height);
// Preserve alpha channel between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
imagecopyresampled($newGD, $this->gd, 0, 0, $left, $top, $width, $height, $width, $height);
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Width of image.
*
* @return int
*/
public function getWidth() {
return $this->width;
}
/**
* Height of image.
*
* @return int
*/
public function getHeight() {
return $this->height;
}
public function resizeByWidth($width) {
$heightScale = $width / $this->width;
return $this->resize($width, $heightScale * $this->height );
}
public function resizeByHeight($height) {
$scale = $height / $this->height;
return $this->resize($scale * $this->width, $height );
}
public function resizeRatio($maxWidth, $maxHeight, $useAsMinimum = false) {
$widthRatio = $maxWidth / $this->width;
$heightRatio = $maxHeight / $this->height;
if( $widthRatio < $heightRatio ) {
return $useAsMinimum
? $this->resizeByHeight( $maxHeight )
: $this->resizeByWidth( $maxWidth );
} else {
return $useAsMinimum
? $this->resizeByWidth( $maxWidth )
: $this->resizeByHeight( $maxHeight );
}
}
public function paddedResize($width, $height, $backgroundColor = "FFFFFF") {
if(!$this->gd) {
return null;
}
$width = round($width);
$height = round($height);
// Check that a resize is actually necessary.
if ($width == $this->width && $height == $this->height) {
return $this;
}
$newGD = imagecreatetruecolor($width, $height);
// Preserves transparency between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
$bg = $this->colourWeb2GD($newGD, $backgroundColor);
imagefilledrectangle($newGD, 0, 0, $width, $height, $bg);
$destAR = $width / $height;
if ($this->width > 0 && $this->height > 0) {
// We can't divide by zero theres something wrong.
$srcAR = $this->width / $this->height;
// Destination narrower than the source
if($destAR > $srcAR) {
$destY = 0;
$destHeight = $height;
$destWidth = round( $height * $srcAR );
$destX = round( ($width - $destWidth) / 2 );
// Destination shorter than the source
} else {
$destX = 0;
$destWidth = $width;
$destHeight = round( $width / $srcAR );
$destY = round( ($height - $destHeight) / 2 );
}
imagecopyresampled($newGD, $this->gd,
$destX, $destY, 0, 0,
$destWidth, $destHeight, $this->width, $this->height);
}
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Make the image greyscale
* $rv = red value, defaults to 38
* $gv = green value, defaults to 36
* $bv = blue value, defaults to 26
* Based (more or less entirely, with changes for readability) on code from
* http://www.teckis.com/scriptix/thumbnails/teck.html
*/
public function greyscale($rv=38, $gv=36, $bv=26) {
if(!$this->gd) {
return null;
}
$width = $this->width;
$height = $this->height;
$newGD = imagecreatetruecolor($this->width, $this->height);
// Preserves transparency between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
$rt = $rv + $bv + $gv;
$rr = ($rv == 0) ? 0 : 1/($rt/$rv);
$br = ($bv == 0) ? 0 : 1/($rt/$bv);
$gr = ($gv == 0) ? 0 : 1/($rt/$gv);
for($dy = 0; $dy < $height; $dy++) {
for($dx = 0; $dx < $width; $dx++) {
$pxrgb = imagecolorat($this->gd, $dx, $dy);
$heightgb = ImageColorsforIndex($this->gd, $pxrgb);
$newcol = ($rr*$heightgb['red']) + ($br*$heightgb['blue']) + ($gr*$heightgb['green']);
$setcol = ImageColorAllocateAlpha($newGD, $newcol, $newcol, $newcol, $heightgb['alpha']);
imagesetpixel($newGD, $dx, $dy, $setcol);
}
}
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
public function writeToStore(AssetStore $assetStore, $filename, $hash = null, $variant = null, $conflictResolution = null) {
// Write to temporary file, taking care to maintain the extension
$path = tempnam(sys_get_temp_dir(), 'gd');
if($extension = pathinfo($filename, PATHINFO_EXTENSION)) {
$path .= "." . $extension;
}
$this->writeTo($path);
$result = $assetStore->setFromLocalFile($path, $filename, $hash, $variant, $conflictResolution);
unlink($path);
return $result;
}
public function writeTo($filename) {
if(!$filename) {
return;
}
// Get current image data
if(file_exists($filename)) {
list($width, $height, $type, $attr) = getimagesize($filename);
unlink($filename);
} else {
Filesystem::makeFolder(dirname($filename));
}
// If image type isn't known, guess from extension
$ext = strtolower(substr($filename, strrpos($filename,'.')+1));
if(empty($type)) {
switch($ext) {
case "gif":
$type = IMAGETYPE_GIF;
break;
case "jpeg":
case "jpg":
case "jpe":
$type = IMAGETYPE_JPEG;
break;
default:
$type = IMAGETYPE_PNG;
break;
}
}
// if $this->interlace != 0, the output image will be interlaced
imageinterlace ($this->gd, $this->interlace);
// if the extension does not exist, the file will not be created!
switch($type) {
case IMAGETYPE_GIF:
imagegif($this->gd, $filename);
break;
case IMAGETYPE_JPEG:
imagejpeg($this->gd, $filename, $this->quality);
break;
// case 3, and everything else
default:
// Save them as 8-bit images
// imagetruecolortopalette($this->gd, false, 256);
imagepng($this->gd, $filename);
break;
}
if(file_exists($filename)) {
@chmod($filename, 0664);
}
}
/**
* Helper function to allocate a colour to an image
*
* @param resource $image
* @param string $webColor
* @return int
*/
protected function colourWeb2GD($image, $webColor) {
if(substr($webColor,0,1) == "#") {
$webColor = substr($webColor, 1);
}
$r = hexdec(substr($webColor,0,2));
$g = hexdec(substr($webColor,2,2));
$b = hexdec(substr($webColor,4,2));
return imagecolorallocate($image, $r, $g, $b);
}
public static function flush() {
// Clear factory
$cache = SS_Cache::factory('GDBackend_Manipulations');
$cache->clean(Zend_Cache::CLEANING_MODE_ALL);
}
}

View File

@ -0,0 +1,767 @@
<?php
namespace SilverStripe\Filesystem;
use Config;
use Convert;
use DBField;
use DBFile;
use HTMLText;
use Image_Backend;
use Injector;
use InvalidArgumentException;
use SilverStripe\Filesystem\Storage\AssetContainer;
use SilverStripe\Filesystem\Storage\AssetStore;
/**
* Provides image manipulation functionality.
* Provides limited thumbnail generation functionality for non-image files.
* Should only be applied to implementors of AssetContainer
*
* Allows raw images to be resampled via Resampled()
*
* Image scaling manipluations, including:
* - Fit()
* - FitMax()
* - ScaleWidth()
* - ScaleMaxWidth()
* - ScaleHeight()
* - ScaleMaxHeight()
* - ResizedImage()
*
* Image cropping manipulations, including:
* - CropHeight()
* - CropWidth()
* - Fill()
* - FillMax()
*
* Thumbnail generation methods including:
* - Icon()
* - CMSThumbnail()
*
* @mixin AssetContainer
*/
trait ImageManipulation {
/**
* @return string Data from the file in this container
*/
abstract public function getString();
/**
* @return resource Data stream to the asset in this container
*/
abstract public function getStream();
/**
* @return string public url to the asset in this container
*/
abstract public function getURL();
/**
* @return string The absolute URL to the asset in this container
*/
abstract public function getAbsoluteURL();
/**
* Get metadata for this file
*
* @return array|null File information
*/
abstract public function getMetaData();
/**
* Get mime type
*
* @return string Mime type for this file
*/
abstract public function getMimeType();
/**
* Return file size in bytes.
*
* @return int
*/
abstract public function getAbsoluteSize();
/**
* Determine if this container has a valid value
*
* @return bool Flag as to whether the file exists
*/
abstract public function exists();
/**
* Get value of filename
*
* @return string
*/
abstract public function getFilename();
/**
* Get value of hash
*
* @return string
*/
abstract public function getHash();
/**
* Get value of variant
*
* @return string
*/
abstract public function getVariant();
/**
* Determine if a valid non-empty image exists behind this asset
*
* @return bool
*/
abstract public function getIsImage();
/**
* @config
* @var bool Force all images to resample in all cases
*/
private static $force_resample = true;
/**
* @config
* @var int The width of an image thumbnail in a strip.
*/
private static $strip_thumbnail_width = 50;
/**
* @config
* @var int The height of an image thumbnail in a strip.
*/
private static $strip_thumbnail_height = 50;
/**
* The width of an image thumbnail in the CMS.
*
* @config
* @var int
*/
private static $cms_thumbnail_width = 100;
/**
* The height of an image thumbnail in the CMS.
*
* @config
* @var int
*/
private static $cms_thumbnail_height = 100;
/**
* The width of an image preview in the Asset section
*
* This thumbnail is only sized to width.
*
* @config
* @var int
*/
private static $asset_preview_width = 400;
/**
* Fit image to specified dimensions and fill leftover space with a solid colour (default white). Use in templates with $Pad.
*
* @param integer $width The width to size to
* @param integer $height The height to size to
* @return AssetContainer
*/
public function Pad($width, $height, $backgroundColor = 'FFFFFF') {
if($this->isSize($width, $height)) {
return $this;
}
$variant = $this->variantName(__FUNCTION__, $width, $height, $backgroundColor);
return $this->manipulateImage(
$variant,
function(Image_Backend $backend) use($width, $height, $backgroundColor) {
return $backend->paddedResize($width, $height, $backgroundColor);
}
);
}
/**
* Forces the image to be resampled, if possible
*
* @return AssetContainer
*/
public function Resampled() {
// If image is already resampled, return self reference
$variant = $this->getVariant();
if($variant) {
return $this;
}
// Resample, but fallback to original object
$result = $this->manipulateImage(__FUNCTION__, function(Image_Backend $backend) {
return $backend;
});
if($result) {
return $result;
}
return $this;
}
/**
* Update the url to point to a resampled version if forcing
*
* @param string $url
*/
public function updateURL(&$url) {
// Skip if resampling is off, or is already resampled, or is not an image
if(!Config::inst()->get(get_class($this), 'force_resample') || $this->getVariant() || !$this->getIsImage()) {
return;
}
// Attempt to resample
$resampled = $this->Resampled();
if(!$resampled) {
return;
}
// Only update if resampled file is a smaller file size
if($resampled->getAbsoluteSize() < $this->getAbsoluteSize()) {
$url = $resampled->getURL();
}
}
/**
* Generate a resized copy of this image with the given width & height.
* This can be used in templates with $ResizedImage but should be avoided,
* as it's the only image manipulation function which can skew an image.
*
* @param integer $width Width to resize to
* @param integer $height Height to resize to
* @return AssetContainer
*/
public function ResizedImage($width, $height) {
if($this->isSize($width, $height)) {
return $this;
}
$variant = $this->variantName(__FUNCTION__, $width, $height);
return $this->manipulateImage($variant, function(Image_Backend $backend) use ($width, $height) {
return $backend->resize($width, $height);
});
}
/**
* Scale image proportionally to fit within the specified bounds
*
* @param integer $width The width to size within
* @param integer $height The height to size within
* @return AssetContainer
*/
public function Fit($width, $height) {
// Prevent divide by zero on missing/blank file
if(!$this->getWidth() || !$this->getHeight()) {
return null;
}
// Check if image is already sized to the correct dimension
$widthRatio = $width / $this->getWidth();
$heightRatio = $height / $this->getHeight();
if( $widthRatio < $heightRatio ) {
// Target is higher aspect ratio than image, so check width
if($this->isWidth($width)) {
return $this;
}
} else {
// Target is wider or same aspect ratio as image, so check height
if($this->isHeight($height)) {
return $this;
}
}
// Item must be regenerated
$variant = $this->variantName(__FUNCTION__, $width, $height);
return $this->manipulateImage($variant, function(Image_Backend $backend) use ($width, $height) {
return $backend->resizeRatio($width, $height);
});
}
/**
* Proportionally scale down this image if it is wider or taller than the specified dimensions.
* Similar to Fit but without up-sampling. Use in templates with $FitMax.
*
* @uses ScalingManipulation::Fit()
* @param integer $width The maximum width of the output image
* @param integer $height The maximum height of the output image
* @return AssetContainer
*/
public function FitMax($width, $height) {
return $this->getWidth() > $width || $this->getHeight() > $height
? $this->Fit($width,$height)
: $this;
}
/**
* Scale image proportionally by width. Use in templates with $ScaleWidth.
*
* @param integer $width The width to set
* @return AssetContainer
*/
public function ScaleWidth($width) {
if($this->isWidth($width)) {
return $this;
}
$variant = $this->variantName(__FUNCTION__, $width);
return $this->manipulateImage($variant, function(Image_Backend $backend) use ($width) {
return $backend->resizeByWidth($width);
});
}
/**
* Proportionally scale down this image if it is wider than the specified width.
* Similar to ScaleWidth but without up-sampling. Use in templates with $ScaleMaxWidth.
*
* @uses ScalingManipulation::ScaleWidth()
* @param integer $width The maximum width of the output image
* @return AssetContainer
*/
public function ScaleMaxWidth($width) {
return $this->getWidth() > $width
? $this->ScaleWidth($width)
: $this;
}
/**
* Scale image proportionally by height. Use in templates with $ScaleHeight.
*
* @param int $height The height to set
* @return AssetContainer
*/
public function ScaleHeight($height) {
if($this->isHeight($height)) {
return $this;
}
$variant = $this->variantName(__FUNCTION__, $height);
return $this->manipulateImage($variant, function(Image_Backend $backend) use ($height) {
return $backend->resizeByHeight($height);
});
}
/**
* Proportionally scale down this image if it is taller than the specified height.
* Similar to ScaleHeight but without up-sampling. Use in templates with $ScaleMaxHeight.
*
* @uses ScalingManipulation::ScaleHeight()
* @param integer $height The maximum height of the output image
* @return AssetContainer
*/
public function ScaleMaxHeight($height) {
return $this->getHeight() > $height
? $this->ScaleHeight($height)
: $this;
}
/**
* Crop image on X axis if it exceeds specified width. Retain height.
* Use in templates with $CropWidth. Example: $Image.ScaleHeight(100).$CropWidth(100)
*
* @uses CropManipulation::Fill()
* @param integer $width The maximum width of the output image
* @return AssetContainer
*/
public function CropWidth($width) {
return $this->getWidth() > $width
? $this->Fill($width, $this->getHeight())
: $this;
}
/**
* Crop image on Y axis if it exceeds specified height. Retain width.
* Use in templates with $CropHeight. Example: $Image.ScaleWidth(100).CropHeight(100)
*
* @uses CropManipulation::Fill()
* @param integer $height The maximum height of the output image
* @return AssetContainer
*/
public function CropHeight($height) {
return $this->getHeight() > $height
? $this->Fill($this->getWidth(), $height)
: $this;
}
/**
* Crop this image to the aspect ratio defined by the specified width and height,
* then scale down the image to those dimensions if it exceeds them.
* Similar to Fill but without up-sampling. Use in templates with $FillMax.
*
* @uses ImageManipulation::Fill()
* @param integer $width The relative (used to determine aspect ratio) and maximum width of the output image
* @param integer $height The relative (used to determine aspect ratio) and maximum height of the output image
* @return AssetContainer
*/
public function FillMax($width, $height) {
// Prevent divide by zero on missing/blank file
if(!$this->getWidth() || !$this->getHeight()) {
return null;
}
// Is the image already the correct size?
if ($this->isSize($width, $height)) {
return $this;
}
// If not, make sure the image isn't upsampled
$imageRatio = $this->getWidth() / $this->getHeight();
$cropRatio = $width / $height;
// If cropping on the x axis compare heights
if ($cropRatio < $imageRatio && $this->getHeight() < $height) {
return $this->Fill($this->getHeight() * $cropRatio, $this->getHeight());
}
// Otherwise we're cropping on the y axis (or not cropping at all) so compare widths
if ($this->getWidth() < $width) {
return $this->Fill($this->getWidth(), $this->getWidth() / $cropRatio);
}
return $this->Fill($width, $height);
}
/**
* Resize and crop image to fill specified dimensions.
* Use in templates with $Fill
*
* @param integer $width Width to crop to
* @param integer $height Height to crop to
* @return AssetContainer
*/
public function Fill($width, $height) {
if($this->isSize($width, $height)) {
return $this;
}
// Resize
$variant = $this->variantName(__FUNCTION__, $width, $height);
return $this->manipulateImage($variant, function(Image_Backend $backend) use ($width, $height) {
return $backend->croppedResize($width, $height);
});
}
/**
* Default CMS thumbnail
*
* @return DBFile|HTMLText Either a resized thumbnail, or html for a thumbnail icon
*/
public function CMSThumbnail() {
$width = Config::inst()->get(get_class($this), 'cms_thumbnail_width');
$height = Config::inst()->get(get_class($this), 'cms_thumbnail_height');
return $this->ThumbnailIcon($width, $height);
}
/**
* Generates a thumbnail for use in the gridfield view
*
* @return AssetContainer|HTMLText Either a resized thumbnail, or html for a thumbnail icon
*/
public function StripThumbnail() {
$width = Config::inst()->get(get_class($this), 'strip_thumbnail_width');
$height = Config::inst()->get(get_class($this), 'strip_thumbnail_height');
return $this->ThumbnailIcon($width, $height);
}
/**
* Get preview for this file
*
* @return AssetContainer|HTMLText Either a resized thumbnail, or html for a thumbnail icon
*/
public function PreviewThumbnail() {
$width = Config::inst()->get(get_class($this), 'asset_preview_width');
return $this->ScaleWidth($width) ?: $this->IconTag();
}
/**
* Default thumbnail generation for Images
*
* @param int $width
* @param int $height
* @return AssetContainer
*/
public function Thumbnail($width, $height) {
return $this->Pad($height, $height);
}
/**
* Thubnail generation for all file types.
*
* Resizes images, but returns an icon <img /> tag if this is not a resizable image
*
* @param int $width
* @param int $height
* @return AssetContainer|HTMLText
*/
public function ThumbnailIcon($width, $height) {
return $this->Thumbnail($width, $height) ?: $this->IconTag();
}
/**
* Get HTML for img containing the icon for this file
*
* @return type
*/
public function IconTag() {
return DBField::create_field(
'HTMLText',
'<img src="' . Convert::raw2att($this->getIcon()) . '" />'
);
}
/**
* Get URL to thumbnail of the given size.
*
* May fallback to default icon
*
* @param int $width
* @param int $height
* @return string
*/
public function ThumbnailURL($width, $height) {
$thumbnail = $this->Thumbnail($width, $height);
if($thumbnail) {
return $thumbnail->getURL();
}
return $this->getIcon();
}
/**
* Return the relative URL of an icon for the file type,
* based on the {@link appCategory()} value.
* Images are searched for in "framework/images/app_icons/".
*
* @return string URL to icon
*/
public function getIcon() {
$filename = $this->getFilename();
$ext = pathinfo($filename, PATHINFO_EXTENSION);
return \File::get_icon_for_extension($ext);
}
/**
* Get Image_Backend instance for this image
*
* @return Image_Backend
*/
public function getImageBackend() {
if(!$this->getIsImage()) {
return null;
}
// Create backend for this object
return Injector::inst()->createWithArgs('Image_Backend', array($this));
}
/**
* Get the dimensions of this Image.
*
* @param string $dim One of the following:
* - "string": return the dimensions in string form
* - "array": it'll return the raw result
* - 0: return the height
* - 1: return the width
* @return string|int|array|null
*/
public function getDimensions($dim = "string") {
if(!$this->getIsImage()) {
return null;
}
$content = $this->getString();
if(!$content) {
return null;
}
// Get raw content
$size = getimagesizefromstring($content);
if($size === false) {
return null;
}
if($dim === 'array') {
return $size;
}
// Get single dimension
if(is_numeric($dim)) {
return $size[$dim];
}
return "$size[0]x$size[1]";
}
/**
* Get the width of this image.
*
* @return int
*/
public function getWidth() {
return $this->getDimensions(0);
}
/**
* Get the height of this image.
*
* @return int
*/
public function getHeight() {
return $this->getDimensions(1);
}
/**
* Get the orientation of this image.
*
* @return ORIENTATION_SQUARE | ORIENTATION_PORTRAIT | ORIENTATION_LANDSCAPE
*/
public function getOrientation() {
$width = $this->getWidth();
$height = $this->getHeight();
if($width > $height) {
return Image_Backend::ORIENTATION_LANDSCAPE;
} elseif($height > $width) {
return Image_Backend::ORIENTATION_PORTRAIT;
} else {
return Image_Backend::ORIENTATION_SQUARE;
}
}
/**
* Determine if this image is of the specified size
*
* @param integer $width Width to check
* @param integer $height Height to check
* @return boolean
*/
public function isSize($width, $height) {
return $this->isWidth($width) && $this->isHeight($height);
}
/**
* Determine if this image is of the specified width
*
* @param integer $width Width to check
* @return boolean
*/
public function isWidth($width) {
if(empty($width) || !is_numeric($width)) {
throw new InvalidArgumentException("Invalid value for width");
}
return $this->getWidth() == $width;
}
/**
* Determine if this image is of the specified width
*
* @param integer $height Height to check
* @return boolean
*/
public function isHeight($height) {
if(empty($height) || !is_numeric($height)) {
throw new InvalidArgumentException("Invalid value for height");
}
return $this->getHeight() == $height;
}
/**
* Wrapper for manipulate that passes in and stores Image_Backend objects instead of tuples
*
* @param string $variant
* @param callable $callback Callback which takes an Image_Backend object, and returns an Image_Backend result
* @return DBFile The manipulated file
*/
public function manipulateImage($variant, $callback) {
return $this->manipulate(
$variant,
function(AssetStore $store, $filename, $hash, $variant) use ($callback) {
$backend = $this->getImageBackend();
if(!$backend) {
return null;
}
$backend = $callback($backend);
if(!$backend) {
return null;
}
return $backend->writeToStore($store, $filename, $hash, $variant, AssetStore::CONFLICT_USE_EXISTING);
}
);
}
/**
* Generate a new DBFile instance using the given callback if it hasn't been created yet, or
* return the existing one if it has.
*
* @param string $variant name of the variant to create
* @param callable $callback Callback which should return a new tuple as an array.
* This callback will be passed the backend, filename, hash, and variant
* This will not be called if the file does not
* need to be created.
* @return DBFile The manipulated file
*/
public function manipulate($variant, $callback) {
// Verify this manipulation is applicable to this instance
if(!$this->exists()) {
return null;
}
// Build output tuple
$filename = $this->getFilename();
$hash = $this->getHash();
$existingVariant = $this->getVariant();
if($existingVariant) {
$variant = $existingVariant . '_' . $variant;
}
// Skip empty files (e.g. Folder does not have a hash)
if(empty($filename) || empty($hash)) {
return null;
}
// Create this asset in the store if it doesn't already exist,
// otherwise use the existing variant
$store = Injector::inst()->get('AssetStore');
$result = null;
if(!$store->exists($filename, $hash, $variant)) {
$result = call_user_func($callback, $store, $filename, $hash, $variant);
} else {
$result = array(
'Filename' => $filename,
'Hash' => $hash,
'Variant' => $variant
);
}
// Callback may fail to perform this manipulation (e.g. resize on text file)
if(!$result) {
return null;
}
// Store result in new DBFile instance
return DBField::create_field('DBFile', $result)
->setOriginal($this);
}
/**
* Name a variant based on a format with arbitrary parameters
*
* @param string $format The format name.
* @param mixed ...$args Additional arguments
* @return string
* @throws InvalidArgumentException
*/
public function variantName($format) {
$args = func_get_args();
array_shift($args);
return $format . Convert::base64url_encode($args);
}
}

View File

@ -1,11 +1,17 @@
<?php
use SilverStripe\Filesystem\Storage\AssetContainer;
use SilverStripe\Filesystem\Storage\AssetStore;
/**
* @package framework
* @subpackage filesystem
*/
if(class_exists('Imagick')) {
if(!class_exists('Imagick')) {
return;
}
class ImagickBackend extends Imagick implements Image_Backend {
/**
@ -15,111 +21,74 @@ class ImagickBackend extends Imagick implements Image_Backend {
private static $default_quality = 75;
/**
* __construct
* Create a new backend with the given object
*
* @param string $filename = null
* @param array $args = array()
* @return void
* @param AssetContainer $assetContainer Object to load from
*/
public function __construct($filename = null, $args = array()) {
if(is_string($filename)) {
parent::__construct($filename);
public function __construct(AssetContainer $assetContainer = null) {
parent::__construct();
if($assetContainer) {
$this->loadFromContainer($assetContainer);
}
$this->setQuality(Config::inst()->get('ImagickBackend','default_quality'));
}
/**
* writeTo
*
* @param string $path
* @return void
*/
public function loadFromContainer(AssetContainer $assetContainer) {
$stream = $assetContainer->getStream();
$this->readimagefile($stream);
fclose($stream);
$this->setDefaultQuality();
}
public function loadFrom($path) {
$this->readimage($path);
$this->setDefaultQuality();
}
protected function setDefaultQuality() {
$this->setQuality(Config::inst()->get('ImagickBackend', 'default_quality'));
}
public function writeToStore(AssetStore $assetStore, $filename, $hash = null, $variant = null, $conflictResolution = null) {
// Write to temporary file, taking care to maintain the extension
$path = tempnam(sys_get_temp_dir(), 'imagemagick');
if($extension = pathinfo($filename, PATHINFO_EXTENSION)) {
$path .= "." . $extension;
}
$this->writeimage($path);
$result = $assetStore->setFromLocalFile($path, $filename, $hash, $variant, $conflictResolution);
unlink($path);
return $result;
}
public function writeTo($path) {
Filesystem::makeFolder(dirname($path));
if(is_dir(dirname($path)))
self::writeImage($path);
}
/**
* set_default_quality
*
* @deprecated 4.0 Use the "ImagickBackend.default_quality" config setting instead
* @param int $quality
* @return void
*/
public static function set_default_quality($quality) {
Deprecation::notice('4.0', 'Use the "ImagickBackend.default_quality" config setting instead');
if(is_numeric($quality) && (int) $quality >= 0 && (int) $quality <= 100) {
Config::inst()->update('ImagickBackend', 'default_quality', (int) $quality);
if(is_dir(dirname($path))) {
$this->writeImage($path);
}
}
/**
* setQuality
*
* @param int $quality
* @return void
*/
public function setQuality($quality) {
self::setImageCompressionQuality($quality);
$this->setImageCompressionQuality($quality);
}
/**
* setImageResource
*
* Set the backend-specific resource handling the manipulations. Replaces Image::setGD()
*
* @param mixed $resource
* @return void
*/
public function setImageResource($resource) {
trigger_error("Imagick::setImageResource is not supported", E_USER_ERROR);
}
/**
* getImageResource
*
* Get the backend-specific resource handling the manipulations. Replaces Image::getGD()
*
* @return mixed
*/
public function getImageResource() {
return $this;
}
/**
* hasImageResource
*
* @return boolean
*/
public function hasImageResource() {
return true; // $this is the resource, necessarily
}
/**
* @todo Implement memory checking for Imagick? See {@link GD}
*
* @param string $filename
* @return boolean
*/
public function imageAvailable($filename) {
return true;
}
/**
* resize
*
* @param int $width
* @param int $height
* @return Image_Backend
*/
public function resize($width, $height) {
if(!$this->valid()) return;
if(!$this->valid()) {
return null;
}
if($width < 0 || $height < 0) throw new InvalidArgumentException("Image resizing dimensions cannot be negative");
if(!$width && !$height) throw new InvalidArgumentException("No dimensions given when resizing image");
if(!$width) throw new InvalidArgumentException("Width not given when resizing image");
if(!$height) throw new InvalidArgumentException("Height not given when resizing image");
if($width < 0 || $height < 0) {
throw new InvalidArgumentException("Image resizing dimensions cannot be negative");
}
if(!$width && !$height) {
throw new InvalidArgumentException("No dimensions given when resizing image");
}
if(!$width) {
throw new InvalidArgumentException("Width not given when resizing image");
}
if(!$height) {
throw new InvalidArgumentException("Height not given when resizing image");
}
//use whole numbers, ensuring that size is at least 1x1
$width = max(1, round($width));
@ -128,7 +97,7 @@ class ImagickBackend extends Imagick implements Image_Backend {
$geometry = $this->getImageGeometry();
// Check that a resize is actually necessary.
if ($width == $geometry["width"] && $height == $geometry["height"]) {
if ($width === $geometry["width"] && $height === $geometry["height"]) {
return $this;
}
@ -138,35 +107,31 @@ class ImagickBackend extends Imagick implements Image_Backend {
return $new;
}
/**
* resizeRatio
*
* @param int $width
* @param int $height
* @return Image_Backend
*/
public function resizeRatio($maxWidth, $maxHeight, $useAsMinimum = false) {
if(!$this->valid()) return;
if(!$this->valid()) {
return null;
}
$geometry = $this->getImageGeometry();
$widthRatio = $maxWidth / $geometry["width"];
$heightRatio = $maxHeight / $geometry["height"];
if( $widthRatio < $heightRatio )
return $useAsMinimum ? $this->resizeByHeight( $maxHeight ) : $this->resizeByWidth( $maxWidth );
else
return $useAsMinimum ? $this->resizeByWidth( $maxWidth ) : $this->resizeByHeight( $maxHeight );
if( $widthRatio < $heightRatio ) {
return $useAsMinimum
? $this->resizeByHeight( $maxHeight )
: $this->resizeByWidth( $maxWidth );
} else {
return $useAsMinimum
? $this->resizeByWidth( $maxWidth )
: $this->resizeByHeight( $maxHeight );
}
}
/**
* resizeByWidth
*
* @param int $width
* @return Image_Backend
*/
public function resizeByWidth($width) {
if(!$this->valid()) return;
if(!$this->valid()) {
return null;
}
$geometry = $this->getImageGeometry();
@ -174,14 +139,10 @@ class ImagickBackend extends Imagick implements Image_Backend {
return $this->resize( $width, $heightScale * $geometry["height"] );
}
/**
* resizeByHeight
*
* @param int $height
* @return Image_Backend
*/
public function resizeByHeight($height) {
if(!$this->valid()) return;
if(!$this->valid()) {
return null;
}
$geometry = $this->getImageGeometry();
@ -189,32 +150,23 @@ class ImagickBackend extends Imagick implements Image_Backend {
return $this->resize( $scale * $geometry["width"], $height );
}
/**
* paddedResize
*
* @param int $width
* @param int $height
* @return Image_Backend
*/
public function paddedResize($width, $height, $backgroundColor = "FFFFFF") {
if(!$this->valid()) {
return null;
}
$new = $this->resizeRatio($width, $height);
$new->setImageBackgroundColor("#".$backgroundColor);
$w = $new->getImageWidth();
$h = $new->getImageHeight();
$new->extentImage($width,$height,($w-$width)/2,($h-$height)/2);
return $new;
}
/**
* croppedResize
*
* @param int $width
* @param int $height
* @return Image_Backend
*/
public function croppedResize($width, $height) {
if(!$this->valid()) return;
if(!$this->valid()) {
return null;
}
$width = round($width);
$height = round($height);
@ -229,22 +181,21 @@ class ImagickBackend extends Imagick implements Image_Backend {
$new->setBackgroundColor(new ImagickPixel('transparent'));
if(($geo['width']/$width) < ($geo['height']/$height)){
$new->cropImage($geo['width'], floor($height*$geo['width']/$width),
0, (($geo['height']-($height*$geo['width']/$width))/2));
$new->cropImage(
$geo['width'],
floor($height*$geo['width']/$width),
0,
($geo['height'] - ($height*$geo['width']/$width))/2
);
}else{
$new->cropImage(ceil($width*$geo['height']/$height), $geo['height'],
(($geo['width']-($width*$geo['height']/$height))/2), 0);
$new->cropImage(
ceil($width*$geo['height']/$height),
$geo['height'],
($geo['width'] - ($width*$geo['height']/$height))/2,
0
);
}
$new->ThumbnailImage($width,$height,true);
return $new;
}
/**
* @param Image $frontend
* @return void
*/
public function onBeforeDelete($frontend) {
// Not in use
}
}
}

View File

@ -1,4 +1,9 @@
<?php
use SilverStripe\Filesystem\Storage\AssetContainer;
use SilverStripe\Filesystem\Storage\AssetNameGenerator;
use SilverStripe\Filesystem\Storage\AssetStore;
/**
* Manages uploads via HTML forms processed by PHP,
* uploads to Silverstripe's default upload directory,
@ -27,9 +32,9 @@ class Upload extends Controller {
);
/**
* A File object
* A dataobject (typically {@see File}) which implements {@see AssetContainer}
*
* @var File
* @var AssetContainer
*/
protected $file;
@ -108,6 +113,17 @@ class Upload extends Controller {
$this->validator = $validator;
}
/**
* Get an asset renamer for the given filename.
*
* @param string $filename Path name
* @return AssetNameGenerator
*/
protected function getNameGenerator($filename){
return Injector::inst()->createWithArgs('AssetNameGenerator', array($filename));
}
/**
* Save an file passed from a form post into this object.
* File names are filtered through {@link FileNameFilter}, see class documentation
@ -118,111 +134,103 @@ class Upload extends Controller {
* @return Boolean|string Either success or error-message.
*/
public function load($tmpFile, $folderPath = false) {
$this->clearErrors();
if(!$folderPath) $folderPath = $this->config()->uploads_folder;
if(!is_array($tmpFile)) {
user_error("Upload::load() Not passed an array. Most likely, the form hasn't got the right enctype",
E_USER_ERROR);
throw new InvalidArgumentException(
"Upload::load() Not passed an array. Most likely, the form hasn't got the right enctype"
);
}
if(!$tmpFile['size']) {
$this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.');
// Validate
$this->clearErrors();
$valid = $this->validate($tmpFile);
if(!$valid) {
return false;
}
$valid = $this->validate($tmpFile);
if(!$valid) return false;
// @TODO This puts a HUGE limitation on files especially when lots
// have been uploaded.
$base = Director::baseFolder();
$parentFolder = Folder::find_or_make($folderPath);
// Generate default filename
// Clean filename
if(!$folderPath) {
$folderPath = $this->config()->uploads_folder;
}
$nameFilter = FileNameFilter::create();
$file = $nameFilter->filter($tmpFile['name']);
$fileName = basename($file);
$filename = basename($file);
if($folderPath) {
$filename = File::join_paths($folderPath, $filename);
}
$relativeFolderPath = $parentFolder
? $parentFolder->getRelativePath()
: ASSETS_DIR . '/';
$relativeFilePath = $relativeFolderPath . $fileName;
// Validate filename
$filename = $this->resolveExistingFile($filename);
// Save file into backend
$conflictResolution = $this->replaceFile ? AssetStore::CONFLICT_OVERWRITE : AssetStore::CONFLICT_RENAME;
$this->file->setFromLocalFile($tmpFile['tmp_name'], $filename, null, null, $conflictResolution);
// Save changes to underlying record (if it's a DataObject)
if($this->file instanceof DataObject) {
$this->file->write();
}
//to allow extensions to e.g. create a version after an upload
$this->file->extend('onAfterUpload');
$this->extend('onAfterLoad', $this->file);
return true;
}
/**
* Given a file and filename, ensure that file renaming / replacing rules are satisfied
*
* If replacing, this method may replace $this->file with an existing record to overwrite.
* If renaming, a new value for $filename may be returned
*
* @param string $filename
* @return string $filename A filename safe to write to
*/
protected function resolveExistingFile($filename) {
// Create a new file record (or try to retrieve an existing one)
if(!$this->file) {
$fileClass = File::get_class_for_file_extension(pathinfo($tmpFile['name'], PATHINFO_EXTENSION));
$this->file = new $fileClass();
$fileClass = File::get_class_for_file_extension(
File::get_file_extension($filename)
);
$this->file = $fileClass::create();
}
if(!$this->file->ID && $this->replaceFile) {
$fileClass = $this->file->class;
$file = File::get()
->filter(array(
'ClassName' => $fileClass,
'Name' => $fileName,
'ParentID' => $parentFolder ? $parentFolder->ID : 0
))->First();
if($file) {
$this->file = $file;
// Skip this step if not writing File dataobjects
if(! ($this->file instanceof File) ) {
return $filename;
}
// Check there is if existing file
$existing = File::find($filename);
// If replacing (or no file exists) confirm this filename is safe
if($this->replaceFile || !$existing) {
// If replacing files, make sure to update the OwnerID
if(!$this->file->ID && $this->replaceFile && $existing) {
$this->file = $existing;
$this->file->OwnerID = Member::currentUserID();
}
// Filename won't change if replacing
return $filename;
}
// if filename already exists, version the filename (e.g. test.gif to test-v2.gif, test-v2.gif to test-v3.gif)
if(!$this->replaceFile) {
$fileSuffixArray = explode('.', $fileName);
$fileTitle = array_shift($fileSuffixArray);
$fileSuffix = !empty($fileSuffixArray)
? '.' . implode('.', $fileSuffixArray)
: null;
// make sure files retain valid extensions
$oldFilePath = $relativeFilePath;
$relativeFilePath = $relativeFolderPath . $fileTitle . $fileSuffix;
if($oldFilePath !== $relativeFilePath) {
user_error("Couldn't fix $relativeFilePath", E_USER_ERROR);
$renamer = $this->getNameGenerator($filename);
foreach($renamer as $newName) {
if(!File::find($newName)) {
return $newName;
}
while(file_exists("$base/$relativeFilePath")) {
$i = isset($i) ? ($i+1) : 2;
$oldFilePath = $relativeFilePath;
$prefix = $this->config()->version_prefix;
$pattern = '/' . preg_quote($prefix) . '([0-9]+$)/';
if(preg_match($pattern, $fileTitle, $matches)) {
$fileTitle = preg_replace($pattern, $prefix . ($matches[1] + 1), $fileTitle);
} else {
$fileTitle .= $prefix . $i;
}
$relativeFilePath = $relativeFolderPath . $fileTitle . $fileSuffix;
if($oldFilePath == $relativeFilePath && $i > 2) {
user_error("Couldn't fix $relativeFilePath with $i tries", E_USER_ERROR);
}
}
} else {
//reset the ownerID to the current member when replacing files
$this->file->OwnerID = (Member::currentUser() ? Member::currentUser()->ID : 0);
}
if(file_exists($tmpFile['tmp_name']) && copy($tmpFile['tmp_name'], "$base/$relativeFilePath")) {
$this->file->ParentID = $parentFolder ? $parentFolder->ID : 0;
// This is to prevent it from trying to rename the file
$this->file->Name = basename($relativeFilePath);
$this->file->write();
$this->file->onAfterUpload();
$this->extend('onAfterLoad', $this->file); //to allow extensions to e.g. create a version after an upload
return true;
} else {
$this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.');
return false;
}
// Fail
$tries = $renamer->getMaxTries();
throw new Exception("Could not rename {$filename} with {$tries} tries");
}
/**
* Load temporary PHP-upload into File-object.
*
* @param array $tmpFile
* @param File $file
* @param AssetContainer $file
* @return Boolean
*/
public function loadIntoFile($tmpFile, $file, $folderPath = false) {
@ -268,7 +276,7 @@ class Upload extends Controller {
* Get file-object, either generated from {load()},
* or manually set.
*
* @return File
* @return AssetContainer
*/
public function getFile() {
return $this->file;
@ -277,9 +285,9 @@ class Upload extends Controller {
/**
* Set a file-object (similiar to {loadIntoFile()})
*
* @param File $file
* @param AssetContainer $file
*/
public function setFile($file) {
public function setFile(AssetContainer $file) {
$this->file = $file;
}
@ -525,13 +533,21 @@ class Upload_Validator {
*/
public function validate() {
// we don't validate for empty upload fields yet
if(!isset($this->tmpFile['name']) || empty($this->tmpFile['name'])) return true;
if(empty($this->tmpFile['name']) || empty($this->tmpFile['tmp_name'])) {
return true;
}
$isRunningTests = (class_exists('SapphireTest', false) && SapphireTest::is_running_test());
if(isset($this->tmpFile['tmp_name']) && !is_uploaded_file($this->tmpFile['tmp_name']) && !$isRunningTests) {
$this->errors[] = _t('File.NOVALIDUPLOAD', 'File is not a valid upload');
return false;
}
// Check file isn't empty
if(empty($this->tmpFile['size']) || !filesize($this->tmpFile['tmp_name'])) {
$this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.');
return false;
}
$pathInfo = pathinfo($this->tmpFile['name']);
// filesize validation

View File

@ -14,8 +14,27 @@ use League\Flysystem\Adapter\Local;
*/
class AssetAdapter extends Local {
/**
* Config compatible permissions configuration
*
* @config
* @var array
*/
private static $file_permissions = array(
'file' => [
'public' => 0744,
'private' => 0700,
],
'dir' => [
'public' => 0755,
'private' => 0700,
]
);
public function __construct($root = null, $writeFlags = LOCK_EX, $linkHandling = self::DISALLOW_LINKS) {
parent::__construct($root ?: ASSETS_PATH, $writeFlags, $linkHandling);
// Override permissions with config
$permissions = \Config::inst()->get(get_class($this), 'file_permissions');
parent::__construct($root ?: ASSETS_PATH, $writeFlags, $linkHandling, $permissions);
}
/**

View File

@ -52,22 +52,22 @@ class FlysystemAssetStore implements AssetStore {
return $this->filesystem;
}
public function getAsStream($hash, $filename, $variant = null) {
$fileID = $this->getFileID($hash, $filename, $variant);
public function getAsStream($filename, $hash, $variant = null) {
$fileID = $this->getFileID($filename, $hash, $variant);
return $this->getFilesystem()->readStream($fileID);
}
public function getAsString($hash, $filename, $variant = null) {
$fileID = $this->getFileID($hash, $filename, $variant);
public function getAsString($filename, $hash, $variant = null) {
$fileID = $this->getFileID($filename, $hash, $variant);
return $this->getFilesystem()->read($fileID);
}
public function getAsURL($hash, $filename, $variant = null) {
$fileID = $this->getFileID($hash, $filename, $variant);
public function getAsURL($filename, $hash, $variant = null) {
$fileID = $this->getFileID($filename, $hash, $variant);
return $this->getFilesystem()->getPublicUrl($fileID);
}
public function setFromLocalFile($path, $filename = null, $conflictResolution = null) {
public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $conflictResolution = null) {
// Validate this file exists
if(!file_exists($path)) {
throw new InvalidArgumentException("$path does not exist");
@ -91,28 +91,36 @@ class FlysystemAssetStore implements AssetStore {
return $result;
};
// When saving original filename, generate hash
if(!$variant) {
$hash = sha1_file($path);
}
// Submit to conflict check
$hash = sha1_file($path);
return $this->writeWithCallback($callback, $hash, $filename, $conflictResolution);
return $this->writeWithCallback($callback, $filename, $hash, $variant, $conflictResolution);
}
public function setFromString($data, $filename, $conflictResolution = null) {
public function setFromString($data, $filename, $hash = null, $variant = null, $conflictResolution = null) {
// Callback for saving content
$filesystem = $this->getFilesystem();
$callback = function($fileID) use ($filesystem, $data) {
return $filesystem->put($fileID, $data);
};
// When saving original filename, generate hash
if(!$variant) {
$hash = sha1($data);
}
// Submit to conflict check
$hash = sha1($data);
return $this->writeWithCallback($callback, $hash, $filename, $conflictResolution);
return $this->writeWithCallback($callback, $filename, $hash, $variant, $conflictResolution);
}
public function setFromStream($stream, $filename, $conflictResolution = null) {
public function setFromStream($stream, $filename, $hash = null, $variant = null, $conflictResolution = null) {
// If the stream isn't rewindable, write to a temporary filename
if(!$this->isSeekableStream($stream)) {
$path = $this->getStreamAsFile($stream);
$result = $this->setFromLocalFile($path, $filename, $conflictResolution);
$result = $this->setFromLocalFile($path, $filename, $hash, $variant, $conflictResolution);
unlink($path);
return $result;
}
@ -123,9 +131,13 @@ class FlysystemAssetStore implements AssetStore {
return $filesystem->putStream($fileID, $stream);
};
// When saving original filename, generate hash
if(!$variant) {
$hash = $this->getStreamSHA1($stream);
}
// Submit to conflict check
$hash = $this->getStreamSHA1($stream);
return $this->writeWithCallback($callback, $hash, $filename, $conflictResolution);
return $this->writeWithCallback($callback, $filename, $hash, $variant, $conflictResolution);
}
/**
@ -180,58 +192,110 @@ class FlysystemAssetStore implements AssetStore {
* the storage request is approved.
*
* @param callable $callback Will be invoked and passed a fileID if the file should be stored
* @param string $hash SHA1 of the file content
* @param string $filename Name for the resulting file
* @param string $hash SHA1 of the original file content
* @param string $variant Variant to write
* @param string $conflictResolution {@see AssetStore}. Will default to one chosen by the backend
* @return array Tuple associative array (Filename, Hash, Variant)
* @throws Exception
*/
protected function writeWithCallback($callback, $hash, $filename, $conflictResolution = null) {
protected function writeWithCallback($callback, $filename, $hash, $variant = null, $conflictResolution = null) {
// Set default conflict resolution
if(!$conflictResolution) {
$conflictResolution = $this->getDefaultConflictResolution($variant);
}
// Validate parameters
if($variant && $conflictResolution === AssetStore::CONFLICT_RENAME) {
// As variants must follow predictable naming rules, they should not be dynamically renamed
throw new InvalidArgumentException("Rename cannot be used when writing variants");
}
if(!$filename) {
throw new InvalidArgumentException("Filename is missing");
}
if(!$hash) {
throw new InvalidArgumentException("File hash is missing");
}
$filename = $this->cleanFilename($filename);
$fileID = $this->getFileID($hash, $filename);
$fileID = $this->getFileID($filename, $hash, $variant);
// Check conflict resolution scheme
$resolvedID = $this->resolveConflicts($conflictResolution, $fileID);
if($resolvedID === false) {
// If defering to the existing file, return the sha of the existing file
$stream = $this
->getFilesystem()
->readStream($fileID);
$hash = $this->getStreamSHA1($stream);
} else {
if($resolvedID !== false) {
// Submit and validate result
$result = $callback($resolvedID);
if(!$result) {
throw new Exception("Could not save {$filename}");
}
// in case conflict resolution renamed the file, return the renamed
$filename = $this->getOriginalFilename($resolvedID);
} elseif(empty($variant)) {
// If defering to the existing file, return the sha of the existing file,
// unless we are writing a variant (which has the same hash value as its original file)
$stream = $this
->getFilesystem()
->readStream($fileID);
$hash = $this->getStreamSHA1($stream);
}
return array(
'Hash' => $hash,
'Filename' => $filename,
'Variant' => ''
'Hash' => $hash,
'Variant' => $variant
);
}
public function getMetadata($hash, $filename, $variant = null) {
$fileID = $this->getFileID($hash, $filename, $variant);
/**
* Choose a default conflict resolution
*
* @param string $variant
* @return string
*/
protected function getDefaultConflictResolution($variant) {
// If using new naming scheme (segment by hash) it's normally safe to overwrite files.
// Variants are also normally safe to overwrite, since lazy-generation is implemented at a higher level.
$legacy = $this->useLegacyFilenames();
if(!$legacy || $variant) {
return AssetStore::CONFLICT_OVERWRITE;
}
// Legacy behaviour is to rename
return AssetStore::CONFLICT_RENAME;
}
/**
* Determine if legacy filenames should be used. These do not have hash path parts.
*
* @return bool
*/
protected function useLegacyFilenames() {
return Config::inst()->get(get_class($this), 'legacy_filenames');
}
public function getMetadata($filename, $hash, $variant = null) {
$fileID = $this->getFileID($filename, $hash, $variant);
return $this->getFilesystem()->getMetadata($fileID);
}
public function getMimeType($hash, $filename, $variant = null) {
$fileID = $this->getFileID($hash, $filename, $variant);
public function getMimeType($filename, $hash, $variant = null) {
$fileID = $this->getFileID($filename, $hash, $variant);
return $this->getFilesystem()->getMimetype($fileID);
}
public function exists($filename, $hash, $variant = null) {
$fileID = $this->getFileID($filename, $hash, $variant);
return $this->getFilesystem()->has($fileID);
}
/**
* Determine the path that should be written to, given the conflict resolution scheme
*
* @param string $conflictResolution
* @param string $fileID
* @return string|false Safe filename to write to. If false, then don't write.
* @return string|false Safe filename to write to. If false, then don't write, and use existing file.
* @throws Exception
*/
protected function resolveConflicts($conflictResolution, $fileID) {
@ -265,7 +329,7 @@ class FlysystemAssetStore implements AssetStore {
throw new \InvalidArgumentException("File could not be renamed with path {$fileID}");
}
// Default to use existing file
// Use existing file
case AssetStore::CONFLICT_USE_EXISTING:
default: {
return false;
@ -312,12 +376,16 @@ class FlysystemAssetStore implements AssetStore {
$variant = $matches['variant'];
}
// Remove hash
return preg_replace(
'/(?<hash>[a-zA-Z0-9]{10}\\/)(?<name>[^\\/]+)$/',
'$2',
$original
);
// Remove hash (unless using legacy filenames, without hash)
if($this->useLegacyFilenames()) {
return $original;
} else {
return preg_replace(
'/(?<hash>[a-zA-Z0-9]{10}\\/)(?<name>[^\\/]+)$/',
'$2',
$original
);
}
}
/**
@ -325,12 +393,12 @@ class FlysystemAssetStore implements AssetStore {
*
* The resulting file will look something like my/directory/EA775CB4D4/filename__variant.jpg
*
* @param string $hash
* @param string $filename Name of file
* @param string $hash Hash of original file
* @param string $variant (if given)
* @return string Adaptor specific identifier for this file/version
*/
protected function getFileID($hash, $filename, $variant = null) {
protected function getFileID($filename, $hash, $variant = null) {
// Since we use double underscore to delimit variants, eradicate them from filename
$filename = $this->cleanFilename($filename);
$name = basename($filename);

View File

@ -7,41 +7,53 @@ namespace SilverStripe\Filesystem\Storage;
*
* This is used as a use-agnostic interface to a single asset backed by an AssetStore
*
* Note that there are no setter equivalents for each of getHash, getVariant and getFilename.
* User code should utilise the setFrom* methods instead.
*
* @package framework
* @subpackage filesystem
*/
interface AssetContainer {
/**
* Assign a set of data to this container
* Assign a set of data to the backend
*
* @param string $data Raw binary/text content
* @param string $filename Name for the resulting file
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param string $conflictResolution {@see AssetStore}. Will default to one chosen by the backend
* @return array Tuple associative array (Filename, Hash, Variant)
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the given data.
*/
public function setFromString($data, $filename, $conflictResolution = null);
public function setFromString($data, $filename, $hash = null, $variant = null, $conflictResolution = null);
/**
* Assign a local file to this container
* Assign a local file to the backend.
*
* @param string $path Absolute filesystem path to file
* @param type $filename Optional path to ask the backend to name as.
* Will default to the filename of the $path, excluding directories.
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param string $conflictResolution {@see AssetStore}
* @return array Tuple associative array (Filename, Hash, Variant)
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the local file content.
*/
public function setFromLocalFile($path, $filename = null, $conflictResolution = null);
public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $conflictResolution = null);
/**
* Assign a stream to this container
* Assign a stream to the backend
*
* @param resource $stream Streamable resource
* @param string $filename Name for the resulting file
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param string $conflictResolution {@see AssetStore}
* @return array Tuple associative array (Filename, Hash, Variant)
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the raw stream.
*/
public function setFromStream($stream, $filename, $conflictResolution = null);
public function setFromStream($stream, $filename, $hash = null, $variant = null, $conflictResolution = null);
/**
* @return string Data from the file in this container
@ -76,4 +88,46 @@ interface AssetContainer {
* @return string Mime type for this file
*/
public function getMimeType();
/**
* Return file size in bytes.
*
* @return int
*/
public function getAbsoluteSize();
/**
* Determine if a valid non-empty image exists behind this asset
*
* @return bool
*/
public function getIsImage();
/**
* Determine if this container has a valid value
*
* @return bool Flag as to whether the file exists
*/
public function exists();
/**
* Get value of filename
*
* @return string
*/
public function getFilename();
/**
* Get value of hash
*
* @return string
*/
public function getHash();
/**
* Get value of variant
*
* @return string
*/
public function getVariant();
}

View File

@ -19,4 +19,11 @@ interface AssetNameGenerator extends \Iterator {
* @param string $filename
*/
public function __construct($filename);
/**
* Number of attempts allowed
*
* @return int
*/
public function getMaxTries();
}

View File

@ -3,7 +3,31 @@
namespace SilverStripe\Filesystem\Storage;
/**
* Represents an abstract asset persistence layer. Acts as a backend to files
* Represents an abstract asset persistence layer. Acts as a backend to files.
*
* Asset storage is identified by the following values arranged into a tuple:
*
* - "Filename" - Descriptive path for a file, although not necessarily a physical location. This could include
* custom directory names as a parent, as well as an extension.
* - "Hash" - The SHA1 of the file. This means that multiple files with the same Filename could be
* stored independently (depending on implementation) as long as they have different hashes.
* When a variant is identified, this value will refer to the hash of the file it was generated
* from, not the hash of the actual generated file.
* - "Variant" - An arbitrary string (which should not contain filesystem invalid characters) used
* to identify an asset which is a variant of an original. The asset storage backend has no knowledge
* of the mechanism used to generate this file, and is up to user code to perform the actual
* generation. An empty variant identifies this file as the original file.
*
* When assets are stored in the backend, user code may request one of the following conflict resolution
* mechanisms:
*
* - CONFLICT_OVERWRITE - If there is an existing file with this tuple, overwrite it.
* - CONFLICT_RENAME - If there is an existing file with this tuple, pick a new Filename for it and return it.
* This option is not allowed for use when storing variants, which should not modify the underlying
* Filename tuple value.
* - CONFLICT_USE_EXISTING - If there is an existing file with this tuple, return the tuple for the
* existing file instead.
* - CONFLICT_EXCEPTION - If there is an existing file with this tuple, throw an exception.
*
* @package framework
* @subpackage filesystem
@ -21,8 +45,10 @@ interface AssetStore {
const CONFLICT_OVERWRITE = 'overwrite';
/**
* Rename on file conflict. Rename rules will be
* determined by the backend
* Rename on file conflict. Rename rules will be determined by the backend.
*
* This option is not allowed for use when storing variants, which should not modify the underlying
* Filename tuple value.
*/
const CONFLICT_RENAME = 'rename';
@ -36,10 +62,13 @@ interface AssetStore {
*
* @param string $data Raw binary/text content
* @param string $filename Name for the resulting file
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param string $conflictResolution {@see AssetStore}. Will default to one chosen by the backend
* @return array Tuple associative array (Filename, Hash, Variant)
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the given data.
*/
public function setFromString($data, $filename, $conflictResolution = null);
public function setFromString($data, $filename, $hash = null, $variant = null, $conflictResolution = null);
/**
* Assign a local file to the backend.
@ -47,68 +76,90 @@ interface AssetStore {
* @param string $path Absolute filesystem path to file
* @param type $filename Optional path to ask the backend to name as.
* Will default to the filename of the $path, excluding directories.
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param string $conflictResolution {@see AssetStore}
* @return array Tuple associative array (Filename, Hash, Variant)
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the local file content.
*/
public function setFromLocalFile($path, $filename = null, $conflictResolution = null);
public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $conflictResolution = null);
/**
* Assign a stream to the backend
*
* @param resource $stream Streamable resource
* @param string $filename Name for the resulting file
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param string $conflictResolution {@see AssetStore}
* @return array Tuple associative array (Filename, Hash, Variant)
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the raw stream.
*/
public function setFromStream($stream, $filename, $conflictResolution = null);
public function setFromStream($stream, $filename, $hash = null, $variant = null, $conflictResolution = null);
/**
* Get contents of a given file
*
* @param string $hash sha1 hash of the file content
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
* If a variant is requested, this is the hash of the file before it was modified.
* @param string|null $variant Optional variant string for this file
* @return string Data from the file.
*/
public function getAsString($hash, $filename, $variant = null);
public function getAsString($filename, $hash, $variant = null);
/**
* Get a stream for this file
*
* @param string $hash sha1 hash of the file content
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
* If a variant is requested, this is the hash of the file before it was modified.
* @param string|null $variant Optional variant string for this file
* @return resource Data stream
*/
public function getAsStream($hash, $filename, $variant = null);
public function getAsStream($filename, $hash, $variant = null);
/**
* Get the url for the file
*
* @param string $hash sha1 hash of the file content
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
* If a variant is requested, this is the hash of the file before it was modified.
* @param string|null $variant Optional variant string for this file
* @return string public url to this resource
*/
public function getAsURL($hash, $filename, $variant = null);
public function getAsURL($filename, $hash, $variant = null);
/**
* Get metadata for this file, if available
*
* @param string $hash sha1 hash of the file content
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
* If a variant is requested, this is the hash of the file before it was modified.
* @param string|null $variant Optional variant string for this file
* @return array|null File information, or null if no metadata available
*/
public function getMetadata($hash, $filename, $variant = null);
public function getMetadata($filename, $hash, $variant = null);
/**
* Get mime type of this file
*
* @param string $hash sha1 hash of the file content
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
* If a variant is requested, this is the hash of the file before it was modified.
* @param string|null $variant Optional variant string for this file
* @return string Mime type for this file
*/
public function getMimeType($hash, $filename, $variant = null);
public function getMimeType($filename, $hash, $variant = null);
/**
* Determine if a file exists with the given tuple
*
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
* If a variant is requested, this is the hash of the file before it was modified.
* @param string|null $variant Optional variant string for this file
* @return bool Flag as to whether the file exists
*/
public function exists($filename, $hash, $variant = null);
}

View File

@ -0,0 +1,457 @@
<?php
use SilverStripe\Filesystem\ImageManipulation;
use SilverStripe\Filesystem\Storage\AssetContainer;
use SilverStripe\Filesystem\Storage\AssetStore;
// Un-comment once https://github.com/silverstripe/silverstripe-framework/pull/4551/ is merged
// namespace SilverStripe\Filesystem\Storage;
/**
* Represents a file reference stored in a database
*
* @property string $Hash SHA of the file
* @property string $Filename Name of the file, including directory
* @property string $Variant Variant of the file
*
* @package framework
* @subpackage model
*/
class DBFile extends CompositeDBField implements AssetContainer, ShortcodeHandler {
use ImageManipulation;
/**
* List of allowed file categories.
*
* {@see File::$app_categories}
*
* @var array
*/
protected $allowedCategories = array();
/**
* List of image mime types supported by the image manipulations API
*
* {@see File::app_categories} for matching extensions.
*
* @config
* @var array
*/
private static $supported_images = array(
'image/jpeg',
'image/gif',
'image/png'
);
/**
* Create a new image manipulation
*
* @param string $name
* @param array|string $allowed List of allowed file categories (not extensions), as per File::$app_categories
*/
public function __construct($name = null, $allowed = array()) {
parent::__construct($name);
$this->setAllowedCategories($allowed);
}
/**
* Determine if a valid non-empty image exists behind this asset, which is a format
* compatible with image manipulations
*
* @return boolean
*/
public function getIsImage() {
// Check file type
$mime = $this->getMimeType();
return $mime && in_array($mime, $this->config()->supported_images);
}
/**
* @return AssetStore
*/
protected function getStore() {
return Injector::inst()->get('AssetStore');
}
private static $composite_db = array(
"Hash" => "Varchar(255)", // SHA of the base content
"Filename" => "Varchar(255)", // Path identifier of the base content
"Variant" => "Varchar(255)", // Identifier of the variant to the base, if given
);
private static $casting = array(
'URL' => 'Varchar',
'AbsoluteURL' => 'Varchar',
'Basename' => 'Varchar',
'Title' => 'Varchar',
'MimeType' => 'Varchar',
'String' => 'Text',
'Tag' => 'HTMLText'
);
public function scaffoldFormField($title = null, $params = null) {
return null;
// @todo
//return new AssetUploadField($this->getName(), $title);
}
/**
* Return a html5 tag of the appropriate for this file (normally img or a)
*
* @return string
*/
public function forTemplate() {
return $this->getTag() ?: '';
}
/**
* Return a html5 tag of the appropriate for this file (normally img or a)
*
* @return string
*/
public function getTag() {
$template = $this->getFrontendTemplate();
if(empty($template)) {
return '';
}
return (string)$this->renderWith($template);
}
/**
* Determine the template to render as on the frontend
*
* @return string Name of template
*/
public function getFrontendTemplate() {
// Check that path is available
$url = $this->getURL();
if(empty($url)) {
return null;
}
// Image template for supported images
if($this->getIsImage()) {
return 'DBFile_image';
}
// Default download
return 'DBFile_download';
}
/**
* Get trailing part of filename
*
* @return string
*/
public function getBasename() {
if($this->exists()) {
return basename($this->getSourceURL());
}
}
/**
* Get file extension
*
* @return string
*/
public function getExtension() {
if($this->exists()) {
return pathinfo($this->Filename, PATHINFO_EXTENSION);
}
}
/**
* Alt title for this
*
* @return string
*/
public function getTitle() {
// If customised, use the customised title
if($this->failover && ($title = $this->failover->Title)) {
return $title;
}
// fallback to using base name
return $this->getBasename();
}
public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $conflictResolution = null) {
$this->assertFilenameValid($filename ?: $path);
$result = $this
->getStore()
->setFromLocalFile($path, $filename, $hash, $variant, $conflictResolution);
// Update from result
if($result) {
$this->setValue($result);
}
return $result;
}
public function setFromStream($stream, $filename, $hash = null, $variant = null, $conflictResolution = null) {
$this->assertFilenameValid($filename);
$result = $this
->getStore()
->setFromStream($stream, $filename, $hash, $variant, $conflictResolution);
// Update from result
if($result) {
$this->setValue($result);
}
return $result;
}
public function setFromString($data, $filename, $hash = null, $variant = null, $conflictResolution = null) {
$this->assertFilenameValid($filename);
$result = $this
->getStore()
->setFromString($data, $filename, $hash, $variant, $conflictResolution);
// Update from result
if($result) {
$this->setValue($result);
}
return $result;
}
public function getStream() {
if(!$this->exists()) {
return null;
}
return $this
->getStore()
->getAsStream($this->Filename, $this->Hash, $this->Variant);
}
public function getString() {
if(!$this->exists()) {
return null;
}
return $this
->getStore()
->getAsString($this->Filename, $this->Hash, $this->Variant);
}
public function getURL() {
if(!$this->exists()) {
return null;
}
$url = $this->getSourceURL();
$this->updateURL($url);
$this->extend('updateURL', $url);
return $url;
}
/**
* Get URL, but without resampling.
*
* @return string
*/
public function getSourceURL() {
if(!$this->exists()) {
return null;
}
return $this
->getStore()
->getAsURL($this->Filename, $this->Hash, $this->Variant);
}
/**
* Get the absolute URL to this resource
*
* @return type
*/
public function getAbsoluteURL() {
if(!$this->exists()) {
return null;
}
return Director::absoluteURL($this->getURL());
}
public function getMetaData() {
if(!$this->exists()) {
return null;
}
return $this
->getStore()
->getMetadata($this->Filename, $this->Hash, $this->Variant);
}
public function getMimeType() {
if(!$this->exists()) {
return null;
}
return $this
->getStore()
->getMimeType($this->Filename, $this->Hash, $this->Variant);
}
public function getValue() {
if($this->exists()) {
return array(
'Filename' => $this->Filename,
'Hash' => $this->Hash,
'Variant' => $this->Variant
);
}
}
public function exists() {
return !empty($this->Filename);
}
public static function get_shortcodes() {
return 'dbfile_link';
}
public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = array()) {
// @todo
}
public function getFilename() {
return $this->getField('Filename');
}
public function getHash() {
return $this->getField('Hash');
}
public function getVariant() {
return $this->getField('Variant');
}
/**
* Return file size in bytes.
*
* @return int
*/
public function getAbsoluteSize() {
$metadata = $this->getMetaData();
if(isset($metadata['size'])) {
return $metadata['size'];
}
}
/**
* Customise this object with an "original" record for getting other customised fields
*
* @param AssetContainer $original
* @return $this
*/
public function setOriginal($original) {
$this->failover = $original;
return $this;
}
/**
* Get list of allowed file categories
*
* @return array
*/
public function getAllowedCategories() {
return $this->allowedCategories;
}
/**
* Assign allowed categories
*
* @param array|string $categories
* @return $this
*/
public function setAllowedCategories($categories) {
if(is_string($categories)) {
$categories = preg_split('/\s*,\s*/', $categories);
}
$this->allowedCategories = (array)$categories;
return $this;
}
/**
* Gets the list of extensions (if limited) for this field. Empty list
* means there is no restriction on allowed types.
*
* @return array
*/
protected function getAllowedExtensions() {
$categories = $this->getAllowedCategories();
return File::get_category_extensions($categories);
}
/**
* Validate that this DBFile accepts this filename as valid
*
* @param string $filename
* @throws ValidationException
* @return bool
*/
protected function isValidFilename($filename) {
$extension = strtolower(File::get_file_extension($filename));
// Validate true if within the list of allowed extensions
$allowed = $this->getAllowedExtensions();
if($allowed) {
return in_array($extension, $allowed);
}
// If no extensions are configured, fallback to global list
$globalList = File::config()->allowed_extensions;
if(in_array($extension, $globalList)) {
return true;
}
// Only admins can bypass global rules
return !File::config()->apply_restrictions_to_admin && Permission::check('ADMIN');
}
/**
* Check filename, and raise a ValidationException if invalid
*
* @param string $filename
* @throws ValidationException
*/
protected function assertFilenameValid($filename) {
$result = new ValidationResult();
$this->validate($result, $filename);
if(!$result->valid()) {
throw new ValidationException($result);
}
}
/**
* Hook to validate this record against a validation result
*
* @param ValidationResult $result
* @param string $filename Optional filename to validate. If omitted, the current value is validated.
* @return bool Valid flag
*/
public function validate(ValidationResult $result, $filename = null) {
if(empty($filename)) {
$filename = $this->getFilename();
}
if(empty($filename) || $this->isValidFilename($filename)) {
return true;
}
// Check allowed extensions
$extensions = $this->getAllowedExtensions();
if(empty($extensions)) {
$extensions = File::config()->allowed_extensions;
}
sort($extensions);
$message = _t(
'File.INVALIDEXTENSION',
'Extension is not allowed (valid: {extensions})',
'Argument 1: Comma-separated list of valid extensions',
array('extensions' => wordwrap(implode(', ',$extensions)))
);
$result->error($message);
return false;
}
public function setField($field, $value, $markChanged = true) {
// Catch filename validation on direct assignment
if($field === 'Filename' && $value) {
$this->assertFilenameValid($value);
}
return parent::setField($field, $value, $markChanged);
}
}

View File

@ -65,6 +65,13 @@ class DefaultAssetNameGenerator implements AssetNameGenerator {
*/
protected $max = 100;
/**
* Number of digits to prefix with 0, if padding
*
* @var int
*/
protected $padding = 0;
/**
* First version
*
@ -76,6 +83,7 @@ class DefaultAssetNameGenerator implements AssetNameGenerator {
$this->filename = $filename;
$this->directory = ltrim(dirname($filename), '.');
$name = basename($this->filename);
// Note: Unlike normal extensions, we want to split at the first period, not the last.
if(($pos = strpos($name, '.')) !== false) {
$this->extension = substr($name, $pos);
$name = substr($name, 0, $pos);
@ -84,10 +92,15 @@ class DefaultAssetNameGenerator implements AssetNameGenerator {
}
// Extract version prefix if already applied to this file
$pattern = '/^(?<name>.+)' . preg_quote($this->getPrefix()) . '(?<version>[0-9]+)$/';
$this->padding = 0;
$pattern = '/^(?<name>[^\/]+?)' . preg_quote($this->getPrefix()) . '(?<version>[0-9]+)$/';
if(preg_match($pattern, $name, $matches)) {
$this->first = $matches['version'] + 1;
$this->first = (int)$matches['version'];
$this->name = $matches['name'];
// Check if number is padded
if(strpos($matches['version'], '0') === 0) {
$this->padding = strlen($matches['version']);
}
} else {
$this->first = 1;
$this->name = $name;
@ -109,13 +122,16 @@ class DefaultAssetNameGenerator implements AssetNameGenerator {
$version = $this->version;
// Initially suggest original name
if($version === 1) {
if($version === $this->first) {
return $this->filename;
}
// If there are more than $this->max files we need a new scheme
if($version >= $this->max) {
if($version >= $this->max + $this->first - 1) {
$version = substr(md5(time()), 0, 10);
} elseif($this->padding) {
// Else, pad
$version = str_pad($version, $this->padding, '0', STR_PAD_LEFT);
}
// Build next name
@ -127,7 +143,7 @@ class DefaultAssetNameGenerator implements AssetNameGenerator {
}
public function key() {
return $this->version;
return $this->version - $this->first;
}
public function next() {
@ -139,7 +155,11 @@ class DefaultAssetNameGenerator implements AssetNameGenerator {
}
public function valid() {
return $this->version <= $this->max;
return $this->version < $this->max + $this->first;
}
public function getMaxTries() {
return $this->max;
}
}

View File

@ -108,18 +108,23 @@ class FileField extends FormField {
}
$fileClass = File::get_class_for_file_extension(
pathinfo($_FILES[$this->name]['name'], PATHINFO_EXTENSION)
File::get_file_extension($_FILES[$this->name]['name'], PATHINFO_EXTENSION)
);
if($this->relationAutoSetting) {
// assume that the file is connected via a has-one
$hasOnes = $record->hasOne($this->name);
// try to create a file matching the relation
$file = (is_string($hasOnes)) ? Object::create($hasOnes) : new $fileClass();
$objectClass = $record->hasOne($this->name);
if($objectClass === 'File' || empty($objectClass)) {
// Create object of the appropriate file class
$file = Object::create($fileClass);
} else {
// try to create a file matching the relation
$file = Object::create($objectClass);
}
} else if($record instanceof File) {
$file = $record;
} else {
$file = new $fileClass();
$file = Object::create($fileClass);
}
$this->upload->loadIntoFile($_FILES[$this->name], $file, $this->getFolderName());
@ -129,7 +134,7 @@ class FileField extends FormField {
}
if($this->relationAutoSetting) {
if(!$hasOnes) {
if(!$objectClass) {
return false;
}
@ -148,7 +153,7 @@ class FileField extends FormField {
/**
* Get custom validator for this field
*
* @param Upload_Validator $validator
* @return Upload_Validator
*/
public function getValidator() {
return $this->upload->getValidator();
@ -158,7 +163,7 @@ class FileField extends FormField {
* Set custom validator for this field
*
* @param Upload_Validator $validator
* @return FileField Self reference
* @return $this Self reference
*/
public function setValidator($validator) {
$this->upload->setValidator($validator);
@ -239,7 +244,7 @@ class FileField extends FormField {
/**
* Limit allowed file extensions by specifying categories of file types.
* These may be 'image', 'audio', 'mov', 'zip', 'flash', or 'doc'
* These may be 'image', 'image/supported', 'audio', 'video', 'archive', 'flash', or 'document'
* See {@link File::$allowed_extensions} for details of allowed extensions
* for each of these categories
*
@ -248,23 +253,7 @@ class FileField extends FormField {
* @return $this
*/
public function setAllowedFileCategories($category) {
$extensions = array();
$knownCategories = File::config()->app_categories;
// Parse arguments
$categories = func_get_args();
if(func_num_args() === 1 && is_array(reset($categories))) {
$categories = reset($categories);
}
// Merge all categories into list of extensions
foreach(array_filter($categories) as $category) {
if(isset($knownCategories[$category])) {
$extensions = array_merge($extensions, $knownCategories[$category]);
} else {
user_error("Unknown file category: $category", E_USER_ERROR);
}
}
$extensions = File::get_category_extensions(func_get_args());
return $this->setAllowedExtensions($extensions);
}

View File

@ -1260,4 +1260,24 @@ class FormField extends RequestHandler {
return $field;
}
/**
* Determine if escaping of this field should be disabled
*
* @param bool $dontEscape
* @return $this
*/
public function setDontEscape($dontEscape) {
$this->dontEscape = $dontEscape;
return $this;
}
/**
* Determine if escaping is disabled
*
* @return bool
*/
public function getDontEscape() {
return $this->dontEscape;
}
}

View File

@ -9,23 +9,24 @@
class HtmlEditorField extends TextareaField {
/**
* Use TinyMCE's GZIP compressor
*
* @config
* @var Boolean Use TinyMCE's GZIP compressor
* @var bool
*/
private static $use_gzip = true;
/**
* @config
* @var Integer Default insertion width for Images and Media
*/
private static $insert_width = 600;
/**
* @config
* @var bool Should we check the valid_elements (& extended_valid_elements) rules from HtmlEditorConfig server side?
*/
private static $sanitise_server_side = false;
/**
* Number of rows
*
* @var int
*/
protected $rows = 30;
/**
@ -87,14 +88,17 @@ class HtmlEditorField extends TextareaField {
$img->setAttribute('src', preg_replace('/([^\?]*)\?r=[0-9]+$/i', '$1', $img->getAttribute('src')));
// Resample the images if the width & height have changed.
if($image = File::find(urldecode(Director::makeRelative($img->getAttribute('src'))))){
$fileID = $img->getAttribute('data-fileid');
if($fileID && ($image = File::get()->byID($fileID))) {
$width = (int)$img->getAttribute('width');
$height = (int)$img->getAttribute('height');
if($width && $height && ($width != $image->getWidth() || $height != $image->getHeight())) {
//Make sure that the resized image actually returns an image:
$resized=$image->ResizedImage($width, $height);
if($resized) $img->setAttribute('src', $resized->getRelativePath());
$resized = $image->ResizedImage($width, $height);
if($resized) {
$img->setAttribute('src', $resized->getURL());
}
}
}
@ -322,9 +326,6 @@ class HtmlEditorField_Toolbar extends RequestHandler {
'Created' => 'SS_Datetime->Nice'
));
$numericLabelTmpl = '<span class="step-label"><span class="flyout">%d</span><span class="arrow"></span>'
. '<strong class="title">%s</strong></span>';
$fromCMS = new CompositeField(
$select = TreeDropdownField::create('ParentID', "", 'Folder')
->addExtraClass('noborder')
@ -407,29 +408,27 @@ class HtmlEditorField_Toolbar extends RequestHandler {
/**
* View of a single file, either on the filesystem or on the web.
*
* @param SS_HTTPRequest $request
* @return string
*/
public function viewfile($request) {
// TODO Would be cleaner to consistently pass URL for both local and remote files,
// but GridField doesn't allow for this kind of metadata customization at the moment.
$file = null;
if($url = $request->getVar('FileURL')) {
if(Director::is_absolute_url($url) && !Director::is_site_url($url)) {
$file = new File(array(
'Title' => basename($url),
'Filename' => $url
));
} else {
$url = Director::makeRelative($request->getVar('FileURL'));
$url = Image::strip_resampled_prefix($url);
$file = File::get()->filter('Filename', $url)->first();
if(!$file) $file = new File(array(
'Title' => basename($url),
'Filename' => $url
));
}
// URLS should be used for remote resources (not local assets)
$url = Director::absoluteURL($url);
} elseif($id = $request->getVar('ID')) {
// Use local dataobject
$file = DataObject::get_by_id('File', $id);
$url = $file->RelativeLink();
if(!$file) {
throw new InvalidArgumentException("File could not be found");
}
$url = $file->getURL();
if(!$url) {
return $this->httpError(404, 'File not found');
}
} else {
throw new LogicException('Need either "ID" or "FileURL" parameter to identify the file');
}
@ -437,17 +436,30 @@ class HtmlEditorField_Toolbar extends RequestHandler {
// Instanciate file wrapper and get fields based on its type
// Check if appCategory is an image and exists on the local system, otherwise use oEmbed to refference a
// remote image
if($file && $file->appCategory() == 'image' && Director::is_site_url($url)) {
$fileWrapper = new HtmlEditorField_Image($url, $file);
} elseif(!Director::is_site_url($url)) {
$fileWrapper = new HtmlEditorField_Embed($url, $file);
} else {
$fileWrapper = new HtmlEditorField_File($url, $file);
$fileCategory = File::get_app_category(File::get_file_extension($url));
switch($fileCategory) {
case 'image':
case 'image/supported':
$fileWrapper = new HtmlEditorField_Image($url, $file);
break;
case 'flash':
$fileWrapper = new HtmlEditorField_Flash($url, $file);
break;
default:
// Only remote files can be linked via o-embed
// {@see HtmlEditorField_Toolbar::getAllowedExtensions())
if($file) {
throw new InvalidArgumentException(
"Oembed is only compatible with remote files"
);
}
// Other files should fallback to oembed
$fileWrapper = new HtmlEditorField_Embed($url, $file);
break;
}
// Render fields and return
$fields = $this->getFieldsForFile($url, $fileWrapper);
$this->extend('updateFieldsForFile', $fields, $url, $fileWrapper);
return $fileWrapper->customise(array(
'Fields' => $fields,
))->renderWith($this->templateViewFile);
@ -493,243 +505,33 @@ class HtmlEditorField_Toolbar extends RequestHandler {
* for manipulating the instance of the file as inserted into the HTML content,
* not the "master record" in the database - hence there's no form or saving logic.
*
* @param String Relative or absolute URL to file
* @param string $url Abolute URL to asset
* @param HtmlEditorField_File $file Asset wrapper
* @return FieldList
*/
protected function getFieldsForFile($url, $file) {
protected function getFieldsForFile($url, HtmlEditorField_File $file) {
$fields = $this->extend('getFieldsForFile', $url, $file);
if(!$fields) {
if($file instanceof HtmlEditorField_Embed) {
$fields = $this->getFieldsForOembed($url, $file);
} elseif($file->Extension == 'swf') {
$fields = $this->getFieldsForFlash($url, $file);
} else {
$fields = $this->getFieldsForImage($url, $file);
}
$fields->push(new HiddenField('URL', false, $url));
$fields = $file->getFields();
$file->extend('updateFields', $fields);
}
$this->extend('updateFieldsForFile', $fields, $url, $file);
return $fields;
}
/**
* @return FieldList
*/
protected function getFieldsForOembed($url, $file) {
if(isset($file->Oembed->thumbnail_url)) {
$thumbnailURL = Convert::raw2att($file->Oembed->thumbnail_url);
} elseif($file->Type == 'photo') {
$thumbnailURL = Convert::raw2att($file->Oembed->url);
} else {
$thumbnailURL = FRAMEWORK_DIR . '/images/default_media.png';
}
$fileName = Convert::raw2att($file->Name);
$fields = new FieldList(
$filePreview = CompositeField::create(
CompositeField::create(
new LiteralField(
"ImageFull",
"<img id='thumbnailImage' class='thumbnail-preview' "
. "src='{$thumbnailURL}?r=" . rand(1,100000) . "' alt='$fileName' />\n"
)
)->setName("FilePreviewImage")->addExtraClass('cms-file-info-preview'),
CompositeField::create(
CompositeField::create(
new ReadonlyField("FileType", _t('AssetTableField.TYPE','File type') . ':', $file->Type),
$urlField = ReadonlyField::create(
'ClickableURL',
_t('AssetTableField.URL','URL'),
sprintf(
'<a href="%s" target="_blank" class="file">%s</a>',
Convert::raw2att($url),
Convert::raw2att($url)
)
)->addExtraClass('text-wrap')
)
)->setName("FilePreviewData")->addExtraClass('cms-file-info-data')
)->setName("FilePreview")->addExtraClass('cms-file-info'),
new TextField('CaptionText', _t('HtmlEditorField.CAPTIONTEXT', 'Caption text')),
DropdownField::create(
'CSSClass',
_t('HtmlEditorField.CSSCLASS', 'Alignment / style'),
array(
'leftAlone' => _t('HtmlEditorField.CSSCLASSLEFTALONE', 'On the left, on its own.'),
'center' => _t('HtmlEditorField.CSSCLASSCENTER', 'Centered, on its own.'),
'left' => _t('HtmlEditorField.CSSCLASSLEFT', 'On the left, with text wrapping around.'),
'right' => _t('HtmlEditorField.CSSCLASSRIGHT', 'On the right, with text wrapping around.')
)
)->addExtraClass('last')
);
if($file->Width != null){
$fields->push(
FieldGroup::create(
_t('HtmlEditorField.IMAGEDIMENSIONS', 'Dimensions'),
TextField::create(
'Width',
_t('HtmlEditorField.IMAGEWIDTHPX', 'Width'),
$file->InsertWidth
)->setMaxLength(5),
TextField::create(
'Height',
_t('HtmlEditorField.IMAGEHEIGHTPX', 'Height'),
$file->InsertHeight
)->setMaxLength(5)
)->addExtraClass('dimensions last')
);
}
$urlField->dontEscape = true;
if($file->Type == 'photo') {
$fields->insertBefore('CaptionText', new TextField(
'AltText',
_t('HtmlEditorField.IMAGEALTTEXT', 'Alternative text (alt) - shown if image can\'t be displayed'),
$file->Title,
80
));
$fields->insertBefore('CaptionText', new TextField(
'Title',
_t('HtmlEditorField.IMAGETITLE', 'Title text (tooltip) - for additional information about the image')
));
}
$this->extend('updateFieldsForOembed', $fields, $url, $file);
return $fields;
}
/**
* @return FieldList
*/
protected function getFieldsForFlash($url, $file) {
$fields = new FieldList(
FieldGroup::create(
_t('HtmlEditorField.IMAGEDIMENSIONS', 'Dimensions'),
TextField::create(
'Width',
_t('HtmlEditorField.IMAGEWIDTHPX', 'Width'),
$file->Width
)->setMaxLength(5),
TextField::create(
'Height',
" x " . _t('HtmlEditorField.IMAGEHEIGHTPX', 'Height'),
$file->Height
)->setMaxLength(5)
)->addExtraClass('dimensions')
);
$this->extend('updateFieldsForFlash', $fields, $url, $file);
return $fields;
}
/**
* @return FieldList
*/
protected function getFieldsForImage($url, $file) {
if($file->File instanceof Image) {
$formattedImage = $file->File->generateFormattedImage('ScaleWidth',
Config::inst()->get('Image', 'asset_preview_width'));
$thumbnailURL = Convert::raw2att($formattedImage ? $formattedImage->URL : $url);
} else {
$thumbnailURL = Convert::raw2att($url);
}
$fileName = Convert::raw2att($file->Name);
$fields = new FieldList(
CompositeField::create(
CompositeField::create(
LiteralField::create(
"ImageFull",
"<img id='thumbnailImage' class='thumbnail-preview' "
. "src='{$thumbnailURL}?r=" . rand(1,100000) . "' alt='$fileName' />\n"
)
)->setName("FilePreviewImage")->addExtraClass('cms-file-info-preview'),
CompositeField::create(
CompositeField::create(
new ReadonlyField("FileType", _t('AssetTableField.TYPE','File type'), $file->FileType),
new ReadonlyField("Size", _t('AssetTableField.SIZE','File size'), $file->getSize()),
$urlField = new ReadonlyField(
'ClickableURL',
_t('AssetTableField.URL','URL'),
sprintf(
'<a href="%s" title="%s" target="_blank" class="file-url">%s</a>',
Convert::raw2att($file->Link()),
Convert::raw2att($file->Link()),
Convert::raw2att($file->RelativeLink())
)
),
new DateField_Disabled("Created", _t('AssetTableField.CREATED','First uploaded'),
$file->Created),
new DateField_Disabled("LastEdited", _t('AssetTableField.LASTEDIT','Last changed'),
$file->LastEdited)
)
)->setName("FilePreviewData")->addExtraClass('cms-file-info-data')
)->setName("FilePreview")->addExtraClass('cms-file-info'),
TextField::create(
'AltText',
_t('HtmlEditorField.IMAGEALT', 'Alternative text (alt)'),
$file->Title,
80
)->setDescription(
_t('HtmlEditorField.IMAGEALTTEXTDESC', 'Shown to screen readers or if image can\'t be displayed')),
TextField::create(
'Title',
_t('HtmlEditorField.IMAGETITLETEXT', 'Title text (tooltip)')
)->setDescription(
_t('HtmlEditorField.IMAGETITLETEXTDESC', 'For additional information about the image')),
new TextField('CaptionText', _t('HtmlEditorField.CAPTIONTEXT', 'Caption text')),
DropdownField::create(
'CSSClass',
_t('HtmlEditorField.CSSCLASS', 'Alignment / style'),
array(
'leftAlone' => _t('HtmlEditorField.CSSCLASSLEFTALONE', 'On the left, on its own.'),
'center' => _t('HtmlEditorField.CSSCLASSCENTER', 'Centered, on its own.'),
'left' => _t('HtmlEditorField.CSSCLASSLEFT', 'On the left, with text wrapping around.'),
'right' => _t('HtmlEditorField.CSSCLASSRIGHT', 'On the right, with text wrapping around.')
)
)->addExtraClass('last')
);
if($file->Width != null){
$fields->push(
FieldGroup::create(_t('HtmlEditorField.IMAGEDIMENSIONS', 'Dimensions'),
TextField::create(
'Width',
_t('HtmlEditorField.IMAGEWIDTHPX', 'Width'),
$file->InsertWidth
)->setMaxLength(5),
TextField::create(
'Height',
" x " . _t('HtmlEditorField.IMAGEHEIGHTPX', 'Height'),
$file->InsertHeight
)->setMaxLength(5)
)->addExtraClass('dimensions last')
);
}
$urlField->dontEscape = true;
$this->extend('updateFieldsForImage', $fields, $url, $file);
return $fields;
}
/**
* @param Int
* Gets files filtered by a given parent with the allowed extensions
*
* @param int $parentID
* @return DataList
*/
protected function getFiles($parentID = null) {
$exts = $this->getAllowedExtensions();
$dotExts = array_map(function($ext) { return ".{$ext}"; }, $exts);
$files = File::get()->filter('Filename:EndsWith', $dotExts);
$dotExts = array_map(function($ext) {
return ".{$ext}";
}, $exts);
$files = File::get()->filter('Name:EndsWith', $dotExts);
// Limit by folder (if required)
if($parentID) {
@ -743,7 +545,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
* @return Array All extensions which can be handled by the different views.
*/
protected function getAllowedExtensions() {
$exts = array('jpg', 'gif', 'png', 'swf','jpeg');
$exts = array('jpg', 'gif', 'png', 'swf', 'jpeg');
$this->extend('updateAllowedExtensions', $exts);
return $exts;
}
@ -759,76 +561,357 @@ class HtmlEditorField_Toolbar extends RequestHandler {
* @package forms
* @subpackage fields-formattedinput
*/
class HtmlEditorField_File extends ViewableData {
abstract class HtmlEditorField_File extends ViewableData {
/**
* Default insertion width for Images and Media
*
* @config
* @var int
*/
private static $insert_width = 600;
/**
* Default insert height for images and media
*
* @config
* @var int
*/
private static $insert_height = 360;
/**
* Max width for insert-media preview.
*
* Matches CSS rule for .cms-file-info-preview
*
* @var int
*/
private static $media_preview_width = 176;
/**
* Max height for insert-media preview.
*
* Matches CSS rule for .cms-file-info-preview
*
* @var int
*/
private static $media_preview_height = 128;
private static $casting = array(
'URL' => 'Varchar',
'Name' => 'Varchar'
);
/** @var String */
/**
* Absolute URL to asset
*
* @var string
*/
protected $url;
/** @var File */
/**
* File dataobject (if available)
*
* @var File
*/
protected $file;
/**
* @param String
* @param File
* @param string $url
* @param File $file
*/
public function __construct($url, $file = null) {
public function __construct($url, File $file = null) {
$this->url = $url;
$this->file = $file;
$this->failover = $file;
parent::__construct();
}
/**
* @return File Might not be set (for remote files)
* @return FieldList
*/
public function getFields() {
$fields = new FieldList(
CompositeField::create(
CompositeField::create(LiteralField::create("ImageFull", $this->getPreview()))
->setName("FilePreviewImage")
->addExtraClass('cms-file-info-preview'),
CompositeField::create($this->getDetailFields())
->setName("FilePreviewData")
->addExtraClass('cms-file-info-data')
)
->setName("FilePreview")
->addExtraClass('cms-file-info'),
TextField::create('CaptionText', _t('HtmlEditorField.CAPTIONTEXT', 'Caption text')),
DropdownField::create(
'CSSClass',
_t('HtmlEditorField.CSSCLASS', 'Alignment / style'),
array(
'leftAlone' => _t('HtmlEditorField.CSSCLASSLEFTALONE', 'On the left, on its own.'),
'center' => _t('HtmlEditorField.CSSCLASSCENTER', 'Centered, on its own.'),
'left' => _t('HtmlEditorField.CSSCLASSLEFT', 'On the left, with text wrapping around.'),
'right' => _t('HtmlEditorField.CSSCLASSRIGHT', 'On the right, with text wrapping around.')
)
),
FieldGroup::create(_t('HtmlEditorField.IMAGEDIMENSIONS', 'Dimensions'),
TextField::create(
'Width',
_t('HtmlEditorField.IMAGEWIDTHPX', 'Width'),
$this->getInsertWidth()
)->setMaxLength(5),
TextField::create(
'Height',
" x " . _t('HtmlEditorField.IMAGEHEIGHTPX', 'Height'),
$this->getInsertHeight()
)->setMaxLength(5)
)->addExtraClass('dimensions last'),
HiddenField::create('URL', false, $this->getURL()),
HiddenField::create('FileID', false, $this->getFileID())
);
return $fields;
}
/**
* Get list of fields for previewing this records details
*
* @return FieldList
*/
protected function getDetailFields() {
$fields = new FieldList(
ReadonlyField::create("FileType", _t('AssetTableField.TYPE','File type'), $this->getFileType()),
ReadonlyField::create(
'ClickableURL', _t('AssetTableField.URL','URL'), $this->getExternalLink()
)->setDontEscape(true)
);
// Get file size
if($this->getSize()) {
$fields->insertAfter(
'FileType',
ReadonlyField::create("Size", _t('AssetTableField.SIZE','File size'), $this->getSize())
);
}
// Get modified details of local record
if($this->getFile()) {
$fields->push(new DateField_Disabled(
"Created",
_t('AssetTableField.CREATED', 'First uploaded'),
$this->getFile()->Created
));
$fields->push(new DateField_Disabled(
"LastEdited",
_t('AssetTableField.LASTEDIT','Last changed'),
$this->getFile()->LastEdited
));
}
return $fields;
}
/**
* Get file DataObject
*
* Might not be set (for remote files)
*
* @return File
*/
public function getFile() {
return $this->file;
}
/**
* Get file ID
*
* @return int
*/
public function getFileID() {
if($file = $this->getFile()) {
return $file->ID;
}
}
/**
* Get absolute URL
*
* @return string
*/
public function getURL() {
return $this->url;
}
/**
* Get basename
*
* @return string
*/
public function getName() {
return ($this->file) ? $this->file->Name : preg_replace('/\?.*/', '', basename($this->url));
return $this->file
? $this->file->Name
: preg_replace('/\?.*/', '', basename($this->url));
}
/**
* @return String HTML
* Get descriptive file type
*
* @return string
*/
public function getFileType() {
return File::get_file_type($this->getName());
}
/**
* Get file size (if known) as string
*
* @return string|false String value, or false if doesn't exist
*/
public function getSize() {
if($this->file) {
return $this->file->getSize();
}
return false;
}
/**
* HTML content for preview
*
* @return string HTML
*/
public function getPreview() {
$preview = $this->extend('getPreview');
if($preview) return $preview;
if($preview) {
return $preview;
}
// Generate tag from preview
$thumbnailURL = Convert::raw2att(
Controller::join_links($this->getPreviewURL(), "?r=" . rand(1,100000))
);
$fileName = Convert::raw2att($this->Name);
return sprintf(
"<img id='thumbnailImage' class='thumbnail-preview' src='%s' alt='%s' />\n",
$thumbnailURL,
$fileName
);
}
/**
* HTML Content for external link
*
* @return string
*/
public function getExternalLink() {
$title = $this->file
? $this->file->getTitle()
: $this->getName();
return sprintf(
'<a href="%1$s" title="%2$s" target="_blank" rel="external" class="file-url">%1$s</a>',
Convert::raw2att($this->url),
Convert::raw2att($title)
);
}
/**
* Generate thumbnail url
*
* @return string
*/
public function getPreviewURL() {
// Get preview from file
if($this->file) {
return $this->file->CMSThumbnail();
} else {
// Hack to use the framework's built-in thumbnail support without creating a local file representation
$tmpFile = new File(array('Name' => $this->Name, 'Filename' => $this->Name));
return $tmpFile->CMSThumbnail();
return $this->getFilePreviewURL();
}
// Generate default icon html
return File::get_icon_for_extension($this->getExtension());
}
/**
* Generate thumbnail URL from file dataobject (if available)
*
* @return string
*/
protected function getFilePreviewURL() {
// Get preview from file
if($this->file) {
$width = $this->config()->media_preview_width;
$height = $this->config()->media_preview_height;
return $this->file->ThumbnailURL($width, $height);
}
}
/**
* Get file extension
*
* @return string
*/
public function getExtension() {
return strtolower(($this->file) ? $this->file->Extension : pathinfo($this->Name, PATHINFO_EXTENSION));
$extension = File::get_file_extension($this->getName());
return strtolower($extension);
}
/**
* Category name
*
* @return string
*/
public function appCategory() {
if($this->file) {
return $this->file->appCategory();
} else {
// Hack to use the framework's built-in thumbnail support without creating a local file representation
$tmpFile = new File(array('Name' => $this->Name, 'Filename' => $this->Name));
return $tmpFile->appCategory();
return File::get_app_category($this->getExtension());
}
}
/**
* Get height of this item
*/
public function getHeight() {
if($this->file) {
$height = $this->file->getHeight();
if($height) {
return $height;
}
}
return $this->config()->insert_height;
}
/**
* Get width of this item
*
* @return type
*/
public function getWidth() {
if($this->file) {
$width = $this->file->getWidth();
if($width) {
return $width;
}
}
return $this->config()->insert_width;
}
/**
* Provide an initial width for inserted media, restricted based on $embed_width
*
* @return int
*/
public function getInsertWidth() {
$width = $this->getWidth();
$maxWidth = $this->config()->insert_width;
return ($width <= $maxWidth) ? $width : $maxWidth;
}
/**
* Provide an initial height for inserted media, scaled proportionally to the initial width
*
* @return int
*/
public function getInsertHeight() {
$width = $this->getWidth();
$height = $this->getHeight();
$maxWidth = $this->config()->insert_width;
return ($width <= $maxWidth) ? $height : round($height*($maxWidth/$width));
}
}
/**
@ -845,9 +928,14 @@ class HtmlEditorField_Embed extends HtmlEditorField_File {
'Info' => 'Varchar'
);
/**
* Oembed result
*
* @var Oembed_Result
*/
protected $oembed;
public function __construct($url, $file = null) {
public function __construct($url, File $file = null) {
parent::__construct($url, $file);
$this->oembed = Oembed::get_oembed_from_url($url);
if(!$this->oembed) {
@ -866,41 +954,59 @@ class HtmlEditorField_Embed extends HtmlEditorField_File {
}
}
/**
* Get file-edit fields for this filed
*
* @return FieldList
*/
public function getFields() {
$fields = parent::getFields();
if($this->Type === 'photo') {
$fields->insertBefore('CaptionText', new TextField(
'AltText',
_t('HtmlEditorField.IMAGEALTTEXT', 'Alternative text (alt) - shown if image can\'t be displayed'),
$this->Title,
80
));
$fields->insertBefore('CaptionText', new TextField(
'Title',
_t('HtmlEditorField.IMAGETITLE', 'Title text (tooltip) - for additional information about the image')
));
}
return $fields;
}
/**
* Get width of this oembed
*
* @return int
*/
public function getWidth() {
return $this->oembed->Width ?: 100;
}
/**
* Get height of this oembed
*
* @return int
*/
public function getHeight() {
return $this->oembed->Height ?: 100;
}
/**
* Provide an initial width for inserted media, restricted based on $embed_width
*
* @return int
*/
public function getInsertWidth() {
$width = $this->getWidth();
$maxWidth = Config::inst()->get('HtmlEditorField', 'insert_width');
return ($width <= $maxWidth) ? $width : $maxWidth;
}
/**
* Provide an initial height for inserted media, scaled proportionally to the initial width
*
* @return int
*/
public function getInsertHeight() {
$width = $this->getWidth();
$height = $this->getHeight();
$maxWidth = Config::inst()->get('HtmlEditorField', 'insert_width');
return ($width <= $maxWidth) ? $height : round($height*($maxWidth/$width));
}
public function getPreview() {
if(isset($this->oembed->thumbnail_url)) {
return sprintf('<img src="%s" />', Convert::raw2att($this->oembed->thumbnail_url));
public function getPreviewURL() {
// Use thumbnail url
if(!empty($this->oembed->thumbnail_url)) {
return $this->oembed->thumbnail_url;
}
// Use direct image type
if($this->getType() == 'photo' && !empty($this->Oembed->url)) {
return $this->Oembed->url;
}
// Default media
return FRAMEWORK_DIR . '/images/default_media.png';
}
public function getName() {
@ -911,10 +1017,23 @@ class HtmlEditorField_Embed extends HtmlEditorField_File {
}
}
/**
* Get OEmbed type
*
* @return string
*/
public function getType() {
return $this->oembed->type;
}
public function getFileType() {
return $this->getType()
?: parent::getFileType();
}
/**
* @return Oembed_Result
*/
public function getOembed() {
return $this->oembed;
}
@ -923,6 +1042,11 @@ class HtmlEditorField_Embed extends HtmlEditorField_File {
return 'embed';
}
/**
* Info for this oembed
*
* @return string
*/
public function getInfo() {
return $this->oembed->info;
}
@ -936,14 +1060,37 @@ class HtmlEditorField_Embed extends HtmlEditorField_File {
*/
class HtmlEditorField_Image extends HtmlEditorField_File {
/**
* @var int
*/
protected $width;
/**
* @var int
*/
protected $height;
public function __construct($url, $file = null) {
/**
* File size details
*
* @var string
*/
protected $size;
public function __construct($url, File $file = null) {
parent::__construct($url, $file);
// Get dimensions for remote file
if($file) {
return;
}
// Get size of remote file
$size = @filesize($url);
if($size) {
$this->size = $size;
}
// Get dimensions of remote file
$info = @getimagesize($url);
if($info) {
$this->width = $info[0];
@ -951,12 +1098,117 @@ class HtmlEditorField_Image extends HtmlEditorField_File {
}
}
public function getFields() {
$fields = parent::getFields();
// Alt text
$fields->insertBefore(
'CaptionText',
TextField::create(
'AltText',
_t('HtmlEditorField.IMAGEALT', 'Alternative text (alt)'),
$this->Title,
80
)->setDescription(
_t('HtmlEditorField.IMAGEALTTEXTDESC', 'Shown to screen readers or if image can\'t be displayed')
)
);
// Tooltip
$fields->insertAfter(
'AltText',
TextField::create(
'Title',
_t('HtmlEditorField.IMAGETITLETEXT', 'Title text (tooltip)')
)->setDescription(
_t('HtmlEditorField.IMAGETITLETEXTDESC', 'For additional information about the image')
)
);
return $fields;
}
protected function getDetailFields() {
$fields = parent::getDetailFields();
$width = $this->getOriginalWidth();
$height = $this->getOriginalHeight();
// Show dimensions of original
if($width && $height) {
$fields->insertAfter(
'ClickableURL',
ReadonlyField::create(
"OriginalWidth",
_t('AssetTableField.WIDTH','Width'),
$width
)
);
$fields->insertAfter(
'OriginalWidth',
ReadonlyField::create(
"OriginalHeight",
_t('AssetTableField.HEIGHT','Height'),
$height
)
);
}
return $fields;
}
/**
* Get width of original, if known
*
* @return int
*/
public function getOriginalWidth() {
if($this->width) {
return $this->width;
}
if($this->file) {
$width = $this->file->getWidth();
if($width) {
return $width;
}
}
}
/**
* Get height of original, if known
*
* @return int
*/
public function getOriginalHeight() {
if($this->height) {
return $this->height;
}
if($this->file) {
$height = $this->file->getHeight();
if($height) {
return $height;
}
}
}
public function getWidth() {
return ($this->file) ? $this->file->Width : $this->width;
if($this->width) {
return $this->width;
}
return parent::getWidth();
}
public function getHeight() {
return ($this->file) ? $this->file->Height : $this->height;
if($this->height) {
return $this->height;
}
return parent::getHeight();
}
public function getSize() {
if($this->size) {
return File::format_size($this->size);
}
parent::getSize();
}
/**
@ -966,8 +1218,10 @@ class HtmlEditorField_Image extends HtmlEditorField_File {
*/
public function getInsertWidth() {
$width = $this->getWidth();
$maxWidth = Config::inst()->get('HtmlEditorField', 'insert_width');
return ($width <= $maxWidth) ? $width : $maxWidth;
$maxWidth = $this->config()->insert_width;
return $width <= $maxWidth
? $width
: $maxWidth;
}
/**
@ -978,12 +1232,29 @@ class HtmlEditorField_Image extends HtmlEditorField_File {
public function getInsertHeight() {
$width = $this->getWidth();
$height = $this->getHeight();
$maxWidth = Config::inst()->get('HtmlEditorField', 'insert_width');
$maxWidth = $this->config()->insert_width;
return ($width <= $maxWidth) ? $height : round($height*($maxWidth/$width));
}
public function getPreview() {
return ($this->file) ? $this->file->CMSThumbnail() : sprintf('<img src="%s" />', Convert::raw2att($this->url));
}
public function getPreviewURL() {
// Get preview from file
if($this->file) {
return $this->getFilePreviewURL();
}
// Embed image directly
return $this->url;
}
}
/**
* Generate flash file embed
*/
class HtmlEditorField_Flash extends HtmlEditorField_File {
public function getFields() {
$fields = parent::getFields();
$fields->removeByName('CaptionText', true);
return $fields;
}
}

View File

@ -1,5 +1,7 @@
<?php
use SilverStripe\Filesystem\Storage\AssetContainer;
/**
* Field for uploading single or multiple files of all types, including images.
*
@ -370,7 +372,7 @@ class UploadField extends FileField {
* @param array $value Array of submitted form data, if submitting from a form
* @param array|DataObject|SS_List $record Full source record, either as a DataObject,
* SS_List of items, or an array of submitted form data
* @return UploadField Self reference
* @return $this Self reference
*/
public function setValue($value, $record = null) {
@ -513,10 +515,10 @@ class UploadField extends FileField {
* Customises a file with additional details suitable for rendering in the
* UploadField.ss template
*
* @param File $file
* @param AssetContainer $file
* @return ViewableData_Customised
*/
protected function customiseFile(File $file) {
protected function customiseFile(AssetContainer $file) {
$file = $file->customise(array(
'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file),
'UploadFieldDeleteLink' => $this->getItemHandler($file->ID)->DeleteLink(),
@ -750,11 +752,10 @@ class UploadField extends FileField {
* FieldList $fields for the EditForm
* @example 'getCMSFields'
*
* @param File $file File context to generate fields for
* @param DataObject $file File context to generate fields for
* @return FieldList List of form fields
*/
public function getFileEditFields(File $file) {
public function getFileEditFields(DataObject $file) {
// Empty actions, generate default
if(empty($this->fileEditFields)) {
$fields = $file->getCMSFields();
@ -766,7 +767,9 @@ class UploadField extends FileField {
}
// Fields instance
if ($this->fileEditFields instanceof FieldList) return $this->fileEditFields;
if ($this->fileEditFields instanceof FieldList) {
return $this->fileEditFields;
}
// Method to call on the given file
if($file->hasMethod($this->fileEditFields)) {
@ -792,11 +795,10 @@ class UploadField extends FileField {
* FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
* @example 'getCMSActions'
*
* @param File $file File context to generate form actions for
* @param DataObject $file File context to generate form actions for
* @return FieldList Field list containing FormAction
*/
public function getFileEditActions(File $file) {
public function getFileEditActions(DataObject $file) {
// Empty actions, generate default
if(empty($this->fileEditActions)) {
$actions = new FieldList($saveAction = new FormAction('doEdit', _t('UploadField.DOEDIT', 'Save')));
@ -805,7 +807,9 @@ class UploadField extends FileField {
}
// Actions instance
if ($this->fileEditActions instanceof FieldList) return $this->fileEditActions;
if ($this->fileEditActions instanceof FieldList) {
return $this->fileEditActions;
}
// Method to call on the given file
if($file->hasMethod($this->fileEditActions)) {
@ -831,15 +835,19 @@ class UploadField extends FileField {
* Determines the validator to use for the edit form
* @example 'getCMSValidator'
*
* @param File $file File context to generate validator from
* @param DataObject $file File context to generate validator from
* @return Validator Validator object
*/
public function getFileEditValidator(File $file) {
public function getFileEditValidator(DataObject $file) {
// Empty validator
if(empty($this->fileEditValidator)) return null;
if(empty($this->fileEditValidator)) {
return null;
}
// Validator instance
if($this->fileEditValidator instanceof Validator) return $this->fileEditValidator;
if($this->fileEditValidator instanceof Validator) {
return $this->fileEditValidator;
}
// Method to call on the given file
if($file->hasMethod($this->fileEditValidator)) {
@ -862,24 +870,32 @@ class UploadField extends FileField {
}
/**
* @param File $file
* @return string
*
* @param AssetContainer $file
* @return string URL to thumbnail
*/
protected function getThumbnailURLForFile(File $file) {
if ($file->exists() && file_exists(Director::baseFolder() . '/' . $file->getFilename())) {
$width = $this->getPreviewMaxWidth();
$height = $this->getPreviewMaxHeight();
if ($file->hasMethod('getThumbnail')) {
return $file->getThumbnail($width, $height)->getURL();
} elseif ($file->hasMethod('getThumbnailURL')) {
return $file->getThumbnailURL($width, $height);
} elseif ($file->hasMethod('Fit')) {
return $file->Fit($width, $height)->getURL();
} else {
return $file->Icon();
}
protected function getThumbnailURLForFile(AssetContainer $file) {
if (!$file->exists()) {
return null;
}
// Attempt to generate image at given size
$width = $this->getPreviewMaxWidth();
$height = $this->getPreviewMaxHeight();
if ($file->hasMethod('ThumbnailURL')) {
return $file->ThumbnailURL($width, $height);
}
if ($file->hasMethod('Thumbnail')) {
return $file->Thumbnail($width, $height)->getURL();
}
if ($file->hasMethod('Fit')) {
return $file->Fit($width, $height)->getURL();
}
// Check if unsized icon is available
if($file->hasMethod('getIcon')) {
return $file->getIcon();
}
return false;
}
public function getAttributes() {
@ -890,8 +906,13 @@ class UploadField extends FileField {
}
public function extraClass() {
if($this->isDisabled()) $this->addExtraClass('disabled');
if($this->isReadonly()) $this->addExtraClass('readonly');
if($this->isDisabled()) {
$this->addExtraClass('disabled');
}
if($this->isReadonly()) {
$this->addExtraClass('readonly');
}
return parent::extraClass();
}
@ -970,7 +991,7 @@ class UploadField extends FileField {
$config['errorMessages']['overwriteWarning'] =
_t('UploadField.OVERWRITEWARNING', 'File with the same name already exists');
}
$mergedConfig = array_merge($config, $this->ufConfig);
return $this->customise(array(
'configString' => str_replace('"', "&quot;", Convert::raw2json($mergedConfig)),
@ -1094,10 +1115,9 @@ class UploadField extends FileField {
*
* @param array $tmpFile Temporary file data
* @param string $error Error message
* @return File File object, or null if error
* @return AssetContainer File object, or null if error
*/
protected function saveTemporaryFile($tmpFile, &$error = null) {
// Determine container object
$error = null;
$fileObject = null;
@ -1117,6 +1137,9 @@ class UploadField extends FileField {
if ($relationClass = $this->getRelationAutosetClass(null)) {
// Create new object explicitly. Otherwise rely on Upload::load to choose the class.
$fileObject = Object::create($relationClass);
if(! ($fileObject instanceof DataObject) || !($fileObject instanceof AssetContainer)) {
throw new InvalidArgumentException("Invalid asset container $relationClass");
}
}
// Get the uploaded file into a new file object.
@ -1142,22 +1165,21 @@ class UploadField extends FileField {
* Safely encodes the File object with all standard fields required
* by the front end
*
* @param File $file
* @param AssetContainer $file Object which contains a file
* @return array Array encoded list of file attributes
*/
protected function encodeFileAttributes(File $file) {
protected function encodeFileAttributes(AssetContainer $file) {
// Collect all output data.
$file = $this->customiseFile($file);
$customised = $this->customiseFile($file);
return array(
'id' => $file->ID,
'name' => $file->Name,
'url' => $file->URL,
'thumbnail_url' => $file->UploadFieldThumbnailURL,
'edit_url' => $file->UploadFieldEditLink,
'size' => $file->AbsoluteSize,
'type' => $file->FileType,
'buttons' => $file->UploadFieldFileButtons,
'name' => basename($file->getFilename()),
'url' => $file->getURL(),
'thumbnail_url' => $customised->UploadFieldThumbnailURL,
'edit_url' => $customised->UploadFieldEditLink,
'size' => $file->getAbsoluteSize(),
'type' => File::get_file_type($file->getFilename()),
'buttons' => $customised->UploadFieldFileButtons,
'fieldname' => $this->getName()
);
}
@ -1241,13 +1263,10 @@ class UploadField extends FileField {
// Resolve expected folder name
$folderName = $this->getFolderName();
$folder = Folder::find_or_make($folderName);
$parentPath = $folder
? BASE_PATH."/".$folder->getFilename()
: ASSETS_PATH."/";
$parentPath = $folder ? $folder->getFilename() : '';
// check if either file exists
return file_exists($parentPath.$originalFile)
|| file_exists($parentPath.$filteredFile);
return File::find($parentPath.$originalFile) || File::find($parentPath.$filteredFile);
}
/**
@ -1339,7 +1358,7 @@ class UploadField_ItemHandler extends RequestHandler {
/**
* @param UploadFIeld $parent
* @param int $item
* @param int $itemID
*/
public function __construct($parent, $itemID) {
$this->parent = $parent;

View File

@ -1296,7 +1296,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
'width' : width ? parseInt(width, 10) : null,
'height' : height ? parseInt(height, 10) : null,
'title' : this.find(':input[name=Title]').val(),
'class' : this.find(':input[name=CSSClass]').val()
'class' : this.find(':input[name=CSSClass]').val(),
'data-fileid' : this.find(':input[name=FileID]').val()
};
},
getExtraData: function() {
@ -1379,6 +1380,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
this.find(':input[name=Width]').val(node.width());
this.find(':input[name=Height]').val(node.height());
this.find(':input[name=CaptionText]').val(node.siblings('.caption:first').text());
this.find(':input[name=FileID]').val(node.data('fileid'));
}
});
@ -1394,7 +1396,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
return {
'src' : this.find(':input[name=URL]').val(),
'width' : width ? parseInt(width, 10) : null,
'height' : height ? parseInt(height, 10) : null
'height' : height ? parseInt(height, 10) : null,
'data-fileid' : this.find(':input[name=FileID]').val()
};
},
getHTML: function() {
@ -1431,7 +1434,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
'height' : height ? parseInt(height, 10) : null,
'class' : this.find(':input[name=CSSClass]').val(),
'alt' : this.find(':input[name=AltText]').val(),
'title' : this.find(':input[name=Title]').val()
'title' : this.find(':input[name=Title]').val(),
'data-fileid' : this.find(':input[name=FileID]').val()
};
},
getExtraData: function() {
@ -1471,6 +1475,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
this.find(':input[name=Height]').val(node.height());
this.find(':input[name=Title]').val(node.attr('title'));
this.find(':input[name=CSSClass]').val(node.data('cssclass'));
this.find(':input[name=FileID]').val(node.data('fileid'));
}
});

View File

@ -139,7 +139,9 @@ class DataDifferencer extends ViewableData {
if($this->fromRecord->hasMethod($relName)) {
$relObjFrom = $this->fromRecord->$relName();
if($relObjFrom) {
$fromTitle = ($relObjFrom->hasMethod('Title') || $relObjFrom->hasField('Title')) ? $relObjFrom->Title : '';
$fromTitle = ($relObjFrom->hasMethod('Title') || $relObjFrom->hasField('Title'))
? $relObjFrom->Title
: '';
} else {
$fromTitle = '';
}

View File

@ -353,7 +353,9 @@ class DataQuery {
}
/**
* Execute the query and return the result as {@link Query} object.
* Execute the query and return the result as {@link SS_Query} object.
*
* @return SS_Query
*/
public function execute() {
return $this->getFinalisedQuery()->execute();

View File

@ -6,1120 +6,22 @@
* @package framework
* @subpackage filesystem
*/
class Image extends File implements Flushable {
const ORIENTATION_SQUARE = 0;
const ORIENTATION_PORTRAIT = 1;
const ORIENTATION_LANDSCAPE = 2;
private static $backend = "GDBackend";
private static $casting = array(
'Tag' => 'HTMLText',
);
/**
* @config
* @var int The width of an image thumbnail in a strip.
*/
private static $strip_thumbnail_width = 50;
/**
* @config
* @var int The height of an image thumbnail in a strip.
*/
private static $strip_thumbnail_height = 50;
/**
* @config
* @var int The width of an image thumbnail in the CMS.
*/
private static $cms_thumbnail_width = 100;
/**
* @config
* @var int The height of an image thumbnail in the CMS.
*/
private static $cms_thumbnail_height = 100;
/**
* @config
* @var int The width of an image thumbnail in the Asset section.
*/
private static $asset_thumbnail_width = 100;
/**
* @config
* @var int The height of an image thumbnail in the Asset section.
*/
private static $asset_thumbnail_height = 100;
/**
* @config
* @var int The width of an image preview in the Asset section.
*/
private static $asset_preview_width = 400;
/**
* @config
* @var int The height of an image preview in the Asset section.
*/
private static $asset_preview_height = 200;
/**
* @config
* @var bool Force all images to resample in all cases
*/
private static $force_resample = true;
/**
* @config
* @var bool Regenerates images if set to true. This is set by {@link flush()}
*/
private static $flush = false;
/**
* Triggered early in the request when someone requests a flush.
*/
public static function flush() {
self::$flush = true;
}
public static function set_backend($backend) {
self::config()->backend = $backend;
}
public static function get_backend() {
return self::config()->backend;
}
/**
* Retrieve the original filename from the path of a transformed image.
* Any other filenames pass through unchanged.
*
* @param string $path
* @return string
*/
public static function strip_resampled_prefix($path) {
return preg_replace('/_resampled\/(.+\/|[^-]+-)/', '', $path);
}
/**
* Set up template methods to access the transformations generated by 'generate' methods.
*/
public function defineMethods() {
$methodNames = $this->allMethodNames();
foreach($methodNames as $methodName) {
if(substr($methodName,0,8) == 'generate') {
$this->addWrapperMethod(substr($methodName,8), 'getFormattedImage');
}
}
parent::defineMethods();
class Image extends File {
public function __construct($record = null, $isSingleton = false, $model = null) {
parent::__construct($record, $isSingleton, $model);
$this->File->setAllowedCategories('image/supported');
}
public function getCMSFields() {
$fields = parent::getCMSFields();
$urlLink = "<div class='field readonly'>";
$urlLink .= "<label class='left'>"._t('AssetTableField.URL','URL')."</label>";
$urlLink .= "<span class='readonly'><a href='{$this->Link()}'>{$this->RelativeLink()}</a></span>";
$urlLink .= "</div>";
// todo: check why the above code is here, since $urlLink is not used?
//attach the addition file information for an image to the existing FieldGroup create in the parent class
$fileAttributes = $fields->fieldByName('Root.Main.FilePreview')->fieldByName('FilePreviewData');
$fileAttributes->push(new ReadonlyField("Dimensions", _t('AssetTableField.DIM','Dimensions') . ':'));
$fields->insertAfter(
'LastEdited',
new ReadonlyField("Dimensions", _t('AssetTableField.DIM','Dimensions') . ':')
);
return $fields;
}
/**
* Return an XHTML img tag for this Image,
* or NULL if the image file doesn't exist on the filesystem.
*
* @return string
*/
public function getTag() {
if($this->exists()) {
$url = $this->getURL();
$title = ($this->Title) ? $this->Title : $this->Filename;
if($this->Title) {
$title = Convert::raw2att($this->Title);
} else {
if(preg_match("/([^\/]*)\.[a-zA-Z0-9]{1,6}$/", $title, $matches)) {
$title = Convert::raw2att($matches[1]);
}
}
return "<img src=\"$url\" alt=\"$title\" />";
}
}
/**
* Return an XHTML img tag for this Image.
*
* @return string
*/
public function forTemplate() {
return $this->getTag();
}
/**
* Gets the source image URL for this resource
*
* @return string
*/
public function getSourceURL() {
return parent::getURL();
}
/**
* Gets the relative URL accessible through the web. If forced resampling is enabled
* the URL will point to an optimised file, if it is smaller than the original
*
* @uses Director::baseURL()
* @return string
*/
public function getURL() {
if ($this->config()->force_resample) {
//return $resampled->getURL();
$resampled = $this->Resampled();
if ($resampled !== $this && $resampled->getAbsoluteSize() < $this->getAbsoluteSize()) {
return $resampled->getURL();
}
}
return $this->getSourceURL();
}
/**
* File names are filtered through {@link FileNameFilter}, see class documentation
* on how to influence this behaviour.
*
* @deprecated 4.0
*/
public function loadUploadedImage($tmpFile) {
Deprecation::notice('4.0', 'Use the Upload::loadIntoFile()');
if(!is_array($tmpFile)) {
user_error("Image::loadUploadedImage() Not passed an array. Most likely, the form hasn't got the right"
. "enctype", E_USER_ERROR);
}
if(!$tmpFile['size']) {
return;
}
$class = $this->class;
// Create a folder
if(!file_exists(ASSETS_PATH)) {
mkdir(ASSETS_PATH, Config::inst()->get('Filesystem', 'folder_create_mask'));
}
if(!file_exists(ASSETS_PATH . "/$class")) {
mkdir(ASSETS_PATH . "/$class", Config::inst()->get('Filesystem', 'folder_create_mask'));
}
// Generate default filename
$nameFilter = FileNameFilter::create();
$file = $nameFilter->filter($tmpFile['name']);
if(!$file) $file = "file.jpg";
$file = ASSETS_PATH . "/$class/$file";
while(file_exists(BASE_PATH . "/$file")) {
$i = $i ? ($i+1) : 2;
$oldFile = $file;
$file = preg_replace('/[0-9]*(\.[^.]+$)/', $i . '\\1', $file);
if($oldFile == $file && $i > 2) user_error("Couldn't fix $file with $i", E_USER_ERROR);
}
if(file_exists($tmpFile['tmp_name']) && copy($tmpFile['tmp_name'], BASE_PATH . "/$file")) {
// Remove the old images
$this->deleteFormattedImages();
return true;
}
}
/**
* Scale image proportionally to fit within the specified bounds
*
* @param integer $width The width to size within
* @param integer $height The height to size within
* @return Image
*/
public function Fit($width, $height) {
// Prevent divide by zero on missing/blank file
if(!$this->getWidth() || !$this->getHeight()) return null;
// Check if image is already sized to the correct dimension
$widthRatio = $width / $this->getWidth();
$heightRatio = $height / $this->getHeight();
if( $widthRatio < $heightRatio ) {
// Target is higher aspect ratio than image, so check width
if($this->isWidth($width)) return $this;
} else {
// Target is wider or same aspect ratio as image, so check height
if($this->isHeight($height)) return $this;
}
// Item must be regenerated
return $this->getFormattedImage('Fit', $width, $height);
}
/**
* Scale image proportionally to fit within the specified bounds
*
* @param Image_Backend $backend
* @param integer $width The width to size within
* @param integer $height The height to size within
* @return Image_Backend
*/
public function generateFit(Image_Backend $backend, $width, $height) {
return $backend->resizeRatio($width, $height);
}
/**
* Proportionally scale down this image if it is wider or taller than the specified dimensions.
* Similar to Fit but without up-sampling. Use in templates with $FitMax.
*
* @uses Image::Fit()
* @param integer $width The maximum width of the output image
* @param integer $height The maximum height of the output image
* @return Image
*/
public function FitMax($width, $height) {
return $this->getWidth() > $width || $this->getHeight() > $height
? $this->Fit($width,$height)
: $this;
}
/**
* Resize and crop image to fill specified dimensions.
* Use in templates with $Fill
*
* @param integer $width Width to crop to
* @param integer $height Height to crop to
* @return Image
*/
public function Fill($width, $height) {
return $this->isSize($width, $height)
? $this
: $this->getFormattedImage('Fill', $width, $height);
}
/**
* Resize and crop image to fill specified dimensions.
* Use in templates with $Fill
*
* @param Image_Backend $backend
* @param integer $width Width to crop to
* @param integer $height Height to crop to
* @return Image_Backend
*/
public function generateFill(Image_Backend $backend, $width, $height) {
return $backend->croppedResize($width, $height);
}
/**
* Crop this image to the aspect ratio defined by the specified width and height,
* then scale down the image to those dimensions if it exceeds them.
* Similar to Fill but without up-sampling. Use in templates with $FillMax.
*
* @uses Image::Fill()
* @param integer $width The relative (used to determine aspect ratio) and maximum width of the output image
* @param integer $height The relative (used to determine aspect ratio) and maximum height of the output image
* @return Image
*/
public function FillMax($width, $height) {
// Prevent divide by zero on missing/blank file
if(!$this->getWidth() || !$this->getHeight()) return null;
// Is the image already the correct size?
if ($this->isSize($width, $height)) return $this;
// If not, make sure the image isn't upsampled
$imageRatio = $this->getWidth() / $this->getHeight();
$cropRatio = $width / $height;
// If cropping on the x axis compare heights
if ($cropRatio < $imageRatio && $this->getHeight() < $height) return $this->Fill($this->getHeight()*$cropRatio, $this->getHeight());
// Otherwise we're cropping on the y axis (or not cropping at all) so compare widths
if ($this->getWidth() < $width) return $this->Fill($this->getWidth(), $this->getWidth()/$cropRatio);
return $this->Fill($width, $height);
}
/**
* Fit image to specified dimensions and fill leftover space with a solid colour (default white). Use in templates with $Pad.
*
* @param integer $width The width to size to
* @param integer $height The height to size to
* @return Image
*/
public function Pad($width, $height, $backgroundColor='FFFFFF') {
return $this->isSize($width, $height)
? $this
: $this->getFormattedImage('Pad', $width, $height, $backgroundColor);
}
/**
* Fit image to specified dimensions and fill leftover space with a solid colour (default white). Use in templates with $Pad.
*
* @param Image_Backend $backend
* @param integer $width The width to size to
* @param integer $height The height to size to
* @return Image_Backend
*/
public function generatePad(Image_Backend $backend, $width, $height, $backgroundColor='FFFFFF') {
return $backend->paddedResize($width, $height, $backgroundColor);
}
/**
* Scale image proportionally by width. Use in templates with $ScaleWidth.
*
* @param integer $width The width to set
* @return Image
*/
public function ScaleWidth($width) {
return $this->isWidth($width)
? $this
: $this->getFormattedImage('ScaleWidth', $width);
}
/**
* Scale image proportionally by width. Use in templates with $ScaleWidth.
*
* @param Image_Backend $backend
* @param int $width The width to set
* @return Image_Backend
*/
public function generateScaleWidth(Image_Backend $backend, $width) {
return $backend->resizeByWidth($width);
}
/**
* Proportionally scale down this image if it is wider than the specified width.
* Similar to ScaleWidth but without up-sampling. Use in templates with $ScaleMaxWidth.
*
* @uses Image::ScaleWidth()
* @param integer $width The maximum width of the output image
* @return Image
*/
public function ScaleMaxWidth($width) {
return $this->getWidth() > $width
? $this->ScaleWidth($width)
: $this;
}
/**
* Scale image proportionally by height. Use in templates with $ScaleHeight.
*
* @param integer $height The height to set
* @return Image
*/
public function ScaleHeight($height) {
return $this->isHeight($height)
? $this
: $this->getFormattedImage('ScaleHeight', $height);
}
/**
* Scale image proportionally by height. Use in templates with $ScaleHeight.
*
* @param Image_Backend $backend
* @param integer $height The height to set
* @return Image_Backend
*/
public function generateScaleHeight(Image_Backend $backend, $height){
return $backend->resizeByHeight($height);
}
/**
* Proportionally scale down this image if it is taller than the specified height.
* Similar to ScaleHeight but without up-sampling. Use in templates with $ScaleMaxHeight.
*
* @uses Image::ScaleHeight()
* @param integer $height The maximum height of the output image
* @return Image
*/
public function ScaleMaxHeight($height) {
return $this->getHeight() > $height
? $this->ScaleHeight($height)
: $this;
}
/**
* Crop image on X axis if it exceeds specified width. Retain height.
* Use in templates with $CropWidth. Example: $Image.ScaleHeight(100).$CropWidth(100)
*
* @uses Image::Fill()
* @param integer $width The maximum width of the output image
* @return Image
*/
public function CropWidth($width) {
return $this->getWidth() > $width
? $this->Fill($width, $this->getHeight())
: $this;
}
/**
* Crop image on Y axis if it exceeds specified height. Retain width.
* Use in templates with $CropHeight. Example: $Image.ScaleWidth(100).CropHeight(100)
*
* @uses Image::Fill()
* @param integer $height The maximum height of the output image
* @return Image
*/
public function CropHeight($height) {
return $this->getHeight() > $height
? $this->Fill($this->getWidth(), $height)
: $this;
}
/**
* Resample this Image to ensure quality preference is applied.
* Warning: it's possible this will produce a larger file of lower quality
*
* @return Image
*/
public function Resampled() {
return $this->getFormattedImage('Resampled');
}
/**
* Resample this Image to apply quality preference
*
* @param Image_Backend $backend
* @return Image_Backend
*/
public function generateResampled(Image_Backend $backend){
return $backend;
}
/**
* Resize the image by preserving aspect ratio, keeping the image inside the
* $width and $height
*
* @param integer $width The width to size within
* @param integer $height The height to size within
* @return Image
* @deprecated 4.0 Use Fit instead
*/
public function SetRatioSize($width, $height) {
Deprecation::notice('4.0', 'Use Fit instead');
return $this->Fit($width, $height);
}
/**
* Resize the image by preserving aspect ratio, keeping the image inside the
* $width and $height
*
* @param Image_Backend $backend
* @param integer $width The width to size within
* @param integer $height The height to size within
* @return Image_Backend
* @deprecated 4.0 Use generateFit instead
*/
public function generateSetRatioSize(Image_Backend $backend, $width, $height) {
Deprecation::notice('4.0', 'Use generateFit instead');
return $backend->resizeRatio($width, $height);
}
/**
* Resize this Image by width, keeping aspect ratio. Use in templates with $SetWidth.
*
* @param integer $width The width to set
* @return Image
* @deprecated 4.0 Use ScaleWidth instead
*/
public function SetWidth($width) {
Deprecation::notice('4.0', 'Use ScaleWidth instead');
return $this->ScaleWidth($width);
}
/**
* Resize this Image by width, keeping aspect ratio. Use in templates with $SetWidth.
*
* @param Image_Backend $backend
* @param int $width The width to set
* @return Image_Backend
* @deprecated 4.0 Use generateScaleWidth instead
*/
public function generateSetWidth(Image_Backend $backend, $width) {
Deprecation::notice('4.0', 'Use generateScaleWidth instead');
return $backend->resizeByWidth($width);
}
/**
* Resize this Image by height, keeping aspect ratio. Use in templates with $SetHeight.
*
* @param integer $height The height to set
* @return Image
* @deprecated 4.0 Use ScaleHeight instead
*/
public function SetHeight($height) {
Deprecation::notice('4.0', 'Use ScaleHeight instead');
return $this->ScaleHeight($height);
}
/**
* Resize this Image by height, keeping aspect ratio. Use in templates with $SetHeight.
*
* @param Image_Backend $backend
* @param integer $height The height to set
* @return Image_Backend
* @deprecated 4.0 Use generateScaleHeight instead
*/
public function generateSetHeight(Image_Backend $backend, $height){
Deprecation::notice('4.0', 'Use generateScaleHeight instead');
return $backend->resizeByHeight($height);
}
/**
* Resize this Image by both width and height, using padded resize. Use in templates with $SetSize.
* @see Image::PaddedImage()
*
* @param integer $width The width to size to
* @param integer $height The height to size to
* @return Image
* @deprecated 4.0 Use Pad instead
*/
public function SetSize($width, $height) {
Deprecation::notice('4.0', 'Use Pad instead');
return $this->Pad($width, $height);
}
/**
* Resize this Image by both width and height, using padded resize. Use in templates with $SetSize.
*
* @param Image_Backend $backend
* @param integer $width The width to size to
* @param integer $height The height to size to
* @return Image_Backend
* @deprecated 4.0 Use generatePad instead
*/
public function generateSetSize(Image_Backend $backend, $width, $height) {
Deprecation::notice('4.0', 'Use generatePad instead');
return $backend->paddedResize($width, $height);
}
public function CMSThumbnail() {
return $this->getFormattedImage('CMSThumbnail');
}
/**
* Resize this image for the CMS. Use in templates with $CMSThumbnail.
* @return Image_Backend
*/
public function generateCMSThumbnail(Image_Backend $backend) {
return $backend->paddedResize($this->stat('cms_thumbnail_width'),$this->stat('cms_thumbnail_height'));
}
/**
* Resize this image for preview in the Asset section. Use in templates with $AssetLibraryPreview.
* @return Image_Backend
*/
public function generateAssetLibraryPreview(Image_Backend $backend) {
return $backend->paddedResize($this->stat('asset_preview_width'),$this->stat('asset_preview_height'));
}
/**
* Resize this image for thumbnail in the Asset section. Use in templates with $AssetLibraryThumbnail.
* @return Image_Backend
*/
public function generateAssetLibraryThumbnail(Image_Backend $backend) {
return $backend->paddedResize($this->stat('asset_thumbnail_width'),$this->stat('asset_thumbnail_height'));
}
/**
* Resize this image for use as a thumbnail in a strip. Use in templates with $StripThumbnail.
* @return Image_Backend
*/
public function generateStripThumbnail(Image_Backend $backend) {
return $backend->croppedResize($this->stat('strip_thumbnail_width'),$this->stat('strip_thumbnail_height'));
}
/**
* Resize this Image by both width and height, using padded resize. Use in templates with $PaddedImage.
* @see Image::SetSize()
*
* @param integer $width The width to size to
* @param integer $height The height to size to
* @return Image
* @deprecated 4.0 Use Pad instead
*/
public function PaddedImage($width, $height, $backgroundColor='FFFFFF') {
Deprecation::notice('4.0', 'Use Pad instead');
return $this->Pad($width, $height, $backgroundColor);
}
/**
* Resize this Image by both width and height, using padded resize. Use in templates with $PaddedImage.
*
* @param Image_Backend $backend
* @param integer $width The width to size to
* @param integer $height The height to size to
* @return Image_Backend
* @deprecated 4.0 Use generatePad instead
*/
public function generatePaddedImage(Image_Backend $backend, $width, $height, $backgroundColor='FFFFFF') {
Deprecation::notice('4.0', 'Use generatePad instead');
return $backend->paddedResize($width, $height, $backgroundColor);
}
/**
* Determine if this image is of the specified size
*
* @param integer $width Width to check
* @param integer $height Height to check
* @return boolean
*/
public function isSize($width, $height) {
return $this->isWidth($width) && $this->isHeight($height);
}
/**
* Determine if this image is of the specified width
*
* @param integer $width Width to check
* @return boolean
*/
public function isWidth($width) {
return !empty($width) && $this->getWidth() == $width;
}
/**
* Determine if this image is of the specified width
*
* @param integer $height Height to check
* @return boolean
*/
public function isHeight($height) {
return !empty($height) && $this->getHeight() == $height;
}
/**
* Return an image object representing the image in the given format.
* This image will be generated using generateFormattedImage().
* The generated image is cached, to flush the cache append ?flush=1 to your URL.
*
* Just pass the correct number of parameters expected by the working function
*
* @param string $format The name of the format.
* @return Image_Cached
*/
public function getFormattedImage($format) {
$args = func_get_args();
if($this->exists()) {
$cacheFile = call_user_func_array(array($this, "cacheFilename"), $args);
if(!file_exists(Director::baseFolder()."/".$cacheFile) || self::$flush) {
call_user_func_array(array($this, "generateFormattedImage"), $args);
}
$cached = new Image_Cached($cacheFile, false, $this);
return $cached;
}
}
/**
* Return the filename for the cached image, given its format name and arguments.
* @param string $format The format name.
* @return string
* @throws InvalidArgumentException
*/
public function cacheFilename($format) {
$args = func_get_args();
array_shift($args);
// Note: $folder holds the *original* file, while the Image we're working with
// may be a formatted image in a child directory (this happens when we're chaining formats)
$folder = $this->ParentID ? $this->Parent()->Filename : ASSETS_DIR . "/";
$format = $format . Convert::base64url_encode($args);
$filename = $format . "/" . $this->Name;
$pattern = $this->getFilenamePatterns($this->Name);
// Any previous formats need to be derived from this Image's directory, and prepended to the new filename
$prepend = array();
if(($pos = stripos($this->Filename, '_resampled')) !== false ) {
$candidate = substr($this->Filename, $pos + strlen('_resampled'));
preg_match_all($pattern['GeneratorPattern'], $candidate, $matches, PREG_SET_ORDER);
foreach($matches as $formatdir) {
$prepend[] = $formatdir[0];
}
}
$filename = implode($prepend) . $filename;
if (!preg_match($pattern['FullPattern'], $filename)) {
throw new InvalidArgumentException('Filename ' . $filename
. ' that should be used to cache a resized image is invalid');
}
return $folder . "_resampled/" . $filename;
}
/**
* Generate an image on the specified format. It will save the image
* at the location specified by cacheFilename(). The image will be generated
* using the specific 'generate' method for the specified format.
*
* @param string $format Name of the format to generate.
*/
public function generateFormattedImage($format) {
$args = func_get_args();
$cacheFile = call_user_func_array(array($this, "cacheFilename"), $args);
$backend = Injector::inst()->createWithArgs(self::config()->backend, array(
Director::baseFolder()."/" . $this->Filename,
$args
));
if($backend->hasImageResource()) {
$generateFunc = "generate$format";
if($this->hasMethod($generateFunc)){
array_shift($args);
array_unshift($args, $backend);
$backend = call_user_func_array(array($this, $generateFunc), $args);
if($backend){
$backend->writeTo(Director::baseFolder()."/" . $cacheFile);
}
} else {
user_error("Image::generateFormattedImage - Image $format public function not found.",E_USER_WARNING);
}
}
}
/**
* Generate a resized copy of this image with the given width & height.
* This can be used in templates with $ResizedImage but should be avoided,
* as it's the only image manipulation function which can skew an image.
*
* @param integer $width Width to resize to
* @param integer $height Height to resize to
* @return Image
*/
public function ResizedImage($width, $height) {
return $this->isSize($width, $height)
? $this
: $this->getFormattedImage('ResizedImage', $width, $height);
}
/**
* Generate a resized copy of this image with the given width & height.
* Use in templates with $ResizedImage.
*
* @param Image_Backend $backend
* @param integer $width Width to resize to
* @param integer $height Height to resize to
* @return Image_Backend
*/
public function generateResizedImage(Image_Backend $backend, $width, $height) {
if(!$backend){
user_error("Image::generateFormattedImage - generateResizedImage is being called by legacy code"
. " or Image::\$backend is not set.",E_USER_WARNING);
}else{
return $backend->resize($width, $height);
}
}
/**
* Generate a resized copy of this image with the given width & height, cropping to maintain aspect ratio.
* Use in templates with $CroppedImage
*
* @param integer $width Width to crop to
* @param integer $height Height to crop to
* @return Image
* @deprecated 4.0 Use Fill instead
*/
public function CroppedImage($width, $height) {
Deprecation::notice('4.0', 'Use Fill instead');
return $this->Fill($width, $height);
}
/**
* Generate a resized copy of this image with the given width & height, cropping to maintain aspect ratio.
* Use in templates with $CroppedImage
*
* @param Image_Backend $backend
* @param integer $width Width to crop to
* @param integer $height Height to crop to
* @return Image_Backend
* @deprecated 4.0 Use generateFill instead
*/
public function generateCroppedImage(Image_Backend $backend, $width, $height) {
Deprecation::notice('4.0', 'Use generateFill instead');
return $backend->croppedResize($width, $height);
}
/**
* Generate patterns that will help to match filenames of cached images
* @param string $filename Filename of source image
* @return array
*/
private function getFilenamePatterns($filename) {
$methodNames = $this->allMethodNames(true);
$generateFuncs = array();
foreach($methodNames as $methodName) {
if(substr($methodName, 0, 8) == 'generate') {
$format = substr($methodName, 8);
$generateFuncs[] = preg_quote($format);
}
}
// All generate functions may appear any number of times in the image cache name.
$generateFuncs = implode('|', $generateFuncs);
$base64url_match = "[a-zA-Z0-9_~]*={0,2}";
return array(
'FullPattern' => "/^((?P<Generator>{$generateFuncs})(?P<Args>" . $base64url_match . ")\/)+"
. preg_quote($filename) . "$/i",
'GeneratorPattern' => "/(?P<Generator>{$generateFuncs})(?P<Args>" . $base64url_match . ")\//i"
);
}
/**
* Generate a list of images that were generated from this image
*
* @return array
*/
protected function getGeneratedImages() {
$generatedImages = array();
$cachedFiles = array();
$folder = $this->ParentID ? $this->Parent()->Filename : ASSETS_DIR . '/';
$cacheDir = Director::getAbsFile($folder . '_resampled/');
// Find all paths with the same filename as this Image (the path contains the transformation info)
if(is_dir($cacheDir)) {
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($cacheDir));
foreach($files as $path => $file){
if ($file->getFilename() == $this->Name) {
$cachedFiles[] = $path;
}
}
}
$pattern = $this->getFilenamePatterns($this->Name);
// Reconstruct the image transformation(s) from the format-folder(s) in the path
// (if chained, they contain the transformations in the correct order)
foreach($cachedFiles as $cf_path) {
if(($pos = stripos($cf_path, '_resampled')) === false ) {
continue;
}
$cf_generated = substr($cf_path, $pos + strlen('_resampled'));
preg_match_all($pattern['GeneratorPattern'], $cf_generated, $matches, PREG_SET_ORDER);
$generatorArray = array();
foreach ($matches as $singleMatch) {
$generatorArray[] = array(
'Generator' => $singleMatch['Generator'],
'Args' => Convert::base64url_decode($singleMatch['Args']) ?: array()
);
}
$generatedImages[] = array(
'FileName' => $cf_path,
'Generators' => $generatorArray
);
}
return $generatedImages;
}
/**
* Regenerate all of the formatted cached images for this image.
*
* @return int The number of formatted images regenerated
*/
public function regenerateFormattedImages() {
if(!$this->Filename) return 0;
// Without this, not a single file would be written
// caused by a check in getFormattedImage()
$this->flush();
$numGenerated = 0;
$generatedImages = $this->getGeneratedImages();
$doneList = array();
foreach($generatedImages as $singleImage) {
$cachedImage = $this;
if (in_array($singleImage['FileName'], $doneList) ) continue;
foreach($singleImage['Generators'] as $singleGenerator) {
$args = array_merge(array($singleGenerator['Generator']), $singleGenerator['Args']);
$cachedImage = call_user_func_array(array($cachedImage, "getFormattedImage"), $args);
}
$doneList[] = $singleImage['FileName'];
$numGenerated++;
}
return $numGenerated;
}
/**
* Remove all of the formatted cached images for this image.
*
* @return int The number of formatted images deleted
*/
public function deleteFormattedImages() {
if(!$this->Filename) return 0;
$numDeleted = 0;
$generatedImages = $this->getGeneratedImages();
foreach($generatedImages as $singleImage) {
$path = $singleImage['FileName'];
unlink($path);
$numDeleted++;
do {
$path = dirname($path);
}
// remove the folder if it's empty (and it's not the assets folder)
while(!preg_match('/assets$/', $path) && Filesystem::remove_folder_if_empty($path));
}
return $numDeleted;
}
/**
* Get the dimensions of this Image.
* @param string $dim If this is equal to "string", return the dimensions in string form,
* if it is 0 return the height, if it is 1 return the width.
* @return string|int
*/
public function getDimensions($dim = "string") {
if($this->getField('Filename')) {
$imagefile = $this->getFullPath();
if($this->exists()) {
$size = getimagesize($imagefile);
return ($dim === "string") ? "$size[0]x$size[1]" : $size[$dim];
} else {
return ($dim === "string") ? "file '$imagefile' not found" : null;
}
}
}
/**
* Get the width of this image.
* @return int
*/
public function getWidth() {
return $this->getDimensions(0);
}
/**
* Get the height of this image.
* @return int
*/
public function getHeight() {
return $this->getDimensions(1);
}
/**
* Get the orientation of this image.
* @return ORIENTATION_SQUARE | ORIENTATION_PORTRAIT | ORIENTATION_LANDSCAPE
*/
public function getOrientation() {
$width = $this->getWidth();
$height = $this->getHeight();
if($width > $height) {
return self::ORIENTATION_LANDSCAPE;
} elseif($height > $width) {
return self::ORIENTATION_PORTRAIT;
} else {
return self::ORIENTATION_SQUARE;
}
}
public function onAfterUpload() {
$this->deleteFormattedImages();
parent::onAfterUpload();
}
protected function onBeforeDelete() {
$backend = Injector::inst()->create(self::get_backend());
$backend->onBeforeDelete($this);
$this->deleteFormattedImages();
parent::onBeforeDelete();
}
}
/**
* A resized / processed {@link Image} object.
* When Image object are processed or resized, a suitable Image_Cached object is returned, pointing to the
* cached copy of the processed image.
*
* @package framework
* @subpackage filesystem
*/
class Image_Cached extends Image {
/**
* Create a new cached image.
* @param string $filename The filename of the image.
* @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
* Singletons don't have their defaults set.
*/
public function __construct($filename = null, $isSingleton = false, Image $sourceImage = null) {
parent::__construct(array(), $isSingleton);
if ($sourceImage) $this->update($sourceImage->toMap());
$this->ID = -1;
$this->Filename = $filename;
}
public function getURL() {
return $this->getSourceURL();
}
/**
* Override the parent's exists method becuase the ID is explicitly set to -1 on a cached image we can't use the
* default check
*
* @return bool Whether the cached image exists
*/
public function exists() {
return file_exists($this->getFullPath());
}
public function getRelativePath() {
return $this->getField('Filename');
}
/**
* Prevent creating new tables for the cached record
*
* @return false
*/
public function requireTable() {
return false;
}
/**
* Prevent writing the cached image to the database
*
* @throws Exception
*/
public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
throw new Exception("{$this->ClassName} can not be written back to the database.");
}
/**
* Resampled images don't need subsequent resampling
*
* @param $this
*/
public function Resampled() {
return $this;
public function getIsImage() {
return true;
}
}

View File

@ -1,4 +1,8 @@
<?php
use SilverStripe\Filesystem\Storage\AssetContainer;
use SilverStripe\Filesystem\Storage\AssetStore;
/**
* Image_Backend
*
@ -10,122 +14,120 @@
interface Image_Backend {
/**
* __construct
*
* @param string $filename = null
* @param array $args = array()
* @return void
* Represents a square orientation
*/
public function __construct($filename = null, $args = array());
const ORIENTATION_SQUARE = 0;
/**
* writeTo
* Represents a portrait orientation
*/
const ORIENTATION_PORTRAIT = 1;
/**
* Represents a landscape orientation
*/
const ORIENTATION_LANDSCAPE = 2;
/**
* Create a new backend with the given object
*
* @param AssetContainer $assetContainer Object to load from
*/
public function __construct(AssetContainer $assetContainer = null);
/**
* Populate the backend with a given object
*
* @param AssetContainer $assetContainer Object to load from
*/
public function loadFromContainer(AssetContainer $assetContainer);
/**
* Populate the backend from a local path
*
* @param string $path
* @return void
*/
public function loadFrom($path);
/**
* Write to the given asset store
*
* @param AssetStore $assetStore
* @param string $filename Name for the resulting file
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param string $conflictResolution {@see AssetStore}. Will default to one chosen by the backend
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the given data.
*/
public function writeToStore(AssetStore $assetStore, $filename, $hash = null, $variant = null, $conflictResolution = null);
/**
* Write the backend to a local path
*
* @param string $path
*/
public function writeTo($path);
/**
* setQuality
* Set the quality to a value between 0 and 100
*
* @param int $quality
* @return void
*/
public function setQuality($quality);
/**
* setImageResource
*
* Set the backend-specific resource handling the manipulations. Replaces Image::setGD()
*
* @param mixed $resource
* @return void
*/
public function setImageResource($resource);
/**
* getImageResource
*
* Get the backend-specific resource handling the manipulations. Replaces Image::getGD()
*
* @return mixed
*/
public function getImageResource();
/**
* hasImageResource
*
* @return boolean
*/
public function hasImageResource();
/**
* resize
* Resize an image, skewing it as necessary.
*
* @param int $width
* @param int $height
* @return Image_Backend
* @return static
*/
public function resize($width, $height);
/**
* resizeRatio
* Resize the image by preserving aspect ratio. By default, it will keep the image inside the maxWidth
* and maxHeight. Passing useAsMinimum will make the smaller dimension equal to the maximum corresponding dimension
*
* @param int $width
* @param int $height
* @return Image_Backend
* @param bool $useAsMinimum If true, image will be sized outside of these dimensions.
* If false (default) image will be sized inside these dimensions.
* @return static
*/
public function resizeRatio($maxWidth, $maxHeight, $useAsMinimum = false);
public function resizeRatio($width, $height, $useAsMinimum = false);
/**
* resizeByWidth
* Resize an image by width. Preserves aspect ratio.
*
* @param int $width
* @return Image_Backend
* @return static
*/
public function resizeByWidth($width);
/**
* resizeByHeight
* Resize an image by height. Preserves aspect ratio.
*
* @param int $height
* @return Image_Backend
* @return static
*/
public function resizeByHeight($height);
/**
* paddedResize
* Return a clone of this image resized, with space filled in with the given colour
*
* @param int $width
* @param int $height
* @return Image_Backend
* @return static
*/
public function paddedResize($width, $height, $backgroundColor = "FFFFFF");
/**
* croppedResize
* Resize an image to cover the given width/height completely, and crop off any overhanging edges.
*
* @param int $width
* @param int $height
* @return Image_Backend
* @return static
*/
public function croppedResize($width, $height);
/**
* imageAvailable
*
* @param string $filename
* @return boolean
*/
public function imageAvailable($filename, $manipulation);
/**
* onBeforeDelete
*
* @param Image $frontend
* @return void
*/
public function onBeforeDelete($frontend);
}

View File

@ -128,10 +128,9 @@ abstract class CompositeDBField extends DBField {
* If $record is assigned to a dataobject, this field becomes a loose wrapper over
* the records on that object instead.
*
* @param type $value
* @param mixed $value
* @param DataObject $record
* @param type $markChanged
* @return type
* @param bool $markChanged
*/
public function setValue($value, $record = null, $markChanged = true) {
$this->isChanged = $markChanged;

View File

@ -1,202 +0,0 @@
<?php
use SilverStripe\Filesystem\Storage\AssetContainer;
use SilverStripe\Filesystem\Storage\AssetStore;
/**
* Represents a file reference stored in a database
*
* @property string $Hash SHA of the file
* @property string $Filename Name of the file, including directory
* @property string $Variant Variant of the file
*
* @package framework
* @subpackage model
*/
class DBFile extends CompositeDBField implements AssetContainer {
/**
* @return AssetStore
*/
protected function getStore() {
return Injector::inst()->get('AssetStore');
}
/**
* Mapping of mime patterns to templates to use
*/
private static $templates = array(
'/image\\/.+/' => 'DBFile_image',
'/.+/' => 'DBFile_download'
);
private static $composite_db = array(
"Hash" => "Varchar(255)", // SHA of the base content
"Filename" => "Varchar(255)", // Path identifier of the base content
"Variant" => "Varchar(255)", // Identifier of the variant to the base, if given
);
private static $casting = array(
'URL' => 'Varchar',
'AbsoluteURL' => 'Varchar',
'Basename' => 'Varchar',
'Title' => 'Varchar',
'MimeType' => 'Varchar',
'String' => 'Text',
'Tag' => 'HTMLText'
);
public function scaffoldFormField($title = null, $params = null) {
// @todo - This doesn't actually work with DBFile yet
return new UploadField($this->getName(), $title);
}
/**
* Return a html5 tag of the appropriate for this file (normally img or a)
*
* @return string
*/
public function forTemplate() {
return $this->getTag() ?: '';
}
/**
* Return a html5 tag of the appropriate for this file (normally img or a)
*
* @return string
*/
public function getTag() {
// Check mime type
$mime = $this->getMimeType();
if(empty($mime)) {
return '';
}
// Check that path is available
$url = $this->getURL();
if(empty($url)) {
return '';
}
$template = $this->getTemplateForMime($mime);
if(empty($template)) {
return '';
}
// Render
return (string)$this->renderWith($template);
}
/**
* Given a mime type, determine the template to render as on the frontend
*
* @param string $mimetype
* @return string Name of template
*/
protected function getTemplateForMime($mimetype) {
foreach($this->config()->templates as $pattern => $template) {
if($pattern === $mimetype || preg_match($pattern, $mimetype)) {
return $template;
}
}
return null;
}
/**
* Get trailing part of filename
*
* @return string
*/
public function getBasename() {
// @todo - add variant onto this ?
if($this->Filename) {
return basename($this->Filename);
}
}
/**
* Alt title for this
*
* @return string
*/
public function getTitle() {
// @todo - better solution?
return $this->getBasename();
}
public function setFromLocalFile($path, $filename = null, $conflictResolution = null) {
$result = $this
->getStore()
->setFromLocalFile($path, $filename, $conflictResolution);
// Update from result
if($result) {
$this->setValue($result);
}
return $result;
}
public function setFromStream($stream, $filename, $conflictResolution = null) {
$result = $this
->getStore()
->setFromStream($stream, $filename, $conflictResolution);
// Update from result
if($result) {
$this->setValue($result);
}
return $result;
}
public function setFromString($data, $filename, $conflictResolution = null) {
$result = $this
->getStore()
->setFromString($data, $filename, $conflictResolution);
// Update from result
if($result) {
$this->setValue($result);
}
return $result;
}
public function getStream() {
return $this
->getStore()
->getAsStream($this->Hash, $this->Filename, $this->Variant);
}
public function getString() {
return $this
->getStore()
->getAsString($this->Hash, $this->Filename, $this->Variant);
}
public function getURL() {
return $this
->getStore()
->getAsURL($this->Hash, $this->Filename, $this->Variant);
}
/**
* Get the absolute URL to this resource
*
* @return type
*/
public function getAbsoluteURL() {
return Director::absoluteURL($this->getURL());
}
public function getMetaData() {
return $this
->getStore()
->getMetadata($this->Hash, $this->Filename, $this->Variant);
}
public function getMimeType() {
return $this
->getStore()
->getMimeType($this->Hash, $this->Filename, $this->Variant);
}
public function exists() {
return !empty($this->Filename);
}
}

View File

@ -32,26 +32,29 @@ class ForeignKey extends Int {
}
$relationName = substr($this->name,0,-2);
$hasOneClass = $this->object->hasOneComponent($relationName);
if($hasOneClass && singleton($hasOneClass) instanceof Image) {
if(empty($hasOneClass)) {
return null;
}
$hasOneSingleton = singleton($hasOneClass);
if($hasOneSingleton instanceof File) {
$field = new UploadField($relationName, $title);
$field->getValidator()->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif'));
} elseif($hasOneClass && singleton($hasOneClass) instanceof File) {
$field = new UploadField($relationName, $title);
} else {
$titleField = (singleton($hasOneClass)->hasField('Title')) ? "Title" : "Name";
$list = DataList::create($hasOneClass);
// Don't scaffold a dropdown for large tables, as making the list concrete
// might exceed the available PHP memory in creating too many DataObject instances
if($list->count() < 100) {
$field = new DropdownField($this->name, $title, $list->map('ID', $titleField));
$field->setEmptyString(' ');
} else {
$field = new NumericField($this->name, $title);
if($hasOneSingleton instanceof Image) {
$field->setAllowedFileCategories('image/supported');
}
return $field;
}
// Build selector / numeric field
$titleField = $hasOneSingleton->hasField('Title') ? "Title" : "Name";
$list = DataList::create($hasOneClass);
// Don't scaffold a dropdown for large tables, as making the list concrete
// might exceed the available PHP memory in creating too many DataObject instances
if($list->count() < 100) {
$field = new DropdownField($this->name, $title, $list->map('ID', $titleField));
$field->setEmptyString(' ');
} else {
$field = new NumericField($this->name, $title);
}
return $field;
}

View File

@ -21,7 +21,7 @@
* @subpackage oembed
*/
class Oembed {
class Oembed implements ShortcodeHandler {
public static function is_enabled() {
return Config::inst()->get('Oembed', 'enabled');
@ -179,23 +179,35 @@ class Oembed {
return false;
}
public static function handle_shortcode($arguments, $url, $parser, $shortcode) {
public static function get_shortcodes() {
return 'embed';
}
public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = array()) {
if(isset($arguments['type'])) {
$type = $arguments['type'];
unset($arguments['type']);
} else {
$type = false;
}
$oembed = self::get_oembed_from_url($url, $type, $arguments);
$oembed = self::get_oembed_from_url($content, $type, $arguments);
if($oembed && $oembed->exists()) {
return $oembed->forTemplate();
} else {
return '<a href="' . $url . '">' . $url . '</a>';
return '<a href="' . $content . '">' . $content . '</a>';
}
}
}
/**
* @property string $Type Oembed type
* @property string $Title Title
* @property string $URL URL to asset
* @property string $Provider_URL Url for provider
* @property int $Width
* @property int $Height
* @property string $Info Descriptive text for this oembed
*
* @package framework
* @subpackage oembed
*/

View File

@ -0,0 +1,19 @@
<?php
/**
* Abstract interface for a class which handles shortcodes
*/
interface ShortcodeHandler {
/**
* Generate content with a shortcode value
*
* @param array $arguments Arguments passed to the parser
* @param string $content Raw shortcode
* @param ShortcodeParser $parser Parser
* @param string $shortcode Name of shortcode used to register this handler
* @param array $extra Extra arguments
* @return string Result of the handled shortcode
*/
public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = array());
}

View File

@ -72,9 +72,15 @@ class ShortcodeParser extends Object {
*
* @param string $shortcode The shortcode tag to map to the callback - normally in lowercase_underscore format.
* @param callback $callback The callback to replace the shortcode with.
* @return $this
*/
public function register($shortcode, $callback) {
if(is_callable($callback)) $this->shortcodes[$shortcode] = $callback;
if(is_callable($callback)) {
$this->shortcodes[$shortcode] = $callback;
} else {
throw new InvalidArgumentException("Callback is not callable");
}
return $this;
}
/**

View File

@ -1,59 +0,0 @@
<?php
/**
* Wipe the cache of failed image manipulations. When {@link GDBackend} attempts to resample an image, it will write
* the attempted manipulation to the cache and remove it from the cache if the resample is successful. The objective
* of the cache is to prevent fatal errors (for example from exceeded memory limits) from occurring more than once.
*
* @package framework
* @subpackage filesystem
*/
class CleanImageManipulationCache extends BuildTask {
protected $title = 'Clean Image Manipulation Cache';
protected $description = 'Clean the failed image manipulation cache. Use this to allow SilverStripe to attempt
to resample images that have previously failed to resample (for example if memory limits were exceeded).';
/**
* Check that the user has appropriate permissions to execute this task
*/
public function init() {
if(!Director::is_cli() && !Director::isDev() && !Permission::check('ADMIN')) {
return Security::permissionFailure();
}
parent::init();
}
/**
* Clear out the image manipulation cache
* @param SS_HTTPRequest $request
*/
public function run($request) {
$failedManipulations = 0;
$processedImages = 0;
$images = DataObject::get('Image');
if($images && Image::get_backend() == "GDBackend") {
$cache = SS_Cache::factory('GDBackend_Manipulations');
foreach($images as $image) {
$path = $image->getFullPath();
if (file_exists($path)) {
$key = md5(implode('_', array($path, filemtime($path))));
if ($manipulations = unserialize($cache->load($key))) {
$failedManipulations += count($manipulations);
$processedImages++;
$cache->remove($key);
}
}
}
}
echo "Cleared $failedManipulations failed manipulations from
$processedImages Image objects stored in the Database.";
}
}

23
tasks/MigrateFileTask.php Normal file
View File

@ -0,0 +1,23 @@
<?php
/**
* Migrates all 3.x file dataobjects to use the new DBFile field.
*
* @package framework
* @subpackage filesystem
*/
class MigrateFileTask extends BuildTask {
protected $title = 'Migrate File dataobjects from 3.x';
protected $description
= 'Imports all files referenced by File dataobjects into the new Asset Persistence Layer introduced in 4.0';
public function run($request) {
$migrated = FileMigrationHelper::singleton()->run();
if($migrated) {
DB::alteration_message("{$migrated} File DataObjects upgraded", "changed");
}
}
}

View File

@ -581,6 +581,6 @@ class ControllerTest_SubController extends Controller implements TestOnly {
}
class ControllerTest_SubController_Exception extends Exception {
class ControllerTest_SubController_Exception extends Exception implements TestOnly {
}

View File

@ -2,10 +2,10 @@
use Filesystem as SS_Filesystem;
use League\Flysystem\Filesystem;
use League\Flysystem\Util;
use SilverStripe\Filesystem\Flysystem\AssetAdapter;
use SilverStripe\Filesystem\Flysystem\FlysystemAssetStore;
use SilverStripe\Filesystem\Flysystem\FlysystemUrlPlugin;
use SilverStripe\Filesystem\Storage\AssetContainer;
use SilverStripe\Filesystem\Storage\AssetStore;
class AssetStoreTest extends SapphireTest {
@ -13,30 +13,17 @@ class AssetStoreTest extends SapphireTest {
public function setUp() {
parent::setUp();
// Set backend
$adapter = new AssetAdapter(ASSETS_PATH . '/DBFileTest');
$filesystem = new Filesystem($adapter);
$filesystem->addPlugin(new FlysystemUrlPlugin());
$backend = new AssetStoreTest_SpyStore();
$backend->setFilesystem($filesystem);
Injector::inst()->registerService($backend, 'AssetStore');
// Disable legacy
Config::inst()->remove(get_class(new FlysystemAssetStore()), 'legacy_filenames');
AssetStoreTest_SpyStore::$seekable_override = null;
// Update base url
Config::inst()->update('Director', 'alternate_base_url', '/mysite/');
// Set backend and base url
AssetStoreTest_SpyStore::activate('DBFileTest');
}
public function tearDown() {
SS_Filesystem::removeFolder(ASSETS_PATH . '/DBFileTest');
AssetStoreTest_SpyStore::$seekable_override = null;
AssetStoreTest_SpyStore::reset();
parent::tearDown();
}
/**
* @return AssetStore
* @return AssetStoreTest_SpyStore
*/
protected function getBackend() {
return Injector::inst()->get('AssetStore');
@ -61,7 +48,7 @@ class AssetStoreTest extends SapphireTest {
);
// Test setFromStream (seekable)
$fish1 = realpath(__DIR__ .'/../model/testimages/test_image_high-quality.jpg');
$fish1 = realpath(__DIR__ .'/../model/testimages/test-image-high-quality.jpg');
$fish1Stream = fopen($fish1, 'r');
$fish1Tuple = $backend->setFromStream($fish1Stream, 'parent/awesome-fish.jpg');
fclose($fish1Stream);
@ -76,7 +63,7 @@ class AssetStoreTest extends SapphireTest {
// Test with non-seekable streams
AssetStoreTest_SpyStore::$seekable_override = false;
$fish2 = realpath(__DIR__ .'/../model/testimages/test_image_low-quality.jpg');
$fish2 = realpath(__DIR__ .'/../model/testimages/test-image-low-quality.jpg');
$fish2Stream = fopen($fish2, 'r');
$fish2Tuple = $backend->setFromStream($fish2Stream, 'parent/mediocre-fish.jpg');
fclose($fish2Stream);
@ -99,7 +86,7 @@ class AssetStoreTest extends SapphireTest {
$backend = $this->getBackend();
// Put a file in
$fish1 = realpath(__DIR__ .'/../model/testimages/test_image_high-quality.jpg');
$fish1 = realpath(__DIR__ .'/../model/testimages/test-image-high-quality.jpg');
$this->assertFileExists($fish1);
$fish1Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish.jpg');
$this->assertEquals(
@ -111,14 +98,14 @@ class AssetStoreTest extends SapphireTest {
$fish1Tuple
);
$this->assertEquals(
'/mysite/assets/DBFileTest/directory/a870de278b/lovely-fish.jpg',
$backend->getAsURL($fish1Tuple['Hash'], $fish1Tuple['Filename'])
'/assets/DBFileTest/directory/a870de278b/lovely-fish.jpg',
$backend->getAsURL($fish1Tuple['Filename'], $fish1Tuple['Hash'])
);
// Write a different file with same name. Should not detect duplicates since sha are different
$fish2 = realpath(__DIR__ .'/../model/testimages/test_image_low-quality.jpg');
$fish2 = realpath(__DIR__ .'/../model/testimages/test-image-low-quality.jpg');
try {
$fish2Tuple = $backend->setFromLocalFile($fish2, 'directory/lovely-fish.jpg', AssetStore::CONFLICT_EXCEPTION);
$fish2Tuple = $backend->setFromLocalFile($fish2, 'directory/lovely-fish.jpg', null, null, AssetStore::CONFLICT_EXCEPTION);
} catch(Exception $ex) {
return $this->fail('Writing file with different sha to same location failed with exception');
}
@ -131,13 +118,13 @@ class AssetStoreTest extends SapphireTest {
$fish2Tuple
);
$this->assertEquals(
'/mysite/assets/DBFileTest/directory/33be1b95cb/lovely-fish.jpg',
$backend->getAsURL($fish2Tuple['Hash'], $fish2Tuple['Filename'])
'/assets/DBFileTest/directory/33be1b95cb/lovely-fish.jpg',
$backend->getAsURL($fish2Tuple['Filename'], $fish2Tuple['Hash'])
);
// Write original file back with rename
$this->assertFileExists($fish1);
$fish3Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish.jpg', AssetStore::CONFLICT_RENAME);
$fish3Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish.jpg', null, null, AssetStore::CONFLICT_RENAME);
$this->assertEquals(
array(
'Hash' => 'a870de278b475cb75f5d9f451439b2d378e13af1',
@ -147,12 +134,12 @@ class AssetStoreTest extends SapphireTest {
$fish3Tuple
);
$this->assertEquals(
'/mysite/assets/DBFileTest/directory/a870de278b/lovely-fish-v2.jpg',
$backend->getAsURL($fish3Tuple['Hash'], $fish3Tuple['Filename'])
'/assets/DBFileTest/directory/a870de278b/lovely-fish-v2.jpg',
$backend->getAsURL($fish3Tuple['Filename'], $fish3Tuple['Hash'])
);
// Write another file should increment to -v3
$fish4Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish-v2.jpg', AssetStore::CONFLICT_RENAME);
$fish4Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish-v2.jpg', null, null, AssetStore::CONFLICT_RENAME);
$this->assertEquals(
array(
'Hash' => 'a870de278b475cb75f5d9f451439b2d378e13af1',
@ -162,12 +149,12 @@ class AssetStoreTest extends SapphireTest {
$fish4Tuple
);
$this->assertEquals(
'/mysite/assets/DBFileTest/directory/a870de278b/lovely-fish-v3.jpg',
$backend->getAsURL($fish4Tuple['Hash'], $fish4Tuple['Filename'])
'/assets/DBFileTest/directory/a870de278b/lovely-fish-v3.jpg',
$backend->getAsURL($fish4Tuple['Filename'], $fish4Tuple['Hash'])
);
// Test conflict use existing file
$fish5Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish.jpg', AssetStore::CONFLICT_USE_EXISTING);
$fish5Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish.jpg', null, null, AssetStore::CONFLICT_USE_EXISTING);
$this->assertEquals(
array(
'Hash' => 'a870de278b475cb75f5d9f451439b2d378e13af1',
@ -177,12 +164,12 @@ class AssetStoreTest extends SapphireTest {
$fish5Tuple
);
$this->assertEquals(
'/mysite/assets/DBFileTest/directory/a870de278b/lovely-fish.jpg',
$backend->getAsURL($fish5Tuple['Hash'], $fish5Tuple['Filename'])
'/assets/DBFileTest/directory/a870de278b/lovely-fish.jpg',
$backend->getAsURL($fish5Tuple['Filename'], $fish5Tuple['Hash'])
);
// Test conflict use existing file
$fish6Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish.jpg', AssetStore::CONFLICT_OVERWRITE);
$fish6Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish.jpg', null, null, AssetStore::CONFLICT_OVERWRITE);
$this->assertEquals(
array(
'Hash' => 'a870de278b475cb75f5d9f451439b2d378e13af1',
@ -192,8 +179,8 @@ class AssetStoreTest extends SapphireTest {
$fish6Tuple
);
$this->assertEquals(
'/mysite/assets/DBFileTest/directory/a870de278b/lovely-fish.jpg',
$backend->getAsURL($fish6Tuple['Hash'], $fish6Tuple['Filename'])
'/assets/DBFileTest/directory/a870de278b/lovely-fish.jpg',
$backend->getAsURL($fish6Tuple['Filename'], $fish6Tuple['Hash'])
);
}
@ -251,27 +238,27 @@ class AssetStoreTest extends SapphireTest {
$store = new AssetStoreTest_SpyStore();
$this->assertEquals(
'directory/2a17a9cb4b/file.jpg',
$store->getFileID(sha1('puppies'), 'directory/file.jpg')
$store->getFileID('directory/file.jpg', sha1('puppies'))
);
$this->assertEquals(
'2a17a9cb4b/file.jpg',
$store->getFileID(sha1('puppies'), 'file.jpg')
$store->getFileID('file.jpg', sha1('puppies'))
);
$this->assertEquals(
'dir_ectory/2a17a9cb4b/fil_e.jpg',
$store->getFileID(sha1('puppies'), 'dir__ectory/fil__e.jpg')
$store->getFileID('dir__ectory/fil__e.jpg', sha1('puppies'))
);
$this->assertEquals(
'directory/2a17a9cb4b/file_variant.jpg',
$store->getFileID(sha1('puppies'), 'directory/file__variant.jpg', null)
$store->getFileID('directory/file__variant.jpg', sha1('puppies'), null)
);
$this->assertEquals(
'directory/2a17a9cb4b/file__variant.jpg',
$store->getFileID(sha1('puppies'), 'directory/file.jpg', 'variant')
$store->getFileID('directory/file.jpg', sha1('puppies'), 'variant')
);
$this->assertEquals(
'2a17a9cb4b/file__var__iant.jpg',
$store->getFileID(sha1('puppies'), 'file.jpg', 'var__iant')
$store->getFileID('file.jpg', sha1('puppies'), 'var__iant')
);
}
@ -279,13 +266,13 @@ class AssetStoreTest extends SapphireTest {
$backend = $this->getBackend();
// jpg
$fish = realpath(__DIR__ .'/../model/testimages/test_image_high-quality.jpg');
$fish = realpath(__DIR__ .'/../model/testimages/test-image-high-quality.jpg');
$fishTuple = $backend->setFromLocalFile($fish, 'parent/awesome-fish.jpg');
$this->assertEquals(
'image/jpeg',
$backend->getMimeType($fishTuple['Hash'], $fishTuple['Filename'])
$backend->getMimeType($fishTuple['Filename'], $fishTuple['Hash'])
);
$fishMeta = $backend->getMetadata($fishTuple['Hash'], $fishTuple['Filename']);
$fishMeta = $backend->getMetadata($fishTuple['Filename'], $fishTuple['Hash']);
$this->assertEquals(151889, $fishMeta['size']);
$this->assertEquals('file', $fishMeta['type']);
$this->assertNotEmpty($fishMeta['timestamp']);
@ -296,9 +283,9 @@ class AssetStoreTest extends SapphireTest {
$puppiesTuple = $backend->setFromString($puppies, 'pets/my-puppy.txt');
$this->assertEquals(
'text/plain',
$backend->getMimeType($puppiesTuple['Hash'], $puppiesTuple['Filename'])
$backend->getMimeType($puppiesTuple['Filename'], $puppiesTuple['Hash'])
);
$puppiesMeta = $backend->getMetadata($puppiesTuple['Hash'], $puppiesTuple['Filename']);
$puppiesMeta = $backend->getMetadata($puppiesTuple['Filename'], $puppiesTuple['Hash']);
$this->assertEquals(7, $puppiesMeta['size']);
$this->assertEquals('file', $puppiesMeta['type']);
$this->assertNotEmpty($puppiesMeta['timestamp']);
@ -313,7 +300,7 @@ class AssetStoreTest extends SapphireTest {
$backend = $this->getBackend();
// Put a file in
$fish1 = realpath(__DIR__ .'/../model/testimages/test_image_high-quality.jpg');
$fish1 = realpath(__DIR__ .'/../model/testimages/test-image-high-quality.jpg');
$this->assertFileExists($fish1);
$fish1Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish.jpg');
$this->assertEquals(
@ -325,22 +312,22 @@ class AssetStoreTest extends SapphireTest {
$fish1Tuple
);
$this->assertEquals(
'/mysite/assets/DBFileTest/directory/lovely-fish.jpg',
$backend->getAsURL($fish1Tuple['Hash'], $fish1Tuple['Filename'])
'/assets/DBFileTest/directory/lovely-fish.jpg',
$backend->getAsURL($fish1Tuple['Filename'], $fish1Tuple['Hash'])
);
// Write a different file with same name.
// Since we are using legacy filenames, this should generate a new filename
$fish2 = realpath(__DIR__ .'/../model/testimages/test_image_low-quality.jpg');
$fish2 = realpath(__DIR__ .'/../model/testimages/test-image-low-quality.jpg');
try {
$backend->setFromLocalFile($fish2, 'directory/lovely-fish.jpg', AssetStore::CONFLICT_EXCEPTION);
$backend->setFromLocalFile($fish2, 'directory/lovely-fish.jpg', null, null, AssetStore::CONFLICT_EXCEPTION);
return $this->fail('Writing file with different sha to same location should throw exception');
} catch(Exception $ex) {
// Success
}
// Re-attempt this file write with conflict_rename
$fish3Tuple = $backend->setFromLocalFile($fish2, 'directory/lovely-fish.jpg', AssetStore::CONFLICT_RENAME);
$fish3Tuple = $backend->setFromLocalFile($fish2, 'directory/lovely-fish.jpg', null, null, AssetStore::CONFLICT_RENAME);
$this->assertEquals(
array(
'Hash' => '33be1b95cba0358fe54e8b13532162d52f97421c',
@ -350,12 +337,12 @@ class AssetStoreTest extends SapphireTest {
$fish3Tuple
);
$this->assertEquals(
'/mysite/assets/DBFileTest/directory/lovely-fish-v2.jpg',
$backend->getAsURL($fish3Tuple['Hash'], $fish3Tuple['Filename'])
'/assets/DBFileTest/directory/lovely-fish-v2.jpg',
$backend->getAsURL($fish3Tuple['Filename'], $fish3Tuple['Hash'])
);
// Write back original file, but with CONFLICT_EXISTING. The file should not change
$fish4Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish-v2.jpg', AssetStore::CONFLICT_USE_EXISTING);
$fish4Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish-v2.jpg', null, null, AssetStore::CONFLICT_USE_EXISTING);
$this->assertEquals(
array(
'Hash' => '33be1b95cba0358fe54e8b13532162d52f97421c',
@ -365,12 +352,12 @@ class AssetStoreTest extends SapphireTest {
$fish4Tuple
);
$this->assertEquals(
'/mysite/assets/DBFileTest/directory/lovely-fish-v2.jpg',
$backend->getAsURL($fish4Tuple['Hash'], $fish4Tuple['Filename'])
'/assets/DBFileTest/directory/lovely-fish-v2.jpg',
$backend->getAsURL($fish4Tuple['Filename'], $fish4Tuple['Hash'])
);
// Write back original file with CONFLICT_OVERWRITE. The file sha should now be updated
$fish5Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish-v2.jpg', AssetStore::CONFLICT_OVERWRITE);
$fish5Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish-v2.jpg', null, null, AssetStore::CONFLICT_OVERWRITE);
$this->assertEquals(
array(
'Hash' => 'a870de278b475cb75f5d9f451439b2d378e13af1',
@ -380,10 +367,27 @@ class AssetStoreTest extends SapphireTest {
$fish5Tuple
);
$this->assertEquals(
'/mysite/assets/DBFileTest/directory/lovely-fish-v2.jpg',
$backend->getAsURL($fish5Tuple['Hash'], $fish5Tuple['Filename'])
'/assets/DBFileTest/directory/lovely-fish-v2.jpg',
$backend->getAsURL($fish5Tuple['Filename'], $fish5Tuple['Hash'])
);
}
/**
* Test default conflict resolution
*/
public function testDefaultConflictResolution() {
$store = $this->getBackend();
// Disable legacy filenames
Config::inst()->update(get_class(new FlysystemAssetStore()), 'legacy_filenames', false);
$this->assertEquals(AssetStore::CONFLICT_OVERWRITE, $store->getDefaultConflictResolution(null));
$this->assertEquals(AssetStore::CONFLICT_OVERWRITE, $store->getDefaultConflictResolution('somevariant'));
// Enable legacy filenames
Config::inst()->update(get_class(new FlysystemAssetStore()), 'legacy_filenames', true);
$this->assertEquals(AssetStore::CONFLICT_RENAME, $store->getDefaultConflictResolution(null));
$this->assertEquals(AssetStore::CONFLICT_OVERWRITE, $store->getDefaultConflictResolution('somevariant'));
}
}
/**
@ -393,23 +397,87 @@ class AssetStoreTest_SpyStore extends FlysystemAssetStore {
/**
* Set to true|false to override all isSeekableStream calls
*
*
* @var null|bool
*/
public static $seekable_override = null;
/**
* Base dir of current file
*
* @var string
*/
public static $basedir = null;
/**
* Set this store as the new asset backend
*
* @param string $basedir Basedir to store assets, which will be placed beneath 'assets' folder
*/
public static function activate($basedir) {
// Assign this as the new store
$adapter = new AssetAdapter(ASSETS_PATH . '/' . $basedir);
$filesystem = new Filesystem($adapter);
$filesystem->addPlugin(new FlysystemUrlPlugin());
$backend = new AssetStoreTest_SpyStore();
$backend->setFilesystem($filesystem);
Injector::inst()->registerService($backend, 'AssetStore');
// Disable legacy and set defaults
Config::inst()->remove(get_class(new FlysystemAssetStore()), 'legacy_filenames');
Config::inst()->update('Director', 'alternate_base_url', '/');
DBFile::config()->force_resample = false;
File::config()->force_resample = false;
self::reset();
self::$basedir = $basedir;
// Ensure basedir exists
SS_Filesystem::makeFolder(ASSETS_PATH . '/' . self::$basedir);
}
/**
* Reset defaults for this store
*/
public static function reset() {
if(self::$basedir) {
$path = ASSETS_PATH . '/' . self::$basedir;
if(file_exists($path)) {
SS_Filesystem::removeFolder($path);
}
}
self::$seekable_override = null;
self::$basedir = null;
}
/**
* Helper method to get local filesystem path for this file
*
* @param AssetContainer $asset
*/
public static function getLocalPath(AssetContainer $asset) {
if($asset instanceof Folder) {
return ASSETS_PATH . '/' . self::$basedir . '/' . $asset->getFilename();
}
return BASE_PATH . $asset->getUrl();
}
public function cleanFilename($filename) {
return parent::cleanFilename($filename);
}
public function getFileID($hash, $filename, $variant = null) {
return parent::getFileID($hash, $filename, $variant);
public function getFileID($filename, $hash, $variant = null) {
return parent::getFileID($filename, $hash, $variant);
}
public function getOriginalFilename($fileID, &$variant = '') {
return parent::getOriginalFilename($fileID, $variant);
}
public function getDefaultConflictResolution($variant) {
return parent::getDefaultConflictResolution($variant);
}
protected function isSeekableStream($stream) {
if(isset(self::$seekable_override)) {
return self::$seekable_override;

View File

@ -0,0 +1,100 @@
<?php
use SilverStripe\Filesystem\Storage\DefaultAssetNameGenerator;
/**
* covers {@see DefaultAssetNameGenerator}
*/
class DefaultAssetNameGeneratorTest extends SapphireTest {
/**
* Test non-prefix behaviour
*/
public function testWithoutPrefix() {
Config::inst()->update('SilverStripe\Filesystem\Storage\DefaultAssetNameGenerator', 'version_prefix', '');
$generator = new DefaultAssetNameGenerator('folder/MyFile-001.jpg');
$suggestions = iterator_to_array($generator);
// Expect 100 suggestions
$this->assertEquals(100, count($suggestions));
// First item is always the same as input
$this->assertEquals('folder/MyFile-001.jpg', $suggestions[0]);
// Check that padding is respected
$this->assertEquals('folder/MyFile-002.jpg', $suggestions[1]);
$this->assertEquals('folder/MyFile-003.jpg', $suggestions[2]);
$this->assertEquals('folder/MyFile-004.jpg', $suggestions[3]);
$this->assertEquals('folder/MyFile-021.jpg', $suggestions[20]);
$this->assertEquals('folder/MyFile-099.jpg', $suggestions[98]);
// Last item should be some semi-random string, not in the same numeric sequence
$this->assertNotEquals('folder/MyFile-0100.jpg', $suggestions[99]);
$this->assertNotEquals('folder/MyFile-100.jpg', $suggestions[99]);
// Test with a value starting above 1
$generator = new DefaultAssetNameGenerator('folder/MyFile-024.jpg');
$suggestions = iterator_to_array($generator);
$this->assertEquals(100, count($suggestions));
$this->assertEquals('folder/MyFile-024.jpg', $suggestions[0]);
$this->assertEquals('folder/MyFile-025.jpg', $suggestions[1]);
$this->assertEquals('folder/MyFile-026.jpg', $suggestions[2]);
$this->assertEquals('folder/MyFile-048.jpg', $suggestions[24]);
$this->assertEquals('folder/MyFile-122.jpg', $suggestions[98]);
$this->assertNotEquals('folder/MyFile-0123.jpg', $suggestions[99]);
$this->assertNotEquals('folder/MyFile-123.jpg', $suggestions[99]); // Last suggestion is semi-random
// Test without numeric value
$generator = new DefaultAssetNameGenerator('folder/MyFile.jpg');
$suggestions = iterator_to_array($generator);
$this->assertEquals(100, count($suggestions));
$this->assertEquals('folder/MyFile.jpg', $suggestions[0]);
$this->assertEquals('folder/MyFile2.jpg', $suggestions[1]);
$this->assertEquals('folder/MyFile3.jpg', $suggestions[2]);
$this->assertEquals('folder/MyFile25.jpg', $suggestions[24]);
$this->assertEquals('folder/MyFile99.jpg', $suggestions[98]);
$this->assertNotEquals('folder/MyFile100.jpg', $suggestions[99]); // Last suggestion is semi-random
}
/**
* Test with default -v prefix
*/
public function testWithDefaultPrefix() {
Config::inst()->update('SilverStripe\Filesystem\Storage\DefaultAssetNameGenerator', 'version_prefix', '-v');
// Test with item that doesn't contain the prefix
$generator = new DefaultAssetNameGenerator('folder/MyFile-001.jpg');
$suggestions = iterator_to_array($generator);
$this->assertEquals(100, count($suggestions));
$this->assertEquals('folder/MyFile-001.jpg', $suggestions[0]);
$this->assertEquals('folder/MyFile-001-v2.jpg', $suggestions[1]);
$this->assertEquals('folder/MyFile-001-v4.jpg', $suggestions[3]);
$this->assertEquals('folder/MyFile-001-v21.jpg', $suggestions[20]);
$this->assertEquals('folder/MyFile-001-v99.jpg', $suggestions[98]);
$this->assertNotEquals('folder/MyFile-001-v100.jpg', $suggestions[99]); // Last suggestion is semi-random
// Test with item that contains prefix
$generator = new DefaultAssetNameGenerator('folder/MyFile-v24.jpg');
$suggestions = iterator_to_array($generator);
$this->assertEquals(100, count($suggestions));
$this->assertEquals('folder/MyFile-v24.jpg', $suggestions[0]);
$this->assertEquals('folder/MyFile-v25.jpg', $suggestions[1]);
$this->assertEquals('folder/MyFile-v26.jpg', $suggestions[2]);
$this->assertEquals('folder/MyFile-v48.jpg', $suggestions[24]);
$this->assertEquals('folder/MyFile-v122.jpg', $suggestions[98]);
$this->assertNotEquals('folder/MyFile-v123.jpg', $suggestions[99]);
$this->assertNotEquals('folder/MyFile-123.jpg', $suggestions[99]);
// Test without numeric value
$generator = new DefaultAssetNameGenerator('folder/MyFile.jpg');
$suggestions = iterator_to_array($generator);
$this->assertEquals(100, count($suggestions));
$this->assertEquals('folder/MyFile.jpg', $suggestions[0]);
$this->assertEquals('folder/MyFile-v2.jpg', $suggestions[1]);
$this->assertEquals('folder/MyFile-v3.jpg', $suggestions[2]);
$this->assertEquals('folder/MyFile-v25.jpg', $suggestions[24]);
$this->assertEquals('folder/MyFile-v99.jpg', $suggestions[98]);
$this->assertNotEquals('folder/MyFile-v100.jpg', $suggestions[99]);
}
}

View File

@ -0,0 +1,97 @@
<?php
use Filesystem as SS_Filesystem;
/**
* Ensures that File dataobjects can be safely migrated from 3.x
*/
class FileMigrationHelperTest extends SapphireTest {
protected static $fixture_file = 'FileMigrationHelperTest.yml';
protected $requiredExtensions = array(
"File" => array(
"FileMigrationHelperTest_Extension"
)
);
/**
* get the BASE_PATH for this test
*
* @return string
*/
protected function getBasePath() {
return ASSETS_PATH . '/FileMigrationHelperTest';
}
public function setUp() {
Config::nest(); // additional nesting here necessary
Config::inst()->update('File', 'migrate_legacy_file', false);
parent::setUp();
// Set backend root to /FileMigrationHelperTest/assets
AssetStoreTest_SpyStore::activate('FileMigrationHelperTest/assets');
// Ensure that each file has a local record file in this new assets base
$from = FRAMEWORK_PATH . '/tests/model/testimages/test-image-low-quality.jpg';
foreach(File::get()->exclude('ClassName', 'Folder') as $file) {
$dest = $this->getBasePath() . '/assets/' . $file->getFilename();
SS_Filesystem::makeFolder(dirname($dest));
copy($from, $dest);
}
}
public function tearDown() {
AssetStoreTest_SpyStore::reset();
SS_Filesystem::removeFolder($this->getBasePath());
parent::tearDown();
Config::unnest();
}
/**
* Test file migration
*/
public function testMigration() {
// Prior to migration, check that each file has empty Filename / Hash properties
foreach(File::get()->exclude('ClassName', 'Folder') as $file) {
$filename = $file->getFilename();
$this->assertNotEmpty($filename, "File {$file->Name} has a filename");
$this->assertEmpty($file->File->getFilename(), "File {$file->Name} has no DBFile filename");
$this->assertEmpty($file->File->getHash(), "File {$file->Name} has no hash");
$this->assertFalse($file->exists(), "File with name {$file->Name} does not yet exist");
}
// Do migration
$helper = new FileMigrationHelper();
$result = $helper->run($this->getBasePath());
$this->assertEquals(5, $result);
// Test that each file exists
foreach(File::get()->exclude('ClassName', 'Folder') as $file) {
$filename = $file->File->getFilename();
$this->assertNotEmpty($filename, "File {$file->Name} has a Filename");
$this->assertEquals(
'33be1b95cba0358fe54e8b13532162d52f97421c',
$file->File->getHash(),
"File with name {$filename} has the correct hash"
);
$this->assertTrue($file->exists(), "File with name {$filename} exists");
}
}
}
class FileMigrationHelperTest_Extension extends DataExtension implements TestOnly {
/**
* Ensure that File dataobject has the legacy "Filename" field
*/
private static $db = array(
"Filename" => "Text",
);
public function onBeforeWrite() {
// Ensure underlying filename field is written to the database
$this->owner->setField('Filename', 'assets/' . $this->owner->getFilename());
}
}

View File

@ -0,0 +1,21 @@
Folder:
parent:
Name: ParentFolder
subfolder:
Name: SubFolder
Parent: =>Folder.parent
Image:
image1:
Name: myimage.jpg
image2:
Name: myimage.jpg
ParentID: =>Folder.subfolder
File:
file1:
Name: anotherfile.jpg
file2:
Name: file.jpg
ParentID: =>Folder.parent
file3:
Name: picture.jpg
ParentID: =>Folder.subfolder

View File

@ -1,5 +1,7 @@
<?php
use Filesystem as SS_Filesystem;
/**
* Tests for the File class
*/
@ -9,11 +11,56 @@ class FileTest extends SapphireTest {
protected $extraDataObjects = array('FileTest_MyCustomFile');
public function setUp() {
parent::setUp();
// Set backend root to /ImageTest
AssetStoreTest_SpyStore::activate('FileTest');
// 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);
$filePath = ASSETS_PATH . '/FileTest/' . $folder->getFilename();
SS_Filesystem::makeFolder($filePath);
}
// 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);
$root = ASSETS_PATH . '/FileTest/';
if($folder = $file->Parent()) {
$root .= $folder->getFilename();
}
$path = $root . substr($file->getHash(), 0, 10) . '/' . basename($file->getFilename());
SS_Filesystem::makeFolder(dirname($path));
$fh = fopen($path, "w+");
fwrite($fh, str_repeat('x', 1000000));
fclose($fh);
}
// Conditional fixture creation in case the 'cms' module is installed
if(class_exists('ErrorPage')) {
$page = new ErrorPage(array(
'Title' => 'Page not Found',
'ErrorCode' => 404
));
$page->write();
$page->publish('Stage', 'Live');
}
}
public function tearDown() {
AssetStoreTest_SpyStore::reset();
parent::tearDown();
}
public function testLinkShortcodeHandler() {
$testFile = $this->objFromFixture('File', 'asdf');
$parser = new ShortcodeParser();
$parser->register('file_link', array('File', 'link_shortcode_handler'));
$parser->register('file_link', array('File', 'handle_shortcode'));
$fileShortcode = sprintf('[file_link,id=%d]', $testFile->ID);
$fileEnclosed = sprintf('[file_link,id=%d]Example Content[/file_link]', $testFile->ID);
@ -57,28 +104,33 @@ class FileTest extends SapphireTest {
// 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::find_or_make('/FileTest/');
$testfilePath = 'assets/FileTest/CreateWithFilenameHasCorrectPath.txt'; // Important: No leading slash
$fh = fopen(BASE_PATH . '/' . $testfilePath, "w");
$testfilePath = BASE_PATH . '/assets/FileTest/CreateWithFilenameHasCorrectPath.txt'; // Important: No leading slash
$fh = fopen($testfilePath, "w");
fwrite($fh, str_repeat('x',1000000));
fclose($fh);
$file = new File();
$file->Filename = $testfilePath;
// TODO This should be auto-detected
$file->setFromLocalFile($testfilePath);
$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');
$this->assertEquals(
'CreateWithFilenameHasCorrectPath.txt',
$file->Name,
'"Name" property is automatically set from "Filename"'
);
$this->assertEquals(
'FileTest/CreateWithFilenameHasCorrectPath.txt',
$file->Filename,
'"Filename" property remains unchanged'
);
// TODO This should be auto-detected, see File->updateFilesystem()
// $this->assertInstanceOf('Folder', $file->Parent(), 'Parent folder is created in database');
// $this->assertFileExists($file->Parent()->getFullPath(), 'Parent folder is created on filesystem');
// $this->assertFileExists($file->Parent()->getURL(), 'Parent folder is created on filesystem');
// $this->assertEquals('FileTest', $file->Parent()->Name);
// $this->assertInstanceOf('Folder', $file->Parent()->Parent(), 'Grandparent folder is created in database');
// $this->assertFileExists($file->Parent()->Parent()->getFullPath(),
// $this->assertFileExists($file->Parent()->Parent()->getURL(),
// 'Grandparent folder is created on filesystem');
// $this->assertEquals('assets', $file->Parent()->Parent()->Name);
}
@ -121,47 +173,99 @@ class FileTest extends SapphireTest {
Config::inst()->update('File', 'allowed_extensions', $orig);
}
public function testAppCategory() {
// Test various categories
$this->assertEquals('image', File::get_app_category('jpg'));
$this->assertEquals('image', File::get_app_category('JPG'));
$this->assertEquals('image', File::get_app_category('JPEG'));
$this->assertEquals('image', File::get_app_category('png'));
$this->assertEquals('image', File::get_app_category('tif'));
$this->assertEquals('document', File::get_app_category('pdf'));
$this->assertEquals('video', File::get_app_category('mov'));
$this->assertEquals('audio', File::get_app_category('OGG'));
}
public function testGetCategoryExtensions() {
// Test specific categories
$images = array(
'alpha', 'als', 'bmp', 'cel', 'gif', 'ico', 'icon', 'jpeg', 'jpg', 'pcx', 'png', 'ps', 'tif', 'tiff'
);
$this->assertEquals($images, File::get_category_extensions('image'));
$this->assertEquals(array('gif', 'jpeg', 'jpg', 'png'), File::get_category_extensions('image/supported'));
$this->assertEquals($images, File::get_category_extensions(array('image', 'image/supported')));
$this->assertEquals(
array('fla', 'gif', 'jpeg', 'jpg', 'png', 'swf'),
File::get_category_extensions(array('flash', 'image/supported'))
);
// Test other categories have at least one item
$this->assertNotEmpty(File::get_category_extensions('archive'));
$this->assertNotEmpty(File::get_category_extensions('audio'));
$this->assertNotEmpty(File::get_category_extensions('document'));
$this->assertNotEmpty(File::get_category_extensions('flash'));
$this->assertNotEmpty(File::get_category_extensions('video'));
}
/**
* @dataProvider allowedExtensions
* @param string $extension
*/
public function testAllFilesHaveCategory($extension) {
$this->assertNotEmpty(
File::get_app_category($extension),
"Assert that extension {$extension} has a valid category"
);
}
/**
* Gets the list of all extensions for testing
*
* @return array
*/
public function allowedExtensions() {
$args = array();
foreach(array_filter(File::config()->allowed_extensions) as $ext) {
$args[] = array($ext);
}
return $args;
}
public function testSetNameChangesFilesystemOnWrite() {
$file = $this->objFromFixture('File', 'asdf');
$oldPath = $file->getFullPath();
$oldPath = AssetStoreTest_SpyStore::getLocalPath($file);
$newPath = str_replace('FileTest.txt', 'renamed.txt', $oldPath);
// 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');
$this->assertFileExists($oldPath, 'Old path is still present');
$this->assertFileNotExists($newPath, '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()');
$this->assertFileExists($oldPath, 'Old path is left after write()');
$this->assertFileExists($newPath, 'New path is created after write()');
}
public function testSetParentIDChangesFilesystemOnWrite() {
$file = $this->objFromFixture('File', 'asdf');
$subfolder = $this->objFromFixture('Folder', 'subfolder');
$oldPath = $file->getFullPath();
$oldPath = AssetStoreTest_SpyStore::getLocalPath($file);
$newPath = str_replace('assets/FileTest/', 'assets/FileTest/FileTest-subfolder/', $oldPath);
// 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');
$this->assertFileExists($oldPath, 'Old path is still present');
$this->assertFileNotExists($newPath, 'New path is updated in memory, not written before write() is called');
$this->assertEquals($oldPath, AssetStoreTest_SpyStore::getLocalPath($file), 'URL is not updated until 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()');
$this->assertFileExists($oldPath, 'Old path is left after write()');
$this->assertFileExists($newPath, 'New path is created after write()');
$this->assertEquals($newPath, AssetStoreTest_SpyStore::getLocalPath($file), 'URL is updated after write is called');
}
/**
@ -175,7 +279,7 @@ class FileTest extends SapphireTest {
Config::inst()->update('File', 'allowed_extensions', array('txt'));
$file = $this->objFromFixture('File', 'asdf');
$oldPath = $file->getFullPath();
$oldPath = $file->getURL();
$file->Name = 'renamed.php'; // evil extension
try {
@ -187,50 +291,22 @@ class FileTest extends SapphireTest {
}
}
public function testLinkAndRelativeLink() {
$file = $this->objFromFixture('File', 'asdf');
$this->assertEquals(ASSETS_DIR . '/FileTest.txt', $file->RelativeLink());
$this->assertEquals(Director::baseURL() . ASSETS_DIR . '/FileTest.txt', $file->Link());
}
public 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()');
}
public function testGetFullPath() {
$rootfile = $this->objFromFixture('File', 'asdf');
$this->assertEquals(ASSETS_PATH . '/FileTest.txt', $rootfile->getFullPath(), 'File in assets/ folder');
}
public function testGetURL() {
$rootfile = $this->objFromFixture('File', 'asdf');
$this->assertEquals(Director::baseURL() . $rootfile->getFilename(), $rootfile->getURL());
$this->assertEquals('/assets/FileTest/55b443b601/FileTest.txt', $rootfile->getURL());
}
public function testGetAbsoluteURL() {
$rootfile = $this->objFromFixture('File', 'asdf');
$this->assertEquals(Director::absoluteBaseURL() . $rootfile->getFilename(), $rootfile->getAbsoluteURL());
$this->assertEquals(
Director::absoluteBaseURL() . 'assets/FileTest/55b443b601/FileTest.txt',
$rootfile->getAbsoluteURL()
);
}
public 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('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 . '/FileTest.png', $file->Filename);
// When name is assigned, title is automatically assigned
$file = $this->objFromFixture('Image', 'setfromname');
$this->assertEquals('FileTest', $file->Title);
}
@ -244,13 +320,13 @@ class FileTest extends SapphireTest {
}
public function testFileType() {
$file = $this->objFromFixture('File', 'gif');
$file = $this->objFromFixture('Image', 'gif');
$this->assertEquals("GIF image - good for diagrams", $file->FileType);
$file = $this->objFromFixture('File', 'pdf');
$this->assertEquals("Adobe Acrobat PDF file", $file->FileType);
$file = $this->objFromFixture('File', 'gifupper');
$file = $this->objFromFixture('Image', 'gifupper');
$this->assertEquals("GIF image - good for diagrams", $file->FileType);
/* Only a few file types are given special descriptions; the rest are unknown */
@ -279,11 +355,9 @@ class FileTest extends SapphireTest {
public function testDeleteDatabaseOnly() {
$file = $this->objFromFixture('File', 'asdf');
$fileID = $file->ID;
$filePath = $file->getFullPath();
$filePath = AssetStoreTest_SpyStore::getLocalPath($file);
$file->deleteDatabaseOnly();
DataObject::flush_and_destroy_cache();
$file->delete();
$this->assertFileExists($filePath);
$this->assertFalse(DataObject::get_by_id('File', $fileID));
@ -300,9 +374,11 @@ class FileTest extends SapphireTest {
//get folder again and see if the filename has changed
$folder = DataObject::get_by_id('Folder',$folderID);
$this->assertEquals($folder->Filename, ASSETS_DIR ."/". $newTitle ."/",
"Folder Filename updated after rename of Title");
$this->assertEquals(
$newTitle . "/",
$folder->Filename,
"Folder Filename updated after rename of Title"
);
//rename a folder's name
$newTitle2 = "FileTest-folder-renamed2";
@ -326,43 +402,6 @@ class FileTest extends SapphireTest {
"Folder Title updated after rename of Filename");
}
public function testGetClassForFileExtension() {
$orig = File::config()->class_for_file_extension;
File::config()->class_for_file_extension = array('*' => 'MyGenericFileClass');
File::config()->class_for_file_extension = array('foo' => 'MyFooFileClass');
$this->assertEquals(
'MyFooFileClass',
File::get_class_for_file_extension('foo'),
'Finds directly mapped file classes'
);
$this->assertEquals(
'MyFooFileClass',
File::get_class_for_file_extension('FOO'),
'Works without case sensitivity'
);
$this->assertEquals(
'MyGenericFileClass',
File::get_class_for_file_extension('unknown'),
'Falls back to generic class for unknown extensions'
);
File::config()->class_for_file_extension = $orig;
}
public function testFolderConstructChild() {
$orig = File::config()->class_for_file_extension;
File::config()->class_for_file_extension = array('gif' => 'FileTest_MyCustomFile');
$folder1 = $this->objFromFixture('Folder', 'folder1');
$fileID = $folder1->constructChild('myfile.gif');
$file = DataObject::get_by_id('File', $fileID);
$this->assertEquals('FileTest_MyCustomFile', get_class($file));
File::config()->class_for_file_extension = $orig;
}
public function testSetsOwnerOnFirstWrite() {
Session::set('loggedInAs', null);
$member1 = new Member();
@ -386,7 +425,7 @@ class FileTest extends SapphireTest {
}
public function testCanEdit() {
$file = $this->objFromFixture('File', 'gif');
$file = $this->objFromFixture('Image', 'gif');
// Test anonymous permissions
Session::set('loggedInAs', null);
@ -413,73 +452,14 @@ class FileTest extends SapphireTest {
$this->assertTrue($file->canEdit(), "Admins can edit files");
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
public 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);
}
// Conditional fixture creation in case the 'cms' module is installed
if(class_exists('ErrorPage')) {
$page = new ErrorPage(array(
'Title' => 'Page not Found',
'ErrorCode' => 404
));
$page->write();
$page->publish('Stage', 'Live');
}
}
public function tearDown() {
parent::tearDown();
/* 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);
if($folder && file_exists(BASE_PATH."/$folder->Filename")) {
Filesystem::removeFolder(BASE_PATH."/$folder->Filename");
}
}
// Remove left over folders and any files that may exist
if(file_exists('../assets/FileTest')) Filesystem::removeFolder('../assets/FileTest');
if(file_exists('../assets/FileTest-subfolder')) Filesystem::removeFolder('../assets/FileTest-subfolder');
if(file_exists('../assets/FileTest.txt')) unlink('../assets/FileTest.txt');
if (file_exists("../assets/FileTest-folder-renamed1")) {
Filesystem::removeFolder("../assets/FileTest-folder-renamed1");
}
if (file_exists("../assets/FileTest-folder-renamed2")) {
Filesystem::removeFolder("../assets/FileTest-folder-renamed2");
}
if (file_exists("../assets/FileTest-folder-renamed3")) {
Filesystem::removeFolder("../assets/FileTest-folder-renamed3");
}
public function testJoinPaths() {
$this->assertEquals('name/file.jpg', File::join_paths('/name', 'file.jpg'));
$this->assertEquals('name/file.jpg', File::join_paths('name', 'file.jpg'));
$this->assertEquals('name/file.jpg', File::join_paths('/name', '/file.jpg'));
$this->assertEquals('name/file.jpg', File::join_paths('name/', '/', 'file.jpg'));
$this->assertEquals('file.jpg', File::join_paths('/', '/', 'file.jpg'));
$this->assertEquals('', File::join_paths('/', '/'));
}
}

View File

@ -10,26 +10,41 @@ Folder:
ParentID: =>Folder.folder1
File:
asdf:
Filename: assets/FileTest.txt
gif:
Filename: assets/FileTest.gif
gifupper:
Filename: assets/FileTest.GIF
FileFilename: FileTest.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTest.txt
pdf:
Filename: assets/FileTest.pdf
setfromname:
Name: FileTest.png
ParentID: 0
FileFilename: FileTest.pdf
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTest.pdf
subfolderfile:
Filename: assets/FileTest-subfolder/FileTestSubfolder.txt
FileFilename: FileTest-subfolder/FileTestSubfolder.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTestSubfolder.txt
ParentID: =>Folder.subfolder
subfolderfile-setfromname:
FileFilename: FileTest-subfolder/FileTestSubfolder2.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTestSubfolder2.txt
ParentID: =>Folder.subfolder
file1-folder1:
Filename: assets/FileTest-folder1/File1.txt
FileFilename: FileTest-folder1/File1.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: File1.txt
ParentID: =>Folder.folder1
Image:
gif:
FileFilename: FileTest.gif
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTest.gif
gifupper:
FileFilename: FileTest-gifupper.GIF
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTest-gifupper.GIF
setfromname:
FileFilename: FileTest.png
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTest.png
Permission:
admin:
Code: ADMIN

View File

@ -1,7 +1,9 @@
<?php
use Filesystem as SS_Filesystem;
/**
* @author Ingo Schommer (ingo at silverstripe dot com)
* @todo There's currently no way to save outside of assets/ folder
*
* @package framework
* @subpackage tests
@ -10,6 +12,34 @@ class FolderTest extends SapphireTest {
protected static $fixture_file = 'FileTest.yml';
public function setUp() {
parent::setUp();
// Set backend root to /FolderTest
AssetStoreTest_SpyStore::activate('FolderTest');
// Create a test folders for each of the fixture references
foreach(Folder::get() as $folder) {
$path = AssetStoreTest_SpyStore::getLocalPath($folder);
SS_Filesystem::makeFolder($path);
}
// Create a test files for each of the fixture references
$files = File::get()->exclude('ClassName', 'Folder');
foreach($files as $file) {
$path = AssetStoreTest_SpyStore::getLocalPath($file);
SS_Filesystem::makeFolder(dirname($path));
$fh = fopen($path, "w+");
fwrite($fh, str_repeat('x', 1000000));
fclose($fh);
}
}
public function tearDown() {
AssetStoreTest_SpyStore::reset();
parent::tearDown();
}
public function testCreateFromNameAndParentIDSetsFilename() {
$folder1 = $this->objFromFixture('Folder', 'folder1');
$newFolder = new Folder();
@ -32,101 +62,43 @@ class FolderTest extends SapphireTest {
}
public function testFindOrMake() {
$path = '/FolderTest/testFindOrMake/';
$path = 'parent/testFindOrMake/';
$folder = Folder::find_or_make($path);
$this->assertEquals(ASSETS_DIR . $path,$folder->getRelativePath(),
$this->assertEquals(
ASSETS_PATH . '/FolderTest/' . $path,
AssetStoreTest_SpyStore::getLocalPath($folder),
'Nested path information is correctly saved to database (with trailing slash)'
);
$this->assertTrue(file_exists(ASSETS_PATH . $path), 'File');
// Folder does not exist until it contains files
$this->assertFileNotExists(
AssetStoreTest_SpyStore::getLocalPath($folder),
'Empty folder does not have a filesystem record automatically'
);
$parentFolder = DataObject::get_one('Folder', array(
'"File"."Name"' => 'FolderTest'
'"File"."Name"' => 'parent'
));
$this->assertNotNull($parentFolder);
$this->assertEquals($parentFolder->ID, $folder->ParentID);
$path = '/FolderTest/testFindOrMake'; // no trailing slash
$path = 'parent/testFindOrMake'; // no trailing slash
$folder = Folder::find_or_make($path);
$this->assertEquals(ASSETS_DIR . $path . '/',$folder->getRelativePath(),
$this->assertEquals(
ASSETS_PATH . '/FolderTest/' . $path . '/', // Slash is automatically added here
AssetStoreTest_SpyStore::getLocalPath($folder),
'Path information is correctly saved to database (without trailing slash)'
);
$path = '/assets/'; // relative to "assets/" folder, should produce "assets/assets/"
$path = 'assets/'; // relative to "assets/" folder, should produce "assets/assets/"
$folder = Folder::find_or_make($path);
$this->assertEquals(ASSETS_DIR . $path,$folder->getRelativePath(),
$this->assertEquals(
ASSETS_PATH . '/FolderTest/' . $path,
AssetStoreTest_SpyStore::getLocalPath($folder),
'A folder named "assets/" within "assets/" is allowed'
);
}
/**
* @see FileTest->testSetNameChangesFilesystemOnWrite()
*/
public 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()
*/
public 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()');
}
/**
* Tests for the bug #5994 - Moving folder after executing Folder::findOrMake will not set the Filenames properly
*/
@ -135,15 +107,25 @@ class FolderTest extends SapphireTest {
Folder::find_or_make($folder1->Filename);
$folder2 = $this->objFromFixture('Folder', 'folder2');
// set ParentID
// set ParentID. This should cause updateFilesystem to be called on all children
$folder1->ParentID = $folder2->ID;
$folder1->write();
// Check if the file in the folder moved along
$file1 = DataObject::get_by_id('File', $this->idFromFixture('File', 'file1-folder1'), false);
$this->assertFileExists($file1->getFullPath());
$this->assertEquals($file1->Filename, 'assets/FileTest-folder2/FileTest-folder1/File1.txt',
'The file DataObject has updated path');
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($file1));
$this->assertEquals(
'FileTest-folder2/FileTest-folder1/File1.txt',
$file1->Filename,
'The file DataObject has updated path'
);
// File should be located in new folder
$this->assertEquals(
ASSETS_PATH . '/FolderTest/FileTest-folder2/FileTest-folder1/55b443b601/File1.txt',
AssetStoreTest_SpyStore::getLocalPath($file1)
);
}
/**
@ -160,198 +142,36 @@ class FolderTest extends SapphireTest {
// Check if the file in the folder moved along
$file1 = DataObject::get_by_id('File', $this->idFromFixture('File', 'file1-folder1'), false);
$this->assertFileExists($file1->getFullPath());
$this->assertEquals($file1->Filename, 'assets/FileTest-folder1-changed/File1.txt',
'The file DataObject path uses renamed folder');
$this->assertFileExists(
AssetStoreTest_SpyStore::getLocalPath($file1)
);
$this->assertEquals(
$file1->Filename,
'FileTest-folder1-changed/File1.txt',
'The file DataObject path uses renamed folder'
);
// File should be located in new folder
$this->assertEquals(
ASSETS_PATH . '/FolderTest/FileTest-folder1-changed/55b443b601/File1.txt',
AssetStoreTest_SpyStore::getLocalPath($file1)
);
}
/**
* @see FileTest->testLinkAndRelativeLink()
* URL and Link are undefined for folder dataobjects
*/
public 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()
*/
public function testGetRelativePath() {
$rootfolder = $this->objFromFixture('Folder', 'folder1');
$this->assertEquals('assets/FileTest-folder1/', $rootfolder->getRelativePath(), 'Folder in assets/');
}
/**
* @see FileTest->testGetFullPath()
*/
public function testGetFullPath() {
$rootfolder = $this->objFromFixture('Folder', 'folder1');
$this->assertEquals(ASSETS_PATH . '/FileTest-folder1/', $rootfolder->getFullPath(), 'File in assets/ folder');
}
public function testDeleteAlsoRemovesFilesystem() {
$path = '/FolderTest/DeleteAlsoRemovesFilesystemAndChildren';
$folder = Folder::find_or_make($path);
$this->assertFileExists(ASSETS_PATH . $path);
$folder->delete();
$this->assertFileNotExists(ASSETS_PATH . $path);
}
public function testDeleteAlsoRemovesSubfoldersInDatabaseAndFilesystem() {
$path = '/FolderTest/DeleteAlsoRemovesSubfoldersInDatabaseAndFilesystem';
$subfolderPath = $path . '/subfolder';
$folder = Folder::find_or_make($path);
$subfolder = Folder::find_or_make($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');
}
public function testDeleteAlsoRemovesContainedFilesInDatabaseAndFilesystem() {
$path = '/FolderTest/DeleteAlsoRemovesContainedFilesInDatabaseAndFilesystem';
$folder = Folder::find_or_make($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()
*/
public 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));
}
public 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);
}
}
public 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();
}
public function testSyncedChildren() {
mkdir(ASSETS_PATH ."/FolderTest");
mkdir(ASSETS_PATH ."/FolderTest/sync");
$files = array(
'.htaccess',
'.git',
'web.config',
'.DS_Store',
'_my_synced_file.txt',
'invalid_extension.xyz123'
);
$folders = array(
'_combinedfiles',
'_resampled',
'_testsync'
);
foreach($files as $file) {
$fh = fopen(ASSETS_PATH."/FolderTest/sync/$file", "w");
fwrite($fh, 'test');
fclose($fh);
}
foreach($folders as $folder) {
mkdir(ASSETS_PATH ."/FolderTest/sync/". $folder);
}
$folder = Folder::find_or_make('/FolderTest/sync');
$result = $folder->syncChildren();
$this->assertEquals(11, $result['skipped']);
$this->assertEquals(2, $result['added']);
// folder with a path of _test should exist
$this->assertEquals(1, Folder::get()->filter(array(
'Name' => '_testsync'
))->count());
$this->assertEquals(1, File::get()->filter(array(
'Name' => '_my_synced_file.txt'
))->count());
$this->assertEquals(0, File::get()->filter(array(
'Name' => 'invalid_extension.xyz123'
))->count());
$this->assertEmpty($folder->getURL());
$this->assertEmpty($folder->Link());
}
public function testIllegalFilenames() {
// Test that generating a filename with invalid characters generates a correctly named folder.
$folder = Folder::find_or_make('/FolderTest/EN_US Lang');
$this->assertEquals(ASSETS_DIR.'/FolderTest/EN-US-Lang/', $folder->getRelativePath());
$this->assertEquals('FolderTest/EN-US-Lang/', $folder->getFilename());
// Test repeatitions of folder
$folder2 = Folder::find_or_make('/FolderTest/EN_US Lang');

View File

@ -15,9 +15,14 @@ class GDTest extends SapphireTest {
'png32' => 'test_png32.png'
);
public function setUp() {
parent::setUp();
GDBackend::flush();
}
public function tearDown() {
$cache = SS_Cache::factory('GDBackend_Manipulations');
$cache->clean(Zend_Cache::CLEANING_MODE_ALL);
GDBackend::flush();
parent::tearDown();
}
/**
@ -30,7 +35,8 @@ class GDTest extends SapphireTest {
$gds = array();
foreach(self::$filenames as $type => $file) {
$fullPath = realpath(dirname(__FILE__) . '/gdtest/' . $file);
$gd = new GDBackend($fullPath);
$gd = new GDBackend();
$gd->loadFrom($fullPath);
if($callback) {
$gd = $callback($gd);
}
@ -147,7 +153,8 @@ class GDTest extends SapphireTest {
*/
public function testImageSkippedWhenUnavailable() {
$fullPath = realpath(dirname(__FILE__) . '/gdtest/test_jpg.jpg');
$gd = new GDBackend_ImageUnavailable($fullPath);
$gd = new GDBackend_ImageUnavailable();
$gd->loadFrom($fullPath);
/* Ensure no image resource is created if the image is unavailable */
$this->assertNull($gd->getImageResource());
@ -155,23 +162,19 @@ class GDTest extends SapphireTest {
/**
* Tests the integrity of the manipulation cache when an error occurs
* @return void
*/
public function testCacheIntegrity() {
$fullPath = realpath(dirname(__FILE__) . '/gdtest/test_jpg.jpg');
$fullPath = realpath(dirname(__FILE__) . '/gdtest/nonimagedata.jpg');
try {
$gdFailure = new GDBackend_Failure($fullPath, array('ScaleWidth', 123));
$this->fail('GDBackend_Failure should throw an exception when setting image resource');
} catch (GDBackend_Failure_Exception $e) {
$cache = SS_Cache::factory('GDBackend_Manipulations');
$key = md5(implode('_', array($fullPath, filemtime($fullPath))));
// Load invalid file
$gd = new GDBackend();
$gd->loadFrom($fullPath);
$data = unserialize($cache->load($key));
$this->assertArrayHasKey('ScaleWidth|123', $data);
$this->assertTrue($data['ScaleWidth|123']);
}
// Cache should refer to this file
$cache = SS_Cache::factory('GDBackend_Manipulations');
$key = sha1(implode('|', array($fullPath, filemtime($fullPath))));
$data = $cache->load($key);
$this->assertEquals('1', $data);
}
/**
@ -180,32 +183,24 @@ class GDTest extends SapphireTest {
* @return void
*/
public function testFailedResample() {
$fullPath = realpath(dirname(__FILE__) . '/gdtest/test_jpg.jpg');
$fullPath = realpath(dirname(__FILE__) . '/gdtest/nonimagedata.jpg');
$fullPath2 = realpath(dirname(__FILE__) . '/gdtest/test_gif.gif');
try {
$gdFailure = new GDBackend_Failure($fullPath, array('ScaleWidth-failed', 123));
$this->fail('GDBackend_Failure should throw an exception when setting image resource');
} catch (GDBackend_Failure_Exception $e) {
$gd = new GDBackend($fullPath, array('ScaleWidth', 123));
$this->assertTrue($gd->failedResample($fullPath, 'ScaleWidth-failed|123'));
$this->assertFalse($gd->failedResample($fullPath, 'ScaleWidth-not-failed|123'));
}
// Load invalid file
$gd = new GDBackend();
$gd->loadFrom($fullPath);
// Cache should refre to this file
$this->assertTrue($gd->failedResample($fullPath, filemtime($fullPath)));
$this->assertFalse($gd->failedResample($fullPath2, filemtime($fullPath2)));
}
}
class GDBackend_ImageUnavailable extends GDBackend implements TestOnly {
public function imageAvailable($filename, $manipulation) {
return false;
}
}
class GDBackend_Failure extends GDBackend implements TestOnly {
public function setImageResource($resource) {
throw new GDBackend_Failure_Exception('GD failed to load image');
public function failedResample() {
return true;
}
}

View File

@ -1,10 +1,22 @@
<?php
/**
* @package framework
* @subpackage tests
*/
class UploadTest extends SapphireTest {
protected static $fixture_file = 'UploadTest.yml';
protected $usesDatabase = true;
public function setUp() {
parent::setUp();
AssetStoreTest_SpyStore::activate('UploadTest');
}
public function tearDown() {
AssetStoreTest_SpyStore::reset();
parent::tearDown();
}
public function testUpload() {
// create tmp file
@ -31,39 +43,36 @@ class UploadTest extends SapphireTest {
$u1->setValidator($v);
$u1->load($tmpFile);
$file1 = $u1->getFile();
$this->assertTrue(
file_exists($file1->getFullPath()),
$this->assertEquals(
'Uploads/UploadTest-testUpload.txt',
$file1->getFilename()
);
$this->assertEquals(
BASE_PATH . '/assets/UploadTest/Uploads/315ae4c3d4/UploadTest-testUpload.txt',
AssetStoreTest_SpyStore::getLocalPath($file1)
);
$this->assertFileExists(
AssetStoreTest_SpyStore::getLocalPath($file1),
'File upload to standard directory in /assets'
);
$this->assertTrue(
(
strpos(
$file1->getFullPath(),
Director::baseFolder() . '/assets/' . Config::inst()->get('Upload', 'uploads_folder')
)
!== false
),
'File upload to standard directory in /assets'
);
$file1->delete();
// test upload into custom folder
$customFolder = 'UploadTest-testUpload';
$u2 = new Upload();
$u2->load($tmpFile, $customFolder);
$file2 = $u2->getFile();
$this->assertTrue(
file_exists($file2->getFullPath()),
$this->assertEquals(
'UploadTest-testUpload/UploadTest-testUpload.txt',
$file2->getFilename()
);
$this->assertEquals(
BASE_PATH . '/assets/UploadTest/UploadTest-testUpload/315ae4c3d4/UploadTest-testUpload.txt',
AssetStoreTest_SpyStore::getLocalPath($file2)
);
$this->assertFileExists(
AssetStoreTest_SpyStore::getLocalPath($file2),
'File upload to custom directory in /assets'
);
$this->assertTrue(
(strpos($file2->getFullPath(), Director::baseFolder() . '/assets/' . $customFolder) !== false),
'File upload to custom directory in /assets'
);
$file2->delete();
unlink($tmpFilePath);
rmdir(Director::baseFolder() . '/assets/' . $customFolder);
}
public function testAllowedFilesize() {
@ -93,7 +102,7 @@ class UploadTest extends SapphireTest {
$result = $u1->load($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
$v->setAllowedMaxFileSize(array('[doc]' => 10));
$v->setAllowedMaxFileSize(array('[document]' => 10));
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
@ -152,13 +161,13 @@ class UploadTest extends SapphireTest {
// Check instance values for max file size
$maxFileSizes = array(
'[doc]' => 2000,
'[document]' => 2000,
'txt' => '4k'
);
$v = new UploadTest_Validator();
$v->setAllowedMaxFileSize($maxFileSizes);
$retrievedSize = $v->getAllowedMaxFileSize('[doc]');
$retrievedSize = $v->getAllowedMaxFileSize('[document]');
$this->assertEquals(2000, $retrievedSize, 'Max file size check on instance values failed (instance category set check)');
// Check that the instance values overwrote the default values
@ -167,7 +176,7 @@ class UploadTest extends SapphireTest {
$this->assertFalse($retrievedSize, 'Max file size check on instance values failed (config overridden check)');
// Check a category that has not been set before
$retrievedSize = $v->getAllowedMaxFileSize('[zip]');
$retrievedSize = $v->getAllowedMaxFileSize('[archive]');
$this->assertFalse($retrievedSize, 'Max file size check on instance values failed (category not set check)');
// Check a file extension that has not been set before
@ -264,11 +273,10 @@ class UploadTest extends SapphireTest {
$u->setValidator($v);
$u->load($tmpFile);
$file = $u->getFile();
$this->assertTrue(
file_exists($file->getFullPath()),
$this->assertFileExists(
AssetStoreTest_SpyStore::getLocalPath($file),
'File upload to custom directory in /assets'
);
$file->delete();
}
public function testUploadDeniesNoExtensionFilesIfNoEmptyStringSetForValidatorExtensions() {
@ -298,19 +306,6 @@ class UploadTest extends SapphireTest {
$this->assertFalse($result, 'Load failed because extension was not accepted');
$this->assertEquals(1, count($u->getErrors()), 'There is a single error of the file extension');
}
// Delete files in the default uploads directory that match the name pattern.
// @param String $namePattern A regular expression applied to files in the directory. If the name matches
// the pattern, it is deleted. Directories, . and .. are excluded.
public function deleteTestUploadFiles($namePattern) {
$tmpFolder = ASSETS_PATH . "/" . Config::inst()->get('Upload', 'uploads_folder');
$files = scandir($tmpFolder);
foreach ($files as $f) {
if ($f == "." || $f == ".." || is_dir("$tmpFolder/$f")) continue;
if (preg_match($namePattern, $f)) unlink("$tmpFolder/$f");
}
}
public function testUploadTarGzFileTwiceAppendsNumber() {
@ -331,9 +326,6 @@ class UploadTest extends SapphireTest {
'error' => UPLOAD_ERR_OK,
);
// Make sure there are none here, otherwise they get renamed incorrectly for the test.
$this->deleteTestUploadFiles("/UploadTest-testUpload.*tar\.gz/");
// test upload into default folder
$u = new Upload();
$u->load($tmpFile);
@ -344,7 +336,7 @@ class UploadTest extends SapphireTest {
'File has a name without a number because it\'s not a duplicate'
);
$this->assertFileExists(
BASE_PATH . '/' . $file->getRelativePath(),
AssetStoreTest_SpyStore::getLocalPath($file),
'File exists'
);
@ -357,7 +349,7 @@ class UploadTest extends SapphireTest {
'File receives a number attached to the end before the extension'
);
$this->assertFileExists(
BASE_PATH . '/' . $file2->getRelativePath(),
AssetStoreTest_SpyStore::getLocalPath($file2),
'File exists'
);
$this->assertGreaterThan(
@ -375,7 +367,7 @@ class UploadTest extends SapphireTest {
'File receives a number attached to the end before the extension'
);
$this->assertFileExists(
BASE_PATH . '/' . $file3->getRelativePath(),
AssetStoreTest_SpyStore::getLocalPath($file3),
'File exists'
);
$this->assertGreaterThan(
@ -383,10 +375,6 @@ class UploadTest extends SapphireTest {
$file3->ID,
'File database record is not the same'
);
$file->delete();
$file2->delete();
$file3->delete();
}
public function testUploadFileWithNoExtensionTwiceAppendsNumber() {
@ -407,9 +395,6 @@ class UploadTest extends SapphireTest {
'error' => UPLOAD_ERR_OK,
);
// Make sure there are none here, otherwise they get renamed incorrectly for the test.
$this->deleteTestUploadFiles("/UploadTest-testUpload.*/");
$v = new UploadTest_Validator();
$v->setAllowedExtensions(array(''));
@ -425,7 +410,7 @@ class UploadTest extends SapphireTest {
'File is uploaded without extension'
);
$this->assertFileExists(
BASE_PATH . '/' . $file->getRelativePath(),
AssetStoreTest_SpyStore::getLocalPath($file),
'File exists'
);
@ -439,7 +424,7 @@ class UploadTest extends SapphireTest {
'File receives a number attached to the end'
);
$this->assertFileExists(
BASE_PATH . '/' . $file2->getRelativePath(),
AssetStoreTest_SpyStore::getLocalPath($file2),
'File exists'
);
$this->assertGreaterThan(
@ -447,9 +432,6 @@ class UploadTest extends SapphireTest {
$file2->ID,
'File database record is not the same'
);
$file->delete();
$file2->delete();
}
public function testReplaceFile() {
@ -470,9 +452,6 @@ class UploadTest extends SapphireTest {
'error' => UPLOAD_ERR_OK,
);
// Make sure there are none here, otherwise they get renamed incorrectly for the test.
$this->deleteTestUploadFiles("/UploadTest-testUpload.*/");
$v = new UploadTest_Validator();
$v->setAllowedExtensions(array(''));
@ -488,7 +467,7 @@ class UploadTest extends SapphireTest {
'File is uploaded without extension'
);
$this->assertFileExists(
BASE_PATH . '/' . $file->getRelativePath(),
AssetStoreTest_SpyStore::getLocalPath($file),
'File exists'
);
@ -503,7 +482,7 @@ class UploadTest extends SapphireTest {
'File does not receive new name'
);
$this->assertFileExists(
BASE_PATH . '/' . $file2->getRelativePath(),
AssetStoreTest_SpyStore::getLocalPath($file2),
'File exists'
);
$this->assertEquals(
@ -511,9 +490,6 @@ class UploadTest extends SapphireTest {
$file2->ID,
'File database record is the same'
);
$file->delete();
$file2->delete();
}
public function testReplaceFileWithLoadIntoFile() {
@ -535,9 +511,6 @@ class UploadTest extends SapphireTest {
'error' => UPLOAD_ERR_OK,
);
// Make sure there are none here, otherwise they get renamed incorrectly for the test.
$this->deleteTestUploadFiles("/UploadTest-testUpload.*/");
$v = new UploadTest_Validator();
// test upload into default folder
@ -547,13 +520,13 @@ class UploadTest extends SapphireTest {
$file = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload.txt',
$file->Name,
'File is uploaded without extension'
'UploadTest-testUpload.txt',
$file->Name,
'File is uploaded without extension'
);
$this->assertFileExists(
BASE_PATH . '/' . $file->getFilename(),
'File exists'
AssetStoreTest_SpyStore::getLocalPath($file),
'File exists'
);
// replace=true
@ -563,18 +536,18 @@ class UploadTest extends SapphireTest {
$u->loadIntoFile($tmpFile, new File());
$file2 = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload.txt',
$file2->Name,
'File does not receive new name'
'UploadTest-testUpload.txt',
$file2->Name,
'File does not receive new name'
);
$this->assertFileExists(
BASE_PATH . '/' . $file2->getFilename(),
'File exists'
AssetStoreTest_SpyStore::getLocalPath($file2),
'File exists'
);
$this->assertEquals(
$file->ID,
$file2->ID,
'File database record is the same'
$file->ID,
$file2->ID,
'File database record is the same'
);
// replace=false
@ -584,23 +557,19 @@ class UploadTest extends SapphireTest {
$u->loadIntoFile($tmpFile, new File());
$file3 = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload-v2.txt',
$file3->Name,
'File does receive new name'
'UploadTest-testUpload-v2.txt',
$file3->Name,
'File does receive new name'
);
$this->assertFileExists(
BASE_PATH . '/' . $file3->getFilename(),
'File exists'
AssetStoreTest_SpyStore::getLocalPath($file3),
'File exists'
);
$this->assertGreaterThan(
$file2->ID,
$file3->ID,
'File database record is not the same'
$file2->ID,
$file3->ID,
'File database record is not the same'
);
$file->delete();
$file2->delete();
$file3->delete();
}
public function testDeleteResampledImagesOnUpload() {
@ -633,16 +602,13 @@ class UploadTest extends SapphireTest {
// Image upload and generate a resampled image
$image = $uploadImage();
$resampled = $image->ResizedImage(123, 456);
$resampledPath = $resampled->getFullPath();
$this->assertTrue(file_exists($resampledPath));
$resampledPath = AssetStoreTest_SpyStore::getLocalPath($resampled);
$this->assertFileExists($resampledPath);
// Re-upload the image, overwriting the original
// Resampled images should removed when their parent file is overwritten
$image = $uploadImage();
$this->assertFalse(file_exists($resampledPath));
unlink($tmpFilePath);
$image->delete();
$this->assertFileExists($resampledPath);
}
public function testFileVersioningWithAnExistingFile() {
@ -676,8 +642,7 @@ class UploadTest extends SapphireTest {
};
// test empty file version prefix
$originalVersionPrefix = Config::inst()->get('Upload', 'version_prefix');
Config::inst()->update('Upload', 'version_prefix', '');
Config::inst()->update('SilverStripe\Filesystem\Storage\DefaultAssetNameGenerator', 'version_prefix', '');
$file1 = $upload('UploadTest-IMG001.jpg');
$this->assertEquals(
@ -688,23 +653,23 @@ class UploadTest extends SapphireTest {
$file2 = $upload('UploadTest-IMG001.jpg');
$this->assertEquals(
'UploadTest-IMG2.jpg',
'UploadTest-IMG002.jpg',
$file2->Name,
'File does receive new name'
);
$file3 = $upload('UploadTest-IMG001.jpg');
$file3 = $upload('UploadTest-IMG002.jpg');
$this->assertEquals(
'UploadTest-IMG3.jpg',
'UploadTest-IMG003.jpg',
$file3->Name,
'File does receive new name'
);
$file4 = $upload('UploadTest-IMG3.jpg');
$this->assertEquals(
'UploadTest-IMG4.jpg',
'UploadTest-IMG3.jpg',
$file4->Name,
'File does receive new name'
'File does not receive new name'
);
$file1->delete();
@ -713,7 +678,7 @@ class UploadTest extends SapphireTest {
$file4->delete();
// test '-v' file version prefix
Config::inst()->update('Upload', 'version_prefix', '-v');
Config::inst()->update('SilverStripe\Filesystem\Storage\DefaultAssetNameGenerator', 'version_prefix', '-v');
$file1 = $upload('UploadTest2-IMG001.jpg');
$this->assertEquals(
@ -742,16 +707,9 @@ class UploadTest extends SapphireTest {
$file4->Name,
'File does receive new name'
);
$file1->delete();
$file2->delete();
$file3->delete();
$file4->delete();
Config::inst()->update('Upload', 'version_prefix', $originalVersionPrefix);
}
}
class UploadTest_Validator extends Upload_Validator implements TestOnly {
/**

View File

@ -0,0 +1 @@
No this is not an image file

View File

@ -1,11 +1,5 @@
<?php
use Filesystem as SS_Filesystem;
use League\Flysystem\Filesystem;
use SilverStripe\Filesystem\Flysystem\AssetAdapter;
use SilverStripe\Filesystem\Flysystem\FlysystemAssetStore;
use SilverStripe\Filesystem\Flysystem\FlysystemUrlPlugin;
/**
* Description of DBFileTest
*
@ -24,22 +18,12 @@ class DBFileTest extends SapphireTest {
parent::setUp();
// Set backend
$adapter = new AssetAdapter(ASSETS_PATH . '/DBFileTest');
$filesystem = new Filesystem($adapter);
$filesystem->addPlugin(new FlysystemUrlPlugin());
$backend = new AssetStoreTest_SpyStore();
$backend->setFilesystem($filesystem);
Injector::inst()->registerService($backend, 'AssetStore');
// Disable legacy
Config::inst()->remove(get_class(new FlysystemAssetStore()), 'legacy_filenames');
// Update base url
AssetStoreTest_SpyStore::activate('DBFileTest');
Config::inst()->update('Director', 'alternate_base_url', '/mysite/');
}
public function tearDown() {
SS_Filesystem::removeFolder(ASSETS_PATH . '/DBFileTest');
AssetStoreTest_SpyStore::reset('DBFileTest');
parent::tearDown();
}
@ -50,7 +34,7 @@ class DBFileTest extends SapphireTest {
$obj = new DBFileTest_Object();
// Test image tag
$fish = realpath(__DIR__ .'/../model/testimages/test_image_high-quality.jpg');
$fish = realpath(__DIR__ .'/../model/testimages/test-image-high-quality.jpg');
$this->assertFileExists($fish);
$obj->MyFile->setFromLocalFile($fish, 'awesome-fish.jpg');
$this->assertEquals(
@ -66,6 +50,19 @@ class DBFileTest extends SapphireTest {
);
}
public function testValidation() {
$obj = new DBFileTest_ImageOnly();
// Test from image
$fish = realpath(__DIR__ .'/../model/testimages/test-image-high-quality.jpg');
$this->assertFileExists($fish);
$obj->MyFile->setFromLocalFile($fish, 'awesome-fish.jpg');
// This should fail
$this->setExpectedException('ValidationException');
$obj->MyFile->setFromString('puppies', 'subdir/puppy-document.txt');
}
}
/**
@ -73,16 +70,21 @@ class DBFileTest extends SapphireTest {
*/
class DBFileTest_Object extends DataObject implements TestOnly {
private static $db = array(
'MyFile' => 'DBFile'
"MyFile" => "DBFile"
);
}
class DBFileTest_Subclass extends DBFileTest_Object implements TestOnly {
private static $db = array(
'AnotherFile' => 'DBFile'
"AnotherFile" => "DBFile"
);
}
class DBFileTest_ImageOnly extends DataObject implements TestOnly {
private static $db = array(
"MyFile" => "DBFile('image/supported')"
);
}

View File

@ -1,12 +1,12 @@
FormScaffolderTest_Tag:
tag1:
Title: Tag 1
tag1:
Title: Tag 1
FormScaffolderTest_Article:
article1:
Title: Article 1
Content: Test
Tags: =>FormScaffolderTest_Tag.tag1
article1:
Title: Article 1
Content: Test
Tags: =>FormScaffolderTest_Tag.tag1
FormScaffolderTest_Author:
author1:
FirstName: Author 1
Tags: =>FormScaffolderTest_Article.article1
author1:
FirstName: Author 1
Tags: =>FormScaffolderTest_Article.article1

View File

@ -1,4 +1,7 @@
<?php
use Filesystem as SS_Filesystem;
/**
* @package framework
* @subpackage tests
@ -15,6 +18,27 @@ class HtmlEditorFieldTest extends FunctionalTest {
protected $extraDataObjects = array('HtmlEditorFieldTest_Object');
public function setUp() {
parent::setUp();
// Set backend root to /HtmlEditorFieldTest
AssetStoreTest_SpyStore::activate('HtmlEditorFieldTest');
// Create a test files for each of the fixture references
$files = File::get()->exclude('ClassName', 'Folder');
foreach($files as $file) {
$fromPath = BASE_PATH . '/framework/tests/forms/images/' . $file->Name;
$destPath = BASE_PATH . $file->getURL(); // Only correct for test asset store
SS_Filesystem::makeFolder(dirname($destPath));
copy($fromPath, $destPath);
}
}
public function tearDown() {
AssetStoreTest_SpyStore::reset();
parent::tearDown();
}
public function testBasicSaving() {
$obj = new HtmlEditorFieldTest_Object();
$editor = new HtmlEditorField('Content');
@ -63,22 +87,11 @@ class HtmlEditorFieldTest extends FunctionalTest {
$obj = new HtmlEditorFieldTest_Object();
$editor = new HtmlEditorField('Content');
/*
* Following stuff is neccessary to
* a) use the proper filename for the image we are referencing
* b) not confuse the "existing" filesystem by our test
*/
$imageFile = $this->objFromFixture('Image', 'example_image');
$imageFile->Filename = FRAMEWORK_DIR . '/' . $imageFile->Filename;
$origUpdateFilesystem = Config::inst()->get('File', 'update_filesystem');
Config::inst()->update('File', 'update_filesystem', false);
$imageFile->write();
Config::inst()->update('File', 'update_filesystem', $origUpdateFilesystem);
/*
* End of test bet setting
*/
$editor->setValue('<img src="assets/HTMLEditorFieldTest_example.jpg" width="10" height="20" />');
$fileID = $this->idFromFixture('Image', 'example_image');
$editor->setValue(sprintf(
'<img src="assets/HTMLEditorFieldTest_example.jpg" width="10" height="20" data-fileid="%d" />',
$fileID
));
$editor->saveInto($obj);
$parser = new CSSContentParser($obj->Content);
@ -88,9 +101,9 @@ class HtmlEditorFieldTest extends FunctionalTest {
$this->assertEquals(10, (int)$xml[0]['width'], 'Width tag of resized image is set.');
$this->assertEquals(20, (int)$xml[0]['height'], 'Height tag of resized image is set.');
$neededFilename = 'assets/_resampled/ResizedImage' . Convert::base64url_encode(array(10,20)) .
'/HTMLEditorFieldTest_example.jpg';
$neededFilename
= '/assets/HtmlEditorFieldTest/f5c7c2f814/HTMLEditorFieldTest-example__ResizedImageWzEwLDIwXQ.jpg';
$this->assertEquals($neededFilename, (string)$xml[0]['src'], 'Correct URL of resized image is set.');
$this->assertTrue(file_exists(BASE_PATH.DIRECTORY_SEPARATOR.$neededFilename), 'File for resized image exists');
$this->assertEquals(false, $obj->HasBrokenFile, 'Referenced image file exists.');
@ -117,7 +130,7 @@ class HtmlEditorFieldTest extends FunctionalTest {
}
public function testHtmlEditorFieldFileLocal() {
$file = new HtmlEditorField_File('http://domain.com/folder/my_image.jpg?foo=bar');
$file = new HtmlEditorField_Image('http://domain.com/folder/my_image.jpg?foo=bar');
$this->assertEquals('http://domain.com/folder/my_image.jpg?foo=bar', $file->URL);
$this->assertEquals('my_image.jpg', $file->Name);
$this->assertEquals('jpg', $file->Extension);
@ -126,7 +139,7 @@ class HtmlEditorFieldTest extends FunctionalTest {
public function testHtmlEditorFieldFileRemote() {
$fileFixture = new File(array('Name' => 'my_local_image.jpg', 'Filename' => 'folder/my_local_image.jpg'));
$file = new HtmlEditorField_File('http://localdomain.com/folder/my_local_image.jpg', $fileFixture);
$file = new HtmlEditorField_Image('http://localdomain.com/folder/my_local_image.jpg', $fileFixture);
$this->assertEquals('http://localdomain.com/folder/my_local_image.jpg', $file->URL);
$this->assertEquals('my_local_image.jpg', $file->Name);
$this->assertEquals('jpg', $file->Extension);

View File

@ -1,12 +1,8 @@
File:
example_file:
Name: example.pdf
Filename: folder/subfolder/example.pdf
Image:
example_image:
FileFilename: HTMLEditorFieldTest-example.jpg
FileHash: f5c7c2f814b0f20ceb30b72edbac220d6eff65ed
Name: HTMLEditorFieldTest_example.jpg
Filename: tests/forms/images/HTMLEditorFieldTest_example.jpg
HtmlEditorFieldTest_Object:
home:
@ -14,4 +10,4 @@ HtmlEditorFieldTest_Object:
about:
Title: About Us
contact:
Title: Contact Us
Title: Contact Us

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -1,9 +1,11 @@
<?php
use Filesystem as SS_Filesystem;
/**
* @package framework
* @subpackage tests
*/
class UploadFieldTest extends FunctionalTest {
protected static $fixture_file = 'UploadFieldTest.yml';
@ -14,21 +16,47 @@ class UploadFieldTest extends FunctionalTest {
'File' => array('UploadFieldTest_FileExtension')
);
public function setUp() {
parent::setUp();
// Set backend root to /UploadFieldTest
AssetStoreTest_SpyStore::activate('UploadFieldTest');
// Create a test folders for each of the fixture references
foreach(Folder::get() as $folder) {
$path = AssetStoreTest_SpyStore::getLocalPath($folder);
SS_Filesystem::makeFolder($path);
}
// Create a test files for each of the fixture references
$files = File::get()->exclude('ClassName', 'Folder');
foreach($files as $file) {
$path = AssetStoreTest_SpyStore::getLocalPath($file);
SS_Filesystem::makeFolder(dirname($path));
$fh = fopen($path, "w+");
fwrite($fh, str_repeat('x', 1000000));
fclose($fh);
}
}
public function tearDown() {
AssetStoreTest_SpyStore::reset();
parent::tearDown();
}
/**
* Test that files can be uploaded against an object with no relation
*/
public function testUploadNoRelation() {
$this->loginWithPermission('ADMIN');
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$tmpFileName = 'testUploadBasic.txt';
$response = $this->mockFileUpload('NoRelationField', $tmpFileName);
$this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('File', array(
'"File"."Name"' => $tmpFileName
));
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($uploadedFile));
$this->assertTrue(is_object($uploadedFile), 'The file object is created');
}
@ -47,11 +75,11 @@ class UploadFieldTest extends FunctionalTest {
$tmpFileName = 'testUploadHasOneRelation.txt';
$response = $this->mockFileUpload('HasOneFile', $tmpFileName);
$this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('File', array(
'"File"."Name"' => $tmpFileName
));
$this->assertTrue(is_object($uploadedFile), 'The file object is created');
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($uploadedFile));
// Secondly, ensure that simply uploading an object does not save the file against the relation
$record = DataObject::get_by_id($record->class, $record->ID, false);
@ -80,11 +108,11 @@ class UploadFieldTest extends FunctionalTest {
$tmpFileName = 'testUploadHasOneRelationWithExtendedFile.txt';
$response = $this->mockFileUpload('HasOneExtendedFile', $tmpFileName);
$this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('UploadFieldTest_ExtendedFile', array(
'"File"."Name"' => $tmpFileName
));
$this->assertTrue(is_object($uploadedFile), 'The file object is created');
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($uploadedFile));
// Test that the record isn't written to automatically
$record = DataObject::get_by_id($record->class, $record->ID, false);
@ -111,11 +139,11 @@ class UploadFieldTest extends FunctionalTest {
$tmpFileName = 'testUploadHasManyRelation.txt';
$response = $this->mockFileUpload('HasManyFiles', $tmpFileName);
$this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('File', array(
'"File"."Name"' => $tmpFileName
));
$this->assertTrue(is_object($uploadedFile), 'The file object is created');
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($uploadedFile));
// Test that the record isn't written to automatically
$record = DataObject::get_by_id($record->class, $record->ID, false);
@ -142,11 +170,11 @@ class UploadFieldTest extends FunctionalTest {
$tmpFileName = 'testUploadManyManyRelation.txt';
$response = $this->mockFileUpload('ManyManyFiles', $tmpFileName);
$this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('File', array(
'"File"."Name"' => $tmpFileName
));
$this->assertTrue(is_object($uploadedFile), 'The file object is created');
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($uploadedFile));
// Test that the record isn't written to automatically
$record = DataObject::get_by_id($record->class, $record->ID, false);
@ -317,7 +345,10 @@ class UploadFieldTest extends FunctionalTest {
$this->assertFalse($record->HasOneFile()->exists());
// Check file object itself exists
$this->assertFileExists($file1->FullPath, 'File is only detached, not deleted from filesystem');
$this->assertFileExists(
AssetStoreTest_SpyStore::getLocalPath($file1),
'File is only detached, not deleted from filesystem'
);
}
/**
@ -340,7 +371,10 @@ class UploadFieldTest extends FunctionalTest {
$this->assertEquals(array('File3'), $record->HasManyFiles()->column('Title'));
// Check file 2 object itself exists
$this->assertFileExists($file3->FullPath, 'File is only detached, not deleted from filesystem');
$this->assertFileExists(
AssetStoreTest_SpyStore::getLocalPath($file3),
'File is only detached, not deleted from filesystem'
);
}
/**
@ -365,11 +399,14 @@ class UploadFieldTest extends FunctionalTest {
$this->assertContains('File5', $record->ManyManyFiles()->column('Title'));
// check file 4 object exists
$this->assertFileExists($file4->FullPath, 'File is only detached, not deleted from filesystem');
$this->assertFileExists(
AssetStoreTest_SpyStore::getLocalPath($file4),
'File is only detached, not deleted from filesystem'
);
}
/**
* Test that files can be deleted from has_one and the filesystem
* Test that files can be deleted from has_one
*/
public function testDeleteFromHasOne() {
$this->loginWithPermission('ADMIN');
@ -379,9 +416,9 @@ class UploadFieldTest extends FunctionalTest {
// Check that file initially exists
$this->assertTrue($record->HasOneFile()->exists());
$this->assertFileExists($file1->FullPath);
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($file1));
// Delete physical file and update record
// Delete file and update record
$response = $this->mockFileDelete('HasOneFile', $file1->ID);
$this->assertFalse($response->isError());
$response = $this->mockUploadFileIDs('HasOneFile', array());
@ -390,13 +427,10 @@ class UploadFieldTest extends FunctionalTest {
// Check that file is not set against record
$record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertFalse($record->HasOneFile()->exists());
// Check that the physical file is deleted
$this->assertFileNotExists($file1->FullPath, 'File is also removed from filesystem');
}
/**
* Test that files can be deleted from has_many and the filesystem
* Test that files can be deleted from has_many
*/
public function testDeleteFromHasMany() {
$this->loginWithPermission('ADMIN');
@ -407,10 +441,10 @@ class UploadFieldTest extends FunctionalTest {
// Check that files initially exists
$this->assertEquals(array('File2', 'File3'), $record->HasManyFiles()->column('Title'));
$this->assertFileExists($file2->FullPath);
$this->assertFileExists($file3->FullPath);
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($file2));
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($file3));
// Delete physical file and update record without file 2
// Delete dataobject file and update record without file 2
$response = $this->mockFileDelete('HasManyFiles', $file2->ID);
$this->assertFalse($response->isError());
$response = $this->mockUploadFileIDs('HasManyFiles', array($file3->ID));
@ -419,9 +453,6 @@ class UploadFieldTest extends FunctionalTest {
// Test that file is removed from record
$record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertEquals(array('File3'), $record->HasManyFiles()->column('Title'));
// Test that physical file is removed
$this->assertFileNotExists($file2->FullPath, 'File is also removed from filesystem');
}
/**
@ -440,9 +471,9 @@ class UploadFieldTest extends FunctionalTest {
$this->assertContains('File4', $setFiles);
$this->assertContains('File5', $setFiles);
$this->assertContains('nodelete.txt', $setFiles);
$this->assertFileExists($file4->FullPath);
$this->assertFileExists($file5->FullPath);
$this->assertFileExists($fileNoDelete->FullPath);
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($file4));
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($file5));
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($fileNoDelete));
// Delete physical file and update record without file 4
$response = $this->mockFileDelete('ManyManyFiles', $file4->ID);
@ -453,9 +484,6 @@ class UploadFieldTest extends FunctionalTest {
$this->assertNotContains('File4', $record->ManyManyFiles()->column('Title'));
$this->assertContains('File5', $record->ManyManyFiles()->column('Title'));
// Check physical file is removed from filesystem
$this->assertFileNotExists($file4->FullPath, 'File is also removed from filesystem');
// Test record-based permissions
$response = $this->mockFileDelete('ManyManyFiles', $fileNoDelete->ID);
$this->assertEquals(403, $response->getStatusCode());
@ -764,17 +792,17 @@ class UploadFieldTest extends FunctionalTest {
$response = $this->mockFileUpload('NoRelationField', $tmpFileName);
$this->assertFalse($response->isError());
$responseData = Convert::json2array($response->getBody());
$this->assertFileExists(ASSETS_PATH . '/UploadFieldTest/' . $responseData[0]['name']);
$uploadedFile = DataObject::get_by_id('File', (int) $responseData[0]['id']);
$this->assertTrue(is_object($uploadedFile), 'The file object is created');
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($uploadedFile));
$tmpFileName = 'testUploadBasic.txt';
$response = $this->mockFileUpload('NoRelationField', $tmpFileName);
$this->assertFalse($response->isError());
$responseData = Convert::json2array($response->getBody());
$this->assertFileExists(ASSETS_PATH . '/UploadFieldTest/' . $responseData[0]['name']);
$uploadedFile2 = DataObject::get_by_id('File', (int) $responseData[0]['id']);
$this->assertTrue(is_object($uploadedFile2), 'The file object is created');
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($uploadedFile2));
$this->assertTrue(
$uploadedFile->Filename !== $uploadedFile2->Filename,
'Filename is not the same'
@ -783,9 +811,6 @@ class UploadFieldTest extends FunctionalTest {
$uploadedFile->ID !== $uploadedFile2->ID,
'File database record is not the same'
);
$uploadedFile->delete();
$uploadedFile2->delete();
}
/**
@ -811,7 +836,7 @@ class UploadFieldTest extends FunctionalTest {
$tmpFileName = 'testUploadBasic.txt';
$response = $this->mockFileUpload('RootFolderTest', $tmpFileName);
$this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/$tmpFileName");
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/315ae4c3d4/$tmpFileName");
$responseExists = $this->mockFileExists('RootFolderTest', $tmpFileName);
$responseExistsData = json_decode($responseExists->getBody());
$this->assertFalse($responseExists->isError());
@ -820,7 +845,7 @@ class UploadFieldTest extends FunctionalTest {
// Check that uploaded files can be detected
$response = $this->mockFileUpload('NoRelationField', $tmpFileName);
$this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/UploadFieldTest/315ae4c3d4/$tmpFileName");
$responseExists = $this->mockFileExists('NoRelationField', $tmpFileName);
$responseExistsData = json_decode($responseExists->getBody());
$this->assertFalse($responseExists->isError());
@ -832,7 +857,7 @@ class UploadFieldTest extends FunctionalTest {
$tmpFileNameExpected = 'test-Upload-Bad.txt';
$response = $this->mockFileUpload('NoRelationField', $tmpFileName);
$this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileNameExpected");
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/UploadFieldTest/315ae4c3d4/$tmpFileNameExpected");
// With original file
$responseExists = $this->mockFileExists('NoRelationField', $tmpFileName);
$responseExistsData = json_decode($responseExists->getBody());
@ -973,57 +998,6 @@ class UploadFieldTest extends FunctionalTest {
);
}
public function setUp() {
Config::inst()->update('File', 'update_filesystem', false);
parent::setUp();
if(!file_exists(ASSETS_PATH)) mkdir(ASSETS_PATH);
/* Create a test folders for each of the fixture references */
$folders = Folder::get()->byIDs($this->allFixtureIDs('Folder'));
foreach($folders as $folder) {
if(!file_exists($folder->getFullPath())) mkdir($folder->getFullPath());
}
/* Create a test files for each of the fixture references */
$files = File::get()->byIDs($this->allFixtureIDs('File'));
foreach($files as $file) {
$fh = fopen($file->getFullPath(), "w");
fwrite($fh, str_repeat('x',1000000));
fclose($fh);
}
}
public function tearDown() {
parent::tearDown();
/* 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);
if($folder && file_exists(BASE_PATH."/$folder->Filename")) {
Filesystem::removeFolder(BASE_PATH."/$folder->Filename");
}
}
// Remove left over folders and any files that may exist
if(file_exists(ASSETS_PATH.'/UploadFieldTest')) {
Filesystem::removeFolder(ASSETS_PATH.'/UploadFieldTest');
}
// Remove file uploaded to root folder
if(file_exists(ASSETS_PATH.'/testUploadBasic.txt')) {
unlink(ASSETS_PATH.'/testUploadBasic.txt');
}
}
}
class UploadFieldTest_Record extends DataObject implements TestOnly {

View File

@ -7,43 +7,61 @@ Folder:
File:
file1:
Title: File1
Filename: assets/UploadFieldTest/file1.txt
FileFilename: assets/UploadFieldTest/file1.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: file1.txt
Parent: =>Folder.folder1
file2:
Title: File2
Filename: assets/UploadFieldTest/file2.txt
FileFilename: UploadFieldTest/file2.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: file2.txt
Parent: =>Folder.folder1
file3:
Title: File3
Filename: assets/UploadFieldTest/file3.txt
FileFilename: UploadFieldTest/file3.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: file3.txt
Parent: =>Folder.folder1
file4:
Title: File4
Filename: assets/UploadFieldTest/file4.txt
FileFilename: UploadFieldTest/file4.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: file4.txt
Parent: =>Folder.folder1
file5:
Title: File5
Filename: assets/UploadFieldTest/file5.txt
FileFilename: UploadFieldTest/file5.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: file5.txt
Parent: =>Folder.folder1
file-noview:
Title: noview.txt
Name: noview.txt
Filename: assets/UploadFieldTest/noview.txt
FileFilename: UploadFieldTest/noview.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: noview.txt
Parent: =>Folder.folder1
file-noedit:
Title: noedit.txt
Name: noedit.txt
Filename: assets/UploadFieldTest/noedit.txt
FileFilename: UploadFieldTest/noedit.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: noedit.txt
Parent: =>Folder.folder1
file-nodelete:
Title: nodelete.txt
Name: nodelete.txt
Filename: assets/UploadFieldTest/nodelete.txt
FileFilename: UploadFieldTest/nodelete.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: nodelete.txt
Parent: =>Folder.folder1
file-subfolder:
Title: file-subfolder.txt
Name: file-subfolder.txt
Filename: assets/UploadFieldTest/subfolder1/file-subfolder.txt
FileFilename: UploadFieldTest/subfolder1/file-subfolder.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: file-subfolder.txt
Parent: =>Folder.folder1-subfolder1
UploadFieldTest_Record:
record1:

View File

@ -1,4 +1,7 @@
<?php
use Filesystem as SS_Filesystem;
/**
* @package framework
* @subpackage tests
@ -10,10 +13,30 @@ class DataDifferencerTest extends SapphireTest {
protected $extraDataObjects = array(
'DataDifferencerTest_Object',
'DataDifferencerTest_HasOneRelationObject',
'DataDifferencerTest_MockImage',
'DataDifferencerTest_HasOneRelationObject'
);
public function setUp() {
parent::setUp();
// Set backend root to /DataDifferencerTest
AssetStoreTest_SpyStore::activate('DataDifferencerTest');
// Create a test files for each of the fixture references
$files = File::get()->exclude('ClassName', 'Folder');
foreach($files as $file) {
$fromPath = BASE_PATH . '/framework/tests/model/testimages/' . $file->Name;
$destPath = BASE_PATH . $file->getURL(); // Only correct for test asset store
SS_Filesystem::makeFolder(dirname($destPath));
copy($fromPath, $destPath);
}
}
public function tearDown() {
AssetStoreTest_SpyStore::reset();
parent::tearDown();
}
public function testArrayValues() {
$obj1 = $this->objFromFixture('DataDifferencerTest_Object', 'obj1');
// create a new version
@ -30,22 +53,11 @@ class DataDifferencerTest extends SapphireTest {
public function testHasOnes() {
$obj1 = $this->objFromFixture('DataDifferencerTest_Object', 'obj1');
$image1 = $this->objFromFixture('DataDifferencerTest_MockImage', 'image1');
$image2 = $this->objFromFixture('DataDifferencerTest_MockImage', 'image2');
$image1 = $this->objFromFixture('Image', 'image1');
$image2 = $this->objFromFixture('Image', 'image2');
$relobj1 = $this->objFromFixture('DataDifferencerTest_HasOneRelationObject', 'relobj1');
$relobj2 = $this->objFromFixture('DataDifferencerTest_HasOneRelationObject', 'relobj2');
// in order to ensure the Filename path is correct, append the correct FRAMEWORK_DIR to the start
// this is only really necessary to make the test pass when FRAMEWORK_DIR is not "framework"
$image1->Filename = FRAMEWORK_DIR . substr($image1->Filename, 9);
$image2->Filename = FRAMEWORK_DIR . substr($image2->Filename, 9);
$origUpdateFilesystem = Config::inst()->get('File', 'update_filesystem');
// we don't want the filesystem being updated on write, as we're only dealing with mock files
Config::inst()->update('File', 'update_filesystem', false);
$image1->write();
$image2->write();
Config::inst()->update('File', 'update_filesystem', $origUpdateFilesystem);
// create a new version
$obj1->ImageID = $image2->ID;
$obj1->HasOneRelationID = $relobj2->ID;
@ -54,11 +66,13 @@ class DataDifferencerTest extends SapphireTest {
$obj1v2 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $obj1->Version);
$differ = new DataDifferencer($obj1v1, $obj1v2);
$obj1Diff = $differ->diffedData();
$this->assertContains($image1->Name, $obj1Diff->getField('Image'));
$this->assertContains($image2->Name, $obj1Diff->getField('Image'));
$this->assertContains('<ins>obj2</ins><del>obj1</del>',
str_replace(' ','',$obj1Diff->getField('HasOneRelationID')));
$this->assertContains(
'<ins>obj2</ins><del>obj1</del>',
str_replace(' ', '', $obj1Diff->getField('HasOneRelationID'))
);
}
}
@ -71,7 +85,7 @@ class DataDifferencerTest_Object extends DataObject implements TestOnly {
);
private static $has_one = array(
'Image' => 'DataDifferencerTest_MockImage',
'Image' => 'Image',
'HasOneRelation' => 'DataDifferencerTest_HasOneRelationObject'
);
@ -99,13 +113,4 @@ class DataDifferencerTest_HasOneRelationObject extends DataObject implements Tes
private static $has_many = array(
'Objects' => 'DataDifferencerTest_Object'
);
}
class DataDifferencerTest_MockImage extends Image implements TestOnly {
public function generateFormattedImage($format, $arg1 = null, $arg2 = null) {
$cacheFile = $this->cacheFilename($format, $arg1, $arg2);
$gd = new GDBackend(Director::baseFolder()."/" . $this->Filename);
// Skip aktual generation
return $gd;
}
}
}

View File

@ -1,15 +1,19 @@
DataDifferencerTest_MockImage:
image1:
Filename: framework/tests/model/testimages/test_image.png
image2:
Filename: framework/tests/model/testimages/test.image.with.dots.png
Image:
image1:
FileFilename: test-image.png
FileHash: 444065542b5dd5187166d8e1cd684e0d724c5a97
Name: test-image.png
image2:
FileFilename: test.image.with.dots.png
FileHash: 46affab7043cfd9f1ded919dd24affd08e926eca
Name: test.image.with.dots.png
DataDifferencerTest_HasOneRelationObject:
relobj1:
Title: obj1
relobj2:
Title: obj2
relobj1:
Title: obj1
relobj2:
Title: obj2
DataDifferencerTest_Object:
obj1:
Choices: a,b
Image: =>DataDifferencerTest_MockImage.image1
HasOneRelation: =>DataDifferencerTest_HasOneRelationObject.relobj1
obj1:
Choices: a,b
Image: =>Image.image1
HasOneRelation: =>DataDifferencerTest_HasOneRelationObject.relobj1

View File

@ -3,16 +3,18 @@
class GDImageTest extends ImageTest {
public function setUp() {
if(!extension_loaded("gd")) {
$this->markTestSkipped("The GD extension is required");
$skip = !extension_loaded("gd");
if($skip) {
$this->skipTest = true;
parent::setUp();
return;
}
Image::set_backend("GDBackend");
parent::setUp();
if($skip) {
$this->markTestSkipped("The GD extension is required");
}
Config::inst()->update('Injector', 'Image_Backend', 'GDBackend');
}
public function tearDown() {
@ -20,35 +22,4 @@ class GDImageTest extends ImageTest {
$cache->clean(Zend_Cache::CLEANING_MODE_ALL);
parent::tearDown();
}
/**
* Test that the cache of manipulation failures is cleared when deleting
* the image object
* @return void
*/
public function testCacheCleaningOnDelete() {
$image = $this->objFromFixture('Image', 'imageWithTitle');
$cache = SS_Cache::factory('GDBackend_Manipulations');
$fullPath = $image->getFullPath();
$key = md5(implode('_', array($fullPath, filemtime($fullPath))));
try {
// Simluate a failed manipulation
$gdFailure = new GDBackend_Failure($fullPath, array('ScaleWidth', 123));
$this->fail('GDBackend_Failure should throw an exception when setting image resource');
} catch (GDBackend_Failure_Exception $e) {
// Check that the cache has stored the manipulation failure
$data = unserialize($cache->load($key));
$this->assertArrayHasKey('ScaleWidth|123', $data);
$this->assertTrue($data['ScaleWidth|123']);
// Delete the image object
$image->delete();
// Check that the cache has been removed
$data = unserialize($cache->load($key));
$this->assertFalse($data);
}
}
}

View File

@ -1,5 +1,11 @@
<?php
use Filesystem as SS_Filesystem;
use League\Flysystem\Filesystem;
use SilverStripe\Filesystem\Flysystem\AssetAdapter;
use SilverStripe\Filesystem\Flysystem\FlysystemAssetStore;
use SilverStripe\Filesystem\Flysystem\FlysystemUrlPlugin;
/**
* @package framework
* @subpackage tests
@ -8,94 +14,65 @@ class ImageTest extends SapphireTest {
protected static $fixture_file = 'ImageTest.yml';
protected $origBackend;
public function setUp() {
if(get_class($this) == "ImageTest") $this->skipTest = true;
if(get_class($this) == "ImageTest") {
$this->skipTest = true;
}
parent::setUp();
$this->origBackend = Image::get_backend();
if($this->skipTest)
if($this->skipTest) {
return;
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");
}
// Set backend root to /ImageTest
AssetStoreTest_SpyStore::activate('ImageTest');
// Copy test images for each of the fixture references
$imageIDs = $this->allFixtureIDs('Image');
foreach($imageIDs as $imageID) {
$image = DataObject::get_by_id('Image', $imageID);
$filePath = BASE_PATH."/$image->Filename";
$sourcePath = str_replace('assets/ImageTest/', 'framework/tests/model/testimages/', $filePath);
$files = File::get()->exclude('ClassName', 'Folder');
foreach($files as $image) {
$filePath = BASE_PATH . $image->getURL(); // Only correct for test asset store
$sourcePath = BASE_PATH . '/framework/tests/model/testimages/' . $image->Name;
if(!file_exists($filePath)) {
if (!copy($sourcePath, $filePath)) user_error('Failed to copy test images', E_USER_ERROR);
SS_Filesystem::makeFolder(dirname($filePath));
if (!copy($sourcePath, $filePath)) {
user_error('Failed to copy test images', E_USER_ERROR);
}
}
}
}
public function tearDown() {
if($this->origBackend) Image::set_backend($this->origBackend);
// Remove the test files that we've created
$fileIDs = $this->allFixtureIDs('Image');
foreach($fileIDs as $fileID) {
$file = DataObject::get_by_id('Image', $fileID);
if($file && file_exists(BASE_PATH."/$file->Filename")) unlink(BASE_PATH."/$file->Filename");
}
// Remove the test folders that we've created
$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");
}
if($folder && file_exists(BASE_PATH."/".$folder->Filename."_resampled")) {
Filesystem::removeFolder(BASE_PATH."/".$folder->Filename."_resampled");
}
}
AssetStoreTest_SpyStore::reset();
parent::tearDown();
}
public function testGetTagWithTitle() {
Config::inst()->update('Image', 'force_resample', false);
Config::inst()->update('DBFile', 'force_resample', false);
$image = $this->objFromFixture('Image', 'imageWithTitle');
$expected = '<img src="' . Director::baseUrl()
. 'assets/ImageTest/test_image.png" alt="This is a image Title" />';
$actual = $image->getTag();
$expected = '<img src="/assets/ImageTest/folder/444065542b/test-image.png" alt="This is a image Title" />';
$actual = trim($image->getTag());
$this->assertEquals($expected, $actual);
}
public function testGetTagWithoutTitle() {
Config::inst()->update('Image', 'force_resample', false);
Config::inst()->update('DBFile', 'force_resample', false);
$image = $this->objFromFixture('Image', 'imageWithoutTitle');
$expected = '<img src="' . Director::baseUrl() . 'assets/ImageTest/test_image.png" alt="test_image" />';
$actual = $image->getTag();
$expected = '<img src="/assets/ImageTest/folder/444065542b/test-image.png" alt="test image" />';
$actual = trim($image->getTag());
$this->assertEquals($expected, $actual);
}
public function testGetTagWithoutTitleContainingDots() {
Config::inst()->update('Image', 'force_resample', false);
Config::inst()->update('DBFile', 'force_resample', false);
$image = $this->objFromFixture('Image', 'imageWithoutTitleContainingDots');
$expected = '<img src="' . Director::baseUrl()
. 'assets/ImageTest/test.image.with.dots.png" alt="test.image.with.dots" />';
$actual = $image->getTag();
$expected = '<img src="/assets/ImageTest/folder/46affab704/test.image.with.dots.png" alt="test.image.with.dots" />';
$actual = trim($image->getTag());
$this->assertEquals($expected, $actual);
}
@ -110,10 +87,10 @@ class ImageTest extends SapphireTest {
$this->assertNotNull($imageFirst);
$expected = 200;
$actual = $imageFirst->getWidth();
$this->assertEquals($expected, $actual);
$imageSecond = $imageFirst->setHeight(100);
$imageSecond = $imageFirst->ScaleHeight(100);
$this->assertNotNull($imageSecond);
$expected = 100;
$actual = $imageSecond->getHeight();
@ -181,7 +158,7 @@ class ImageTest extends SapphireTest {
$imageLQR = $imageLQ->Resampled();
// Test resampled file is served when force_resample = true
Config::inst()->update('Image', 'force_resample', true);
Config::inst()->update('DBFile', 'force_resample', true);
$this->assertLessThan($imageHQ->getAbsoluteSize(), $imageHQR->getAbsoluteSize(), 'Resampled image is smaller than original');
$this->assertEquals($imageHQ->getURL(), $imageHQR->getSourceURL(), 'Path to a resampled image was returned by getURL()');
@ -190,7 +167,7 @@ class ImageTest extends SapphireTest {
$this->assertNotEquals($imageLQ->getURL(), $imageLQR->getSourceURL(), 'Path to the original image file was returned by getURL()');
// Test original file is served when force_resample = false
Config::inst()->update('Image', 'force_resample', false);
Config::inst()->update('DBFile', 'force_resample', false);
$this->assertNotEquals($imageHQ->getURL(), $imageHQR->getSourceURL(), 'Path to the original image file was returned by getURL()');
}
@ -252,71 +229,19 @@ class ImageTest extends SapphireTest {
*/
public function testGenerateImageWithInvalidParameters() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle');
$image->setHeight('String');
$image->ScaleHeight('String');
$image->Pad(600,600,'XXXXXX');
}
public function testCacheFilename() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle');
$imageFirst = $image->Pad(200,200,'CCCCCC');
$imageFilename = $imageFirst->getFullPath();
$imageFilename = $imageFirst->getURL();
// Encoding of the arguments is duplicated from cacheFilename
$neededPart = 'Pad' . Convert::base64url_encode(array(200,200,'CCCCCC'));
$this->assertContains($neededPart, $imageFilename, 'Filename for cached image is correctly generated');
}
public function testMultipleGenerateManipulationCalls_Regeneration() {
Config::inst()->update('Image', 'force_resample', false);
$image = $this->objFromFixture('Image', 'imageWithoutTitle');
$folder = new SS_FileFinder();
$imageFirst = $image->Pad(200,200);
$this->assertNotNull($imageFirst);
$expected = 200;
$actual = $imageFirst->getWidth();
$this->assertEquals($expected, $actual);
$imageSecond = $imageFirst->setHeight(100);
$this->assertNotNull($imageSecond);
$expected = 100;
$actual = $imageSecond->getHeight();
$this->assertEquals($expected, $actual);
$imageThird = $imageSecond->Pad(600,600,'0F0F0F');
// Encoding of the arguments is duplicated from cacheFilename
$argumentString = Convert::base64url_encode(array(600,600,'0F0F0F'));
$this->assertNotNull($imageThird);
$this->assertContains($argumentString, $imageThird->getFullPath(),
'Image contains background color for padded resizement');
$resampledFolder = dirname($image->getFullPath()) . "/_resampled";
$filesInFolder = $folder->find($resampledFolder);
$this->assertEquals(3, count($filesInFolder),
'Image folder contains only the expected number of images before regeneration');
$imageThirdPath = $imageThird->getFullPath();
$hash = md5_file($imageThirdPath);
$this->assertEquals(3, $image->regenerateFormattedImages(),
'Cached images were regenerated in the right number');
$this->assertEquals($hash, md5_file($imageThirdPath), 'Regeneration of third image is correct');
/* Check that no other images exist, to ensure that the regeneration did not create other images */
$this->assertEquals($filesInFolder, $folder->find($resampledFolder),
'Image folder contains only the expected image files after regeneration');
}
public function testRegenerateImages() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle');
$image_generated = $image->ScaleWidth(200);
$p = $image_generated->getFullPath();
$this->assertTrue(file_exists($p), 'Resized image exists after creation call');
$this->assertEquals(1, $image->regenerateFormattedImages(), 'Cached images were regenerated correct');
$this->assertEquals($image_generated->getWidth(), 200,
'Resized image has correct width after regeneration call');
$this->assertTrue(file_exists($p), 'Resized image exists after regeneration call');
}
/**
* Test that propertes from the source Image are inherited by resampled images
*/
@ -329,163 +254,4 @@ class ImageTest extends SapphireTest {
$resampled2 = $resampled->ScaleWidth(5);
$this->assertEquals($resampled2->TestProperty, $testString);
}
/**
* Tests that cached images are regenerated properly after a cached file is renamed with new arguments
* ToDo: This doesn't seem like something that is worth testing - what is the point of this?
*/
public function testRegenerateImagesWithRenaming() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle');
$image_generated = $image->ScaleWidth(200);
$p = $image_generated->getFullPath();
$this->assertTrue(file_exists($p), 'Resized image exists after creation call');
// Encoding of the arguments is duplicated from cacheFilename
$oldArgumentString = Convert::base64url_encode(array(200));
$newArgumentString = Convert::base64url_encode(array(300));
$newPath = str_replace($oldArgumentString, $newArgumentString, $p);
if(!file_exists(dirname($newPath))) mkdir(dirname($newPath));
$newRelative = str_replace($oldArgumentString, $newArgumentString, $image_generated->getFileName());
rename($p, $newPath);
$this->assertFalse(file_exists($p), 'Resized image does not exist at old path after renaming');
$this->assertTrue(file_exists($newPath), 'Resized image exists at new path after renaming');
$this->assertEquals(1, $image->regenerateFormattedImages(),
'Cached images were regenerated in the right number');
$image_generated_2 = new Image_Cached($newRelative);
$this->assertEquals(300, $image_generated_2->getWidth(), 'Cached image was regenerated with correct width');
}
public function testGeneratedImageDeletion() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle');
$image_generated = $image->ScaleWidth(200);
$p = $image_generated->getFullPath();
$this->assertTrue(file_exists($p), 'Resized image exists after creation call');
$numDeleted = $image->deleteFormattedImages();
$this->assertEquals(1, $numDeleted, 'Expected one image to be deleted, but deleted ' . $numDeleted . ' images');
$this->assertFalse(file_exists($p), 'Resized image not existing after deletion call');
}
/**
* Tests that generated images with multiple image manipulations are all deleted
*/
public function testMultipleGenerateManipulationCallsImageDeletion() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle');
$firstImage = $image->ScaleWidth(200);
$firstImagePath = $firstImage->getFullPath();
$this->assertTrue(file_exists($firstImagePath));
$secondImage = $firstImage->ScaleHeight(100);
$secondImagePath = $secondImage->getFullPath();
$this->assertTrue(file_exists($secondImagePath));
$image->deleteFormattedImages();
$this->assertFalse(file_exists($firstImagePath));
$this->assertFalse(file_exists($secondImagePath));
}
/**
* Tests path properties of cached images with multiple image manipulations
*/
public function testPathPropertiesCachedImage() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle');
$firstImage = $image->ScaleWidth(200);
$firstImagePath = $firstImage->getRelativePath();
$this->assertEquals($firstImagePath, $firstImage->Filename);
$secondImage = $firstImage->ScaleHeight(100);
$secondImagePath = $secondImage->getRelativePath();
$this->assertEquals($secondImagePath, $secondImage->Filename);
}
/**
* Tests the static function Image::strip_resampled_prefix, to ensure that
* the original filename can be extracted from the path of transformed images,
* both in current and previous formats
*/
public function testStripResampledPrefix() {
$orig_image = $this->objFromFixture('Image', 'imageWithoutTitleContainingDots');
// current format (3.3+). Example:
// assets/ImageTest/_resampled/ScaleHeightWzIwMF0=/ScaleWidthWzQwMF0=/test.image.with.dots.png;
$firstImage = $orig_image->ScaleWidth(200);
$secondImage = $firstImage->ScaleHeight(200);
$paths_1 = $firstImage->Filename;
$paths_2 = $secondImage->Filename;
// 3.2 format (did not work for multiple transformations)
$paths_3 = 'assets/ImageTest/_resampled/ScaleHeightWzIwMF0=-test.image.with.dots.png';
// 3.1 (and earlier) format (did not work for multiple transformations)
$paths_4 = 'assets/ImageTest/_resampled/ScaleHeight200-test.image.with.dots.png';
$this->assertEquals($orig_image->Filename, Image::strip_resampled_prefix($paths_1));
$this->assertEquals($orig_image->Filename, Image::strip_resampled_prefix($paths_2));
$this->assertEquals($orig_image->Filename, Image::strip_resampled_prefix($paths_3));
$this->assertEquals($orig_image->Filename, Image::strip_resampled_prefix($paths_4));
}
/**
* Test all generate methods
*/
public function testGenerateMethods() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle');
$generateMethods = $this->getGenerateMethods();
// test each generate method
foreach ($generateMethods as $method) {
$generatedImage = $image->$method(333, 333, 'FFFFFF');
$this->assertFileExists(
$generatedImage->getFullPath(),
'Formatted ' . $method . ' image exists'
);
}
}
/**
* Test deleteFormattedImages() against all generate methods
*/
public function testDeleteFormattedImages() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle');
$generateMethods = $this->getGenerateMethods();
// get paths for each generate method
$paths = array();
foreach ($generateMethods as $method) {
$generatedImage = $image->$method(333, 333, 'FFFFFF');
$paths[$method] = $generatedImage->getFullPath();
}
// delete formatted images
$image->deleteFormattedImages();
// test that all formatted images are deleted
foreach ($paths as $method => $path) {
$this->assertFalse(
file_exists($path),
'Formatted ' . $method . ' image does not exist'
);
}
}
/**
* @param bool $custom include methods added dynamically at runtime
* @return array
*/
protected function getGenerateMethods($custom = true) {
$generateMethods = array();
$methodNames = Image::create()->allMethodNames($custom);
foreach ($methodNames as $methodName) {
if (substr($methodName, 0, 8) == 'generate' && $methodName != 'generateformattedimage') {
$format = substr($methodName, 8);
$generateMethods[] = $format;
}
}
return $generateMethods;
}
}

View File

@ -1,26 +1,38 @@
Folder:
folder1:
Filename: assets/ImageTest/
folder1:
Name: folder
Image:
imageWithTitle:
Title: This is a image Title
Filename: assets/ImageTest/test_image.png
Parent: =>Folder.folder1
imageWithoutTitle:
Filename: assets/ImageTest/test_image.png
Parent: =>Folder.folder1
imageWithoutTitleContainingDots:
Filename: assets/ImageTest/test.image.with.dots.png
Parent: =>Folder.folder1
imageWithMetacharacters:
Title: This is a/an image Title
Filename: assets/ImageTest/test_image.png
Parent: =>Folder.folder1
lowQualityJPEG:
Title: This is a low quality JPEG
Filename: assets/ImageTest/test_image_low-quality.jpg
Parent: =>Folder.folder1
highQualityJPEG:
Title: This is a high quality JPEG
Filename: assets/ImageTest/test_image_high-quality.jpg
Parent: =>Folder.folder1
imageWithTitle:
Title: This is a image Title
FileFilename: folder/test-image.png
FileHash: 444065542b5dd5187166d8e1cd684e0d724c5a97
Parent: =>Folder.folder1
Name: test-image.png
imageWithoutTitle:
FileFilename: folder/test-image.png
FileHash: 444065542b5dd5187166d8e1cd684e0d724c5a97
Parent: =>Folder.folder1
Name: test-image.png
imageWithoutTitleContainingDots:
FileFilename: folder/test.image.with.dots.png
FileHash: 46affab7043cfd9f1ded919dd24affd08e926eca
Parent: =>Folder.folder1
Name: test.image.with.dots.png
imageWithMetacharacters:
Title: This is a/an image Title
FileFilename: folder/test-image.png
FileHash: 444065542b5dd5187166d8e1cd684e0d724c5a97
Parent: =>Folder.folder1
Name: test-image.png
lowQualityJPEG:
Title: This is a low quality JPEG
FileFilename: folder/test-image-low-quality.jpg
FileHash: 33be1b95cba0358fe54e8b13532162d52f97421c
Parent: =>Folder.folder1
Name: test-image-low-quality.jpg
highQualityJPEG:
Title: This is a high quality JPEG
FileFilename: folder/test-image-high-quality.jpg
FileHash: a870de278b475cb75f5d9f451439b2d378e13af1
Parent: =>Folder.folder1
Name: test-image-high-quality.jpg

View File

@ -1,15 +1,18 @@
<?php
class ImagickImageTest extends ImageTest {
public function setUp() {
if(!extension_loaded("imagick")) {
$this->markTestSkipped("The Imagick extension is not available.");
$skip = !extension_loaded("imagick");
if($skip) {
$this->skipTest = true;
parent::setUp();
return;
}
Image::set_backend("ImagickBackend");
parent::setUp();
if($skip) {
$this->markTestSkipped("The Imagick extension is not available.");
}
Config::inst()->update('Injector', 'Image_Backend', 'ImagickBackend');
}
}

View File

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB