Completed
Pull Request — master (#315)
by Robbie
01:44
created

Subsite::getCMSFields()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 61
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 61
rs 9.5147
c 0
b 0
f 0
cc 3
eloc 35
nc 1
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Subsites\Model;
4
5
use SilverStripe\Admin\CMSMenu;
6
use SilverStripe\CMS\Model\SiteTree;
7
use SilverStripe\Control\Director;
8
use SilverStripe\Control\Session;
9
use SilverStripe\Core\Convert;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\Forms\CheckboxField;
12
use SilverStripe\Forms\CheckboxSetField;
13
use SilverStripe\Forms\DropdownField;
14
use SilverStripe\Forms\FieldList;
15
use SilverStripe\Forms\GridField\GridField;
16
use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor;
17
use SilverStripe\Forms\HeaderField;
18
use SilverStripe\Forms\HiddenField;
19
use SilverStripe\Forms\LiteralField;
20
use SilverStripe\Forms\Tab;
21
use SilverStripe\Forms\TabSet;
22
use SilverStripe\Forms\TextField;
23
use SilverStripe\Forms\ToggleCompositeField;
24
use SilverStripe\i18n\Data\Intl\IntlLocales;
25
use SilverStripe\i18n\i18n;
26
use SilverStripe\ORM\ArrayLib;
27
use SilverStripe\ORM\ArrayList;
28
use SilverStripe\ORM\DataList;
29
use SilverStripe\ORM\DataObject;
30
use SilverStripe\ORM\DB;
31
use SilverStripe\ORM\SS_List;
32
use SilverStripe\Security\Group;
33
use SilverStripe\Security\Member;
34
use SilverStripe\Security\Permission;
35
use SilverStripe\Security\Security;
36
use SilverStripe\Subsites\State\SubsiteState;
37
use SilverStripe\Versioned\Versioned;
38
use UnexpectedValueException;
39
40
/**
41
 * A dynamically created subsite. SiteTree objects can now belong to a subsite.
42
 * You can simulate subsite access without setting up virtual hosts by appending ?SubsiteID=<ID> to the request.
43
 *
44
 * @package subsites
45
 */
46
class Subsite extends DataObject
47
{
48
49
    private static $table_name = 'Subsite';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $table_name is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
50
51
    /**
52
     * @var boolean $disable_subsite_filter If enabled, bypasses the query decoration
53
     * to limit DataObject::get*() calls to a specific subsite. Useful for debugging.
54
     */
55
    public static $disable_subsite_filter = false;
56
57
    /**
58
     * Allows you to force a specific subsite ID, or comma separated list of IDs.
59
     * Only works for reading. An object cannot be written to more than 1 subsite.
60
     */
61
    public static $force_subsite = null;
62
63
    /**
64
     *
65
     * @var boolean
66
     */
67
    public static $write_hostmap = true;
68
69
    /**
70
     * Memory cache of accessible sites
71
     *
72
     * @array
73
     */
74
    private static $_cache_accessible_sites = [];
75
76
    /**
77
     * Memory cache of subsite id for domains
78
     *
79
     * @var array
80
     */
81
    private static $_cache_subsite_for_domain = [];
82
83
    /**
84
     * @var array $allowed_themes Numeric array of all themes which are allowed to be selected for all subsites.
85
     * Corresponds to subfolder names within the /themes folder. By default, all themes contained in this folder
86
     * are listed.
87
     */
88
    private static $allowed_themes = [];
89
90
    /**
91
     * @var Boolean If set to TRUE, don't assume 'www.example.com' and 'example.com' are the same.
92
     * Doesn't affect wildcard matching, so '*.example.com' will match 'www.example.com' (but not 'example.com')
93
     * in both TRUE or FALSE setting.
94
     */
95
    public static $strict_subdomain_matching = false;
96
97
    /**
98
     * @var boolean Respects the IsPublic flag when retrieving subsites
99
     */
100
    public static $check_is_public = true;
101
102
    /*** @return array
103
     */
104
    private static $summary_fields = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $summary_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
105
        'Title',
106
        'PrimaryDomain',
107
        'IsPublic'
108
    ];
109
110
    /**
111
     * @var array
112
     */
113
    private static $db = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $db is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
114
        'Title' => 'Varchar(255)',
115
        'RedirectURL' => 'Varchar(255)',
116
        'DefaultSite' => 'Boolean',
117
        'Theme' => 'Varchar',
118
        'Language' => 'Varchar(6)',
119
120
        // Used to hide unfinished/private subsites from public view.
121
        // If unset, will default to true
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% 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...
122
        'IsPublic' => 'Boolean',
123
124
        // Comma-separated list of disallowed page types
125
        'PageTypeBlacklist' => 'Text',
126
    ];
