Completed
Push — master ( 0ebf95...33622c )
by Damian
12s
created

SiteTreeSubsites::isMainSite()   A

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 0
dl 0
loc 3
rs 10
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\Model\SiteTree;
7
use SilverStripe\Control\Controller;
8
use SilverStripe\Control\Director;
9
use SilverStripe\Control\HTTP;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Core\Convert;
12
use SilverStripe\Forms\CheckboxField;
13
use SilverStripe\Forms\DropdownField;
14
use SilverStripe\Forms\FieldList;
15
use SilverStripe\Forms\FormAction;
16
use SilverStripe\Forms\ToggleCompositeField;
17
use SilverStripe\i18n\i18n;
18
use SilverStripe\ORM\DataExtension;
19
use SilverStripe\ORM\DataObject;
20
use SilverStripe\ORM\DataQuery;
21
use SilverStripe\ORM\Queries\SQLSelect;
22
use SilverStripe\Security\Security;
23
use SilverStripe\SiteConfig\SiteConfig;
24
use SilverStripe\Subsites\Model\Subsite;
25
use SilverStripe\Subsites\State\SubsiteState;
26
use SilverStripe\View\SSViewer;
27
28
/**
29
 * Extension for the SiteTree object to add subsites support
30
 */
31
class SiteTreeSubsites extends DataExtension
32
{
33
    private static $has_one = [
34
        'Subsite' => Subsite::class, // The subsite that this page belongs to
35
    ];
36
37
    private static $many_many = [
38
        'CrossSubsiteLinkTracking' => SiteTree::class // Stored separately, as the logic for URL rewriting is different
39
    ];
40
41
    private static $many_many_extraFields = [
42
        'CrossSubsiteLinkTracking' => ['FieldName' => 'Varchar']
43
    ];
44
45
    public function isMainSite()
46
    {
47
        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 null|mixed 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...
48
    }
49
50
    /**
51
     * Update any requests to limit the results to the current site
52
     * @param SQLSelect $query
53
     * @param DataQuery $dataQuery
54
     */
55
    public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null)
56
    {
57
        if (Subsite::$disable_subsite_filter) {
58
            return;
59
        }
60
        if ($dataQuery && $dataQuery->getQueryParam('Subsite.filter') === false) {
61
            return;
62
        }
63
64
        // If you're querying by ID, ignore the sub-site - this is a bit ugly...
65
        // if(!$query->where || (strpos($query->where[0], ".\"ID\" = ") === false && strpos($query->where[0], ".`ID` = ") === false && strpos($query->where[0], ".ID = ") === false && strpos($query->where[0], "ID = ") !== 0)) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
61% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
66
        if ($query->filtersOnID()) {
67
            return;
68
        }
69
70
        $subsiteID = null;
71
        if (Subsite::$force_subsite) {
72
            $subsiteID = Subsite::$force_subsite;
73
        } else {
74
            $subsiteID = SubsiteState::singleton()->getSubsiteId();
75
        }
76
77
        if ($subsiteID === null) {
78
            return;
79
        }
80
81
        // The foreach is an ugly way of getting the first key :-)
82
        foreach ($query->getFrom() as $tableName => $info) {
83
            // The tableName should be SiteTree or SiteTree_Live...
84
            $siteTreeTableName = SiteTree::getSchema()->tableName(SiteTree::class);
85
            if (strpos($tableName, $siteTreeTableName) === false) {
86
                break;
87
            }
88
            $query->addWhere("\"$tableName\".\"SubsiteID\" IN ($subsiteID)");
89
            break;
90
        }
91
    }
92
93
    public function onBeforeWrite()
94
    {
95 View Code Duplication
        if (!$this->owner->ID && !$this->owner->SubsiteID) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
96
            $this->owner->SubsiteID = SubsiteState::singleton()->getSubsiteId();
97
        }
98
99
        parent::onBeforeWrite();
100
    }
101
102
    public function updateCMSFields(FieldList $fields)
