Completed
Push — 3.7 ( 81b2d8...ef0909 )
by
unknown
09:42
created

viewfile_getLocalFileByURL()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 1
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
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
class HtmlEditorField extends TextareaField {
10
11
	/**
12
	 * @config
13
	 * @var Boolean Use TinyMCE's GZIP compressor
14
	 */
15
	private static $use_gzip = true;
16
17
	/**
18
	 * @config
19
	 * @var Integer Default insertion width for Images and Media
20
	 */
21
	private static $insert_width = 600;
22
23
	/**
24
	 * @config
25
	 * @var string Default alignment for Images and Media. Options: leftAlone|center|left|right
26
	 */
27
	private static $media_alignment = 'leftAlone';
28
29
	/**
30
	 * @config
31
	 * @var bool Should we check the valid_elements (& extended_valid_elements) rules from HtmlEditorConfig server side?
32
	 */
33
	private static $sanitise_server_side = false;
34
35
	protected $rows = 30;
36
37
	/**
38
	 * @deprecated since version 4.0
39
	 */
40
	public static function include_js() {
41
		Deprecation::notice('4.0', 'Use HtmlEditorConfig::require_js() instead');
42
		HtmlEditorConfig::require_js();
43
	}
44
45
46
	protected $editorConfig = null;
47
48
	/**
49
	 * Creates a new HTMLEditorField.
50
	 * @see TextareaField::__construct()
51
	 *
52
	 * @param string $name The internal field name, passed to forms.
53
	 * @param string $title The human-readable field label.
54
	 * @param mixed $value The value of the field.
55
	 * @param string $config HTMLEditorConfig identifier to be used. Default to the active one.
56
	 */
57
	public function __construct($name, $title = null, $value = '', $config = null) {
58
		parent::__construct($name, $title, $value);
59
60
		$this->editorConfig = $config ? $config : HtmlEditorConfig::get_active_identifier();
61
	}
62
63
	public function getAttributes() {
64
		return array_merge(
65
			parent::getAttributes(),
66
			array(
67
				'tinymce' => 'true',
68
				'style'   => 'width: 97%; height: ' . ($this->rows * 16) . 'px', // prevents horizontal scrollbars
69
				'value' => null,
70
				'data-config' => $this->editorConfig
71
			)
72
		);
73
	}
74
75
	public function saveInto(DataObjectInterface $record) {
76
		if($record->hasField($this->name) && $record->escapeTypeForField($this->name) != 'xml') {
77
			throw new Exception (
78
				'HtmlEditorField->saveInto(): This field should save into a HTMLText or HTMLVarchar field.'
79
			);
80
		}
81
82
		$htmlValue = Injector::inst()->create('HTMLValue', $this->value);
83
84
		// Sanitise if requested
85
		if($this->config()->sanitise_server_side) {
86
			$santiser = Injector::inst()->create('HtmlEditorSanitiser', HtmlEditorConfig::get_active());
87
			$santiser->sanitise($htmlValue);
88
		}
89
90
		// Resample images and add default attributes
91
		if($images = $htmlValue->getElementsByTagName('img')) foreach($images as $img) {
92
			// strip any ?r=n data from the src attribute
93
			$img->setAttribute('src', preg_replace('/([^\?]*)\?r=[0-9]+$/i', '$1', $img->getAttribute('src')));
94
95
			// Resample the images if the width & height have changed.
96
			$image = File::find(urldecode(Director::makeRelative($img->getAttribute('src'))));
97
			if($image instanceof Image){
98
				$width  = (int)$img->getAttribute('width');
99
				$height = (int)$img->getAttribute('height');
100
101
				if($width && $height && ($width != $image->getWidth() || $height != $image->getHeight())) {
102
					//Make sure that the resized image actually returns an image:
103
					$resized = $image->ResizedImage($width, $height);
104
					if($resized) $img->setAttribute('src', $resized->getRelativePath());
105
				}
106
			}
107
108
			// Add default empty title & alt attributes.
109
			if(!$img->getAttribute('alt')) $img->setAttribute('alt', '');
110
			if(!$img->getAttribute('title')) $img->setAttribute('title', '');
111
112
			// Use this extension point to manipulate images inserted using TinyMCE, e.g. add a CSS class, change default title
113
			// $image is the image, $img is the DOM model
114
			$this->extend('processImage', $image, $img);
115
		}
116
117
		// optionally manipulate the HTML after a TinyMCE edit and prior to a save
118
		$this->extend('processHTML', $htmlValue);
119
120
		// Store into record
121
		$record->{$this->name} = $htmlValue->getContent();
122
	}
123
124
	/**
125
	 * @return HtmlEditorField_Readonly
126
	 */
127
	public function performReadonlyTransformation() {
128
		$field = $this->castedCopy('HtmlEditorField_Readonly');
129
		$field->dontEscape = true;
130
131
		return $field;
132
	}
133
134
	public function performDisabledTransformation() {
135
		return $this->performReadonlyTransformation();
136
	}
137
}
138
139
/**
140
 * Readonly version of an {@link HTMLEditorField}.
141
 * @package forms
142
 * @subpackage fields-formattedinput
143
 */
144
class HtmlEditorField_Readonly extends ReadonlyField {
145
	public function Field($properties = array()) {
146
		$valforInput = $this->value ? Convert::raw2att($this->value) : "";
147
		return "<span class=\"readonly typography\" id=\"" . $this->id() . "\">"
148
			. ( $this->value && $this->value != '<p></p>' ? $this->value : '<i>(not set)</i>' )
149
			. "</span><input type=\"hidden\" name=\"".$this->name."\" value=\"".$valforInput."\" />";
150
	}
151
	public function Type() {
152
		return 'htmleditorfield readonly';
153
	}
154
}
155
156
/**
157
 * Toolbar shared by all instances of {@link HTMLEditorField}, to avoid too much markup duplication.
158
 *  Needs to be inserted manually into the template in order to function - see {@link LeftAndMain->EditorToolbar()}.
159
 *
160
 * @package forms
161
 * @subpackage fields-formattedinput
162
 */
