<?php

/**
 * Field for uploading single or multiple files of all types, including images.
 * <b>NOTE: this Field will call write() on the supplied record</b>
 * 
 * <b>Features (some might not be available to old browsers):</b>
 * 
 * - File Drag&Drop support
 * - Progressbar
 * - Image thumbnail/file icons even before upload finished
 * - Saving into relations
 * - Edit file
 * - allowedExtensions is by default File::$allowed_extensions<li>maxFileSize the value of min(upload_max_filesize,
 * post_max_size) from php.ini
 *
 * <>Usage</b>
 * 
 * @example <code>
 * $UploadField = new UploadField('myFiles', 'Please upload some images <span>(max. 5 files)</span>');
 * $UploadField->getValidator()->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif'));
 * $UploadField->setConfig('allowedMaxFileNumber', 5);
 * </code>
 * 
 * @author Zauberfisch
 * @package framework
 * @subpackage forms
 */
class UploadField extends FileField {

	/**
	 * @var array
	 */
	public static $allowed_actions = array(
		'upload',
		'attach',
		'handleItem',
		'handleSelect',
	);

	/**
	 * @var array
	 */
	public static $url_handlers = array(
		'item/$ID' => 'handleItem',
		'select' => 'handleSelect',
		'$Action!' => '$Action',
	);

	/**
	 * @var String
	 */
	protected $templateFileButtons = 'UploadField_FileButtons';

	/**
	 * @var String
	 */
	protected $templateFileEdit = 'UploadField_FileEdit';

	/**
	 * @var DataObject
	 */
	protected $record;

	/**
	 * @var SS_List
	 */
	protected $items;

	/**
	 * @var array Config for this field used in both, php and javascript 
	 * (will be merged into the config of the javascript file upload plugin).
	 * See framework/_config/uploadfield.yml for configuration defaults and documentation.
	 */
	protected $ufConfig = array(
		/**
		 * @var boolean
		 */
		'autoUpload' => true,
		/**
		 * 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
		 * @var int
		 */
		'allowedMaxFileNumber' => null,
		/**
		 * @var boolean|string Can the user upload new files, or just select from existing files.
		 * String values are interpreted as permission codes.
		 */
		'canUpload' => true,
		/**
		 * @var boolean|string Can the user attach files from the assets archive on the site?
		 * String values are interpreted as permission codes.
		 */
		'canAttachExisting' => "CMS_ACCESS_AssetAdmin",
		/**
		 * @var boolean If a second file is uploaded, should it replace the existing one rather than throwing an errror?
		 * This only applies for has_one relationships, and only replaces the association
		 * rather than the actual file database record or filesystem entry.
		 */
		'replaceExistingFile' => false,
		/**
		 * @var int
		 */
		'previewMaxWidth' => 80,
		/**
		 * @var int
		 */
		'previewMaxHeight' => 60,
		/**
		 * javascript template used to display uploading files
		 * @see javascript/UploadField_uploadtemplate.js
		 * @var string
		 */
		'uploadTemplateName' => 'ss-uploadfield-uploadtemplate',
		/**
		 * javascript template used to display already uploaded files
		 * @see javascript/UploadField_downloadtemplate.js
		 * @var string
		 */
		'downloadTemplateName' => 'ss-uploadfield-downloadtemplate',
		/**
		 * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm
		 * @example 'getCMSFields'
		 * @var FieldList|string
		 */
		'fileEditFields' => null,
		/**
		 * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
		 * @example 'getCMSActions'
		 * @var FieldList|string
		 */
		'fileEditActions' => null,
		/**
		 * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm
		 * @example 'getCMSValidator'
		 * @var string
		 */
		'fileEditValidator' => null
	);