103
    {
104
        $subsites = Subsite::accessible_sites('CMS_ACCESS_CMSMain');
105
        $subsitesMap = [];
106
        if ($subsites && $subsites->count()) {
107
            $subsitesToMap = $subsites->exclude('ID', $this->owner->SubsiteID);
108
            $subsitesMap = $subsitesToMap->map('ID', 'Title');
109
        }
110
111
        // Master page edit field (only allowed from default subsite to avoid inconsistent relationships)
112
        $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 null|mixed 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...
113
114
        if ($isDefaultSubsite && $subsitesMap) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $subsitesMap of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
115
            $fields->addFieldToTab(
116
                'Root.Main',
117
                ToggleCompositeField::create(
118
                    'SubsiteOperations',
119
                    _t(__CLASS__ . '.SubsiteOperations', 'Subsite Operations'),
120
                    [
121
                        new DropdownField('CopyToSubsiteID', _t(
122
                            __CLASS__ . '.CopyToSubsite',
123
                            'Copy page to subsite'
124
                        ), $subsitesMap),
125
                        new CheckboxField(
126
                            'CopyToSubsiteWithChildren',
127
                            _t(__CLASS__ . '.CopyToSubsiteWithChildren', 'Include children pages?')
128
                        ),
129
                        $copyAction = new FormAction(
0 ignored issues
show
Unused Code introduced by
The assignment to $copyAction is dead and can be removed.
Loading history...
130
                            'copytosubsite',
131
                            _t(__CLASS__ . '.CopyAction', 'Copy')
132
                        )
133
                    ]
134
                )->setHeadingLevel(4)
135
            );
136
137
            // @todo check if this needs re-implementation
138
//            $copyAction->includeDefaultJS(false);
139
        }
140
141
        // replace readonly link prefix
142
        $subsite = $this->owner->Subsite();
143
        $nested_urls_enabled = Config::inst()->get(SiteTree::class, 'nested_urls');
144
        if ($subsite && $subsite->exists()) {
145
            // Use baseurl from domain
146
            $baseLink = $subsite->absoluteBaseURL();
147
148
            // Add parent page if enabled
149
            if ($nested_urls_enabled && $this->owner->ParentID) {
150
                $baseLink = Controller::join_links(
151
                    $baseLink,
152
                    $this->owner->Parent()->RelativeLink(true)
153
                );
154
            }
155
156
            $urlsegment = $fields->dataFieldByName('URLSegment');
157
            $urlsegment->setURLPrefix($baseLink);
158
        }
159
    }
160
161
    /**
162
     * Does the basic duplication, but doesn't write anything
163
     * this means we can subclass this easier and do more complex
164
     * relation duplication.
165
     */
166
    public function duplicateToSubsitePrep($subsiteID)
167
    {
168
        if (is_object($subsiteID)) {
169
            $subsiteID = $subsiteID->ID;
170
        }
171
172
        $oldSubsite = SubsiteState::singleton()->getSubsiteId();
173
        if ($subsiteID) {
174
            Subsite::changeSubsite($subsiteID);
175
        } else {
176
            $subsiteID = $oldSubsite;
177
        }
178
        // doesn't write as we need to reset the SubsiteID, ParentID etc
179
        $clone = $this->owner->duplicate(false);
180
        $clone->CheckedPublicationDifferences = $clone->AddedToStage = true;
181
        $subsiteID = ($subsiteID ? $subsiteID : $oldSubsite);
182
        $clone->SubsiteID = $subsiteID;
183
        // We have no idea what the parentID should be, so as a workaround use the url-segment and subsite ID
184
        if ($this->owner->Parent()) {
185
            $parentSeg = $this->owner->Parent()->URLSegment;
186
            $newParentPage = Page::get()->filter('URLSegment', $parentSeg)->first();
187
            if ($newParentPage) {
188
                $clone->ParentID = $newParentPage->ID;
189
            } else {
190
                // reset it to the top level, so the user can decide where to put it
191
                $clone->ParentID = 0;
192
            }
193
        }
194
        // MasterPageID is here for legacy purposes, to satisfy the subsites_relatedpages module
195
        $clone->MasterPageID = $this->owner->ID;
196
        return $clone;
197
    }
