Passed
Pull Request — 4 (#911)
by Guy
03:00
created

BaseElement::getAnchorsInContent()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 13
c 1
b 0
f 0
nc 4
nop 0
dl 0
loc 20
rs 9.8333
1
<?php
2
3
namespace DNADesign\Elemental\Models;
4
5
use DNADesign\Elemental\Controllers\ElementController;
6
use DNADesign\Elemental\Forms\TextCheckboxGroupField;
7
use DNADesign\Elemental\ORM\FieldType\DBObjectType;
8
use Exception;
9
use SilverStripe\CMS\Controllers\CMSPageEditController;
10
use SilverStripe\CMS\Model\SiteTree;
11
use SilverStripe\Control\Controller;
12
use SilverStripe\Control\Director;
13
use SilverStripe\Core\ClassInfo;
14
use SilverStripe\Core\Injector\Injector;
15
use SilverStripe\Forms\DropdownField;
16
use SilverStripe\Forms\FieldList;
17
use SilverStripe\Forms\HiddenField;
18
use SilverStripe\Forms\NumericField;
19
use SilverStripe\Forms\TextField;
20
use SilverStripe\GraphQL\Scaffolding\StaticSchema;
21
use SilverStripe\GraphQL\Schema\Exception\SchemaBuilderException;
0 ignored issues
show
Bug introduced by
The type SilverStripe\GraphQL\Sch...\SchemaBuilderException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
use SilverStripe\ORM\DataObject;
23
use SilverStripe\ORM\FieldType\DBBoolean;
24
use SilverStripe\ORM\FieldType\DBField;
25
use SilverStripe\ORM\FieldType\DBHTMLText;
26
use SilverStripe\ORM\ValidationException;
27
use SilverStripe\Security\Member;
28
use SilverStripe\Security\Permission;
29
use SilverStripe\Versioned\Versioned;
30
use SilverStripe\VersionedAdmin\Forms\HistoryViewerField;
31
use SilverStripe\View\ArrayData;
32
use SilverStripe\View\Parsers\URLSegmentFilter;
33
use SilverStripe\View\Requirements;
34
use SilverStripe\ORM\CMSPreviewable;
35
36
/**
37
 * Class BaseElement
38
 * @package DNADesign\Elemental\Models
39
 *
40
 * @property string $Title
41
 * @property bool $ShowTitle
42
 * @property int $Sort
43
 * @property string $ExtraClass
44
 * @property string $Style
45
 * @property int $ParentID
46
 *
47
 * @method ElementalArea Parent()
48
 *
49
 * @mixin Versioned
50
 */
51
class BaseElement extends DataObject implements CMSPreviewable
52
{
53
    /**
54
     * Override this on your custom elements to specify a CSS icon class
55
     *
56
     * @var string
57
     */
58
    private static $icon = 'font-icon-block-layout';
0 ignored issues
show
introduced by
The private property $icon is not used, and could be removed.
Loading history...
59
60
    /**
61
     * Describe the purpose of this element
62
     *
63
     * @config
64
     * @var string
65
     */
66
    private static $description = 'Base element class';
0 ignored issues
show
introduced by
The private property $description is not used, and could be removed.
Loading history...
67
68
    private static $db = [
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
69
        'Title' => 'Varchar(255)',
70
        'ShowTitle' => 'Boolean',
71
        'Sort' => 'Int',
72
        'ExtraClass' => 'Varchar(255)',
73
        'Style' => 'Varchar(255)'
74
    ];
75
76
    private static $has_one = [
0 ignored issues
show
introduced by
The private property $has_one is not used, and could be removed.
Loading history...
77
        'Parent' => ElementalArea::class
78
    ];
79
80
    private static $extensions = [
0 ignored issues
show
introduced by
The private property $extensions is not used, and could be removed.
Loading history...
81
        Versioned::class
82
    ];
83
84
    private static $casting = [
0 ignored issues
show
introduced by
The private property $casting is not used, and could be removed.
Loading history...
85
        'BlockSchema' => DBObjectType::class,
86
        'IsLiveVersion' => DBBoolean::class,
87
        'IsPublished' => DBBoolean::class,
88
        'canCreate' => DBBoolean::class,
89
        'canPublish' => DBBoolean::class,
90
        'canUnpublish' => DBBoolean::class,
91
        'canDelete' => DBBoolean::class,
92
    ];
93
94
    private static $indexes = [
0 ignored issues
show
introduced by
The private property $indexes is not used, and could be removed.
Loading history...
95
        'Sort' => true,
96
    ];
97
98
    private static $versioned_gridfield_extensions = true;
0 ignored issues
show
introduced by
The private property $versioned_gridfield_extensions is not used, and could be removed.
Loading history...
99
100
    private static $table_name = 'Element';
0 ignored issues
show
introduced by
The private property $table_name is not used, and could be removed.
Loading history...
101
102
    /**
103
     * @var string
104
     */
105
    private static $controller_class = ElementController::class;
106
107
    /**
108
     * @var string
109
     */
110
    private static $controller_template = 'ElementHolder';
0 ignored issues
show
introduced by
The private property $controller_template is not used, and could be removed.
Loading history...
111
112
    /**
113
     * @var ElementController
114
     */
115
    protected $controller;
116
117
    private static $show_stage_link = true;
0 ignored issues
show
introduced by
The private property $show_stage_link is not used, and could be removed.
Loading history...
118
119
    private static $show_live_link = true;
0 ignored issues
show
introduced by
The private property $show_live_link is not used, and could be removed.
Loading history...
120
121
    /**
122
     * Cache various data to improve CMS load time
123
     *
124
     * @internal
125
     * @var array
126
     */
127
    protected $cacheData;
128
129
    private static $default_sort = 'Sort';
0 ignored issues
show
introduced by
The private property $default_sort is not used, and could be removed.
Loading history...
130
131
    private static $singular_name = 'block';
0 ignored issues
show
introduced by
The private property $singular_name is not used, and could be removed.
Loading history...
132
133
    private static $plural_name = 'blocks';
0 ignored issues
show
introduced by
The private property $plural_name is not used, and could be removed.
Loading history...
134
135
    private static $summary_fields = [
0 ignored issues
show
introduced by
The private property $summary_fields is not used, and could be removed.
Loading history...
136
        'EditorPreview' => 'Summary'
137
    ];
138
139
    /**
140
     * @config
141
     * @var array
142
     */
143
    private static $styles = [];
0 ignored issues
show
introduced by
The private property $styles is not used, and could be removed.
Loading history...
144
145
    private static $searchable_fields = [
0 ignored issues
show
introduced by
The private property $searchable_fields is not used, and could be removed.
Loading history...
146
        'ID' => [
147
            'field' => NumericField::class,
148
        ],
149
        'Title',
150
        'LastEdited'
151
    ];
152
153
    /**
154
     * Enable for backwards compatibility
155
     *
156
     * @var boolean
157
     */
158
    private static $disable_pretty_anchor_name = false;
159
160
    /**
161
     * Set to false to prevent an in-line edit form from showing in an elemental area. Instead the element will be
162
     * clickable and a GridFieldDetailForm will be used.
163
     *
164
     * @config
165
     * @var bool
166
     */
167
    private static $inline_editable = true;
0 ignored issues
show
introduced by
The private property $inline_editable is not used, and could be removed.
Loading history...
168
169
    /**
170
     * Display a show title button
171
     *
172
     * @config
173
     * @var boolean
174
     */
175
    private static $displays_title_in_template = true;
0 ignored issues
show
introduced by
The private property $displays_title_in_template is not used, and could be removed.
Loading history...
176
177
    /**
178
     * Determines whether a block should be indexable in search.
179
     *
180
     * @config
181
     * @var boolean
182
     * @see ElementalPageExtension::getElementsForSearch()
183
     */
184
    private static $search_indexable = true;
0 ignored issues
show
introduced by
The private property $search_indexable is not used, and could be removed.
Loading history...
185
186
    /**
187
     * Store used anchor names, this is to avoid title clashes
188
     * when calling 'getAnchor'
189
     *
190
     * @var array
191
     */
192
    protected static $used_anchors = [];
193
194
    /**
195
     * For caching 'getAnchor'
196
     *
197
     * @var string
198
     */
199
    protected $anchor = null;
200
201
    /**
202
     * Basic permissions, defaults to page perms where possible.
203
     *
204
     * @param Member $member
205
     * @return boolean
206
     */
207
    public function canView($member = null)
208
    {
209
        $extended = $this->extendedCan(__FUNCTION__, $member);
210
        if ($extended !== null) {
211
            return $extended;
212
        }
213
214
        if ($this->hasMethod('getPage')) {
215
            if ($page = $this->getPage()) {
216
                return $page->canView($member);
217
            }
218
        }
219
220
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
221
    }
222
223
    /**
224
     * Basic permissions, defaults to page perms where possible.
225
     *
226
     * @param Member $member
227
     *
228
     * @return boolean
229
     */
230
    public function canEdit($member = null)
231
    {
232
        $extended = $this->extendedCan(__FUNCTION__, $member);
233
        if ($extended !== null) {
234
            return $extended;
235
        }
236
237
        if ($this->hasMethod('getPage')) {
238
            if ($page = $this->getPage()) {
239
                return $page->canEdit($member);
240
            }
241
        }
242
243
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
244
    }
245
246
    /**
247
     * Basic permissions, defaults to page perms where possible.
248
     *
249
     * Uses archive not delete so that current stage is respected i.e if a
250
     * element is not published, then it can be deleted by someone who doesn't
251
     * have publishing permissions.
252
     *
253
     * @param Member $member
254
     *
255
     * @return boolean
256
     */
257
    public function canDelete($member = null)
258
    {
259
        $extended = $this->extendedCan(__FUNCTION__, $member);
260
261
        if ($extended !== null) {
262
            return $extended;
263
        }
264
265
        if ($this->hasMethod('getPage')) {
266
            if ($page = $this->getPage()) {
267
                if ($page->hasExtension(Versioned::class)) {
268
                    return $page->canArchive($member);
269
                } else {
270
                    return $page->canDelete($member);
271
                }
272
            }
273
        }
274
275
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
276
    }
277
278
    /**
279
     * Basic permissions, defaults to page perms where possible.
280
     *
281
     * @param Member $member
282
     * @param array $context
283
     *
284
     * @return boolean
285
     */
286
    public function canCreate($member = null, $context = array())
287
    {
288
        $extended = $this->extendedCan(__FUNCTION__, $member);
289
        if ($extended !== null) {
290
            return $extended;
291
        }
292
293
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
294
    }
295
296
    /**
297
     * Increment the sort order if one hasn't been already defined. This
298
     * ensures that new elements are created at the end of the list by default.
299
     *
300
     * {@inheritDoc}
301
     */
302
    public function onBeforeWrite()
303
    {
304
        parent::onBeforeWrite();
305
306
        // If a Sort has already been set, then we can exit early
307
        if ($this->Sort) {
308
            return;
309
        }
310
311
        // If no ParentID is currently set for the Element, then we don't want to define an initial Sort yet
312
        if (!$this->ParentID) {
313
            return;
314
        }
315
316
        if ($this->hasExtension(Versioned::class)) {
317
            $records = Versioned::get_by_stage(BaseElement::class, Versioned::DRAFT);
318
        } else {
319
            $records = BaseElement::get();
320
        }
321
322
        $records = $records->filter('ParentID', $this->ParentID);
323
324
        $this->Sort = $records->max('Sort') + 1;
325
    }
326
327
    public function getCMSFields()
328
    {
329
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
330
            // Remove relationship fields
331
            $fields->removeByName('ParentID');
332
            $fields->removeByName('Sort');
333
334
            // Remove link and file tracking tabs
335
            $fields->removeByName(['LinkTracking', 'FileTracking']);
336
337
            $fields->addFieldToTab(
338
                'Root.Settings',
339
                TextField::create('ExtraClass', _t(__CLASS__ . '.ExtraCssClassesLabel', 'Custom CSS classes'))
340
                    ->setAttribute(
341
                        'placeholder',
342
                        _t(__CLASS__ . '.ExtraCssClassesPlaceholder', 'my_class another_class')
343
                    )
344
            );
345
346
            // Rename the "Settings" tab
347
            $fields->fieldByName('Root.Settings')
0 ignored issues
show
Bug introduced by
Are you sure the usage of $fields->fieldByName('Root.Settings') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

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

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

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

Loading history...
348
                ->setTitle(_t(__CLASS__ . '.SettingsTabLabel', 'Settings'));
349
350
            // Add a combined field for "Title" and "Displayed" checkbox in a Bootstrap input group
351
            $fields->removeByName('ShowTitle');
352
353
            if ($this->config()->get('displays_title_in_template')) {
354
                $fields->replaceField(
355
                    'Title',
356
                    TextCheckboxGroupField::create()
357
                        ->setName('Title')
358
                );
359
            }
360
361
            // Rename the "Main" tab
362
            $fields->fieldByName('Root.Main')
0 ignored issues
show
Bug introduced by
Are you sure the usage of $fields->fieldByName('Root.Main') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

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

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

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

Loading history...
363
                ->setTitle(_t(__CLASS__ . '.MainTabLabel', 'Content'));
364
365
            $fields->addFieldsToTab('Root.Main', [
366
                HiddenField::create('AbsoluteLink', false, Director::absoluteURL($this->PreviewLink())),
367
                HiddenField::create('LiveLink', false, Director::absoluteURL($this->Link())),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->Link() targeting DNADesign\Elemental\Models\BaseElement::Link() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

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

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

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

Loading history...
368
                HiddenField::create('StageLink', false, Director::absoluteURL($this->PreviewLink())),
369
            ]);
370
371
            $styles = $this->config()->get('styles');
372
373
            if ($styles && count($styles ?? []) > 0) {
374
                $styleDropdown = DropdownField::create('Style', _t(__CLASS__.'.STYLE', 'Style variation'), $styles);
375
376
                $fields->insertBefore($styleDropdown, 'ExtraClass');
0 ignored issues
show
Bug introduced by
'ExtraClass' of type string is incompatible with the type SilverStripe\Forms\FormField expected by parameter $item of SilverStripe\Forms\FieldList::insertBefore(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

376
                $fields->insertBefore($styleDropdown, /** @scrutinizer ignore-type */ 'ExtraClass');
Loading history...
377
378
                $styleDropdown->setEmptyString(_t(__CLASS__.'.CUSTOM_STYLES', 'Select a style..'));
379
            } else {
380
                $fields->removeByName('Style');
381
            }
382
383
            // Hide the navigation section of the tabs in the React component {@see silverstripe/admin Tabs}
384
            $rootTabset = $fields->fieldByName('Root');
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $rootTabset is correct as $fields->fieldByName('Root') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

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

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

}

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

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

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

Loading history...
385
            $rootTabset->setSchemaState(['hideNav' => true]);
386
387
            if ($this->isInDB()) {
388
                $fields->addFieldsToTab('Root.History', [
389
                    HistoryViewerField::create('ElementHistory')
390
                        ->addExtraClass('history-viewer--standalone'),
391
                ]);
392
                // Add class to containing tab
393
                $fields->fieldByName('Root.History')
0 ignored issues
show
Bug introduced by
Are you sure the usage of $fields->fieldByName('Root.History') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

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

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

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

Loading history...
394
                    ->addExtraClass('elemental-block__history-tab tab--history-viewer');
395
396
                // Hack: automatically navigate to the History tab with `#Root_History` is in the URL
397
                // To unhack, fix this: https://github.com/silverstripe/silverstripe-admin/issues/911
398
                Requirements::customScript(<<<JS
399
    document.addEventListener('DOMContentLoaded', () => {
400
        var hash = window.location.hash.substr(1);
401
        if (hash !== 'Root_History') {
402
            return null;
403
        }
404
        jQuery('.cms-tabset-nav-primary li[aria-controls="Root_History"] a').trigger('click')
405
    });
406
JS
407
                );
408
            }
409
        });
410
411
        return parent::getCMSFields();
412
    }