127
128
    /**
129
     * @var array
130
     */
131
    private static $has_many = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $has_many is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
132
        'Domains' => SubsiteDomain::class,
133
    ];
134
135
    /**
136
     * @var array
137
     */
138
    private static $belongs_many_many = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $belongs_many_many is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
139
        'Groups' => Group::class,
140
    ];
141
142
    /**
143
     * @var array
144
     */
145
    private static $defaults = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $defaults is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
146
        'IsPublic' => 1
147
    ];
148
149
    /**
150
     * @var array
151
     */
152
    private static $searchable_fields = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $searchable_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
153
        'Title',
154
        'Domains.Domain',
155
        'IsPublic',
156
    ];
157
158
    /**
159
     * @var string
160
     */
161
    private static $default_sort = '"Title" ASC';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $default_sort is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
162
163
    /**
164
     * Set allowed themes
165
     *
166
     * @param array $themes - Numeric array of all themes which are allowed to be selected for all subsites.
167
     */
168
    public static function set_allowed_themes($themes)
169
    {
170
        self::$allowed_themes = $themes;
171
    }
172
173
    /**
174
     * Gets the subsite currently set in the session.
175
     *
176
     * @uses ControllerSubsites->controllerAugmentInit()
177
     * @return DataObject The current Subsite
178
     */
179
    public static function currentSubsite()
180
    {
181
        return Subsite::get()->byID(SubsiteState::singleton()->getSubsiteId());
182
    }
183
184
    /**
185
     * This function gets the current subsite ID from the session. It used in the backend so Ajax requests
186
     * use the correct subsite. The frontend handles subsites differently. It calls getSubsiteIDForDomain
187
     * directly from ModelAsController::getNestedController. Only gets Subsite instances which have their
188
     * {@link IsPublic} flag set to TRUE.
189
     *
190
     * You can simulate subsite access without creating virtual hosts by appending ?SubsiteID=<ID> to the request.
191
     *
192
     * @return int ID of the current subsite instance
193
     *
194
     * @deprecated 2.0..3.0 Use SubsiteState::singleton()->getSubsiteId() instead
195
     */
196
    public static function currentSubsiteID()
197
    {
198
        Deprecation::notice('3.0', 'Use SubsiteState::singleton()->getSubsiteId() instead');
199
        return SubsiteState::singleton()->getSubsiteId();
200
    }
201
202
    /**
203
     * Switch to another subsite through storing the subsite identifier in the current PHP session.
204
     * Only takes effect when {@link SubsiteState::singleton()->getUseSessions()} is set to TRUE.
205
     *
206
     * @param int|Subsite $subsite Either the ID of the subsite, or the subsite object itself
207
     */
208
    public static function changeSubsite($subsite)
209
    {
210
        // Session subsite change only meaningful if the session is active.
211
        // Otherwise we risk setting it to wrong value, e.g. if we rely on currentSubsiteID.
212
        if (!SubsiteState::singleton()->getUseSessions()) {
213
            return;
214
        }
215
216
        if (is_object($subsite)) {
217
            $subsiteID = $subsite->ID;
218
        } else {
219
            $subsiteID = $subsite;
220
        }
221
222
        SubsiteState::singleton()->setSubsiteId($subsiteID);
223
224
        // Set locale
225
        if (is_object($subsite) && $subsite->Language !== '') {
0 ignored issues
show
Documentation introduced by
The property Language does not exist on object<SilverStripe\Subsites\Model\Subsite>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
226
            $locale = (new IntlLocales())->localeFromLang($subsite->Language);
0 ignored issues
show
Documentation introduced by
The property Language does not exist on object<SilverStripe\Subsites\Model\Subsite>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
227
            if ($locale) {
228
                i18n::set_locale($locale);
229
            }
230
        }
231
232
        Permission::reset();
233
    }
234
235
    /**
236
     * Get a matching subsite for the given host, or for the current HTTP_HOST.
237
     * Supports "fuzzy" matching of domains by placing an asterisk at the start of end of the string,
238
     * for example matching all subdomains on *.example.com with one subsite,
239
     * and all subdomains on *.example.org on another.
240
     *
241
     * @param $host string The host to find the subsite for.  If not specified, $_SERVER['HTTP_HOST'] is used.
242
     * @param bool $checkPermissions
243
     * @return int Subsite ID
244
     */
245
    public static function getSubsiteIDForDomain($host = null, $checkPermissions = true)