	/**
	 * @param string $name The internal field name, passed to forms.
	 * @param string $title The field label.
	 * @param SS_List $items If no items are defined, the field will try to auto-detect an existing relation on
	 *                       @link $record}, with the same name as the field name.
	 * @param Form $form Reference to the container form
	 */
	public function __construct($name, $title = null, SS_List $items = null) {
		// TODO thats the first thing that came to my head, feel free to change it
		$this->addExtraClass('ss-upload'); // class, used by js
		$this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only

		$this->ufConfig = array_merge($this->ufConfig, Config::inst()->get('UploadField', 'defaultConfig'));

		parent::__construct($name, $title);

		if($items) $this->setItems($items);

		// filter out '' since this would be a regex problem on JS end
		$this->getValidator()->setAllowedExtensions(array_filter(File::$allowed_extensions)); 
		// get the lower max size
		$this->getValidator()->setAllowedMaxFileSize(min(File::ini2bytes(ini_get('upload_max_filesize')),
			File::ini2bytes(ini_get('post_max_size'))));
	}

	/**
	 * Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension)
	 * 
	 * @param String
	 */
	public function setTemplateFileButtons($template) {
		$this->templateFileButtons = $template;
		return $this;
	}

	/**
	 * @return String
	 */
	public function getTemplateFileButtons() {
		return $this->templateFileButtons;
	}

	/**
	 * Set name of template used for the edit (inline & popup) of a file file (without path or extension)
	 * 
	 * @param String
	 */
	public function setTemplateFileEdit($template) {
		$this->templateFileEdit = $template;
		return $this;
	}

	/**
	 * @return String
	 */
	public function getTemplateFileEdit() {
		return $this->templateFileEdit;
	}

	/**
	 * Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File)
	 * @param DataObject $record
	 */
	public function setRecord($record) {
		$this->record = $record;
		return $this;
	}
	/**
	 * Get the record to use as "Parent" for uploaded Files (eg a Page with a has_one to File) If none is set, it will
	 * use Form->getRecord() or Form->Controller()->data()
	 * @return DataObject
	 */
	public function getRecord() {
		if (!$this->record && $this->form) {
			if ($this->form->getRecord() && is_a($this->form->getRecord(), 'DataObject')) {
				$this->record = $this->form->getRecord();
			} elseif ($this->form->Controller() && $this->form->Controller()->hasMethod('data') 
					&& $this->form->Controller()->data() && is_a($this->form->Controller()->data(), 'DataObject')) {
				$this->record = $this->form->Controller()->data();
			}
		}
		return $this->record;
	}

	/**
	 * @param SS_List $items
	 */
	public function setItems(SS_List $items) {
		$this->items = $items; 
		return $this;
	}

	/**
	 * @return SS_List
	 */
	public function getItems() {
		$name = $this->getName();
		if (!$this->items || !$this->items->exists()) {
			$record = $this->getRecord();
			$this->items = array();
			// Try to auto-detect relationship
			if ($record && $record->exists()) {
				if ($record->has_many($name) || $record->many_many($name)) {
					// Ensure relationship is cast to an array, as we can't alter the items of a DataList/RelationList
					// (see below)
					$this->items = $record->{$name}()->toArray();
				} elseif($record->has_one($name)) {
					$item = $record->{$name}();
					if ($item && $item->exists())
						$this->items = array($record->{$name}());
				}
			}
			$this->items = new ArrayList($this->items);
			// hack to provide $UploadFieldThumbnailURL, $hasRelation and $UploadFieldEditLink in template for each
			// file
			if ($this->items->exists()) {
				foreach ($this->items as $i=>$file) {
					$this->items[$i] = $this->customiseFile($file);	
					if(!$file->canView()) unset($this->items[$i]); // Respect model permissions
				}
			}
		}
		return $this->items;
	}

