Completed
Push — namespace-model ( 5d15d8...2589e8 )
by Sam
08:38
created

HtmlEditorField_Toolbar::getAttachParentID()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 5
rs 9.4285
cc 1
eloc 4
nc 1
nop 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
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
17
class HtmlEditorField extends TextareaField {
18
19
	/**
20
	 * Use TinyMCE's GZIP compressor
21
	 *
22
	 * @config
23
	 * @var bool
24
	 */
25
	private static $use_gzip = true;
26
27
	/**
28
	 * Should we check the valid_elements (& extended_valid_elements) rules from HtmlEditorConfig server side?
29
	 *
30
	 * @config
31
	 * @var bool
32
	 */
33
	private static $sanitise_server_side = false;
34
35
	/**
36
	 * Number of rows
37
	 *
38
	 * @config
39
	 * @var int
40
	 */
41
	private static $default_rows = 30;
42
43
	/**
44
	 * ID or instance of editorconfig
45
	 *
46
	 * @var string|HtmlEditorConfig
47
	 */
48
	protected $editorConfig = null;
49
50
	/**
51
	 * Gets the HtmlEditorConfig instance
52
	 *
53
	 * @return HtmlEditorConfig
54
	 */
55
	public function getEditorConfig() {
56
		// Instance override
57
		if($this->editorConfig instanceof HtmlEditorConfig) {
58
			return $this->editorConfig;
59
		}
60
61
		// Get named / active config
62
		return HtmlEditorConfig::get($this->editorConfig);
63
	}
64
65
	/**
66
	 * Assign a new configuration instance or identifier
67
	 *
68
	 * @param string|HtmlEditorConfig $config
69
	 * @return $this
70
	 */
71
	public function setEditorConfig($config) {
72
		$this->editorConfig = $config;
73
		return $this;
74
	}
75
76
	/**
77
	 * Creates a new HTMLEditorField.
78
	 * @see TextareaField::__construct()
79
	 *
80
	 * @param string $name The internal field name, passed to forms.
81
	 * @param string $title The human-readable field label.
82
	 * @param mixed $value The value of the field.
83
	 * @param string $config HtmlEditorConfig identifier to be used. Default to the active one.
84
	 */
85
	public function __construct($name, $title = null, $value = '', $config = null) {
86
		parent::__construct($name, $title, $value);
87
88
		if($config) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $config of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
89
			$this->setEditorConfig($config);
90
		}
91
92
		$this->setRows($this->config()->default_rows);
93
	}
94
95
	public function getAttributes() {
96
		return array_merge(
97
			parent::getAttributes(),
98
			$this->getEditorConfig()->getAttributes()
99
		);
100
	}
101
102
	public function saveInto(DataObjectInterface $record) {
103
		if($record->hasField($this->name) && $record->escapeTypeForField($this->name) != 'xml') {
104
			throw new Exception (
105
				'HtmlEditorField->saveInto(): This field should save into a HTMLText or HTMLVarchar field.'
106
			);
107
		}
108
109
		// Sanitise if requested
110
		$htmlValue = Injector::inst()->create('HTMLValue', $this->Value());
111
		if($this->config()->sanitise_server_side) {
112
			$santiser = Injector::inst()->create('HtmlEditorSanitiser', HtmlEditorConfig::get_active());
113
			$santiser->sanitise($htmlValue);
114
		}
115
116
		// optionally manipulate the HTML after a TinyMCE edit and prior to a save
117
		$this->extend('processHTML', $htmlValue);
118
119
		// Store into record
120
		$record->{$this->name} = $htmlValue->getContent();
121
	}
122
123
	public function setValue($value) {
124
		// Regenerate links prior to preview, so that the editor can see them.
125
		$value = Image::regenerate_html_links($value);
126
		return parent::setValue($value);
127
	}
128
129
	/**
130
	 * @return HtmlEditorField_Readonly
131
	 */
132
	public function performReadonlyTransformation() {
133
		$field = $this->castedCopy('HtmlEditorField_Readonly');
134
		$field->dontEscape = true;
135
136
		return $field;
137
	}
138
139
	public function performDisabledTransformation() {
140
		return $this->performReadonlyTransformation();
141
	}
142
143
	public function Field($properties = array()) {
144
		// Include requirements
145
		$this->getEditorConfig()->init();
146
		return parent::Field($properties);
147
	}
148
}
149
150
/**
151
 * Readonly version of an {@link HTMLEditorField}.
152
 * @package forms
153
 * @subpackage fields-formattedinput
154
 */
155
class HtmlEditorField_Readonly extends ReadonlyField {
156
	public function Field($properties = array()) {
157
		$valforInput = $this->value ? Convert::raw2att($this->value) : "";
158
		return "<span class=\"readonly typography\" id=\"" . $this->id() . "\">"
159
			. ( $this->value && $this->value != '<p></p>' ? $this->value : '<i>(not set)</i>' )
160
			. "</span><input type=\"hidden\" name=\"".$this->name."\" value=\"".$valforInput."\" />";
161
	}
162
	public function Type() {
163
		return 'htmleditorfield readonly';
164
	}
165
}
166
167
/**
168
 * Toolbar shared by all instances of {@link HTMLEditorField}, to avoid too much markup duplication.
169
 *  Needs to be inserted manually into the template in order to function - see {@link LeftAndMain->EditorToolbar()}.
170
 *
171
 * @package forms
172
 * @subpackage fields-formattedinput
173
 */
174
class HtmlEditorField_Toolbar extends RequestHandler {
175
176
	private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
177
		'LinkForm',
178
		'MediaForm',
179
		'viewfile',
180
		'getanchors'
181
	);
182
183
	/**
184
	 * @var string
185
	 */
186
	protected $templateViewFile = 'HtmlEditorField_viewfile';
187
188
	protected $controller, $name;
189
190
	public function __construct($controller, $name) {
191
		parent::__construct();
192
193
		$this->controller = $controller;
194
		$this->name = $name;
195
	}