413
414
    /**
415
     * Get the type of the current block, for use in GridField summaries, block
416
     * type dropdowns etc. Examples are "Content", "File", "Media", etc.
417
     *
418
     * @return string
419
     */
420
    public function getType()
421
    {
422
        $default = $this->i18n_singular_name() ?: 'Block';
423
424
        return _t(__CLASS__ . '.BlockType', $default);
425
    }
426
427
    /**
428
     * Proxy through to configuration setting 'inline_editable'
429
     *
430
     * @return bool
431
     */
432
    public function inlineEditable()
433
    {
434
        return static::config()->get('inline_editable');
435
    }
436
437
    /**
438
     * @param ElementController $controller
439
     *
440
     * @return $this
441
     */
442
    public function setController($controller)
443
    {
444
        $this->controller = $controller;
445
446
        return $this;
447
    }
448
449
    /**
450
     * @throws Exception If the specified controller class doesn't exist
451
     *
452
     * @return ElementController
453
     */
454
    public function getController()
455
    {
456
        if ($this->controller) {
457
            return $this->controller;
458
        }
459
460
        $controllerClass = self::config()->controller_class;
461
462
        if (!class_exists($controllerClass ?? '')) {
463
            throw new Exception(
464
                'Could not find controller class ' . $controllerClass . ' as defined in ' . static::class
465
            );
466
        }
467
468
        $this->controller = Injector::inst()->create($controllerClass, $this);
469
        $this->controller->doInit();
470
471
        return $this->controller;
472
    }
