SiteTreeSubsites::canEdit()   A
last analyzed

Complexity

Conditions 6
Paths 10

Size

Total Lines 28
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 11
c 1
b 0
f 0
nc 10
nop 1
dl 0
loc 28
rs 9.2222
1
<?php
2
3
namespace SilverStripe\Subsites\Extensions;
4
5
use Page;
0 ignored issues
show
Bug introduced by
The type Page 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...
6
use SilverStripe\CMS\Forms\SiteTreeURLSegmentField;
7
use SilverStripe\CMS\Model\SiteTree;
8
use SilverStripe\Control\Controller;
9
use SilverStripe\Control\Director;
10
use SilverStripe\Control\HTTP;
11
use SilverStripe\Core\Config\Config;
12
use SilverStripe\Core\Convert;
13
use SilverStripe\Forms\CheckboxField;
14
use SilverStripe\Forms\DropdownField;
15
use SilverStripe\Forms\FieldList;
16
use SilverStripe\Forms\FormAction;
17
use SilverStripe\Forms\ToggleCompositeField;
18
use SilverStripe\i18n\i18n;
19
use SilverStripe\ORM\ArrayList;
20
use SilverStripe\ORM\DataExtension;
21
use SilverStripe\ORM\DataObject;
22
use SilverStripe\ORM\DataQuery;
23
use SilverStripe\ORM\Map;
24
use SilverStripe\ORM\Queries\SQLSelect;
25
use SilverStripe\Security\Member;
26
use SilverStripe\Security\Security;
27
use SilverStripe\SiteConfig\SiteConfig;
28
use SilverStripe\Subsites\Model\Subsite;
29
use SilverStripe\Subsites\Service\ThemeResolver;
30
use SilverStripe\Subsites\State\SubsiteState;
31
use SilverStripe\View\SSViewer;
32
33
/**
34
 * Extension for the SiteTree object to add subsites support
35
 */
36
class SiteTreeSubsites extends DataExtension
37
{
38
    private static $has_one = [
39
        'Subsite' => Subsite::class, // The subsite that this page belongs to
40
    ];
41
42
    private static $many_many = [
43
        'CrossSubsiteLinkTracking' => SiteTree::class // Stored separately, as the logic for URL rewriting is different
44
    ];
45
46
    private static $many_many_extraFields = [
47
        'CrossSubsiteLinkTracking' => ['FieldName' => 'Varchar']
48
    ];
49
50
    public function isMainSite()
51
    {
52
        return $this->owner->SubsiteID == 0;
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $this->owner->SubsiteID of type mixed|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...
53
    }
54
55
    /**
56
     * Update any requests to limit the results to the current site
57
     * @param SQLSelect $query
58
     * @param DataQuery $dataQuery
59
     */
60
    public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null)
61
    {
62
        if (Subsite::$disable_subsite_filter) {
63
            return;
64
        }
65
        if ($dataQuery && $dataQuery->getQueryParam('Subsite.filter') === false) {
0 ignored issues
show
introduced by
The condition $dataQuery->getQueryPara...site.filter') === false is always false.
Loading history...
66
            return;
67
        }
68
69
        // If you're querying by ID, ignore the sub-site - this is a bit ugly...
70
        // if(!$query->where
71
        // || (strpos($query->where[0], ".\"ID\" = ") === false
72
        // && strpos($query->where[0], ".`ID` = ") === false && strpos($query->where[0], ".ID = ") === false
73
        // && strpos($query->where[0], "ID = ") !== 0)) {
74
        if ($query->filtersOnID()) {
75
            return;
76
        }
77
78
        $subsiteID = null;
79
        if (Subsite::$force_subsite) {
80
            $subsiteID = Subsite::$force_subsite;
81
        } else {
82
            $subsiteID = SubsiteState::singleton()->getSubsiteId();
83
        }
84
85
        if ($subsiteID === null) {
86
            return;
87
        }
88
89
        // The foreach is an ugly way of getting the first key :-)
90
        foreach ($query->getFrom() as $tableName => $info) {
91
            // The tableName should be SiteTree or SiteTree_Live...
92
            $siteTreeTableName = SiteTree::getSchema()->tableName(SiteTree::class);
93
            if (strpos($tableName, $siteTreeTableName) === false) {
94
                break;
95
            }
96
            $query->addWhere("\"$tableName\".\"SubsiteID\" IN ($subsiteID)");
97
            break;
98
        }
99
    }
100
101
    public function onBeforeWrite()
102
    {
103
        if (!$this->owner->ID && !$this->owner->SubsiteID) {
104
            $this->owner->SubsiteID = SubsiteState::singleton()->getSubsiteId();
105
        }
106
107
        parent::onBeforeWrite();
108
    }