196
197
	public function forTemplate() {
198
		return sprintf(
199
			'<div id="cms-editor-dialogs" data-url-linkform="%s" data-url-mediaform="%s"></div>',
200
			Controller::join_links($this->controller->Link(), $this->name, 'LinkForm', 'forTemplate'),
201
			Controller::join_links($this->controller->Link(), $this->name, 'MediaForm', 'forTemplate')
202
		);
203
	}
204
205
	/**
206
	 * Searches the SiteTree for display in the dropdown
207
	 *
208
	 * @return callback
209
	 */
210
	public function siteTreeSearchCallback($sourceObject, $labelField, $search) {
211
		return DataObject::get($sourceObject)->filterAny(array(
212
			'MenuTitle:PartialMatch' => $search,
213
			'Title:PartialMatch' => $search
214
		));
215
	}
216
217
	/**
218
	 * Return a {@link Form} instance allowing a user to
219
	 * add links in the TinyMCE content editor.
220
	 *
221
	 * @return Form
222
	 */
223
	public function LinkForm() {
224
		$siteTree = TreeDropdownField::create('internal', _t('HtmlEditorField.PAGE', "Page"),
225
			'SiteTree', 'ID', 'MenuTitle', true);
226
		// mimic the SiteTree::getMenuTitle(), which is bypassed when the search is performed
227
		$siteTree->setSearchFunction(array($this, 'siteTreeSearchCallback'));
228
229
		$numericLabelTmpl = '<span class="step-label"><span class="flyout">%d</span><span class="arrow"></span>'
230
			. '<strong class="title">%s</strong></span>';
231
		$form = new Form(
232
			$this->controller,
233
			"{$this->name}/LinkForm",
234
			new FieldList(
235
				$headerWrap = new CompositeField(
236
					new LiteralField(
237
						'Heading',
238
						sprintf('<h3 class="htmleditorfield-mediaform-heading insert">%s</h3>',
239
							_t('HtmlEditorField.LINK', 'Insert Link'))
240
					)
241
				),
242
				$contentComposite = new CompositeField(
243
					OptionsetField::create(
244
						'LinkType',
245
						sprintf($numericLabelTmpl, '1', _t('HtmlEditorField.LINKTO', 'Link to')),
246
						array(
247
							'internal' => _t('HtmlEditorField.LINKINTERNAL', 'Page on the site'),
248
							'external' => _t('HtmlEditorField.LINKEXTERNAL', 'Another website'),
249
							'anchor' => _t('HtmlEditorField.LINKANCHOR', 'Anchor on this page'),
250
							'email' => _t('HtmlEditorField.LINKEMAIL', 'Email address'),
251
							'file' => _t('HtmlEditorField.LINKFILE', 'Download a file'),
252
						),
253
						'internal'
254
					),
255
					LiteralField::create('Step2',
256
						'<div class="step2">'
257
						. sprintf($numericLabelTmpl, '2', _t('HtmlEditorField.DETAILS', 'Details')) . '</div>'
258
					),
259
					$siteTree,
260
					TextField::create('external', _t('HtmlEditorField.URL', 'URL'), 'http://'),
261
					EmailField::create('email', _t('HtmlEditorField.EMAIL', 'Email address')),
262
					$fileField = UploadField::create('file', _t('HtmlEditorField.FILE', 'File')),
263
					TextField::create('Anchor', _t('HtmlEditorField.ANCHORVALUE', 'Anchor')),
264
					TextField::create('Subject', _t('HtmlEditorField.SUBJECT', 'Email subject')),
265
					TextField::create('Description', _t('HtmlEditorField.LINKDESCR', 'Link description')),
266
					CheckboxField::create('TargetBlank',
267
						_t('HtmlEditorField.LINKOPENNEWWIN', 'Open link in a new window?')),
268
					HiddenField::create('Locale', null, $this->controller->Locale)
269
				)
270
			),
271
			new FieldList()
272
		);
273
274
		$headerWrap->addExtraClass('CompositeField composite cms-content-header nolabel ');
275
		$contentComposite->addExtraClass('ss-insert-link content');
276
		$fileField->setAllowedMaxFileNumber(1);
277
278
		$form->unsetValidator();
279
		$form->loadDataFrom($this);
0 ignored issues
show
Documentation introduced by
$this is of type this<HtmlEditorField_Toolbar>, but the function expects a array|object<DataObject>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
280
		$form->addExtraClass('htmleditorfield-form htmleditorfield-linkform cms-mediaform-content');
281
282
		$this->extend('updateLinkForm', $form);
283
284
		return $form;
285
	}
286
287
	/**
288
	 * Get the folder ID to filter files by for the "from cms" tab
289
	 *
290
	 * @return int
291
	 */
292
	protected function getAttachParentID() {
293
		$parentID = $this->controller->getRequest()->requestVar('ParentID');
294
		$this->extend('updateAttachParentID', $parentID);
295
		return $parentID;
296
	}
297
298
	/**
299
	 * Return a {@link Form} instance allowing a user to
300
	 * add images and flash objects to the TinyMCE content editor.
301
	 *
302
	 * @return Form
303
	 */
