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

src/Extensions/SiteTreeSubsites.php (1 issue)

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