473
474
    /**
475
     * @param string $name
476
     * @return $this
477
     */
478
    public function setAreaRelationNameCache($name)
479
    {
480
        $this->cacheData['area_relation_name'] = $name;
481
482
        return $this;
483
    }
484
485
    /**
486
     * @return Controller
487
     */
488
    public function Top()
489
    {
490
        return (Controller::has_curr()) ? Controller::curr() : null;
491
    }
492
493
    /**
494
     * Determines whether this elemental block is indexable in search.
495
     *
496
     * By default, this uses the configurable variable search_indexable, but
497
     * this method can be overridden to provide more complex logic if required.
498
     *
499
     * @return boolean
500
     */
501
    public function getSearchIndexable(): bool
502
    {
503
        return (bool) $this->config()->get('search_indexable');
504
    }
505
506
    /**
507
     * Provides content to be indexed in search.
508
     *
509
     * @return string
510
     */
511
    public function getContentForSearchIndex(): string
512
    {
513
        // Strips tags but be sure there's a space between words.
514
        $content = trim(strip_tags(str_replace('<', ' <', $this->forTemplate() ?? '') ?? ''));
515
        // Allow projects to update indexable content of third-party elements.
516
        $this->extend('updateContentForSearchIndex', $content);
517
        return $content;
518
    }