0 ignored issues
show
Unused Code introduced by
The parameter $checkPermissions is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Coding Style introduced by
getSubsiteIDForDomain uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
246
    {
247
        if ($host == null && isset($_SERVER['HTTP_HOST'])) {
248
            $host = $_SERVER['HTTP_HOST'];
249
        }
250
251
        $matchingDomains = null;
252
        $cacheKey = null;
253
        if ($host) {
254
            if (!self::$strict_subdomain_matching) {
255
                $host = preg_replace('/^www\./', '', $host);
256
            }
257
258
            $currentUserId = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0;
259
            $cacheKey = implode('_', [$host, $currentUserId, self::$check_is_public]);
260
            if (isset(self::$_cache_subsite_for_domain[$cacheKey])) {
261
                return self::$_cache_subsite_for_domain[$cacheKey];
262
            }
263
264
            $SQL_host = Convert::raw2sql($host);
265
266
            /** @skipUpgrade */
267
            if (!in_array('SubsiteDomain', DB::table_list())) {
268
                // Table hasn't been created yet. Might be a dev/build, skip.
269
                return 0;
270
            }
271
272
            /** @skipUpgrade */
273
            $matchingDomains = DataObject::get(
274
                SubsiteDomain::class,
275
                "'$SQL_host' LIKE replace(\"SubsiteDomain\".\"Domain\",'*','%')",
276
                '"IsPrimary" DESC'
277
            )->innerJoin(
278
                'Subsite',
279
                '"Subsite"."ID" = "SubsiteDomain"."SubsiteID" AND "Subsite"."IsPublic"=1'
280
            );
281
        }
282
283
        if ($matchingDomains && $matchingDomains->count()) {
284
            $subsiteIDs = array_unique($matchingDomains->column('SubsiteID'));
285
            $subsiteDomains = array_unique($matchingDomains->column('Domain'));
286
            if (sizeof($subsiteIDs) > 1) {
287
                throw new UnexpectedValueException(sprintf(
288
                    "Multiple subsites match on '%s': %s",
289
                    $host,
290
                    implode(',', $subsiteDomains)
291
                ));
292
            }
293
294
            $subsiteID = $subsiteIDs[0];
295
        } else {
296
            if ($default = DataObject::get_one(Subsite::class, '"DefaultSite" = 1')) {
297
                // Check for a 'default' subsite
298
                $subsiteID = $default->ID;
299
            } else {
300
                // Default subsite id = 0, the main site
301
                $subsiteID = 0;
302
            }
303
        }
304
305
        if ($cacheKey) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cacheKey of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
306
            self::$_cache_subsite_for_domain[$cacheKey] = $subsiteID;
307
        }
308
309
        return $subsiteID;
310
    }
311
312
    /**
313
     *
314
     * @param string $className
315
     * @param string $filter
316
     * @param string $sort
317
     * @param string $join
318
     * @param string $limit
319
     * @return DataList
320
     */
321
    public static function get_from_all_subsites($className, $filter = '', $sort = '', $join = '', $limit = '')
322
    {
323
        $result = DataObject::get($className, $filter, $sort, $join, $limit);
324
        $result = $result->setDataQueryParam('Subsite.filter', false);
325
        return $result;
326
    }
327
328
    /**
329
     * Disable the sub-site filtering; queries will select from all subsites
330
     * @param bool $disabled
331
     */
332
    public static function disable_subsite_filter($disabled = true)
333
    {
334
        self::$disable_subsite_filter = $disabled;
335
    }
336
337
    /**
338
     * Flush caches on database reset
339
     */
340
    public static function on_db_reset()
341
    {
342
        self::$_cache_accessible_sites = [];
343
        self::$_cache_subsite_for_domain = [];
344
    }
345
346
    /**
347
     * Return all subsites, regardless of permissions (augmented with main site).
348
     *
349
     * @param bool $includeMainSite
350
     * @param string $mainSiteTitle
351
     * @return SS_List List of <a href='psi_element://Subsite'>Subsite</a> objects (DataList or ArrayList).
352
     * objects (DataList or ArrayList).
353
     */
354
    public static function all_sites($includeMainSite = true, $mainSiteTitle = 'Main site')
355
    {
356
        $subsites = Subsite::get();
357
358 View Code Duplication
        if ($includeMainSite) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
359
            $subsites = $subsites->toArray();
360
361
            $mainSite = new Subsite();
362
            $mainSite->Title = $mainSiteTitle;
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<SilverStripe\Subsites\Model\Subsite>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
363
            array_unshift($subsites, $mainSite);
364
365
            $subsites = ArrayList::create($subsites);
366
        }
367
368
        return $subsites;
369
    }
370
371
    /*
372
     * Returns an ArrayList of the subsites accessible to the current user.
373
     * It's enough for any section to be accessible for the site to be included.
374
     *
375
     * @return ArrayList of {@link Subsite} instances.
376
     */