304
	public function MediaForm() {
305
		// TODO Handle through GridState within field - currently this state set too late to be useful here (during
306
		// request handling)
307
		$parentID = $this->getAttachParentID();
308
309
		$fileFieldConfig = GridFieldConfig::create()->addComponents(
310
			new GridFieldFilterHeader(),
311
			new GridFieldSortableHeader(),
312
			new GridFieldDataColumns(),
313
			new GridFieldPaginator(7),
314
			// TODO Shouldn't allow delete here, its too confusing with a "remove from editor view" action.
315
			// Remove once we can fit the search button in the last actual title column
316
			new GridFieldDeleteAction(),
317
			new GridFieldDetailForm()
318
		);
319
		$fileField = GridField::create('Files', false, null, $fileFieldConfig);
320
		$fileField->setList($this->getFiles($parentID));
321
		$fileField->setAttribute('data-selectable', true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
322
		$fileField->setAttribute('data-multiselect', true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
323
		$columns = $fileField->getConfig()->getComponentByType('GridFieldDataColumns');
324
		$columns->setDisplayFields(array(
325
			'StripThumbnail' => false,
326
			'Title' => _t('File.Title'),
327
			'Created' => singleton('File')->fieldLabel('Created'),
328
		));
329
		$columns->setFieldCasting(array(
330
			'Created' => 'SS_Datetime->Nice'
331
		));
332
333
		$fromCMS = new CompositeField(
334
			$select = TreeDropdownField::create('ParentID', "", 'Folder')
335
				->addExtraClass('noborder')
336
				->setValue($parentID),
337
			$fileField
338
		);
339
340
		$fromCMS->addExtraClass('content ss-uploadfield htmleditorfield-from-cms');
341
		$select->addExtraClass('content-select');
342
343
344
		$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.');
345
		$fromWeb = new CompositeField(
346
			$description = new LiteralField('URLDescription', '<div class="url-description">' . $URLDescription . '</div>'),
347
			$remoteURL = new TextField('RemoteURL', 'http://'),
348
			new LiteralField('addURLImage',
349
				'<button type="button" class="action ui-action-constructive ui-button field font-icon-plus add-url">' .
350
				_t('HtmlEditorField.BUTTONADDURL', 'Add url').'</button>')
351
		);
352
353
		$remoteURL->addExtraClass('remoteurl');
354
		$fromWeb->addExtraClass('content ss-uploadfield htmleditorfield-from-web');
355
356
		Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/AssetUploadField.css');
357
		$computerUploadField = Object::create('UploadField', 'AssetUploadField', '');
358
		$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...
359
		$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...
360
		$computerUploadField->addExtraClass('ss-assetuploadfield htmleditorfield-from-computer');
361
		$computerUploadField->removeExtraClass('ss-uploadfield');
362
		$computerUploadField->setTemplate('HtmlEditorField_UploadField');
363
		$computerUploadField->setFolderName(Config::inst()->get('Upload', 'uploads_folder'));
364
365
		$defaultPanel = new CompositeField(
366
			$computerUploadField,
367
			$fromCMS
368
		);
369
370
		$fromWebPanel = new CompositeField(
371
			$fromWeb
372
		);
373
374
		$defaultPanel->addExtraClass('htmleditorfield-default-panel');
375
		$fromWebPanel->addExtraClass('htmleditorfield-web-panel');
376
377
		$allFields = new CompositeField(
378
			$defaultPanel,
379
			$fromWebPanel,
380
			$editComposite = new CompositeField(
381
				new LiteralField('contentEdit', '<div class="content-edit ss-uploadfield-files files"></div>')
382
			)
383
		);
384
385
		$allFields->addExtraClass('ss-insert-media');
386
387
		$headings = new CompositeField(
388
			new LiteralField(
389
				'Heading',
390
				sprintf('<h3 class="htmleditorfield-mediaform-heading insert">%s</h3>',
391
					_t('HtmlEditorField.INSERTMEDIA', 'Insert media from')).
392
				sprintf('<h3 class="htmleditorfield-mediaform-heading update">%s</h3>',
393
					_t('HtmlEditorField.UpdateMEDIA', 'Update media'))
394
			)
395
		);
396
397
		$headings->addExtraClass('cms-content-header');
398
		$editComposite->addExtraClass('ss-assetuploadfield');
399
400
		$fields = new FieldList(
401
			$headings,
402
			$allFields
403
		);
404
405
		$form = new Form(
406
			$this->controller,
407
			"{$this->name}/MediaForm",
408
			$fields,
409
			new FieldList()
410
		);
411
412
413
		$form->unsetValidator();
414
		$form->disableSecurityToken();
415
		$form->loadDataFrom($this);
0 ignored issues
show
Documentation introduced by
$this is of type this<HtmlEditorField_Toolbar>, but the function expects a array|object<DataObject>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
416
		$form->addExtraClass('htmleditorfield-form htmleditorfield-mediaform cms-dialog-content');
417
418
		// Allow other people to extend the fields being added to the imageform
419
		$this->extend('updateMediaForm', $form);
420
421
		return $form;
422
	}
423
424
	/**
425
	 * List of allowed schemes (no wildcard, all lower case) or empty to allow all schemes
426
	 *
427
	 * @config
428
	 * @var array
429
	 */
430
	private static $fileurl_scheme_whitelist = array('http', 'https');
431
432
	/**
433
	 * List of allowed domains (no wildcard, all lower case) or empty to allow all domains
434
	 *
435
	 * @config
436
	 * @var array
437
	 */
438
	private static $fileurl_domain_whitelist = array();
439
440
	/**
441
	 * Find local File dataobject given ID
442
	 *
443
	 * @param int $id
444
	 * @return array
445
	 */
446
	protected function viewfile_getLocalFileByID($id) {
447
		/** @var File $file */
448
		$file = DataObject::get_by_id('File', $id);
449
		if ($file && $file->canView()) {
450
			return array($file, $file->getURL());
451
		}
452
		return [null, null];
453
	}
454
455
	/**
456
	 * Get remote File given url
457
	 *
458
	 * @param string $fileUrl Absolute URL
459
	 * @return array
460
	 * @throws SS_HTTPResponse_Exception
461
	 */
462
	protected function viewfile_getRemoteFileByURL($fileUrl) {
463
		if(!Director::is_absolute_url($fileUrl)) {
464
			throw $this->getErrorFor(_t(
465
				"HtmlEditorField_Toolbar.ERROR_ABSOLUTE",
466
				"Only absolute urls can be embedded"
467
			));
468
		}
469
		$scheme = strtolower(parse_url($fileUrl, PHP_URL_SCHEME));
470
		$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...
471
		if (!$scheme || ($allowed_schemes && !in_array($scheme, $allowed_schemes))) {
472
			throw $this->getErrorFor(_t(
473
				"HtmlEditorField_Toolbar.ERROR_SCHEME",
474
				"This file scheme is not included in the whitelist"
475
			));
476
		}
477
		$domain = strtolower(parse_url($fileUrl, PHP_URL_HOST));
478
		$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...
479
		if (!$domain || ($allowed_domains && !in_array($domain, $allowed_domains))) {
480
			throw $this->getErrorFor(_t(
481
				"HtmlEditorField_Toolbar.ERROR_HOSTNAME",
482
				"This file hostname is not included in the whitelist"
483
			));
484
		}
485
		return [null, $fileUrl];
486
	}
487
488
	/**
489
	 * Prepare error for the front end
490
	 *
491
	 * @param string $message
492
	 * @param int $code
493
	 * @return SS_HTTPResponse_Exception
494
	 */
495
	protected function getErrorFor($message, $code = 400) {
496
		$exception = new SS_HTTPResponse_Exception($message, $code);
497
		$exception->getResponse()->addHeader('X-Status', $message);
498
		return $exception;
499
	}
500
501
	/**
502
	 * View of a single file, either on the filesystem or on the web.
503
	 *
504
	 * @throws SS_HTTPResponse_Exception
505
	 * @param SS_HTTPRequest $request
506
	 * @return string
507
	 */
508
	public function viewfile($request) {
509
		$file = null;
510
		$url = null;
511
		// Get file and url by request method
512
		if($fileUrl = $request->getVar('FileURL')) {
513
			// Get remote url
514
			list($file, $url) = $this->viewfile_getRemoteFileByURL($fileUrl);
515
		} elseif($id = $request->getVar('ID')) {
516
			// Or we could have been passed an ID directly
517
			list($file, $url) = $this->viewfile_getLocalFileByID($id);
518
		} else {
519
			// Or we could have been passed nothing, in which case panic
520
			throw $this->getErrorFor(_t(
521
				"HtmlEditorField_Toolbar.ERROR_ID",
522
				'Need either "ID" or "FileURL" parameter to identify the file'
523
			));
524
		}
525
526
		// Validate file exists
527
		if(!$url) {
528
			throw $this->getErrorFor(_t(
529
				"HtmlEditorField_Toolbar.ERROR_NOTFOUND",
530
				'Unable to find file to view'
531
			));
532
		}
533
534
		// Instanciate file wrapper and get fields based on its type
535
		// Check if appCategory is an image and exists on the local system, otherwise use oEmbed to refference a
536
		// remote image
537
		$fileCategory = $this->getFileCategory($url, $file);
538
		switch($fileCategory) {
539
			case 'image':
540
			case 'image/supported':
541
				$fileWrapper = new HtmlEditorField_Image($url, $file);
542
				break;
543
			case 'flash':
544
				$fileWrapper = new HtmlEditorField_Flash($url, $file);
545
				break;
546
			default:
547
				// Only remote files can be linked via o-embed
548
				// {@see HtmlEditorField_Toolbar::getAllowedExtensions())
549
				if($file) {
550
					throw $this->getErrorFor(_t(
551
						"HtmlEditorField_Toolbar.ERROR_OEMBED_REMOTE",
552
						"Oembed is only compatible with remote files"
553
					));
554
				}
555
556
				// Other files should fallback to oembed
557
				$fileWrapper = new HtmlEditorField_Embed($url, $file);
558
				break;
559
		}
560
561
		// Render fields and return
562
		$fields = $this->getFieldsForFile($url, $fileWrapper);
563
		return $fileWrapper->customise(array(
564
			'Fields' => $fields,
565
		))->renderWith($this->templateViewFile);
566
	}
567
568
	/**
569
	 * Guess file category from either a file or url
570
	 *
571
	 * @param string $url
572
	 * @param File $file
573
	 * @return string
574
	 */
575
	protected function getFileCategory($url, $file) {
576
		if($file) {
577
			return $file->appCategory();
578
		}
579
		if($url) {
580
			return File::get_app_category(File::get_file_extension($url));
581
		}
582
		return null;
583
	}
584
585
	/**
586
	 * Find all anchors available on the given page.
587
	 *
588
	 * @return array
589
	 * @throws SS_HTTPResponse_Exception
590
	 */
591
	public function getanchors() {
592
		$id = (int)$this->getRequest()->getVar('PageID');
593
		$anchors = array();
594
595
		if (($page = Page::get()->byID($id)) && !empty($page)) {
596
			if (!$page->canView()) {
597
				throw new HTTPResponse_Exception(
598
					_t(
599
						'HtmlEditorField.ANCHORSCANNOTACCESSPAGE',
600
						'You are not permitted to access the content of the target page.'
601
					),
602
					403
603
				);
604
			}
605
606
			// Similar to the regex found in HtmlEditorField.js / getAnchors method.
607
			if (preg_match_all(
608
				"/\\s+(name|id)\\s*=\\s*([\"'])([^\\2\\s>]*?)\\2|\\s+(name|id)\\s*=\\s*([^\"']+)[\\s +>]/im",
609
				$page->Content,
610
				$matches
611
			)) {
612
				$anchors = array_values(array_unique(array_filter(
613
					array_merge($matches[3], $matches[5]))
614
				));
615
			}
616
617
		} else {
618
			throw new HTTPResponse_Exception(
619
				_t('HtmlEditorField.ANCHORSPAGENOTFOUND', 'Target page not found.'),
620
				404
621
			);
622
		}
623
624
		return json_encode($anchors);
625
	}
626
627
	/**
628
	 * Similar to {@link File->getCMSFields()}, but only returns fields
629
	 * for manipulating the instance of the file as inserted into the HTML content,
630
	 * not the "master record" in the database - hence there's no form or saving logic.
631
	 *
632
	 * @param string $url Abolute URL to asset
633
	 * @param HtmlEditorField_File $file Asset wrapper
634
	 * @return FieldList
635
	 */
636
	protected function getFieldsForFile($url, HtmlEditorField_File $file) {
637
		$fields = $this->extend('getFieldsForFile', $url, $file);
638
		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...
639
			$fields = $file->getFields();
640
			$file->extend('updateFields', $fields);
641
		}
642
		$this->extend('updateFieldsForFile', $fields, $url, $file);
643
		return $fields;
644
	}
645
646
647
	/**
648
	 * Gets files filtered by a given parent with the allowed extensions
649
	 *
650
	 * @param int $parentID
651
	 * @return DataList
652
	 */
653
	protected function getFiles($parentID = null) {
654
		$exts = $this->getAllowedExtensions();
655
		$dotExts = array_map(function($ext) {
656
			return ".{$ext}";
657
		}, $exts);
658
		$files = File::get()->filter('Name:EndsWith', $dotExts);
659
660
		// Limit by folder (if required)
661
		if($parentID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parentID of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
662
			$files = $files->filter('ParentID', $parentID);
663
		}
664
665
		return $files;
666
	}
667
668
	/**
669
	 * @return Array All extensions which can be handled by the different views.
670
	 */
671
	protected function getAllowedExtensions() {
672
		$exts = array('jpg', 'gif', 'png', 'swf', 'jpeg');
673
		$this->extend('updateAllowedExtensions', $exts);
674
		return $exts;
675
	}
676
677
}
678
679
/**
680
 * Encapsulation of a file which can either be a remote URL
681
 * or a {@link File} on the local filesystem, exhibiting common properties
682
 * such as file name or the URL.
683
 *
684
 * @todo Remove once core has support for remote files
685
 * @package forms
686
 * @subpackage fields-formattedinput
687
 */