519
520
    /**
521
     * Default way to render element in templates. Note that all blocks should
522
     * be rendered through their {@link ElementController} class as this
523
     * contains the holder styles.
524
     *
525
     * @return string|null HTML
526
     */
527
    public function forTemplate($holder = true)
528
    {
529
        $templates = $this->getRenderTemplates();
530
531
        if ($templates) {
532
            return $this->renderWith($templates);
533
        }
534
535
        return null;
536
    }
537
538
    /**
539
     * @param string $suffix
540
     *
541
     * @return array
542
     */
543
    public function getRenderTemplates($suffix = '')
544
    {
545
        $classes = ClassInfo::ancestry($this->ClassName);
546
        $classes[static::class] = static::class;
547
        $classes = array_reverse($classes ?? []);
548
        $templates = [];
549
550
        foreach ($classes as $key => $class) {
551
            if ($class == BaseElement::class) {
552
                continue;
553
            }
554
555
            if ($class == DataObject::class) {
556
                break;
557
            }
558
559
            if ($style = $this->Style) {
560
                $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName() . '_' . $style;
561
                $templates[$class][] = $class . $suffix . '_' . $style;
562
            }
563
            $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName();
564
            $templates[$class][] = $class . $suffix;
565
        }
566
567
        $this->extend('updateRenderTemplates', $templates, $suffix);
568
569
        $templateFlat = [];
570
        foreach ($templates as $class => $variations) {
571
            $templateFlat = array_merge($templateFlat, $variations);
572
        }
573
574
        return $templateFlat;
575
    }