163
class HtmlEditorField_Toolbar extends RequestHandler {
164
165
	private static $allowed_actions = array(
166
		'LinkForm',
167
		'MediaForm',
168
		'viewfile',
169
		'getanchors'
170
	);
171
172
	/**
173
	 * @var string
174
	 */
175
	protected $templateViewFile = 'HtmlEditorField_viewfile';
176
177
	protected $controller, $name;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
178
179
	public function __construct($controller, $name) {
180
		parent::__construct();
181
182
		Requirements::javascript(FRAMEWORK_DIR . "/thirdparty/jquery/jquery.js");
183
		Requirements::javascript(THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js');
184
		Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
185
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/ssui.core.js');
186
187
		HtmlEditorConfig::require_js();
188
		Requirements::javascript(FRAMEWORK_DIR ."/javascript/HtmlEditorField.js");
189
190
		Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
191
192
		$this->controller = $controller;
193
		$this->name = $name;
194
	}
195
196
	public function forTemplate() {
197
		return sprintf(
198
			'<div id="cms-editor-dialogs" data-url-linkform="%s" data-url-mediaform="%s"></div>',
199
			Controller::join_links($this->controller->Link(), $this->name, 'LinkForm', 'forTemplate'),
200
			Controller::join_links($this->controller->Link(), $this->name, 'MediaForm', 'forTemplate')
201
		);
202
	}
203
204
	/**
205
	 * Searches the SiteTree for display in the dropdown
206
	 *
207
	 * @return callback
208
	 */
209
	public function siteTreeSearchCallback($sourceObject, $labelField, $search) {
210
		return DataObject::get($sourceObject)->filterAny(array(
211
			'MenuTitle:PartialMatch' => $search,
212
			'Title:PartialMatch' => $search
213
		));
214
	}
215
216
	/**
217
	 * Return a {@link Form} instance allowing a user to
218
	 * add links in the TinyMCE content editor.
219
	 *
220
	 * @return Form
221
	 */
222
	public function LinkForm() {
223
		$siteTree = TreeDropdownField::create('internal', _t('HtmlEditorField.PAGE', "Page"),
224
			'SiteTree', 'ID', 'MenuTitle', true);
225
		// mimic the SiteTree::getMenuTitle(), which is bypassed when the search is performed
226
		$siteTree->setSearchFunction(array($this, 'siteTreeSearchCallback'));
227
228
		$numericLabelTmpl = '<span class="step-label"><span class="flyout">%d</span><span class="arrow"></span>'
229
			. '<strong class="title">%s</strong></span>';
230
		$form = new Form(
231
			$this->controller,
232
			"{$this->name}/LinkForm",
233
			new FieldList(
234
				$headerWrap = new CompositeField(
235
					new LiteralField(
236
						'Heading',
237
						sprintf('<h3 class="htmleditorfield-mediaform-heading insert">%s</h3>',
238
							_t('HtmlEditorField.LINK', 'Insert Link'))
239
					)
240
				),
241
				$contentComposite = new CompositeField(
242
					OptionsetField::create(
243
						'LinkType',
244
						sprintf($numericLabelTmpl, '1', _t('HtmlEditorField.LINKTO', 'Link to')),
245
						array(
246
							'internal' => _t('HtmlEditorField.LINKINTERNAL', 'Page on the site'),
247
							'external' => _t('HtmlEditorField.LINKEXTERNAL', 'Another website'),
248
							'anchor' => _t('HtmlEditorField.LINKANCHOR', 'Anchor on this page'),
249
							'email' => _t('HtmlEditorField.LINKEMAIL', 'Email address'),
250
							'file' => _t('HtmlEditorField.LINKFILE', 'Download a file'),
251
						),
252
						'internal'
253
					),
254
					LiteralField::create('Step2',
255
						'<div class="step2">'
256
						. sprintf($numericLabelTmpl, '2', _t('HtmlEditorField.DETAILS', 'Details')) . '</div>'
257
					),
258
					$siteTree,
259
					TextField::create('external', _t('HtmlEditorField.URL', 'URL'), 'http://'),
260
					EmailField::create('email', _t('HtmlEditorField.EMAIL', 'Email address')),
261
					$fileField = UploadField::create('file', _t('HtmlEditorField.FILE', 'File')),
262
					TextField::create('Anchor', _t('HtmlEditorField.ANCHORVALUE', 'Anchor')),
263
					TextField::create('Subject', _t('HtmlEditorField.SUBJECT', 'Email subject')),
264
					TextField::create('Description', _t('HtmlEditorField.LINKDESCR', 'Link description')),
265
					CheckboxField::create('TargetBlank',
266
						_t('HtmlEditorField.LINKOPENNEWWIN', 'Open link in a new window?')),
267
					HiddenField::create('Locale', null, $this->controller->Locale)
268
				)
269
			),
270
			new FieldList(
271
				ResetFormAction::create('remove', _t('HtmlEditorField.BUTTONREMOVELINK', 'Remove link'))
272
					->addExtraClass('ss-ui-action-destructive')
273
					->setUseButtonTag(true)
274
				,
275
				FormAction::create('insert', _t('HtmlEditorField.BUTTONINSERTLINK', 'Insert link'))
276
					->addExtraClass('ss-ui-action-constructive')
277
					->setAttribute('data-icon', 'accept')
278
					->setUseButtonTag(true)
279
			)
280
		);
281
282
		$headerWrap->addExtraClass('CompositeField composite cms-content-header nolabel ');
283
		$contentComposite->addExtraClass('ss-insert-link content');
284
		$fileField->setAllowedMaxFileNumber(1);
285
286
		$form->unsetValidator();
287
		$form->loadDataFrom($this);
288
		$form->addExtraClass('htmleditorfield-form htmleditorfield-linkform cms-dialog-content');
289
290
		$this->extend('updateLinkForm', $form);
291
292
		return $form;
293
	}
294
295
	/**
296
	 * Get the folder ID to filter files by for the "from cms" tab
297
	 *
298
	 * @return int
299
	 */
300
	protected function getAttachParentID() {
301
		$parentID = $this->controller->getRequest()->requestVar('ParentID');
302
		$this->extend('updateAttachParentID', $parentID);
303
		return $parentID;
304
	}
305
306
	/**
307
	 * Return a {@link Form} instance allowing a user to
308
	 * add images and flash objects to the TinyMCE content editor.
309
	 *
310
	 * @return Form
311
	 */
312
	public function MediaForm() {
313
		// TODO Handle through GridState within field - currently this state set too late to be useful here (during
314
		// request handling)
315
		$parentID = $this->getAttachParentID();
316
317
		$fileFieldConfig = GridFieldConfig::create()->addComponents(
318
			new GridFieldFilterHeader(),
319
			new GridFieldSortableHeader(),
320
			new GridFieldDataColumns(),
321
			new GridFieldPaginator(7),
322
			// TODO Shouldn't allow delete here, its too confusing with a "remove from editor view" action.
323
			// Remove once we can fit the search button in the last actual title column
324
			new GridFieldDeleteAction(),
325
			new GridFieldDetailForm()
326
		);
327
		$fileField = new GridField('Files', false, null, $fileFieldConfig);
328
		$fileField->setList($this->getFiles($parentID));
329
		$fileField->setAttribute('data-selectable', true);
330
		$fileField->setAttribute('data-multiselect', true);
331
		$columns = $fileField->getConfig()->getComponentByType('GridFieldDataColumns');
332
		$columns->setDisplayFields(array(
333
			'StripThumbnail' => false,
334
			'Title' => _t('File.Title'),
335
			'Created' => singleton('File')->fieldLabel('Created'),
336
		));
337
		$columns->setFieldCasting(array(
338
			'Created' => 'SS_Datetime->Nice'
339
		));
340
341
		$numericLabelTmpl = '<span class="step-label"><span class="flyout">%d</span><span class="arrow"></span>'
342
			. '<strong class="title">%s</strong></span>';
343
344
		$fromCMS = new CompositeField(
345
			new LiteralField('headerSelect',
346
				'<h4>'.sprintf($numericLabelTmpl, '1', _t('HtmlEditorField.FindInFolder', 'Find in Folder')).'</h4>'),
347
			$select = TreeDropdownField::create('ParentID', "", 'Folder')
348
				->addExtraClass('noborder')
349
				->setValue($parentID),
350
			$fileField
351
		);
352
353
		$fromCMS->addExtraClass('content ss-uploadfield');
354
		$select->addExtraClass('content-select');
355
356
357
		$fromWeb = new CompositeField(
358
			new LiteralField('headerURL',
359
				'<h4>' . sprintf($numericLabelTmpl, '1', _t('HtmlEditorField.ADDURL', 'Add URL')) . '</h4>'),
360
			$remoteURL = new TextField('RemoteURL', ''),
361
			new LiteralField('addURLImage',
362
				'<button type="button" class="action ui-action-constructive ui-button field add-url" data-icon="addMedia">' .
363
				_t('HtmlEditorField.BUTTONADDURL', 'Add url').'</button>')
364
		);
365
366
		$remoteURL->addExtraClass('remoteurl');
367
		$fromWeb->addExtraClass('content ss-uploadfield');
368
369
		Requirements::css(FRAMEWORK_DIR . '/css/AssetUploadField.css');
370
		$computerUploadField = SS_Object::create('UploadField', 'AssetUploadField', '');
371
		$computerUploadField->setConfig('previewMaxWidth', 40);
0 ignored issues
show
Bug introduced by
The method setConfig() does not exist on HtmlEditorField_Toolbar. Did you maybe mean config()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
372
		$computerUploadField->setConfig('previewMaxHeight', 30);
0 ignored issues
show
Bug introduced by
The method setConfig() does not exist on HtmlEditorField_Toolbar. Did you maybe mean config()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
373
		$computerUploadField->addExtraClass('ss-assetuploadfield');
374
		$computerUploadField->removeExtraClass('ss-uploadfield');
375
		$computerUploadField->setTemplate('HtmlEditorField_UploadField');
376
		$computerUploadField->setFolderName(Config::inst()->get('Upload', 'uploads_folder'));
377
378
		$tabSet = new TabSet(
379
			"MediaFormInsertMediaTabs",
380
			Tab::create(
381
				'FromComputer',
382
				_t('HtmlEditorField.FROMCOMPUTER','From your computer'),
383
				$computerUploadField
384
			)->addExtraClass('htmleditorfield-from-computer'),
385
			Tab::create(
386
				'FromWeb',
387
				_t('HtmlEditorField.FROMWEB', 'From the web'),
388
				$fromWeb
389
			)->addExtraClass('htmleditorfield-from-web'),
390
			Tab::create(
391
				'FromCms',
392
				_t('HtmlEditorField.FROMCMS','From the CMS'),
393
				$fromCMS
394
			)->addExtraClass('htmleditorfield-from-cms')
395
		);
396
		$tabSet->addExtraClass('cms-tabset-primary');
397
398
		$allFields = new CompositeField(
399
			$tabSet,
400
			new LiteralField('headerEdit', '<h4 class="field noborder header-edit">' . sprintf($numericLabelTmpl, '2',
401
				_t('HtmlEditorField.ADJUSTDETAILSDIMENSIONS', 'Details &amp; dimensions')) . '</h4>'),
402
			$editComposite = new CompositeField(
403
				new LiteralField('contentEdit', '<div class="content-edit ss-uploadfield-files files"></div>')
404
			)
405
		);
406
407
		$allFields->addExtraClass('ss-insert-media');
408
409
		$headings = new CompositeField(
410
			new LiteralField(
411
				'Heading',
412
				sprintf('<h3 class="htmleditorfield-mediaform-heading insert">%s</h3>',
413
					_t('HtmlEditorField.INSERTMEDIA', 'Insert Media')).
414
				sprintf('<h3 class="htmleditorfield-mediaform-heading update">%s</h3>',
415
					_t('HtmlEditorField.UpdateMEDIA', 'Update Media'))
416
			)
417
		);
418
419
		$headings->addExtraClass('cms-content-header');
420
		$editComposite->addExtraClass('ss-assetuploadfield');
421
422
		$fields = new FieldList(
423
			$headings,
424
			$allFields
425
		);
426
427
		$actions = new FieldList(
428
			FormAction::create('insertmedia', _t('HtmlEditorField.BUTTONINSERT', 'Insert'))
429
				->addExtraClass('ss-ui-action-constructive media-insert')
430
				->setAttribute('data-icon', 'accept')
431
				->setUseButtonTag(true),
432
			FormAction::create('insertmedia', _t('HtmlEditorField.BUTTONUpdate', 'Update'))
433
				->addExtraClass('ss-ui-action-constructive media-update')
434
				->setAttribute('data-icon', 'accept')
435
				->setUseButtonTag(true)
436
		);
437
438
		$form = new Form(
439
			$this->controller,
440
			"{$this->name}/MediaForm",
441
			$fields,
442
			$actions
443
		);
444
445
446
		$form->unsetValidator();
447
		$form->disableSecurityToken();
448
		$form->loadDataFrom($this);
449
		$form->addExtraClass('htmleditorfield-form htmleditorfield-mediaform cms-dialog-content');
450
		// TODO Re-enable once we remove $.metadata dependency which currently breaks the JS due to $.ui.widget
451
		// $form->setAttribute('data-urlViewfile', $this->controller->Link($this->name));
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
452
453
		// Allow other people to extend the fields being added to the imageform
454
		$this->extend('updateMediaForm', $form);
455
456
		return $form;
457
	}
458
459
	/**
460
	 * @config
461
	 * @var array - list of allowed schemes (no wildcard, all lower case) or empty to allow all schemes
462
	 */
463
	private static $fileurl_scheme_whitelist = array('http', 'https');
464
465
	/**
466
	 * @config
467
	 * @var array - list of allowed domains (no wildcard, all lower case) or empty to allow all domains
468
	 */
469
	private static $fileurl_domain_whitelist = array();
470
471
	protected function viewfile_getLocalFileByID($id) {
472
		$file = DataObject::get_by_id('File', $id);
473
474
		if ($file && $file->canView()) return array($file, $file->RelativeLink());
475
		return array(null, null);
476
	}
477
478
	protected function viewfile_getLocalFileByURL($fileUrl) {
479
		$filteredUrl = Director::makeRelative($fileUrl);
480
481
		// Remove prefix and querystring
482
		$filteredUrl = Image::strip_resampled_prefix($filteredUrl);
483
		list($filteredUrl) = explode('?', $filteredUrl);
484
485
		$file = File::get()->filter('Filename', $filteredUrl)->first();
486
487
		if ($file && $file->canView()) return array($file, $filteredUrl);
488
		return array(null, null);
489
	}
490
491
	protected function viewfile_getRemoteFileByURL($fileUrl) {
492
		$scheme = strtolower(parse_url($fileUrl, PHP_URL_SCHEME));
493
		$allowed_schemes = self::config()->fileurl_scheme_whitelist;
0 ignored issues
show
Documentation introduced by
The property fileurl_scheme_whitelist does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
494
495
		if (!$scheme || ($allowed_schemes && !in_array($scheme, $allowed_schemes))) {
496
			$exception = new SS_HTTPResponse_Exception("This file scheme is not included in the whitelist", 400);
497
			$exception->getResponse()->addHeader('X-Status', $exception->getMessage());
498
			throw $exception;
499
		}
500
501
		$domain = strtolower(parse_url($fileUrl, PHP_URL_HOST));
502
		$allowed_domains = self::config()->fileurl_domain_whitelist;
0 ignored issues
show
Documentation introduced by
The property fileurl_domain_whitelist does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
503
504
		if (!$domain || ($allowed_domains && !in_array($domain, $allowed_domains))) {
505
			$exception = new SS_HTTPResponse_Exception("This file hostname is not included in the whitelist", 400);
506
			$exception->getResponse()->addHeader('X-Status', $exception->getMessage());
507
			throw $exception;
508
		}
509
510
		return array(
511
			new File(array(
512
				'Title' => basename($fileUrl),
513
				'Filename' => $fileUrl
514
			)),
515
			$fileUrl
516
		);
517
	}
518
519
	/**
520
	 * View of a single file, either on the filesystem or on the web.
521
	 */
522
	public function viewfile($request) {
523
		$file = null;
524
		$url = null;
525
526
527
		// TODO Would be cleaner to consistently pass URL for both local and remote files,
528
		// but GridField doesn't allow for this kind of metadata customization at the moment.
529
		if($fileUrl = $request->getVar('FileURL')) {
530
			// If this isn't an absolute URL, or is, but is to this site, try and get the File object
531
			// that is associated with it
532
			if(!Director::is_absolute_url($fileUrl) || Director::is_site_url($fileUrl)) {
533
				list($file, $url) = $this->viewfile_getLocalFileByURL($fileUrl);
534
			}
535
			// If this is an absolute URL, but not to this site, use as an oembed source (after whitelisting URL)
536
			else {
537
				list($file, $url) = $this->viewfile_getRemoteFileByURL($fileUrl);
538
			}
539
		}
540
		// Or we could have been passed an ID directly
541
		elseif($id = $request->getVar('ID')) {
542
			list($file, $url) = $this->viewfile_getLocalFileByID($id);
543
		}
544
		// Or we could have been passed nothing, in which case panic
545
		else {
546
			throw new SS_HTTPResponse_Exception('Need either "ID" or "FileURL" parameter to identify the file', 400);
547
		}
548
549
		// Instanciate file wrapper and get fields based on its type
550
		// Check if appCategory is an image and exists on the local system, otherwise use oEmbed to refference a
551
		// remote image
552
		if (!$file || !$url) {
553
			throw new SS_HTTPResponse_Exception('Unable to find file to view', 404);
554
		} elseif($file->appCategory() == 'image' && Director::is_site_url($url)) {
555
			$fileWrapper = new HtmlEditorField_Image($url, $file);
556
		} elseif(!Director::is_site_url($url)) {
557
			$fileWrapper = new HtmlEditorField_Embed($url, $file);
558
		} else {
559
			$fileWrapper = new HtmlEditorField_File($url, $file);
560
		}
561
562
		$fields = $this->getFieldsForFile($url, $fileWrapper);
563
		$this->extend('updateFieldsForFile', $fields, $url, $fileWrapper);
564
565
		return $fileWrapper->customise(array(
566
			'Fields' => $fields,
567
		))->renderWith($this->templateViewFile);
568
	}
569
570
	/**
571
	 * Find all anchors available on the given page.
572
	 *
573
	 * @return array
574
	 */
575
	public function getanchors() {
576
		$id = (int)$this->getRequest()->getVar('PageID');
577
		$anchors = array();
578
579
		if (($page = SiteTree::get()->byID($id)) && !empty($page)) {
580
			if (!$page->canView()) {
581
				throw new SS_HTTPResponse_Exception(
582
					_t(
583
						'HtmlEditorField.ANCHORSCANNOTACCESSPAGE',
584
						'You are not permitted to access the content of the target page.'
585
					),
586
					403
587
				);
588
			}
589
590
			// Similar to the regex found in HtmlEditorField.js / getAnchors method.
591
			if (preg_match_all(
592
				"/\\s+(name|id)\\s*=\\s*([\"'])([^\\2\\s>]*?)\\2|\\s+(name|id)\\s*=\\s*([^\"']+)[\\s +>]/im",
593
				$page->Content,
594
				$matches
595
			)) {
596
				$anchors = array_values(array_unique(array_filter(
597
					array_merge($matches[3], $matches[5]))
598
				));
599
			}
600
601
		} else {
602
			throw new SS_HTTPResponse_Exception(
603
				_t('HtmlEditorField.ANCHORSPAGENOTFOUND', 'Target page not found.'),
604
				404
605
			);
606
		}
607
608
		return json_encode($anchors);
609
	}
610
611
	/**
612
	 * Similar to {@link File->getCMSFields()}, but only returns fields
613
	 * for manipulating the instance of the file as inserted into the HTML content,
614
	 * not the "master record" in the database - hence there's no form or saving logic.
615
	 *
616
	 * @param String Relative or absolute URL to file
617
	 * @return FieldList
618
	 */
619
	protected function getFieldsForFile($url, $file) {
620
		$fields = $this->extend('getFieldsForFile', $url, $file);
621
		if(!$fields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
622
			if($file instanceof HtmlEditorField_Embed) {
623
				$fields = $this->getFieldsForOembed($url, $file);
624
			} elseif($file->Extension == 'swf') {
625
				$fields = $this->getFieldsForFlash($url, $file);
626
			} else {
627
				$fields = $this->getFieldsForImage($url, $file);
628
			}
629
			$fields->push(new HiddenField('URL', false, $url));
630
		}
631
632
		$this->extend('updateFieldsForFile', $fields, $url, $file);
633
634
		return $fields;
635
	}
636
637
	/**
638
	 * @return FieldList
639
	 */
640
	protected function getFieldsForOembed($url, $file) {
641
		if(isset($file->Oembed->thumbnail_url)) {
642
			$thumbnailURL = Convert::raw2att($file->Oembed->thumbnail_url);
643
		} elseif($file->Type == 'photo') {
644
			$thumbnailURL = Convert::raw2att($file->Oembed->url);
645
		} else {
646
			$thumbnailURL = FRAMEWORK_DIR . '/images/default_media.png';
647
		}
648
649
		$fileName = Convert::raw2att($file->Name);
650
651
		$fields = new FieldList(
652
			$filePreview = CompositeField::create(
653
				CompositeField::create(
654
					new LiteralField(
655
						"ImageFull",
656
						"<img id='thumbnailImage' class='thumbnail-preview' "
657
							. "src='{$thumbnailURL}?r=" . rand(1,100000) . "' alt='$fileName' />\n"
658
					)
659
				)->setName("FilePreviewImage")->addExtraClass('cms-file-info-preview'),
660
				CompositeField::create(
661
					CompositeField::create(
662
						new ReadonlyField("FileType", _t('AssetTableField.TYPE','File type') . ':', $file->Type),
663
						$urlField = ReadonlyField::create(
664
							'ClickableURL',
665
							_t('AssetTableField.URL','URL'),
666
							sprintf(
667
								'<a href="%s" target="_blank" class="file">%s</a>',
668
								Convert::raw2att($url),
669
								Convert::raw2att($url)
670
							)
671
						)->addExtraClass('text-wrap')
672
					)
673
				)->setName("FilePreviewData")->addExtraClass('cms-file-info-data')
674
			)->setName("FilePreview")->addExtraClass('cms-file-info'),
675
			new TextField('CaptionText', _t('HtmlEditorField.CAPTIONTEXT', 'Caption text')),
676
			DropdownField::create(
677
				'CSSClass',
678
				_t('HtmlEditorField.CSSCLASS', 'Alignment / style'),
679
				array(
680
					'leftAlone' => _t('HtmlEditorField.CSSCLASSLEFTALONE', 'On the left, on its own.'),
681
					'center' => _t('HtmlEditorField.CSSCLASSCENTER', 'Centered, on its own.'),
682
					'left' => _t('HtmlEditorField.CSSCLASSLEFT', 'On the left, with text wrapping around.'),
683
					'right' => _t('HtmlEditorField.CSSCLASSRIGHT', 'On the right, with text wrapping around.')
684
				),
685
				HtmlEditorField::config()->get('media_alignment')
686
687
			)->addExtraClass('last')
688
		);
689
690
		if($file->Width != null){
691
			$fields->push(
692
				FieldGroup::create(
693
					_t('HtmlEditorField.IMAGEDIMENSIONS', 'Dimensions'),
694
					TextField::create(
695
						'Width',
696
						_t('HtmlEditorField.IMAGEWIDTHPX', 'Width'),
697
						$file->InsertWidth
698
					)->setMaxLength(5),
699
					TextField::create(
700
						'Height',
701
						_t('HtmlEditorField.IMAGEHEIGHTPX', 'Height'),
702
						$file->InsertHeight
703
					)->setMaxLength(5)
704
				)->addExtraClass('dimensions last')
705
			);
706
		}
707
		$urlField->dontEscape = true;
708
709
		if($file->Type == 'photo') {
710
			$fields->insertBefore('CaptionText', new TextField(
711
				'AltText',
712
				_t('HtmlEditorField.IMAGEALTTEXT', 'Alternative text (alt) - shown if image can\'t be displayed'),
713
				$file->Title,
714
				80
715
			));
716
			$fields->insertBefore('CaptionText', new TextField(
717
				'Title',
718
				_t('HtmlEditorField.IMAGETITLE', 'Title text (tooltip) - for additional information about the image')
719
			));
720
		}
721
722
		$this->extend('updateFieldsForOembed', $fields, $url, $file);
723
724
		return $fields;
725
	}
726
727
	/**
728
	 * @return FieldList
729
	 */
730
	protected function getFieldsForFlash($url, $file) {
731
		$fields = new FieldList(
732
			FieldGroup::create(
733
				_t('HtmlEditorField.IMAGEDIMENSIONS', 'Dimensions'),
734
				TextField::create(
735
					'Width',
736
					_t('HtmlEditorField.IMAGEWIDTHPX', 'Width'),
737
					$file->Width
738
				)->setMaxLength(5),
739
				TextField::create(
740
					'Height',
741
					" x " . _t('HtmlEditorField.IMAGEHEIGHTPX', 'Height'),
742
					$file->Height
743
				)->setMaxLength(5)
744
			)->addExtraClass('dimensions')
745
		);
746
747
		$this->extend('updateFieldsForFlash', $fields, $url, $file);
748
749
		return $fields;
750
	}
751
752
	/**
753
	 * @return FieldList
754
	 */
755
	protected function getFieldsForImage($url, $file) {
756
		if($file->File instanceof Image) {
757
			$formattedImage = $file->File->generateFormattedImage('ScaleWidth',
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $formattedImage is correct as $file->File->generateFor...'asset_preview_width')) (which targets Image::generateFormattedImage()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
758
				Config::inst()->get('Image', 'asset_preview_width'));
759
			$thumbnailURL = Convert::raw2att($formattedImage ? $formattedImage->URL : $url);
760
		} else {
761
			$thumbnailURL = Convert::raw2att($url);
762
		}
763
764
		$fileName = Convert::raw2att($file->Name);
765
766
		$fields = new FieldList(
767
			CompositeField::create(
768
				CompositeField::create(
769
					LiteralField::create(
770
						"ImageFull",
771
						"<img id='thumbnailImage' class='thumbnail-preview' "
772
							. "src='{$thumbnailURL}?r=" . rand(1,100000) . "' alt='$fileName' />\n"
773
					)
774
				)->setName("FilePreviewImage")->addExtraClass('cms-file-info-preview'),
775
				CompositeField::create(
776
					CompositeField::create(
777
						new ReadonlyField("FileType", _t('AssetTableField.TYPE','File type'), $file->FileType),
778
						new ReadonlyField("Size", _t('AssetTableField.SIZE','File size'), $file->getSize()),
779
						$urlField = new ReadonlyField(
780
							'ClickableURL',
781
							_t('AssetTableField.URL','URL'),
782
							sprintf(
783
								'<a href="%s" title="%s" target="_blank" class="file-url">%s</a>',
784
								Convert::raw2att($file->Link()),
785
								Convert::raw2att($file->Link()),
786
								Convert::raw2att($file->RelativeLink())
787
							)
788
						),
789
						new DateField_Disabled("Created", _t('AssetTableField.CREATED','First uploaded'),
790
							$file->Created),
791
						new DateField_Disabled("LastEdited", _t('AssetTableField.LASTEDIT','Last changed'),
792
							$file->LastEdited)
793
					)
794
				)->setName("FilePreviewData")->addExtraClass('cms-file-info-data')
795
			)->setName("FilePreview")->addExtraClass('cms-file-info'),
796
797
			TextField::create(
798
				'AltText',
799
				_t('HtmlEditorField.IMAGEALT', 'Alternative text (alt)'),
800
				$file->Title,
801
				80
802
			)->setDescription(
803
				_t('HtmlEditorField.IMAGEALTTEXTDESC', 'Shown to screen readers or if image can\'t be displayed')),
804
805
			TextField::create(
806
				'Title',
807
				_t('HtmlEditorField.IMAGETITLETEXT', 'Title text (tooltip)')
808
			)->setDescription(
809
				_t('HtmlEditorField.IMAGETITLETEXTDESC', 'For additional information about the image')),
810
811
			new TextField('CaptionText', _t('HtmlEditorField.CAPTIONTEXT', 'Caption text')),
812
			DropdownField::create(
813
				'CSSClass',
814
				_t('HtmlEditorField.CSSCLASS', 'Alignment / style'),
815
				array(
816
					'leftAlone' => _t('HtmlEditorField.CSSCLASSLEFTALONE', 'On the left, on its own.'),
817
					'center' => _t('HtmlEditorField.CSSCLASSCENTER', 'Centered, on its own.'),
818
					'left' => _t('HtmlEditorField.CSSCLASSLEFT', 'On the left, with text wrapping around.'),
819
					'right' => _t('HtmlEditorField.CSSCLASSRIGHT', 'On the right, with text wrapping around.')
820
				),
821
				HtmlEditorField::config()->get('media_alignment')
822
			)->addExtraClass('last')
823
		);
824
825
		if($file->Width != null){
826
			$fields->push(
827
				FieldGroup::create(_t('HtmlEditorField.IMAGEDIMENSIONS', 'Dimensions'),
828
					TextField::create(
829
						'Width',
830
						_t('HtmlEditorField.IMAGEWIDTHPX', 'Width'),
831
						$file->InsertWidth
832
					)->setMaxLength(5),
833
					TextField::create(
834
						'Height',
835
						" x " . _t('HtmlEditorField.IMAGEHEIGHTPX', 'Height'),
836
						$file->InsertHeight
837
					)->setMaxLength(5)
838
				)->addExtraClass('dimensions last')
839
			);
840
		}
841
		$urlField->dontEscape = true;
842
843
		$this->extend('updateFieldsForImage', $fields, $url, $file);
844
845
		return $fields;
846
	}
847
848
	/**
849
	 * @param Int
850
	 * @return DataList
851
	 */
852
	protected function getFiles($parentID = null) {
853
		$exts = $this->getAllowedExtensions();
854
		$dotExts = array_map(function($ext) { return ".{$ext}"; }, $exts);
855
		$files = File::get()->filter('Filename:EndsWith', $dotExts);
856
857
		// Limit by folder (if required)
858
		if($parentID) {
859
			$files = $files->filter('ParentID', $parentID);
860
		}
861
862
		return $files;
863
	}
864
865
	/**
866
	 * @return Array All extensions which can be handled by the different views.
867
	 */
868
	protected function getAllowedExtensions() {
869
		$exts = array('jpg', 'gif', 'png', 'swf','jpeg');
870
		$this->extend('updateAllowedExtensions', $exts);
871
		return $exts;
872
	}
873
874
}
875
876
/**
877
 * Encapsulation of a file which can either be a remote URL
878
 * or a {@link File} on the local filesystem, exhibiting common properties
879
 * such as file name or the URL.
880
 *
881
 * @todo Remove once core has support for remote files
882
 * @package forms
883
 * @subpackage fields-formattedinput
884
 */