688
abstract class HtmlEditorField_File extends ViewableData {
689
690
	/**
691
	 * Default insertion width for Images and Media
692
	 *
693
	 * @config
694
	 * @var int
695
	 */
696
	private static $insert_width = 600;
697
698
	/**
699
	 * Default insert height for images and media
700
	 *
701
	 * @config
702
	 * @var int
703
	 */
704
	private static $insert_height = 360;
705
706
	/**
707
	 * Max width for insert-media preview.
708
	 *
709
	 * Matches CSS rule for .cms-file-info-preview
710
	 *
711
	 * @var int
712
	 */
713
	private static $media_preview_width = 176;
714
715
	/**
716
	 * Max height for insert-media preview.
717
	 *
718
	 * Matches CSS rule for .cms-file-info-preview
719
	 *
720
	 * @var int
721
	 */
722
	private static $media_preview_height = 128;
723
724
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
725
		'URL' => 'Varchar',
726
		'Name' => 'Varchar'
727
	);
728
729
	/**
730
	 * Absolute URL to asset
731
	 *
732
	 * @var string
733
	 */
734
	protected $url;
735
736
	/**
737
	 * File dataobject (if available)
738
	 *
739
	 * @var File
740
	 */
741
	protected $file;
742
743
	/**
744
	 * @param string $url
745
	 * @param File $file
746
	 */
