Passed
Pull Request — 4 (#913)
by Guy
02:52
created

BaseElement::getEditLink()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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