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

src/Extensions/SiteTreeSubsites.php (13 issues)

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