Completed
Push — master ( 0ebf95...33622c )
by Damian
12s
created

Subsite   F

Complexity

Total Complexity 97

Size/Duplication

Total Lines 924
Duplicated Lines 1.84 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 17
loc 924
rs 1.263
wmc 97

29 Methods

Rating   Name   Duplication   Size   Complexity  
B hasMainSitePermission() 0 47 6
A domain() 0 10 2
D accessible_sites() 8 103 15
A getPageTypeMap() 0 12 2
B allowedThemes() 0 18 5
A get_from_all_subsites() 0 5 1
A absoluteBaseURL() 0 10 2
A onAfterWrite() 0 4 1
A adminDuplicate() 0 12 1
C getSubsiteIDForDomain() 0 65 13
A set_allowed_themes() 0 3 1
A validate() 0 7 2
A currentSubsite() 0 3 1
B changeSubsite() 0 25 6
A currentSubsiteID() 0 4 1
B all_accessible_sites() 0 33 6
A getLanguage() 0 7 2
A on_db_reset() 0 4 1
A getPrimaryDomain() 0 3 1
A activate() 0 3 1
B duplicate() 0 37 4
A getPrimarySubsiteDomain() 0 6 1
A disable_subsite_filter() 0 3 1
C writeHostMap() 0 37 11
A getCMSFields() 0 59 3
A fieldLabels() 0 14 1
A all_sites() 9 15 2
A canEdit() 0 8 2
A getMembersByPermission() 0 14 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Subsite often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Subsite, and based on these observations, apply Extract Interface, too.

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\Dev\Deprecation;
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\HiddenField;
18
use SilverStripe\Forms\Tab;
19
use SilverStripe\Forms\ToggleCompositeField;
20
use SilverStripe\ORM\ArrayLib;
21
use SilverStripe\ORM\ArrayList;
22
use SilverStripe\ORM\DB;
23
use SilverStripe\ORM\DataList;
24
use SilverStripe\ORM\DataObject;
25
use SilverStripe\ORM\SS_List;
26
use SilverStripe\Security\Group;
27
use SilverStripe\Security\Member;
28
use SilverStripe\Security\Permission;
29
use SilverStripe\Security\Security;
30
use SilverStripe\Subsites\State\SubsiteState;
31
use SilverStripe\Versioned\Versioned;
32
use SilverStripe\i18n\Data\Intl\IntlLocales;
33
use SilverStripe\i18n\i18n;
34
use UnexpectedValueException;
35
36
/**
37
 * A dynamically created subsite. SiteTree objects can now belong to a subsite.
38
 * You can simulate subsite access without setting up virtual hosts by appending ?SubsiteID=<ID> to the request.
39
 *
40
 * @package subsites
41
 */