377
    public static function all_accessible_sites($includeMainSite = true, $mainSiteTitle = 'Main site', $member = null)
378
    {
379
        // Rationalise member arguments
380
        if (!$member) {
381
            $member = Security::getCurrentUser();
382
        }
383
        if (!$member) {
384
            return ArrayList::create();
385
        }
386
        if (!is_object($member)) {
387
            $member = DataObject::get_by_id(Member::class, $member);
388
        }
389
390
        $subsites = ArrayList::create();
391
392
        // Collect subsites for all sections.
393
        $menu = CMSMenu::get_viewable_menu_items();
394
        foreach ($menu as $candidate) {
395
            if ($candidate->controller) {
396
                $accessibleSites = singleton($candidate->controller)->sectionSites(
397
                    $includeMainSite,
398
                    $mainSiteTitle,
399
                    $member
400
                );
401
402
                // Replace existing keys so no one site appears twice.
403
                $subsites->merge($accessibleSites);
404
            }
405
        }
406
407
        $subsites->removeDuplicates();
408
409
        return $subsites;
410
    }
411
412
    /**
413
     * Return the subsites that the current user can access by given permission.
414
     * Sites will only be included if they have a Title.
415
     *
416
     * @param $permCode array|string Either a single permission code or an array of permission codes.
417
     * @param $includeMainSite bool If true, the main site will be included if appropriate.
418
     * @param $mainSiteTitle string The label to give to the main site
419
     * @param $member int|Member The member attempting to access the sites
420
     * @return DataList|ArrayList of {@link Subsite} instances
421
     */
422
    public static function accessible_sites(
423
        $permCode,
424
        $includeMainSite = true,
425
        $mainSiteTitle = 'Main site',
426
        $member = null
427
    ) {
428
429
        // Rationalise member arguments
430
        if (!$member) {
431
            $member = Member::currentUser();
0 ignored issues
show
Deprecated Code introduced by
The method SilverStripe\Security\Member::currentUser() has been deprecated with message: 5.0.0 use Security::getCurrentUser()

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
432
        }
433
        if (!$member) {
434
            return new ArrayList();
435
        }
436
        if (!is_object($member)) {
437
            $member = DataObject::get_by_id(Member::class, $member);
438
        }
439
440
        // Rationalise permCode argument
441
        if (is_array($permCode)) {
442
            $SQL_codes = "'" . implode("', '", Convert::raw2sql($permCode)) . "'";
443
        } else {
444
            $SQL_codes = "'" . Convert::raw2sql($permCode) . "'";
445
        }
446
447
        // Cache handling
448
        $cacheKey = $SQL_codes . '-' . $member->ID . '-' . $includeMainSite . '-' . $mainSiteTitle;
449
        if (isset(self::$_cache_accessible_sites[$cacheKey])) {
450
            return self::$_cache_accessible_sites[$cacheKey];
451
        }
452
453
        /** @skipUpgrade */
454
        $subsites = DataList::create(Subsite::class)
455
            ->where("\"Subsite\".\"Title\" != ''")
456
            ->leftJoin('Group_Subsites', '"Group_Subsites"."SubsiteID" = "Subsite"."ID"')
457
            ->innerJoin(
458
                'Group',
459
                '"Group"."ID" = "Group_Subsites"."GroupID" OR "Group"."AccessAllSubsites" = 1'
460
            )
461
            ->innerJoin(
462
                'Group_Members',
463
                "\"Group_Members\".\"GroupID\"=\"Group\".\"ID\" AND \"Group_Members\".\"MemberID\" = $member->ID"
464
            )
465
            ->innerJoin(
466
                'Permission',
467
                "\"Group\".\"ID\"=\"Permission\".\"GroupID\" AND \"Permission\".\"Code\" IN ($SQL_codes, 'CMS_ACCESS_LeftAndMain', 'ADMIN')"
468
            );
469
470
        if (!$subsites) {
471
            $subsites = new ArrayList();
472
        }
473
474
        /** @var DataList $rolesSubsites */
475
        /** @skipUpgrade */
476
        $rolesSubsites = DataList::create(Subsite::class)
477
            ->where("\"Subsite\".\"Title\" != ''")
478
            ->leftJoin('Group_Subsites', '"Group_Subsites"."SubsiteID" = "Subsite"."ID"')
479
            ->innerJoin(
480
                'Group',
481
                '"Group"."ID" = "Group_Subsites"."GroupID" OR "Group"."AccessAllSubsites" = 1'
482
            )
483
            ->innerJoin(
484
                'Group_Members',
485
                "\"Group_Members\".\"GroupID\"=\"Group\".\"ID\" AND \"Group_Members\".\"MemberID\" = $member->ID"
486
            )
487
            ->innerJoin('Group_Roles', '"Group_Roles"."GroupID"="Group"."ID"')
488
            ->innerJoin('PermissionRole', '"Group_Roles"."PermissionRoleID"="PermissionRole"."ID"')
489
            ->innerJoin(
490
                'PermissionRoleCode',
491
                "\"PermissionRole\".\"ID\"=\"PermissionRoleCode\".\"RoleID\" AND \"PermissionRoleCode\".\"Code\" IN ($SQL_codes, 'CMS_ACCESS_LeftAndMain', 'ADMIN')"
492
            );
493
494
        if (!$subsites && $rolesSubsites) {
495
            return $rolesSubsites;
496
        }
497
498
        $subsites = new ArrayList($subsites->toArray());
499
500
        if ($rolesSubsites) {
501
            foreach ($rolesSubsites as $subsite) {
502
                if (!$subsites->find('ID', $subsite->ID)) {
503
                    $subsites->push($subsite);
504
                }
505
            }
506
        }
507
508
        if ($includeMainSite) {
509
            if (!is_array($permCode)) {
510
                $permCode = [$permCode];
511
            }
512 View Code Duplication
            if (self::hasMainSitePermission($member, $permCode)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
513
                $subsites = $subsites->toArray();
514
515
                $mainSite = new Subsite();
516
                $mainSite->Title = $mainSiteTitle;
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<SilverStripe\Subsites\Model\Subsite>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
517
                array_unshift($subsites, $mainSite);
518
                $subsites = ArrayList::create($subsites);
519
            }
520
        }
521
522
        self::$_cache_accessible_sites[$cacheKey] = $subsites;
523
524
        return $subsites;
525
    }
