Passed
Push — master ( 5b8a0d...bf7dd9 )
by Robbie
04:51
created

SiteTreeSubsites::duplicateToSubsite()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 15
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Subsites\Extensions;
4
5
use Page;
0 ignored issues
show
Bug introduced by
The type Page was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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\ArrayList;
19
use SilverStripe\ORM\DataExtension;
20
use SilverStripe\ORM\DataObject;
21
use SilverStripe\ORM\DataQuery;
22
use SilverStripe\ORM\Map;
23
use SilverStripe\ORM\Queries\SQLSelect;
24
use SilverStripe\Security\Member;
25
use SilverStripe\Security\Security;
26
use SilverStripe\SiteConfig\SiteConfig;
27
use SilverStripe\Subsites\Model\Subsite;
28
use SilverStripe\Subsites\State\SubsiteState;
29
use SilverStripe\View\SSViewer;
30
31
/**
32
 * Extension for the SiteTree object to add subsites support
33
 */
34
class SiteTreeSubsites extends DataExtension
35
{
36
    private static $has_one = [
37
        'Subsite' => Subsite::class, // The subsite that this page belongs to
38
    ];
39
40
    private static $many_many = [
41
        'CrossSubsiteLinkTracking' => SiteTree::class // Stored separately, as the logic for URL rewriting is different
42
    ];
43
44
    private static $many_many_extraFields = [
45
        'CrossSubsiteLinkTracking' => ['FieldName' => 'Varchar']
46
    ];
47
48
    public function isMainSite()
49
    {
50
        return $this->owner->SubsiteID == 0;
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $this->owner->SubsiteID of type null|mixed to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
51
    }
52
53
    /**
54
     * Update any requests to limit the results to the current site
55
     * @param SQLSelect $query
56
     * @param DataQuery $dataQuery
57
     */
58
    public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null)
59
    {
60
        if (Subsite::$disable_subsite_filter) {
61
            return;
62
        }
63
        if ($dataQuery && $dataQuery->getQueryParam('Subsite.filter') === false) {
0 ignored issues
show
introduced by
The condition $dataQuery->getQueryPara...site.filter') === false is always false.
Loading history...
64
            return;
65
        }
66
67
        // If you're querying by ID, ignore the sub-site - this is a bit ugly...
68
        // if(!$query->where
69
        // || (strpos($query->where[0], ".\"ID\" = ") === false
70
        // && strpos($query->where[0], ".`ID` = ") === false && strpos($query->where[0], ".ID = ") === false
71
        // && strpos($query->where[0], "ID = ") !== 0)) {
72
        if ($query->filtersOnID()) {
73
            return;
74
        }
75
76
        $subsiteID = null;
77
        if (Subsite::$force_subsite) {
78
            $subsiteID = Subsite::$force_subsite;
79
        } else {
80
            $subsiteID = SubsiteState::singleton()->getSubsiteId();
81
        }
82
83
        if ($subsiteID === null) {
84
            return;
85
        }
86
87
        // The foreach is an ugly way of getting the first key :-)
88
        foreach ($query->getFrom() as $tableName => $info) {
89
            // The tableName should be SiteTree or SiteTree_Live...
90
            $siteTreeTableName = SiteTree::getSchema()->tableName(SiteTree::class);
91
            if (strpos($tableName, $siteTreeTableName) === false) {
92
                break;
93
            }
94
            $query->addWhere("\"$tableName\".\"SubsiteID\" IN ($subsiteID)");
95
            break;
96
        }
97
    }
98
99
    public function onBeforeWrite()
100
    {
101
        if (!$this->owner->ID && !$this->owner->SubsiteID) {
102
            $this->owner->SubsiteID = SubsiteState::singleton()->getSubsiteId();
103
        }
104
105
        parent::onBeforeWrite();
106
    }
107
108
    public function updateCMSFields(FieldList $fields)