	/**
	 * Hack to add some Variables and a dynamic template to a File
	 * @param File $file
	 * @return ViewableData_Customised
	 */
	protected function customiseFile(File $file) {
		$file = $file->customise(array(
			'UploadFieldHasRelation' => $this->managesRelation(),
			'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file),
			'UploadFieldRemoveLink' => $this->getItemHandler($file->ID)->RemoveLink(),
			'UploadFieldDeleteLink' => $this->getItemHandler($file->ID)->DeleteLink(),
			'UploadFieldEditLink' => $this->getItemHandler($file->ID)->EditLink()
		));
		// we do this in a second customise to have the access to the previous customisations
		return $file->customise(array(
			'UploadFieldFileButtons' => $file->renderWith($this->getTemplateFileButtons())
		));
	}

	/**
	 * @param string $key
	 * @param mixed $val
	 */
	public function setConfig($key, $val) {
		$this->ufConfig[$key] = $val;
		return $this;
	}

	/**
	 * @param string $key
	 * @return mixed
	 */
	public function getConfig($key) {
		return $this->ufConfig[$key];
	}
	
	/**
	 * Used to get config in the template
	 */
	public function getAutoUpload() {
		return $this->getConfig('autoUpload');
	}

	/**
	 * @param File $file
	 * @return string
	 */
	protected function getThumbnailURLForFile(File $file) {
		if ($file && $file->exists() && file_exists(Director::baseFolder() . '/' . $file->getFilename())) {
			if ($file->hasMethod('getThumbnail')) {
				return $file->getThumbnail($this->getConfig('previewMaxWidth'),
					$this->getConfig('previewMaxHeight'))->getURL();
			} elseif ($file->hasMethod('getThumbnailURL')) {
				return $file->getThumbnailURL($this->getConfig('previewMaxWidth'),
					$this->getConfig('previewMaxHeight'));
			} elseif ($file->hasMethod('SetRatioSize')) {
				return $file->SetRatioSize($this->getConfig('previewMaxWidth'),
					$this->getConfig('previewMaxHeight'))->getURL();
			} else {
				return $file->Icon();
			}
		}
		return false;
	}

	public function getAttributes() {
		return array_merge(
			parent::getAttributes(),
			array('data-selectdialog-url', $this->Link('select'))
		);
	}

	public function extraClass() {
		if($this->isDisabled()) $this->addExtraClass('disabled');
		if($this->isReadonly()) $this->addExtraClass('readonly');
		return parent::extraClass();
	}

