Passed
Push — 4 ( 529852...87c3e3 )
by Steve
10:18
created

BaseElement::getContentForSearchIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 7
rs 10
c 0
b 0
f 0
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
        if ($extended !== null) {
261
            return $extended;
262
        }
263
264
        if ($this->hasMethod('getPage')) {
265
            if ($page = $this->getPage()) {
266
                return $page->canArchive($member);
267
            }
268
        }
269
270
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
271
    }
272
273
    /**
274
     * Basic permissions, defaults to page perms where possible.
275
     *
276
     * @param Member $member
277
     * @param array $context
278
     *
279
     * @return boolean
280
     */
281
    public function canCreate($member = null, $context = array())
282
    {
283
        $extended = $this->extendedCan(__FUNCTION__, $member);
284
        if ($extended !== null) {
285
            return $extended;
286
        }
287
288
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
289
    }
290
291
    /**
292
     * Increment the sort order if one hasn't been already defined. This
293
     * ensures that new elements are created at the end of the list by default.
294
     *
295
     * {@inheritDoc}
296
     */
297
    public function onBeforeWrite()
298
    {
299
        parent::onBeforeWrite();
300
301
        // If a Sort has already been set, then we can exit early
302
        if ($this->Sort) {
303
            return;
304
        }
305
306
        // If no ParentID is currently set for the Element, then we don't want to define an initial Sort yet
307
        if (!$this->ParentID) {
308
            return;
309
        }
310
311
        if ($this->hasExtension(Versioned::class)) {
312
            $records = Versioned::get_by_stage(BaseElement::class, Versioned::DRAFT);
313
        } else {
314
            $records = BaseElement::get();
315
        }
316
317
        $records = $records->filter('ParentID', $this->ParentID);
318
319
        $this->Sort = $records->max('Sort') + 1;
320
    }
321
322
    public function getCMSFields()
323
    {
324
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
325
            // Remove relationship fields
326
            $fields->removeByName('ParentID');
327
            $fields->removeByName('Sort');
328
329
            // Remove link and file tracking tabs
330
            $fields->removeByName(['LinkTracking', 'FileTracking']);
331
332
            $fields->addFieldToTab(
333
                'Root.Settings',
334
                TextField::create('ExtraClass', _t(__CLASS__ . '.ExtraCssClassesLabel', 'Custom CSS classes'))
335
                    ->setAttribute(
336
                        'placeholder',
337
                        _t(__CLASS__ . '.ExtraCssClassesPlaceholder', 'my_class another_class')
338
                    )
339
            );
340
341
            // Rename the "Settings" tab
342
            $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...
343
                ->setTitle(_t(__CLASS__ . '.SettingsTabLabel', 'Settings'));
344
345
            // Add a combined field for "Title" and "Displayed" checkbox in a Bootstrap input group
346
            $fields->removeByName('ShowTitle');
347
348
            if ($this->config()->get('displays_title_in_template')) {
349
                $fields->replaceField(
350
                    'Title',
351
                    TextCheckboxGroupField::create()
352
                        ->setName('Title')
353
                );
354
            }
355
356
            // Rename the "Main" tab
357
            $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...
358
                ->setTitle(_t(__CLASS__ . '.MainTabLabel', 'Content'));
359
360
            $fields->addFieldsToTab('Root.Main', [
361
                HiddenField::create('AbsoluteLink', false, Director::absoluteURL($this->PreviewLink())),
362
                HiddenField::create('LiveLink', false, Director::absoluteURL($this->Link())),
363
                HiddenField::create('StageLink', false, Director::absoluteURL($this->PreviewLink())),
364
            ]);
365
366
            $styles = $this->config()->get('styles');
367
368
            if ($styles && count($styles) > 0) {
369
                $styleDropdown = DropdownField::create('Style', _t(__CLASS__.'.STYLE', 'Style variation'), $styles);
370
371
                $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

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

652
                /** @scrutinizer ignore-call */ 
653
                $anchorTitle = $this->getAnchorTitle();
Loading history...
653
            } elseif ($this->config()->enable_title_in_template) {
654
                $anchorTitle = $this->getField('Title');
655
            }
656
        }
657
658
        if (!$anchorTitle) {
659
            $anchorTitle = 'e'.$this->ID;
660
        }
661
662
        $filter = URLSegmentFilter::create();
663
        $titleAsURL = $filter->filter($anchorTitle);
664
665
        // Ensure that this anchor name isn't already in use
666
        // ie. If two elemental blocks have the same title, it'll append '-2', '-3'
667
        $result = $titleAsURL;
