Completed
Push — namespace-model ( 2589e8...dc57a0 )
by Sam
15:10 queued 06:48
created

HtmlEditorField_Toolbar   D

Complexity

Total Complexity 39

Size/Duplication

Total Lines 504
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 32

Importance

Changes 2
Bugs 2 Features 0
Metric Value
c 2
b 2
f 0
dl 0
loc 504
rs 4.2439
wmc 39
lcom 1
cbo 32
1
<?php
2
/**
3
 * A TinyMCE-powered WYSIWYG HTML editor field with image and link insertion and tracking capabilities. Editor fields
4
 * are created from <textarea> tags, which are then converted with JavaScript.
5
 *
6
 * @package forms
7
 * @subpackage fields-formattedinput
8
 */
9
10
use SilverStripe\Model\DataObjectInterface;
11
use SilverStripe\Model\DataObject;
12
use SilverStripe\Control\Controller;
13
use SilverStripe\Control\Director;
14
use SilverStripe\Control\HTTPResponse_Exception;
15
use SilverStripe\Control\RequestHandler;
16
use SilverStripe\Control\HTTPResponse_Exception;
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Cannot use SilverStripe\Control\HTTPResponse_Exception as HTTPResponse_Exception because the name is already in use
Loading history...
17
18
19
class HtmlEditorField extends TextareaField {
20
21
	/**
22
	 * Use TinyMCE's GZIP compressor
23
	 *
24
	 * @config
25
	 * @var bool
26
	 */
27
	private static $use_gzip = true;
28
29
	/**
30
	 * Should we check the valid_elements (& extended_valid_elements) rules from HtmlEditorConfig server side?
31
	 *
32
	 * @config
33
	 * @var bool
34
	 */
35
	private static $sanitise_server_side = false;
36
37
	/**
38
	 * Number of rows
39
	 *
40
	 * @config
41
	 * @var int
42
	 */
43
	private static $default_rows = 30;
44
45
	/**
46
	 * ID or instance of editorconfig
47
	 *
48
	 * @var string|HtmlEditorConfig
49
	 */
50
	protected $editorConfig = null;
51
52
	/**
53
	 * Gets the HtmlEditorConfig instance
54
	 *
55
	 * @return HtmlEditorConfig
56
	 */
57
	public function getEditorConfig() {
58
		// Instance override
59
		if($this->editorConfig instanceof HtmlEditorConfig) {
60
			return $this->editorConfig;
61
		}
62
63
		// Get named / active config
64
		return HtmlEditorConfig::get($this->editorConfig);
65
	}
66
67
	/**
68
	 * Assign a new configuration instance or identifier
69
	 *
70
	 * @param string|HtmlEditorConfig $config
71
	 * @return $this
72
	 */
73
	public function setEditorConfig($config) {
74
		$this->editorConfig = $config;
75
		return $this;
76
	}
77
78
	/**
79
	 * Creates a new HTMLEditorField.
80
	 * @see TextareaField::__construct()
81
	 *
82
	 * @param string $name The internal field name, passed to forms.
83
	 * @param string $title The human-readable field label.
84
	 * @param mixed $value The value of the field.
85
	 * @param string $config HtmlEditorConfig identifier to be used. Default to the active one.
86
	 */
87
	public function __construct($name, $title = null, $value = '', $config = null) {
88
		parent::__construct($name, $title, $value);
89
90
		if($config) {
91
			$this->setEditorConfig($config);
92
		}
93
94
		$this->setRows($this->config()->default_rows);
95
	}
96
97
	public function getAttributes() {
98
		return array_merge(
99
			parent::getAttributes(),
100
			$this->getEditorConfig()->getAttributes()
101
		);
102
	}
103
104
	public function saveInto(DataObjectInterface $record) {
105
		if($record->hasField($this->name) && $record->escapeTypeForField($this->name) != 'xml') {
106
			throw new Exception (
107
				'HtmlEditorField->saveInto(): This field should save into a HTMLText or HTMLVarchar field.'
108
			);
109
		}
110
111
		// Sanitise if requested
112
		$htmlValue = Injector::inst()->create('HTMLValue', $this->Value());
113
		if($this->config()->sanitise_server_side) {
114
			$santiser = Injector::inst()->create('HtmlEditorSanitiser', HtmlEditorConfig::get_active());
115
			$santiser->sanitise($htmlValue);
116
		}
117
118
		// optionally manipulate the HTML after a TinyMCE edit and prior to a save
119
		$this->extend('processHTML', $htmlValue);
120
121
		// Store into record
122
		$record->{$this->name} = $htmlValue->getContent();
123
	}
124
125
	public function setValue($value) {
126
		// Regenerate links prior to preview, so that the editor can see them.
127
		$value = Image::regenerate_html_links($value);
128
		return parent::setValue($value);
129
	}
130
131
	/**
132
	 * @return HtmlEditorField_Readonly
133
	 */
134
	public function performReadonlyTransformation() {
135
		$field = $this->castedCopy('HtmlEditorField_Readonly');
136
		$field->dontEscape = true;
137
138
		return $field;
139
	}
140
141
	public function performDisabledTransformation() {
142
		return $this->performReadonlyTransformation();
143
	}
144
145
	public function Field($properties = array()) {
146
		// Include requirements
147
		$this->getEditorConfig()->init();
148
		return parent::Field($properties);
149
	}
150
}
151
152
/**
153
 * Readonly version of an {@link HTMLEditorField}.
154
 * @package forms
155
 * @subpackage fields-formattedinput
156
 */
