Completed
Push — master ( ff684d...fed1b1 )
by Will
02:29
created

ElementPageExtension::onBeforeRollback()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 9.0534
c 0
b 0
f 0
cc 4
eloc 11
nc 4
nop 1
1
<?php
2
3
use \Heyday\VersionedDataObjects\VersionedDataObjectDetailsForm;
4
5
/**
6
 * @package elemental
7
 */
8
class ElementPageExtension extends DataExtension
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
9
{
10
11
    /**
12
     * @config
13
     *
14
     * @var string $elements_title Title of the element in the CMS.
15
     */
16
    private static $elements_title = 'Content Blocks';
0 ignored issues
show
Unused Code introduced by
The property $elements_title is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
17
18
    /**
19
     * @config
20
     *
21
     * @var boolean $disable_element_publish_button Disable publish / unpublish buttons in GridFieldDetailForm.
22
     */
23
    private static $disable_element_publish_button = false;
0 ignored issues
show
Unused Code introduced by
The property $disable_element_publish_button is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
24
25
    /**
26
     * @config
27
     *
28
     * @var array $ignored_classes Classes to ignore adding elements too.
29
     */
30
    private static $ignored_classes = array();
0 ignored issues
show
Unused Code introduced by
The property $ignored_classes is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
31
32
    /**
33
     * @config
34
     *
35
     * @var boolean
36
     */
37
    private static $copy_element_content_to_contentfield = true;
0 ignored issues
show
Unused Code introduced by
The property $copy_element_content_to_contentfield is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
38
39
    /**
40
     * @config
41
     *
42
     * @var boolean
43
     */
44
    private static $clear_contentfield = false;
0 ignored issues
show
Unused Code introduced by
The property $clear_contentfield is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
45
46
    /**
47
     * @var array $db
48
     */
49
    private static $db = array();
0 ignored issues
show
Unused Code introduced by
The property $db is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
50
51
    /**
52
     * @var array $has_one
53
     */
54
    private static $has_one = array(
0 ignored issues
show
Unused Code introduced by
The property $has_one is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
55
        'ElementArea' => 'ElementalArea'
56
    );
57
58
    /**
59
     * Setup the CMS Fields
60
     *
61
     * @param FieldList
62
     */
63
    public function updateCMSFields(FieldList $fields)
64
    {
65
        if(!$this->supportsElemental()) {
66
            return;
67
        }
68
69
        // add an empty holder for content as some module explicitly use insert
70
        // after content.
71
        $fields->replaceField('Content', new LiteralField('Content', ''));
72
73
        $adder = new ElementalGridFieldAddNewMultiClass();
74
75
        $list = $this->getAvailableTypes();
76
        if($list) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $list of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
77
            $adder->setClasses($list);
78
        }
79
80
        $area = $this->owner->ElementArea();
81
        if ($this->owner->exists() && (!$area->exists() || !$area->isInDB())) {
82
            $area->write();
83
84
            $this->owner->ElementAreaID = $area->ID;
85
            $this->owner->write();
86
        }
87
88
        $gridField = GridField::create('ElementArea',
89
            Config::inst()->get("ElementPageExtension", 'elements_title'),
90
            $area->AllElements(),
91
            GridFieldConfig_RelationEditor::create()
92
                ->removeComponentsByType('GridFieldAddNewButton')
93
                ->removeComponentsByType('GridFieldSortableHeader')
94
                ->removeComponentsByType('GridFieldDeleteAction')
95
                ->removeComponentsByType('GridFieldAddExistingAutocompleter')
96
                ->addComponent($autocomplete = new ElementalGridFieldAddExistingAutocompleter())
97
                ->addComponent(new ElementalGridFieldDeleteAction())
98
                ->addComponent(new GridFieldTitleHeader())
99
                ->addComponent($adder)
100
                ->addComponent(new GridFieldSortableRows('Sort'))
101
        );
102
103
        if($list) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $list of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
104
            $autocomplete->setSearchList(BaseElement::get()->filter('ClassName', array_keys($list)));
105
        }
106
107
        $config = $gridField->getConfig();
108
        $paginator = $config->getComponentByType('GridFieldPaginator');
109
        $paginator->setItemsPerPage(100);
110
111
        if (!$this->owner->config()->disable_element_publish_button) {
112
            $config->removeComponentsByType('GridFieldDetailForm');
113
            $config->addComponent($obj = new VersionedDataObjectDetailsForm());
114
        }