668
        $count = 1;
669
        while (isset(self::$used_anchors[$result]) && self::$used_anchors[$result] !== $this->ID) {
670
            ++$count;
671
            $result = $titleAsURL . '-' . $count;
672
        }
673
        self::$used_anchors[$result] = $this->ID;
674
        return $this->anchor = $result;
675
    }
676
677
    /**
678
     * @param string|null $action
679
     * @return string|null
680
     * @throws \Psr\Container\NotFoundExceptionInterface
681
     * @throws \SilverStripe\ORM\ValidationException
682
     */
683
    public function AbsoluteLink($action = null)
684
    {
685
        if ($page = $this->getPage()) {
686
            $link = $page->AbsoluteLink($action) . '#' . $this->getAnchor();
687
688
            $this->extend('updateAbsoluteLink', $link);
689
690
            return $link;
691
        }
692
693
        return null;
694
    }
695
696
    /**
697
     * @param string|null $action
698
     * @return string
699
     * @throws \Psr\Container\NotFoundExceptionInterface
700
     * @throws \SilverStripe\ORM\ValidationException
701
     */
702
    public function Link($action = null)
703
    {
704
        if ($page = $this->getPage()) {
705
            $link = $page->Link($action) . '#' . $this->getAnchor();
706
707
            $this->extend('updateLink', $link);
708
709
            return $link;
710
        }
711
712
        return null;
713
    }
714
715
    /**
716
     * @param string|null $action
717
     * @return string
718
     * @throws \Psr\Container\NotFoundExceptionInterface
719
     * @throws \SilverStripe\ORM\ValidationException
720
     */
721
    public function PreviewLink($action = null)
722
    {
723
        $action = $action . '?ElementalPreview=' . mt_rand();
724
        $link = $this->Link($action);
725
        $this->extend('updatePreviewLink', $link);
726
727
        return $link;
728
    }
729
730
    /**
731
     * @return boolean
732
     */
733
    public function isCMSPreview()
734
    {
735
        if (Controller::has_curr()) {
736
            $controller = Controller::curr();
737
738
            if ($controller->getRequest()->requestVar('CMSPreview')) {
739
                return true;
740
            }
741
        }
742
743
        return false;
744
    }
745
746
    /**
747
     * @param bool $directLink Indicates that the GridFieldDetailEdit form link should be given even if the block can be
748
     *                         edited in-line.
749
     * @return null|string
750
     * @throws \SilverStripe\ORM\ValidationException
751
     */
752
    public function CMSEditLink($directLink = false)
753
    {
754
        // Allow for repeated calls to be returned from cache
755
        if (isset($this->cacheData['cms_edit_link'])) {
756
            return $this->cacheData['cms_edit_link'];
757
        }
758
759
        $relationName = $this->getAreaRelationName();
760
        $page = $this->getPage();
761
762
        if (!$page) {
763
            $link = null;
764
            $this->extend('updateCMSEditLink', $link);
765
            return $link;
766
        }
767
768
        if (!$page instanceof SiteTree && method_exists($page, 'CMSEditLink')) {
0 ignored issues
show
introduced by
$page is always a sub-type of SilverStripe\CMS\Model\SiteTree.
Loading history...
769
            $link = Controller::join_links($page->CMSEditLink(), 'ItemEditForm');
770
        } else {
771
            $link = $page->CMSEditLink();
772
        }
773
774
        // In-line editable blocks should just take you to the page. Editable ones should add the suffix for detail form
775
        if (!$this->inlineEditable() || $directLink) {
776
            $link = Controller::join_links(
777
                singleton(CMSPageEditController::class)->Link('EditForm'),
778
                $page->ID,
779
                'field/' . $relationName . '/item/',
780
                $this->ID,
781
                'edit'
782
            );
783
        }
784
785
        $this->extend('updateCMSEditLink', $link);
786
787
        $this->cacheData['cms_edit_link'] = $link;
788
        return $link;
789
    }
790
791
    /**
792
     * Retrieve a elemental area relation for creating cms links
793
     *
794
     * @return int|string The name of a valid elemental area relation
795
     * @throws \Psr\Container\NotFoundExceptionInterface
796
     * @throws \SilverStripe\ORM\ValidationException
797
     */
798
    public function getAreaRelationName()