157
class HtmlEditorField_Readonly extends ReadonlyField {
158
	public function Field($properties = array()) {
159
		$valforInput = $this->value ? Convert::raw2att($this->value) : "";
160
		return "<span class=\"readonly typography\" id=\"" . $this->id() . "\">"
161
			. ( $this->value && $this->value != '<p></p>' ? $this->value : '<i>(not set)</i>' )
162
			. "</span><input type=\"hidden\" name=\"".$this->name."\" value=\"".$valforInput."\" />";
163
	}
164
	public function Type() {
165
		return 'htmleditorfield readonly';
166
	}
167
}
168
169
/**
170
 * Toolbar shared by all instances of {@link HTMLEditorField}, to avoid too much markup duplication.
171
 *  Needs to be inserted manually into the template in order to function - see {@link LeftAndMain->EditorToolbar()}.
172
 *
173
 * @package forms
174
 * @subpackage fields-formattedinput
175
 */
176
class HtmlEditorField_Toolbar extends RequestHandler {
177
178
	private static $allowed_actions = array(
179
		'LinkForm',
180
		'MediaForm',
181
		'viewfile',
182
		'getanchors'
183
	);
184
185
	/**
186
	 * @var string
187
	 */
188
	protected $templateViewFile = 'HtmlEditorField_viewfile';
189
190
	protected $controller, $name;
191
192
	public function __construct($controller, $name) {
193
		parent::__construct();
194
195
		$this->controller = $controller;
196
		$this->name = $name;
197
	}
198
199
	public function forTemplate() {
200
		return sprintf(
201
			'<div id="cms-editor-dialogs" data-url-linkform="%s" data-url-mediaform="%s"></div>',
202
			Controller::join_links($this->controller->Link(), $this->name, 'LinkForm', 'forTemplate'),
203
			Controller::join_links($this->controller->Link(), $this->name, 'MediaForm', 'forTemplate')
204
		);
205
	}
206
207
	/**
208
	 * Searches the SiteTree for display in the dropdown
209
	 *
210
	 * @return callback
211
	 */
212
	public function siteTreeSearchCallback($sourceObject, $labelField, $search) {
213
		return DataObject::get($sourceObject)->filterAny(array(
214
			'MenuTitle:PartialMatch' => $search,
215
			'Title:PartialMatch' => $search
216
		));
217
	}
218
219
	/**
220
	 * Return a {@link Form} instance allowing a user to
221
	 * add links in the TinyMCE content editor.
222
	 *
223
	 * @return Form
224
	 */
225
	public function LinkForm() {
226
		$siteTree = TreeDropdownField::create('internal', _t('HtmlEditorField.PAGE', "Page"),
227
			'SiteTree', 'ID', 'MenuTitle', true);
228
		// mimic the SiteTree::getMenuTitle(), which is bypassed when the search is performed
229
		$siteTree->setSearchFunction(array($this, 'siteTreeSearchCallback'));
230
231
		$numericLabelTmpl = '<span class="step-label"><span class="flyout">%d</span><span class="arrow"></span>'
232
			. '<strong class="title">%s</strong></span>';
233
		$form = new Form(
234
			$this->controller,
235
			"{$this->name}/LinkForm",
236
			new FieldList(
237
				$headerWrap = new CompositeField(
238
					new LiteralField(
239
						'Heading',
240
						sprintf('<h3 class="htmleditorfield-mediaform-heading insert">%s</h3>',
241
							_t('HtmlEditorField.LINK', 'Insert Link'))
242
					)
243
				),
244
				$contentComposite = new CompositeField(
245
					OptionsetField::create(
246
						'LinkType',
247
						sprintf($numericLabelTmpl, '1', _t('HtmlEditorField.LINKTO', 'Link to')),
248
						array(
249
							'internal' => _t('HtmlEditorField.LINKINTERNAL', 'Page on the site'),
250
							'external' => _t('HtmlEditorField.LINKEXTERNAL', 'Another website'),
251
							'anchor' => _t('HtmlEditorField.LINKANCHOR', 'Anchor on this page'),
252
							'email' => _t('HtmlEditorField.LINKEMAIL', 'Email address'),
253
							'file' => _t('HtmlEditorField.LINKFILE', 'Download a file'),
254
						),
255
						'internal'
256
					),
257
					LiteralField::create('Step2',
258
						'<div class="step2">'
259
						. sprintf($numericLabelTmpl, '2', _t('HtmlEditorField.DETAILS', 'Details')) . '</div>'
260
					),
261
					$siteTree,
262
					TextField::create('external', _t('HtmlEditorField.URL', 'URL'), 'http://'),
263
					EmailField::create('email', _t('HtmlEditorField.EMAIL', 'Email address')),
264
					$fileField = UploadField::create('file', _t('HtmlEditorField.FILE', 'File')),
265
					TextField::create('Anchor', _t('HtmlEditorField.ANCHORVALUE', 'Anchor')),
266
					TextField::create('Subject', _t('HtmlEditorField.SUBJECT', 'Email subject')),
267
					TextField::create('Description', _t('HtmlEditorField.LINKDESCR', 'Link description')),
268
					CheckboxField::create('TargetBlank',
269
						_t('HtmlEditorField.LINKOPENNEWWIN', 'Open link in a new window?')),
270
					HiddenField::create('Locale', null, $this->controller->Locale)
271
				)
272
			),
273
			new FieldList()
274
		);
275
276
		$headerWrap->addExtraClass('CompositeField composite cms-content-header nolabel ');
277
		$contentComposite->addExtraClass('ss-insert-link content');
278
		$fileField->setAllowedMaxFileNumber(1);
279
280
		$form->unsetValidator();
281
		$form->loadDataFrom($this);
282
		$form->addExtraClass('htmleditorfield-form htmleditorfield-linkform cms-mediaform-content');
283
284
		$this->extend('updateLinkForm', $form);
285
286
		return $form;
287
	}
288
289
	/**
290
	 * Get the folder ID to filter files by for the "from cms" tab
291
	 *
292
	 * @return int
293
	 */
294
	protected function getAttachParentID() {
295
		$parentID = $this->controller->getRequest()->requestVar('ParentID');
296
		$this->extend('updateAttachParentID', $parentID);
297
		return $parentID;
298
	}
299
300
	/**
301
	 * Return a {@link Form} instance allowing a user to
302
	 * add images and flash objects to the TinyMCE content editor.
303
	 *
304
	 * @return Form
305
	 */
306
	public function MediaForm() {
307
		// TODO Handle through GridState within field - currently this state set too late to be useful here (during
308
		// request handling)
309
		$parentID = $this->getAttachParentID();
310
311
		$fileFieldConfig = GridFieldConfig::create()->addComponents(
312
			new GridFieldFilterHeader(),
313
			new GridFieldSortableHeader(),
314
			new GridFieldDataColumns(),
315
			new GridFieldPaginator(7),
316
			// TODO Shouldn't allow delete here, its too confusing with a "remove from editor view" action.
317
			// Remove once we can fit the search button in the last actual title column
318
			new GridFieldDeleteAction(),
319
			new GridFieldDetailForm()
320
		);
321
		$fileField = GridField::create('Files', false, null, $fileFieldConfig);
322
		$fileField->setList($this->getFiles($parentID));
323
		$fileField->setAttribute('data-selectable', true);
324
		$fileField->setAttribute('data-multiselect', true);
325
		$columns = $fileField->getConfig()->getComponentByType('GridFieldDataColumns');
326
		$columns->setDisplayFields(array(
327
			'StripThumbnail' => false,
328
			'Title' => _t('File.Title'),
329
			'Created' => singleton('File')->fieldLabel('Created'),
330
		));
331
		$columns->setFieldCasting(array(
332
			'Created' => 'SS_Datetime->Nice'
333
		));
334
335
		$fromCMS = new CompositeField(
336
			$select = TreeDropdownField::create('ParentID', "", 'Folder')
337
				->addExtraClass('noborder')
338
				->setValue($parentID),
339
			$fileField
340
		);
341
342
		$fromCMS->addExtraClass('content ss-uploadfield htmleditorfield-from-cms');
343
		$select->addExtraClass('content-select');
344
345
346
		$URLDescription = _t('HtmlEditorField.URLDESCRIPTION', 'Insert videos and images from the web into your page simply by entering the URL of the file. Make sure you have the rights or permissions before sharing media directly from the web.<br /><br />Please note that files are not added to the file store of the CMS but embeds the file from its original location, if for some reason the file is no longer available in its original location it will no longer be viewable on this page.');
347
		$fromWeb = new CompositeField(
348
			$description = new LiteralField('URLDescription', '<div class="url-description">' . $URLDescription . '</div>'),
349
			$remoteURL = new TextField('RemoteURL', 'http://'),
350
			new LiteralField('addURLImage',
351
				'<button type="button" class="action ui-action-constructive ui-button field font-icon-plus add-url">' .
352
				_t('HtmlEditorField.BUTTONADDURL', 'Add url').'</button>')
353
		);
354
355
		$remoteURL->addExtraClass('remoteurl');
356
		$fromWeb->addExtraClass('content ss-uploadfield htmleditorfield-from-web');
357
358
		Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/AssetUploadField.css');
359
		$computerUploadField = Object::create('UploadField', 'AssetUploadField', '');
360
		$computerUploadField->setConfig('previewMaxWidth', 40);
361
		$computerUploadField->setConfig('previewMaxHeight', 30);
362
		$computerUploadField->addExtraClass('ss-assetuploadfield htmleditorfield-from-computer');
363
		$computerUploadField->removeExtraClass('ss-uploadfield');
364
		$computerUploadField->setTemplate('HtmlEditorField_UploadField');
365
		$computerUploadField->setFolderName(Config::inst()->get('Upload', 'uploads_folder'));
366
367
		$defaultPanel = new CompositeField(
368
			$computerUploadField,
369
			$fromCMS
370
		);
371
372
		$fromWebPanel = new CompositeField(
373
			$fromWeb
374
		);
375
376
		$defaultPanel->addExtraClass('htmleditorfield-default-panel');
377
		$fromWebPanel->addExtraClass('htmleditorfield-web-panel');
378
379
		$allFields = new CompositeField(
380
			$defaultPanel,
381
			$fromWebPanel,
382
			$editComposite = new CompositeField(
383
				new LiteralField('contentEdit', '<div class="content-edit ss-uploadfield-files files"></div>')
384
			)
385
		);
386
387
		$allFields->addExtraClass('ss-insert-media');
388
389
		$headings = new CompositeField(
390
			new LiteralField(
391
				'Heading',
392
				sprintf('<h3 class="htmleditorfield-mediaform-heading insert">%s</h3>',
393
					_t('HtmlEditorField.INSERTMEDIA', 'Insert media from')).
394
				sprintf('<h3 class="htmleditorfield-mediaform-heading update">%s</h3>',
395
					_t('HtmlEditorField.UpdateMEDIA', 'Update media'))
396
			)
397
		);
398
399
		$headings->addExtraClass('cms-content-header');
400
		$editComposite->addExtraClass('ss-assetuploadfield');
401
402
		$fields = new FieldList(
403
			$headings,
404
			$allFields
405
		);
406
407
		$form = new Form(
408
			$this->controller,
409
			"{$this->name}/MediaForm",
410
			$fields,
411
			new FieldList()
412
		);
413
414
415
		$form->unsetValidator();
416
		$form->disableSecurityToken();
417
		$form->loadDataFrom($this);
418
		$form->addExtraClass('htmleditorfield-form htmleditorfield-mediaform cms-dialog-content');
419
420
		// Allow other people to extend the fields being added to the imageform
421
		$this->extend('updateMediaForm', $form);
422
423
		return $form;
424
	}
425
426
	/**
427
	 * List of allowed schemes (no wildcard, all lower case) or empty to allow all schemes
428
	 *
429
	 * @config
430
	 * @var array
431
	 */
432
	private static $fileurl_scheme_whitelist = array('http', 'https');
433
434
	/**
435
	 * List of allowed domains (no wildcard, all lower case) or empty to allow all domains
436
	 *
437
	 * @config
438
	 * @var array
439
	 */
440
	private static $fileurl_domain_whitelist = array();
441
442
	/**
443
	 * Find local File dataobject given ID
444
	 *
445
	 * @param int $id
446
	 * @return array
447
	 */
448
	protected function viewfile_getLocalFileByID($id) {
449
		/** @var File $file */
450
		$file = DataObject::get_by_id('File', $id);
451
		if ($file && $file->canView()) {
452
			return array($file, $file->getURL());
453
		}
454
		return [null, null];
455
	}
456
457
	/**
458
	 * Get remote File given url
459
	 *
460
	 * @param string $fileUrl Absolute URL
461
	 * @return array
462
	 * @throws SS_HTTPResponse_Exception
463
	 */
464
	protected function viewfile_getRemoteFileByURL($fileUrl) {
465
		if(!Director::is_absolute_url($fileUrl)) {
466
			throw $this->getErrorFor(_t(
467
				"HtmlEditorField_Toolbar.ERROR_ABSOLUTE",
468
				"Only absolute urls can be embedded"
469
			));
470
		}
471
		$scheme = strtolower(parse_url($fileUrl, PHP_URL_SCHEME));
472
		$allowed_schemes = self::config()->fileurl_scheme_whitelist;
473
		if (!$scheme || ($allowed_schemes && !in_array($scheme, $allowed_schemes))) {
474
			throw $this->getErrorFor(_t(
475
				"HtmlEditorField_Toolbar.ERROR_SCHEME",
476
				"This file scheme is not included in the whitelist"
477
			));
478
		}
479
		$domain = strtolower(parse_url($fileUrl, PHP_URL_HOST));
480
		$allowed_domains = self::config()->fileurl_domain_whitelist;
481
		if (!$domain || ($allowed_domains && !in_array($domain, $allowed_domains))) {
482
			throw $this->getErrorFor(_t(
483
				"HtmlEditorField_Toolbar.ERROR_HOSTNAME",
484
				"This file hostname is not included in the whitelist"
485
			));
486
		}
487
		return [null, $fileUrl];
488
	}
489
490
	/**
491
	 * Prepare error for the front end
492
	 *
493
	 * @param string $message
494
	 * @param int $code
495
	 * @return SS_HTTPResponse_Exception
496
	 */
497
	protected function getErrorFor($message, $code = 400) {
498
		$exception = new HTTPResponse_Exception($message, $code);
499
		$exception->getResponse()->addHeader('X-Status', $message);
500
		return $exception;
501
	}
502
503
	/**
504
	 * View of a single file, either on the filesystem or on the web.
505
	 *
506
	 * @throws SS_HTTPResponse_Exception
507
	 * @param SS_HTTPRequest $request
508
	 * @return string
509
	 */
510
	public function viewfile($request) {
511
		$file = null;
512
		$url = null;
513
		// Get file and url by request method
514
		if($fileUrl = $request->getVar('FileURL')) {
515
			// Get remote url
516
			list($file, $url) = $this->viewfile_getRemoteFileByURL($fileUrl);
517
		} elseif($id = $request->getVar('ID')) {
518
			// Or we could have been passed an ID directly
519
			list($file, $url) = $this->viewfile_getLocalFileByID($id);
520
		} else {
521
			// Or we could have been passed nothing, in which case panic
522
			throw $this->getErrorFor(_t(
523
				"HtmlEditorField_Toolbar.ERROR_ID",
524
				'Need either "ID" or "FileURL" parameter to identify the file'
525
			));
526
		}
527
528
		// Validate file exists
529
		if(!$url) {
530
			throw $this->getErrorFor(_t(
531
				"HtmlEditorField_Toolbar.ERROR_NOTFOUND",
532
				'Unable to find file to view'
533
			));
534
		}
535
536
		// Instanciate file wrapper and get fields based on its type
537
		// Check if appCategory is an image and exists on the local system, otherwise use oEmbed to refference a
538
		// remote image
539
		$fileCategory = $this->getFileCategory($url, $file);
540
		switch($fileCategory) {
541
			case 'image':
542
			case 'image/supported':
543
				$fileWrapper = new HtmlEditorField_Image($url, $file);
544
				break;
545
			case 'flash':
546
				$fileWrapper = new HtmlEditorField_Flash($url, $file);
547
				break;
548
			default:
549
				// Only remote files can be linked via o-embed
550
				// {@see HtmlEditorField_Toolbar::getAllowedExtensions())
551
				if($file) {
552
					throw $this->getErrorFor(_t(
553
						"HtmlEditorField_Toolbar.ERROR_OEMBED_REMOTE",
554
						"Oembed is only compatible with remote files"
555
					));
556
				}
557
558
				// Other files should fallback to oembed
559
				$fileWrapper = new HtmlEditorField_Embed($url, $file);
560
				break;
561
		}
562
563
		// Render fields and return
564
		$fields = $this->getFieldsForFile($url, $fileWrapper);
565
		return $fileWrapper->customise(array(
566
			'Fields' => $fields,
567
		))->renderWith($this->templateViewFile);
568
	}
569
570
	/**
571
	 * Guess file category from either a file or url
572
	 *
573
	 * @param string $url
574
	 * @param File $file
575
	 * @return string
576
	 */
577
	protected function getFileCategory($url, $file) {
578
		if($file) {
579
			return $file->appCategory();
580
		}
581
		if($url) {
582
			return File::get_app_category(File::get_file_extension($url));
583
		}
584
		return null;
585
	}
586
587
	/**
588
	 * Find all anchors available on the given page.
589
	 *
590
	 * @return array
591
	 * @throws SS_HTTPResponse_Exception
592
	 */
593
	public function getanchors() {
594
		$id = (int)$this->getRequest()->getVar('PageID');
595
		$anchors = array();
596
597
		if (($page = Page::get()->byID($id)) && !empty($page)) {
598
			if (!$page->canView()) {
599
				throw new HTTPResponse_Exception(
600
					_t(
601
						'HtmlEditorField.ANCHORSCANNOTACCESSPAGE',
602
						'You are not permitted to access the content of the target page.'
603
					),
604
					403
605
				);
606
			}
607
608
			// Similar to the regex found in HtmlEditorField.js / getAnchors method.
609
			if (preg_match_all(
610
				"/\\s+(name|id)\\s*=\\s*([\"'])([^\\2\\s>]*?)\\2|\\s+(name|id)\\s*=\\s*([^\"']+)[\\s +>]/im",
611
				$page->Content,
612
				$matches
613
			)) {
614
				$anchors = array_values(array_unique(array_filter(
615
					array_merge($matches[3], $matches[5]))
616
				));
617
			}
618
619
		} else {
620
			throw new HTTPResponse_Exception(
621
				_t('HtmlEditorField.ANCHORSPAGENOTFOUND', 'Target page not found.'),
622
				404
623
			);
624
		}
625
626
		return json_encode($anchors);
627
	}
628
629
	/**
630
	 * Similar to {@link File->getCMSFields()}, but only returns fields
631
	 * for manipulating the instance of the file as inserted into the HTML content,
632
	 * not the "master record" in the database - hence there's no form or saving logic.
633
	 *
634
	 * @param string $url Abolute URL to asset
635
	 * @param HtmlEditorField_File $file Asset wrapper
636
	 * @return FieldList
637
	 */
638
	protected function getFieldsForFile($url, HtmlEditorField_File $file) {
639
		$fields = $this->extend('getFieldsForFile', $url, $file);
640
		if(!$fields) {
641
			$fields = $file->getFields();
642
			$file->extend('updateFields', $fields);
643
		}
644
		$this->extend('updateFieldsForFile', $fields, $url, $file);
645
		return $fields;
646
	}
647
648
649
	/**
650
	 * Gets files filtered by a given parent with the allowed extensions
651
	 *
652
	 * @param int $parentID
653
	 * @return DataList
654
	 */
655
	protected function getFiles($parentID = null) {
656
		$exts = $this->getAllowedExtensions();
657
		$dotExts = array_map(function($ext) {
658
			return ".{$ext}";
659
		}, $exts);
660
		$files = File::get()->filter('Name:EndsWith', $dotExts);
661
662
		// Limit by folder (if required)
663
		if($parentID) {
664
			$files = $files->filter('ParentID', $parentID);
665
		}
666
667
		return $files;
668
	}
669
670
	/**
671
	 * @return Array All extensions which can be handled by the different views.
672
	 */
673
	protected function getAllowedExtensions() {
674
		$exts = array('jpg', 'gif', 'png', 'swf', 'jpeg');
675
		$this->extend('updateAllowedExtensions', $exts);
676
		return $exts;
677
	}
678
679
}
680
681
/**
682
 * Encapsulation of a file which can either be a remote URL
683
 * or a {@link File} on the local filesystem, exhibiting common properties
684
 * such as file name or the URL.
685
 *
686
 * @todo Remove once core has support for remote files
687
 * @package forms
688
 * @subpackage fields-formattedinput
689
 */