198
199
    /**
200
     * Create a duplicate of this page and save it to another subsite
201
     * @param $subsiteID int|Subsite The Subsite to copy to, or its ID
202
     */
203
    public function duplicateToSubsite($subsiteID = null)
204
    {
205
        $clone = $this->owner->duplicateToSubsitePrep($subsiteID);
206
        $clone->invokeWithExtensions('onBeforeDuplicateToSubsite', $this->owner);
207
        $clone->write();
208
        $clone->duplicateSubsiteRelations($this->owner);
209
        // new extension hooks which happens after write,
210
        // onAfterDuplicate isn't reliable due to
211
        // https://github.com/silverstripe/silverstripe-cms/issues/1253
212
        $clone->invokeWithExtensions('onAfterDuplicateToSubsite', $this->owner);
213
        return $clone;
214
    }
215
216
    /**
217
     * Duplicate relations using a static property to define
218
     * which ones we want to duplicate
219
     *
220
     * It may be that some relations are not diostinct to sub site so can stay
221
     * whereas others may need to be duplicated
222
     *
223
     */
224
    public function duplicateSubsiteRelations($originalPage)
225
    {
226
        $thisClass = $originalPage->ClassName;
227
        $relations = Config::inst()->get($thisClass, 'duplicate_to_subsite_relations');
228
229
        if ($relations && !empty($relations)) {
230
            foreach ($relations as $relation) {
231
                $items = $originalPage->$relation();
232
                foreach ($items as $item) {
233
                    $duplicateItem = $item->duplicate(false);
234
                    $duplicateItem->{$thisClass.'ID'} = $this->owner->ID;
235
                    $duplicateItem->write();
236
                }
237
            }
238
        }
239
    }
240
241
    /**
242
     * @return SiteConfig
243
     */
244
    public function alternateSiteConfig()
245
    {
246
        if (!$this->owner->SubsiteID) {
247
            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...
248
        }
249
        $sc = DataObject::get_one(SiteConfig::class, '"SubsiteID" = ' . $this->owner->SubsiteID);
250
        if (!$sc) {
251
            $sc = new SiteConfig();
252
            $sc->SubsiteID = $this->owner->SubsiteID;
253
            $sc->Title = _t('SilverStripe\\Subsites\\Model\\Subsite.SiteConfigTitle', 'Your Site Name');
254
            $sc->Tagline = _t('SilverStripe\\Subsites\\Model\\Subsite.SiteConfigSubtitle', 'Your tagline here');
255
            $sc->write();
256
        }
257
        return $sc;
258
    }
259
260
    /**
261
     * Only allow editing of a page if the member satisfies one of the following conditions:
262
     * - Is in a group which has access to the subsite this page belongs to
263
     * - Is in a group with edit permissions on the "main site"
264
     *
265
     * @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...
266
     * @return bool
267
     */
268
    public function canEdit($member = null)
269
    {
270
        if (!$member) {
271
            $member = Security::getCurrentUser();
272
        }
273
274
        // Find the sites that this user has access to
275
        $goodSites = Subsite::accessible_sites('CMS_ACCESS_CMSMain', true, 'all', $member)->column('ID');
276
277
        if (!is_null($this->owner->SubsiteID)) {
278
            $subsiteID = $this->owner->SubsiteID;
279
        } else {
280
            // The relationships might not be available during the record creation when using a GridField.
281
            // In this case the related objects will have empty fields, and SubsiteID will not be available.
282
            //
283
            // We do the second best: fetch the likely SubsiteID from the session. The drawback is this might
284
            // make it possible to force relations to point to other (forbidden) subsites.
285
            $subsiteID = SubsiteState::singleton()->getSubsiteId();
286
        }
287
288
        // Return true if they have access to this object's site
289
        if (!(in_array(0, $goodSites) || in_array($subsiteID, $goodSites))) {
290
            return false;
291
        }
292
    }