109
    {
110
        $subsites = Subsite::accessible_sites('CMS_ACCESS_CMSMain');
111
        if ($subsites && $subsites->count()) {
112
            $subsitesToMap = $subsites->exclude('ID', $this->owner->SubsiteID);
113
            $subsitesMap = $subsitesToMap->map('ID', 'Title');
114
        } else {
115
            $subsitesMap = new Map(ArrayList::create());
116
        }
117
118
        // Master page edit field (only allowed from default subsite to avoid inconsistent relationships)
119
        $isDefaultSubsite = $this->owner->SubsiteID == 0 || $this->owner->Subsite()->DefaultSite;
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $this->owner->SubsiteID of type mixed|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
120
121
        if ($isDefaultSubsite && $subsitesMap->count()) {
122
            $fields->addFieldToTab(
123
                'Root.Main',
124
                ToggleCompositeField::create(
125
                    'SubsiteOperations',
126
                    _t(__CLASS__ . '.SubsiteOperations', 'Subsite Operations'),
127
                    [
128
                        DropdownField::create('CopyToSubsiteID', _t(
129
                            __CLASS__ . '.CopyToSubsite',
130
                            'Copy page to subsite'
131
                        ), $subsitesMap),
132
                        CheckboxField::create(
133
                            'CopyToSubsiteWithChildren',
134
                            _t(__CLASS__ . '.CopyToSubsiteWithChildren', 'Include children pages?')
135
                        ),
136
                        $copyAction = FormAction::create(
137
                            'copytosubsite',
138
                            _t(__CLASS__ . '.CopyAction', 'Copy')
139
                        )
140
                    ]
141
                )->setHeadingLevel(4)
142
            );
143
144
            $copyAction->addExtraClass('btn btn-primary font-icon-save ml-3');
145
146
            // @todo check if this needs re-implementation
147
//            $copyAction->includeDefaultJS(false);
148
        }
149
150
        // replace readonly link prefix
151
        $subsite = $this->owner->Subsite();
152
        $nested_urls_enabled = Config::inst()->get(SiteTree::class, 'nested_urls');
153
        if ($subsite && $subsite->exists()) {
154
            // Use baseurl from domain
155
            $baseLink = $subsite->absoluteBaseURL();
156
157
            // Add parent page if enabled
158
            if ($nested_urls_enabled && $this->owner->ParentID) {
159
                $baseLink = Controller::join_links(
160
                    $baseLink,
161
                    $this->owner->Parent()->RelativeLink(true)
162
                );
163
            }
164
165
            $urlsegment = $fields->dataFieldByName('URLSegment');
166
            if ($urlsegment) {
0 ignored issues
show
introduced by
$urlsegment is of type SilverStripe\Forms\FormField, thus it always evaluated to true.
Loading history...
167
                $urlsegment->setURLPrefix($baseLink);
168
            }
169
        }
170
    }
171
172
    /**
173
     * Does the basic duplication, but doesn't write anything this means we can subclass this easier and do more
174
     * complex relation duplication.
175
     *
176
     * Note that when duplicating including children, everything is written.
177
     *
178
     * @param Subsite|int $subsiteID
179
     * @param bool $includeChildren
180
     * @return SiteTree
181
     */
182
    public function duplicateToSubsitePrep($subsiteID, $includeChildren)
183
    {
184
        if (is_object($subsiteID)) {
185
            $subsiteID = $subsiteID->ID;
186
        }
187
188
        return SubsiteState::singleton()
189
            ->withState(function (SubsiteState $newState) use ($subsiteID, $includeChildren) {
190
                $newState->setSubsiteId($subsiteID);
191
192
                /** @var SiteTree $page */
193
                $page = $this->owner;
194
195
                try {
196
                    // We have no idea what the ParentID should be, but it shouldn't be the same as it was since
197
                    // we're now in a different subsite. As a workaround use the url-segment and subsite ID.
198
                    if ($page->Parent()) {
199
                        $parentSeg = $page->Parent()->URLSegment;
200
                        $newParentPage = Page::get()->filter('URLSegment', $parentSeg)->first();
201
                        $originalParentID = $page->ParentID;
202
                        if ($newParentPage) {
203
                            $page->ParentID = (int) $newParentPage->ID;
0 ignored issues
show
Bug Best Practice introduced by
The property ParentID does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
204
                        } else {
205
                            // reset it to the top level, so the user can decide where to put it
206
                            $page->ParentID = 0;
207
                        }
208
                    }
209
210
                    // Disable query filtering by subsite during actual duplication
211
                    $originalFilter = Subsite::$disable_subsite_filter;
212
                    Subsite::disable_subsite_filter(true);
213
214
                    return $includeChildren ? $page->duplicateWithChildren() : $page->duplicate(false);
215
                } finally {
216
                    Subsite::disable_subsite_filter($originalFilter);
217
218
                    // Re-set the original parent ID for the current page
219
                    $page->ParentID = $originalParentID;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $originalParentID does not seem to be defined for all execution paths leading up to this point.
Loading history...
220
                }
221
            });
222
    }