526
527
    /**
528
     * Write a host->domain map to subsites/host-map.php
529
     *
530
     * This is used primarily when using subsites in conjunction with StaticPublisher
531
     *
532
     * @param string $file - filepath of the host map to be written
533
     * @return void
534
     */
535
    public static function writeHostMap($file = null)
536
    {
537
        if (!self::$write_hostmap) {
538
            return;
539
        }
540
541
        if (!$file) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $file of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
542
            $file = Director::baseFolder() . '/subsites/host-map.php';
543
        }
544
        $hostmap = [];
545
546
        $subsites = DataObject::get(Subsite::class);
547
548
        if ($subsites) {
549
            foreach ($subsites as $subsite) {
550
                $domains = $subsite->Domains();
551
                if ($domains) {
552
                    foreach ($domains as $domain) {
553
                        $domainStr = $domain->Domain;
554
                        if (!self::$strict_subdomain_matching) {
555
                            $domainStr = preg_replace('/^www\./', '', $domainStr);
556
                        }
557
                        $hostmap[$domainStr] = $subsite->domain();
558
                    }
559
                }
560
                if ($subsite->DefaultSite) {
561
                    $hostmap['default'] = $subsite->domain();
562
                }
563
            }
564
        }
565
566
        $data = "<?php \n";
567
        $data .= "// Generated by Subsite::writeHostMap() on " . date('d/M/y') . "\n";
568
        $data .= '$subsiteHostmap = ' . var_export($hostmap, true) . ';';
569
570
        if (is_writable(dirname($file)) || is_writable($file)) {
571
            file_put_contents($file, $data);
572
        }
573
    }
574
575
    /**
576
     * Checks if a member can be granted certain permissions, regardless of the subsite context.
577
     * Similar logic to {@link Permission::checkMember()}, but only returns TRUE
578
     * if the member is part of a group with the "AccessAllSubsites" flag set.
579
     * If more than one permission is passed to the method, at least one of them must
580
     * be granted for if to return TRUE.
581
     *
582
     * @todo Allow permission inheritance through group hierarchy.
583
     *
584
     * @param Member Member to check against. Defaults to currently logged in member
585
     * @param array $permissionCodes
586
     * @return bool
587
     */
588
    public static function hasMainSitePermission($member = null, $permissionCodes = ['ADMIN'])
