Passed
Push — master ( d78e3c...c732c0 )
by Robbie
03:58
created

SiteTreeSubsites::updatePreviewLink()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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