LeftAndMainSubsites   F
last analyzed

Complexity

Total Complexity 68

Size/Duplication

Total Lines 381
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 143
c 2
b 0
f 1
dl 0
loc 381
rs 2.96
wmc 68

15 Methods

Rating   Name   Duplication   Size   Complexity  
A Subsites() 0 3 1
A CanAddSubsites() 0 3 1
A updatePageOptions() 0 3 1
A getCMSTreeTitle() 0 4 2
A init() 0 5 1
A shouldChangeSubsite() 0 9 4
A ListSubsites() 0 24 6
A alternateMenuDisplayCheck() 0 21 5
A alternateAccessCheck() 0 3 1
C sectionSites() 0 55 11
A onAfterSave() 0 6 4
A augmentNewSiteTreeItem() 0 4 2
A canAccess() 0 20 5
D onBeforeInit() 0 113 22
A copytosubsite() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like LeftAndMainSubsites often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LeftAndMainSubsites, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Subsites\Extensions;
4
5
use SilverStripe\Admin\AdminRootController;
6
use SilverStripe\Admin\CMSMenu;
7
use SilverStripe\Admin\LeftAndMainExtension;
8
use SilverStripe\CMS\Controllers\CMSPagesController;
9
use SilverStripe\CMS\Model\SiteTree;
10
use SilverStripe\CMS\Controllers\CMSPageEditController;
11
use SilverStripe\Control\Controller;
12
use SilverStripe\Core\Config\Config;
13
use SilverStripe\Core\Convert;
14
use SilverStripe\Forms\HiddenField;
15
use SilverStripe\ORM\ArrayList;
16
use SilverStripe\ORM\DataObject;
17
use SilverStripe\Security\Member;
18
use SilverStripe\Security\Permission;
19
use SilverStripe\Security\Security;
20
use SilverStripe\Subsites\Controller\SubsiteXHRController;
21
use SilverStripe\Subsites\Model\Subsite;
22
use SilverStripe\Subsites\State\SubsiteState;
23
use SilverStripe\View\ArrayData;
24
use SilverStripe\View\Requirements;
25
26
/**
27
 * Decorator designed to add subsites support to LeftAndMain
28
 *
29
 * @package subsites
30
 */