576
577
    /**
578
     * Given form data (wit
579
     *
580
     * @param $data
581
     */
582
    public function updateFromFormData($data)
583
    {
584
        $cmsFields = $this->getCMSFields();
585
586
        foreach ($data as $field => $datum) {
587
            $field = $cmsFields->dataFieldByName($field);
588
589
            if (!$field) {
590
                continue;
591
            }
592
593
            $field->setSubmittedValue($datum);
594
            $field->saveInto($this);
595
        }
596
    }
597
598
    /**
599
     * Strip all namespaces from class namespace.
600
     *
601
     * @param string $classname e.g. "\Fully\Namespaced\Class"
602
     *
603
     * @return string following the param example, "Class"
604
     */
605
    protected function stripNamespacing($classname)
606
    {
607
        $classParts = explode('\\', $classname ?? '');
608
        return array_pop($classParts);
609
    }
610
611
    /**
612
     * @return string
613
     */
614
    public function getSimpleClassName()
615
    {
616
        return strtolower($this->sanitiseClassName($this->ClassName, '__') ?? '');
617
    }
618
619
    /**
620
     * Despite the name of the method, getPage can return any type of DataObject
621
     * @return null|DataObject
622
     * @throws \Psr\Container\NotFoundExceptionInterface
623
     * @throws \SilverStripe\ORM\ValidationException
624
     */
625
    public function getPage()
626
    {
627
        // Allow for repeated calls to be cached
628
        if (isset($this->cacheData['page'])) {
629
            return $this->cacheData['page'];
630
        }
631
632
        $class = DataObject::getSchema()->hasOneComponent($this, 'Parent');
633
        $area = ($this->ParentID) ? DataObject::get_by_id($class, $this->ParentID) : null;
634
        if ($area instanceof ElementalArea && $area->exists()) {
635
            $this->cacheData['page'] = $area->getOwnerPage();
636
            return $this->cacheData['page'];
637
        }
638
639
        return null;
640
    }
641
642
    /**
643
     * Get a unique anchor name
644
     *
645
     * @return string
646
     */
647
    public function getAnchor()
648
    {
649
        if ($this->anchor !== null) {
650
            return $this->anchor;
651
        }
652
653
        $anchorTitle = '';
654
655
        if (!$this->config()->disable_pretty_anchor_name) {
656
            if ($this->hasMethod('getAnchorTitle')) {
657
                $anchorTitle = $this->getAnchorTitle();
0 ignored issues
show
Bug introduced by
The method getAnchorTitle() does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

657
                /** @scrutinizer ignore-call */ 
658
                $anchorTitle = $this->getAnchorTitle();
Loading history...
658
            } elseif ($this->config()->enable_title_in_template) {
659
                $anchorTitle = $this->getField('Title');
660
            }
661
        }
662
663
        if (!$anchorTitle) {
664
            $anchorTitle = 'e'.$this->ID;
665
        }
666
667
        $filter = URLSegmentFilter::create();
668
        $titleAsURL = $filter->filter($anchorTitle);
669
670
        // Ensure that this anchor name isn't already in use
671
        // ie. If two elemental blocks have the same title, it'll append '-2', '-3'
672
        $result = $titleAsURL;
673
        $count = 1;
674
        while (isset(self::$used_anchors[$result]) && self::$used_anchors[$result] !== $this->ID) {
675
            ++$count;
676
            $result = $titleAsURL . '-' . $count;
677
        }
678
        self::$used_anchors[$result] = $this->ID;
679
        return $this->anchor = $result;
680
    }
681
682
    /**
683
     * Get anchors in this block's content.
684
     * Used to populate the "anchor on a page" link in the WYSIWYG
685
     *
686
     * By default, this finds anchors in any HTMLText field on the block, but
687
     * this method should be overridden if anchors are provided in other ways
688
     * for this block or if not all HTMLText fields for this block are
689
     * displayed on the front-end.
690
     */
691
    public function getAnchorsInContent(): array
692
    {
693
        $anchors = [$this->getAnchor()];
694
        $anchorRegex = "/\\s+(name|id)\\s*=\\s*([\"'])([^\\2\\s>]*?)\\2|\\s+(name|id)\\s*=\\s*([^\"']+)[\\s +>]/im";
695
        $allFields = DataObject::getSchema()->fieldSpecs($this);
696
        foreach ($allFields as $field => $fieldSpec) {
697
            $fieldObj = $this->owner->dbObject($field);
0 ignored issues
show
Bug introduced by
The method dbObject() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

697
            /** @scrutinizer ignore-call */ 
698
            $fieldObj = $this->owner->dbObject($field);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug Best Practice introduced by
The property owner does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __get, consider adding a @property annotation.
Loading history...
698
            if ($fieldObj instanceof DBHTMLText) {
699
                $parseSuccess = preg_match_all($anchorRegex, $fieldObj->getValue() ?? '', $matches);
700
                if ($parseSuccess >= 1) {
701
                    $fieldAnchors = array_values(array_filter(
702
                        array_merge($matches[3], $matches[5])
703
                    ));
704
                    $anchors = array_unique(array_merge($anchors, $fieldAnchors));
705
                }
706
            }
707
        }
708
709
        $this->extend('updateAnchorsInContent', $anchors);
710
        return $anchors;
711
    }
