Completed
Push — master ( 5cf2d8...46bcff )
by Damian
13s
created

code/extensions/LeftAndMainSubsites.php (2 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\CMSMenu;
6
use SilverStripe\Admin\LeftAndMainExtension;
7
use SilverStripe\CMS\Model\SiteTree;
8
use SilverStripe\Control\Controller;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\Core\Convert;
11
use SilverStripe\Core\Manifest\ModuleLoader;
12
use SilverStripe\Forms\HiddenField;
13
use SilverStripe\ORM\ArrayList;
14
use SilverStripe\ORM\DataObject;
15
use SilverStripe\Security\Member;
16
use SilverStripe\Security\Permission;
17
use SilverStripe\Security\Security;
18
use SilverStripe\Subsites\Controller\SubsiteXHRController;
19
use SilverStripe\Subsites\Model\Subsite;
20
use SilverStripe\Subsites\State\SubsiteState;
21
use SilverStripe\View\ArrayData;
22
use SilverStripe\View\Requirements;
23
24
/**
25
 * Decorator designed to add subsites support to LeftAndMain
26
 *
27
 * @package subsites
28
 */
29
class LeftAndMainSubsites extends LeftAndMainExtension
30
{
31
    private static $allowed_actions = ['CopyToSubsite'];
32
33
    /**
34
     * Normally SubsiteID=0 on a DataObject means it is only accessible from the special "main site".
35
     * However in some situations SubsiteID=0 will be understood as a "globally accessible" object in which
36
     * case this property is set to true (i.e. in AssetAdmin).
37
     */
38
    private static $treats_subsite_0_as_global = false;
39
40
    public function init()
41
    {
42
        $module = ModuleLoader::getModule('silverstripe/subsites');
43
44
        Requirements::css($module->getRelativeResourcePath('css/LeftAndMain_Subsites.css'));
45
        Requirements::javascript($module->getRelativeResourcePath('javascript/LeftAndMain_Subsites.js'));
46
        Requirements::javascript($module->getRelativeResourcePath('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('LeftAndMain.SITECONTENTLEFT');
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
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) {
80
            $member = Member::currentUser();
81
        }
82
        if (!$member) {
83
            return new ArrayList();
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($this->owner->class, 'required_permission_codes');
93
        if ($extraCodes !== false) {
94
            if ($extraCodes) {
95
                $codes = array_merge($codes, (array)$extraCodes);
96
            } else {
97
                $codes[] = "CMS_ACCESS_{$this->owner->class}";
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 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...
151
        }
152
153
        $module = ModuleLoader::getModule('silverstripe/subsites');
154
        Requirements::javascript($module->getRelativeResourcePath('javascript/LeftAndMain_Subsites.js'));
155
156
        $output = new ArrayList();
157
158
        foreach ($list as $subsite) {
159
            $CurrentState = $subsite->ID == $currentSubsiteID ? 'selected' : '';
160
161
            $output->push(new ArrayData([
162
                'CurrentState' => $CurrentState,
163
                'ID' => $subsite->ID,
164
                'Title' => Convert::raw2xml($subsite->Title)
165
            ]));
166
        }
167
168
        return $output;
169
    }
170
171
    public function alternateMenuDisplayCheck($controllerName)
172
    {
173
        if (!class_exists($controllerName)) {
174
            return false;
175
        }
176
177
        // Don't display SubsiteXHRController
178
        if ($controllerName == SubsiteXHRController::class) {
179
            return false;
180
        }
181
182
        // Check subsite support.
183
        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...
184
            // Main site always supports everything.
185
            return true;
186
        }
187
188
        // It's not necessary to check access permissions here. Framework calls canView on the controller,
189
        // which in turn uses the Permission API which is augmented by our GroupSubsites.
190
        $controller = singleton($controllerName);
191
        return $controller->hasMethod('subsiteCMSShowInMenu') && $controller->subsiteCMSShowInMenu();
192
    }
193
194
    public function CanAddSubsites()
195
    {
196
        return Permission::check('ADMIN', 'any', null, 'all');
197
    }
198
199
    /**
200
     * Helper for testing if the subsite should be adjusted.
201
     * @param string $adminClass
202
     * @param int $recordSubsiteID
203
     * @param int $currentSubsiteID
204
     * @return bool
205
     */
206
    public function shouldChangeSubsite($adminClass, $recordSubsiteID, $currentSubsiteID)
207
    {
208
        if (Config::inst()->get($adminClass, 'treats_subsite_0_as_global') && $recordSubsiteID == 0) {
209
            return false;
210
        }
211
        if ($recordSubsiteID != $currentSubsiteID) {
212
            return true;
213
        }
214
        return false;
215
    }
216
217
    /**
218
     * Check if the current controller is accessible for this user on this subsite.
219
     */
220
    public function canAccess()
221
    {
222
        // Admin can access everything, no point in checking.
223
        $member = Member::currentUser();
224
        if ($member &&
225
        (
226
            Permission::checkMember($member, 'ADMIN') || // 'Full administrative rights' in SecurityAdmin
227
            Permission::checkMember($member, 'CMS_ACCESS_LeftAndMain') // 'Access to all CMS sections' in SecurityAdmin
228
        )) {
229
            return true;
230
        }
231
232
        // Check if we have access to current section on the current subsite.
233
        $accessibleSites = $this->owner->sectionSites(true, 'Main site', $member);
234
        return $accessibleSites->count() && $accessibleSites->find('ID', SubsiteState::singleton()->getSubsiteId());
235
    }
236
237
    /**
238
     * Prevent accessing disallowed resources. This happens after onBeforeInit has executed,
239
     * so all redirections should've already taken place.
240
     */
241
    public function alternateAccessCheck()
242
    {
243
        return $this->owner->canAccess();
244
    }
245
246
    /**
247
     * Redirect the user to something accessible if the current section/subsite is forbidden.
248
     *
249
     * This is done via onBeforeInit as it needs to be done before the LeftAndMain::init has a
250
     * chance to forbids access via alternateAccessCheck.
251
     *
252
     * If we need to change the subsite we force the redirection to /admin/ so the frontend is
253
     * fully re-synchronised with the internal session. This is better than risking some panels
254
     * showing data from another subsite.
255
     */
256
    public function onBeforeInit()
257
    {
258
        // We are accessing the CMS, so we need to let Subsites know we will be using the session.
259
        Subsite::$use_session_subsiteid = true;
260
261
        $session = Controller::curr()->getRequest()->getSession();
262
263
        // FIRST, check if we need to change subsites due to the URL.
264
265
        // Catch forced subsite changes that need to cause CMS reloads.
266
        if (isset($_GET['SubsiteID'])) {
267
            // Clear current page when subsite changes (or is set for the first time)
268
            if (!$session->get('SubsiteID') || $_GET['SubsiteID'] != $session->get('SubsiteID')) {
269
                $session->clear("{$this->owner->class}.currentPage");
270
            }
271
272
            // Update current subsite in session
273
            Subsite::changeSubsite($_GET['SubsiteID']);
274
275
            //Redirect to clear the current page
276
            if ($this->owner->canView(Member::currentUser())) {
277
                //Redirect to clear the current page
278
                return $this->owner->redirect($this->owner->Link());
279
            }
280
            //Redirect to the default CMS section
281
            return $this->owner->redirect('admin/');
282
        }
283
284
        // Automatically redirect the session to appropriate subsite when requesting a record.
285
        // This is needed to properly initialise the session in situations where someone opens the CMS via a link.
286
        $record = $this->owner->currentPage();
287
        if ($record
288
            && isset($record->SubsiteID, $this->owner->urlParams['ID'])
289
            && is_numeric($record->SubsiteID)
290
            && $this->shouldChangeSubsite(
291
                $this->owner->class,
292
                $record->SubsiteID,
293
                SubsiteState::singleton()->getSubsiteId()
294
            )
295
        ) {
296
            // Update current subsite in session
297
            Subsite::changeSubsite($record->SubsiteID);
298
299
            if ($this->owner->canView(Member::currentUser())) {
300
                //Redirect to clear the current page
301
                return $this->owner->redirect($this->owner->Link());
302
            }
303
            //Redirect to the default CMS section
304
            return $this->owner->redirect('admin/');
305
        }
306
307
        // SECOND, check if we need to change subsites due to lack of permissions.
308
309
        if (!$this->owner->canAccess()) {
310
            $member = Member::currentUser();
311
312
            // Current section is not accessible, try at least to stick to the same subsite.
313
            $menu = CMSMenu::get_menu_items();
314
            foreach ($menu as $candidate) {
315
                if ($candidate->controller && $candidate->controller != $this->owner->class) {
316
                    $accessibleSites = singleton($candidate->controller)->sectionSites(true, 'Main site', $member);
317
                    if ($accessibleSites->count()
318
                        && $accessibleSites->find('ID', SubsiteState::singleton()->getSubsiteId())
319
                    ) {
320
                        // Section is accessible, redirect there.
321
                        return $this->owner->redirect(singleton($candidate->controller)->Link());
322
                    }
323
                }
324
            }
325
326
            // If no section is available, look for other accessible subsites.
327
            foreach ($menu as $candidate) {
328
                if ($candidate->controller) {
329
                    $accessibleSites = singleton($candidate->controller)->sectionSites(true, 'Main site', $member);
330
                    if ($accessibleSites->count()) {
331
                        Subsite::changeSubsite($accessibleSites->First()->ID);
332
                        return $this->owner->redirect(singleton($candidate->controller)->Link());
333
                    }
334
                }
335
            }
336
337
            // We have not found any accessible section or subsite. User should be denied access.
338
            return Security::permissionFailure($this->owner);
339
        }
340
341
        // Current site is accessible. Allow through.
342
        return;
343
    }
344
345
    public function augmentNewSiteTreeItem(&$item)
346
    {
347
        $item->SubsiteID = isset($_POST['SubsiteID']) ? $_POST['SubsiteID'] : SubsiteState::singleton()->getSubsiteId();
348
    }
349
350
    public function onAfterSave($record)
351
    {
352
        if ($record->hasMethod('NormalRelated') && ($record->NormalRelated() || $record->ReverseRelated())) {
353
            $this->owner->response->addHeader(
354
                'X-Status',
355
                rawurlencode(_t('LeftAndMainSubsites.Saved', 'Saved, please update related pages.'))
356
            );
357
        }
358
    }
359
360
    /**
361
     * @param array $data
362
     * @param Form $form
363
     */
364
    public function copytosubsite($data, $form)
365
    {
366
        $page = DataObject::get_by_id('SiteTree', $data['ID']);
367
        $subsite = DataObject::get_by_id('Subsite', $data['CopyToSubsiteID']);
368
        $includeChildren = (isset($data['CopyToSubsiteWithChildren'])) ? $data['CopyToSubsiteWithChildren'] : false;
369
370
        $newPage = $page->duplicateToSubsite($subsite->ID, $includeChildren);
371
        $response = $this->owner->getResponse();
372
        $response->addHeader('X-Reload', true);
373
374
        return $this->owner->redirect(Controller::join_links(
375
            $this->owner->Link('show'),
376
            $newPage->ID
377
        ));
378
    }
379
}
380