silverstripe-cms/code/AssetAdmin.php

677 lines
19 KiB
PHP
Raw Normal View History

<?php
/**
* AssetAdmin is the 'file store' section of the CMS.
* It provides an interface for maniupating the File and Folder objects in the system.
*/
class AssetAdmin extends LeftAndMain {
static $tree_class = "File";
public function Link($action=null) {
if(!$action) $action = "index";
return "admin/assets/$action/" . $this->currentPageID();
}
/**
* Return fake-ID "root" if no ID is found (needed to upload files into the root-folder)
*/
public function currentPageID() {
if(isset($_REQUEST['ID']) && is_numeric($_REQUEST['ID'])) {
return $_REQUEST['ID'];
} elseif (is_numeric($this->urlParams['ID'])) {
return $this->urlParams['ID'];
} elseif(is_numeric(Session::get("{$this->class}.currentPage"))) {
return Session::get("{$this->class}.currentPage");
} else {
return "root";
}
}
/**
* Set up the controller, in particular, re-sync the File database with the assets folder./
*/
function init() {
parent::init();
// needed for MemberTableField (Requirements not determined before Ajax-Call)
Requirements::javascript("sapphire/javascript/ComplexTableField.js");
Requirements::css("jsparty/greybox/greybox.css");
Requirements::css("sapphire/css/ComplexTableField.css");
Requirements::javascript("cms/javascript/AssetAdmin.js");
Requirements::javascript("cms/javascript/AssetAdmin_left.js");
Requirements::javascript("cms/javascript/AssetAdmin_right.js");
// Requirements::javascript('sapphire/javascript/TableListField.js');
// Include the right JS]
// Hayden: This didn't appear to be used at all
/*$fileList = new FileList("Form_EditForm_Files", null);
$fileList->setClick_AjaxLoad('admin/assets/getfile/', 'Form_SubForm');
$fileList->FieldHolder();*/
Requirements::javascript("jsparty/greybox/AmiJS.js");
Requirements::javascript("jsparty/greybox/greybox.js");
Requirements::css("jsparty/greybox/greybox.css");
}
/**
* Display the upload form. Returns an iframe tag that will show admin/assets/uploadiframe.
*/
function getUploadIframe() {
return <<<HTML
<iframe name="AssetAdmin_upload" src="admin/assets/uploadiframe/{$this->urlParams['ID']}" id="AssetAdmin_upload" border="0" style="border-style: none; width: 100%; height: 200px">
</iframe>
HTML;
}
function index() {
File::sync();
return array();
}
/**
* Show the content of the upload iframe. The form is specified by a template.
*/
function uploadiframe() {
Requirements::clear();
Requirements::javascript("jsparty/prototype.js");
Requirements::javascript("jsparty/loader.js");
Requirements::javascript("jsparty/behaviour.js");
Requirements::javascript("jsparty/prototype_improvements.js");
Requirements::javascript("jsparty/layout_helpers.js");
Requirements::javascript("cms/javascript/LeftAndMain.js");
Requirements::javascript("jsparty/multifile/multifile.js");
Requirements::css("jsparty/multifile/multifile.css");
Requirements::css("cms/css/typography.css");
Requirements::css("cms/css/layout.css");
Requirements::css("cms/css/cms_left.css");
Requirements::css("cms/css/cms_right.css");
if(isset($data['ID']) && $data['ID'] != 'root') $folder = DataObject::get_by_id("Folder", $data['ID']);
else $folder = singleton('Folder');
$canUpload = $folder->userCanEdit();
return array( 'CanUpload' => $canUpload );
}
/**
* Return the form object shown in the uploadiframe.
*/
function UploadForm() {
return new Form($this,'UploadForm', new FieldSet(
new HiddenField("ID", "", $this->currentPageID()),
// needed because the button-action is triggered outside the iframe
new HiddenField("action_doUpload", "", "1"),
new FileField("Files[0]" , "Choose file "),
new LiteralField('UploadButton',"
<input type='submit' value='Upload Files Listed Below' name='action_upload' id='Form_UploadForm_action_upload' class='action' />
"),
new LiteralField('MultifileCode',"
<p>Files ready to upload:</p>
<div id='Form_UploadForm_FilesList'></div>
<script>
var multi_selector = new MultiSelector($('Form_UploadForm_FilesList'), null, $('Form_UploadForm_action_upload'));
multi_selector.addElement($('Form_UploadForm_Files-0'));
</script>
")
), new FieldSet(
));
}
/**
* This method processes the results of the UploadForm.
* It will save the uploaded files to /assets/ and create new File objects as required.
*/
function doUpload($data, $form) {
foreach($data['Files'] as $param => $files) {
foreach($files as $key => $value) {
$processedFiles[$key][$param] = $value;
}
}
if($data['ID'] && $data['ID'] != 'root') $folder = DataObject::get_by_id("Folder", $data['ID']);
else $folder = singleton('Folder');
$newFiles = array();
$fileSizeWarnings = '';
$uploadErrors = '';
foreach($processedFiles as $file) {
if($file['error'] == UPLOAD_ERR_NO_TMP_DIR) {
$status = 'bad';
$statusMessage = 'There is no temporary folder for uploads. Please set upload_tmp_dir in php.ini.';
break;
}
if($file['tmp_name']) {
// Workaround open_basedir problems
if(ini_get("open_basedir")) {
$newtmp = TEMP_FOLDER . '/' . $file['name'];
move_uploaded_file($file['tmp_name'], $newtmp);
$file['tmp_name'] = $newtmp;
}
// check that the file can be uploaded and isn't too large
$extensionIndex = strripos( $file['name'], '.' );
$extension = strtolower( substr( $file['name'], $extensionIndex + 1 ) );
if( $extensionIndex !== FALSE )
list( $maxSize, $warnSize ) = File::getMaxFileSize( $extension );
else
list( $maxSize, $warnSize ) = File::getMaxFileSize();
// check that the file is not too large or that the current user is an administrator
if( $this->can('AdminCMS') || ( File::allowedFileType( $extension ) && (!isset($maxsize) || $file['size'] < $maxSize)))
$newFiles[] = $folder->addUploadToFolder($file);
elseif( !File::allowedFileType( $extension ) ) {
$fileSizeWarnings .= "alert( 'Only administrators can upload $extension files.' );";
} else {
if( $file['size'] > 1048576 )
$fileSize = "" . ceil( $file['size'] / 1048576 ) . "MB";
elseif( $file['size'] > 1024 )
$fileSize = "" . ceil( $file['size'] / 1024 ) . "KB";
else
$fileSize = "" . ceil( $file['size'] ) . "B";
$fileSizeWarnings .= "alert( '\\'" . $file['name'] . "\\' is too large ($fileSize). Files of this type cannot be larger than $warnSize ' );";
}
}
}
if($newFiles) {
$numFiles = sizeof($newFiles);
$statusMessage = "Uploaded $numFiles files";
$status = "good";
} else if($status != 'bad') {
$statusMessage = "There was nothing to upload";
$status = "";
}
echo <<<HTML
<script type="text/javascript">
var form = parent.document.getElementById('Form_EditForm');
form.getPageFromServer(form.elements.ID.value);
parent.statusMessage("{$statusMessage}","{$status}");
$fileSizeWarnings
parent.document.getElementById('sitetree').getTreeNodeByIdx( "{$folder->ID}" ).getElementsByTagName('a')[0].className += ' contents';
</script>
HTML;
}
/**
* Needs to be overridden to make sure an ID with value "0" is still valid (rootfolder)
*/
/**
* Return the form that displays the details of a folder, including a file list and fields for editing the folder name.
*/
function getEditForm($id) {
if($id && $id != "root") {
$record = DataObject::get_by_id("File", $id);
} else {
$record = singleton("Folder");
}
$fileList = new AssetTableField(
$this,
"Files",
"File",
array("Title" => "Title", "LinkedURL" => "Filename"),
""
);
$fileList->setFolder($record);
$fileList->setPopupCaption("View/Edit Asset");
if($record) {
$nameField = ($id != "root") ? new TextField("Name") : new HiddenField("Name");
$fields = new FieldSet(
new HiddenField("Title"),
$nameField,
new TabSet("Root",
new Tab("Files",
$fileList
),
new Tab("Details",
new ReadonlyField("URL"),
new ReadonlyField("ClassName", "Type"),
new ReadonlyField("Created", "First Uploaded"),
new ReadonlyField("LastEdited", "Last Updated")
),
new Tab("Upload",
new LiteralField("UploadIframe",
$this->getUploadIframe()
)
)
),
new HiddenField("ID")
);
$actions = new FieldSet();
if( $record->userCanEdit() ) {
$actions = new FieldSet(
new FormAction('deletemarked',"Delete files"),
new FormAction('movemarked',"Move files..."),
new FormAction('save',"Save")
);
}
$form = new Form($this, "EditForm", $fields, $actions);
if($record->ID) {
$form->loadDataFrom($record);
} else {
$form->loadDataFrom(array(
"ID" => "root",
"URL" => Director::absoluteBaseURL() . 'assets/',
));
}
// @todo: These workflow features aren't really appropriate for all projects
if( Member::currentUser()->_isAdmin() && project() == 'mot' ) {
$fields->addFieldsToTab( 'Root.Workflow', new DropdownField("Owner", "Owner", Member::map() ) );
$fields->addFieldsToTab( 'Root.Workflow', new TreeMultiselectField("CanUse", "Content usable by") );
$fields->addFieldsToTab( 'Root.Workflow', new TreeMultiselectField("CanEdit", "Content modifiable by") );
}
if( !$record->userCanEdit() )
$form->makeReadonly();
return $form;
}
}
/**
* Returns the form used to specify options for the "move marked" action.
*/
public function MoveMarkedOptionsForm() {
$folderDropdown = new TreeDropdownField("DestFolderID", "Move files to", "Folder");
$folderDropdown->setFilterFunction(create_function('$obj', 'return $obj->class == "Folder";'));
return new CMSActionOptionsForm($this, "MoveMarkedOptionsForm", new FieldSet(
new HiddenField("ID"),
new HiddenField("FileIDs"),
$folderDropdown
),
new FieldSet(
new FormAction("movemarked", "Move marked files")
));
}
/**
* Perform the "move marked" action.
* Called by ajax, with a JavaScript return.
*/
public function movemarked() {
if($_REQUEST['DestFolderID'] && is_numeric($_REQUEST['DestFolderID'])) {
$destFolderID = $_REQUEST['DestFolderID'];
$fileList = "'" . ereg_replace(' *, *',"','",trim(addslashes($_REQUEST['FileIDs']))) . "'";
$numFiles = 0;
if($fileList != "''") {
$files = DataObject::get("File", "`File`.ID IN ($fileList)");
if($files) {
foreach($files as $file) {
$file->ParentID = $destFolderID;
$file->write();
$numFiles++;
}
} else {
user_error("No files in $fileList could be found!", E_USER_ERROR);
}
}
echo <<<JS
statusMessage("Moved $numFiles files");
JS;
} else {
user_error("Bad data: $_REQUEST[DestFolderID]", E_USER_ERROR);
}
}
/**
* Returns the form used to specify options for the "delete marked" action.
* In actual fact, this form only has hidden fields and the button is auto-clickd without the
* form being displayed; it's just the most consistent way of providing this information to the
* CMS.
*/
public function DeleteMarkedOptionsForm() {
return new CMSActionOptionsForm($this, "DeleteMarkedOptionsForm", new FieldSet(
new HiddenField("ID"),
new HiddenField("FileIDs")
),
new FieldSet(
new FormAction("deletemarked", "Delete marked files")
));
}
/**
* Perform the "delete marked" action.
* Called by ajax, with a JavaScript return.
*/
public function deletemarked() {
$fileList = "'" . ereg_replace(' *, *',"','",trim(addslashes($_REQUEST['FileIDs']))) . "'";
$numFiles = 0;
$folderID = 0;
$deleteList = '';
$brokenPageList = '';
if($fileList != "''") {
$files = DataObject::get("File", "`File`.ID IN ($fileList)");
if($files) {
foreach($files as $file) {
if( !$folderID )
$folderID = $file->ParentID;
// $deleteList .= "\$('Form_EditForm_Files').removeById($file->ID);\n";
$file->delete();
$numFiles++;
}
if($brokenPages = Notifications::getItems("BrokenLink")) {
$brokenPageList = " These pages now have broken links:</ul>";
foreach($brokenPages as $brokenPage) {
$brokenPageList .= "<li style=&quot;font-size: 65%&quot;>" . $brokenPage->Breadcrumbs(3, true) . "</li>";
}
$brokenPageList .= "</ul>";
Notifications::notifyByEmail("BrokenLink", "Page_BrokenLinkEmail");
} else {
$brokenPageList = '';
}
$deleteList = '';
if( $folderID ) {
$remaining = DB::query("SELECT COUNT(*) FROM `File` WHERE `ParentID`=$folderID")->value();
if( !$remaining )
$deleteList = "Element.removeClassName(\$('sitetree').getTreeNodeByIdx( '$folderID' ).getElementsByTagName('a')[0],'contents');";
}
} else {
user_error("No files in $fileList could be found!", E_USER_ERROR);
}
}
echo <<<JS
$deleteList
$('Form_EditForm').getPageFromServer($('Form_EditForm_ID').value);
statusMessage("Deleted $numFiles files.$brokenPageList");
JS;
}
/**
* Returns the content to be placed in Form_SubForm when editing a file.
* Called using ajax.
*/
public function getfile() {
SSViewer::setOption('rewriteHashlinks', false);
// bdc: only try to return something if user clicked on an object
if (is_object($this->getSubForm($this->urlParams['ID']))) {
return $this->getSubForm($this->urlParams['ID'])->formHtmlContent();
}
else return null;
}
/**
* Action handler for the save button on the file subform.
* Saves the file
*/
public function savefile($data, $form) {
$record = DataObject::get_by_id("File", $data['ID']);
$form->saveInto($record);
$record->write();
$title = Convert::raw2js($record->Title);
$name = Convert::raw2js($record->Name);
echo <<<JS
statusMessage('Saved file #$data[ID]');
$('record-$data[ID]').getElementsByTagName('td')[1].innerHTML = "$title";
$('record-$data[ID]').getElementsByTagName('td')[2].innerHTML = "$name";
JS;
}
/**
* Return the entire site tree as a nested set of ULs
*/
public function SiteTreeAsUL() {
$obj = singleton('Folder');
$obj->setMarkingFilter("ClassName", "Folder");
$obj->markPartialTree();
if($p = $this->currentPage()) $obj->markToExpose($p);
// getChildrenAsUL is a flexible and complex way of traversing the tree
$siteTree = $obj->getChildrenAsUL("",
' "<li id=\"record-$child->ID\" class=\"$child->class" . $child->markingClasses() . ($extraArg->isCurrentPage($child) ? " current" : "") . "\">" . ' .
' "<a href=\"" . Director::link(substr($extraArg->Link(),0,-1), "show", $child->ID) . "\" class=\"" . ($child->hasChildren() ? " contents" : "") . "\" >" . $child->Title . "</a>" ',
$this, true);
// Wrap the root if needs be.
$rootLink = $this->Link() . 'show/root';
if(!isset($rootID)) $siteTree = "<ul id=\"sitetree\" class=\"tree unformatted\"><li id=\"record-root\" class=\"Root\"><a href=\"$rootLink\">http://www.yoursite.com/assets</a>"
. $siteTree . "</li></ul>";
return $siteTree;
}
/**
* Returns a subtree of items underneat the given folder.
*/
public function getsubtree() {
$obj = DataObject::get_by_id("Folder", $_REQUEST['ID']);
$obj->setMarkingFilter("ClassName", "Folder");
$obj->markPartialTree();
$results = $obj->getChildrenAsUL("",
' "<li id=\"record-$child->ID\" class=\"$child->class" . $child->markingClasses() . ($extraArg->isCurrentPage($child) ? " current" : "") . "\">" . ' .
' "<a href=\"" . Director::link(substr($extraArg->Link(),0,-1), "show", $child->ID) . "\" >" . $child->Title . "</a>" ',
$this, true);
return substr(trim($results), 4,-5);
}
//------------------------------------------------------------------------------------------//
// Data saving handlers
/**
* Add a new folder and return its details suitable for ajax.
*/
public function addfolder() {
$parent = ($_REQUEST['ParentID'] && is_numeric($_REQUEST['ParentID'])) ? $_REQUEST['ParentID'] : 0;
if($parent) {
$parentObj = DataObject::get_by_id("File", $parent);
if(!$parentObj || !$parentObj->ID) $parent = 0;
}
$p = new Folder();
$p->ParentID = $parent;
$p->Title = "NewFolder";
$p->Name = "NewFolder";
// Get the folder to be created
if(isset($parentObj->ID)) $filename = $parentObj->FullPath . $p->Name;
else $filename = '../assets/' . $p->Name;
// Ensure uniqueness
$i = 2;
$baseFilename = $filename . '-';
while(file_exists($filename)) {
$filename = $baseFilename . $i;
$p->Name = $p->Title = basename($filename);
$i++;
}
// Actually create
mkdir($filename);
chmod($filename, 02775);
$p->write();
return $this->returnItemToUser($p);
}
/**
* Return the given tree item to the client.
* If called by ajax, this will be some javascript commands.
* Otherwise, it will redirect back.
*/
public function returnItemToUser($p) {
if($_REQUEST['ajax']) {
$parentID = (int)$p->ParentID;
return <<<JS
tree = $('sitetree');
var newNode = tree.createTreeNode($p->ID, "$p->Title", "$p->class");
tree.getTreeNodeByIdx($parentID).appendTreeNode(newNode);
newNode.selectTreeNode();
JS;
} else {
Director::redirectBack();
}
}
/**
* Delete a folder
*/
public function deletefolder() {
$script = '';
$ids = split(' *, *', $_REQUEST['csvIDs']);
$script = '';
foreach($ids as $id) {
if(is_numeric($id)) {
$record = DataObject::get_by_id($this->stat('tree_class'), $id);
if(!$record)
Debug::message( "Record appears to be null" );
/*if($record->hasMethod('BackLinkTracking')) {
$brokenPages = $record->BackLinkTracking();
foreach($brokenPages as $brokenPage) {
$brokenPageList .= "<li style=\"font-size: 65%\">" . $brokenPage->Breadcrumbs(3, true) . "</li>";
$brokenPage->HasBrokenLink = true;
$notifications[$brokenPage->OwnerID][] = $brokenPage;
$brokenPage->write();
}
}*/
$record->delete();
$record->destroy();
// DataObject::delete_by_id($this->stat('tree_class'), $id);
$script .= $this->deleteTreeNodeJS($record);
}
}
/*if($notifications) foreach($notifications as $memberID => $pages) {
$email = new Page_BrokenLinkEmail();
$email->populateTemplate(new ArrayData(array(
"Recipient" => DataObject::get_by_id("Member", $memberID),
"BrokenPages" => new DataObjectSet($pages),
)));
$email->debug();
$email->send();
}*/
$s = (sizeof($ids) > 1) ? "s" :"";
$message = sizeof($ids) . " folder$s deleted.";
//
if(isset($brokenPageList)) $message .= " The following pages now have broken links:<ul>" . addslashes($brokenPageList) . "</ul>Their owners have been emailed and they will fix up those pages.";
$script .= "statusMessage('$message');";
echo $script;
}
public function removefile(){
if($fileID = $this->urlParams['ID']){
$file = DataObject::get_by_id('File', $fileID);
$file->delete();
$file->destroy();
if(Director::is_ajax()) {
echo <<<JS
$('Form_EditForm_Files').removeFile($fileID);
statusMessage('removed file', 'good');
JS;
}else{
Director::redirectBack();
}
}else{
user_error("AssetAdmin::removefile: Bad parameters: File=$fileID", E_USER_ERROR);
}
}
public function save($urlParams, $form) {
// Don't save the root folder - there's no database record
if($_REQUEST['ID'] == 'root') {
FormResponse::status_message("Saved", "good");
return FormResponse::respond();
}
$form->dataFieldByName('Title')->value = $form->dataFieldByName('Name')->value;
return parent::save($urlParams, $form);
}
}