109
110
    public function updateCMSFields(FieldList $fields)
111
    {
112
        $subsites = Subsite::accessible_sites('CMS_ACCESS_CMSMain');
113
        if ($subsites && $subsites->count()) {
114
            $subsitesToMap = $subsites->exclude('ID', $this->owner->SubsiteID);
115
            $subsitesMap = $subsitesToMap->map('ID', 'Title');
116
        } else {
117
            $subsitesMap = new Map(ArrayList::create());
118
        }
119
120
        // Master page edit field (only allowed from default subsite to avoid inconsistent relationships)
121
        $isDefaultSubsite = $this->owner->SubsiteID == 0 || $this->owner->Subsite()->DefaultSite;
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $this->owner->SubsiteID of type mixed|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...
122
123
        if ($isDefaultSubsite && $subsitesMap->count()) {
124
            $fields->addFieldToTab(
125
                'Root.Main',
126
                ToggleCompositeField::create(
127
                    'SubsiteOperations',
128
                    _t(__CLASS__ . '.SubsiteOperations', 'Subsite Operations'),
129
                    [
130
                        DropdownField::create('CopyToSubsiteID', _t(
131
                            __CLASS__ . '.CopyToSubsite',
132
                            'Copy page to subsite'
133
                        ), $subsitesMap),
134
                        CheckboxField::create(
135
                            'CopyToSubsiteWithChildren',
136
                            _t(__CLASS__ . '.CopyToSubsiteWithChildren', 'Include children pages?')
137
                        ),
138
                        $copyAction = FormAction::create(
139
                            'copytosubsite',
140
                            _t(__CLASS__ . '.CopyAction', 'Copy')
141
                        )
142
                    ]
143
                )->setHeadingLevel(4)
144
            );
145
146
            $copyAction->addExtraClass('btn btn-primary font-icon-save ml-3');
147
148
            // @todo check if this needs re-implementation
149
//            $copyAction->includeDefaultJS(false);
150
        }
151
152
        // replace readonly link prefix
153
        $subsite = $this->owner->Subsite();
154
        $nested_urls_enabled = Config::inst()->get(SiteTree::class, 'nested_urls');
155
        /** @var Subsite $subsite */
156
        if ($subsite && $subsite->exists()) {
157
            // Use baseurl from domain
158
            $baseLink = $subsite->absoluteBaseURL();
159
160
            // Add parent page if enabled
161
            if ($nested_urls_enabled && $this->owner->ParentID) {
162
                $baseLink = Controller::join_links(
163
                    $baseLink,
164
                    $this->owner->Parent()->RelativeLink(true)
165
                );
166
            }
167
168
            $urlsegment = $fields->dataFieldByName('URLSegment');
169
            if ($urlsegment && $urlsegment instanceof SiteTreeURLSegmentField) {
170
                $urlsegment->setURLPrefix($baseLink);
171
            }
172
        }
173
    }
174
175
    /**
176
     * Does the basic duplication, but doesn't write anything this means we can subclass this easier and do more
177
     * complex relation duplication.
178
     *
179
     * Note that when duplicating including children, everything is written.
180
     *
181
     * @param Subsite|int $subsiteID
182
     * @param bool $includeChildren
183
     * @return SiteTree
184
     */
185
    public function duplicateToSubsitePrep($subsiteID, $includeChildren)
186
    {
187
        if (is_object($subsiteID)) {
188
            $subsiteID = $subsiteID->ID;
189
        }
190
191
        return SubsiteState::singleton()
192
            ->withState(function (SubsiteState $newState) use ($subsiteID, $includeChildren) {
193
                $newState->setSubsiteId($subsiteID);
194
195
                /** @var SiteTree $page */
196
                $page = $this->owner;
197
198
                try {
199
                    // We have no idea what the ParentID should be, but it shouldn't be the same as it was since
200
                    // we're now in a different subsite. As a workaround use the url-segment and subsite ID.
201
                    if ($page->Parent()) {
202
                        $parentSeg = $page->Parent()->URLSegment;
203
                        $newParentPage = Page::get()->filter('URLSegment', $parentSeg)->first();
204
                        $originalParentID = $page->ParentID;
205
                        if ($newParentPage) {
206
                            $page->ParentID = (int) $newParentPage->ID;
0 ignored issues
show
Bug Best Practice introduced by
The property ParentID does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
207
                        } else {
208
                            // reset it to the top level, so the user can decide where to put it
209
                            $page->ParentID = 0;
210
                        }
211
                    }
212
213
                    // Disable query filtering by subsite during actual duplication
214
                    $originalFilter = Subsite::$disable_subsite_filter;
215
                    Subsite::disable_subsite_filter(true);
216
217
                    return $includeChildren ? $page->duplicateWithChildren() : $page->duplicate(false);
218
                } finally {
219
                    Subsite::disable_subsite_filter($originalFilter);
220
221
                    // Re-set the original parent ID for the current page
222
                    $page->ParentID = $originalParentID;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $originalParentID does not seem to be defined for all execution paths leading up to this point.
Loading history...
223
                }
224
            });
225
    }