747
	public function __construct($url, File $file = null) {
748
		$this->url = $url;
749
		$this->file = $file;
750
		$this->failover = $file;
751
		parent::__construct();
752
	}
753
754
	/**
755
	 * @return FieldList
756
	 */
757
	public function getFields() {
758
		$fields = new FieldList(
759
			CompositeField::create(
760
				CompositeField::create(LiteralField::create("ImageFull", $this->getPreview()))
761
					->setName("FilePreviewImage")
762
					->addExtraClass('cms-file-info-preview'),
763
				CompositeField::create($this->getDetailFields())
764
					->setName("FilePreviewData")
765
					->addExtraClass('cms-file-info-data')
766
			)
767
				->setName("FilePreview")
768
				->addExtraClass('cms-file-info'),
769
			TextField::create('CaptionText', _t('HtmlEditorField.CAPTIONTEXT', 'Caption text')),
770
			DropdownField::create(
771
				'CSSClass',
772
				_t('HtmlEditorField.CSSCLASS', 'Alignment / style'),
773
				array(
774
					'leftAlone' => _t('HtmlEditorField.CSSCLASSLEFTALONE', 'On the left, on its own.'),
775
					'center' => _t('HtmlEditorField.CSSCLASSCENTER', 'Centered, on its own.'),
776
					'left' => _t('HtmlEditorField.CSSCLASSLEFT', 'On the left, with text wrapping around.'),
777
					'right' => _t('HtmlEditorField.CSSCLASSRIGHT', 'On the right, with text wrapping around.')
778
				)
779
			),
780
			FieldGroup::create(_t('HtmlEditorField.IMAGEDIMENSIONS', 'Dimensions'),
781
				TextField::create(
782
					'Width',
783
					_t('HtmlEditorField.IMAGEWIDTHPX', 'Width'),
784
					$this->getInsertWidth()
785
				)->setMaxLength(5),
786
				TextField::create(
787
					'Height',
788
					" x " . _t('HtmlEditorField.IMAGEHEIGHTPX', 'Height'),
789
					$this->getInsertHeight()
790
				)->setMaxLength(5)
791
			)->addExtraClass('dimensions last'),
792
			HiddenField::create('URL', false, $this->getURL()),
793
			HiddenField::create('FileID', false, $this->getFileID())
794
		);
795
		return $fields;
796
	}
797
798
	/**
799
	 * Get list of fields for previewing this records details
800
	 *
801
	 * @return FieldList
802
	 */
