LeftAndMainSubsites::alternateAccessCheck()   A
last analyzed

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 1
dl 0
loc 3
rs 10
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
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
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
$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
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
$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
$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
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
'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
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
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