885
class HtmlEditorField_File extends ViewableData {
886
887
	private static $casting = array(
888
		'URL' => 'Varchar',
889
		'Name' => 'Varchar'
890
	);
891
892
	/** @var String */
893
	protected $url;
894
895
	/** @var File */
896
	protected $file;
897
898
	/**
899
	 * @param String
900
	 * @param File
901
	 */
902
	public function __construct($url, $file = null) {
903
		$this->url = $url;
904
		$this->file = $file;
905
		$this->failover = $file;
906
907
		parent::__construct();
908
	}
909
910
	/**
911
	 * @return File Might not be set (for remote files)
912
	 */
913
	public function getFile() {
914
		return $this->file;
915
	}
916
917
	public function getURL() {
918
		return $this->url;
919
	}
920
921
	public function getName() {
922
		return ($this->file) ? $this->file->Name : preg_replace('/\?.*/', '', basename($this->url));
923
	}
924
925
	/**
926
	 * @return String HTML
927
	 */
928
	public function getPreview() {
929
		$preview = $this->extend('getPreview');
930
		if($preview) return $preview;
0 ignored issues
show
Bug Best Practice introduced by
The expression $preview of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
931
932
		if($this->file) {
933
			return $this->file->CMSThumbnail();
934
		} else {
935
			// Hack to use the framework's built-in thumbnail support without creating a local file representation
936
			$tmpFile = new File(array('Name' => $this->Name, 'Filename' => $this->Name));
937
			return $tmpFile->CMSThumbnail();
938
		}
939
	}
940
941
	public function getExtension() {
942
		return strtolower(($this->file) ? $this->file->Extension : pathinfo($this->Name, PATHINFO_EXTENSION));
0 ignored issues
show
Bug introduced by
The property Extension does not seem to exist. Did you mean extensions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
943
	}
944
945
	public function appCategory() {
946
		if($this->file) {
947
			return $this->file->appCategory();
948
		} else {
949
			// Hack to use the framework's built-in thumbnail support without creating a local file representation
950
			$tmpFile = new File(array('Name' => $this->Name, 'Filename' => $this->Name));
951
			return $tmpFile->appCategory();
952
		}
953
	}
954
955
}
956
957
/**
958
 * Encapsulation of an oembed tag, linking to an external media source.
959
 *
960
 * @see Oembed
961
 * @package forms
962
 * @subpackage fields-formattedinput
963
 */
