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

code/extensions/SiteTreeSubsites.php (1 issue)

Severity

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 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\ORM\DataExtension;
18
use SilverStripe\ORM\DataObject;
19
use SilverStripe\ORM\DataQuery;
20
use SilverStripe\ORM\Queries\SQLSelect;
21
use SilverStripe\Security\Member;
22
use SilverStripe\SiteConfig\SiteConfig;
23
use SilverStripe\Subsites\Model\Subsite;
24
use SilverStripe\Subsites\State\SubsiteState;
25
use SilverStripe\View\SSViewer;
26
27
/**
28
 * Extension for the SiteTree object to add subsites support
29
 */
30
class SiteTreeSubsites extends DataExtension
31
{
32
    private static $has_one = [
33
        'Subsite' => Subsite::class, // The subsite that this page belongs to
34
    ];
35
36
    private static $many_many = [
37
        'CrossSubsiteLinkTracking' => SiteTree::class // Stored separately, as the logic for URL rewriting is different
38
    ];
39
40
    private static $many_many_extraFields = [
41
        'CrossSubsiteLinkTracking' => ['FieldName' => 'Varchar']
42
    ];
43
44
    public function isMainSite()
45
    {
46
        return $this->owner->SubsiteID == 0;
47
    }
48
49
    /**
50
     * Update any requests to limit the results to the current site
51
     * @param SQLSelect $query
52
     * @param DataQuery $dataQuery
53
     */
54
    public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null)
55
    {
56
        if (Subsite::$disable_subsite_filter) {
57
            return;
58
        }
59
        if ($dataQuery && $dataQuery->getQueryParam('Subsite.filter') === false) {
60
            return;
61
        }
62
63
        // If you're querying by ID, ignore the sub-site - this is a bit ugly...
64
        // 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)) {
65
        if ($query->filtersOnID()) {
66
            return;
67
        }
68
69
        $subsiteID = null;
0 ignored issues
show
$subsiteID is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

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