Completed
Pull Request — master (#54)
by John
02:07
created

ElementPageExtension::onAfterDuplicate()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 17
Code Lines 10

Duplication

Lines 6
Ratio 35.29 %

Importance

Changes 0
Metric Value
dl 6
loc 17
rs 9.2
c 0
b 0
f 0
cc 4
eloc 10
nc 3
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
     * Ensure that if there are elements that belong to this page
232
     * and are virtualised (Virtual Element links to them), that we move the
233
     * original element to replace one of the virtual elements
234
     * But only if it's a delete not an unpublish
235
     */
236
    public function onAfterDelete() {
237
        if(Versioned::get_reading_mode() == 'Stage.Stage') {
238
            $area = $this->owner->ElementArea();
239
            foreach ($area->Widgets() as $element) {
240
                $firstVirtual = false;
241
                if ($element->getPublishedVirtualLinkedElements()->Count() > 0) {
242
                    // choose the first one
243
                    $firstVirtual = $element->getPublishedVirtualLinkedElements()->First();
244
                    $wasPublished = true;
245
                } else if ($element->getVirtualLinkedElements()->Count() > 0) {
246
                    // choose the first one
247
                    $firstVirtual = $element->getVirtualLinkedElements()->First();
248
                    $wasPublished = false;
249
                }
250
                if ($firstVirtual) {
251
                    $origParentID = $element->ParentID;
252
                    $origSort = $element->Sort;
253
254
                    // change element to first's values
255
                    $element->ParentID = $firstVirtual->ParentID;
256
                    $element->Sort = $firstVirtual->Sort;
257
258
                    $firstVirtual->ParentID = $origParentID;
259
                    $firstVirtual->Sort = $origSort;
260
                    // write
261
                    $element->write();
262
                    $firstVirtual->write();
263
                    if ($wasPublished) {
0 ignored issues
show
Bug introduced by
The variable $wasPublished does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
264
                        $element->doPublish();
265
                        $firstVirtual->doPublish();
266
                    }
267
                }
268
            }
269
        }
270
    }
271
272
    /**
273
     * @return boolean
274
     */
275
    public function supportsElemental() {
276
        if ($this->owner->hasMethod('includeElemental')) {
277
            $res = $this->owner->includeElemental();
278
            if ($res !== null) {
279
                return $res;
280
            }
281
        }
282
283
        if (is_a($this->owner, 'RedirectorPage')) {
284
            return false;
285
        } else if ($ignored = Config::inst()->get('ElementPageExtension', 'ignored_classes')) {
286
            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...
287
                if (is_a($this->owner, $check)) {
288
                    return false;
289
                }
290
            }
291
        }
292
293
        return true;
294
    }
295
296
    /**
297
     * If the page is duplicated, copy the widgets across too.
298
     *
299
     * Gets called twice from either direction, due to bad DataObject and SiteTree code, hence the weird if statement
300
     *
301
     * @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...
302
     */
303
    public function onAfterDuplicate($duplicatePage)
304
    {
305
        if ($this->owner->ID != 0 && $this->owner->ID < $duplicatePage->ID) {
306
            $originalWidgetArea = $this->owner->getComponent('ElementArea');
307
            $duplicateWidgetArea = $originalWidgetArea->duplicate(false);
308
            $duplicateWidgetArea->write();
309
            $duplicatePage->ElementAreaID = $duplicateWidgetArea->ID;
310
            $duplicatePage->write();
311
312 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...
313
                $duplicateWidget = $originalWidget->duplicate(true);
314
315
                // manually set the ParentID of each widget, so we don't get versioning issues
316
                DB::query(sprintf("UPDATE Widget SET ParentID = %d WHERE ID = %d", $duplicateWidgetArea->ID, $duplicateWidget->ID));
317
            }
318
        }
319
    }
320
321
    /**
322
     * If the page is duplicated across subsites, copy the widgets across too.
323
     *
324
     * @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...
325
     */
326
    public function onAfterDuplicateToSubsite($originalPage)
327
    {
328
        $originalWidgetArea = $originalPage->getComponent('ElementArea');
329
        $duplicateWidgetArea = $originalWidgetArea->duplicate(false);
330
        $duplicateWidgetArea->write();
331
        $this->owner->ElementAreaID = $duplicateWidgetArea->ID;
332
        $this->owner->write();
333
334 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...
335
            $duplicateWidget = $originalWidget->duplicate(true);
336
337
            // manually set the ParentID of each widget, so we don't get versioning issues
338
            DB::query(sprintf("UPDATE Widget SET ParentID = %d WHERE ID = %d", $duplicateWidgetArea->ID, $duplicateWidget->ID));
339
        }
340
    }
341
342
    /**
343
     * Publish
344
     */
345
    public function onAfterPublish()
346
    {
347
        if ($id = $this->owner->ElementAreaID) {
348
            $widgets = Versioned::get_by_stage('BaseElement', 'Stage', "ParentID = '$id'");
349
            $staged = array();
350
351
            foreach ($widgets as $widget) {
352
                $staged[] = $widget->ID;
353
354
                $widget->publish('Stage', 'Live');
355
            }
356
357
            // remove any elements that are on live but not in draft.
358
            $widgets = Versioned::get_by_stage('BaseElement', 'Live', "ParentID = '$id'");
359
360
            foreach ($widgets as $widget) {
361
                if (!in_array($widget->ID, $staged)) {
362
                    $widget->deleteFromStage('Live');
363
                }
364
            }
365
        }
366
    }
367
368
    /**
369
     * Roll back all changes if the parent page has a rollback event
370
     *
371
     * Only do rollback if it's the 'cancel draft changes' rollback, not a specific version
372
     * rollback.
373
     *
374
     * @param string $version
375
     * @return null
376
     */
377
    public function onBeforeRollback($version)
378
    {
379
        if ($version !== 'Live') {
380
            // we don't yet have a smart way of rolling back to a specific version
381
            return;
382
        }
383
        if ($id = $this->owner->ElementAreaID) {
384
            $widgets = Versioned::get_by_stage('BaseElement', 'Live', "ParentID = '$id'");
385
            $staged = array();
386
387
            foreach ($widgets as $widget) {
388
                $staged[] = $widget->ID;
389
390
                $widget->invokeWithExtensions('onBeforeRollback', $widget);
391
392
                $widget->publish("Live", "Stage", false);
393
394
                $widget->invokeWithExtensions('onAfterRollback', $widget);
395
            }
396
        }
397
    }
398
}
399