799
    {
800
        // Allow repeated calls to return from internal cache
801
        if (isset($this->cacheData['area_relation_name'])) {
802
            return $this->cacheData['area_relation_name'];
803
        }
804
805
        $page = $this->getPage();
806
807
        $result = 'ElementalArea';
808
809
        if ($page) {
810
            $has_one = $page->config()->get('has_one');
811
            $area = $this->Parent();
812
813
            foreach ($has_one as $relationName => $relationClass) {
814
                if ($page instanceof BaseElement && $relationName === 'Parent') {
815
                    continue;
816
                }
817
                if ($relationClass === $area->ClassName && $page->{$relationName}()->ID === $area->ID) {
818
                    $result = $relationName;
819
                    break;
820
                }
821
            }
822
        }
823
824
        $this->setAreaRelationNameCache($result);
825
826
        return $result;
827
    }
828
829
    /**
830
     * Sanitise a model class' name for inclusion in a link.
831
     *
832
     * @return string
833
     */
834
    public function sanitiseClassName($class, $delimiter = '-')
835
    {
836
        return str_replace('\\', $delimiter, $class);
837
    }
838
839
    public function unsanitiseClassName($class, $delimiter = '-')
840
    {
841
        return str_replace($delimiter, '\\', $class);
842
    }
843
844
    /**
845
     * @return null|string
846
     * @throws \Psr\Container\NotFoundExceptionInterface
847
     * @throws \SilverStripe\ORM\ValidationException
848
     */
849
    public function getEditLink()
850
    {
851
        return Director::absoluteURL($this->CMSEditLink());
852
    }
853
854
    /**
855
     * @return DBField|null
856
     * @throws \Psr\Container\NotFoundExceptionInterface
857
     * @throws \SilverStripe\ORM\ValidationException
858
     */
859
    public function PageCMSEditLink()
860
    {
861
        if ($page = $this->getPage()) {
862
            return DBField::create_field('HTMLText', sprintf(
863
                '<a href="%s">%s</a>',
864
                $page->CMSEditLink(),
865
                $page->Title
866
            ));
867
        }
868
869
        return null;
870
    }
871
872
    /**
873
     * @return string
874
     */
875
    public function getMimeType()
876
    {
877
        return 'text/html';
878
    }
879
880
    /**
881
     * This can be overridden on child elements to create a summary for display
882
     * in GridFields.
883
     *
884
     * @return string
885
     */
886
    public function getSummary()
887
    {
888
        return '';
889
    }
890
891
    /**
892
     * The block config defines a set of data (usually set through config on the element) that will be made available in
893
     * client side config. Individual element types may choose to add config variable for use in React code
894
     *
895
     * @return array
896
     */
897
    public static function getBlockConfig()
898
    {
899
        return [];
900
    }
901
902
    /**
903
     * The block actions is an associative array available for providing data to the client side to be used to describe
904
     * actions that may be performed. This is available as a plain "ObjectType" in the GraphQL schema.
905
     *
906
     * By default the only action is "edit" which is simply the URL where the block may be edited.
907
     *
908
     * To modify the actions, either use the extension point or overload the `provideBlockSchema` method.
909
     *
910
     * @internal This API may change in future. Treat this as a `final` method.
911
     * @return array
912
     */
913
    public function getBlockSchema()
914
    {
915
        $blockSchema = $this->provideBlockSchema();
916
917
        $this->extend('updateBlockSchema', $blockSchema);
918
919
        return $blockSchema;
920
    }
921
922
    /**
923
     * Provide block schema data, which will be serialised and sent via GraphQL to the editor client.
924
     *
925
     * Overload this method in child element classes to augment, or use the extension point on `getBlockSchema`
926
     * to update it from an `Extension`.
927
     *
928
     * @return array
929
     * @throws SchemaBuilderException
930
     * @throws ValidationException
931
     */
932
    protected function provideBlockSchema()
933
    {
934
        return [
935
            'typeName' => static::getGraphQLTypeName(),
936
            'actions' => [
937
                'edit' => $this->getEditLink(),
938
            ],
939
        ];
940
    }
941
942
    /**
943
     * Generate markup for element type icons suitable for use in GridFields.
944
     *
945
     * @return null|DBHTMLText
946
     */
947
    public function getIcon()