31
class LeftAndMainSubsites extends LeftAndMainExtension
32
{
33
    private static $allowed_actions = ['CopyToSubsite'];
0 ignored issues
show
introduced by Werner M. Krauß
The private property $allowed_actions is not used, and could be removed.
Loading history...
34
35
    /**
36
     * Normally SubsiteID=0 on a DataObject means it is only accessible from the special "main site".
37
     * However in some situations SubsiteID=0 will be understood as a "globally accessible" object in which
38
     * case this property is set to true (i.e. in AssetAdmin).
39
     */
40
    private static $treats_subsite_0_as_global = false;
0 ignored issues
show
introduced by Werner M. Krauß
The private property $treats_subsite_0_as_global is not used, and could be removed.
Loading history...
41
42
    public function init()
43
    {
44
        Requirements::css('silverstripe/subsites:css/LeftAndMain_Subsites.css');
45
        Requirements::javascript('silverstripe/subsites:javascript/LeftAndMain_Subsites.js');
46
        Requirements::javascript('silverstripe/subsites:javascript/VirtualPage_Subsites.js');
47
    }
48
49
    /**
50
     * Set the title of the CMS tree
51
     */
52
    public function getCMSTreeTitle()
53
    {
54
        $subsite = Subsite::currentSubsite();
55
        return $subsite ? Convert::raw2xml($subsite->Title) : _t(__CLASS__.'.SITECONTENTLEFT', 'Site Content');
0 ignored issues
show
introduced by Robbie Averill
$subsite is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
56
    }
57
58
    public function updatePageOptions(&$fields)
59
    {
60
        $fields->push(HiddenField::create('SubsiteID', 'SubsiteID', SubsiteState::singleton()->getSubsiteId()));
61
    }
62
63
    /**
64
     * Find all subsites accessible for current user on this controller.
65
     *
66
     * @param bool $includeMainSite
67
     * @param string $mainSiteTitle
68
     * @param null $member
0 ignored issues
show
Documentation Bug introduced by Werner M. Krauß
Are you sure the doc-type for parameter $member is correct as it would always require null to be passed?
Loading history...
69
     * @return ArrayList of <a href='psi_element://Subsite'>Subsite</a> instances.
70
     * instances.
71
     */
72
    public function sectionSites($includeMainSite = true, $mainSiteTitle = 'Main site', $member = null)
73
    {
74
        if ($mainSiteTitle == 'Main site') {
75
            $mainSiteTitle = _t('Subsites.MainSiteTitle', 'Main site');
76
        }
77
78
        // Rationalise member arguments
79
        if (!$member) {
0 ignored issues
show
introduced by Werner M. Krauß
$member is of type null, thus it always evaluated to false.
Loading history...
80
            $member = Security::getCurrentUser();
81
        }
82
        if (!$member) {
0 ignored issues
show
introduced by Werner M. Krauß
$member is of type SilverStripe\Security\Member, thus it always evaluated to true.
Loading history...
83
            return ArrayList::create();
84
        }
85
        if (!is_object($member)) {
86
            $member = DataObject::get_by_id(Member::class, $member);
87
        }
88
89
        // Collect permissions - honour the LeftAndMain::required_permission_codes, current model requires
90
        // us to check if the user satisfies ALL permissions. Code partly copied from LeftAndMain::canView.
91
        $codes = [];
92
        $extraCodes = Config::inst()->get(get_class($this->owner), 'required_permission_codes');
93
        if ($extraCodes !== false) {
94
            if ($extraCodes) {
95
                $codes = array_merge($codes, (array)$extraCodes);
96
            } else {
97
                $codes[] = sprintf('CMS_ACCESS_%s', get_class($this->owner));
98
            }
99
        } else {
100
            // Check overriden - all subsites accessible.
101
            return Subsite::all_sites();
102
        }
103
104
        // Find subsites satisfying all permissions for the Member.
105
        $codesPerSite = [];
106
        $sitesArray = [];
107
        foreach ($codes as $code) {
108
            $sites = Subsite::accessible_sites($code, $includeMainSite, $mainSiteTitle, $member);
109
            foreach ($sites as $site) {
110
                // Build the structure for checking how many codes match.
111
                $codesPerSite[$site->ID][$code] = true;
112
113
                // Retain Subsite objects for later.
114
                $sitesArray[$site->ID] = $site;
115
            }
116
        }
117
118
        // Find sites that satisfy all codes conjuncitvely.
119
        $accessibleSites = new ArrayList();
120
        foreach ($codesPerSite as $siteID => $siteCodes) {
121
            if (count($siteCodes) == count($codes)) {
122
                $accessibleSites->push($sitesArray[$siteID]);
123
            }
124
        }
125
126
        return $accessibleSites;
127
    }
128
129
    /*
130
     * Returns a list of the subsites accessible to the current user.
131
     * It's enough for any section to be accessible for the section to be included.
132
     */
133
    public function Subsites()
134
    {
135
        return Subsite::all_accessible_sites();
136
    }
137
138
    /*
139
     * Generates a list of subsites with the data needed to
140
     * produce a dropdown site switcher
141
     * @return ArrayList
142
     */
143
144
    public function ListSubsites()
145
    {
146
        $list = $this->Subsites();
147
        $currentSubsiteID = SubsiteState::singleton()->getSubsiteId();
148
149
        if ($list == null || $list->count() == 1 && $list->first()->DefaultSite == true) {
150
            return false;
0 ignored issues
show
Bug Best Practice introduced by Werner M. Krauß
The expression return false returns the type false which is incompatible with the documented return type SilverStripe\ORM\ArrayList.
Loading history...
151
        }
152
153
        Requirements::javascript('silverstripe/subsites:javascript/LeftAndMain_Subsites.js');
154
155
        $output = ArrayList::create();
156
157
        foreach ($list as $subsite) {
158
            $currentState = $subsite->ID == $currentSubsiteID ? 'selected' : '';
159
160
            $output->push(ArrayData::create([
161
                'CurrentState' => $currentState,
162
                'ID' => $subsite->ID,
163
                'Title' => $subsite->Title,
164
            ]));
165
        }
166
167
        return $output;
168
    }
169
170
    public function alternateMenuDisplayCheck($controllerName)
171
    {
172
        if (!class_exists($controllerName)) {
173
            return false;
174
        }
175
176
        // Don't display SubsiteXHRController
177
        if (singleton($controllerName) instanceof SubsiteXHRController) {
178
            return false;
179
        }
180
181
        // Check subsite support.
182
        if (SubsiteState::singleton()->getSubsiteId() == 0) {
183
            // Main site always supports everything.
184
            return true;
185
        }
186
187
        // It's not necessary to check access permissions here. Framework calls canView on the controller,
188
        // which in turn uses the Permission API which is augmented by our GroupSubsites.
189
        $controller = singleton($controllerName);
190
        return $controller->hasMethod('subsiteCMSShowInMenu') && $controller->subsiteCMSShowInMenu();
191
    }
192
193
    public function CanAddSubsites()
194
    {
195
        return Permission::check('ADMIN', 'any', null, 'all');
0 ignored issues
show
Bug introduced by Werner M. Krauß
'all' of type string is incompatible with the type boolean expected by parameter $strict of SilverStripe\Security\Permission::check(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

195
        return Permission::check('ADMIN', 'any', null, /** @scrutinizer ignore-type */ 'all');
Loading history...
196
    }
197
198
    /**
199
     * Helper for testing if the subsite should be adjusted.
200
     * @param string $adminClass
201
     * @param int $recordSubsiteID
202
     * @param int $currentSubsiteID
203
     * @return bool
204
     */
205
    public function shouldChangeSubsite($adminClass, $recordSubsiteID, $currentSubsiteID)
206
    {
207
        if (Config::inst()->get($adminClass, 'treats_subsite_0_as_global') && $recordSubsiteID == 0) {
208
            return false;
209
        }
210
        if ($recordSubsiteID != $currentSubsiteID) {
211
            return true;
212
        }
213
        return false;
214
    }
215
216
    /**
217
     * Check if the current controller is accessible for this user on this subsite.
218
     *
219
     * @param Member $member
220
     */
221
    public function canAccess(Member $member = null)
222
    {
223
        if (!$member) {
224
            $member = Security::getCurrentUser();
225
        }
226
227
        // Admin can access everything, no point in checking.
228
        if ($member
229
            && (Permission::checkMember($member, [
230
                'ADMIN', // Full administrative rights
231
                'CMS_ACCESS_LeftAndMain', // Access to all CMS sections
232
                'CMS_ACCESS_CMSMain', // Access to CMS controllers
233
            ]))
234
        ) {
235
            return true;
236
        }
237
238
        // Check if we have access to current section on the current subsite.
239
        $accessibleSites = $this->owner->sectionSites(true, 'Main site', $member);
240
        return $accessibleSites->count() && $accessibleSites->find('ID', SubsiteState::singleton()->getSubsiteId());
241
    }
242
243
    /**
244
     * Prevent accessing disallowed resources. This happens after onBeforeInit has executed,
245
     * so all redirections should've already taken place.
246
     *
247
     * @param Member $member
248
     */
249
    public function alternateAccessCheck(Member $member = null)
250
    {
251
        return $this->owner->canAccess($member);
252
    }
253
254
    /**
255
     * Redirect the user to something accessible if the current section/subsite is forbidden.
256
     *
257
     * This is done via onBeforeInit as it needs to be done before the LeftAndMain::init has a
258
     * chance to forbids access via alternateAccessCheck.
259
     *
260
     * If we need to change the subsite we force the redirection to /admin/ so the frontend is
261
     * fully re-synchronised with the internal session. This is better than risking some panels
262
     * showing data from another subsite.
263
     */
264
    public function onBeforeInit()
265
    {
266
        $request = Controller::curr()->getRequest();
267
        $session = $request->getSession();
268
269
        $state = SubsiteState::singleton();
270
271
        // FIRST, check if we need to change subsites due to the URL.
272
273
        // Catch forced subsite changes that need to cause CMS reloads.
274
        if ($request->getVar('SubsiteID') !== null) {
275
            // Clear current page when subsite changes (or is set for the first time)
276
            if ($state->getSubsiteIdWasChanged()) {
277
                // sessionNamespace() is protected - see for info
278
                $override = $this->owner->config()->get('session_namespace');
279
                $sessionNamespace = $override ? $override : get_class($this->owner);
280
                $session->clear($sessionNamespace . '.currentPage');
281
            }
282
283
            // Context: Subsite ID has already been set to the state via InitStateMiddleware
284
285
            // If the user cannot view the current page, redirect to the admin landing section
286
            if (!$this->owner->canView()) {
287
                return $this->owner->redirect(AdminRootController::config()->get('url_base') . '/');
288
            }
289
290
            $currentController = Controller::curr();
291
            if ($currentController instanceof CMSPageEditController) {
292
                /** @var SiteTree $page */
293
                $page = $currentController->currentPage();
294
295
                // If the page exists but doesn't belong to the requested subsite, redirect to admin/pages which
296
                // will show a list of the requested subsite's pages
297
                $currentSubsiteId = $request->getVar('SubsiteID');
298
                if ($page && (int) $page->SubsiteID !== (int) $currentSubsiteId) {
299
                    return $this->owner->redirect(CMSPagesController::singleton()->Link());
300
                }
301
302
                // Page does belong to the current subsite, so remove the query string parameter and refresh the page
303
                // Remove the subsiteID parameter and redirect back to the current URL again
304
                $request->offsetSet('SubsiteID', null);
305
                return $this->owner->redirect($request->getURL(true));
306
            }
307
308
            // Redirect back to the default admin URL
309
            return $this->owner->redirect($request->getURL());
310
        }
311
312
        // Automatically redirect the session to appropriate subsite when requesting a record.
313
        // This is needed to properly initialise the session in situations where someone opens the CMS via a link.
314
        $record = $this->owner->currentPage();
315
        if ($record
316
            && isset($record->SubsiteID, $this->owner->urlParams['ID'])
317
            && is_numeric($record->SubsiteID)
318
            && $this->shouldChangeSubsite(
319
                get_class($this->owner),
320
                $record->SubsiteID,
321
                SubsiteState::singleton()->getSubsiteId()
322
            )
323
        ) {
324
            // Update current subsite
325
            $canViewElsewhere = SubsiteState::singleton()->withState(function ($newState) use ($record) {
326
                $newState->setSubsiteId($record->SubsiteID);
327
328
                return (bool) $this->owner->canView(Security::getCurrentUser());
329
            });
330
331
            if ($canViewElsewhere) {
332
                // Redirect to clear the current page
333
                return $this->owner->redirect(
334
                    Controller::join_links($this->owner->Link('show'), $record->ID, '?SubsiteID=' . $record->SubsiteID)
335
                );
336
            }
337
            // Redirect to the default CMS section
338
            return $this->owner->redirect(AdminRootController::config()->get('url_base') . '/');
339
        }
340
341
        // SECOND, check if we need to change subsites due to lack of permissions.
342
343
        if (!$this->owner->canAccess()) {
344
            $member = Security::getCurrentUser();
345
346
            // Current section is not accessible, try at least to stick to the same subsite.
347
            $menu = CMSMenu::get_menu_items();
348
            foreach ($menu as $candidate) {
349
                if ($candidate->controller && $candidate->controller != get_class($this->owner)) {
350
                    $accessibleSites = singleton($candidate->controller)->sectionSites(true, 'Main site', $member);
351
                    if ($accessibleSites->count()
352
                        && $accessibleSites->find('ID', SubsiteState::singleton()->getSubsiteId())
353
                    ) {
354
                        // Section is accessible, redirect there.
355
                        return $this->owner->redirect(singleton($candidate->controller)->Link());
356
                    }
357
                }
358
            }
359
360
            // If no section is available, look for other accessible subsites.
361
            foreach ($menu as $candidate) {
362
                if ($candidate->controller) {
363
                    $accessibleSites = singleton($candidate->controller)->sectionSites(true, 'Main site', $member);
364
                    if ($accessibleSites->count()) {
365
                        Subsite::changeSubsite($accessibleSites->First()->ID);
366
                        return $this->owner->redirect(singleton($candidate->controller)->Link());
367
                    }
368
                }
369
            }
370
371
            // We have not found any accessible section or subsite. User should be denied access.
372
            return Security::permissionFailure($this->owner);
373
        }
374
375
        // Current site is accessible. Allow through.
376
        return;
377
    }
378
379
    public function augmentNewSiteTreeItem(&$item)
380
    {
381
        $request = Controller::curr()->getRequest();
382
        $item->SubsiteID = $request->postVar('SubsiteID') ?: SubsiteState::singleton()->getSubsiteId();
383
    }
384
385
    public function onAfterSave($record)
386
    {
387
        if ($record->hasMethod('NormalRelated') && ($record->NormalRelated() || $record->ReverseRelated())) {
388
            $this->owner->response->addHeader(
389
                'X-Status',
390
                rawurlencode(_t(__CLASS__ . '.Saved', 'Saved, please update related pages.'))
391
            );
392
        }
393
    }
394
395
    /**
396
     * @param array $data
397
     * @param Form $form
0 ignored issues
show
Bug introduced by Robbie Averill
The type SilverStripe\Subsites\Extensions\Form 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...
398
     */
399
    public function copytosubsite($data, $form)
0 ignored issues
show
Unused Code introduced by Damian Mooyman
The parameter $form is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

399
    public function copytosubsite($data, /** @scrutinizer ignore-unused */ $form)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
400
    {
401
        $page = DataObject::get_by_id(SiteTree::class, $data['ID']);
402
        $subsite = DataObject::get_by_id(Subsite::class, $data['CopyToSubsiteID']);
403
        $includeChildren = (isset($data['CopyToSubsiteWithChildren'])) ? $data['CopyToSubsiteWithChildren'] : false;
404
405
        $newPage = $page->duplicateToSubsite($subsite->ID, $includeChildren);
406
        $response = $this->owner->getResponse();
407
        $response->addHeader('X-Reload', true);
408
409
        return $this->owner->redirect(Controller::join_links(
410
            $this->owner->Link('show'),
411
            $newPage->ID
412
        ));
413
    }
414
}
415