712
713
    /**
714
     * @param string|null $action
715
     * @return string|null
716
     * @throws \Psr\Container\NotFoundExceptionInterface
717
     * @throws \SilverStripe\ORM\ValidationException
718
     */
719
    public function AbsoluteLink($action = null)
720
    {
721
        $page = $this->getPage();
722
723
        if ($page && ClassInfo::hasMethod($page, 'AbsoluteLink')) {
724
            $link = $page->AbsoluteLink($action) . '#' . $this->getAnchor();
725
            $this->extend('updateAbsoluteLink', $link);
726
727
            return $link;
728
        }
729
730
        return null;
731
    }
732
733
    /**
734
     * @param string|null $action
735
     * @return string|null
736
     * @throws \Psr\Container\NotFoundExceptionInterface
737
     * @throws \SilverStripe\ORM\ValidationException
738
     */
739
    public function Link($action = null)
740
    {
741
        $page = $this->getPage();
742
743
        if ($page && ClassInfo::hasMethod($page, 'Link')) {
744
            $link = $page->Link($action) . '#' . $this->getAnchor();
745
            $this->extend('updateLink', $link);
746
747
            return $link;
748
        }
749
750
        return null;
751
    }
752
753
    /**
754
     * @param string|null $action
755
     * @return string|null
756
     * @throws \Psr\Container\NotFoundExceptionInterface
757
     * @throws \SilverStripe\ORM\ValidationException
758
     */
759
    public function PreviewLink($action = null)
760
    {
761
        $link = null;
762
        if ($page = $this->getPage()) {
763
            if (ClassInfo::hasMethod($page, 'Link')) {
764
                $link = $page->Link($action);
765
            }
766
            if (!$link && ($page instanceof CMSPreviewable)) {
767
                $link = $page->PreviewLink($action);
768
            }
769
            if ($link) {
770
                // The ElementalPreview getvar is used in ElementalPageExtension
771
                // The anchor must be at the end of the URL to function correctly
772
                $link .= '?ElementalPreview=' . mt_rand() . '#' . $this->getAnchor();
773
            }
774
        }
775
776
        $this->extend('updatePreviewLink', $link);
777
        return $link;
778
    }
779
780
    /**
781
     * @return boolean
782
     */
783
    public function isCMSPreview()
784
    {
785
        if (Controller::has_curr()) {
786
            $controller = Controller::curr();
787
788
            if ($controller->getRequest()->requestVar('CMSPreview')) {
789
                return true;
790
            }
791
        }
792
793
        return false;
794
    }
795
796
    /**
797
     * @param bool $directLink Indicates that the GridFieldDetailEdit form link should be given even if the block can be
798
     *                         edited in-line.
799
     * @return null|string
800
     * @throws \SilverStripe\ORM\ValidationException
801
     */
802
    public function CMSEditLink($directLink = false)
803
    {
804
        // Allow for repeated calls to be returned from cache
805
        if (isset($this->cacheData['cms_edit_link'])) {
806
            return $this->cacheData['cms_edit_link'];
807
        }
808
809
        $link = $this->getElementCMSLink($directLink);
810
        $this->extend('updateCMSEditLink', $link);
811
812
        if ($link) {
813
            $this->cacheData['cms_edit_link'] = $link;
814
        }
815
816
        return $link;
817
    }
818
819
    /**
820
     * @param bool $directLink
821
     * @return null|string
822
     */
823
    private function getElementCMSLink(bool $directLink)
824
    {
825
        $relationName = $this->getAreaRelationName();
826
        $page = $this->getPage();
827
828
        $link = null;
829
830
        if (!$page) {
831
            return $link;
832
        }
833
834
        if ($page instanceof SiteTree) {
835
            $link = $page->CMSEditLink();
836
        } elseif (ClassInfo::hasMethod($page, 'CMSEditLink')) {
837
            $link = Controller::join_links($page->CMSEditLink(), 'ItemEditForm');
838
        }
839
        // In-line editable blocks should just take you to the page.
840
        // Editable ones should add the suffix for detail form.
841
        if (!$this->inlineEditable() || $directLink) {
842
            if ($page instanceof SiteTree) {
843
                return Controller::join_links(
844
                    singleton(CMSPageEditController::class)->Link('EditForm'),
845
                    $page->ID,
846
                    'field',
847
                    $relationName,
848
                    'item',
849
                    $this->ID,
850
                    'edit'
851
                );
852
            } else {
853
                // If $page is not a Page, then generate $link base on $page->CMSEditLink()
854
                return Controller::join_links(
855
                    $link,
856
                    'field',
857
                    $relationName,
858
                    'item',
859
                    $this->ID,
860
                    'edit'
861
                );
862
            }
863
        }
864
865
        return $link;
866
    }