948
    {
949
        $data = ArrayData::create([]);
950
951
        $iconClass = $this->config()->get('icon');
952
        if ($iconClass) {
953
            $data->IconClass = $iconClass;
954
955
            // Add versioned states (rendered as a circle over the icon)
956
            if ($this->hasExtension(Versioned::class)) {
957
                $data->IsVersioned = true;
958
                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

958
                if ($this->/** @scrutinizer ignore-call */ isOnDraftOnly()) {
Loading history...
959
                    $data->VersionState = 'draft';
960
                    $data->VersionStateTitle = _t(
961
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.ADDEDTODRAFTHELP',
962
                        'Item has not been published yet'
963
                    );
964
                } 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

964
                } elseif ($this->/** @scrutinizer ignore-call */ isModifiedOnDraft()) {
Loading history...
965
                    $data->VersionState = 'modified';
966
                    $data->VersionStateTitle = $data->VersionStateTitle = _t(
967
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.MODIFIEDONDRAFTHELP',
968
                        'Item has unpublished changes'
969
                    );
970
                }
971
            }
972
973
            return $data->renderWith(__CLASS__ . '/PreviewIcon');
974
        }
975
976
        return null;
977
    }
978
979
    /**
980
     * Get a description for this content element, if available
981
     *
982
     * @return string
983
     */
984
    public function getDescription()
985
    {
986
        $description = $this->config()->uninherited('description');
987
        if ($description) {
988
            return _t(__CLASS__ . '.Description', $description);
989
        }
990
        return '';
991
    }
992
993
    /**
994
     * Generate markup for element type, with description suitable for use in
995
     * GridFields.
996
     *
997
     * @return DBField
998
     */
999
    public function getTypeNice()
1000
    {
1001
        $description = $this->getDescription();
1002
        $desc = ($description) ? ' <span class="element__note"> &mdash; ' . $description . '</span>' : '';
1003
1004
        return DBField::create_field(
1005
            'HTMLVarchar',
1006
            $this->getType() . $desc
1007
        );
1008
    }
1009
1010
    /**
1011
     * @return \SilverStripe\ORM\FieldType\DBHTMLText
1012
     */
1013
    public function getEditorPreview()
1014
    {
1015
        $templates = $this->getRenderTemplates('_EditorPreview');
1016
        $templates[] = BaseElement::class . '_EditorPreview';
1017
1018
        return $this->renderWith($templates);
1019
    }
1020
1021
    /**
1022
     * @return Member
1023
     */
1024
    public function getAuthor()
1025
    {
1026
        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...
1027
            return Member::get()->byId($this->AuthorID);
1028
        }
1029
1030
        return null;
1031
    }
1032
1033
    /**
1034
     * Get a user defined style variant for this element, if available
1035
     *
1036
     * @return string
1037
     */
1038
    public function getStyleVariant()
1039
    {
1040
        $style = $this->Style;
1041
        $styles = $this->config()->get('styles');
1042
1043
        if (isset($styles[$style])) {
1044
            $style = strtolower($style);
1045
        } else {
1046
            $style = '';
1047
        }
1048
1049
        $this->extend('updateStyleVariant', $style);
1050
1051
        return $style;
1052
    }
1053
1054
    /**
1055
     * @return mixed|null
1056
     * @throws \Psr\Container\NotFoundExceptionInterface
1057
     * @throws \SilverStripe\ORM\ValidationException
1058
     */
1059
    public function getPageTitle()
1060
    {
1061
        $page = $this->getPage();
1062
1063
        if ($page) {
1064
            return $page->Title;
1065
        }
1066
1067
        return null;
1068
    }
1069
1070
    /**
1071
     * @return boolean
1072
     */
1073
    public function First()
1074
    {
1075
        return ($this->Parent()->Elements()->first()->ID === $this->ID);
1076
    }
1077
1078
    /**
1079
     * @return boolean
1080
     */
1081
    public function Last()
1082
    {
1083
        return ($this->Parent()->Elements()->last()->ID === $this->ID);
1084
    }
1085
1086
    /**
1087
     * @return int
1088
     */
1089
    public function TotalItems()
1090
    {
1091
        return $this->Parent()->Elements()->count();
1092
    }
1093
1094
    /**
1095
     * Returns the position of the current element.
1096
     *
1097
     * @return int
1098
     */
1099
    public function Pos()
1100
    {
1101
        return ($this->Parent()->Elements()->filter('Sort:LessThan', $this->Sort)->count() + 1);
1102
    }
1103
1104
    /**
1105
     * @return string
1106
     */
1107
    public function EvenOdd()
1108
    {
1109
        $odd = (bool) ($this->Pos() % 2);
1110
1111
        return  ($odd) ? 'odd' : 'even';
1112
    }
1113
1114
    /**
1115
     * @return string
1116
     */
1117
    public static function getGraphQLTypeName(): string
1118
    {
1119
        return class_exists(StaticSchema::class)
1120
            ? StaticSchema::inst()->typeNameForDataObject(static::class)
1121
            : str_replace('\\', '_', static::class);
1122
    }
1123
}
1124