	public function Field($properties = array()) {
		$record = $this->getRecord();
		$name = $this->getName();

		// if there is a has_one relation with that name on the record and 
		// allowedMaxFileNumber has not been set, it's wanted to be 1
		if(
			$record && $record->exists()
			&& $record->has_one($name) && !$this->getConfig('allowedMaxFileNumber')
		) {
			$this->setConfig('allowedMaxFileNumber', 1);
		}

		Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
		Requirements::javascript(THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js');
		Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
		Requirements::javascript(FRAMEWORK_DIR . '/javascript/i18n.js');
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/ssui.core.js');

		Requirements::combine_files('uploadfield.js', array(
			THIRDPARTY_DIR . '/javascript-templates/tmpl.js',
			THIRDPARTY_DIR . '/javascript-loadimage/load-image.js',
			THIRDPARTY_DIR . '/jquery-fileupload/jquery.iframe-transport.js',
			THIRDPARTY_DIR . '/jquery-fileupload/cors/jquery.xdr-transport.js',
			THIRDPARTY_DIR . '/jquery-fileupload/jquery.fileupload.js',
			THIRDPARTY_DIR . '/jquery-fileupload/jquery.fileupload-ui.js',
			FRAMEWORK_DIR . '/javascript/UploadField_uploadtemplate.js',
			FRAMEWORK_DIR . '/javascript/UploadField_downloadtemplate.js',
			FRAMEWORK_DIR . '/javascript/UploadField.js',
		));
		Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css'); // TODO hmmm, remove it?
		Requirements::css(FRAMEWORK_DIR . '/css/UploadField.css');

		$config = array(
			'url' => $this->Link('upload'),
			'urlSelectDialog' => $this->Link('select'),
			'urlAttach' => $this->Link('attach'),
			'acceptFileTypes' => '.+$',
			'maxNumberOfFiles' => $this->getConfig('allowedMaxFileNumber')
		);
		if (count($this->getValidator()->getAllowedExtensions())) {
			$allowedExtensions = $this->getValidator()->getAllowedExtensions();
			$config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$';
			$config['errorMessages']['acceptFileTypes'] = _t(
				'File.INVALIDEXTENSIONSHORT', 
				'Extension is not allowed'
			);
		}
		if ($this->getValidator()->getAllowedMaxFileSize()) {
			$config['maxFileSize'] = $this->getValidator()->getAllowedMaxFileSize();
			$config['errorMessages']['maxFileSize'] = _t(
				'File.TOOLARGESHORT', 
				'Filesize exceeds {size}',
				array('size' => File::format_size($config['maxFileSize']))
			);
		}
		if ($config['maxNumberOfFiles'] > 1) {
			$config['errorMessages']['maxNumberOfFiles'] = _t(
				'UploadField.MAXNUMBEROFFILESSHORT', 
				'Can only upload {count} files',
				array('count' => $config['maxNumberOfFiles'])
			);
		}
		$configOverwrite = array();
		if (is_numeric($config['maxNumberOfFiles']) && $this->getItems()->count()) {
			$configOverwrite['maxNumberOfFiles'] = $config['maxNumberOfFiles'] - $this->getItems()->count();
		}
		
		$config = array_merge($config, $this->ufConfig, $configOverwrite);
		
		return $this->customise(array(
			'configString' => str_replace('"', "'", Convert::raw2json($config)),
			'config' => new ArrayData($config),
			'multiple' => $config['maxNumberOfFiles'] !== 1,
			'displayInput' => (!isset($configOverwrite['maxNumberOfFiles']) || $configOverwrite['maxNumberOfFiles'])
		))->renderWith($this->getTemplates());
	}

	/**
	 * Validation method for this field, called when the entire form is validated
	 * 
	 * @param $validator
	 * @return Boolean
	 */
	public function validate($validator) {
		return true;
	}
	
	/**
	 * @param SS_HTTPRequest $request
	 * @return UploadField_ItemHandler
	 */
	public function handleItem(SS_HTTPRequest $request) {
		return $this->getItemHandler($request->param('ID'));
	}

	/**
	 * @param int $itemID
	 * @return UploadField_ItemHandler
	 */
	public function getItemHandler($itemID) {
		return UploadField_ItemHandler::create($this, $itemID);
	}

	/**
	 * @param SS_HTTPRequest $request
	 * @return UploadField_ItemHandler
	 */
	public function handleSelect(SS_HTTPRequest $request) {
		return UploadField_SelectHandler::create($this, $this->folderName);
	}

	/**
	 * Action to handle upload of a single file
	 * 
	 * @param SS_HTTPRequest $request
	 * @return string json
	 */
	public function upload(SS_HTTPRequest $request) {
		if($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) {
			return $this->httpError(403);
		}

		// Protect against CSRF on destructive action
		$token = $this->getForm()->getSecurityToken();
		if(!$token->checkRequest($request)) return $this->httpError(400);

		$name = $this->getName();
		$tmpfile = $request->postVar($name);
		$record = $this->getRecord();
		
		// Check if the file has been uploaded into the temporary storage.
		if (!$tmpfile) {
			$return = array('error' => _t('UploadField.FIELDNOTSET', 'File information not found'));
		} else {
			$return = array(
				'name' => $tmpfile['name'],
				'size' => $tmpfile['size'],
				'type' => $tmpfile['type'],
				'error' => $tmpfile['error']
			);
		}

		// Check for constraints on the record to which the file will be attached.
		if (!$return['error'] && $this->relationAutoSetting && $record && $record->exists()) {
			$tooManyFiles = false;
			// Some relationships allow many files to be attached.
			if ($this->getConfig('allowedMaxFileNumber') && ($record->has_many($name) || $record->many_many($name))) {
				if(!$record->isInDB()) $record->write();
				$tooManyFiles = $record->{$name}()->count() >= $this->getConfig('allowedMaxFileNumber');
			// has_one only allows one file at any given time.
			} elseif($record->has_one($name)) {
				// If we're allowed to replace an existing file, clear out the old one
				if($record->$name && $this->getConfig('replaceExistingFile')) {
					$record->$name = null;
				}
				$tooManyFiles = $record->{$name}() && $record->{$name}()->exists();
			}

			// Report the constraint violation.
			if ($tooManyFiles) {
				if(!$this->getConfig('allowedMaxFileNumber')) $this->setConfig('allowedMaxFileNumber', 1);
				$return['error'] = _t(
					'UploadField.MAXNUMBEROFFILES', 
					'Max number of {count} file(s) exceeded.',
					array('count' => $this->getConfig('allowedMaxFileNumber'))
				);
			}
		}

		// Process the uploaded file
		if (!$return['error']) {
			$fileObject = null;

			if ($this->relationAutoSetting) {
				// Search for relations that can hold the uploaded files.
				if ($relationClass = $this->getRelationAutosetClass()) {
					// Create new object explicitly. Otherwise rely on Upload::load to choose the class.
					$fileObject = Object::create($relationClass);
				}
			}

			// Get the uploaded file into a new file object.
			try {
				$this->upload->loadIntoFile($tmpfile, $fileObject, $this->folderName);
			} catch (Exception $e) {
				// we shouldn't get an error here, but just in case
				$return['error'] = $e->getMessage();
			}

			if (!$return['error']) {
				if ($this->upload->isError()) {
					$return['error'] = implode(' '.PHP_EOL, $this->upload->getErrors());
				} else {
					$file = $this->upload->getFile();

					// Attach the file to the related record.
					if ($this->relationAutoSetting) {
						$this->attachFile($file);
					}

					// Collect all output data.
					$file =  $this->customiseFile($file);
					$return = array_merge($return, array(
						'id' => $file->ID,
						'name' => $file->getTitle() . '.' . $file->getExtension(),
						'url' => $file->getURL(),
						'thumbnail_url' => $file->UploadFieldThumbnailURL,
						'edit_url' => $file->UploadFieldEditLink,
						'size' => $file->getAbsoluteSize(),
						'buttons' => $file->UploadFieldFileButtons
					));
				}
			}
		}
		$response = new SS_HTTPResponse(Convert::raw2json(array($return)));
		$response->addHeader('Content-Type', 'text/plain');
		return $response;
	}

	/**
	 * Add existing {@link File} records to the relationship.
	 */
	public function attach($request) {
		if(!$request->isPOST()) return $this->httpError(403);
		if(!$this->managesRelation()) return $this->httpError(403);
		if(!$this->canAttachExisting()) return $this->httpError(403);

		$return = array();

		$files = File::get()->byIDs($request->postVar('ids'));
		foreach($files as $file) {
			$this->attachFile($file);
			$file =  $this->customiseFile($file);
			$return[] = array(
				'id' => $file->ID,
				'name' => $file->getTitle() . '.' . $file->getExtension(),
				'url' => $file->getURL(),
				'thumbnail_url' => $file->UploadFieldThumbnailURL,
				'edit_url' => $file->UploadFieldEditLink,
				'size' => $file->getAbsoluteSize(),
				'buttons' => $file->UploadFieldFileButtons
			);
		}
		$response = new SS_HTTPResponse(Convert::raw2json($return));
		$response->addHeader('Content-Type', 'application/json');
		return $response;
	}

	/**
	 * @param File
	 */
	protected function attachFile($file) {
		$record = $this->getRecord();
		$name = $this->getName();
		if ($record && $record->exists()) {
			if ($record->has_many($name) || $record->many_many($name)) {
				if(!$record->isInDB()) $record->write();
				$record->{$name}()->add($file);
			} elseif($record->has_one($name)) {
				$record->{$name . 'ID'} = $file->ID;
				$record->write();
			}
		}
	}

	public function performReadonlyTransformation() {
		$clone = clone $this;
		$clone->addExtraClass('readonly');
		$clone->setReadonly(true);
		return $clone;
	}

	/**
	 * Determines if the underlying record (if any) has a relationship
	 * matching the field name. Important for permission control.
	 * 
	 * @return boolean
	 */
	public function managesRelation() {
		$record = $this->getRecord();
		$fieldName = $this->getName();
		return (
			$record 
			&& ($record->has_one($fieldName) || $record->has_many($fieldName) || $record->many_many($fieldName))
		);
	}

	/**
	 * Gets the foreign class that needs to be created.
	 *
	 * @return string Foreign class name.
	 */
	public function getRelationAutosetClass() {
		$name = $this->getName();
		$record = $this->getRecord();

		if (isset($name) && isset($record)) return $record->getRelationClass($name);
	}

	public function isDisabled() {
		return (parent::isDisabled() || !$this->isSaveable());
	}

	/**
	 * Determines if the field can be saved into a database record.
	 * 
	 * @return boolean 
	 */
	public function isSaveable() {
		$record = $this->getRecord();
		// Don't allow upload or edit of a relation when the underlying record hasn't been persisted yet
		return (!$record || !$this->managesRelation() || $record->exists());
	}

	public function canUpload() {
		$can = $this->getConfig('canUpload');
		return (is_bool($can)) ? $can : Permission::check($can);
	}

	public function canAttachExisting() {
		$can = $this->getConfig('canAttachExisting');
		return (is_bool($can)) ? $can : Permission::check($can);
	}

}

/**
 * RequestHandler for actions (edit, remove, delete) on a single item (File) of the UploadField
 * 
 * @author Zauberfisch
 * @package framework
 * @subpackage forms
 */
class UploadField_ItemHandler extends RequestHandler {