690
abstract class HtmlEditorField_File extends ViewableData {
691
692
	/**
693
	 * Default insertion width for Images and Media
694
	 *
695
	 * @config
696
	 * @var int
697
	 */
698
	private static $insert_width = 600;
699
700
	/**
701
	 * Default insert height for images and media
702
	 *
703
	 * @config
704
	 * @var int
705
	 */
706
	private static $insert_height = 360;
707
708
	/**
709
	 * Max width for insert-media preview.
710
	 *
711
	 * Matches CSS rule for .cms-file-info-preview
712
	 *
713
	 * @var int
714
	 */
715
	private static $media_preview_width = 176;
716
717
	/**
718
	 * Max height for insert-media preview.
719
	 *
720
	 * Matches CSS rule for .cms-file-info-preview
721
	 *
722
	 * @var int
723
	 */
724
	private static $media_preview_height = 128;
725
726
	private static $casting = array(
727
		'URL' => 'Varchar',
728
		'Name' => 'Varchar'
729
	);
730
731
	/**
732
	 * Absolute URL to asset
733
	 *
734
	 * @var string
735
	 */
736
	protected $url;
737
738
	/**
739
	 * File dataobject (if available)
740
	 *
741
	 * @var File
742
	 */
743
	protected $file;
744
745
	/**
746
	 * @param string $url
747
	 * @param File $file
748
	 */
749
	public function __construct($url, File $file = null) {
750
		$this->url = $url;
751
		$this->file = $file;
752
		$this->failover = $file;
753
		parent::__construct();
754
	}
755
756
	/**
757
	 * @return FieldList
758
	 */
759
	public function getFields() {
760
		$fields = new FieldList(
761
			CompositeField::create(
762
				CompositeField::create(LiteralField::create("ImageFull", $this->getPreview()))
763
					->setName("FilePreviewImage")
764
					->addExtraClass('cms-file-info-preview'),
765
				CompositeField::create($this->getDetailFields())
766
					->setName("FilePreviewData")
767
					->addExtraClass('cms-file-info-data')
768
			)
769
				->setName("FilePreview")
770
				->addExtraClass('cms-file-info'),
771
			TextField::create('CaptionText', _t('HtmlEditorField.CAPTIONTEXT', 'Caption text')),
772
			DropdownField::create(
773
				'CSSClass',
774
				_t('HtmlEditorField.CSSCLASS', 'Alignment / style'),
775
				array(
776
					'leftAlone' => _t('HtmlEditorField.CSSCLASSLEFTALONE', 'On the left, on its own.'),
777
					'center' => _t('HtmlEditorField.CSSCLASSCENTER', 'Centered, on its own.'),
778
					'left' => _t('HtmlEditorField.CSSCLASSLEFT', 'On the left, with text wrapping around.'),
779
					'right' => _t('HtmlEditorField.CSSCLASSRIGHT', 'On the right, with text wrapping around.')
780
				)
781
			),
782
			FieldGroup::create(_t('HtmlEditorField.IMAGEDIMENSIONS', 'Dimensions'),
783
				TextField::create(
784
					'Width',
785
					_t('HtmlEditorField.IMAGEWIDTHPX', 'Width'),
786
					$this->getInsertWidth()
787
				)->setMaxLength(5),
788
				TextField::create(
789
					'Height',
790
					" x " . _t('HtmlEditorField.IMAGEHEIGHTPX', 'Height'),
791
					$this->getInsertHeight()
792
				)->setMaxLength(5)
793
			)->addExtraClass('dimensions last'),
794
			HiddenField::create('URL', false, $this->getURL()),
795
			HiddenField::create('FileID', false, $this->getFileID())
796
		);
797
		return $fields;
798
	}
799
800
	/**
801
	 * Get list of fields for previewing this records details
802
	 *
803
	 * @return FieldList
804
	 */
805
	protected function getDetailFields() {
806
		$fields = new FieldList(
807
			ReadonlyField::create("FileType", _t('AssetTableField.TYPE','File type'), $this->getFileType()),
808
			ReadonlyField::create(
809
				'ClickableURL', _t('AssetTableField.URL','URL'), $this->getExternalLink()
810
			)->setDontEscape(true)
811
		);
812
		// Get file size
813
		if($this->getSize()) {
814
			$fields->insertAfter(
815
				'FileType',
816
				ReadonlyField::create("Size", _t('AssetTableField.SIZE','File size'), $this->getSize())
817
			);
818
		}
819
		// Get modified details of local record
820
		if($this->getFile()) {
821
			$fields->push(new DateField_Disabled(
822
				"Created",
823
				_t('AssetTableField.CREATED', 'First uploaded'),
824
				$this->getFile()->Created
825
			));
826
			$fields->push(new DateField_Disabled(
827
				"LastEdited",
828
				_t('AssetTableField.LASTEDIT','Last changed'),
829
				$this->getFile()->LastEdited
830
			));
831
		}
832
		return $fields;
833
834
	}
835
836
	/**
837
	 * Get file DataObject
838
	 *
839
	 * Might not be set (for remote files)
840
	 *
841
	 * @return File
842
	 */
843
	public function getFile() {
844
		return $this->file;
845
	}
846
847
	/**
848
	 * Get file ID
849
	 *
850
	 * @return int
851
	 */
852
	public function getFileID() {
853
		if($file = $this->getFile()) {
854
			return $file->ID;
855
		}
856
	}
857
858
	/**
859
	 * Get absolute URL
860
	 *
861
	 * @return string
862
	 */
863
	public function getURL() {
864
		return $this->url;
865
	}
866
867
	/**
868
	 * Get basename
869
	 *
870
	 * @return string
871
	 */
872
	public function getName() {
873
		return $this->file
874
			? $this->file->Name
875
			: preg_replace('/\?.*/', '', basename($this->url));
876
	}
877
878
	/**
879
	 * Get descriptive file type
880
	 *
881
	 * @return string
882
	 */
883
	public function getFileType() {
884
		return File::get_file_type($this->getName());
885
	}
886
887
	/**
888
	 * Get file size (if known) as string
889
	 *
890
	 * @return string|false String value, or false if doesn't exist
891
	 */
892
	public function getSize() {
893
		if($this->file) {
894
			return $this->file->getSize();
895
		}
896
		return false;
897
	}
898
899
	/**
900
	 * HTML content for preview
901
	 *
902
	 * @return string HTML
903
	 */
904
	public function getPreview() {
905
		$preview = $this->extend('getPreview');
906
		if($preview) {
907
			return $preview;
908
		}
909
910
		// Generate tag from preview
911
		$thumbnailURL = Convert::raw2att(
912
			Controller::join_links($this->getPreviewURL(), "?r=" . rand(1,100000))
913
		);
914
		$fileName = Convert::raw2att($this->Name);
915
		return sprintf(
916
			"<img id='thumbnailImage' class='thumbnail-preview'  src='%s' alt='%s' />\n",
917
			$thumbnailURL,
918
			$fileName
919
		);
920
	}
921
922
	/**
923
	 * HTML Content for external link
924
	 *
925
	 * @return string
926
	 */
927
	public function getExternalLink() {
928
		$title = $this->file
929
			? $this->file->getTitle()
930
			: $this->getName();
931
		return sprintf(
932
			'<a href="%1$s" title="%2$s" target="_blank" rel="external" class="file-url">%1$s</a>',
933
			Convert::raw2att($this->url),
934
			Convert::raw2att($title)
935
		);
936
	}
937
938
	/**
939
	 * Generate thumbnail url
940
	 *
941
	 * @return string
942
	 */
943
	public function getPreviewURL() {
944
		// Get preview from file
945
		if($this->file) {
946
			return $this->getFilePreviewURL();
947
		}
948
949
		// Generate default icon html
950
		return File::get_icon_for_extension($this->getExtension());
951
	}
952
953
	/**
954
	 * Generate thumbnail URL from file dataobject (if available)
955
	 *
956
	 * @return string
957
	 */
958
	protected function getFilePreviewURL() {
959
		// Get preview from file
960
		if($this->file) {
961
			$width = $this->config()->media_preview_width;
962
			$height = $this->config()->media_preview_height;
963
			return $this->file->ThumbnailURL($width, $height);
964
		}
965
	}
966
967
	/**
968
	 * Get file extension
969
	 *
970
	 * @return string
971
	 */
972
	public function getExtension() {
973
		$extension = File::get_file_extension($this->getName());
974
		return strtolower($extension);
975
	}
976
977
	/**
978
	 * Category name
979
	 *
980
	 * @return string
981
	 */
982
	public function appCategory() {
983
		if($this->file) {
984
			return $this->file->appCategory();
985
		} else {
986
			return File::get_app_category($this->getExtension());
987
		}
988
	}
989
990
	/**
991
	 * Get height of this item
992
	 */
993
	public function getHeight() {
994
		if($this->file) {
995
			$height = $this->file->getHeight();
996
			if($height) {
997
				return $height;
998
			}
999
		}
1000
		return $this->config()->insert_height;
1001
	}
1002
1003
	/**
1004
	 * Get width of this item
1005
	 *
1006
	 * @return type
1007
	 */
1008
	public function getWidth() {
1009
		if($this->file) {
1010
			$width = $this->file->getWidth();
1011
			if($width) {
1012
				return $width;
1013
			}
1014
		}
1015
		return $this->config()->insert_width;
1016
	}
1017
1018
	/**
1019
	 * Provide an initial width for inserted media, restricted based on $embed_width
1020
	 *
1021
	 * @return int
1022
	 */
1023
	public function getInsertWidth() {
1024
		$width = $this->getWidth();
1025
		$maxWidth = $this->config()->insert_width;
1026
		return ($width <= $maxWidth) ? $width : $maxWidth;
1027
	}
1028
1029
	/**
1030
	 * Provide an initial height for inserted media, scaled proportionally to the initial width
1031
	 *
1032
	 * @return int
1033
	 */
1034
	public function getInsertHeight() {
1035
		$width = $this->getWidth();
1036
		$height = $this->getHeight();
1037
		$maxWidth = $this->config()->insert_width;
1038
		return ($width <= $maxWidth) ? $height : round($height*($maxWidth/$width));
1039
	}
1040
1041
}
1042
1043
/**
1044
 * Encapsulation of an oembed tag, linking to an external media source.
1045
 *
1046
 * @see Oembed
1047
 * @package forms
1048
 * @subpackage fields-formattedinput
1049
 */
