Completed
Push — master ( 60b259...83077f )
by Will
01:57
created

code/extensions/LeftAndMainSubsites.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\Model\SiteTree;
9
use SilverStripe\CMS\Controllers\CMSPageEditController;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Core\Config\Config;
12
use SilverStripe\Core\Convert;
13
use SilverStripe\Core\Manifest\ModuleLoader;
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
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
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;
41
42
    public function init()
43
    {
44
        $module = ModuleLoader::getModule('silverstripe/subsites');
45
46
        Requirements::css($module->getRelativeResourcePath('css/LeftAndMain_Subsites.css'));
47
        Requirements::javascript($module->getRelativeResourcePath('javascript/LeftAndMain_Subsites.js'));
48
        Requirements::javascript($module->getRelativeResourcePath('javascript/VirtualPage_Subsites.js'));
49
    }
50
51
    /**
52
     * Set the title of the CMS tree
53
     */
54
    public function getCMSTreeTitle()
55
    {
56
        $subsite = Subsite::currentSubsite();
57
        return $subsite ? Convert::raw2xml($subsite->Title) : _t('LeftAndMain.SITECONTENTLEFT');
58
    }
59
60
    public function updatePageOptions(&$fields)
61
    {
62
        $fields->push(HiddenField::create('SubsiteID', 'SubsiteID', SubsiteState::singleton()->getSubsiteId()));
63
    }
64
65
    /**
66
     * Find all subsites accessible for current user on this controller.
67
     *
68
     * @param bool $includeMainSite
69
     * @param string $mainSiteTitle
70
     * @param null $member
71
     * @return ArrayList of <a href='psi_element://Subsite'>Subsite</a> instances.
72
     * instances.
73
     */
74
    public function sectionSites($includeMainSite = true, $mainSiteTitle = 'Main site', $member = null)
75
    {
76
        if ($mainSiteTitle == 'Main site') {
77
            $mainSiteTitle = _t('Subsites.MainSiteTitle', 'Main site');
78
        }
79
80
        // Rationalise member arguments
81
        if (!$member) {
82
            $member = Security::getCurrentUser();
83
        }
84
        if (!$member) {
85
            return ArrayList::create();
86
        }
87
        if (!is_object($member)) {
88
            $member = DataObject::get_by_id(Member::class, $member);
89
        }
90
91
        // Collect permissions - honour the LeftAndMain::required_permission_codes, current model requires
92
        // us to check if the user satisfies ALL permissions. Code partly copied from LeftAndMain::canView.
93
        $codes = [];
94
        $extraCodes = Config::inst()->get(get_class($this->owner), 'required_permission_codes');
95
        if ($extraCodes !== false) {
96
            if ($extraCodes) {
97
                $codes = array_merge($codes, (array)$extraCodes);
98
            } else {
99
                $codes[] = sprintf('CMS_ACCESS_%s', get_class($this->owner));
100
            }
101
        } else {
102
            // Check overriden - all subsites accessible.
103
            return Subsite::all_sites();
104
        }
105
106
        // Find subsites satisfying all permissions for the Member.
107
        $codesPerSite = [];
108
        $sitesArray = [];
109
        foreach ($codes as $code) {
110
            $sites = Subsite::accessible_sites($code, $includeMainSite, $mainSiteTitle, $member);
111
            foreach ($sites as $site) {
112
                // Build the structure for checking how many codes match.
113
                $codesPerSite[$site->ID][$code] = true;
114
115
                // Retain Subsite objects for later.
116
                $sitesArray[$site->ID] = $site;
117
            }
118
        }
119
120
        // Find sites that satisfy all codes conjuncitvely.
121
        $accessibleSites = new ArrayList();
122
        foreach ($codesPerSite as $siteID => $siteCodes) {
123
            if (count($siteCodes) == count($codes)) {
124
                $accessibleSites->push($sitesArray[$siteID]);
125
            }
126
        }
127
128
        return $accessibleSites;
129
    }
130
131
    /*
132
     * Returns a list of the subsites accessible to the current user.
133
     * It's enough for any section to be accessible for the section to be included.
134
     */
135
    public function Subsites()
136
    {
137
        return Subsite::all_accessible_sites();
138
    }
139
140
    /*
141
     * Generates a list of subsites with the data needed to
142
     * produce a dropdown site switcher
143
     * @return ArrayList
144
     */
145
146
    public function ListSubsites()
147
    {
148
        $list = $this->Subsites();
149
        $currentSubsiteID = SubsiteState::singleton()->getSubsiteId();
150
151
        if ($list == null || $list->count() == 1 && $list->first()->DefaultSite == true) {
152
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SilverStripe\Subsites\Ex...nSubsites::ListSubsites of type SilverStripe\ORM\ArrayList.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
153
        }
154
155
        $module = ModuleLoader::getModule('silverstripe/subsites');
156
        Requirements::javascript($module->getRelativeResourcePath('javascript/LeftAndMain_Subsites.js'));
157
158
        $output = ArrayList::create();
159
160
        foreach ($list as $subsite) {
161
            $currentState = $subsite->ID == $currentSubsiteID ? 'selected' : '';
162
163
            $output->push(ArrayData::create([
164
                'CurrentState' => $currentState,
165
                'ID' => $subsite->ID,
166
                'Title' => Convert::raw2xml($subsite->Title)
167
            ]));
168
        }
169
170
        return $output;
171
    }
172
173
    public function alternateMenuDisplayCheck($controllerName)
174
    {
175
        if (!class_exists($controllerName)) {
176
            return false;
177
        }
178
179
        // Don't display SubsiteXHRController
180
        if (singleton($controllerName) instanceof SubsiteXHRController) {
181
            return false;
182
        }
183
184
        // Check subsite support.
185
        if (SubsiteState::singleton()->getSubsiteId() == 0) {
0 ignored issues
show
It seems like you are loosely comparing \SilverStripe\Subsites\S...leton()->getSubsiteId() of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
186
            // Main site always supports everything.
187
            return true;
188
        }
189
190
        // It's not necessary to check access permissions here. Framework calls canView on the controller,
191
        // which in turn uses the Permission API which is augmented by our GroupSubsites.
192
        $controller = singleton($controllerName);
193
        return $controller->hasMethod('subsiteCMSShowInMenu') && $controller->subsiteCMSShowInMenu();
194
    }
195
196
    public function CanAddSubsites()
197
    {
198
        return Permission::check('ADMIN', 'any', null, 'all');
0 ignored issues
show
'all' is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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

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

Loading history...
381
    {
382
        $page = DataObject::get_by_id(SiteTree::class, $data['ID']);
383
        $subsite = DataObject::get_by_id(Subsite::class, $data['CopyToSubsiteID']);
384
        $includeChildren = (isset($data['CopyToSubsiteWithChildren'])) ? $data['CopyToSubsiteWithChildren'] : false;
385
386
        $newPage = $page->duplicateToSubsite($subsite->ID, $includeChildren);
387
        $response = $this->owner->getResponse();
388
        $response->addHeader('X-Reload', true);
389
390
        return $this->owner->redirect(Controller::join_links(
391
            $this->owner->Link('show'),
392
            $newPage->ID
393
        ));
394
    }
395
}
396