803
	protected function getDetailFields() {
804
		$fields = new FieldList(
805
			ReadonlyField::create("FileType", _t('AssetTableField.TYPE','File type'), $this->getFileType()),
806
			ReadonlyField::create(
807
				'ClickableURL', _t('AssetTableField.URL','URL'), $this->getExternalLink()
808
			)->setDontEscape(true)
809
		);
810
		// Get file size
811
		if($this->getSize()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getSize() of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
812
			$fields->insertAfter(
813
				'FileType',
814
				ReadonlyField::create("Size", _t('AssetTableField.SIZE','File size'), $this->getSize())
815
			);
816
		}
817
		// Get modified details of local record
818
		if($this->getFile()) {
819
			$fields->push(new DateField_Disabled(
820
				"Created",
821
				_t('AssetTableField.CREATED', 'First uploaded'),
822
				$this->getFile()->Created
823
			));
824
			$fields->push(new DateField_Disabled(
825
				"LastEdited",
826
				_t('AssetTableField.LASTEDIT','Last changed'),
827
				$this->getFile()->LastEdited
828
			));
829
		}
830
		return $fields;
831
832
	}
833
834
	/**
835
	 * Get file DataObject
836
	 *
837
	 * Might not be set (for remote files)
838
	 *
839
	 * @return File
840
	 */
841
	public function getFile() {
842
		return $this->file;
843
	}
844
845
	/**
846
	 * Get file ID
847
	 *
848
	 * @return int
849
	 */
850
	public function getFileID() {
851
		if($file = $this->getFile()) {
852
			return $file->ID;
853
		}
854
	}
855
856
	/**
857
	 * Get absolute URL
858
	 *
859
	 * @return string
860
	 */
861
	public function getURL() {
862
		return $this->url;
863
	}
864
865
	/**
866
	 * Get basename
867
	 *
868
	 * @return string
869
	 */
870
	public function getName() {
871
		return $this->file
872
			? $this->file->Name
873
			: preg_replace('/\?.*/', '', basename($this->url));
874
	}
875
876
	/**
877
	 * Get descriptive file type
878
	 *
879
	 * @return string
880
	 */
881
	public function getFileType() {
882
		return File::get_file_type($this->getName());
883
	}
884
885
	/**
886
	 * Get file size (if known) as string
887
	 *
888
	 * @return string|false String value, or false if doesn't exist
889
	 */
890
	public function getSize() {
891
		if($this->file) {
892
			return $this->file->getSize();
893
		}
894
		return false;
895
	}
896
897
	/**
898
	 * HTML content for preview
899
	 *
900
	 * @return string HTML
901
	 */
902
	public function getPreview() {
903
		$preview = $this->extend('getPreview');
904
		if($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...
905
			return $preview;
906
		}
907
908
		// Generate tag from preview
909
		$thumbnailURL = Convert::raw2att(
910
			Controller::join_links($this->getPreviewURL(), "?r=" . rand(1,100000))
911
		);
912
		$fileName = Convert::raw2att($this->Name);
913
		return sprintf(
914
			"<img id='thumbnailImage' class='thumbnail-preview'  src='%s' alt='%s' />\n",
915
			$thumbnailURL,
916
			$fileName
917
		);
918
	}
919
920
	/**
921
	 * HTML Content for external link
922
	 *
923
	 * @return string
924
	 */
925
	public function getExternalLink() {
926
		$title = $this->file
927
			? $this->file->getTitle()
928
			: $this->getName();
929
		return sprintf(
930
			'<a href="%1$s" title="%2$s" target="_blank" rel="external" class="file-url">%1$s</a>',
931
			Convert::raw2att($this->url),
932
			Convert::raw2att($title)
933
		);
934
	}
935
936
	/**
937
	 * Generate thumbnail url
938
	 *
939
	 * @return string
940
	 */
941
	public function getPreviewURL() {
942
		// Get preview from file
943
		if($this->file) {
944
			return $this->getFilePreviewURL();
945
		}
946
947
		// Generate default icon html
948
		return File::get_icon_for_extension($this->getExtension());
949
	}
950
951
	/**
952
	 * Generate thumbnail URL from file dataobject (if available)
953
	 *
954
	 * @return string
955
	 */
956
	protected function getFilePreviewURL() {
957
		// Get preview from file
958
		if($this->file) {
959
			$width = $this->config()->media_preview_width;
0 ignored issues
show
Documentation introduced by
The property media_preview_width 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...
960
			$height = $this->config()->media_preview_height;
0 ignored issues
show
Documentation introduced by
The property media_preview_height 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...
961
			return $this->file->ThumbnailURL($width, $height);
962
		}
963
	}
964
965
	/**
966
	 * Get file extension
967
	 *
968
	 * @return string
969
	 */
970
	public function getExtension() {
971
		$extension = File::get_file_extension($this->getName());
972
		return strtolower($extension);
973
	}
974
975
	/**
976
	 * Category name
977
	 *
978
	 * @return string
979
	 */
980
	public function appCategory() {
981
		if($this->file) {
982
			return $this->file->appCategory();
983
		} else {
984
			return File::get_app_category($this->getExtension());
985
		}
986
	}
987
988
	/**
989
	 * Get height of this item
990
	 */
991
	public function getHeight() {
992
		if($this->file) {
993
			$height = $this->file->getHeight();
994
			if($height) {
995
				return $height;
996
			}
997
		}
998
		return $this->config()->insert_height;
999
	}
1000
1001
	/**
1002
	 * Get width of this item
1003
	 *
1004
	 * @return type
1005
	 */
1006
	public function getWidth() {
1007
		if($this->file) {
1008
			$width = $this->file->getWidth();
1009
			if($width) {
1010
				return $width;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $width; (string|integer|array) is incompatible with the return type documented by HtmlEditorField_File::getWidth of type type.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1011
			}
1012
		}
1013
		return $this->config()->insert_width;
1014
	}
1015
1016
	/**
1017
	 * Provide an initial width for inserted media, restricted based on $embed_width
1018
	 *
1019
	 * @return int
1020
	 */
