Completed
Push — master ( 5cf2d8...46bcff )
by Damian
13s
created

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