223
224
    /**
225
     * When duplicating a page, assign the current subsite ID from the state
226
     */
227
    public function onBeforeDuplicate()
228
    {
229
        $subsiteId = SubsiteState::singleton()->getSubsiteId();
230
        if ($subsiteId !== null) {
231
            $this->owner->SubsiteID = $subsiteId;
232
        }
233
    }
234
235
    /**
236
     * Create a duplicate of this page and save it to another subsite
237
     *
238
     * @param Subsite|int $subsiteID   The Subsite to copy to, or its ID
239
     * @param boolean $includeChildren Whether to duplicate child pages too
240
     * @return SiteTree                The duplicated page
241
     */
242
    public function duplicateToSubsite($subsiteID = null, $includeChildren = false)
243
    {
244
        $clone = $this->owner->duplicateToSubsitePrep($subsiteID, $includeChildren);
245
        $clone->invokeWithExtensions('onBeforeDuplicateToSubsite', $this->owner);
246
247
        if (!$includeChildren) {
248
            // Write the new page if "include children" is false, because it is written by default when it's true.
249
            $clone->write();
250
        }
251
        // Deprecated: manually duplicate any configured relationships
252
        $clone->duplicateSubsiteRelations($this->owner);
253
254
        $clone->invokeWithExtensions('onAfterDuplicateToSubsite', $this->owner);
255
256
        return $clone;
257
    }
258
259
    /**
260
     * Duplicate relations using a static property to define
261
     * which ones we want to duplicate
262
     *
263
     * It may be that some relations are not diostinct to sub site so can stay
264
     * whereas others may need to be duplicated
265
     *
266
     * @deprecated 2.2..3.0 Use the "cascade_duplicates" config API instead
267
     * @param SiteTree $originalPage
268
     */
269
    public function duplicateSubsiteRelations($originalPage)
270
    {
271
        $thisClass = $originalPage->ClassName;
272
        $relations = Config::inst()->get($thisClass, 'duplicate_to_subsite_relations');
273
274
        if ($relations && !empty($relations)) {
275
            foreach ($relations as $relation) {
276
                $items = $originalPage->$relation();
277
                foreach ($items as $item) {
278
                    $duplicateItem = $item->duplicate(false);
279
                    $duplicateItem->{$thisClass.'ID'} = $this->owner->ID;
280
                    $duplicateItem->write();
281
                }
282
            }
283
        }
284
    }
285
286
    /**
287
     * @return SiteConfig
288
     */
289
    public function alternateSiteConfig()
290
    {
291
        if (!$this->owner->SubsiteID) {
292
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type SilverStripe\SiteConfig\SiteConfig.
Loading history...
293
        }
294
        $sc = DataObject::get_one(SiteConfig::class, '"SubsiteID" = ' . $this->owner->SubsiteID);
295
        if (!$sc) {
296
            $sc = new SiteConfig();
297
            $sc->SubsiteID = $this->owner->SubsiteID;
298
            $sc->Title = _t('SilverStripe\\Subsites\\Model\\Subsite.SiteConfigTitle', 'Your Site Name');
299
            $sc->Tagline = _t('SilverStripe\\Subsites\\Model\\Subsite.SiteConfigSubtitle', 'Your tagline here');
300
            $sc->write();
301
        }
302
        return $sc;
303
    }
304
305
    /**
306
     * Only allow editing of a page if the member satisfies one of the following conditions:
307
     * - Is in a group which has access to the subsite this page belongs to
308
     * - Is in a group with edit permissions on the "main site"
309
     *
310
     * If there are no subsites configured yet, this logic is skipped.
311
     *
312
     * @param Member|null $member
313
     * @return bool|null
314
     */
315
    public function canEdit($member = null)
