Passed
Pull Request — 4 (#913)
by Guy
03:20 queued 15s
created

BaseElement::getIsIndexable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
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\Core\Config\Config;
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
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
    /**
118
     * Cache various data to improve CMS load time
119
     *
120
     * @internal
121
     * @var array
122
     */
123
    protected $cacheData;
124
125
    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...
126
127
    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...
128
129
    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...
130
131
    private static $summary_fields = [
0 ignored issues
show
introduced by
The private property $summary_fields is not used, and could be removed.
Loading history...
132
        'EditorPreview' => 'Summary'
133
    ];
134
135
    /**
136
     * @config
137
     * @var array
138
     */
139
    private static $styles = [];
0 ignored issues
show
introduced by
The private property $styles is not used, and could be removed.
Loading history...
140
141
    private static $searchable_fields = [
0 ignored issues
show
introduced by
The private property $searchable_fields is not used, and could be removed.
Loading history...
142
        'ID' => [
143
            'field' => NumericField::class,
144
        ],
145
        'Title',
146
        'LastEdited'
147
    ];
148
149
    /**
150
     * Enable for backwards compatibility
151
     *
152
     * @var boolean
153
     */
154
    private static $disable_pretty_anchor_name = false;
155
156
    /**
157
     * Set to false to prevent an in-line edit form from showing in an elemental area. Instead the element will be
158
     * clickable and a GridFieldDetailForm will be used.
159
     *
160
     * @config
161
     * @var bool
162
     */
163
    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...
164
165
    /**
166
     * Display a show title button
167
     *
168
     * @config
169
     * @var boolean
170
     */
171
    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...
172
173
    /**
174
     * Determines whether a block should be indexable in search.
175
     *
176
     * @config
177
     * @var boolean
178
     * @see ElementalPageExtension::getElementsForSearch()
179
     */
180
    private static $is_indexable = true;
0 ignored issues
show
introduced by
The private property $is_indexable is not used, and could be removed.
Loading history...
181
182
    /**
183
     * Store used anchor names, this is to avoid title clashes
184
     * when calling 'getAnchor'
185
     *
186
     * @var array
187
     */
188
    protected static $used_anchors = [];
189
190
    /**
191
     * For caching 'getAnchor'
192
     *
193
     * @var string
194
     */
195
    protected $anchor = null;
196
197
    /**
198
     * Basic permissions, defaults to page perms where possible.
199
     *
200
     * @param Member $member
201
     * @return boolean
202
     */
203
    public function canView($member = null)
204
    {
205
        $extended = $this->extendedCan(__FUNCTION__, $member);
206
        if ($extended !== null) {
207
            return $extended;
208
        }
209
210
        if ($this->hasMethod('getPage')) {
211
            if ($page = $this->getPage()) {
212
                return $page->canView($member);
213
            }
214
        }
215
216
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
217
    }
218
219
    /**
220
     * Basic permissions, defaults to page perms where possible.
221
     *
222
     * @param Member $member
223
     *
224
     * @return boolean
225
     */
226
    public function canEdit($member = null)
227
    {
228
        $extended = $this->extendedCan(__FUNCTION__, $member);
229
        if ($extended !== null) {
230
            return $extended;
231
        }
232
233
        if ($this->hasMethod('getPage')) {
234
            if ($page = $this->getPage()) {
235
                return $page->canEdit($member);
236
            }
237
        }
238
239
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
240
    }
241
242
    /**
243
     * Basic permissions, defaults to page perms where possible.
244
     *
245
     * Uses archive not delete so that current stage is respected i.e if a
246
     * element is not published, then it can be deleted by someone who doesn't
247
     * have publishing permissions.
248
     *
249
     * @param Member $member
250
     *
251
     * @return boolean
252
     */
253
    public function canDelete($member = null)
254
    {
255
        $extended = $this->extendedCan(__FUNCTION__, $member);
256
        if ($extended !== null) {
257
            return $extended;
258
        }
259
260
        if ($this->hasMethod('getPage')) {
261
            if ($page = $this->getPage()) {
262
                return $page->canArchive($member);
263
            }
264
        }
265
266
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
267
    }
268
269
    /**
270
     * Basic permissions, defaults to page perms where possible.
271
     *
272
     * @param Member $member
273
     * @param array $context
274
     *
275
     * @return boolean
276
     */
277
    public function canCreate($member = null, $context = array())
278
    {
279
        $extended = $this->extendedCan(__FUNCTION__, $member);
280
        if ($extended !== null) {
281
            return $extended;
282
        }
283
284
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
285
    }
286
287
    /**
288
     * Increment the sort order if one hasn't been already defined. This
289
     * ensures that new elements are created at the end of the list by default.
290
     *
291
     * {@inheritDoc}
292
     */
293
    public function onBeforeWrite()
294
    {
295
        parent::onBeforeWrite();
296
297
        // If a Sort has already been set, then we can exit early
298
        if ($this->Sort) {
299
            return;
300
        }
301
302
        // If no ParentID is currently set for the Element, then we don't want to define an initial Sort yet
303
        if (!$this->ParentID) {
304
            return;
305
        }
306
307
        if ($this->hasExtension(Versioned::class)) {
308
            $records = Versioned::get_by_stage(BaseElement::class, Versioned::DRAFT);
309
        } else {
310
            $records = BaseElement::get();
311
        }
312
313
        $records = $records->filter('ParentID', $this->ParentID);
314
315
        $this->Sort = $records->max('Sort') + 1;
316
    }
317
318
    public function getCMSFields()
319
    {
320
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
321
            // Remove relationship fields
322
            $fields->removeByName('ParentID');
323
            $fields->removeByName('Sort');
324
325
            // Remove link and file tracking tabs
326
            $fields->removeByName(['LinkTracking', 'FileTracking']);
327
328
            $fields->addFieldToTab(
329
                'Root.Settings',
330
                TextField::create('ExtraClass', _t(__CLASS__ . '.ExtraCssClassesLabel', 'Custom CSS classes'))
331
                    ->setAttribute(
332
                        'placeholder',
333
                        _t(__CLASS__ . '.ExtraCssClassesPlaceholder', 'my_class another_class')
334
                    )
335
            );
336
337
            // Rename the "Settings" tab
338
            $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...
339
                ->setTitle(_t(__CLASS__ . '.SettingsTabLabel', 'Settings'));
340
341
            // Add a combined field for "Title" and "Displayed" checkbox in a Bootstrap input group
342
            $fields->removeByName('ShowTitle');
343
344
            if ($this->config()->get('displays_title_in_template')) {
345
                $fields->replaceField(
346
                    'Title',
347
                    TextCheckboxGroupField::create()
348
                        ->setName('Title')
349
                );
350
            }
351
352
            // Rename the "Main" tab
353
            $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...
354
                ->setTitle(_t(__CLASS__ . '.MainTabLabel', 'Content'));
355
356
            $fields->addFieldsToTab('Root.Main', [
357
                HiddenField::create('AbsoluteLink', false, Director::absoluteURL($this->PreviewLink())),
358
                HiddenField::create('LiveLink', false, Director::absoluteURL($this->Link())),
359
                HiddenField::create('StageLink', false, Director::absoluteURL($this->PreviewLink())),
360
            ]);
361
362
            $styles = $this->config()->get('styles');
363
364
            if ($styles && count($styles) > 0) {
365
                $styleDropdown = DropdownField::create('Style', _t(__CLASS__.'.STYLE', 'Style variation'), $styles);
366
367
                $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

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

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

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

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