1021
	public function getInsertWidth() {
1022
		$width = $this->getWidth();
1023
		$maxWidth = $this->config()->insert_width;
0 ignored issues
show
Documentation introduced by
The property insert_width 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...
1024
		return ($width <= $maxWidth) ? $width : $maxWidth;
1025
	}
1026
1027
	/**
1028
	 * Provide an initial height for inserted media, scaled proportionally to the initial width
1029
	 *
1030
	 * @return int
1031
	 */
1032 View Code Duplication
	public function getInsertHeight() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1033
		$width = $this->getWidth();
1034
		$height = $this->getHeight();
1035
		$maxWidth = $this->config()->insert_width;
0 ignored issues
show
Documentation introduced by
The property insert_width 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...
1036
		return ($width <= $maxWidth) ? $height : round($height*($maxWidth/$width));
1037
	}
1038
1039
}
1040
1041
/**
1042
 * Encapsulation of an oembed tag, linking to an external media source.
1043
 *
1044
 * @see Oembed
1045
 * @package forms
1046
 * @subpackage fields-formattedinput
1047
 */
1048
class HtmlEditorField_Embed extends HtmlEditorField_File {
1049
1050
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
1051
		'Type' => 'Varchar',
1052
		'Info' => 'Varchar'
1053
	);
1054
1055
	/**
1056
	 * Oembed result
1057
	 *
1058
	 * @var Oembed_Result
1059
	 */
1060
	protected $oembed;
1061
1062
	public function __construct($url, File $file = null) {
1063
		parent::__construct($url, $file);
1064
		$this->oembed = Oembed::get_oembed_from_url($url);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Oembed::get_oembed_from_url($url) can also be of type false. However, the property $oembed is declared as type object<Oembed_Result>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1065
		if(!$this->oembed) {
1066
			$controller = Controller::curr();
1067
			$response = $controller->getResponse();
1068
			$response->addHeader('X-Status',
1069
				rawurlencode(_t(
1070
					'HtmlEditorField.URLNOTANOEMBEDRESOURCE',
1071
					"The URL '{url}' could not be turned into a media resource.",
1072
					"The given URL is not a valid Oembed resource; the embed element couldn't be created.",
1073
					array('url' => $url)
0 ignored issues
show
Documentation introduced by
array('url' => $url) is of type array<string,?,{"url":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1074
				)));
1075
			$response->setStatusCode(404);
1076
1077
			throw new HTTPResponse_Exception($response);
1078
		}
1079
	}
1080
1081
	/**
1082
	 * Get file-edit fields for this filed
1083
	 *
1084
	 * @return FieldList
1085
	 */
1086
	public function getFields() {
1087
		$fields = parent::getFields();
1088
		if($this->Type === 'photo') {
1089
			$fields->insertBefore('CaptionText', new TextField(
1090
				'AltText',
1091
				_t('HtmlEditorField.IMAGEALTTEXT', 'Alternative text (alt) - shown if image can\'t be displayed'),
1092
				$this->Title,
1093
				80
1094
			));
1095
			$fields->insertBefore('CaptionText', new TextField(
1096
				'Title',
1097
				_t('HtmlEditorField.IMAGETITLE', 'Title text (tooltip) - for additional information about the image')
1098
			));
1099
		}
1100
		return $fields;
1101
	}
1102
1103
	/**
1104
	 * Get width of this oembed
1105
	 *
1106
	 * @return int
1107
	 */
1108
	public function getWidth() {
1109
		return $this->oembed->Width ?: 100;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->oembed->Width ?: 100; (integer) is incompatible with the return type of the parent method HtmlEditorField_File::getWidth of type type.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1110
	}
1111
1112
	/**
1113
	 * Get height of this oembed
1114
	 *
1115
	 * @return int
1116
	 */
1117
	public function getHeight() {
1118
		return $this->oembed->Height ?: 100;
1119
	}
1120
1121
	public function getPreviewURL() {
1122
		// Use thumbnail url
1123
		if(!empty($this->oembed->thumbnail_url)) {
1124
			return $this->oembed->thumbnail_url;
1125
		}
1126
1127
		// Use direct image type
1128
		if($this->getType() == 'photo' && !empty($this->Oembed->url)) {
0 ignored issues
show
Bug introduced by
The property Oembed does not seem to exist. Did you mean oembed?

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...
1129
			return $this->Oembed->url;
0 ignored issues
show
Bug introduced by
The property Oembed does not seem to exist. Did you mean oembed?

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...
1130
		}
1131
1132
		// Default media
1133
		return FRAMEWORK_DIR . '/images/default_media.png';
1134
	}
1135
1136
	public function getName() {
1137
		if(isset($this->oembed->title)) {
1138
			return $this->oembed->title;
1139
		} else {
1140
			return parent::getName();
1141
		}
1142
	}
1143
1144
	/**
1145
	 * Get OEmbed type
1146
	 *
1147
	 * @return string
1148
	 */
1149
	public function getType() {
1150
		return $this->oembed->type;
0 ignored issues
show
Documentation introduced by
The property $type is declared protected in Oembed_Result. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

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...
1151
	}
1152
1153
	public function getFileType() {
1154
		return $this->getType()
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getType() ?: parent::getFileType(); of type boolean|string adds the type boolean to the return on line 1154 which is incompatible with the return type of the parent method HtmlEditorField_File::getFileType of type string.
Loading history...
1155
			?: parent::getFileType();
1156
	}
1157
1158
	/**
1159
	 * @return Oembed_Result
1160
	 */
1161
	public function getOembed() {
1162
		return $this->oembed;
1163
	}
1164
1165
	public function appCategory() {
1166
		return 'embed';
1167
	}
1168
1169
	/**
1170
	 * Info for this oembed
1171
	 *
1172
	 * @return string
1173
	 */
1174
	public function getInfo() {
1175
		return $this->oembed->info;
1176
	}
1177
}
1178
1179
/**
1180
 * Encapsulation of an image tag, linking to an image either internal or external to the site.
1181
 *
1182
 * @package forms
1183
 * @subpackage fields-formattedinput
1184
 */
