Completed
Push — master ( 4af71b...17ddfa )
by Sam
18:54
created

HTMLEditorField_Toolbar   D

Complexity

Total Complexity 43

Size/Duplication

Total Lines 462
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 26

Importance

Changes 0
Metric Value
dl 0
loc 462
rs 4.0512
c 0
b 0
f 0
wmc 43
lcom 1
cbo 26

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getTemplateViewFile() 0 4 1
A __construct() 0 7 1
A forTemplate() 0 7 1
A siteTreeSearchCallback() 0 7 1
B LinkForm() 0 86 1
A EditorExternalLink() 0 9 2
A EditorEmailLink() 0 10 2
A getAttachParentID() 0 6 1
A viewfile_getLocalFileByID() 0 9 3
C viewfile_getRemoteFileByURL() 0 26 8
A getErrorFor() 0 6 1
B viewfile() 0 60 8
A getFileCategory() 0 10 3
B getanchors() 0 38 5
A getFieldsForFile() 0 10 2
A getFiles() 0 15 2
A getAllowedExtensions() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like HTMLEditorField_Toolbar often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HTMLEditorField_Toolbar, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Forms\HTMLEditor;
4
5
use SilverStripe\Admin\Forms\EditorExternalLinkFormFactory;
6
use SilverStripe\Admin\Forms\EditorEmailLinkFormFactory;
7
use SilverStripe\Assets\File;
8
use SilverStripe\CMS\Model\SiteTree;
9
use SilverStripe\Control\Controller;
10
use SilverStripe\Control\Director;
11
use SilverStripe\Control\RequestHandler;
12
use SilverStripe\Control\HTTPRequest;
13
use SilverStripe\Control\HTTPResponse_Exception;
14
use SilverStripe\Core\Injector\Injector;
15
use SilverStripe\Forms\CheckboxField;
16
use SilverStripe\Forms\CompositeField;
17
use SilverStripe\Forms\EmailField;
18
use SilverStripe\Forms\FieldList;
19
use SilverStripe\Forms\Form;
20
use SilverStripe\Forms\HiddenField;
21
use SilverStripe\Forms\LiteralField;
22
use SilverStripe\Forms\OptionsetField;
23
use SilverStripe\Forms\TextField;
24
use SilverStripe\Forms\TreeDropdownField;
25
use SilverStripe\ORM\DataList;
26
use SilverStripe\ORM\DataObject;
27
use SilverStripe\ORM\FieldType\DBField;
28
use SilverStripe\View\SSViewer;
29
30
/**
31
 * Toolbar shared by all instances of {@link HTMLEditorField}, to avoid too much markup duplication.
32
 *  Needs to be inserted manually into the template in order to function - see {@link LeftAndMain->EditorToolbar()}.
33
 */
