diff --git a/forms/htmleditor/HtmlEditorField.php b/forms/htmleditor/HtmlEditorField.php
index 7eb378c5898..a22ef845184 100644
--- a/forms/htmleditor/HtmlEditorField.php
+++ b/forms/htmleditor/HtmlEditorField.php
@@ -413,37 +413,120 @@ public function MediaForm() {
return $form;
}
+ /**
+ * List of allowed schemes (no wildcard, all lower case) or empty to allow all schemes
+ *
+ * @config
+ * @var array
+ */
+ private static $fileurl_scheme_whitelist = array('http', 'https');
+
+ /**
+ * List of allowed domains (no wildcard, all lower case) or empty to allow all domains
+ *
+ * @config
+ * @var array
+ */
+ private static $fileurl_domain_whitelist = array();
+
+ /**
+ * Find local File dataobject given ID
+ *
+ * @param int $id
+ * @return array
+ */
+ protected function viewfile_getLocalFileByID($id) {
+ /** @var File $file */
+ $file = DataObject::get_by_id('File', $id);
+ if ($file && $file->canView()) {
+ return array($file, $file->getURL());
+ }
+ return [null, null];
+ }
+
+ /**
+ * Get remote File given url
+ *
+ * @param string $fileUrl Absolute URL
+ * @return array
+ * @throws SS_HTTPResponse_Exception
+ */
+ protected function viewfile_getRemoteFileByURL($fileUrl) {
+ if(!Director::is_absolute_url($fileUrl)) {
+ throw $this->getErrorFor(_t(
+ "HtmlEditorField_Toolbar.ERROR_ABSOLUTE",
+ "Only absolute urls can be embedded"
+ ));
+ }
+ $scheme = strtolower(parse_url($fileUrl, PHP_URL_SCHEME));
+ $allowed_schemes = self::config()->fileurl_scheme_whitelist;
+ if (!$scheme || ($allowed_schemes && !in_array($scheme, $allowed_schemes))) {
+ throw $this->getErrorFor(_t(
+ "HtmlEditorField_Toolbar.ERROR_SCHEME",
+ "This file scheme is not included in the whitelist"
+ ));
+ }
+ $domain = strtolower(parse_url($fileUrl, PHP_URL_HOST));
+ $allowed_domains = self::config()->fileurl_domain_whitelist;
+ if (!$domain || ($allowed_domains && !in_array($domain, $allowed_domains))) {
+ throw $this->getErrorFor(_t(
+ "HtmlEditorField_Toolbar.ERROR_HOSTNAME",
+ "This file hostname is not included in the whitelist"
+ ));
+ }
+ return [null, $fileUrl];
+ }
+
+ /**
+ * Prepare error for the front end
+ *
+ * @param string $message
+ * @param int $code
+ * @return SS_HTTPResponse_Exception
+ */
+ protected function getErrorFor($message, $code = 400) {
+ $exception = new SS_HTTPResponse_Exception($message, $code);
+ $exception->getResponse()->addHeader('X-Status', $message);
+ return $exception;
+ }
+
/**
* View of a single file, either on the filesystem or on the web.
*
+ * @throws SS_HTTPResponse_Exception
* @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')) {
- // URLS should be used for remote resources (not local assets)
- $url = Director::absoluteURL($url);
+ $url = null;
+ // Get file and url by request method
+ if($fileUrl = $request->getVar('FileURL')) {
+ // Get remote url
+ list($file, $url) = $this->viewfile_getRemoteFileByURL($fileUrl);
} elseif($id = $request->getVar('ID')) {
- // Use local dataobject
- $file = DataObject::get_by_id('File', $id);
- if(!$file) {
- throw new InvalidArgumentException("File could not be found");
- }
- $url = $file->getURL();
- if(!$url) {
- return $this->httpError(404, 'File not found');
- }
+ // Or we could have been passed an ID directly
+ list($file, $url) = $this->viewfile_getLocalFileByID($id);
} else {
- throw new LogicException('Need either "ID" or "FileURL" parameter to identify the file');
+ // Or we could have been passed nothing, in which case panic
+ throw $this->getErrorFor(_t(
+ "HtmlEditorField_Toolbar.ERROR_ID",
+ 'Need either "ID" or "FileURL" parameter to identify the file'
+ ));
+ }
+
+ // Validate file exists
+ if(!$url) {
+ throw $this->getErrorFor(_t(
+ "HtmlEditorField_Toolbar.ERROR_NOTFOUND",
+ 'Unable to find file to view'
+ ));
}
// 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
- $fileCategory = File::get_app_category(File::get_file_extension($url));
+ $fileCategory = $this->getFileCategory($url, $file);
switch($fileCategory) {
case 'image':
case 'image/supported':
@@ -456,10 +539,12 @@ public function viewfile($request) {
// Only remote files can be linked via o-embed
// {@see HtmlEditorField_Toolbar::getAllowedExtensions())
if($file) {
- throw new InvalidArgumentException(
+ throw $this->getErrorFor(_t(
+ "HtmlEditorField_Toolbar.ERROR_OEMBED_REMOTE",
"Oembed is only compatible with remote files"
- );
+ ));
}
+
// Other files should fallback to oembed
$fileWrapper = new HtmlEditorField_Embed($url, $file);
break;
@@ -472,10 +557,28 @@ public function viewfile($request) {
))->renderWith($this->templateViewFile);
}
+ /**
+ * Guess file category from either a file or url
+ *
+ * @param string $url
+ * @param File $file
+ * @return string
+ */
+ protected function getFileCategory($url, $file) {
+ if($file) {
+ return $file->appCategory();
+ }
+ if($url) {
+ return File::get_app_category(File::get_file_extension($url));
+ }
+ return null;
+ }
+
/**
* Find all anchors available on the given page.
*
* @return array
+ * @throws SS_HTTPResponse_Exception
*/
public function getanchors() {
$id = (int)$this->getRequest()->getVar('PageID');
diff --git a/tests/forms/HtmlEditorFieldToolbarTest.php b/tests/forms/HtmlEditorFieldToolbarTest.php
new file mode 100644
index 00000000000..69fd724010d
--- /dev/null
+++ b/tests/forms/HtmlEditorFieldToolbarTest.php
@@ -0,0 +1,81 @@
+update('HtmlEditorField_Toolbar', 'fileurl_scheme_whitelist', array('http'));
+ Config::inst()->update('HtmlEditorField_Toolbar', 'fileurl_domain_whitelist', array('example.com'));
+
+ // Filesystem mock
+ AssetStoreTest_SpyStore::activate(__CLASS__);
+
+ // Load up files
+ /** @var File $file1 */
+ $file1 = $this->objFromFixture('File', 'example_file');
+ $file1->setFromString(str_repeat('x', 1000), $file1->Name);
+ $file1->write();
+
+ /** @var Image $image1 */
+ $image1 = $this->objFromFixture('Image', 'example_image');
+ $image1->setFromLocalFile(
+ __DIR__ . '/images/HTMLEditorFieldTest-example.jpg',
+ 'folder/subfolder/HTMLEditorFieldTest_example.jpg'
+ );
+ $image1->write();
+ }
+
+ public function testValidLocalReference() {
+ /** @var File $exampleFile */
+ $exampleFile = $this->objFromFixture('File', 'example_file');
+ $expectedUrl = $exampleFile->AbsoluteLink();
+ Config::inst()->update('HtmlEditorField_Toolbar', 'fileurl_domain_whitelist', array(
+ 'example.com',
+ strtolower(parse_url($expectedUrl, PHP_URL_HOST))
+ ));
+
+ list($file, $url) = $this->getToolbar()->viewfile_getRemoteFileByURL($exampleFile->AbsoluteLink());
+ $this->assertEquals($expectedUrl, $url);
+ }
+
+ public function testValidScheme() {
+ list($file, $url) = $this->getToolbar()->viewfile_getRemoteFileByURL('http://example.com/test.pdf');
+ $this->assertEquals($url, 'http://example.com/test.pdf');
+ }
+
+ /** @expectedException SS_HTTPResponse_Exception */
+ public function testInvalidScheme() {
+ list($file, $url) = $this->getToolbar()->viewfile_getRemoteFileByURL('nosuchscheme://example.com/test.pdf');
+ }
+
+ public function testValidDomain() {
+ list($file, $url) = $this->getToolbar()->viewfile_getRemoteFileByURL('http://example.com/test.pdf');
+ $this->assertEquals($url, 'http://example.com/test.pdf');
+ }
+
+ /** @expectedException SS_HTTPResponse_Exception */
+ public function testInvalidDomain() {
+ list($file, $url) = $this->getToolbar()->viewfile_getRemoteFileByURL('http://evil.com/test.pdf');
+ }
+
+}
diff --git a/tests/forms/HtmlEditorFieldToolbarTest.yml b/tests/forms/HtmlEditorFieldToolbarTest.yml
new file mode 100644
index 00000000000..bdeb43ae5d2
--- /dev/null
+++ b/tests/forms/HtmlEditorFieldToolbarTest.yml
@@ -0,0 +1,18 @@
+Folder:
+ folder1:
+ Name: folder
+ Title: folder
+ folder2:
+ Name: subfolder
+ Title: subfolder
+ Parent: =>Folder.folder1
+
+File:
+ example_file:
+ Name: example.pdf
+ Parent: =>Folder.folder2
+
+Image:
+ example_image:
+ Name: HTMLEditorFieldTest_example.jpg
+ Parent: =>Folder.folder2