964
class HtmlEditorField_Embed extends HtmlEditorField_File {
965
966
	private static $casting = array(
967
		'Type' => 'Varchar',
968
		'Info' => 'Varchar'
969
	);
970
971
	protected $oembed;
972
973
	public function __construct($url, $file = null) {
974
		parent::__construct($url, $file);
975
		$this->oembed = Oembed::get_oembed_from_url($url);
976
		if(!$this->oembed) {
977
			$controller = Controller::curr();
978
			$response = $controller->getResponse();
979
			$response->addHeader('X-Status',
980
				rawurlencode(_t(
981
					'HtmlEditorField.URLNOTANOEMBEDRESOURCE',
982
					"The URL '{url}' could not be turned into a media resource.",
983
					"The given URL is not a valid Oembed resource; the embed element couldn't be created.",
984
					array('url' => $url)
985
				)));
986
			$response->setStatusCode(404);
987
988
			throw new SS_HTTPResponse_Exception($response);
989
		}
990
	}
991
992
	public function getWidth() {
993
		return $this->oembed->Width ?: 100;
994
	}
995
996
	public function getHeight() {
997
		return $this->oembed->Height ?: 100;
998
	}
999
1000
	/**
1001
	 * Provide an initial width for inserted media, restricted based on $embed_width
1002
	 *
1003
	 * @return int
1004
	 */
1005
	public function getInsertWidth() {
1006
		$width = $this->getWidth();
1007
		$maxWidth = Config::inst()->get('HtmlEditorField', 'insert_width');
1008
		return ($width <= $maxWidth) ? $width : $maxWidth;
1009
	}
1010
1011
	/**
1012
	 * Provide an initial height for inserted media, scaled proportionally to the initial width
1013
	 *
1014
	 * @return int
1015
	 */
1016
	public function getInsertHeight() {
1017
		$width = $this->getWidth();
1018
		$height = $this->getHeight();
1019
		$maxWidth = Config::inst()->get('HtmlEditorField', 'insert_width');
1020
		return ($width <= $maxWidth) ? $height : round($height*($maxWidth/$width));
1021
	}
1022
1023
	public function getPreview() {
1024
		if(isset($this->oembed->thumbnail_url)) {
1025
			return sprintf('<img src="%s" />', Convert::raw2att($this->oembed->thumbnail_url));
1026
		}
1027
	}
1028
1029
	public function getName() {
1030
		if(isset($this->oembed->title)) {
1031
			return $this->oembed->title;
1032
		} else {
1033
			return parent::getName();
1034
		}
1035
	}
1036
1037
	public function getType() {
1038
		return $this->oembed->type;
1039
	}
1040
1041
	public function getOembed() {
1042
		return $this->oembed;
1043
	}
1044
1045
	public function appCategory() {
1046
		return 'embed';
1047
	}
1048
1049
	public function getInfo() {
1050
		return $this->oembed->info;
1051
	}
1052
}
1053
1054
/**
1055
 * Encapsulation of an image tag, linking to an image either internal or external to the site.
1056
 *
1057
 * @package forms
1058
 * @subpackage fields-formattedinput
1059
 */