316
    {
317
        if (!$member) {
318
            $member = Security::getCurrentUser();
319
        }
320
321
        // Do not provide any input if there are no subsites configured
322
        if (!Subsite::get()->exists()) {
323
            return null;
324
        }
325
326
        // Find the sites that this user has access to
327
        $goodSites = Subsite::accessible_sites('CMS_ACCESS_CMSMain', true, 'all', $member)->column('ID');
328
329
        if (!is_null($this->owner->SubsiteID)) {
330
            $subsiteID = $this->owner->SubsiteID;
331
        } else {
332
            // The relationships might not be available during the record creation when using a GridField.
333
            // In this case the related objects will have empty fields, and SubsiteID will not be available.
334
            //
335
            // We do the second best: fetch the likely SubsiteID from the session. The drawback is this might
336
            // make it possible to force relations to point to other (forbidden) subsites.
337
            $subsiteID = SubsiteState::singleton()->getSubsiteId();
338
        }
339
340
        // Return true if they have access to this object's site
341
        if (!(in_array(0, $goodSites) || in_array($subsiteID, $goodSites))) {
342
            return false;
343
        }
344
    }
345
346
    /**
347
     * @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...
348
     * @return bool
349
     */
350
    public function canDelete($member = null)
351
    {
352
        if (!$member && $member !== false) {
0 ignored issues
show
introduced by
$member is of type null, thus it always evaluated to false.
Loading history...
introduced by
The condition $member !== false is always true.
Loading history...
353
            $member = Security::getCurrentUser();
354
        }
355
356
        return $this->canEdit($member);
357
    }
358
359
    /**
360
     * @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...
361
     * @return bool
362
     */
363
    public function canAddChildren($member = null)
364
    {
365
        if (!$member && $member !== false) {
0 ignored issues
show
introduced by
The condition $member !== false is always true.
Loading history...
introduced by
$member is of type null, thus it always evaluated to false.
Loading history...
366
            $member = Security::getCurrentUser();
367
        }
368
369
        return $this->canEdit($member);
370
    }
371
372
    /**
373
     * @param Member|null $member
374
     * @return bool|null
375
     */
376
    public function canPublish($member = null)
377
    {
378
        if (!$member && $member !== false) {
0 ignored issues
show
introduced by
The condition $member !== false is always false.
Loading history...
379
            $member = Security::getCurrentUser();
380
        }
381
382
        return $this->canEdit($member);
383
    }
384
385
    /**
386
     * Called by ContentController::init();
387
     * @param $controller
388
     */
389
    public static function contentcontrollerInit($controller)
390
    {
391
        $subsite = Subsite::currentSubsite();
392
393
        if ($subsite && $subsite->Theme) {
394
            SSViewer::add_themes([$subsite->Theme]);
395
        }
396
397
        if ($subsite && i18n::getData()->validate($subsite->Language)) {
398
            i18n::set_locale($subsite->Language);
399
        }
400
    }
401
402
    /**
403
     * @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...
404
     * @return string
405
     */
406
    public function alternateAbsoluteLink($action = null)
407
    {
408
        // Generate the existing absolute URL and replace the domain with the subsite domain.
409
        // This helps deal with Link() returning an absolute URL.
410
        $url = Director::absoluteURL($this->owner->Link($action));
411
        if ($this->owner->SubsiteID) {
412
            $url = preg_replace('/\/\/[^\/]+\//', '//' . $this->owner->Subsite()->domain() . '/', $url);
413
        }
414
        return $url;
415
    }
416
417
    /**
418
     * Use the CMS domain for iframed CMS previews to prevent single-origin violations
419
     * and SSL cert problems. Always set SubsiteID to avoid errors because a page doesn't
420
     * exist on the CMS domain.
421
     *
422
     * @param string &$link
423
     * @param string|null $action
424
     * @return string
425
     */
426
    public function updatePreviewLink(&$link, $action = null)
427
    {
428
        $url = Director::absoluteURL($this->owner->Link($action));
429
        $link = HTTP::setGetVar('SubsiteID', $this->owner->SubsiteID, $url);
430
        return $link;
431
    }
432
433
    /**
434
     * This function is marked as deprecated for removal in 5.0.0 in silverstripe/cms
435
     * so now simply passes execution to where the functionality exists for backwards compatiblity.
436
     * CMS 4.0.0 SiteTree already throws a SilverStripe deprecation error before calling this function.
437
     * @deprecated 2.2...3.0 use updatePreviewLink instead
438
     *
439
     * @param string|null $action
440
     * @return string
441
     */
442
    public function alternatePreviewLink($action = null)
443
    {
444
        $link = '';
445
        return $this->updatePreviewLink($link, $action);
446
    }