	/**
	 * @var UploadFIeld
	 */
	protected $parent;

	/**
	 * @var int FileID
	 */
	protected $itemID;

	public static $url_handlers = array(
		'$Action!' => '$Action',
		'' => 'index',
	);

	/**
	 * @param UploadFIeld $parent
	 * @param int $item
	 */
	public function __construct($parent, $itemID) {
		$this->parent = $parent;
		$this->itemID = $itemID;

		parent::__construct();
	}

	/**
	 * @return File
	 */
	public function getItem() {
		return DataObject::get_by_id('File', $this->itemID);
	}

	/**
	 * @param string $action
	 * @return string
	 */
	public function Link($action = null) {
		return Controller::join_links($this->parent->Link(), '/item/', $this->itemID, $action);
	}

	/**
	 * @return string
	 */
	public function RemoveLink() {
		$token = $this->parent->getForm()->getSecurityToken();
		return $token->addToUrl($this->Link('remove'));
	}

	/**
	 * @return string
	 */
	public function DeleteLink() {
		$token = $this->parent->getForm()->getSecurityToken();
		return $token->addToUrl($this->Link('delete'));
	}

	/**
	 * @return string
	 */
	public function EditLink() {
		return $this->Link('edit');
	}

	/**
	 * Action to handle removing a single file from the db relation
	 * 
	 * @param SS_HTTPRequest $request
	 * @return SS_HTTPResponse
	 */
	public function remove(SS_HTTPRequest $request) {
		// Check form field state
		if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);