867
868
    /**
869
     * Retrieve a elemental area relation for creating cms links
870
     *
871
     * @return int|string The name of a valid elemental area relation
872
     * @throws \Psr\Container\NotFoundExceptionInterface
873
     * @throws \SilverStripe\ORM\ValidationException
874
     */
875
    public function getAreaRelationName()
876
    {
877
        // Allow repeated calls to return from internal cache
878
        if (isset($this->cacheData['area_relation_name'])) {
879
            return $this->cacheData['area_relation_name'];
880
        }
881
882
        $page = $this->getPage();
883
884
        $result = 'ElementalArea';
885
886
        if ($page) {
887
            $has_one = $page->config()->get('has_one');
888
            $area = $this->Parent();
889
890
            foreach ($has_one as $relationName => $relationClass) {
891
                if ($page instanceof BaseElement && $relationName === 'Parent') {
892
                    continue;
893
                }
894
                if ($relationClass === $area->ClassName && $page->{$relationName}()->ID === $area->ID) {
895
                    $result = $relationName;
896
                    break;
897
                }
898
            }
899
        }
900
901
        $this->setAreaRelationNameCache($result);
902
903
        return $result;
904
    }
905
906
    /**
907
     * Sanitise a model class' name for inclusion in a link.
908
     *
909
     * @return string
910
     */
911
    public function sanitiseClassName($class, $delimiter = '-')
912
    {
913
        return str_replace('\\', $delimiter ?? '', $class ?? '');
914
    }
915
916
    public function unsanitiseClassName($class, $delimiter = '-')
917
    {
918
        return str_replace($delimiter ?? '', '\\', $class ?? '');
919
    }
920
921
    /**
922
     * @return null|string
923
     * @throws \Psr\Container\NotFoundExceptionInterface
924
     * @throws \SilverStripe\ORM\ValidationException
925
     */
926
    public function getEditLink()
927
    {
928
        return Director::absoluteURL($this->CMSEditLink());
929
    }
930
931
    /**
932
     * @return DBField|null
933
     * @throws \Psr\Container\NotFoundExceptionInterface
934
     * @throws \SilverStripe\ORM\ValidationException
935
     */
936
    public function PageCMSEditLink()
937
    {
938
        if ($page = $this->getPage()) {
939
            return DBField::create_field('HTMLText', sprintf(
940
                '<a href="%s">%s</a>',
941
                $page->CMSEditLink(),
942
                $page->Title
943
            ));
944
        }
945
946
        return null;
947
    }
948
949
    /**
950
     * @return string
951
     */
952
    public function getMimeType()
953
    {
954
        return 'text/html';
955
    }
956
957
    /**
958
     * This can be overridden on child elements to create a summary for display
959
     * in GridFields.
960
     *
961
     * @return string
962
     */
963
    public function getSummary()
964
    {
965
        return '';
966
    }
967
968
    /**
969
     * The block config defines a set of data (usually set through config on the element) that will be made available in
970
     * client side config. Individual element types may choose to add config variable for use in React code
971
     *
972
     * @return array
973
     */
974
    public static function getBlockConfig()
975
    {
976
        return [];
977
    }
978
979
    /**
980
     * The block actions is an associative array available for providing data to the client side to be used to describe
981
     * actions that may be performed. This is available as a plain "ObjectType" in the GraphQL schema.
982
     *
983
     * By default the only action is "edit" which is simply the URL where the block may be edited.
984
     *
985
     * To modify the actions, either use the extension point or overload the `provideBlockSchema` method.
986
     *
987
     * @internal This API may change in future. Treat this as a `final` method.
988
     * @return array
989
     */
990
    public function getBlockSchema()
991
    {
992
        $blockSchema = $this->provideBlockSchema();
993
994
        $this->extend('updateBlockSchema', $blockSchema);
995
996
        return $blockSchema;
997
    }
998
999
    /**
1000
     * Provide block schema data, which will be serialised and sent via GraphQL to the editor client.
1001
     *
1002
     * Overload this method in child element classes to augment, or use the extension point on `getBlockSchema`
1003
     * to update it from an `Extension`.
1004
     *
1005
     * @return array
1006
     * @throws SchemaBuilderException
1007
     * @throws ValidationException
1008
     */
1009
    protected function provideBlockSchema()
1010
    {
1011
        return [
1012
            'typeName' => static::getGraphQLTypeName(),
1013
            'actions' => [
1014
                'edit' => $this->getEditLink(),
1015
            ],
1016
        ];
1017
    }
1018
1019
    /**
1020
     * Generate markup for element type icons suitable for use in GridFields.
1021
     *
1022
     * @return null|DBHTMLText
1023
     */
1024
    public function getIcon()