1050
class HtmlEditorField_Embed extends HtmlEditorField_File {
1051
1052
	private static $casting = array(
1053
		'Type' => 'Varchar',
1054
		'Info' => 'Varchar'
1055
	);
1056
1057
	/**
1058
	 * Oembed result
1059
	 *
1060
	 * @var Oembed_Result
1061
	 */
1062
	protected $oembed;
1063
1064
	public function __construct($url, File $file = null) {
1065
		parent::__construct($url, $file);
1066
		$this->oembed = Oembed::get_oembed_from_url($url);
1067
		if(!$this->oembed) {
1068
			$controller = Controller::curr();
1069
			$response = $controller->getResponse();
1070
			$response->addHeader('X-Status',
1071
				rawurlencode(_t(
1072
					'HtmlEditorField.URLNOTANOEMBEDRESOURCE',
1073
					"The URL '{url}' could not be turned into a media resource.",
1074
					"The given URL is not a valid Oembed resource; the embed element couldn't be created.",
1075
					array('url' => $url)
1076
				)));
1077
			$response->setStatusCode(404);
1078
1079
			throw new HTTPResponse_Exception($response);
1080
		}
1081
	}
1082
1083
	/**
1084
	 * Get file-edit fields for this filed
1085
	 *
1086
	 * @return FieldList
1087
	 */
1088
	public function getFields() {
1089
		$fields = parent::getFields();
1090
		if($this->Type === 'photo') {
1091
			$fields->insertBefore('CaptionText', new TextField(
1092
				'AltText',
1093
				_t('HtmlEditorField.IMAGEALTTEXT', 'Alternative text (alt) - shown if image can\'t be displayed'),
1094
				$this->Title,
1095
				80
1096
			));
1097
			$fields->insertBefore('CaptionText', new TextField(
1098
				'Title',
1099
				_t('HtmlEditorField.IMAGETITLE', 'Title text (tooltip) - for additional information about the image')
1100
			));
1101
		}
1102
		return $fields;
1103
	}
1104
1105
	/**
1106
	 * Get width of this oembed
1107
	 *
1108
	 * @return int
1109
	 */
1110
	public function getWidth() {
1111
		return $this->oembed->Width ?: 100;
1112
	}
1113
1114
	/**
1115
	 * Get height of this oembed
1116
	 *
1117
	 * @return int
1118
	 */
1119
	public function getHeight() {
1120
		return $this->oembed->Height ?: 100;
1121
	}
1122
1123
	public function getPreviewURL() {
1124
		// Use thumbnail url
1125
		if(!empty($this->oembed->thumbnail_url)) {
1126
			return $this->oembed->thumbnail_url;
1127
		}
1128
1129
		// Use direct image type
1130
		if($this->getType() == 'photo' && !empty($this->Oembed->url)) {
1131
			return $this->Oembed->url;
1132
		}
1133
1134
		// Default media
1135
		return FRAMEWORK_DIR . '/images/default_media.png';
1136
	}
1137
1138
	public function getName() {
1139
		if(isset($this->oembed->title)) {
1140
			return $this->oembed->title;
1141
		} else {
1142
			return parent::getName();
1143
		}
1144
	}
1145
1146
	/**
1147
	 * Get OEmbed type
1148
	 *
1149
	 * @return string
1150
	 */
1151
	public function getType() {
1152
		return $this->oembed->type;
1153
	}
1154
1155
	public function getFileType() {
1156
		return $this->getType()
1157
			?: parent::getFileType();
1158
	}
1159
1160
	/**
1161
	 * @return Oembed_Result
1162
	 */
1163
	public function getOembed() {
1164
		return $this->oembed;
1165
	}
1166
1167
	public function appCategory() {
1168
		return 'embed';
1169
	}
1170
1171
	/**
1172
	 * Info for this oembed
1173
	 *
1174
	 * @return string
1175
	 */
1176
	public function getInfo() {
1177
		return $this->oembed->info;
1178
	}
1179
}
1180
1181
/**
1182
 * Encapsulation of an image tag, linking to an image either internal or external to the site.
1183
 *
1184
 * @package forms
1185
 * @subpackage fields-formattedinput
1186
 */