		// Protect against CSRF on destructive action
		$token = $this->parent->getForm()->getSecurityToken();
		if(!$token->checkRequest($request)) return $this->httpError(400);

		$response = new SS_HTTPResponse();
		$response->setStatusCode(500);
		$fieldName = $this->parent->getName();
		$record = $this->parent->getRecord();
		$id = $this->getItem()->ID;
		if ($id && $record && $record->exists()) {
			if (($record->has_many($fieldName) || $record->many_many($fieldName))
					&& $file = $record->{$fieldName}()->byID($id)) {

				$record->{$fieldName}()->remove($file);
				$response->setStatusCode(200);
			} elseif($record->has_one($fieldName) && $record->{$fieldName . 'ID'} == $id) {
				$record->{$fieldName . 'ID'} = 0;
				$record->write();
				$response->setStatusCode(200);
			}
		}
		if ($response->getStatusCode() != 200)
			$response->setStatusDescription(_t('UploadField.REMOVEERROR', 'Error removing file'));
		return $response;
	}

	/**
	 * Action to handle deleting of a single file
	 * 
	 * @param SS_HTTPRequest $request
	 * @return SS_HTTPResponse
	 */
	public function delete(SS_HTTPRequest $request) {
		// Check form field state
		if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);

		// Protect against CSRF on destructive action
		$token = $this->parent->getForm()->getSecurityToken();
		if(!$token->checkRequest($request)) return $this->httpError(400);