226
227
    /**
228
     * When duplicating a page, assign the current subsite ID from the state
229
     */
230
    public function onBeforeDuplicate()
231
    {
232
        $subsiteId = SubsiteState::singleton()->getSubsiteId();
233
        if ($subsiteId !== null) {
234
            $this->owner->SubsiteID = $subsiteId;
235
        }
236
    }
237
238
    /**
239
     * Create a duplicate of this page and save it to another subsite
240
     *
241
     * @param Subsite|int $subsiteID   The Subsite to copy to, or its ID
242
     * @param boolean $includeChildren Whether to duplicate child pages too
243
     * @return SiteTree                The duplicated page
244
     */
245
    public function duplicateToSubsite($subsiteID = null, $includeChildren = false)
246
    {
247
        $clone = $this->owner->duplicateToSubsitePrep($subsiteID, $includeChildren);
248
        $clone->invokeWithExtensions('onBeforeDuplicateToSubsite', $this->owner);
249
250
        if (!$includeChildren) {
251
            // Write the new page if "include children" is false, because it is written by default when it's true.
252
            $clone->write();
253
        }
254
        // Deprecated: manually duplicate any configured relationships
255
        $clone->duplicateSubsiteRelations($this->owner);
256
257
        $clone->invokeWithExtensions('onAfterDuplicateToSubsite', $this->owner);
258
259
        return $clone;
260
    }
261
262
    /**
263
     * Duplicate relations using a static property to define
264
     * which ones we want to duplicate
265
     *
266
     * It may be that some relations are not diostinct to sub site so can stay
267
     * whereas others may need to be duplicated
268
     *
269
     * @deprecated 2.2..3.0 Use the "cascade_duplicates" config API instead
270
     * @param SiteTree $originalPage
271
     */
272
    public function duplicateSubsiteRelations($originalPage)
273
    {
274
        $thisClass = $originalPage->ClassName;
275
        $relations = Config::inst()->get($thisClass, 'duplicate_to_subsite_relations');
276
277
        if ($relations && !empty($relations)) {
278
            foreach ($relations as $relation) {
279
                $items = $originalPage->$relation();
280
                foreach ($items as $item) {
281
                    $duplicateItem = $item->duplicate(false);
282
                    $duplicateItem->{$thisClass.'ID'} = $this->owner->ID;
283
                    $duplicateItem->write();
284
                }
285
            }
286
        }
287
    }
288
289
    /**
290
     * @return SiteConfig
291
     */
292
    public function alternateSiteConfig()
293
    {
294
        if (!$this->owner->SubsiteID) {
295
            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\SiteConfig\SiteConfig.
Loading history...
296
        }
297
        $sc = DataObject::get_one(SiteConfig::class, '"SubsiteID" = ' . $this->owner->SubsiteID);
298
        if (!$sc) {
299
            $sc = new SiteConfig();
300
            $sc->SubsiteID = $this->owner->SubsiteID;
301
            $sc->Title = _t('SilverStripe\\Subsites\\Model\\Subsite.SiteConfigTitle', 'Your Site Name');
302
            $sc->Tagline = _t('SilverStripe\\Subsites\\Model\\Subsite.SiteConfigSubtitle', 'Your tagline here');
303
            $sc->write();
304
        }
305
        return $sc;
306
    }
307
308
    /**
309
     * Only allow editing of a page if the member satisfies one of the following conditions:
310
     * - Is in a group which has access to the subsite this page belongs to
311
     * - Is in a group with edit permissions on the "main site"
312
     *
313
     * If there are no subsites configured yet, this logic is skipped.
314
     *
315
     * @param Member|null $member
316
     * @return bool|null
317
     */
318
    public function canEdit($member = null)