447
448
    /**
449
     * Inject the subsite ID into the content so it can be used by frontend scripts.
450
     * @param $tags
451
     * @return string
452
     */
453
    public function MetaTags(&$tags)
454
    {
455
        if ($this->owner->SubsiteID) {
456
            $tags .= '<meta name="x-subsite-id" content="' . $this->owner->SubsiteID . "\" />\n";
457
        }
458
459
        return $tags;
460
    }
461
462
    public function augmentSyncLinkTracking()
463
    {
464
        // Set LinkTracking appropriately
465
        $links = HTTP::getLinksIn($this->owner->Content);
466
        $linkedPages = [];
467
468
        if ($links) {
469
            foreach ($links as $link) {
470
                if (substr($link, 0, strlen('http://')) == 'http://') {
471
                    $withoutHttp = substr($link, strlen('http://'));
472
                    if (strpos($withoutHttp, '/') && strpos($withoutHttp, '/') < strlen($withoutHttp)) {
473
                        $domain = substr($withoutHttp, 0, strpos($withoutHttp, '/'));
474
                        $rest = substr($withoutHttp, strpos($withoutHttp, '/') + 1);
475
476
                        $subsiteID = Subsite::getSubsiteIDForDomain($domain);
477
                        if ($subsiteID == 0) {
478
                            continue;
479
                        } // We have no idea what the domain for the main site is, so cant track links to it
480
481
                        $origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
482
                        Subsite::disable_subsite_filter(true);
483
                        $candidatePage = DataObject::get_one(
484
                            SiteTree::class,
485
                            "\"URLSegment\" = '"
486
                            . Convert::raw2sql(urldecode($rest))
487
                            . "' AND \"SubsiteID\" = "
488
                            . $subsiteID,
489
                            false
490
                        );
491
                        Subsite::disable_subsite_filter($origDisableSubsiteFilter);
492
493
                        if ($candidatePage) {
494
                            $linkedPages[] = $candidatePage->ID;
495
                        } else {
496
                            $this->owner->HasBrokenLink = true;
497
                        }
498
                    }
499
                }
500
            }
501
        }
502
503
        $this->owner->CrossSubsiteLinkTracking()->setByIDList($linkedPages);
504
    }
505
506
    /**
507
     * Ensure that valid url segments are checked within the correct subsite of the owner object,
508
     * even if the current subsiteID is set to some other subsite.
509
     *
510
     * @return null|bool Either true or false, or null to not influence result
511
     */
512
    public function augmentValidURLSegment()
513
    {
514
        // If this page is being filtered in the current subsite, then no custom validation query is required.
515
        $subsite = Subsite::$force_subsite ?: SubsiteState::singleton()->getSubsiteId();
516
        if (empty($this->owner->SubsiteID) || $subsite == $this->owner->SubsiteID) {
517
            return null;
518
        }
519
520
        // Backup forced subsite
521
        $prevForceSubsite = Subsite::$force_subsite;
522
        Subsite::$force_subsite = $this->owner->SubsiteID;
523
524
        // Repeat validation in the correct subsite
525
        $isValid = $this->owner->validURLSegment();
526
527
        // Restore
528
        Subsite::$force_subsite = $prevForceSubsite;
529
530
        return (bool)$isValid;
531
    }
532
533
    /**
534
     * Return a piece of text to keep DataObject cache keys appropriately specific
535
     */
536
    public function cacheKeyComponent()
537
    {
538
        return 'subsite-' . SubsiteState::singleton()->getSubsiteId();
539
    }
540
541
    /**
542
     * @param Member $member
543
     * @return boolean|null
544
     */
545
    public function canCreate($member = null)
546
    {
547
        // Typically called on a singleton, so we're not using the Subsite() relation
548
        $subsite = Subsite::currentSubsite();
549
        if ($subsite && $subsite->exists() && $subsite->PageTypeBlacklist) {
550
            // SS 4.1: JSON encoded. SS 4.0, comma delimited
551
            $blacklist = Convert::json2array($subsite->PageTypeBlacklist);
552
            if ($blacklist === false) {
553
                $blacklist = explode(',', $subsite->PageTypeBlacklist);
554
            }
555
556
            if (in_array(get_class($this->owner), (array) $blacklist)) {
557
                return false;
558
            }
559
        }
560
    }
561
}
562