		// Check item permissions
		$item = $this->getItem();
		if(!$item) return $this->httpError(404);
		if(!$item->canDelete()) return $this->httpError(403);

		// Only allow actions on files in the managed relation (if one exists)
		$items = $this->parent->getItems();
		if($this->parent->managesRelation() && !$items->byID($item->ID)) return $this->httpError(403);

		// First remove the file from the current relationship
		$this->remove($request);

		// Then delete the file from the filesystem
		$item->delete();
	}

	/**
	 * Action to handle editing of a single file
	 * 
	 * @param SS_HTTPRequest $request
	 * @return ViewableData_Customised
	 */
	public function edit(SS_HTTPRequest $request) {
		// Check form field state
		if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);

		// Check item permissions
		$item = $this->getItem();
		if(!$item) return $this->httpError(404);
		if(!$item->canEdit()) return $this->httpError(403);

		// Only allow actions on files in the managed relation (if one exists)
		$items = $this->parent->getItems();
		if($this->parent->managesRelation() && !$items->byID($item->ID)) return $this->httpError(403);

		Requirements::css(FRAMEWORK_DIR . '/css/UploadField.css');

		return $this->customise(array(
			'Form' => $this->EditForm()
		))->renderWith($this->parent->getTemplateFileEdit());
	}

	/**
	 * @return Form
	 */
	public function EditForm() {
		$file = $this->getItem();
		if (is_a($this->parent->getConfig('fileEditFields'), 'FieldList')) {
			$fields = $this->parent->getConfig('fileEditFields');
		} elseif ($file->hasMethod($this->parent->getConfig('fileEditFields'))) {
			$fields = $file->{$this->parent->getConfig('fileEditFields')}();
		} else {
			$fields = $file->getCMSFields();
			// Only display main tab, to avoid overly complex interface
			if($fields->hasTabSet() && $mainTab = $fields->findOrMakeTab('Root.Main')) $fields = $mainTab->Fields();
		}
		if (is_a($this->parent->getConfig('fileEditActions'), 'FieldList')) {
			$actions = $this->parent->getConfig('fileEditActions');
		} elseif ($file->hasMethod($this->parent->getConfig('fileEditActions'))) {
			$actions = $file->{$this->parent->getConfig('fileEditActions')}();
		} else {
			$actions = new FieldList($saveAction = new FormAction('doEdit', _t('UploadField.DOEDIT', 'Save')));
			$saveAction->addExtraClass('ss-ui-action-constructive icon-accept');
		}
		if (is_a($this->parent->getConfig('fileEditValidator'), 'Validator')) {
			$validator = $this->parent->getConfig('fileEditValidator');
		} elseif ($file->hasMethod($this->parent->getConfig('fileEditValidator'))) {
			$validator = $file->{$this->parent->getConfig('fileEditValidator')}();
		} else {
			$validator = null;
		}
		$form = new Form(
			$this,
			__FUNCTION__, 
			$fields,
			$actions,
			$validator
		);
		$form->loadDataFrom($file);
		$form->addExtraClass('small');
		
		return $form;
	}

	/**
	 * @param array $data
	 * @param Form $form
	 * @param SS_HTTPRequest $request
	 */
	public function doEdit(array $data, Form $form, SS_HTTPRequest $request) {
		// Check form field state
		if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);

		// Check item permissions
		$item = $this->getItem();
		if(!$item) return $this->httpError(404);

		// Only allow actions on files in the managed relation (if one exists)
		$items = $this->parent->getItems();
		if($this->parent->managesRelation() && !$items->byID($item->ID)) return $this->httpError(403);

		$form->saveInto($item);
		$item->write();

		$form->sessionMessage(_t('UploadField.Saved', 'Saved'), 'good');

		return $this->edit($request);
	}
	
}