589
    {
590
        if (!is_array($permissionCodes)) {
591
            user_error('Permissions must be passed to Subsite::hasMainSitePermission as an array', E_USER_ERROR);
592
        }
593
594
        if (!$member && $member !== false) {
595
            $member = Security::getCurrentUser();
596
        }
597
598
        if (!$member) {
599
            return false;
600
        }
601
602
        if (!in_array('ADMIN', $permissionCodes)) {
603
            $permissionCodes[] = 'ADMIN';
604
        }
605
606
        $SQLa_perm = Convert::raw2sql($permissionCodes);
607
        $SQL_perms = join("','", $SQLa_perm);
608
        $memberID = (int)$member->ID;
609
610
        // Count this user's groups which can access the main site
611
        $groupCount = DB::query("
612
            SELECT COUNT(\"Permission\".\"ID\")
613
            FROM \"Permission\"
614
            INNER JOIN \"Group\" ON \"Group\".\"ID\" = \"Permission\".\"GroupID\" AND \"Group\".\"AccessAllSubsites\" = 1
615
            INNER JOIN \"Group_Members\" ON \"Group_Members\".\"GroupID\" = \"Permission\".\"GroupID\"
616
            WHERE \"Permission\".\"Code\" IN ('$SQL_perms')
617
            AND \"Group_Members\".\"MemberID\" = {$memberID}
618
        ")->value();
619
620
        // Count this user's groups which have a role that can access the main site
621
        $roleCount = DB::query("
622
            SELECT COUNT(\"PermissionRoleCode\".\"ID\")
623
            FROM \"Group\"
624
            INNER JOIN \"Group_Members\" ON \"Group_Members\".\"GroupID\" = \"Group\".\"ID\"
625
            INNER JOIN \"Group_Roles\" ON \"Group_Roles\".\"GroupID\"=\"Group\".\"ID\"
626
            INNER JOIN \"PermissionRole\" ON \"Group_Roles\".\"PermissionRoleID\"=\"PermissionRole\".\"ID\"
627
            INNER JOIN \"PermissionRoleCode\" ON \"PermissionRole\".\"ID\"=\"PermissionRoleCode\".\"RoleID\"
628
            WHERE \"PermissionRoleCode\".\"Code\" IN ('$SQL_perms')
629
            AND \"Group\".\"AccessAllSubsites\" = 1
630
            AND \"Group_Members\".\"MemberID\" = {$memberID}
631
        ")->value();
632
633
        // There has to be at least one that allows access.
634
        return ($groupCount + $roleCount > 0);
635
    }
636
637
    /**
638
     * @todo Possible security issue, don't grant edit permissions to everybody.
639
     * @param bool $member
640
     * @return bool
641
     */
642
    public function canEdit($member = false)
643
    {
644
        return true;
645
    }
646
647
    /**
648
     * Show the configuration fields for each subsite
649
     *
650
     * @return FieldList
651
     */
652
    public function getCMSFields()
653
    {
654
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
655
            if ($this->exists()) {
656
                // Add a GridField for domains to a new tab if the subsite has already been created
657
                $fields->addFieldsToTab('Root.Domains', [
658
                    GridField::create(
659
                        'Domains',
660
                        _t(__CLASS__ . '.DomainsListTitle', 'Domains'),
661
                        $this->Domains(),
0 ignored issues
show
Documentation Bug introduced by
The method Domains does not exist on object<SilverStripe\Subsites\Model\Subsite>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
662
                        GridFieldConfig_RecordEditor::create(10)
663
                    )
664
                ]);
665
            }
666
667
            // Remove the default scaffolded blacklist field, we replace it with a checkbox set field
668
            // in a wrapper further down. The RedirectURL field is currently not in use.
669
            $fields->removeByName(['PageTypeBlacklist', 'RedirectURL']);
670
671
            // Add the heading to the top of the fields list
672
            $fields->fieldByName('Root.Main')
673
                ->unshift(HeaderField::create('ConfigForSubsiteHeaderField', 'Subsite Configuration'));
674
675
            $fields->addFieldToTab('Root.Main', DropdownField::create(
676
                'Language',
677
                $this->fieldLabel('Language'),
678
                Injector::inst()->get(IntlLocales::class)->getLocales()
679
            ), 'DefaultSite');
680
681
            $fields->addFieldsToTab('Root.Main', [
682
                ToggleCompositeField::create(
683
                    'PageTypeBlacklistToggle',
684
                    _t(__CLASS__ . '.PageTypeBlacklistField', 'Disallow page types?'),
685
                    [
686
                        CheckboxSetField::create('PageTypeBlacklist', '', $this->getPageTypeMap())
687
                    ]
688
                )->setHeadingLevel(4),
689
                HiddenField::create('ID', '', $this->ID),
690
                HiddenField::create('IsSubsite', '', 1)
691
            ]);
692
693
            // If there are any themes available, add the dropdown
694
            $themes = $this->allowedThemes();
695
            if (!empty($themes)) {
696
                $fields->addFieldToTab(
697
                    'Root.Main',
698
                    DropdownField::create('Theme', $this->fieldLabel('Theme'), $this->allowedThemes(), $this->Theme)
0 ignored issues
show
Documentation introduced by
The property Theme does not exist on object<SilverStripe\Subsites\Model\Subsite>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
699
                        ->setEmptyString(_t(__CLASS__ . '.ThemeFieldEmptyString', '-')),
700
                    'PageTypeBlacklistToggle'
701
                );
702
            }
703
704
            // Targetted by the XHR PJAX JavaScript to reload the subsite list in the CMS
705
            $fields->fieldByName('Root.Main')->addExtraClass('subsite-model');
706
707
            // We don't need the Groups many many tab
708
            $fields->removeByName('Groups');
709
        });
710
711
        return parent::getCMSFields();
712
    }
713
714
    /**
715
     * Return a list of the different page types available to the CMS
716
     *
717
     * @return array
718
     */
719
    public function getPageTypeMap()
720
    {
721
        $pageTypeMap = [];
722
723
        $pageTypes = SiteTree::page_type_classes();
724
        foreach ($pageTypes as $pageType) {
725
            $pageTypeMap[$pageType] = singleton($pageType)->i18n_singular_name();
726
        }
727
728
        asort($pageTypeMap);
729
730
        return $pageTypeMap;
731
    }
732
733
    /**
734
     *
735
     * @param boolean $includerelations
736
     * @return array
737
     */
738
    public function fieldLabels($includerelations = true)
739
    {
740
        $labels = parent::fieldLabels($includerelations);
741
        $labels['Title'] = _t('Subsites.TitleFieldLabel', 'Subsite Name');
742
        $labels['RedirectURL'] = _t('Subsites.RedirectURLFieldLabel', 'Redirect URL');
743
        $labels['DefaultSite'] = _t('Subsites.DefaultSiteFieldLabel', 'Default site');
744
        $labels['Theme'] = _t('Subsites.ThemeFieldLabel', 'Theme');
745
        $labels['Language'] = _t('Subsites.LanguageFieldLabel', 'Language');
746
        $labels['IsPublic'] = _t('Subsites.IsPublicFieldLabel', 'Enable public access');
747
        $labels['PageTypeBlacklist'] = _t('Subsites.PageTypeBlacklistFieldLabel', 'Page Type Blacklist');
748
        $labels['Domains.Domain'] = _t('Subsites.DomainFieldLabel', 'Domain');
749
        $labels['PrimaryDomain'] = _t('Subsites.PrimaryDomainFieldLabel', 'Primary Domain');
750
751
        return $labels;
752
    }
753
754
    /**
755
     * Return the themes that can be used with this subsite, as an array of themecode => description
756
     *
757
     * @return array
758
     */
759
    public function allowedThemes()
760
    {
761
        if ($themes = $this->stat('allowed_themes')) {
762
            return ArrayLib::valuekey($themes);
763
        }
764
765
        $themes = [];
766
        if (is_dir(THEMES_PATH)) {
767
            foreach (scandir(THEMES_PATH) as $theme) {
768
                if ($theme[0] == '.') {
769
                    continue;
770
                }
771
                $theme = strtok($theme, '_');
772
                $themes[$theme] = $theme;
773
            }
774
            ksort($themes);
775
        }
776
        return $themes;
777
    }
778
779
    /**
780
     * @return string Current locale of the subsite
781
     */
782
    public function getLanguage()
783
    {
784
        if ($this->getField('Language')) {
785
            return $this->getField('Language');
786
        }
787
788
        return i18n::get_locale();
789
    }
790
791
    /**
792
     *
793
     * @return \SilverStripe\ORM\ValidationResult
794
     */
795
    public function validate()
796
    {
797
        $result = parent::validate();
798
        if (!$this->Title) {
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<SilverStripe\Subsites\Model\Subsite>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
799
            $result->addError(_t(__CLASS__ . '.ValidateTitle', 'Please add a "Title"'));
800
        }
801
        return $result;
802
    }
803
804
    /**
805
     * Whenever a Subsite is written, rewrite the hostmap
806
     *
807
     * @return void
808
     */
809
    public function onAfterWrite()
810
    {
811
        Subsite::writeHostMap();
812
        parent::onAfterWrite();
813
    }
814
815
    /**
816
     * Return the primary domain of this site. Tries to "normalize" the domain name,
817
     * by replacing potential wildcards.
818
     *
819
     * @return string The full domain name of this subsite (without protocol prefix)
820
     */
821
    public function domain()
0 ignored issues
show
Coding Style introduced by
domain uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
822
    {
823
        // Get best SubsiteDomain object
824
        $domainObject = $this->getPrimarySubsiteDomain();
825
        if ($domainObject) {
826
            return $domainObject->SubstitutedDomain;
827
        }
828
829
        // If there are no objects, default to the current hostname
830
        return $_SERVER['HTTP_HOST'];
831
    }
832
833
    /**
834
     * Finds the primary {@see SubsiteDomain} object for this subsite
835
     *
836
     * @return SubsiteDomain
837
     */
838
    public function getPrimarySubsiteDomain()
839
    {
840
        return $this
0 ignored issues
show
Documentation Bug introduced by
The method Domains does not exist on object<SilverStripe\Subsites\Model\Subsite>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
841
            ->Domains()
842
            ->sort('"IsPrimary" DESC')
843
            ->first();
844
    }
845
846
    /**
847
     *
848
     * @return string - The full domain name of this subsite (without protocol prefix)
849
     */
850
    public function getPrimaryDomain()
851
    {
852
        return $this->domain();
853
    }
854
855
    /**
856
     * Get the absolute URL for this subsite
857
     * @return string
858
     */
859
    public function absoluteBaseURL()
860
    {
861
        // Get best SubsiteDomain object
862
        $domainObject = $this->getPrimarySubsiteDomain();
863
        if ($domainObject) {
864
            return $domainObject->absoluteBaseURL();
865
        }
866
867
        // Fall back to the current base url
868
        return Director::absoluteBaseURL();
869
    }
870
871
    /**
872
     * Javascript admin action to duplicate this subsite
873
     *
874
     * @return string - javascript
875
     */
876
    public function adminDuplicate()
877
    {
878
        $newItem = $this->duplicate();
879
        $message = _t(
880
            __CLASS__ . '.CopyMessage',
881
            'Created a copy of {title}',
882
            ['title' => Convert::raw2js($this->Title)]
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<SilverStripe\Subsites\Model\Subsite>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
883
        );
884
885
        return <<<JS
886
            statusMessage($message, 'good');
887
            $('Form_EditForm').loadURLFromServer('admin/subsites/show/$newItem->ID');
888
JS;
889
    }
890
891
    /**
892
     * Make this subsite the current one
893
     */
894
    public function activate()
895
    {
896
        Subsite::changeSubsite($this);
897
    }
898
899
    /**
900
     *
901
     * @param array $permissionCodes
902
     * @return DataList
903
     */
904
    public function getMembersByPermission($permissionCodes = ['ADMIN'])
905
    {
906
        if (!is_array($permissionCodes)) {
907
            user_error('Permissions must be passed to Subsite::getMembersByPermission as an array', E_USER_ERROR);
908
        }
909
        $SQL_permissionCodes = Convert::raw2sql($permissionCodes);
910
911
        $SQL_permissionCodes = join("','", $SQL_permissionCodes);
912
913
        return DataObject::get(
914
            Member::class,
915
            "\"Group\".\"SubsiteID\" = $this->ID AND \"Permission\".\"Code\" IN ('$SQL_permissionCodes')",
916
            '',
917
            'LEFT JOIN "Group_Members" ON "Member"."ID" = "Group_Members"."MemberID"
918
            LEFT JOIN "Group" ON "Group"."ID" = "Group_Members"."GroupID"
919
            LEFT JOIN "Permission" ON "Permission"."GroupID" = "Group"."ID"'
920
        );
921
    }
922
923
    /**
924
     * Duplicate this subsite
925
     * @param bool $doWrite
926
     * @param string $manyMany
927
     * @return DataObject
928
     */
929
    public function duplicate($doWrite = true, $manyMany = 'many_many')
930
    {
931
        $duplicate = parent::duplicate($doWrite);
932
933
        $oldSubsiteID = SubsiteState::singleton()->getSubsiteId();
934
        self::changeSubsite($this->ID);
935
936
        /*
937
         * Copy data from this object to the given subsite. Does this using an iterative depth-first search.
938
         * This will make sure that the new parents on the new subsite are correct, and there are no funny
939
         * issues with having to check whether or not the new parents have been added to the site tree
940
         * when a page, etc, is duplicated
941
         */
942
        $stack = [[0, 0]];
943
        while (count($stack) > 0) {
944
            list($sourceParentID, $destParentID) = array_pop($stack);
945
            $children = Versioned::get_by_stage('Page', 'Live', "\"ParentID\" = $sourceParentID", '');
946
947
            if ($children) {
948
                foreach ($children as $child) {
949
                    self::changeSubsite($duplicate->ID); //Change to destination subsite
950
951
                    $childClone = $child->duplicateToSubsite($duplicate, false);
952
                    $childClone->ParentID = $destParentID;
953
                    $childClone->writeToStage('Stage');
954
                    $childClone->copyVersionToStage('Stage', 'Live');
955
956
                    self::changeSubsite($this->ID); //Change Back to this subsite
957
958
                    array_push($stack, [$child->ID, $childClone->ID]);
959
                }
960
            }
961
        }
962
963
        self::changeSubsite($oldSubsiteID);
964
965
        return $duplicate;
966
    }
967
}
968