293
294
    /**
295
     * @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...
296
     * @return bool
297
     */
298 View Code Duplication
    public function canDelete($member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
299
    {
300
        if (!$member && $member !== false) {
301
            $member = Security::getCurrentUser();
302
        }
303
304
        return $this->canEdit($member);
0 ignored issues
show
Bug introduced by
It seems like $member can also be of type SilverStripe\Security\Member; however, parameter $member of SilverStripe\Subsites\Ex...TreeSubsites::canEdit() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

304
        return $this->canEdit(/** @scrutinizer ignore-type */ $member);
Loading history...
305
    }
306
307
    /**
308
     * @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...
309
     * @return bool
310
     */
311 View Code Duplication
    public function canAddChildren($member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
312
    {
313
        if (!$member && $member !== false) {
314
            $member = Security::getCurrentUser();
315
        }
316
317
        return $this->canEdit($member);
0 ignored issues
show
Bug introduced by
It seems like $member can also be of type SilverStripe\Security\Member; however, parameter $member of SilverStripe\Subsites\Ex...TreeSubsites::canEdit() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

317
        return $this->canEdit(/** @scrutinizer ignore-type */ $member);
Loading history...
318
    }
319
320
    /**
321
     * @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...
322
     * @return bool
323
     */
324 View Code Duplication
    public function canPublish($member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
325
    {
326
        if (!$member && $member !== false) {
327
            $member = Security::getCurrentUser();
328
        }
329
330
        return $this->canEdit($member);
0 ignored issues
show
Bug introduced by
It seems like $member can also be of type SilverStripe\Security\Member; however, parameter $member of SilverStripe\Subsites\Ex...TreeSubsites::canEdit() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

330
        return $this->canEdit(/** @scrutinizer ignore-type */ $member);
Loading history...
331
    }
332
333
    /**
334
     * Called by ContentController::init();
335
     * @param $controller
336
     */
337
    public static function contentcontrollerInit($controller)
338
    {
339
        $subsite = Subsite::currentSubsite();
340
341
        if ($subsite && $subsite->Theme) {
342
            SSViewer::add_themes([$subsite->Theme]);
343
        }
344
345
        if ($subsite && i18n::getData()->validate($subsite->Language)) {
346
            i18n::set_locale($subsite->Language);
347
        }
348
    }
349
350
    /**
351
     * @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...
352
     * @return string
353
     */
354
    public function alternateAbsoluteLink($action = null)
355
    {
356
        // Generate the existing absolute URL and replace the domain with the subsite domain.
357
        // This helps deal with Link() returning an absolute URL.
358
        $url = Director::absoluteURL($this->owner->Link($action));
359
        if ($this->owner->SubsiteID) {
360
            $url = preg_replace('/\/\/[^\/]+\//', '//' . $this->owner->Subsite()->domain() . '/', $url);
361
        }
362
        return $url;
363
    }
364
365
    /**
366
     * Use the CMS domain for iframed CMS previews to prevent single-origin violations
367
     * and SSL cert problems.
368
     * @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...
369
     * @return string
370
     */
371
    public function alternatePreviewLink($action = null)
372
    {
373
        $url = Director::absoluteURL($this->owner->Link());
374
        if ($this->owner->SubsiteID) {
375
            $url = HTTP::setGetVar('SubsiteID', $this->owner->SubsiteID, $url);
376
        }
377
        return $url;
378
    }
379
380
    /**
381
     * Inject the subsite ID into the content so it can be used by frontend scripts.
382
     * @param $tags
383
     * @return string
384
     */
385
    public function MetaTags(&$tags)
386
    {
387
        if ($this->owner->SubsiteID) {
388
            $tags .= '<meta name="x-subsite-id" content="' . $this->owner->SubsiteID . "\" />\n";
389
        }
390
391
        return $tags;
392
    }
393
394
    public function augmentSyncLinkTracking()
395
    {
396
        // Set LinkTracking appropriately
397
        $links = HTTP::getLinksIn($this->owner->Content);
398
        $linkedPages = [];
399
400
        if ($links) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $links of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
401
            foreach ($links as $link) {
402
                if (substr($link, 0, strlen('http://')) == 'http://') {
403
                    $withoutHttp = substr($link, strlen('http://'));
404
                    if (strpos($withoutHttp, '/') && strpos($withoutHttp, '/') < strlen($withoutHttp)) {
405
                        $domain = substr($withoutHttp, 0, strpos($withoutHttp, '/'));
406
                        $rest = substr($withoutHttp, strpos($withoutHttp, '/') + 1);
407
408
                        $subsiteID = Subsite::getSubsiteIDForDomain($domain);
409
                        if ($subsiteID == 0) {
410
                            continue;
411
                        } // We have no idea what the domain for the main site is, so cant track links to it
412
413
                        $origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
414
                        Subsite::disable_subsite_filter(true);
415
                        $candidatePage = DataObject::get_one(
416
                            SiteTree::class,
417
                            "\"URLSegment\" = '" . Convert::raw2sql(urldecode($rest)) . "' AND \"SubsiteID\" = " . $subsiteID,
418
                            false
419
                        );
420
                        Subsite::disable_subsite_filter($origDisableSubsiteFilter);
421
422
                        if ($candidatePage) {
423
                            $linkedPages[] = $candidatePage->ID;
424
                        } else {
425
                            $this->owner->HasBrokenLink = true;
426
                        }
427
                    }
428
                }
429
            }
430
        }
431
432
        $this->owner->CrossSubsiteLinkTracking()->setByIDList($linkedPages);
433
    }
434
435
    /**
436
     * Ensure that valid url segments are checked within the correct subsite of the owner object,
437
     * even if the current subsiteID is set to some other subsite.
438
     *
439
     * @return null|bool Either true or false, or null to not influence result
440
     */
441
    public function augmentValidURLSegment()
442
    {
443
        // If this page is being filtered in the current subsite, then no custom validation query is required.
444
        $subsite = Subsite::$force_subsite ?: SubsiteState::singleton()->getSubsiteId();
445
        if (empty($this->owner->SubsiteID) || $subsite == $this->owner->SubsiteID) {
446
            return null;
447
        }
448
449
        // Backup forced subsite
450
        $prevForceSubsite = Subsite::$force_subsite;
451
        Subsite::$force_subsite = $this->owner->SubsiteID;
452
453
        // Repeat validation in the correct subsite
454
        $isValid = $this->owner->validURLSegment();
455
456
        // Restore
457
        Subsite::$force_subsite = $prevForceSubsite;
458
459
        return (bool)$isValid;
460
    }
461
462
    /**
463
     * Return a piece of text to keep DataObject cache keys appropriately specific
464
     */
465
    public function cacheKeyComponent()
466
    {
467
        return 'subsite-' . SubsiteState::singleton()->getSubsiteId();
468
    }
469
470
    /**
471
     * @param Member
472
     * @return boolean|null
473
     */
474
    public function canCreate($member = null)
475
    {
476
        // Typically called on a singleton, so we're not using the Subsite() relation
477
        $subsite = Subsite::currentSubsite();
478
479
        if ($subsite && $subsite->exists() && $subsite->PageTypeBlacklist) {
480
            $blacklist = str_replace(['[', '"', ']'], '', $subsite->PageTypeBlacklist);
481
            $blacklist = str_replace(['\\\\'], '\\', $blacklist);
482
            $blacklisted = explode(',', $blacklist);
483
484
            if (in_array(get_class($this->owner), $blacklisted)) {
485
                return false;
486
            }
487
        }
488
    }
489
}
490