115
116
        if ($this->owner instanceof SiteTree && $fields->findOrMakeTab('Root.Main')->fieldByName('Metadata')) {
117
            $fields->addFieldToTab('Root.Main', $gridField, 'Metadata');
118
        } else {
119
            $fields->addFieldToTab('Root.Main', $gridField);
120
        }
121
122
        return $fields;
123
    }
124
125
    /**
126
     * @return array
127
     */
128
    public function getAvailableTypes() {
129
        if (is_array($this->owner->config()->get('allowed_elements'))) {
130
            $list = $this->owner->config()->get('allowed_elements');
131
132 View Code Duplication
            if($this->owner->config()->get('sort_types_alphabetically') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
133
                $sorted = array();
134
135
                foreach ($list as $class) {
136
                    $inst = singleton($class);
137
138
                    if ($inst->canCreate()) {
139
                        $sorted[$class] = singleton($class)->i18n_singular_name();
140
                    }
141
                }
142
143
                $list = $sorted;
144
                asort($list);
145
            }
146 View Code Duplication
        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
147
            $classes = ClassInfo::subclassesFor('BaseElement');
148
            $list = array();
149
            unset($classes['BaseElement']);
150
151
            $disallowedElements = (array) $this->owner->config()->get('disallowed_elements');
152
153
            if (!in_array('ElementVirtualLinked', $disallowedElements)) {
154
                array_push($disallowedElements, 'ElementVirtualLinked');
155
            }
156
157
            foreach ($classes as $class) {
0 ignored issues
show
Bug introduced by
The expression $classes of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
158
                $inst = singleton($class);
159
160
                if (!in_array($class, $disallowedElements) && $inst->canCreate()) {
161
                    $list[$class] = singleton($class)->i18n_singular_name();
162
                }
163
            }
164
165
            asort($list);
166
        }
167
168
        if (method_exists($this->owner, 'sortElementalOptions')) {
169
            $this->owner->sortElementalOptions($list);
170
        }
171
172
        return $list;
173
    }
174
175
    /**
176
     * Make sure there is always a WidgetArea sidebar for adding widgets
177
     *
178
     */
179
    public function onBeforeWrite()
180
    {
181
        if(!$this->supportsElemental()) {
182
            return;
183
        }
184
185
        // enable theme in case elements are being rendered with templates stored in theme folder
186
        $originalThemeEnabled = Config::inst()->get('SSViewer', 'theme_enabled');
187
        Config::inst()->update('SSViewer', 'theme_enabled', true);
188
189
190
        if ($this->owner->hasMethod('ElementArea') && Config::inst()->get(__CLASS__, 'copy_element_content_to_contentfield')) {
191
            $elements = $this->owner->ElementArea();
192
193
            if (!$elements->isInDB()) {
194
                $elements->write();
195
                $this->owner->ElementAreaID = $elements->ID;
196
            } else {
197
                // Copy widgets content to Content to enable search
198
                $searchableContent = array();
199
200
                Requirements::clear();
201
202
                foreach ($elements->Elements() as $element) {
203
                    if ($element->config()->exclude_from_content) {
204
                        continue;
205
                    }
206
207
                    $controller = $element->getController();
208
                    $controller->init();
209
210
                    array_push($searchableContent, $controller->WidgetHolder());
211
                }
212
213
                Requirements::restore();
214
215
                $this->owner->Content = trim(implode(' ', $searchableContent));
216
            }
217
        } else {
218
            if(Config::inst()->get(__CLASS__, 'clear_contentfield')) {
219
                $this->owner->Content = '';
220
            }
221
        }
222
223
224
        // set theme_enabled back to what it was
225
        Config::inst()->update('SSViewer', 'theme_enabled', $originalThemeEnabled);
226
227
        parent::onBeforeWrite();
228
    }
229
230
    /**
231
     * @return boolean
232
     */
233
    public function supportsElemental() {
234
        if ($this->owner->hasMethod('includeElemental')) {
235
            $res = $this->owner->includeElemental();
236
            if ($res !== null) {
237
                return $res;
238
            }
239
        }
240
241
        if (is_a($this->owner, 'RedirectorPage')) {
242
            return false;
243
        } else if ($ignored = Config::inst()->get('ElementPageExtension', 'ignored_classes')) {
244
            foreach ($ignored as $check) {
0 ignored issues
show
Bug introduced by
The expression $ignored of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
245
                if (is_a($this->owner, $check)) {
246
                    return false;
247
                }
248
            }
249
        }
250
251
        return true;
252
    }