42
class Subsite extends DataObject
43
{
44
45
    private static $table_name = 'Subsite';
0 ignored issues
show
introduced by Werner M. Krauß
The private property $table_name is not used, and could be removed.
Loading history...
46
47
    /**
48
     * @var boolean $disable_subsite_filter If enabled, bypasses the query decoration
49
     * to limit DataObject::get*() calls to a specific subsite. Useful for debugging.
50
     */
51
    public static $disable_subsite_filter = false;
52
53
    /**
54
     * Allows you to force a specific subsite ID, or comma separated list of IDs.
55
     * Only works for reading. An object cannot be written to more than 1 subsite.
56
     */
57
    public static $force_subsite = null;
58
59
    /**
60
     *
61
     * @var boolean
62
     */
63
    public static $write_hostmap = true;
64
65
    /**
66
     * Memory cache of accessible sites
67
     *
68
     * @array
69
     */
70
    private static $_cache_accessible_sites = [];
71
72
    /**
73
     * Memory cache of subsite id for domains
74
     *
75
     * @var array
76
     */
77
    private static $_cache_subsite_for_domain = [];
78
79
    /**
80
     * @var array $allowed_themes Numeric array of all themes which are allowed to be selected for all subsites.
81
     * Corresponds to subfolder names within the /themes folder. By default, all themes contained in this folder
82
     * are listed.
83
     */
84
    private static $allowed_themes = [];
85
86
    /**
87
     * @var Boolean If set to TRUE, don't assume 'www.example.com' and 'example.com' are the same.
88
     * Doesn't affect wildcard matching, so '*.example.com' will match 'www.example.com' (but not 'example.com')
89
     * in both TRUE or FALSE setting.
90
     */
91
    public static $strict_subdomain_matching = false;
92
93
    /**
94
     * @var boolean Respects the IsPublic flag when retrieving subsites
95
     */
96
    public static $check_is_public = true;
97
98
    /*** @return array
99
     */
100
    private static $summary_fields = [
0 ignored issues
show
introduced by Werner M. Krauß
The private property $summary_fields is not used, and could be removed.
Loading history...
101
        'Title',
102
        'PrimaryDomain',
103
        'IsPublic'
104
    ];
105
106
    /**
107
     * @var array
108
     */
109
    private static $db = [
0 ignored issues
show
introduced by Robbie Averill
The private property $db is not used, and could be removed.
Loading history...
110
        'Title' => 'Varchar(255)',
111
        'RedirectURL' => 'Varchar(255)',
112
        'DefaultSite' => 'Boolean',
113
        'Theme' => 'Varchar',
114
        'Language' => 'Varchar(6)',
115
116
        // Used to hide unfinished/private subsites from public view.
117
        // If unset, will default to true
0 ignored issues
show
Unused Code Comprehensibility introduced by Tim Kung
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...
118
        'IsPublic' => 'Boolean',
119
120
        // Comma-separated list of disallowed page types
121
        'PageTypeBlacklist' => 'Text',
122
    ];
123
124
    /**
125
     * @var array
126
     */
127
    private static $has_many = [
0 ignored issues
show
introduced by Robbie Averill
The private property $has_many is not used, and could be removed.
Loading history...
128
        'Domains' => SubsiteDomain::class,
129
    ];
130
131
    /**
132
     * @var array
133
     */
134
    private static $belongs_many_many = [
0 ignored issues
show
introduced by Robbie Averill
The private property $belongs_many_many is not used, and could be removed.
Loading history...
135
        'Groups' => Group::class,
136
    ];
137
138
    /**
139
     * @var array
140
     */
141
    private static $defaults = [
0 ignored issues
show
introduced by Robbie Averill
The private property $defaults is not used, and could be removed.
Loading history...
142
        'IsPublic' => 1
143
    ];
144
145
    /**
146
     * @var array
147
     */
148
    private static $searchable_fields = [
0 ignored issues
show
introduced by Robbie Averill
The private property $searchable_fields is not used, and could be removed.
Loading history...
149
        'Title',
150
        'Domains.Domain',
151
        'IsPublic',
152
    ];
153
154
    /**
155
     * @var string
156
     */
157
    private static $default_sort = '"Title" ASC';
0 ignored issues
show
introduced by Robbie Averill
The private property $default_sort is not used, and could be removed.
Loading history...
158
159
    /**
160
     * Set allowed themes
161
     *
162
     * @param array $themes - Numeric array of all themes which are allowed to be selected for all subsites.
163
     */
164
    public static function set_allowed_themes($themes)
165
    {
166
        self::$allowed_themes = $themes;
167
    }
168
169
    /**
170
     * Gets the subsite currently set in the session.
171
     *
172
     * @uses ControllerSubsites->controllerAugmentInit()
173
     * @return DataObject The current Subsite
174
     */
175
    public static function currentSubsite()
176
    {
177
        return Subsite::get()->byID(SubsiteState::singleton()->getSubsiteId());
178
    }
179
180
    /**
181
     * This function gets the current subsite ID from the session. It used in the backend so Ajax requests
182
     * use the correct subsite. The frontend handles subsites differently. It calls getSubsiteIDForDomain
183
     * directly from ModelAsController::getNestedController. Only gets Subsite instances which have their
184
     * {@link IsPublic} flag set to TRUE.
185
     *
186
     * You can simulate subsite access without creating virtual hosts by appending ?SubsiteID=<ID> to the request.
187
     *
188
     * @return int ID of the current subsite instance
189
     *
190
     * @deprecated 2.0..3.0 Use SubsiteState::singleton()->getSubsiteId() instead
191
     */
192
    public static function currentSubsiteID()
193
    {
194
        Deprecation::notice('3.0', 'Use SubsiteState::singleton()->getSubsiteId() instead');
195
        return SubsiteState::singleton()->getSubsiteId();
196
    }
197
198
    /**
199
     * Switch to another subsite through storing the subsite identifier in the current PHP session.
200
     * Only takes effect when {@link SubsiteState::singleton()->getUseSessions()} is set to TRUE.
201
     *
202
     * @param int|Subsite $subsite Either the ID of the subsite, or the subsite object itself
203
     */
204
    public static function changeSubsite($subsite)
205
    {
206
        // Session subsite change only meaningful if the session is active.
207
        // Otherwise we risk setting it to wrong value, e.g. if we rely on currentSubsiteID.
208
        if (!SubsiteState::singleton()->getUseSessions()) {
209
            return;
210
        }
211
212
        if (is_object($subsite)) {
213
            $subsiteID = $subsite->ID;
214
        } else {
215
            $subsiteID = $subsite;
216
        }
217
218
        SubsiteState::singleton()->setSubsiteId($subsiteID);
219
220
        // Set locale
221
        if (is_object($subsite) && $subsite->Language !== '') {
0 ignored issues
show
Bug Best Practice introduced by Werner M. Krauß
The property Language does not exist on SilverStripe\Subsites\Model\Subsite. Since you implemented __get, consider adding a @property annotation.
Loading history...
222
            $locale = (new IntlLocales())->localeFromLang($subsite->Language);
223
            if ($locale) {
224
                i18n::set_locale($locale);
225
            }
226
        }
227
228
        Permission::reset();
229
    }
230
231
    /**
232
     * Get a matching subsite for the given host, or for the current HTTP_HOST.
233
     * Supports "fuzzy" matching of domains by placing an asterisk at the start of end of the string,
234
     * for example matching all subdomains on *.example.com with one subsite,
235
     * and all subdomains on *.example.org on another.
236
     *
237
     * @param $host string The host to find the subsite for.  If not specified, $_SERVER['HTTP_HOST'] is used.
238
     * @param bool $checkPermissions
239
     * @return int Subsite ID
240
     */
241
    public static function getSubsiteIDForDomain($host = null, $checkPermissions = true)
242
    {
243
        if ($host == null && isset($_SERVER['HTTP_HOST'])) {
244
            $host = $_SERVER['HTTP_HOST'];
245
        }
246
247
        $matchingDomains = null;
248
        $cacheKey = null;
249
        if ($host) {
250
            if (!self::$strict_subdomain_matching) {
251
                $host = preg_replace('/^www\./', '', $host);
252
            }
253
254
            $currentUserId = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0;
255
            $cacheKey = implode('_', [$host, $currentUserId, self::$check_is_public]);
256
            if (isset(self::$_cache_subsite_for_domain[$cacheKey])) {
257
                return self::$_cache_subsite_for_domain[$cacheKey];
258
            }
259
260
            $SQL_host = Convert::raw2sql($host);
261
262
            /** @skipUpgrade */
263
            if (!in_array('SubsiteDomain', DB::table_list())) {
264
                // Table hasn't been created yet. Might be a dev/build, skip.
265
                return 0;
266
            }
267
268
            /** @skipUpgrade */
269
            $matchingDomains = DataObject::get(
270
                SubsiteDomain::class,
271
                "'$SQL_host' LIKE replace(\"SubsiteDomain\".\"Domain\",'*','%')",
272
                '"IsPrimary" DESC'
273
            )->innerJoin(
274
                'Subsite',
275
                '"Subsite"."ID" = "SubsiteDomain"."SubsiteID" AND "Subsite"."IsPublic"=1'
276
            );
277
        }
278
279
        if ($matchingDomains && $matchingDomains->count()) {
280
            $subsiteIDs = array_unique($matchingDomains->column('SubsiteID'));
281
            $subsiteDomains = array_unique($matchingDomains->column('Domain'));
282
            if (sizeof($subsiteIDs) > 1) {
0 ignored issues
show
Bug introduced by Tim Kung
The call to sizeof() has too few arguments starting with mode. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

282
            if (/** @scrutinizer ignore-call */ sizeof($subsiteIDs) > 1) {

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
283
                throw new UnexpectedValueException(sprintf(
284
                    "Multiple subsites match on '%s': %s",
285
                    $host,
286
                    implode(',', $subsiteDomains)
287
                ));
288
            }
289
290
            $subsiteID = $subsiteIDs[0];
291
        } else {
292
            if ($default = DataObject::get_one(Subsite::class, '"DefaultSite" = 1')) {
293
                // Check for a 'default' subsite
294
                $subsiteID = $default->ID;
295
            } else {
296
                // Default subsite id = 0, the main site
297
                $subsiteID = 0;
298
            }
299
        }
300
301
        if ($cacheKey) {
0 ignored issues
show
Bug Best Practice introduced by Tim Kung
The expression $cacheKey of type null|string 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...
302
            self::$_cache_subsite_for_domain[$cacheKey] = $subsiteID;
303
        }
304
305
        return $subsiteID;
306
    }
307
308
    /**
309
     *
310
     * @param string $className
311
     * @param string $filter
312
     * @param string $sort
313
     * @param string $join
314
     * @param string $limit
315
     * @return DataList
316
     */
317
    public static function get_from_all_subsites($className, $filter = '', $sort = '', $join = '', $limit = '')
318
    {
319
        $result = DataObject::get($className, $filter, $sort, $join, $limit);
320
        $result = $result->setDataQueryParam('Subsite.filter', false);
321
        return $result;
322
    }
323
324
    /**
325
     * Disable the sub-site filtering; queries will select from all subsites
326
     * @param bool $disabled
327
     */
328
    public static function disable_subsite_filter($disabled = true)
329
    {
330
        self::$disable_subsite_filter = $disabled;
331
    }
332
333
    /**
334
     * Flush caches on database reset
335
     */
336
    public static function on_db_reset()
337
    {
338
        self::$_cache_accessible_sites = [];
339
        self::$_cache_subsite_for_domain = [];
340
    }
341
342
    /**
343
     * Return all subsites, regardless of permissions (augmented with main site).
344
     *
345
     * @param bool $includeMainSite
346
     * @param string $mainSiteTitle
347
     * @return SS_List List of <a href='psi_element://Subsite'>Subsite</a> objects (DataList or ArrayList).
348
     * objects (DataList or ArrayList).
349
     */
350
    public static function all_sites($includeMainSite = true, $mainSiteTitle = 'Main site')
351
    {
352
        $subsites = Subsite::get();
353
354 View Code Duplication
        if ($includeMainSite) {
0 ignored issues
show
Duplication introduced by Damian Mooyman
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...
355
            $subsites = $subsites->toArray();
356
357
            $mainSite = new Subsite();
358
            $mainSite->Title = $mainSiteTitle;
0 ignored issues
show
Bug Best Practice introduced by Tim Kung
The property Title does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
359
            array_unshift($subsites, $mainSite);
360
361
            $subsites = ArrayList::create($subsites);
362
        }
363
364
        return $subsites;
365
    }
366
367
    /*
368
     * Returns an ArrayList of the subsites accessible to the current user.
369
     * It's enough for any section to be accessible for the site to be included.
370
     *
371
     * @return ArrayList of {@link Subsite} instances.
372
     */
373
    public static function all_accessible_sites($includeMainSite = true, $mainSiteTitle = 'Main site', $member = null)
374
    {
375
        // Rationalise member arguments
376
        if (!$member) {
377
            $member = Security::getCurrentUser();
378
        }
379
        if (!$member) {
380
            return ArrayList::create();
381
        }
382
        if (!is_object($member)) {
383
            $member = DataObject::get_by_id(Member::class, $member);
384
        }
385
386
        $subsites = ArrayList::create();
387
388
        // Collect subsites for all sections.
389
        $menu = CMSMenu::get_viewable_menu_items();
390
        foreach ($menu as $candidate) {
391
            if ($candidate->controller) {
392
                $accessibleSites = singleton($candidate->controller)->sectionSites(
393
                    $includeMainSite,
394
                    $mainSiteTitle,
395
                    $member
396
                );
397
398
                // Replace existing keys so no one site appears twice.
399
                $subsites->merge($accessibleSites);
400
            }
401
        }
402
403
        $subsites->removeDuplicates();
404
405
        return $subsites;
406
    }
407
408
    /**
409
     * Return the subsites that the current user can access by given permission.
410
     * Sites will only be included if they have a Title.
411
     *
412
     * @param $permCode array|string Either a single permission code or an array of permission codes.
413
     * @param $includeMainSite bool If true, the main site will be included if appropriate.
414
     * @param $mainSiteTitle string The label to give to the main site
415
     * @param $member int|Member The member attempting to access the sites
416
     * @return DataList|ArrayList of {@link Subsite} instances
417
     */
418
    public static function accessible_sites(
419
        $permCode,
420
        $includeMainSite = true,
421
        $mainSiteTitle = 'Main site',
422
        $member = null
423
    ) {
424
425
        // Rationalise member arguments
426
        if (!$member) {
427
            $member = Member::currentUser();
0 ignored issues
show
Deprecated Code introduced by Werner M. Krauß
The function SilverStripe\Security\Member::currentUser() has been deprecated: 5.0.0 use Security::getCurrentUser() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

427
            $member = /** @scrutinizer ignore-deprecated */ Member::currentUser();

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

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

Loading history...
428
        }
429
        if (!$member) {
430
            return new ArrayList();
431
        }
432
        if (!is_object($member)) {
433
            $member = DataObject::get_by_id(Member::class, $member);
434
        }
435
436
        // Rationalise permCode argument
437
        if (is_array($permCode)) {
438
            $SQL_codes = "'" . implode("', '", Convert::raw2sql($permCode)) . "'";
439
        } else {
440
            $SQL_codes = "'" . Convert::raw2sql($permCode) . "'";
0 ignored issues
show
Bug introduced by Tim Kung
Are you sure SilverStripe\Core\Convert::raw2sql($permCode) of type string|array can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

440
            $SQL_codes = "'" . /** @scrutinizer ignore-type */ Convert::raw2sql($permCode) . "'";
Loading history...
441
        }
442
443
        // Cache handling
444
        $cacheKey = $SQL_codes . '-' . $member->ID . '-' . $includeMainSite . '-' . $mainSiteTitle;
445
        if (isset(self::$_cache_accessible_sites[$cacheKey])) {
446
            return self::$_cache_accessible_sites[$cacheKey];
447
        }
448
449
        /** @skipUpgrade */
450
        $subsites = DataList::create(Subsite::class)
0 ignored issues
show
Bug introduced by Werner M. Krauß
SilverStripe\Subsites\Model\Subsite::class of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

450
        $subsites = DataList::create(/** @scrutinizer ignore-type */ Subsite::class)
Loading history...
451
            ->where("\"Subsite\".\"Title\" != ''")
452
            ->leftJoin('Group_Subsites', '"Group_Subsites"."SubsiteID" = "Subsite"."ID"')
453
            ->innerJoin(
454
                'Group',
455
                '"Group"."ID" = "Group_Subsites"."GroupID" OR "Group"."AccessAllSubsites" = 1'
456
            )
457
            ->innerJoin(
458
                'Group_Members',
459
                "\"Group_Members\".\"GroupID\"=\"Group\".\"ID\" AND \"Group_Members\".\"MemberID\" = $member->ID"
460
            )
461
            ->innerJoin(
462
                'Permission',
463
                "\"Group\".\"ID\"=\"Permission\".\"GroupID\" AND \"Permission\".\"Code\" IN ($SQL_codes, 'CMS_ACCESS_LeftAndMain', 'ADMIN')"
464
            );
465
466
        if (!$subsites) {
467
            $subsites = new ArrayList();
468
        }
469
470
        /** @var DataList $rolesSubsites */
471
        /** @skipUpgrade */
472
        $rolesSubsites = DataList::create(Subsite::class)
473
            ->where("\"Subsite\".\"Title\" != ''")
474
            ->leftJoin('Group_Subsites', '"Group_Subsites"."SubsiteID" = "Subsite"."ID"')
475
            ->innerJoin(
476
                'Group',
477
                '"Group"."ID" = "Group_Subsites"."GroupID" OR "Group"."AccessAllSubsites" = 1'
478
            )
479
            ->innerJoin(
480
                'Group_Members',
481
                "\"Group_Members\".\"GroupID\"=\"Group\".\"ID\" AND \"Group_Members\".\"MemberID\" = $member->ID"
482
            )
483
            ->innerJoin('Group_Roles', '"Group_Roles"."GroupID"="Group"."ID"')
484
            ->innerJoin('PermissionRole', '"Group_Roles"."PermissionRoleID"="PermissionRole"."ID"')
485
            ->innerJoin(
486
                'PermissionRoleCode',
487
                "\"PermissionRole\".\"ID\"=\"PermissionRoleCode\".\"RoleID\" AND \"PermissionRoleCode\".\"Code\" IN ($SQL_codes, 'CMS_ACCESS_LeftAndMain', 'ADMIN')"
488
            );
489
490
        if (!$subsites && $rolesSubsites) {
491
            return $rolesSubsites;
492
        }
493
494
        $subsites = new ArrayList($subsites->toArray());
495
496
        if ($rolesSubsites) {
497
            foreach ($rolesSubsites as $subsite) {
498
                if (!$subsites->find('ID', $subsite->ID)) {
499
                    $subsites->push($subsite);
500
                }
501
            }
502
        }
503
504
        if ($includeMainSite) {
505
            if (!is_array($permCode)) {
506
                $permCode = [$permCode];
507
            }
508 View Code Duplication
            if (self::hasMainSitePermission($member, $permCode)) {
0 ignored issues
show
Duplication introduced by Damian Mooyman
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...
509
                $subsites = $subsites->toArray();
510
511
                $mainSite = new Subsite();
512
                $mainSite->Title = $mainSiteTitle;
0 ignored issues
show
Bug Best Practice introduced by Werner M. Krauß
The property Title does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
513
                array_unshift($subsites, $mainSite);
514
                $subsites = ArrayList::create($subsites);
515
            }
516
        }
517
518
        self::$_cache_accessible_sites[$cacheKey] = $subsites;
519
520
        return $subsites;
521
    }
522
523
    /**
524
     * Write a host->domain map to subsites/host-map.php
525
     *
526
     * This is used primarily when using subsites in conjunction with StaticPublisher
527
     *
528
     * @param string $file - filepath of the host map to be written
529
     * @return void
530
     */
531
    public static function writeHostMap($file = null)
532
    {
533
        if (!self::$write_hostmap) {
534
            return;
535
        }
536
537
        if (!$file) {
0 ignored issues
show
Bug Best Practice introduced by Werner M. Krauß
The expression $file of type null|string 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...
538
            $file = Director::baseFolder() . '/subsites/host-map.php';
539
        }
540
        $hostmap = [];
541
542
        $subsites = DataObject::get(Subsite::class);
543
544
        if ($subsites) {
545
            foreach ($subsites as $subsite) {
546
                $domains = $subsite->Domains();
547
                if ($domains) {
548
                    foreach ($domains as $domain) {
549
                        $domainStr = $domain->Domain;
550
                        if (!self::$strict_subdomain_matching) {
551
                            $domainStr = preg_replace('/^www\./', '', $domainStr);
552
                        }
553
                        $hostmap[$domainStr] = $subsite->domain();
554
                    }
555
                }
556
                if ($subsite->DefaultSite) {
557
                    $hostmap['default'] = $subsite->domain();
558
                }
559
            }
560
        }
561
562
        $data = "<?php \n";
563
        $data .= "// Generated by Subsite::writeHostMap() on " . date('d/M/y') . "\n";
0 ignored issues
show
Bug introduced by Damian Mooyman
Are you sure date('d/M/y') of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

563
        $data .= "// Generated by Subsite::writeHostMap() on " . /** @scrutinizer ignore-type */ date('d/M/y') . "\n";
Loading history...
564
        $data .= '$subsiteHostmap = ' . var_export($hostmap, true) . ';';
565
566
        if (is_writable(dirname($file)) || is_writable($file)) {
567
            file_put_contents($file, $data);
568
        }
569
    }
570
571
    /**
572
     * Checks if a member can be granted certain permissions, regardless of the subsite context.
573
     * Similar logic to {@link Permission::checkMember()}, but only returns TRUE
574
     * if the member is part of a group with the "AccessAllSubsites" flag set.
575
     * If more than one permission is passed to the method, at least one of them must
576
     * be granted for if to return TRUE.
577
     *
578
     * @todo Allow permission inheritance through group hierarchy.
579
     *
580
     * @param Member Member to check against. Defaults to currently logged in member
581
     * @param array $permissionCodes
582
     * @return bool
583
     */
584
    public static function hasMainSitePermission($member = null, $permissionCodes = ['ADMIN'])
585
    {
586
        if (!is_array($permissionCodes)) {
587
            user_error('Permissions must be passed to Subsite::hasMainSitePermission as an array', E_USER_ERROR);
588
        }
589
590
        if (!$member && $member !== false) {
591
            $member = Security::getCurrentUser();
592
        }
593
594
        if (!$member) {
595
            return false;
596
        }
597
598
        if (!in_array('ADMIN', $permissionCodes)) {
599
            $permissionCodes[] = 'ADMIN';
600
        }
601
602
        $SQLa_perm = Convert::raw2sql($permissionCodes);
603
        $SQL_perms = join("','", $SQLa_perm);
604
        $memberID = (int)$member->ID;
605
606
        // Count this user's groups which can access the main site
607
        $groupCount = DB::query("
608
            SELECT COUNT(\"Permission\".\"ID\")
609
            FROM \"Permission\"
610
            INNER JOIN \"Group\" ON \"Group\".\"ID\" = \"Permission\".\"GroupID\" AND \"Group\".\"AccessAllSubsites\" = 1
611
            INNER JOIN \"Group_Members\" ON \"Group_Members\".\"GroupID\" = \"Permission\".\"GroupID\"
612
            WHERE \"Permission\".\"Code\" IN ('$SQL_perms')
613
            AND \"Group_Members\".\"MemberID\" = {$memberID}
614
        ")->value();
615
616
        // Count this user's groups which have a role that can access the main site
617
        $roleCount = DB::query("
618
            SELECT COUNT(\"PermissionRoleCode\".\"ID\")
619
            FROM \"Group\"
620
            INNER JOIN \"Group_Members\" ON \"Group_Members\".\"GroupID\" = \"Group\".\"ID\"
621
            INNER JOIN \"Group_Roles\" ON \"Group_Roles\".\"GroupID\"=\"Group\".\"ID\"
622
            INNER JOIN \"PermissionRole\" ON \"Group_Roles\".\"PermissionRoleID\"=\"PermissionRole\".\"ID\"
623
            INNER JOIN \"PermissionRoleCode\" ON \"PermissionRole\".\"ID\"=\"PermissionRoleCode\".\"RoleID\"
624
            WHERE \"PermissionRoleCode\".\"Code\" IN ('$SQL_perms')
625
            AND \"Group\".\"AccessAllSubsites\" = 1
626
            AND \"Group_Members\".\"MemberID\" = {$memberID}
627
        ")->value();
628
629
        // There has to be at least one that allows access.
630
        return ($groupCount + $roleCount > 0);
631
    }
632
633
    /**
634
     * @todo Possible security issue, don't grant edit permissions to everybody.
635
     * @param bool $member
636
     * @return bool
637
     */
638
    public function canEdit($member = false)
639
    {
640
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by Jason Irish
$member of type boolean is incompatible with the type SilverStripe\Security\Member|integer expected by parameter $member of SilverStripe\ORM\DataObject::extendedCan(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

640
        $extended = $this->extendedCan(__FUNCTION__, /** @scrutinizer ignore-type */ $member);
Loading history...
Bug introduced by Jason Irish
Are you sure the assignment to $extended is correct as $this->extendedCan(__FUNCTION__, $member) targeting SilverStripe\ORM\DataObject::extendedCan() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
641
        if ($extended !== null) {
642
            return $extended;
643
        }
644
645
        return true;
646
    }
647
648
    /**
649
     * Show the configuration fields for each subsite
650
     *
651
     * @return FieldList
652
     */
653
    public function getCMSFields()
654
    {
655
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
656
            if ($this->exists()) {
657
                // Add a GridField for domains to a new tab if the subsite has already been created
658
                $fields->addFieldsToTab('Root.Domains', [
659
                    GridField::create(
660
                        'Domains',
0 ignored issues
show
Bug introduced by Robbie Averill
'Domains' of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

660
                        /** @scrutinizer ignore-type */ 'Domains',
Loading history...
661
                        '',
662
                        $this->Domains(),
0 ignored issues
show
Bug introduced by Robbie Averill
The method Domains() does not exist on SilverStripe\Subsites\Model\Subsite. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

662
                        $this->/** @scrutinizer ignore-call */ 
663
                               Domains(),
Loading history...
663
                        GridFieldConfig_RecordEditor::create(10)
0 ignored issues
show
Bug introduced by Robbie Averill
10 of type integer is incompatible with the type array expected by parameter $args of SilverStripe\Forms\GridF...idFieldConfig::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

663
                        GridFieldConfig_RecordEditor::create(/** @scrutinizer ignore-type */ 10)
Loading history...
664
                    )
665
                ]);
666
            }
667
668
            // Remove the default scaffolded blacklist field, we replace it with a checkbox set field
669
            // in a wrapper further down. The RedirectURL field is currently not in use.
670
            $fields->removeByName(['PageTypeBlacklist', 'RedirectURL']);
671
672
            $fields->addFieldToTab('Root.Main', DropdownField::create(
673
                'Language',
674
                $this->fieldLabel('Language'),
675
                Injector::inst()->get(IntlLocales::class)->getLocales()
676
            ), 'DefaultSite');
677
678
            $fields->addFieldsToTab('Root.Main', [
679
                ToggleCompositeField::create(
680
                    'PageTypeBlacklistToggle',
681
                    _t(__CLASS__ . '.PageTypeBlacklistField', 'Disallow page types?'),
682
                    [
683
                        CheckboxSetField::create('PageTypeBlacklist', '', $this->getPageTypeMap())
684
                    ]
685
                )->setHeadingLevel(4),
686
                HiddenField::create('ID', '', $this->ID),
0 ignored issues
show
Bug introduced by Robbie Averill
$this->ID of type integer is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

686
                HiddenField::create('ID', '', /** @scrutinizer ignore-type */ $this->ID),
Loading history...
687
                HiddenField::create('IsSubsite', '', 1)
688
            ]);
689
690
            // If there are any themes available, add the dropdown
691
            $themes = $this->allowedThemes();
692
            if (!empty($themes)) {
693
                $fields->addFieldToTab(
694
                    'Root.Main',
695
                    DropdownField::create('Theme', $this->fieldLabel('Theme'), $this->allowedThemes(), $this->Theme)
0 ignored issues
show
Bug Best Practice introduced by Robbie Averill
The property Theme does not exist on SilverStripe\Subsites\Model\Subsite. Since you implemented __get, consider adding a @property annotation.
Loading history...
696
                        ->setEmptyString(_t(__CLASS__ . '.ThemeFieldEmptyString', '-')),
697
                    'PageTypeBlacklistToggle'
698
                );
699
            }
700
701
            // Targetted by the XHR PJAX JavaScript to reload the subsite list in the CMS
702
            $fields->fieldByName('Root.Main')->addExtraClass('subsite-model');
703
704
            // We don't need the Groups many many tab
705
            $fields->removeByName('Groups');
706
707
            // Rename the main tab to configuration
708
            $fields->fieldByName('Root.Main')->setTitle(_t(__CLASS__ . '.ConfigurationTab', 'Configuration'));
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;
0 ignored issues
show
Bug Best Practice introduced by Tim Kung
The expression return $labels also could return the type string which is incompatible with the documented return type array.
Loading history...
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')) {
0 ignored issues
show
Deprecated Code introduced by Tim Kung
The function SilverStripe\View\ViewableData::stat() has been deprecated: 5.0 Use ->config()->get() instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

761
        if ($themes = /** @scrutinizer ignore-deprecated */ $this->stat('allowed_themes')) {

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

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

Loading history...
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');
0 ignored issues
show
Bug Best Practice introduced by Tim Kung
The expression return $this->getField('Language') also could return the type object which is incompatible with the documented return type string.
Loading history...
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) {
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()
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
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)]
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