1187
class HtmlEditorField_Image extends HtmlEditorField_File {
1188
1189
	/**
1190
	 * @var int
1191
	 */
1192
	protected $width;
1193
1194
	/**
1195
	 * @var int
1196
	 */
1197
	protected $height;
1198
1199
	/**
1200
	 * File size details
1201
	 *
1202
	 * @var string
1203
	 */
1204
	protected $size;
1205
1206
	public function __construct($url, File $file = null) {
1207
		parent::__construct($url, $file);
1208
1209
		if($file) {
1210
			return;
1211
		}
1212
1213
		// Get size of remote file
1214
		$size = @filesize($url);
1215
		if($size) {
1216
			$this->size = $size;
1217
		}
1218
1219
		// Get dimensions of remote file
1220
		$info = @getimagesize($url);
1221
		if($info) {
1222
			$this->width = $info[0];
1223
			$this->height = $info[1];
1224
		}
1225
	}
1226
1227
	public function getFields() {
1228
		$fields = parent::getFields();
1229
1230
		// Alt text
1231
		$fields->insertBefore(
1232
			'CaptionText',
1233
			TextField::create(
1234
				'AltText',
1235
				_t('HtmlEditorField.IMAGEALT', 'Alternative text (alt)'),
1236
				$this->Title,
1237
				80
1238
			)->setDescription(
1239
				_t('HtmlEditorField.IMAGEALTTEXTDESC', 'Shown to screen readers or if image can\'t be displayed')
1240
			)
1241
		);
1242
1243
		// Tooltip
1244
		$fields->insertAfter(
1245
			'AltText',
1246
			TextField::create(
1247
				'Title',
1248
				_t('HtmlEditorField.IMAGETITLETEXT', 'Title text (tooltip)')
1249
			)->setDescription(
1250
				_t('HtmlEditorField.IMAGETITLETEXTDESC', 'For additional information about the image')
1251
			)
1252
		);
1253
1254
		return $fields;
1255
	}
1256
1257
	protected function getDetailFields() {
1258
		$fields = parent::getDetailFields();
1259
		$width = $this->getOriginalWidth();
1260
		$height = $this->getOriginalHeight();
1261
1262
		// Show dimensions of original
1263
		if($width && $height) {
1264
			$fields->insertAfter(
1265
				'ClickableURL',
1266
				ReadonlyField::create(
1267
					"OriginalWidth",
1268
					_t('AssetTableField.WIDTH','Width'),
1269
					$width
1270
				)
1271
			);
1272
			$fields->insertAfter(
1273
				'OriginalWidth',
1274
				ReadonlyField::create(
1275
					"OriginalHeight",
1276
					_t('AssetTableField.HEIGHT','Height'),
1277
					$height
1278
				)
1279
			);
1280
		}
1281
		return $fields;
1282
	}
1283
1284
	/**
1285
	 * Get width of original, if known
1286
	 *
1287
	 * @return int
1288
	 */
1289
	public function getOriginalWidth() {
1290
		if($this->width) {
1291
			return $this->width;
1292
		}
1293
		if($this->file) {
1294
			$width = $this->file->getWidth();
1295
			if($width) {
1296
				return $width;
1297
			}
1298
		}
1299
	}
1300
1301
	/**
1302
	 * Get height of original, if known
1303
	 *
1304
	 * @return int
1305
	 */
1306
	public function getOriginalHeight() {
1307
		if($this->height) {
1308
			return $this->height;
1309
		}
1310
1311
		if($this->file) {
1312
			$height = $this->file->getHeight();
1313
			if($height) {
1314
				return $height;
1315
			}
1316
		}
1317
	}
1318
1319
	public function getWidth() {
1320
		if($this->width) {
1321
			return $this->width;
1322
		}
1323
		return parent::getWidth();
1324
	}
1325
1326
	public function getHeight() {
1327
		if($this->height) {
1328
			return $this->height;
1329
		}
1330
		return parent::getHeight();
1331
	}
1332
1333
	public function getSize() {
1334
		if($this->size) {
1335
			return File::format_size($this->size);
1336
		}
1337
		parent::getSize();
1338
	}
1339
1340
	/**
1341
	 * Provide an initial width for inserted image, restricted based on $embed_width
1342
	 *
1343
	 * @return int
1344
	 */
1345
	public function getInsertWidth() {
1346
		$width = $this->getWidth();
1347
		$maxWidth = $this->config()->insert_width;
1348
		return $width <= $maxWidth
1349
			? $width
1350
			: $maxWidth;
1351
	}
1352
1353
	/**
1354
	 * Provide an initial height for inserted image, scaled proportionally to the initial width
1355
	 *
1356
	 * @return int
1357
	 */
1358
	public function getInsertHeight() {
1359
		$width = $this->getWidth();
1360
		$height = $this->getHeight();
1361
		$maxWidth = $this->config()->insert_width;
1362
		return ($width <= $maxWidth) ? $height : round($height*($maxWidth/$width));
1363
	}
1364
1365
	public function getPreviewURL() {
1366
		// Get preview from file
1367
		if($this->file) {
1368
			return $this->getFilePreviewURL();
1369
		}
1370
1371
		// Embed image directly
1372
		return $this->url;
1373
	}
1374
}
1375
1376
/**
1377
 * Generate flash file embed
1378
 */
1379
class HtmlEditorField_Flash extends HtmlEditorField_File {
1380
1381
	public function getFields() {
1382
		$fields = parent::getFields();
1383
		$fields->removeByName('CaptionText', true);
1384
		return $fields;
1385
	}
1386
}
1387