Merge pull request #1291 from open-sausages/pulls/4.0/dbfile-file-dataobject

API CMS Implementation of RFC-1 Asset Abstraction (DataObject Refactoring)
This commit is contained in:
Ingo Schommer 2015-10-19 16:47:18 +13:00
commit b487d59d02
13 changed files with 271 additions and 361 deletions

View File

@ -1,4 +1,7 @@
<?php
use SilverStripe\Filesystem\Storage\AssetNameGenerator;
/**
* AssetAdmin is the 'file store' section of the CMS.
* It provides an interface for manipulating the File and Folder objects in the system.
@ -35,15 +38,8 @@ class AssetAdmin extends LeftAndMain implements PermissionProvider{
'addfolder',
'delete',
'AddForm',
'DeleteItemsForm',
'SearchForm',
'getsubtree',
'movemarked',
'removefile',
'savefile',
'deleteUnusedThumbnails' => 'ADMIN',
'doSync',
'filter',
'getsubtree'
);
/**
@ -62,13 +58,11 @@ class AssetAdmin extends LeftAndMain implements PermissionProvider{
}
/**
* Set up the controller, in particular, re-sync the File database with the assets folder./
* Set up the controller
*/
public function init() {
parent::init();
// Create base folder if it doesnt exist already
if(!file_exists(ASSETS_PATH)) Filesystem::makeFolder(ASSETS_PATH);
Requirements::javascript(CMS_DIR . "/javascript/AssetAdmin.js");
Requirements::javascript(CMS_DIR . '/javascript/CMSMain.GridField.js');
@ -210,20 +204,6 @@ JS
$addFolderBtn = '';
}
if($folder->canEdit()) {
$syncButton = new LiteralField(
'SyncButton',
sprintf(
'<a class="ss-ui-button ss-ui-action ui-button-text-icon-primary ss-ui-button-ajax font-icon-sync" data-icon="arrow-circle-double" title="%s" href="%s">%s</a>',
_t('AssetAdmin.FILESYSTEMSYNCTITLE', 'Update the CMS database entries of files on the filesystem. Useful when new files have been uploaded outside of the CMS, e.g. through FTP.'),
$this->Link('doSync'),
_t('AssetAdmin.FILESYSTEMSYNC','Sync files')
)
);
} else {
$syncButton = null;
}
// Move existing fields to a "details" tab, unless they've already been tabbed out through extensions.
// Required to keep Folder->getCMSFields() simple and reuseable,
// without any dependencies into AssetAdmin (e.g. useful for "add folder" views).
@ -249,7 +229,6 @@ JS
// the button shouldn't be available. Adding empty values into a ComposteField breaks template rendering.
$actionButtonsComposite = CompositeField::create()->addExtraClass('cms-actions-row');
if($addFolderBtn) $actionButtonsComposite->push($addFolderBtn);
if($syncButton) $actionButtonsComposite->push($syncButton);
// Add the upload field for new media
if($currentPageID = $this->currentPageID()){
@ -266,9 +245,8 @@ JS
$uploadField->removeExtraClass('ss-uploadfield');
$uploadField->setTemplate('AssetUploadField');
if($folder->exists() && $folder->getFilename()) {
// The Upload class expects a folder relative *within* assets/
$path = preg_replace('/^' . ASSETS_DIR . '\//', '', $folder->getFilename());
if($folder->exists()) {
$path = $folder->getFilename();
$uploadField->setFolderName($path);
} else {
$uploadField->setFolderName('/'); // root of the assets
@ -288,7 +266,6 @@ JS
$gridField
));
$treeField = new LiteralField('Tree', '');
// Tree view
$fields->addFieldsToTab('Root.TreeView', array(
clone $actionsComposite,
@ -320,8 +297,6 @@ JS
);
}
$fields->setForm($form);
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
// TODO Can't merge $FormAttributes in template at the moment
@ -353,8 +328,12 @@ JS
$className = $this->stat('tree_class');
$record = DataObject::get_by_id($className, $data['ID']);
if($record && !$record->canDelete()) return Security::permissionFailure();
if(!$record || !$record->ID) throw new SS_HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
if($record && !$record->canDelete()) {
return Security::permissionFailure();
}
if(!$record || !$record->ID) {
throw new SS_HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
}
$parentID = $record->ParentID;
$record->delete();
$this->setCurrentPageID(null);
@ -373,8 +352,12 @@ JS
$context = singleton('File')->getDefaultSearchContext();
// Namespace fields, for easier detection if a search is present
foreach($context->getFields() as $field) $field->setName(sprintf('q[%s]', $field->getName()));
foreach($context->getFilters() as $filter) $filter->setFullName(sprintf('q[%s]', $filter->getFullName()));
foreach($context->getFields() as $field) {
$field->setName(sprintf('q[%s]', $field->getName()));
}
foreach($context->getFilters() as $filter) {
$filter->setFullName(sprintf('q[%s]', $filter->getFullName()));
}
// Customize fields
$dateHeader = HeaderField::create('q[Date]', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
@ -389,12 +372,12 @@ JS
);
$context->addField($dateGroup);
$appCategories = array(
'image' => _t('AssetAdmin.AppCategoryImage', 'Image'),
'archive' => _t('AssetAdmin.AppCategoryArchive', 'Archive', 'A collection of files'),
'audio' => _t('AssetAdmin.AppCategoryAudio', 'Audio'),
'mov' => _t('AssetAdmin.AppCategoryVideo', 'Video'),
'document' => _t('AssetAdmin.AppCategoryDocument', 'Document'),
'flash' => _t('AssetAdmin.AppCategoryFlash', 'Flash', 'The fileformat'),
'zip' => _t('AssetAdmin.AppCategoryArchive', 'Archive', 'A collection of files'),
'doc' => _t('AssetAdmin.AppCategoryDocument', 'Document')
'image' => _t('AssetAdmin.AppCategoryImage', 'Image'),
'video' => _t('AssetAdmin.AppCategoryVideo', 'Video'),
);
$context->addField(
$typeDropdown = new DropdownField(
@ -444,7 +427,6 @@ JS
}
public function AddForm() {
$folder = singleton('Folder');
$form = CMSForm::create(
$this,
'AddForm',
@ -475,7 +457,9 @@ JS
$class = $this->stat('tree_class');
// check create permissions
if(!singleton($class)->canCreate()) return Security::permissionFailure($this);
if(!singleton($class)->canCreate()) {
return Security::permissionFailure($this);
}
// check addchildren permissions
if(
@ -485,44 +469,40 @@ JS
&& $data['ParentID']
) {
$parentRecord = DataObject::get_by_id($class, $data['ParentID']);
if(
$parentRecord->hasMethod('canAddChildren')
&& !$parentRecord->canAddChildren()
) return Security::permissionFailure($this);
if($parentRecord->hasMethod('canAddChildren') && !$parentRecord->canAddChildren()) {
return Security::permissionFailure($this);
}
} else {
$parentRecord = null;
}
$parent = (isset($data['ParentID']) && is_numeric($data['ParentID'])) ? (int)$data['ParentID'] : 0;
$name = (isset($data['Name'])) ? basename($data['Name']) : _t('AssetAdmin.NEWFOLDER',"NewFolder");
if(!$parentRecord || !$parentRecord->ID) $parent = 0;
// Check parent
$parentID = $parentRecord && $parentRecord->ID
? (int)$parentRecord->ID
: 0;
// Build filename
$filename = isset($data['Name'])
? basename($data['Name'])
: _t('AssetAdmin.NEWFOLDER',"NewFolder");
if($parentRecord && $parentRecord->ID) {
$filename = $parentRecord->getFilename() . '/' . $filename;
}
// Get the folder to be created
if($parentRecord && $parentRecord->ID) $filename = $parentRecord->FullPath . $name;
else $filename = ASSETS_PATH . '/' . $name;
// Actually create
if(!file_exists(ASSETS_PATH)) {
mkdir(ASSETS_PATH);
// Ensure name is unique
foreach($this->getNameGenerator($filename) as $filename) {
if(! File::find($filename) ) {
break;
}
}
$record = new Folder();
$record->ParentID = $parent;
// Create record
$record = Folder::create();
$record->ParentID = $parentID;
$record->Name = $record->Title = basename($filename);
// Ensure uniqueness
$i = 2;
$baseFilename = substr($record->Filename, 0, -1) . '-';
while(file_exists($record->FullPath)) {
$record->Filename = $baseFilename . $i . '/';
$i++;
}
$record->Name = $record->Title = basename($record->Filename);
$record->write();
mkdir($record->FullPath);
chmod($record->FullPath, Filesystem::config()->file_create_mask);
if($parentRecord) {
return $this->redirect(Controller::join_links($this->Link('show'), $parentRecord->ID));
@ -531,6 +511,17 @@ JS
}
}
/**
* 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));
}
/**
* Custom currentPage() method to handle opening the 'root' folder
*/
@ -560,117 +551,6 @@ JS
return $this->getSiteTreeFor($this->stat('tree_class'), null, 'ChildFolders', 'numChildFolders');
}
//------------------------------------------------------------------------------------------//
// Data saving handlers
/**
* Can be queried with an ajax request to trigger the filesystem sync. It returns a FormResponse status message
* to display in the CMS
*/
public function doSync() {
$message = Filesystem::sync();
$this->response->addHeader('X-Status', rawurlencode($message));
return;
}
/**
* #################################
* Garbage collection.
* #################################
*/
/**
* Removes all unused thumbnails from the file store
* and returns the status of the process to the user.
*/
public function deleteunusedthumbnails($request) {
// Protect against CSRF on destructive action
if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
$count = 0;
$thumbnails = $this->getUnusedThumbnails();
if($thumbnails) {
foreach($thumbnails as $thumbnail) {
unlink(ASSETS_PATH . "/" . $thumbnail);
$count++;
}
}
$message = _t(
'AssetAdmin.THUMBSDELETED',
'{count} unused thumbnails have been deleted',
array('count' => $count)
);
$this->response->addHeader('X-Status', rawurlencode($message));
return;
}
/**
* Creates array containg all unused thumbnails.
*
* Array is created in three steps:
* 1. Scan assets folder and retrieve all thumbnails
* 2. Scan all HTMLField in system and retrieve thumbnails from them.
* 3. Count difference between two sets (array_diff)
*
* @return array
*/
private function getUnusedThumbnails() {
$allThumbnails = array();
$usedThumbnails = array();
$dirIterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(ASSETS_PATH));
$classes = ClassInfo::subclassesFor('SiteTree');
if($dirIterator) {
foreach($dirIterator as $file) {
if($file->isFile()) {
if(strpos($file->getPathname(), '_resampled') !== false) {
$pathInfo = pathinfo($file->getPathname());
if(in_array(strtolower($pathInfo['extension']), array('jpeg', 'jpg', 'jpe', 'png', 'gif'))) {
$path = str_replace('\\','/', $file->getPathname());
$allThumbnails[] = substr($path, strpos($path, '/assets/') + 8);
}
}
}
}
}
if($classes) {
foreach($classes as $className) {
$SNG_class = singleton($className);
$objects = DataObject::get($className);
if($objects !== NULL) {
foreach($objects as $object) {
foreach($SNG_class->db() as $fieldName => $fieldType) {
if($fieldType == 'HTMLText') {
$url1 = HTTP::findByTagAndAttribute($object->$fieldName,array('img' => 'src'));
if($url1 != NULL) {
$usedThumbnails[] = substr($url1[0], strpos($url1[0], '/assets/') + 8);
}
if($object->latestPublished > 0) {
$object = Versioned::get_latest_version($className, $object->ID);
$url2 = HTTP::findByTagAndAttribute($object->$fieldName, array('img' => 'src'));
if($url2 != NULL) {
$usedThumbnails[] = substr($url2[0], strpos($url2[0], '/assets/') + 8);
}
}
}
}
}
}
}
}
return array_diff($allThumbnails, $usedThumbnails);
}
/**
* @param bool $unlinked
* @return ArrayList

View File

@ -1764,10 +1764,12 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
}
/**
* Rewrite a file URL on this page, after its been renamed. Triggers the onRenameLinkedAsset action on extensions.
* Rewrites any linked images on this page.
* Non-image files should be linked via shortcodes
* Triggers the onRenameLinkedAsset action on extensions.
* TODO: This doesn't work for HTMLText fields on other tables.
*/
public function rewriteFileURL($old, $new) {
$fields = $this->inheritedDatabaseFields();
public function rewriteFileLinks() {
// Update the content without actually creating a new version
foreach(array("SiteTree_Live", "SiteTree") as $table) {
// Published site
@ -1777,15 +1779,20 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
)->record();
$origPublished = $published;
foreach($fields as $fieldName => $fieldType) {
if ($fieldType != 'HTMLText') continue;
foreach($this->db() as $fieldName => $fieldType) {
// Skip if non HTML or if empty
if ($fieldType !== 'HTMLText' || empty($published[$fieldName])) {
continue;
}
// Regenerate content
$content = Image::regenerate_html_links($published[$fieldName]);
if($content === $published[$fieldName]) {
continue;
}
// TODO: This doesn't work for HTMLText fields on other tables.
if(isset($published[$fieldName])) {
$published[$fieldName] = str_replace($old, $new, $published[$fieldName], $numReplaced);
if($numReplaced) {
$query = sprintf('UPDATE "%s" SET "%s" = ? WHERE "ID" = ?', $table, $fieldName);
DB::prepared_query($query, array($published[$fieldName], $this->ID));
DB::prepared_query($query, array($content, $this->ID));
// Tell static caching to update itself
if($table == 'SiteTree_Live') {
@ -1796,8 +1803,6 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
}
}
}
}
}
/**
* Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.

View File

@ -14,7 +14,8 @@ class SiteTreeFileExtension extends DataExtension {
ReadonlyField::create(
'BackLinkCount',
_t('AssetTableField.BACKLINKCOUNT', 'Used on:'),
$this->BackLinkTracking()->Count() . ' ' . _t('AssetTableField.PAGES', 'page(s)'))
$this->BackLinkTracking()->Count() . ' ' . _t('AssetTableField.PAGES', 'page(s)')
)
->addExtraClass('cms-description-toggle')
->setDescription($this->BackLinkHTMLList()),
'LastEdited'
@ -27,19 +28,21 @@ class SiteTreeFileExtension extends DataExtension {
* @return String
*/
public function BackLinkHTMLList() {
$html = '<em>' . _t('SiteTreeFileExtension.BACKLINK_LIST_DESCRIPTION', 'This list shows all pages where the file has been added through a WYSIWYG editor.') . '</em>';
$html = '<em>' . _t(
'SiteTreeFileExtension.BACKLINK_LIST_DESCRIPTION',
'This list shows all pages where the file has been added through a WYSIWYG editor.'
) . '</em>';
$html .= '<ul>';
foreach ($this->BackLinkTracking() as $backLink) {
$listItem = '<li>';
// Add the page link
$listItem .= '<a href="' . $backLink->Link() . '" target="_blank">' . Convert::raw2xml($backLink->MenuTitle) . '</a> &ndash; ';
// Add the CMS link
$listItem .= '<a href="' . $backLink->CMSEditLink() . '">' . _t('SiteTreeFileExtension.EDIT', 'Edit') . '</a>';
$html .= $listItem . '</li>';
// Add the page link and CMS link
$html .= sprintf(
'<li><a href="%s" target="_blank">%s</a> &ndash; <a href="%s">%s</a></li>',
Convert::raw2att($backLink->Link()),
Convert::raw2xml($backLink->MenuTitle),
Convert::raw2att($backLink->CMSEditLink()),
_t('SiteTreeFileExtension.EDIT', 'Edit')
);
}
return $html .= '</ul>';
@ -54,31 +57,13 @@ class SiteTreeFileExtension extends DataExtension {
* @param string $limit
* @return ManyManyList
*/
public function BackLinkTracking($filter = null, $sort = null, $join = null, $limit = null) {
if($filter !== null || $sort !== null || $join !== null || $limit !== null) {
Deprecation::notice('4.0', 'The $filter, $sort, $join and $limit parameters for
SiteTreeFileExtension::BackLinkTracking() have been deprecated.
Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
}
public function BackLinkTracking() {
if(class_exists("Subsite")){
$rememberSubsiteFilter = Subsite::$disable_subsite_filter;
Subsite::disable_subsite_filter(true);
}
if($filter || $sort || $join || $limit) {
Deprecation::notice('4.0', 'The $filter, $sort, $join and $limit parameters for
SiteTreeFileExtension::BackLinkTracking() have been deprecated.
Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
}
$links = $this->owner->getManyManyComponents('BackLinkTracking');
if($this->owner->ID) {
$links = $links
->where($filter)
->sort($sort)
->limit($limit);
}
$this->owner->extend('updateBackLinkTracking', $links);
if(class_exists("Subsite")){
@ -115,7 +100,9 @@ class SiteTreeFileExtension extends DataExtension {
// This will syncLinkTracking on draft
Versioned::reading_stage('Stage');
$brokenPages = DataObject::get('SiteTree')->byIDs($brokenPageIDs);
foreach($brokenPages as $brokenPage) $brokenPage->write();
foreach($brokenPages as $brokenPage) {
$brokenPage->write();
}
// This will syncLinkTracking on published
Versioned::reading_stage('Live');
@ -131,22 +118,23 @@ class SiteTreeFileExtension extends DataExtension {
/**
* Rewrite links to the $old file to now point to the $new file.
*
* @uses SiteTree->rewriteFileURL()
*
* @param String $old File path relative to the webroot
* @param String $new File path relative to the webroot
* @uses SiteTree->rewriteFileID()
*/
public function updateLinks($old, $new) {
if(class_exists('Subsite')) Subsite::disable_subsite_filter(true);
$pages = $this->owner->BackLinkTracking();
$summary = "";
if($pages) {
foreach($pages as $page) $page->rewriteFileURL($old,$new);
public function updateLinks() {
if(class_exists('Subsite')) {
Subsite::disable_subsite_filter(true);
}
if(class_exists('Subsite')) Subsite::disable_subsite_filter(false);
$pages = $this->owner->BackLinkTracking();
if($pages) {
foreach($pages as $page) {
$page->rewriteFileLinks();
}
}
if(class_exists('Subsite')) {
Subsite::disable_subsite_filter(false);
}
}
}

View File

@ -12,13 +12,13 @@
* referenced in any HTMLText fields, and two booleans to indicate if there are any broken links. Call
* augmentSyncLinkTracking to update those fields with any changes to those fields.
*
* @property SiteTree owner
* @property SiteTree $owner
*
* @property bool HasBrokenFile
* @property bool HasBrokenLink
* @property bool $HasBrokenFile
* @property bool $HasBrokenLink
*
* @method ManyManyList LinkTracking List of site pages linked on this page.
* @method ManyManyList ImageTracking List of Images linked on this page.
* @method ManyManyList LinkTracking() List of site pages linked on this page.
* @method ManyManyList ImageTracking() List of Images linked on this page.
*/
class SiteTreeLinkTracking extends DataExtension {
@ -110,22 +110,25 @@ class SiteTreeLinkTracking extends DataExtension {
// Add file tracking for image references
if($images = $htmlValue->getElementsByTagName('img')) foreach($images as $img) {
if($image = File::find($path = urldecode(Director::makeRelative($img->getAttribute('src'))))) {
// {@see HtmlEditorField} for data-fileid source
$fileID = $img->getAttribute('data-fileid');
if(!$fileID) {
continue;
}
// Assuming a local file is linked, check if it's valid
if($image = File::get()->byID($fileID)) {
$linkedFiles[] = $image->ID;
} else {
if(substr($path, 0, strlen(ASSETS_DIR) + 1) == ASSETS_DIR . '/') {
$record->HasBrokenFile = true;
}
}
}
// Update the "LinkTracking" many_many
if($record->ID && $record->manyManyComponent('LinkTracking') && $tracker = $record->LinkTracking()) {
$tracker->removeByFilter(sprintf(
'"FieldName" = \'%s\' AND "%s" = %d',
$fieldName,
$tracker->getForeignKey(),
$record->ID
if($record->ID && $record->manyManyComponent('LinkTracking') && ($tracker = $record->LinkTracking())) {
$tracker->removeByFilter(array(
sprintf('"FieldName" = ? AND "%s" = ?', $tracker->getForeignKey())
=> array($fieldName, $record->ID)
));
if($linkedPages) foreach($linkedPages as $item) {
@ -134,12 +137,10 @@ class SiteTreeLinkTracking extends DataExtension {
}
// Update the "ImageTracking" many_many
if($record->ID && $record->manyManyComponent('ImageTracking') && $tracker = $record->ImageTracking()) {
$tracker->removeByFilter(sprintf(
'"FieldName" = \'%s\' AND "%s" = %d',
$fieldName,
$tracker->getForeignKey(),
$record->ID
if($record->ID && $record->manyManyComponent('ImageTracking') && ($tracker = $record->ImageTracking())) {
$tracker->removeByFilter(array(
sprintf('"FieldName" = ? AND "%s" = ?', $tracker->getForeignKey())
=> array($fieldName, $record->ID)
));
if($linkedFiles) foreach($linkedFiles as $item) {

View File

@ -1,25 +0,0 @@
<?php
/**
* @package cms
* @subpackage assets
*/
class FilesystemSyncTask extends BuildTask {
protected $title = "Sync Files & Images assets";
protected $description = "The Files & Images system in SilverStripe maintains its own database
of the contents of the assets/ folder. This action will update that database, and
should be called whenever files are added to the assets/ folder from outside
SilverStripe, for example, if an author uploads files via FTP.";
public function run($request) {
if(isset($_GET['folderID'])) {
$folderID = $_GET['folderID'];
} else {
$folderID = null;
}
echo Filesystem::sync($folderID, !($request->getVar('skipSyncLinkTracking'))) . "\n";
}
}

View File

@ -32,7 +32,7 @@ So that I can link to a external website or a page on my site
And I fill in the "internal" dropdown with "Details"
And I wait for 1 second
And I select "youranchor" from "Form_EditorToolbarLinkForm_AnchorSelector"
And I press the "Insert link" button
And I press the "Insert" button
Then the "Content" HTML field should contain "<a href="[sitetree_link,id=3]#youranchor">awesome</a>"
# Required to avoid "unsaved changes" browser dialog
Then I press the "Save draft" button
@ -53,8 +53,8 @@ So that I can link to a external website or a page on my site
When I press the "Insert Link" button
When I select the "Download a file" radio button
And I attach the file "testfile.jpg" to "file[Uploads][]" with HTML5
And I press the "Insert link" button
Then the "Content" HTML field should contain "<a href="[file_link,id=3]" target="_blank">awesome</a>"
And I press the "Insert" button
Then the "Content" HTML field should contain "<a href="[file_link,id=4]" target="_blank">awesome</a>"
# Required to avoid "unsaved changes" browser dialog
Then I press the "Save draft" button
@ -64,7 +64,7 @@ So that I can link to a external website or a page on my site
When I press the "Insert Link" button
When I select the "Anchor on this page" radio button
And I select "myanchor" from "Form_EditorToolbarLinkForm_AnchorSelector"
And I press the "Insert link" button
And I press the "Insert" button
Then the "Content" HTML field should contain "<a href="#myanchor">awesome</a>"
# Required to avoid "unsaved changes" browser dialog
Then I press the "Save draft" button
@ -77,7 +77,7 @@ So that I can link to a external website or a page on my site
Then the "Form_EditorToolbarLinkForm_external" field should contain "http://silverstripe.org"
# This doesn't seem to suffer from that issue
When I fill in "http://google.com" for "URL"
And I press the "Insert link" button
And I press the "Insert" button
Then the "Content" HTML field should contain "<a href="http://google.com">awesome</a>"
# Required to avoid "unsaved changes" browser dialog
Then I press the "Save draft" button

View File

@ -18,7 +18,7 @@ Feature: Insert an image into a page
When I click "add by URL" in the ".ss-uploadfield-item-info" element
And I fill in "RemoteURL" with "http://www.silverstripe.org/themes/ssv3/img/ss_logo.png"
And I press the "Add url" button
Then I should see "ss_logo.png (www.silverstripe.org)" in the ".ss-assetuploadfield span.name" element
Then I should see "ss_logo.png" in the ".ss-assetuploadfield span.name" element
When I press the "Insert" button
Then the "Content" HTML field should contain "ss_logo.png"
@ -31,9 +31,9 @@ Feature: Insert an image into a page
And I attach the file "testfile.jpg" to "AssetUploadField" with HTML5
# TODO Delay previous step until upload succeeded
And I wait for 2 seconds
Then there should be a file "assets/Uploads/testfile.jpg"
Then there should be a file "assets/Uploads/59de0c841f/testfile.jpg"
When I press the "Insert" button
Then the "Content" HTML field should contain "testfile.jpg"
Then the "Content" HTML field should contain "testfile__Resampled.jpg"
# Required to avoid "unsaved changed" browser dialog
Then I press the "Save draft" button
@ -45,10 +45,10 @@ Feature: Insert an image into a page
# TODO Delay previous step until upload succeeded
And I wait for 2 seconds
# Note change in default behaviour from 3.1, respect default Upload.replaceFile=false
Then there should be a file "assets/Uploads/file1.jpg"
And there should be a file "assets/Uploads/file1-v2.jpg"
Then there should be a file "assets/Uploads/3d0ef6ec37/file1.jpg"
And there should be a file "assets/Uploads/3d0ef6ec37/file1-v2.jpg"
When I press the "Insert" button
Then the "Content" HTML field should contain "file1-v2.jpg"
Then the "Content" HTML field should contain "file1-v2__Resampled.jpg"
# Required to avoid "unsaved changed" browser dialog
Then I press the "Save draft" button
@ -57,7 +57,7 @@ Feature: Insert an image into a page
And I fill in the "ParentID" dropdown with "folder1"
And I click on "file1" in the "Files" table
When I press the "Insert" button
Then the "Content" HTML field should contain "file1.jpg"
Then the "Content" HTML field should contain "file1__Resampled.jpg"
# Required to avoid "unsaved changed" browser dialog
Then I press the "Save draft" button
@ -68,7 +68,7 @@ Feature: Insert an image into a page
And I press the "Edit" button
When I fill in "Alternative text (alt)" with "My alt"
And I press the "Insert" button
Then the "Content" HTML field should contain "file1.jpg"
Then the "Content" HTML field should contain "file1__Resampled.jpg"
And the "Content" HTML field should contain "My alt"
# Required to avoid "unsaved changed" browser dialog
Then I press the "Save draft" button
@ -76,14 +76,14 @@ Feature: Insert an image into a page
# TODO This needs to support using drag handles, as we no longer have 'Width' or 'Height' input fields
@todo
Scenario: I can edit dimensions of an existing image
Given the "page" "About us" contains "<img src=assets/folder1/file1.jpg>"
Given the "page" "About us" contains "<img src=assets/folder1/3d0ef6ec37/file1.jpg>"
And I reload the current page
When I highlight "<img src=assets/folder1/file1.jpg>" in the "Content" HTML field
When I highlight "<img src=assets/folder1/3d0ef6ec37/file1.jpg>" in the "Content" HTML field
And I press the "Insert Media" button
Then I should see "file1.jpg"
When I fill in "Width" with "10"
When I fill in "Height" with "20"
And I press the "Insert" button
Then the "Content" HTML field should contain "<img src=assets/folder1/file1.jpg width=10 height=20>"
Then the "Content" HTML field should contain "<img src=assets/folder1/3d0ef6ec37/file1.jpg width=10 height=20>"
# Required to avoid "unsaved changed" browser dialog
Then I press the "Save draft" button

View File

@ -6,7 +6,7 @@ Feature: Manage files
Background:
Given a "image" "assets/folder1/file1.jpg" was created "2012-01-01 12:00:00"
And a "image" "assets/folder1/folder1.1/file2.jpg" was created "2010-01-01 12:00:00"
And a "image" "assets/folder1/folder1-1/file2.jpg" was created "2010-01-01 12:00:00"
And a "folder" "assets/folder2"
And I am logged in with "ADMIN" permissions
And I go to "/admin/assets"
@ -21,7 +21,7 @@ Feature: Manage files
Scenario: I can list files in a folder
Given I click on "folder1" in the "Files" table
Then the "folder1" table should contain "file1"
And the "folder1" table should not contain "file1.1"
And the "folder1" table should not contain "file1-1"
Scenario: I can upload a file to a folder
Given I click on "folder1" in the "Files" table

View File

@ -8,40 +8,52 @@ class FileLinkTrackingTest extends SapphireTest {
public function setUp() {
parent::setUp();
AssetStoreTest_SpyStore::activate('FileLinkTrackingTest');
$this->logInWithPermission('ADMIN');
if(!file_exists(ASSETS_PATH)) mkdir(ASSETS_PATH);
$fh = fopen(ASSETS_PATH . '/testscript-test-file.pdf', "w");
fwrite($fh, str_repeat('x',1000000));
fclose($fh);
// Write file contents
$files = File::get()->exclude('ClassName', 'Folder');
foreach($files as $file) {
$destPath = AssetStoreTest_SpyStore::getLocalPath($file);
Filesystem::makeFolder(dirname($destPath));
file_put_contents($destPath, str_repeat('x', 1000000));
}
// Since we can't hard-code IDs, manually inject image tracking shortcode
$imageID = $this->idFromFixture('Image', 'file1');
$page = $this->objFromFixture('Page', 'page1');
$page->Content = sprintf(
'<p><img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg" data-fileid="%d" /></p>',
$imageID
);
$page->write();
}
public function tearDown() {
AssetStoreTest_SpyStore::reset();
parent::tearDown();
$testFiles = array(
'/testscript-test-file.pdf',
'/renamed-test-file.pdf',
'/renamed-test-file-second-time.pdf',
);
foreach($testFiles as $file) {
if(file_exists(ASSETS_PATH . $file)) unlink(ASSETS_PATH . $file);
}
}
public function testFileRenameUpdatesDraftAndPublishedPages() {
$page = $this->objFromFixture('Page', 'page1');
$this->assertTrue($page->doPublish());
$this->assertContains('<img src="assets/testscript-test-file.pdf"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value());
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value()
);
$file = $this->objFromFixture('File', 'file1');
$file->Name = 'renamed-test-file.pdf';
$file = $this->objFromFixture('Image', 'file1');
$file->Name = 'renamed-test-file.jpg';
$file->write();
$this->assertContains('<img src="assets/renamed-test-file.pdf"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value());
$this->assertContains('<img src="assets/renamed-test-file.pdf"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value());
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value()
);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value()
);
}
public function testFileLinkRewritingOnVirtualPages() {
@ -56,15 +68,19 @@ class FileLinkTrackingTest extends SapphireTest {
$svp->doPublish();
// Rename the file
$file = $this->objFromFixture('File', 'file1');
$file->Name = 'renamed-test-file.pdf';
$file = $this->objFromFixture('Image', 'file1');
$file->Name = 'renamed-test-file.jpg';
$file->write();
// Verify that the draft and publish virtual pages both have the corrected link
$this->assertContains('<img src="assets/renamed-test-file.pdf"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($svp->ID))->value());
$this->assertContains('<img src="assets/renamed-test-file.pdf"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($svp->ID))->value());
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($svp->ID))->value()
);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($svp->ID))->value()
);
}
public function testLinkRewritingOnAPublishedPageDoesntMakeItEditedOnDraft() {
@ -74,8 +90,8 @@ class FileLinkTrackingTest extends SapphireTest {
$this->assertFalse($page->getIsModifiedOnStage());
// Rename the file
$file = $this->objFromFixture('File', 'file1');
$file->Name = 'renamed-test-file.pdf';
$file = $this->objFromFixture('Image', 'file1');
$file->Name = 'renamed-test-file.jpg';
$file->write();
// Caching hack
@ -89,25 +105,30 @@ class FileLinkTrackingTest extends SapphireTest {
public function testTwoFileRenamesInARowWork() {
$page = $this->objFromFixture('Page', 'page1');
$this->assertTrue($page->doPublish());
$this->assertContains('<img src="assets/testscript-test-file.pdf"',
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value());
// Rename the file twice
$file = $this->objFromFixture('File', 'file1');
$file->Name = 'renamed-test-file.pdf';
$file = $this->objFromFixture('Image', 'file1');
$file->Name = 'renamed-test-file.jpg';
$file->write();
// TODO Workaround for bug in DataObject->getChangedFields(), which returns stale data,
// and influences File->updateFilesystem()
$file = DataObject::get_by_id('File', $file->ID);
$file->Name = 'renamed-test-file-second-time.pdf';
$file->Name = 'renamed-test-file-second-time.jpg';
$file->write();
// Confirm that the correct image is shown in both the draft and live site
$this->assertContains('<img src="assets/renamed-test-file-second-time.pdf"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value());
$this->assertContains('<img src="assets/renamed-test-file-second-time.pdf"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value());
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file-second-time.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value()
);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file-second-time.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value()
);
}
}

View File

@ -1,10 +1,12 @@
# These need to come first so that SiteTree has the link meta-data written.
File:
Image:
file1:
Filename: assets/testscript-test-file.pdf
FileFilename: testscript-test-file.jpg
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: testscript-test-file.jpg
Page:
page1:
Title: page1
URLSegment: page1
Content: <p><img src="assets/testscript-test-file.pdf" /></p>
Content: '<p><img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg" /></p>'

View File

@ -4,6 +4,25 @@ class SiteTreeHtmlEditorFieldTest extends FunctionalTest {
protected static $use_draft_site = true;
public function setUp() {
parent::setUp();
AssetStoreTest_SpyStore::activate('SiteTreeHtmlEditorFieldTest');
$this->logInWithPermission('ADMIN');
// Write file contents
$files = File::get()->exclude('ClassName', 'Folder');
foreach($files as $file) {
$destPath = AssetStoreTest_SpyStore::getLocalPath($file);
Filesystem::makeFolder(dirname($destPath));
file_put_contents($destPath, str_repeat('x', 1000000));
}
}
public function tearDown() {
AssetStoreTest_SpyStore::reset();
parent::tearDown();
}
public function testLinkTracking() {
$sitetree = $this->objFromFixture('SiteTree', 'home');
$editor = new HtmlEditorField('Content');
@ -103,20 +122,23 @@ class SiteTreeHtmlEditorFieldTest extends FunctionalTest {
public function testImageTracking() {
$sitetree = $this->objFromFixture('SiteTree', 'home');
$editor = new HtmlEditorField('Content');
$fileID = $this->idFromFixture('Image', 'example_image');
$file = $this->objFromFixture('Image', 'example_image');
$editor->setValue('<img src="assets/example.jpg" />');
$editor->setValue(sprintf('<img src="%s" data-fileid="%d" />', $file->getURL(), $file->ID));
$editor->saveInto($sitetree);
$sitetree->write();
$this->assertEquals (
array($fileID => $fileID), $sitetree->ImageTracking()->getIDList(), 'Inserted images are tracked.'
$this->assertEquals(
array($file->ID => $file->ID),
$sitetree->ImageTracking()->getIDList(),
'Inserted images are tracked.'
);
$editor->setValue(null);
$editor->saveInto($sitetree);
$sitetree->write();
$this->assertEquals (
array(), $sitetree->ImageTracking()->getIDList(), 'Tracked images are deleted when removed.'
$this->assertEmpty(
$sitetree->ImageTracking()->getIDList(),
'Tracked images are deleted when removed.'
);
}

View File

@ -8,8 +8,12 @@ SiteTree:
File:
example_file:
FileFilename: example.pdf
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: example.pdf
Image:
example_image:
FileFilename: example.jpg
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: example.jpg

View File

@ -2,7 +2,7 @@
class SiteTreeLinkTrackingTest extends SapphireTest {
function isBroken($content) {
protected function isBroken($content) {
$parser = new SiteTreeLinkTracking_Parser();
$htmlValue = Injector::inst()->create('HTMLValue', $content);
$links = $parser->process($htmlValue);
@ -11,7 +11,7 @@ class SiteTreeLinkTrackingTest extends SapphireTest {
return $links[0]['Broken'];
}
function testParser() {
public function testParser() {
$this->assertTrue($this->isBroken('<a href="[sitetree_link,id=123]">link</a>'));
$this->assertTrue($this->isBroken('<a href="[sitetree_link,id=123]#no-such-anchor">link</a>'));
$this->assertTrue($this->isBroken('<a href="[file_link,id=123]">link</a>'));
@ -34,14 +34,14 @@ class SiteTreeLinkTrackingTest extends SapphireTest {
$this->assertFalse($this->isBroken("<a href=\"[file_link,id=$file->ID]\">link</a>"));
}
function highlight($content) {
protected function highlight($content) {
$page = new Page();
$page->Content = $content;
$page->write();
return $page->Content;
}
function testHighlighter() {
public function testHighlighter() {
$content = $this->highlight('<a href="[sitetree_link,id=123]" class="existing-class">link</a>');
$this->assertEquals(substr_count($content, 'ss-broken'), 1, 'A ss-broken class is added to the broken link.');
$this->assertEquals(substr_count($content, 'existing-class'), 1, 'Existing class is not removed.');
@ -60,4 +60,16 @@ class SiteTreeLinkTrackingTest extends SapphireTest {
$this->assertEquals(substr_count($content, 'existing-class'), 1, 'Existing class is not removed.');
}
public function testHasBrokenFile() {
$this->assertTrue($this->pageIsBrokenFile('<img src="someurl.jpg" data-fileid="99999999" />'));
$this->assertFalse($this->pageIsBrokenFile('<img src="someurl.jpg" />'));
}
protected function pageIsBrokenFile($content) {
$page = new Page();
$page->Content = $content;
$page->write();
return $page->HasBrokenFile;
}
}