1185
class HtmlEditorField_Image extends HtmlEditorField_File {
1186
1187
	/**
1188
	 * @var int
1189
	 */
1190
	protected $width;
1191
1192
	/**
1193
	 * @var int
1194
	 */
1195
	protected $height;
1196
1197
	/**
1198
	 * File size details
1199
	 *
1200
	 * @var string
1201
	 */
1202
	protected $size;
1203
1204
	public function __construct($url, File $file = null) {
1205
		parent::__construct($url, $file);
1206
1207
		if($file) {
1208
			return;
1209
		}
1210
1211
		// Get size of remote file
1212
		$size = @filesize($url);
1213
		if($size) {
1214
			$this->size = $size;
1215
		}
1216
1217
		// Get dimensions of remote file
1218
		$info = @getimagesize($url);
1219
		if($info) {
1220
			$this->width = $info[0];
1221
			$this->height = $info[1];
1222
		}
1223
	}
1224
1225
	public function getFields() {
1226
		$fields = parent::getFields();
1227
1228
		// Alt text
1229
		$fields->insertBefore(
1230
			'CaptionText',
1231
			TextField::create(
1232
				'AltText',
1233
				_t('HtmlEditorField.IMAGEALT', 'Alternative text (alt)'),
1234
				$this->Title,
1235
				80
1236
			)->setDescription(
1237
				_t('HtmlEditorField.IMAGEALTTEXTDESC', 'Shown to screen readers or if image can\'t be displayed')
1238
			)
1239
		);
1240
1241
		// Tooltip
1242
		$fields->insertAfter(
1243
			'AltText',
1244
			TextField::create(
1245
				'Title',
1246
				_t('HtmlEditorField.IMAGETITLETEXT', 'Title text (tooltip)')
1247
			)->setDescription(
1248
				_t('HtmlEditorField.IMAGETITLETEXTDESC', 'For additional information about the image')
1249
			)
1250
		);
1251
1252
		return $fields;
1253
	}
1254
1255
	protected function getDetailFields() {
1256
		$fields = parent::getDetailFields();
1257
		$width = $this->getOriginalWidth();
1258
		$height = $this->getOriginalHeight();
1259
1260
		// Show dimensions of original
1261
		if($width && $height) {
1262
			$fields->insertAfter(
1263
				'ClickableURL',
1264
				ReadonlyField::create(
1265
					"OriginalWidth",
1266
					_t('AssetTableField.WIDTH','Width'),
1267
					$width
1268
				)
1269
			);
1270
			$fields->insertAfter(
1271
				'OriginalWidth',
1272
				ReadonlyField::create(
1273
					"OriginalHeight",
1274
					_t('AssetTableField.HEIGHT','Height'),
1275
					$height
1276
				)
1277
			);
1278
		}
1279
		return $fields;
1280
	}
1281
1282
	/**
1283
	 * Get width of original, if known
1284
	 *
1285
	 * @return int
1286
	 */
1287
	public function getOriginalWidth() {
1288
		if($this->width) {
1289
			return $this->width;
1290
		}
1291
		if($this->file) {
1292
			$width = $this->file->getWidth();
1293
			if($width) {
1294
				return $width;
1295
			}
1296
		}
1297
	}
1298
1299
	/**
1300
	 * Get height of original, if known
1301
	 *
1302
	 * @return int
1303
	 */
1304
	public function getOriginalHeight() {
1305
		if($this->height) {
1306
			return $this->height;
1307
		}
1308
1309
		if($this->file) {
1310
			$height = $this->file->getHeight();
1311
			if($height) {
1312
				return $height;
1313
			}
1314
		}
1315
	}
1316
1317
	public function getWidth() {
1318
		if($this->width) {
1319
			return $this->width;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->width; (integer) is incompatible with the return type of the parent method HtmlEditorField_File::getWidth of type type.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1320
		}
1321
		return parent::getWidth();
1322
	}
1323
1324
	public function getHeight() {
1325
		if($this->height) {
1326
			return $this->height;
1327
		}
1328
		return parent::getHeight();
1329
	}
1330
1331
	public function getSize() {
1332
		if($this->size) {
1333
			return File::format_size($this->size);
1334
		}
1335
		parent::getSize();
1336
	}
1337
1338
	/**
1339
	 * Provide an initial width for inserted image, restricted based on $embed_width
1340
	 *
1341
	 * @return int
1342
	 */
1343
	public function getInsertWidth() {
1344
		$width = $this->getWidth();
1345
		$maxWidth = $this->config()->insert_width;
0 ignored issues
show
Documentation introduced by
The property insert_width 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...
1346
		return $width <= $maxWidth
1347
			? $width
1348
			: $maxWidth;
1349
	}
1350
1351
	/**
1352
	 * Provide an initial height for inserted image, scaled proportionally to the initial width
1353
	 *
1354
	 * @return int
1355
	 */
1356 View Code Duplication
	public function getInsertHeight() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1357
		$width = $this->getWidth();
1358
		$height = $this->getHeight();
1359
		$maxWidth = $this->config()->insert_width;
0 ignored issues
show
Documentation introduced by
The property insert_width 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...
1360
		return ($width <= $maxWidth) ? $height : round($height*($maxWidth/$width));
1361
	}
1362
1363
	public function getPreviewURL() {
1364
		// Get preview from file
1365
		if($this->file) {
1366
			return $this->getFilePreviewURL();
1367
		}
1368
1369
		// Embed image directly
1370
		return $this->url;
1371
	}
1372
}
1373
1374
/**
1375
 * Generate flash file embed
1376
 */
1377
class HtmlEditorField_Flash extends HtmlEditorField_File {
1378
1379
	public function getFields() {
1380
		$fields = parent::getFields();
1381
		$fields->removeByName('CaptionText', true);
1382
		return $fields;
1383
	}
1384
}
1385