/**
 * File selection popup for attaching existing files.
 */
class UploadField_SelectHandler extends RequestHandler {

	/**
	 * @var UploadField
	 */
	protected $parent;

	/**
	 * @var String
	 */
	protected $folderName;

	public static $url_handlers = array(
		'$Action!' => '$Action',
		'' => 'index',
	);

	public function __construct($parent, $folderName = null) {
		$this->parent = $parent;
		$this->folderName = $folderName;

		parent::__construct();
	}

	public function index() {
		// Requires a separate JS file, because we can't reach into the iframe with entwine.
		Requirements::javascript(FRAMEWORK_DIR . '/javascript/UploadField_select.js');
		return $this->renderWith('CMSDialog');
	}

	/**
	 * @param string $action
	 * @return string
	 */
	public function Link($action = null) {
		return Controller::join_links($this->parent->Link(), '/select/', $action);
	}

	/**
	 * Build the file selection form.
	 *
	 * @return Form
	 */
	public function Form() {
		// Find out the requested folder ID.
		$folderID = $this->parent->getRequest()->requestVar('ParentID');
		if (!isset($folderID)) {
			$folder = Folder::find_or_make($this->folderName);
			$folderID = $folder->ID;
		}

		// Construct the form
		$action = new FormAction('doAttach', _t('UploadField.AttachFile', 'Attach file(s)'));
		$action->addExtraClass('ss-ui-action-constructive icon-accept');
		$form = new Form(
			$this,
			'Form',
			new FieldList($this->getListField($folderID)),
			new FieldList($action)
		);

		// Add a class so we can reach the form from the frontend.
		$form->addExtraClass('uploadfield-form');

		return $form;
	}

	/**
	 * @param $folderID The ID of the folder to display.
	 * @return FormField
	 */
	protected function getListField($folderID) {
		// Generate the folder selection field.
		$folderField = new TreeDropdownField('ParentID', _t('HtmlEditorField.FOLDER', 'Folder'), 'Folder');
		$folderField->setValue($folderID);

		// Generate the file list field.
		$config = GridFieldConfig::create();
		$config->addComponent(new GridFieldSortableHeader());
		$config->addComponent(new GridFieldFilterHeader());
		$config->addComponent(new GridFieldDataColumns());
		$config->addComponent(new GridFieldPaginator(10));

		// If relation is to be autoset, we need to make sure we only list compatible objects.
		$baseClass = null;
		if ($this->parent->relationAutoSetting) {
			$baseClass = $this->parent->getRelationAutosetClass();
		}

		// By default we can attach anything that is a file, or derives from file.
		if (!$baseClass) $baseClass = 'File';

		// Create the data source for the list of files within the current directory.
		$files = DataList::create($baseClass)->filter('ParentID', $folderID);

		$fileField = new GridField('Files', false, $files, $config);
		$fileField->setAttribute('data-selectable', true);
		if($this->parent->getConfig('allowedMaxFileNumber') > 1) $fileField->setAttribute('data-multiselect', true);

		$selectComposite = new CompositeField(
			$folderField,
			$fileField
		);

		return $selectComposite;
	}

	public function doAttach($data, $form) {
		// TODO Only implemented via JS for now
	}

}