319
    {
320
        if (!$member) {
321
            $member = Security::getCurrentUser();
322
        }
323
324
        // Do not provide any input if there are no subsites configured
325
        if (!Subsite::get()->exists()) {
326
            return null;
327
        }
328
329
        // Find the sites that this user has access to
330
        $goodSites = Subsite::accessible_sites('CMS_ACCESS_CMSMain', true, 'all', $member)->column('ID');
331
332
        if (!is_null($this->owner->SubsiteID)) {
333
            $subsiteID = $this->owner->SubsiteID;
334
        } else {
335
            // The relationships might not be available during the record creation when using a GridField.
336
            // In this case the related objects will have empty fields, and SubsiteID will not be available.
337
            //
338
            // We do the second best: fetch the likely SubsiteID from the session. The drawback is this might
339
            // make it possible to force relations to point to other (forbidden) subsites.
340
            $subsiteID = SubsiteState::singleton()->getSubsiteId();
341
        }
342
343
        // Return true if they have access to this object's site
344
        if (!(in_array(0, $goodSites) || in_array($subsiteID, $goodSites))) {
345
            return false;
346
        }
347
    }
348
349
    /**
350
     * @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...
351
     * @return bool
352
     */
353
    public function canDelete($member = null)
354
    {
355
        if (!$member && $member !== false) {
0 ignored issues
show
introduced by
$member is of type null, thus it always evaluated to false.
Loading history...
introduced by
The condition $member !== false is always true.
Loading history...
356
            $member = Security::getCurrentUser();
357
        }
358
359
        return $this->canEdit($member);
360
    }
361
362
    /**
363
     * @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...
364
     * @return bool
365
     */
366
    public function canAddChildren($member = null)
367
    {
368
        if (!$member && $member !== false) {
0 ignored issues
show
introduced by
$member is of type null, thus it always evaluated to false.
Loading history...
introduced by
The condition $member !== false is always true.
Loading history...
369
            $member = Security::getCurrentUser();
370
        }
371
372
        return $this->canEdit($member);
373
    }
374
375
    /**
376
     * @param Member|null $member
377
     * @return bool|null
378
     */
379
    public function canPublish($member = null)
380
    {
381
        if (!$member && $member !== false) {
0 ignored issues
show
introduced by
The condition $member !== false is always false.
Loading history...
382
            $member = Security::getCurrentUser();
383
        }
384
385
        return $this->canEdit($member);
386
    }
387
388
    /**
389
     * Called by ContentController::init();
390
     * @param $controller
391
     */
392
    public static function contentcontrollerInit($controller)
393
    {
394
        /** @var Subsite $subsite */
395
        $subsite = Subsite::currentSubsite();
396
397
        if ($subsite && $subsite->Theme) {
0 ignored issues
show
Bug Best Practice introduced by
The property Theme does not exist on SilverStripe\Subsites\Model\Subsite. Since you implemented __get, consider adding a @property annotation.
Loading history...
398
            SSViewer::set_themes(ThemeResolver::singleton()->getThemeList($subsite));
399
        }
400
401
        if ($subsite && i18n::getData()->validate($subsite->Language)) {
0 ignored issues
show
Bug Best Practice introduced by
The property Language does not exist on SilverStripe\Subsites\Model\Subsite. Since you implemented __get, consider adding a @property annotation.
Loading history...
402
            i18n::set_locale($subsite->Language);
403
        }
404
    }
405
406
    /**
407
     * @param null $action
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $action is correct as it would always require null to be passed?
Loading history...
408
     * @return string
409
     */
410
    public function alternateAbsoluteLink($action = null)
411
    {
412
        // Generate the existing absolute URL and replace the domain with the subsite domain.
413
        // This helps deal with Link() returning an absolute URL.
414
        $url = Director::absoluteURL($this->owner->Link($action));
415
        if ($this->owner->SubsiteID) {
416
            $url = preg_replace('/\/\/[^\/]+\//', '//' . $this->owner->Subsite()->domain() . '/', $url);
417
        }
418
        return $url;
419
    }
420
421
    /**
422
     * Use the CMS domain for iframed CMS previews to prevent single-origin violations
423
     * and SSL cert problems. Always set SubsiteID to avoid errors because a page doesn't
424
     * exist on the CMS domain.
425
     *
426
     * @param string &$link
427
     * @param string|null $action
428
     * @return string
429
     */
430
    public function updatePreviewLink(&$link, $action = null)
431
    {
432
        $url = Director::absoluteURL($this->owner->Link($action));
433
        $link = HTTP::setGetVar('SubsiteID', $this->owner->SubsiteID, $url);
434
        return $link;
435
    }
436
437
    /**
438
     * This function is marked as deprecated for removal in 5.0.0 in silverstripe/cms
439
     * so now simply passes execution to where the functionality exists for backwards compatiblity.
440
     * CMS 4.0.0 SiteTree already throws a SilverStripe deprecation error before calling this function.
441
     * @deprecated 2.2...3.0 use updatePreviewLink instead
442
     *
443
     * @param string|null $action
444
     * @return string
445
     */