1025
    {
1026
        $data = ArrayData::create([]);
1027
1028
        $iconClass = $this->config()->get('icon');
1029
        if ($iconClass) {
1030
            $data->IconClass = $iconClass;
1031
1032
            // Add versioned states (rendered as a circle over the icon)
1033
            if ($this->hasExtension(Versioned::class)) {
1034
                $data->IsVersioned = true;
1035
                if ($this->isOnDraftOnly()) {
0 ignored issues
show
Bug introduced by
The method isOnDraftOnly() does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1035
                if ($this->/** @scrutinizer ignore-call */ isOnDraftOnly()) {
Loading history...
1036
                    $data->VersionState = 'draft';
1037
                    $data->VersionStateTitle = _t(
1038
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.ADDEDTODRAFTHELP',
1039
                        'Item has not been published yet'
1040
                    );
1041
                } elseif ($this->isModifiedOnDraft()) {
0 ignored issues
show
Bug introduced by
The method isModifiedOnDraft() does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1041
                } elseif ($this->/** @scrutinizer ignore-call */ isModifiedOnDraft()) {
Loading history...
1042
                    $data->VersionState = 'modified';
1043
                    $data->VersionStateTitle = $data->VersionStateTitle = _t(
1044
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.MODIFIEDONDRAFTHELP',
1045
                        'Item has unpublished changes'
1046
                    );
1047
                }
1048
            }
1049
1050
            return $data->renderWith(__CLASS__ . '/PreviewIcon');
1051
        }
1052
1053
        return null;
1054
    }
1055
1056
    /**
1057
     * Get a description for this content element, if available
1058
     *
1059
     * @return string
1060
     */
1061
    public function getDescription()
1062
    {
1063
        $description = $this->config()->uninherited('description');
1064
        if ($description) {
1065
            return _t(__CLASS__ . '.Description', $description);
1066
        }
1067
        return '';
1068
    }
1069
1070
    /**
1071
     * Generate markup for element type, with description suitable for use in
1072
     * GridFields.
1073
     *
1074
     * @return DBField
1075
     */
1076
    public function getTypeNice()
1077
    {
1078
        $description = $this->getDescription();
1079
        $desc = ($description) ? ' <span class="element__note"> &mdash; ' . $description . '</span>' : '';
1080
1081
        return DBField::create_field(
1082
            'HTMLVarchar',
1083
            $this->getType() . $desc
1084
        );
1085
    }
1086
1087
    /**
1088
     * @return \SilverStripe\ORM\FieldType\DBHTMLText
1089
     */
1090
    public function getEditorPreview()
1091
    {
1092
        $templates = $this->getRenderTemplates('_EditorPreview');
1093
        $templates[] = BaseElement::class . '_EditorPreview';
1094
1095
        return $this->renderWith($templates);
1096
    }
1097
1098
    /**
1099
     * @return Member
1100
     */
1101
    public function getAuthor()
1102
    {
1103
        if ($this->AuthorID) {
0 ignored issues
show
Bug Best Practice introduced by
The property AuthorID does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __get, consider adding a @property annotation.
Loading history...
1104
            return Member::get()->byId($this->AuthorID);
1105
        }
1106
1107
        return null;
1108
    }
1109
1110
    /**
1111
     * Get a user defined style variant for this element, if available
1112
     *
1113
     * @return string
1114
     */
1115
    public function getStyleVariant()
1116
    {
1117
        $style = $this->Style;
1118
        $styles = $this->config()->get('styles');
1119
1120
        if (isset($styles[$style])) {
1121
            $style = strtolower($style ?? '');
1122
        } else {
1123
            $style = '';
1124
        }
1125
1126
        $this->extend('updateStyleVariant', $style);
1127
1128
        return $style;
1129
    }
1130
1131
    /**
1132
     * @return mixed|null
1133
     * @throws \Psr\Container\NotFoundExceptionInterface
1134
     * @throws \SilverStripe\ORM\ValidationException
1135
     */
1136
    public function getPageTitle()
1137
    {
1138
        $page = $this->getPage();
1139
1140
        if ($page) {
1141
            return $page->Title;
1142
        }
1143
1144
        return null;
1145
    }
1146
1147
    /**
1148
     * @return boolean
1149
     */
1150
    public function First()
1151
    {
1152
        return ($this->Parent()->Elements()->first()->ID === $this->ID);
1153
    }
1154
1155
    /**
1156
     * @return boolean
1157
     */
1158
    public function Last()
1159
    {
1160
        return ($this->Parent()->Elements()->last()->ID === $this->ID);
1161
    }
1162
1163
    /**
1164
     * @return int
1165
     */
1166
    public function TotalItems()
1167
    {
1168
        return $this->Parent()->Elements()->count();
1169
    }
1170
1171
    /**
1172
     * Returns the position of the current element.
1173
     *
1174
     * @return int
1175
     */
1176
    public function Pos()
1177
    {
1178
        return ($this->Parent()->Elements()->filter('Sort:LessThan', $this->Sort)->count() + 1);
1179
    }
1180
1181
    /**
1182
     * @return string
1183
     */
1184
    public function EvenOdd()
1185
    {
1186
        $odd = (bool) ($this->Pos() % 2);
1187
1188
        return  ($odd) ? 'odd' : 'even';
1189
    }
1190
1191
    /**
1192
     * @return string
1193
     */
1194
    public static function getGraphQLTypeName(): string
1195
    {
1196
        return class_exists(StaticSchema::class)
1197
            ? StaticSchema::inst()->typeNameForDataObject(static::class)
1198
            : str_replace('\\', '_', static::class);
1199
    }
1200
}
1201