Completed
Push — namespace-model ( dc57a0...2a0ce1 )
by Sam
07:25
created

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