446
    public function alternatePreviewLink($action = null)
447
    {
448
        $link = '';
449
        return $this->updatePreviewLink($link, $action);
450
    }
451
452
    /**
453
     * Inject the subsite ID into the content so it can be used by frontend scripts.
454
     * @param $tags
455
     * @return string
456
     */
457
    public function MetaTags(&$tags)
458
    {
459
        if ($this->owner->SubsiteID) {
460
            $tags .= '<meta name="x-subsite-id" content="' . $this->owner->SubsiteID . "\" />\n";
461
        }
462
463
        return $tags;
464
    }
465
466
    public function augmentSyncLinkTracking()
467
    {
468
        // Set LinkTracking appropriately
469
        $links = HTTP::getLinksIn($this->owner->Content);
470
        $linkedPages = [];
471
472
        if ($links) {
473
            foreach ($links as $link) {
474
                if (substr($link, 0, strlen('http://')) == 'http://') {
475
                    $withoutHttp = substr($link, strlen('http://'));
476
                    if (strpos($withoutHttp, '/') && strpos($withoutHttp, '/') < strlen($withoutHttp)) {
477
                        $domain = substr($withoutHttp, 0, strpos($withoutHttp, '/'));
478
                        $rest = substr($withoutHttp, strpos($withoutHttp, '/') + 1);
479
480
                        $subsiteID = Subsite::getSubsiteIDForDomain($domain);
481
                        if ($subsiteID == 0) {
482
                            continue;
483
                        } // We have no idea what the domain for the main site is, so cant track links to it
484
485
                        $origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
486
                        Subsite::disable_subsite_filter(true);
487
                        $candidatePage = DataObject::get_one(
488
                            SiteTree::class,
489
                            "\"URLSegment\" = '"
490
                            . Convert::raw2sql(urldecode($rest))
491
                            . "' AND \"SubsiteID\" = "
492
                            . $subsiteID,
493
                            false
494
                        );
495
                        Subsite::disable_subsite_filter($origDisableSubsiteFilter);
496
497
                        if ($candidatePage) {
498
                            $linkedPages[] = $candidatePage->ID;
499
                        } else {
500
                            $this->owner->HasBrokenLink = true;
501
                        }
502
                    }
503
                }
504
            }
505
        }
506
507
        $this->owner->CrossSubsiteLinkTracking()->setByIDList($linkedPages);
508
    }
509
510
    /**
511
     * Ensure that valid url segments are checked within the correct subsite of the owner object,
512
     * even if the current subsiteID is set to some other subsite.
513
     *
514
     * @return null|bool Either true or false, or null to not influence result
515
     */
516
    public function augmentValidURLSegment()
517
    {
518
        // If this page is being filtered in the current subsite, then no custom validation query is required.
519
        $subsite = Subsite::$force_subsite ?: SubsiteState::singleton()->getSubsiteId();
520
        if (empty($this->owner->SubsiteID) || $subsite == $this->owner->SubsiteID) {
521
            return null;
522
        }
523
524
        // Backup forced subsite
525
        $prevForceSubsite = Subsite::$force_subsite;
526
        Subsite::$force_subsite = $this->owner->SubsiteID;
527
528
        // Repeat validation in the correct subsite
529
        $isValid = $this->owner->validURLSegment();
530
531
        // Restore
532
        Subsite::$force_subsite = $prevForceSubsite;
533
534
        return (bool)$isValid;
535
    }
536
537
    /**
538
     * Return a piece of text to keep DataObject cache keys appropriately specific
539
     */
540
    public function cacheKeyComponent()
541
    {
542
        return 'subsite-' . SubsiteState::singleton()->getSubsiteId();
543
    }
544
545
    /**
546
     * @param Member $member
547
     * @return boolean|null
548
     */
549
    public function canCreate($member = null)
550
    {
551
        // Typically called on a singleton, so we're not using the Subsite() relation
552
        $subsite = Subsite::currentSubsite();
553
        if ($subsite && $subsite->exists() && $subsite->PageTypeBlacklist) {
554
            // SS 4.1: JSON encoded. SS 4.0, comma delimited
555
            $blacklist = json_decode($subsite->PageTypeBlacklist, true);
556
            if ($blacklist === false) {
557
                $blacklist = explode(',', $subsite->PageTypeBlacklist);
558
            }
559
560
            if (in_array(get_class($this->owner), (array) $blacklist)) {
561
                return false;
562
            }
563
        }
564
    }
565
}
566