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

src/Extensions/SiteTreeSubsites.php (3 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
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()) {
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
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
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
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
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
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
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