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

code/extensions/SiteTreeSubsites.php (16 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 Page;
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 = [
0 ignored issues
show
The property $has_one is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
34
        'Subsite' => Subsite::class, // The subsite that this page belongs to
35
    ];
36
37
    private static $many_many = [
0 ignored issues
show
The property $many_many is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
38
        'CrossSubsiteLinkTracking' => SiteTree::class // Stored separately, as the logic for URL rewriting is different
39
    ];
40
41
    private static $many_many_extraFields = [
0 ignored issues
show
The property $many_many_extraFields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
42
        'CrossSubsiteLinkTracking' => ['FieldName' => 'Varchar']
43
    ];
44
45
    public function isMainSite()
46
    {
47
        return $this->owner->SubsiteID == 0;
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;
0 ignored issues
show
$subsiteID is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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
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;
113
114
        if ($isDefaultSubsite && $subsitesMap) {
115
            $fields->addFieldToTab(
116
                'Root.Main',
117
                ToggleCompositeField::create(
118
                    'SubsiteOperations',
119
                    _t('SiteTreeSubsites.SubsiteOperations', 'Subsite Operations'),
120
                    [
121
                        new DropdownField('CopyToSubsiteID', _t(
122
                            'SiteTreeSubsites.CopyToSubsite',
123
                            'Copy page to subsite'
124
                        ), $subsitesMap),
125
                        new CheckboxField(
126
                            'CopyToSubsiteWithChildren',
127
                            _t('SiteTreeSubsites.CopyToSubsiteWithChildren', 'Include children pages?')
128
                        ),
129
                        $copyAction = new FormAction(
130
                            'copytosubsite',
131
                            _t('SiteTreeSubsites.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 return type of return false; (false) is incompatible with the return type documented by SilverStripe\Subsites\Ex...es::alternateSiteConfig of type SilverStripe\SiteConfig\SiteConfig.

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...
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('Subsite.SiteConfigTitle', 'Your Site Name');
254
            $sc->Tagline = _t('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
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
296
     * @return bool
297
     */
298 View Code Duplication
    public function canDelete($member = null)
0 ignored issues
show
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
It seems like $member defined by \SilverStripe\Security\Security::getCurrentUser() on line 301 can also be of type object<SilverStripe\Security\Member>; however, SilverStripe\Subsites\Ex...TreeSubsites::canEdit() does only seem to accept null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
305
    }
306
307
    /**
308
     * @param null $member
309
     * @return bool
310
     */
311 View Code Duplication
    public function canAddChildren($member = null)
0 ignored issues
show
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
It seems like $member defined by \SilverStripe\Security\Security::getCurrentUser() on line 314 can also be of type object<SilverStripe\Security\Member>; however, SilverStripe\Subsites\Ex...TreeSubsites::canEdit() does only seem to accept null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
318
    }
319
320
    /**
321
     * @param null $member
322
     * @return bool
323
     */
324 View Code Duplication
    public function canPublish($member = null)
0 ignored issues
show
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
It seems like $member defined by \SilverStripe\Security\Security::getCurrentUser() on line 327 can also be of type object<SilverStripe\Security\Member>; however, SilverStripe\Subsites\Ex...TreeSubsites::canEdit() does only seem to accept null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
331
    }
332
333
    /**
334
     * Called by ContentController::init();
335
     * @param $controller
336
     */
337
    public static function contentcontrollerInit($controller)
0 ignored issues
show
The parameter $controller 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...
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
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
369
     * @return string
370
     */
371
    public function alternatePreviewLink($action = null)
0 ignored issues
show
The parameter $action 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...
372
    {
373
        $url = Director::absoluteURL($this->owner->Link());
374
        if ($this->owner->SubsiteID) {
375
            $url = HTTP::setGetVar('SubsiteID', $this->owner->SubsiteID, $url);
0 ignored issues
show
It seems like $url can also be of type false; however, SilverStripe\Control\HTTP::setGetVar() does only seem to accept string|null, did you maybe forget to handle an error condition?
Loading history...
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) {
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
        if ($subsite && $subsite->exists() && $subsite->PageTypeBlacklist) {
479
            $blacklisted = explode(',', $subsite->PageTypeBlacklist);
480
            // All subclasses need to be listed explicitly
481
            if (in_array(get_class($this->owner), $blacklisted)) {
482
                return false;
483
            }
484
        }
485
    }
486
}
487