34
class HTMLEditorField_Toolbar extends RequestHandler
35
{
36
37
    private static $allowed_actions = array(
38
        'LinkForm',
39
        'EditorExternalLink',
40
        'EditorEmailLink',
41
        'viewfile',
42
        'getanchors'
43
    );
44
45
    /**
46
     * @return string
47
     */
48
    public function getTemplateViewFile()
49
    {
50
        return SSViewer::get_templates_by_class(get_class($this), '_viewfile', __CLASS__);
51
    }
52
53
    /**
54
     * @var Controller
55
     */
56
    protected $controller;
57
58
    /**
59
     * @var string
60
     */
61
    protected $name;
62
63
    public function __construct($controller, $name)
64
    {
65
        parent::__construct();
66
67
        $this->controller = $controller;
68
        $this->name = $name;
69
    }
70
71
    public function forTemplate()
72
    {
73
        return sprintf(
74
            '<div id="cms-editor-dialogs" data-url-linkform="%s"></div>',
75
            Controller::join_links($this->controller->Link(), $this->name, 'LinkForm', 'forTemplate')
76
        );
77
    }
78
79
    /**
80
     * Searches the SiteTree for display in the dropdown
81
     *
82
     * @param string $sourceObject
83
     * @param string $labelField
84
     * @param string $search
85
     * @return DataList
86
     */
87
    public function siteTreeSearchCallback($sourceObject, $labelField, $search)
88
    {
89
        return DataObject::get($sourceObject)->filterAny(array(
90
            'MenuTitle:PartialMatch' => $search,
91
            'Title:PartialMatch' => $search
92
        ));
93
    }
94
95
    /**
96
     * Return a {@link Form} instance allowing a user to
97
     * add links in the TinyMCE content editor.
98
     *
99
     * @skipUpgrade
100
     * @return Form
101
     */
102
    public function LinkForm()
103
    {
104
        $siteTree = TreeDropdownField::create(
105
            'internal',
106
            _t(__CLASS__.'.PAGE', "Page"),
107
            SiteTree::class,
108
            'ID',
109
            'MenuTitle',
110
            true
111
        );
112
        // mimic the SiteTree::getMenuTitle(), which is bypassed when the search is performed
113
        $siteTree->setSearchFunction(array($this, 'siteTreeSearchCallback'));
114
115
        $numericLabelTmpl = '<span class="step-label"><span class="flyout">Step %d.</span>'
116
            . '<span class="title">%s</span></span>';
117
118
        $form = new Form(
119
            $this->controller,
120
            "{$this->name}/LinkForm",
121
            new FieldList(
122
                $headerWrap = new CompositeField(
123
                    new LiteralField(
124
                        'Heading',
125
                        sprintf(
126
                            '<h3 class="htmleditorfield-linkform-heading insert">%s</h3>',
127
                            _t(__CLASS__.'.LINK', 'Insert Link')
128
                        )
129
                    )
130
                ),
131
                $contentComposite = new CompositeField(
132
                    OptionsetField::create(
133
                        'LinkType',
134
                        DBField::create_field(
135
                            'HTMLFragment',
136
                            sprintf($numericLabelTmpl, '1', _t(__CLASS__.'.LINKTO', 'Link type'))
137
                        ),
138
                        array(
139
                            'internal' => _t(__CLASS__.'.LINKINTERNAL', 'Link to a page on this site'),
140
                            'external' => _t(__CLASS__.'.LINKEXTERNAL', 'Link to another website'),
141
                            'anchor' => _t(__CLASS__.'.LINKANCHOR', 'Link to an anchor on this page'),
142
                            'email' => _t(__CLASS__.'.LINKEMAIL', 'Link to an email address'),
143
                            'file' => _t(__CLASS__.'.LINKFILE', 'Link to download a file'),
144
                        ),
145
                        'internal'
146
                    ),
147
                    LiteralField::create(
148
                        'Step2',
149
                        '<div class="step2">'
150
                        . sprintf($numericLabelTmpl, '2', _t(__CLASS__.'.LINKDETAILS', 'Link details')) . '</div>'
151
                    ),
152
                    $siteTree,
153
                    TextField::create('external', _t(__CLASS__.'.URL', 'URL'), 'http://'),
154
                    EmailField::create('email', _t(__CLASS__.'.EMAIL', 'Email address')),
155
                    $fileField = TreeDropdownField::create(
156
                        'file',
157
                        _t(__CLASS__.'.FILE', 'File'),
158
                        File::class,
159
                        'ID',
160
                        'Name'
161
                    ),
162
                    TextField::create('Anchor', _t(__CLASS__.'.ANCHORVALUE', 'Anchor')),
163
                    TextField::create('Subject', _t(__CLASS__.'.SUBJECT', 'Email subject')),
164
                    TextField::create('Description', _t(__CLASS__.'.LINKDESCR', 'Link description')),
165
                    CheckboxField::create(
166
                        'TargetBlank',
167
                        _t(__CLASS__.'.LINKOPENNEWWIN', 'Open link in a new window?')
168
                    ),
169
                    HiddenField::create('Locale', null, $this->controller->Locale)
170
                )
171
            ),
172
            new FieldList()
173
        );
174
175
        $headerWrap->setName('HeaderWrap');
176
        $headerWrap->addExtraClass('CompositeField composite cms-content-header form-group--no-label ');
177
        $contentComposite->setName('ContentBody');
178
        $contentComposite->addExtraClass('ss-insert-link content');
179
180
        $form->unsetValidator();
181
        $form->loadDataFrom($this);
182
        $form->addExtraClass('htmleditorfield-form htmleditorfield-linkform cms-linkform-content');
183
184
        $this->extend('updateLinkForm', $form);
185
186
        return $form;
187
    }
188
    
189
    /**
190
     * Builds and returns the external link form
191
     *
192
     * @return null|Form
193
     */
194
    public function EditorExternalLink($id = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
195
    {
196
        /** @var EditorExternalLinkFormFactory $factory */
197
        $factory = Injector::inst()->get(EditorExternalLinkFormFactory::class);
198
        if ($factory) {
199
            return $factory->getForm($this->controller, "{$this->name}/EditorExternalLink");
200
        }
201
        return null;
202
    }
203
    
204
    /**
205
     * Builds and returns the external link form
206
     *
207
     * @return null|Form
208
     */
209
    public function EditorEmailLink($id = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
210
    {
211
        /** @var EditorEmailLinkFormFactory $factory */
212
        $factory = Injector::inst()->get(EditorEmailLinkFormFactory::class);
213
        
214
        if ($factory) {
215
            return $factory->getForm($this->controller, "{$this->name}/EditorEmailLink");
216
        }
217
        return null;
218
    }
219
    
220
    /**
221
     * Get the folder ID to filter files by for the "from cms" tab
222
     *
223
     * @return int
224
     */
225
    protected function getAttachParentID()
226
    {
227
        $parentID = $this->controller->getRequest()->requestVar('ParentID');
228
        $this->extend('updateAttachParentID', $parentID);
229
        return $parentID;
230
    }
231
232
    /**
233
     * List of allowed schemes (no wildcard, all lower case) or empty to allow all schemes
234
     *
235
     * @config
236
     * @var array
237
     */
238
    private static $fileurl_scheme_whitelist = array('http', 'https');
239
240
    /**
241
     * List of allowed domains (no wildcard, all lower case) or empty to allow all domains
242
     *
243
     * @config
244
     * @var array
245
     */
246
    private static $fileurl_domain_whitelist = array();
247
248
    /**
249
     * Find local File dataobject given ID
250
     *
251
     * @param int $id
252
     * @return array
253
     */
254
    protected function viewfile_getLocalFileByID($id)
255
    {
256
        /** @var File $file */
257
        $file = DataObject::get_by_id(File::class, $id);
258
        if ($file && $file->canView()) {
259
            return array($file, $file->getURL());
260
        }
261
        return [null, null];
262
    }
263
264
    /**
265
     * Get remote File given url
266
     *
267
     * @param string $fileUrl Absolute URL
268
     * @return array
269
     * @throws HTTPResponse_Exception
270
     */
271
    protected function viewfile_getRemoteFileByURL($fileUrl)
272
    {
273
        if (!Director::is_absolute_url($fileUrl)) {
274
            throw $this->getErrorFor(_t(
275
                "SilverStripe\\Forms\\HTMLEditor\\HTMLEditorField_Toolbar.ERROR_ABSOLUTE",
276
                "Only absolute urls can be embedded"
277
            ));
278
        }
279
        $scheme = strtolower(parse_url($fileUrl, PHP_URL_SCHEME));
280
        $allowed_schemes = self::config()->get('fileurl_scheme_whitelist');
281
        if (!$scheme || ($allowed_schemes && !in_array($scheme, $allowed_schemes))) {
282
            throw $this->getErrorFor(_t(
283
                "SilverStripe\\Forms\\HTMLEditor\\HTMLEditorField_Toolbar.ERROR_SCHEME",
284
                "This file scheme is not included in the whitelist"
285
            ));
286
        }
287
        $domain = strtolower(parse_url($fileUrl, PHP_URL_HOST));
288
        $allowed_domains = self::config()->get('fileurl_domain_whitelist');
289
        if (!$domain || ($allowed_domains && !in_array($domain, $allowed_domains))) {
290
            throw $this->getErrorFor(_t(
291
                "SilverStripe\\Forms\\HTMLEditor\\HTMLEditorField_Toolbar.ERROR_HOSTNAME",
292
                "This file hostname is not included in the whitelist"
293
            ));
294
        }
295
        return [null, $fileUrl];
296
    }
297
298
    /**
299
     * Prepare error for the front end
300
     *
301
     * @param string $message
302
     * @param int $code
303
     * @return HTTPResponse_Exception
304
     */
305
    protected function getErrorFor($message, $code = 400)
306
    {
307
        $exception = new HTTPResponse_Exception($message, $code);
308
        $exception->getResponse()->addHeader('X-Status', $message);
309
        return $exception;
310
    }
311
312
    /**
313
     * View of a single file, either on the filesystem or on the web.
314
     *
315
     * @throws HTTPResponse_Exception
316
     * @param HTTPRequest $request
317
     * @return string
318
     */
319
    public function viewfile($request)
320
    {
321
        $file = null;
322
        $url = null;
323
        // Get file and url by request method
324
        if ($fileUrl = $request->getVar('FileURL')) {
325
            // Get remote url
326
            list($file, $url) = $this->viewfile_getRemoteFileByURL($fileUrl);
327
        } elseif ($id = $request->getVar('ID')) {
328
            // Or we could have been passed an ID directly
329
            list($file, $url) = $this->viewfile_getLocalFileByID($id);
330
        } else {
331
            // Or we could have been passed nothing, in which case panic
332
            throw $this->getErrorFor(_t(
333
                "SilverStripe\\Forms\\HTMLEditor\\HTMLEditorField_Toolbar.ERROR_ID",
334
                'Need either "ID" or "FileURL" parameter to identify the file'
335
            ));
336
        }
337
338
        // Validate file exists
339
        if (!$url) {
340
            throw $this->getErrorFor(_t(
341
                "SilverStripe\\Forms\\HTMLEditor\\HTMLEditorField_Toolbar.ERROR_NOTFOUND",
342
                'Unable to find file to view'
343
            ));
344
        }
345
346
        // Instantiate file wrapper and get fields based on its type
347
        // Check if appCategory is an image and exists on the local system, otherwise use Embed to reference a
348
        // remote image
349
        $fileCategory = $this->getFileCategory($url, $file);
350
        switch ($fileCategory) {
351
            case 'image':
352
            case 'image/supported':
353
                $fileWrapper = new HTMLEditorField_Image($url, $file);
354
                break;
355
            case 'flash':
356
                $fileWrapper = new HTMLEditorField_Flash($url, $file);
357
                break;
358
            default:
359
                // Only remote files can be linked via o-embed
360
                // {@see HTMLEditorField_Toolbar::getAllowedExtensions())
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
361
                if ($file) {
362
                    throw $this->getErrorFor(_t(
363
                        "SilverStripe\\Forms\\HTMLEditor\\HTMLEditorField_Toolbar.ERROR_OEMBED_REMOTE",
364
                        "Embed is only compatible with remote files"
365
                    ));
366
                }
367
368
                // Other files should fallback to embed
369
                $fileWrapper = new HTMLEditorField_Embed($url, $file);
370
                break;
371
        }
372
373
        // Render fields and return
374
        $fields = $this->getFieldsForFile($url, $fileWrapper);
375
        return $fileWrapper->customise(array(
376
            'Fields' => $fields,
377
        ))->renderWith($this->getTemplateViewFile());
378
    }
379
380
    /**
381
     * Guess file category from either a file or url
382
     *
383
     * @param string $url
384
     * @param File $file
385
     * @return string
386
     */
387
    protected function getFileCategory($url, $file)
388
    {
389
        if ($file) {
390
            return $file->appCategory();
391
        }
392
        if ($url) {
393
            return File::get_app_category(File::get_file_extension($url));
394
        }
395
        return null;
396
    }
397
398
    /**
399
     * Find all anchors available on the given page.
400
     *
401
     * @return array
402
     * @throws HTTPResponse_Exception
403
     */
404
    public function getanchors()
405
    {
406
        $id = (int)$this->getRequest()->getVar('PageID');
407
        $anchors = array();
408
409
        if (($page = SiteTree::get()->byID($id)) && !empty($page)) {
410
            if (!$page->canView()) {
411
                throw new HTTPResponse_Exception(
412
                    _t(
413
                        'SilverStripe\\Forms\\HTMLEditor\\HTMLEditorField.ANCHORSCANNOTACCESSPAGE',
414
                        'You are not permitted to access the content of the target page.'
415
                    ),
416
                    403
417
                );
418
            }
419
420
            // Parse the shortcodes so [img id=x] doesn't end up as anchor x
421
            $htmlValue = $page->obj('Content')->forTemplate();
422
423
            // Similar to the regex found in HTMLEditorField.js / getAnchors method.
424
            if (preg_match_all(
425
                "/\\s+(name|id)\\s*=\\s*([\"'])([^\\2\\s>]*?)\\2|\\s+(name|id)\\s*=\\s*([^\"']+)[\\s +>]/im",
426
                $htmlValue,
427
                $matches
428
            )) {
429
                $anchors = array_values(array_unique(array_filter(
430
                    array_merge($matches[3], $matches[5])
431
                )));
432
            }
433
        } else {
434
            throw new HTTPResponse_Exception(
435
                _t('SilverStripe\\Forms\\HTMLEditor\\HTMLEditorField.ANCHORSPAGENOTFOUND', 'Target page not found.'),
436
                404
437
            );
438
        }
439
440
        return json_encode($anchors);
441
    }
442
443
    /**
444
     * Similar to {@link File->getCMSFields()}, but only returns fields
445
     * for manipulating the instance of the file as inserted into the HTML content,
446
     * not the "master record" in the database - hence there's no form or saving logic.
447
     *
448
     * @param string $url Abolute URL to asset
449
     * @param HTMLEditorField_File $file Asset wrapper
450
     * @return FieldList
451
     */
452
    protected function getFieldsForFile($url, HTMLEditorField_File $file)
453
    {
454
        $fields = $this->extend('getFieldsForFile', $url, $file);
455
        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...
456
            $fields = $file->getFields();
457
            $file->extend('updateFields', $fields);
458
        }
459
        $this->extend('updateFieldsForFile', $fields, $url, $file);
460
        return $fields;
461
    }
462
463
464
    /**
465
     * Gets files filtered by a given parent with the allowed extensions
466
     *
467
     * @param int $parentID
468
     * @return DataList
469
     */
470
    protected function getFiles($parentID = null)
471
    {
472
        $exts = $this->getAllowedExtensions();
473
        $dotExts = array_map(function ($ext) {
474
            return ".{$ext}";
475
        }, $exts);
476
        $files = File::get()->filter('Name:EndsWith', $dotExts);
477
478
        // Limit by folder (if required)
479
        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...
480
            $files = $files->filter('ParentID', $parentID);
481
        }
482
483
        return $files;
484
    }
485
486
    /**
487
     * @return array All extensions which can be handled by the different views.
488
     */
489
    protected function getAllowedExtensions()
490
    {
491
        $exts = array('jpg', 'gif', 'png', 'swf', 'jpeg');
492
        $this->extend('updateAllowedExtensions', $exts);
493
        return $exts;
494
    }
495
}
496