253
254
    /**
255
     * If the page is duplicated, copy the widgets across too.
256
     *
257
     * Gets called twice from either direction, due to bad DataObject and SiteTree code, hence the weird if statement
258
     *
259
     * @return Page The duplicated page
0 ignored issues
show
Documentation introduced by
Should the return type not be Page|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
260
     */
261
    public function onAfterDuplicate($duplicatePage)
262
    {
263
        if ($this->owner->ID != 0 && $this->owner->ID < $duplicatePage->ID) {
264
            $originalWidgetArea = $this->owner->getComponent('ElementArea');
265
            $duplicateWidgetArea = $originalWidgetArea->duplicate(false);
266
            $duplicateWidgetArea->write();
267
            $duplicatePage->ElementAreaID = $duplicateWidgetArea->ID;
268
            $duplicatePage->write();
269
270 View Code Duplication
            foreach ($originalWidgetArea->Items() as $originalWidget) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
271
                $duplicateWidget = $originalWidget->duplicate(true);
272
273
                // manually set the ParentID of each widget, so we don't get versioning issues
274
                DB::query(sprintf("UPDATE Widget SET ParentID = %d WHERE ID = %d", $duplicateWidgetArea->ID, $duplicateWidget->ID));
275
            }
276
        }
277
    }
278
279
    /**
280
     * If the page is duplicated across subsites, copy the widgets across too.
281
     *
282
     * @return Page The duplicated page
0 ignored issues
show
Documentation introduced by
Should the return type not be Page|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
283
     */
284
    public function onAfterDuplicateToSubsite($originalPage)
285
    {
286
        $originalWidgetArea = $originalPage->getComponent('ElementArea');
287
        $duplicateWidgetArea = $originalWidgetArea->duplicate(false);
288
        $duplicateWidgetArea->write();
289
        $this->owner->ElementAreaID = $duplicateWidgetArea->ID;
290
        $this->owner->write();
291
292 View Code Duplication
        foreach ($originalWidgetArea->Items() as $originalWidget) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
293
            $duplicateWidget = $originalWidget->duplicate(true);
294
295
            // manually set the ParentID of each widget, so we don't get versioning issues
296
            DB::query(sprintf("UPDATE Widget SET ParentID = %d WHERE ID = %d", $duplicateWidgetArea->ID, $duplicateWidget->ID));
297
        }
298
    }
299
300
    /**
301
     * Publish
302
     */
303
    public function onAfterPublish()
304
    {
305
        if ($id = $this->owner->ElementAreaID) {
306
            $widgets = Versioned::get_by_stage('BaseElement', 'Stage', "ParentID = '$id'");
307
            $staged = array();
308
309
            foreach ($widgets as $widget) {
310
                $staged[] = $widget->ID;
311
312
                $widget->publish('Stage', 'Live');
313
            }
314
315
            // remove any elements that are on live but not in draft.
316
            $widgets = Versioned::get_by_stage('BaseElement', 'Live', "ParentID = '$id'");
317
318
            foreach ($widgets as $widget) {
319
                if (!in_array($widget->ID, $staged)) {
320
                    $widget->deleteFromStage('Live');
321
                }
322
            }
323
        }
324
    }
325
326
    /**
327
     * Roll back all changes if the parent page has a rollback event
328
     *
329
     * Only do rollback if it's the 'cancel draft changes' rollback, not a specific version
330
     * rollback.
331
     *
332
     * @param string $version
333
     * @return null
334
     */
335
    public function onBeforeRollback($version)
336
    {
337
        if ($version !== 'Live') {
338
            // we don't yet have a smart way of rolling back to a specific version
339
            return;
340
        }
341
        if ($id = $this->owner->ElementAreaID) {
342
            $widgets = Versioned::get_by_stage('BaseElement', 'Live', "ParentID = '$id'");
343
            $staged = array();
344
345
            foreach ($widgets as $widget) {
346
                $staged[] = $widget->ID;
347
348
                $widget->invokeWithExtensions('onBeforeRollback', $widget);
349
350
                $widget->publish("Live", "Stage", false);
351
352
                $widget->invokeWithExtensions('onAfterRollback', $widget);
353
            }
354
        }
355
    }
356
}
357