Completed
Push — master ( cde41c...73943a )
by Daniel
21s
created

src/Extensions/LeftAndMainSubsites.php (1 issue)

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