1060
class HtmlEditorField_Image extends HtmlEditorField_File {
1061
1062
	protected $width;
1063
1064
	protected $height;
1065
1066
	public function __construct($url, $file = null) {
1067
		parent::__construct($url, $file);
1068
1069
		// Get dimensions for remote file
1070
		$info = @getimagesize($url);
1071
		if($info) {
1072
			$this->width = $info[0];
1073
			$this->height = $info[1];
1074
		}
1075
	}
1076
1077
	public function getWidth() {
1078
		return ($this->file) ? $this->file->Width : $this->width;
1079
	}
1080
1081
	public function getHeight() {
1082
		return ($this->file) ? $this->file->Height : $this->height;
1083
	}
1084
1085
	/**
1086
	 * Provide an initial width for inserted image, restricted based on $embed_width
1087
	 *
1088
	 * @return int
1089
	 */
1090
	public function getInsertWidth() {
1091
		$width = $this->getWidth();
1092
		$maxWidth = Config::inst()->get('HtmlEditorField', 'insert_width');
1093
		return ($width <= $maxWidth) ? $width : $maxWidth;
1094
	}
1095
1096
	/**
1097
	 * Provide an initial height for inserted image, scaled proportionally to the initial width
1098
	 *
1099
	 * @return int
1100
	 */
1101
	public function getInsertHeight() {
1102
		$width = $this->getWidth();
1103
		$height = $this->getHeight();
1104
		$maxWidth = Config::inst()->get('HtmlEditorField', 'insert_width');
1105
		return ($width <= $maxWidth) ? $height : round($height*($maxWidth/$width));
1106
	}
1107
1108
	public function getPreview() {
1109
		return ($this->file) ? $this->file->CMSThumbnail() : sprintf('<img src="%s" />', Convert::raw2att($this->url));
1110
	}
1111
1112
}
1113