Subsite   F
last analyzed

Complexity

Total Complexity 100

Size/Duplication

Total Lines 972
Duplicated Lines 0 %

Importance

Changes 10
Bugs 0 Features 0
Metric Value
eloc 362
c 10
b 0
f 0
dl 0
loc 972
rs 2
wmc 100

30 Methods

Rating   Name   Duplication   Size   Complexity  
A set_allowed_themes() 0 3 1
A hasMainSitePermission() 0 49 6
C accessible_sites() 0 107 15
A domain() 0 10 2
A getPageTypeMap() 0 12 2
A get_from_all_subsites() 0 5 1
A absoluteBaseURL() 0 10 2
A onAfterWrite() 0 7 2
A adminDuplicate() 0 12 1
C getSubsiteIDForDomain() 0 74 13
A validate() 0 7 2
A currentSubsite() 0 3 1
A changeSubsite() 0 25 6
A currentSubsiteID() 0 4 1
A 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
A duplicate() 0 37 4
A getPrimarySubsiteDomain() 0 6 1
A disable_subsite_filter() 0 3 1
B writeHostMap() 0 38 11
A getCMSFields() 0 59 3
A fieldLabels() 0 14 1
A all_sites() 0 15 2
A createDefaultPages() 0 9 1
A canEdit() 0 8 2
A getMembersByPermission() 0 14 2
A allowedThemes() 0 18 6

How to fix   Complexity   

Complex Class

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\Core\Convert;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\Core\Manifest\ModuleLoader;
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\ToggleCompositeField;
19
use SilverStripe\i18n\Data\Intl\IntlLocales;
20
use SilverStripe\i18n\i18n;
21
use SilverStripe\ORM\ArrayLib;
22
use SilverStripe\ORM\ArrayList;
23
use SilverStripe\ORM\DataList;
24
use SilverStripe\ORM\DataObject;
25
use SilverStripe\ORM\DB;
26
use SilverStripe\ORM\SS_List;
27
use SilverStripe\Security\Group;
28
use SilverStripe\Security\Member;
29
use SilverStripe\Security\Permission;
30
use SilverStripe\Security\Security;
31
use SilverStripe\Subsites\Service\ThemeResolver;
32
use SilverStripe\Subsites\State\SubsiteState;
33
use SilverStripe\Versioned\Versioned;
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. Note that
50
     * for now this is left as a public static property to avoid having to nest and mutate the
51
     * configuration manifest.
52
     */
53
    public static $disable_subsite_filter = false;
54
55
    /**
56
     * Allows you to force a specific subsite ID, or comma separated list of IDs.
57
     * Only works for reading. An object cannot be written to more than 1 subsite.
58
     *
59
     * @deprecated 2.0.0..3.0.0 Use SubsiteState::singleton()->withState() instead.
60
     */
61
    public static $force_subsite = null;
62
63
    /**
64
     * Whether to write a host-map.php file
65
     *
66
     * @config
67
     * @var boolean
68
     */
69
    private static $write_hostmap = true;
0 ignored issues
show
introduced by Robbie Averill
The private property $write_hostmap is not used, and could be removed.
Loading history...
70
71
    /**
72
     * Memory cache of accessible sites
73
     *
74
     * @array
75
     */
76
    protected static $cache_accessible_sites = [];
77
78
    /**
79
     * Memory cache of subsite id for domains
80
     *
81
     * @var array
82
     */
83
    protected static $cache_subsite_for_domain = [];
84
85
    /**
86
     * Numeric array of all themes which are allowed to be selected for all subsites.
87
     * Corresponds to subfolder names within the /themes folder. By default, all themes contained in this folder
88
     * are listed.
89
     *
90
     * @var array
91
     */
92
    protected static $allowed_themes = [];
93
94
    /**
95
     * If set to TRUE, don't assume 'www.example.com' and 'example.com' are the same.
96
     * Doesn't affect wildcard matching, so '*.example.com' will match 'www.example.com' (but not 'example.com')
97
     * in both TRUE or FALSE setting.
98
     *
99
     * @config
100
     * @var boolean
101
     */
102
    private static $strict_subdomain_matching = false;
0 ignored issues
show
introduced by Robbie Averill
The private property $strict_subdomain_matching is not used, and could be removed.
Loading history...
103
104
    /**
105
     * Respects the IsPublic flag when retrieving subsites
106
     *
107
     * @config
108
     * @var boolean
109
     */
110
    private static $check_is_public = true;
0 ignored issues
show
introduced by Robbie Averill
The private property $check_is_public is not used, and could be removed.
Loading history...
111
112
    /**
113
     * @var array
114
     */
115
    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...
116
        'Title',
117
        'PrimaryDomain',
118
        'IsPublic.Nice'
119
    ];
120
121
    /**
122
     * @var array
123
     */
124
    private static $db = [
0 ignored issues
show
introduced by Robbie Averill
The private property $db is not used, and could be removed.
Loading history...
125
        'Title' => 'Varchar(255)',
126
        'RedirectURL' => 'Varchar(255)',
127
        'DefaultSite' => 'Boolean',
128
        'Theme' => 'Varchar',
129
        'Language' => 'Varchar(6)',
130
131
        // Used to hide unfinished/private subsites from public view.
132
        // If unset, will default to true
133
        'IsPublic' => 'Boolean',
134
135
        // Comma-separated list of disallowed page types
136
        'PageTypeBlacklist' => 'Text',
137
    ];
138
139
    /**
140
     * @var array
141
     */
142
    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...
143
        'Domains' => SubsiteDomain::class,
144
    ];
145
146
    /**
147
     * @var array
148
     */
149
    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...
150
        'Groups' => Group::class,
151
    ];
152
153
    /**
154
     * @var array
155
     */
156
    private static $defaults = [
0 ignored issues
show
introduced by Robbie Averill
The private property $defaults is not used, and could be removed.
Loading history...
157
        'IsPublic' => 1
158
    ];
159
160
    /**
161
     * @var array
162
     */
163
    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...
164
        'Title',
165
        'Domains.Domain',
166
        'IsPublic',
167
    ];
168
169
    /**
170
     * @var string
171
     */
172
    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...
173
174
    /**
175
     * Set allowed themes
176
     *
177
     * @param array $themes - Numeric array of all themes which are allowed to be selected for all subsites.
178
     */
179
    public static function set_allowed_themes($themes)
180
    {
181
        self::$allowed_themes = $themes;
182
    }
183
184
    /**
185
     * Gets the subsite currently set in the session.
186
     *
187
     * @return DataObject The current Subsite
188
     */
189
    public static function currentSubsite()
190
    {
191
        return Subsite::get()->byID(SubsiteState::singleton()->getSubsiteId());
192
    }
193
194
    /**
195
     * This function gets the current subsite ID from the session. It used in the backend so Ajax requests
196
     * use the correct subsite. The frontend handles subsites differently. It calls getSubsiteIDForDomain
197
     * directly from ModelAsController::getNestedController. Only gets Subsite instances which have their
198
     * {@link IsPublic} flag set to TRUE.
199
     *
200
     * You can simulate subsite access without creating virtual hosts by appending ?SubsiteID=<ID> to the request.
201
     *
202
     * @return int ID of the current subsite instance
203
     *
204
     * @deprecated 2.0..3.0 Use SubsiteState::singleton()->getSubsiteId() instead
205
     */
206
    public static function currentSubsiteID()
207
    {
208
        Deprecation::notice('3.0', 'Use SubsiteState::singleton()->getSubsiteId() instead');
209
        return SubsiteState::singleton()->getSubsiteId();
210
    }
211
212
    /**
213
     * Switch to another subsite through storing the subsite identifier in the current PHP session.
214
     * Only takes effect when {@link SubsiteState::singleton()->getUseSessions()} is set to TRUE.
215
     *
216
     * @param int|Subsite $subsite Either the ID of the subsite, or the subsite object itself
217
     */
218
    public static function changeSubsite($subsite)
219
    {
220
        // Session subsite change only meaningful if the session is active.
221
        // Otherwise we risk setting it to wrong value, e.g. if we rely on currentSubsiteID.
222
        if (!SubsiteState::singleton()->getUseSessions()) {
223
            return;
224
        }
225
226
        if (is_object($subsite)) {
227
            $subsiteID = $subsite->ID;
228
        } else {
229
            $subsiteID = $subsite;
230
        }
231
232
        SubsiteState::singleton()->setSubsiteId($subsiteID);
233
234
        // Set locale
235
        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...
236
            $locale = (new IntlLocales())->localeFromLang($subsite->Language);
237
            if ($locale) {
238
                i18n::set_locale($locale);
239
            }
240
        }
241
242
        Permission::reset();
243
    }
244
245
    /**
246
     * Get a matching subsite for the given host, or for the current HTTP_HOST.
247
     * Supports "fuzzy" matching of domains by placing an asterisk at the start of end of the string,
248
     * for example matching all subdomains on *.example.com with one subsite,
249
     * and all subdomains on *.example.org on another.
250
     *
251
     * @param $host string The host to find the subsite for.  If not specified, $_SERVER['HTTP_HOST'] is used.
252
     * @param bool $checkPermissions
253
     * @return int Subsite ID
254
     */
255
    public static function getSubsiteIDForDomain($host = null, $checkPermissions = true)
256
    {
257
        if ($host == null && isset($_SERVER['HTTP_HOST'])) {
258
            $host = $_SERVER['HTTP_HOST'];
259
        }
260
261
        // Remove ports, we aren't concerned with them in terms of detecting subsites via domains
262
        $hostParts = explode(':', $host, 2);
263
        $host = reset($hostParts);
264
265
        $matchingDomains = null;
266
        $cacheKey = null;
267
        if ($host) {
268
            if (!static::config()->get('strict_subdomain_matching')) {
269
                $host = preg_replace('/^www\./', '', $host);
270
            }
271
272
            $currentUserId = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0;
273
            $cacheKey = implode('_', [$host, $currentUserId, static::config()->get('check_is_public')]);
274
            if (isset(self::$cache_subsite_for_domain[$cacheKey])) {
275
                return self::$cache_subsite_for_domain[$cacheKey];
276
            }
277
278
            $SQL_host = Convert::raw2sql($host);
279
280
            $schema = DataObject::getSchema();
281
282
            /** @skipUpgrade */
283
            $domainTableName = $schema->tableName(SubsiteDomain::class);
284
            if (!in_array($domainTableName, DB::table_list())) {
285
                // Table hasn't been created yet. Might be a dev/build, skip.
286
                return 0;
287
            }
288
289
            $subsiteTableName = $schema->tableName(__CLASS__);
290
            /** @skipUpgrade */
291
            $matchingDomains = DataObject::get(
292
                SubsiteDomain::class,
293
                "'$SQL_host' LIKE replace(\"{$domainTableName}\".\"Domain\",'*','%')",
294
                '"IsPrimary" DESC'
295
            )->innerJoin(
296
                $subsiteTableName,
297
                '"' . $subsiteTableName . '"."ID" = "SubsiteDomain"."SubsiteID" AND "'
298
                    . $subsiteTableName . '"."IsPublic"=1'
299
            );
300
        }
301
302
        if ($matchingDomains && $matchingDomains->count()) {
303
            $subsiteIDs = array_unique($matchingDomains->column('SubsiteID'));
304
            $subsiteDomains = array_unique($matchingDomains->column('Domain'));
305
            if (sizeof($subsiteIDs) > 1) {
306
                throw new UnexpectedValueException(sprintf(
307
                    "Multiple subsites match on '%s': %s",
308
                    $host,
309
                    implode(',', $subsiteDomains)
310
                ));
311
            }
312
313
            $subsiteID = $subsiteIDs[0];
314
        } else {
315
            if ($default = DataObject::get_one(Subsite::class, '"DefaultSite" = 1')) {
316
                // Check for a 'default' subsite
317
                $subsiteID = $default->ID;
318
            } else {
319
                // Default subsite id = 0, the main site
320
                $subsiteID = 0;
321
            }
322
        }
323
324
        if ($cacheKey) {
325
            self::$cache_subsite_for_domain[$cacheKey] = $subsiteID;
326
        }
327
328
        return $subsiteID;
329
    }
330
331
    /**
332
     *
333
     * @param string $className
334
     * @param string $filter
335
     * @param string $sort
336
     * @param string $join
337
     * @param string $limit
338
     * @return DataList
339
     */
340
    public static function get_from_all_subsites($className, $filter = '', $sort = '', $join = '', $limit = '')
341
    {
342
        $result = DataObject::get($className, $filter, $sort, $join, $limit);
343
        $result = $result->setDataQueryParam('Subsite.filter', false);
344
        return $result;
345
    }
346
347
    /**
348
     * Disable the sub-site filtering; queries will select from all subsites
349
     * @param bool $disabled
350
     */
351
    public static function disable_subsite_filter($disabled = true)
352
    {
353
        self::$disable_subsite_filter = $disabled;
354
    }
355
356
    /**
357
     * Flush caches on database reset
358
     */
359
    public static function on_db_reset()
360
    {
361
        self::$cache_accessible_sites = [];
362
        self::$cache_subsite_for_domain = [];
363
    }
364
365
    /**
366
     * Return all subsites, regardless of permissions (augmented with main site).
367
     *
368
     * @param bool $includeMainSite
369
     * @param string $mainSiteTitle
370
     * @return SS_List List of <a href='psi_element://Subsite'>Subsite</a> objects (DataList or ArrayList).
371
     * objects (DataList or ArrayList).
372
     */
373
    public static function all_sites($includeMainSite = true, $mainSiteTitle = 'Main site')
374
    {
375
        $subsites = Subsite::get();
376
377
        if ($includeMainSite) {
378
            $subsites = $subsites->toArray();
379
380
            $mainSite = new Subsite();
381
            $mainSite->Title = $mainSiteTitle;
382
            array_unshift($subsites, $mainSite);
383
384
            $subsites = ArrayList::create($subsites);
385
        }
386
387
        return $subsites;
388
    }
389
390
    /*
391
     * Returns an ArrayList of the subsites accessible to the current user.
392
     * It's enough for any section to be accessible for the site to be included.
393
     *
394
     * @return ArrayList of {@link Subsite} instances.
395
     */
396
    public static function all_accessible_sites($includeMainSite = true, $mainSiteTitle = 'Main site', $member = null)
397
    {
398
        // Rationalise member arguments
399
        if (!$member) {
400
            $member = Security::getCurrentUser();
401
        }
402
        if (!$member) {
403
            return ArrayList::create();
404
        }
405
        if (!is_object($member)) {
406
            $member = DataObject::get_by_id(Member::class, $member);
407
        }
408
409
        $subsites = ArrayList::create();
410
411
        // Collect subsites for all sections.
412
        $menu = CMSMenu::get_viewable_menu_items();
413
        foreach ($menu as $candidate) {
414
            if ($candidate->controller) {
415
                $accessibleSites = singleton($candidate->controller)->sectionSites(
416
                    $includeMainSite,
417
                    $mainSiteTitle,
418
                    $member
419
                );
420
421
                // Replace existing keys so no one site appears twice.
422
                $subsites->merge($accessibleSites);
423
            }
424
        }
425
426
        $subsites->removeDuplicates();
427
428
        return $subsites;
429
    }
430
431
    /**
432
     * Return the subsites that the current user can access by given permission.
433
     * Sites will only be included if they have a Title.
434
     *
435
     * @param $permCode array|string Either a single permission code or an array of permission codes.
436
     * @param $includeMainSite bool If true, the main site will be included if appropriate.
437
     * @param $mainSiteTitle string The label to give to the main site
438
     * @param $member int|Member The member attempting to access the sites
439
     * @return DataList|ArrayList of {@link Subsite} instances
440
     */
441
    public static function accessible_sites(
442
        $permCode,
443
        $includeMainSite = true,
444
        $mainSiteTitle = 'Main site',
445
        $member = null
446
    ) {
447
448
        // Rationalise member arguments
449
        if (!$member) {
450
            $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

450
            $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...
451
        }
452
        if (!$member) {
453
            return new ArrayList();
454
        }
455
        if (!is_object($member)) {
456
            $member = DataObject::get_by_id(Member::class, $member);
457
        }
458
459
        // Rationalise permCode argument
460
        if (is_array($permCode)) {
461
            $SQL_codes = "'" . implode("', '", Convert::raw2sql($permCode)) . "'";
462
        } else {
463
            $SQL_codes = "'" . Convert::raw2sql($permCode) . "'";
0 ignored issues
show
Bug introduced by Tim Kung
Are you sure SilverStripe\Core\Convert::raw2sql($permCode) of type array|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

463
            $SQL_codes = "'" . /** @scrutinizer ignore-type */ Convert::raw2sql($permCode) . "'";
Loading history...
464
        }
465
466
        // Cache handling
467
        $cacheKey = $SQL_codes . '-' . $member->ID . '-' . $includeMainSite . '-' . $mainSiteTitle;
468
        if (isset(self::$cache_accessible_sites[$cacheKey])) {
469
            return self::$cache_accessible_sites[$cacheKey];
470
        }
471
472
        /** @skipUpgrade */
473
        $subsites = DataList::create(Subsite::class)
474
            ->where("\"Subsite\".\"Title\" != ''")
475
            ->leftJoin('Group_Subsites', '"Group_Subsites"."SubsiteID" = "Subsite"."ID"')
476
            ->innerJoin(
477
                'Group',
478
                '"Group"."ID" = "Group_Subsites"."GroupID" OR "Group"."AccessAllSubsites" = 1'
479
            )
480
            ->innerJoin(
481
                'Group_Members',
482
                "\"Group_Members\".\"GroupID\"=\"Group\".\"ID\" AND \"Group_Members\".\"MemberID\" = $member->ID"
483
            )
484
            ->innerJoin(
485
                'Permission',
486
                "\"Group\".\"ID\"=\"Permission\".\"GroupID\" 
487
                AND \"Permission\".\"Code\" 
488
                IN ($SQL_codes, 'CMS_ACCESS_LeftAndMain', 'ADMIN')"
489
            );
490
491
        if (!$subsites) {
492
            $subsites = new ArrayList();
493
        }
494
495
        /** @var DataList $rolesSubsites */
496
        /** @skipUpgrade */
497
        $rolesSubsites = DataList::create(Subsite::class)
498
            ->where("\"Subsite\".\"Title\" != ''")
499
            ->leftJoin('Group_Subsites', '"Group_Subsites"."SubsiteID" = "Subsite"."ID"')
500
            ->innerJoin(
501
                'Group',
502
                '"Group"."ID" = "Group_Subsites"."GroupID" OR "Group"."AccessAllSubsites" = 1'
503
            )
504
            ->innerJoin(
505
                'Group_Members',
506
                "\"Group_Members\".\"GroupID\"=\"Group\".\"ID\" AND \"Group_Members\".\"MemberID\" = $member->ID"
507
            )
508
            ->innerJoin('Group_Roles', '"Group_Roles"."GroupID"="Group"."ID"')
509
            ->innerJoin('PermissionRole', '"Group_Roles"."PermissionRoleID"="PermissionRole"."ID"')
510
            ->innerJoin(
511
                'PermissionRoleCode',
512
                "\"PermissionRole\".\"ID\"=\"PermissionRoleCode\".\"RoleID\" 
513
                AND \"PermissionRoleCode\".\"Code\" 
514
                IN ($SQL_codes, 'CMS_ACCESS_LeftAndMain', 'ADMIN')"
515
            );
516
517
        if (!$subsites && $rolesSubsites) {
518
            return $rolesSubsites;
519
        }
520
521
        $subsites = new ArrayList($subsites->toArray());
522
523
        if ($rolesSubsites) {
524
            foreach ($rolesSubsites as $subsite) {
525
                if (!$subsites->find('ID', $subsite->ID)) {
526
                    $subsites->push($subsite);
527
                }
528
            }
529
        }
530
531
        if ($includeMainSite) {
532
            if (!is_array($permCode)) {
533
                $permCode = [$permCode];
534
            }
535
            if (self::hasMainSitePermission($member, $permCode)) {
536
                $subsites = $subsites->toArray();
537
538
                $mainSite = new Subsite();
539
                $mainSite->Title = $mainSiteTitle;
540
                array_unshift($subsites, $mainSite);
541
                $subsites = ArrayList::create($subsites);
542
            }
543
        }
544
545
        self::$cache_accessible_sites[$cacheKey] = $subsites;
546
547
        return $subsites;
548
    }
549
550
    /**
551
     * Write a host->domain map to subsites/host-map.php
552
     *
553
     * This is used primarily when using subsites in conjunction with StaticPublisher
554
     *
555
     * @param string $file - filepath of the host map to be written
556
     * @return void
557
     */
558
    public static function writeHostMap($file = null)
559
    {
560
        if (!static::config()->get('write_hostmap')) {
561
            return;
562
        }
563
564
        if (!$file) {
565
            $subsitesPath = ModuleLoader::getModule('silverstripe/subsites')->getRelativePath();
566
            $file = Director::baseFolder() . $subsitesPath . '/host-map.php';
567
        }
568
        $hostmap = [];
569
570
        $subsites = DataObject::get(Subsite::class);
571
572
        if ($subsites) {
0 ignored issues
show
introduced by Werner M. Krauß
$subsites is of type SilverStripe\ORM\DataList, thus it always evaluated to true.
Loading history...
573
            foreach ($subsites as $subsite) {
574
                $domains = $subsite->Domains();
575
                if ($domains) {
576
                    foreach ($domains as $domain) {
577
                        $domainStr = $domain->Domain;
578
                        if (!static::config()->get('strict_subdomain_matching')) {
579
                            $domainStr = preg_replace('/^www\./', '', $domainStr);
580
                        }
581
                        $hostmap[$domainStr] = $subsite->domain();
582
                    }
583
                }
584
                if ($subsite->DefaultSite) {
585
                    $hostmap['default'] = $subsite->domain();
586
                }
587
            }
588
        }
589
590
        $data = "<?php \n";
591
        $data .= "// Generated by Subsite::writeHostMap() on " . date('d/M/y') . "\n";
592
        $data .= '$subsiteHostmap = ' . var_export($hostmap, true) . ';';
593
594
        if (is_writable(dirname($file)) || is_writable($file)) {
595
            file_put_contents($file, $data);
596
        }
597
    }
598
599
    /**
600
     * Checks if a member can be granted certain permissions, regardless of the subsite context.
601
     * Similar logic to {@link Permission::checkMember()}, but only returns TRUE
602
     * if the member is part of a group with the "AccessAllSubsites" flag set.
603
     * If more than one permission is passed to the method, at least one of them must
604
     * be granted for if to return TRUE.
605
     *
606
     * @todo Allow permission inheritance through group hierarchy.
607
     *
608
     * @param Member Member to check against. Defaults to currently logged in member
609
     * @param array $permissionCodes
610
     * @return bool
611
     */
612
    public static function hasMainSitePermission($member = null, $permissionCodes = ['ADMIN'])
613
    {
614
        if (!is_array($permissionCodes)) {
0 ignored issues
show
introduced by Werner M. Krauß
The condition is_array($permissionCodes) is always true.
Loading history...
615
            user_error('Permissions must be passed to Subsite::hasMainSitePermission as an array', E_USER_ERROR);
616
        }
617
618
        if (!$member && $member !== false) {
619
            $member = Security::getCurrentUser();
620
        }
621
622
        if (!$member) {
623
            return false;
624
        }
625
626
        if (!in_array('ADMIN', $permissionCodes)) {
627
            $permissionCodes[] = 'ADMIN';
628
        }
629
630
        $SQLa_perm = Convert::raw2sql($permissionCodes);
631
        $SQL_perms = join("','", $SQLa_perm);
632
        $memberID = (int)$member->ID;
633
634
        // Count this user's groups which can access the main site
635
        $groupCount = DB::query("
636
            SELECT COUNT(\"Permission\".\"ID\")
637
            FROM \"Permission\"
638
            INNER JOIN \"Group\"
639
            ON \"Group\".\"ID\" = \"Permission\".\"GroupID\" AND \"Group\".\"AccessAllSubsites\" = 1
640
            INNER JOIN \"Group_Members\"
641
            ON \"Group_Members\".\"GroupID\" = \"Permission\".\"GroupID\"
642
            WHERE \"Permission\".\"Code\"
643
            IN ('$SQL_perms') AND \"Group_Members\".\"MemberID\" = {$memberID}
644
        ")->value();
645
646
        // Count this user's groups which have a role that can access the main site
647
        $roleCount = DB::query("
648
            SELECT COUNT(\"PermissionRoleCode\".\"ID\")
649
            FROM \"Group\"
650
            INNER JOIN \"Group_Members\" ON \"Group_Members\".\"GroupID\" = \"Group\".\"ID\"
651
            INNER JOIN \"Group_Roles\" ON \"Group_Roles\".\"GroupID\"=\"Group\".\"ID\"
652
            INNER JOIN \"PermissionRole\" ON \"Group_Roles\".\"PermissionRoleID\"=\"PermissionRole\".\"ID\"
653
            INNER JOIN \"PermissionRoleCode\" ON \"PermissionRole\".\"ID\"=\"PermissionRoleCode\".\"RoleID\"
654
            WHERE \"PermissionRoleCode\".\"Code\" IN ('$SQL_perms')
655
            AND \"Group\".\"AccessAllSubsites\" = 1
656
            AND \"Group_Members\".\"MemberID\" = {$memberID}
657
        ")->value();
658
659
        // There has to be at least one that allows access.
660
        return ($groupCount + $roleCount > 0);
661
    }
662
663
    /**
664
     * @todo Possible security issue, don't grant edit permissions to everybody.
665
     * @param bool $member
666
     * @return bool
667
     */
668
    public function canEdit($member = false)
669
    {
670
        $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

670
        $extended = $this->extendedCan(__FUNCTION__, /** @scrutinizer ignore-type */ $member);
Loading history...
671
        if ($extended !== null) {
672
            return $extended;
673
        }
674
675
        return true;
676
    }
677
678
    /**
679
     * Show the configuration fields for each subsite
680
     *
681
     * @return FieldList
682
     */
683
    public function getCMSFields()
684
    {
685
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
686
            if ($this->exists()) {
687
                // Add a GridField for domains to a new tab if the subsite has already been created
688
                $fields->addFieldsToTab('Root.Domains', [
689
                    GridField::create(
690
                        'Domains',
691
                        '',
692
                        $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

692
                        $this->/** @scrutinizer ignore-call */ 
693
                               Domains(),
Loading history...
693
                        GridFieldConfig_RecordEditor::create(10)
694
                    )
695
                ]);
696
            }
697
698
            // Remove the default scaffolded blacklist field, we replace it with a checkbox set field
699
            // in a wrapper further down. The RedirectURL field is currently not in use.
700
            $fields->removeByName(['PageTypeBlacklist', 'RedirectURL']);
701
702
            $fields->addFieldToTab('Root.Main', DropdownField::create(
703
                'Language',
704
                $this->fieldLabel('Language'),
705
                Injector::inst()->get(IntlLocales::class)->getLocales()
706
            ), 'DefaultSite');
707
708
            $fields->addFieldsToTab('Root.Main', [
709
                ToggleCompositeField::create(
710
                    'PageTypeBlacklistToggle',
711
                    _t(__CLASS__ . '.PageTypeBlacklistField', 'Disallow page types?'),
712
                    [
713
                        CheckboxSetField::create('PageTypeBlacklist', '', $this->getPageTypeMap())
714
                    ]
715
                )->setHeadingLevel(4),
716
                HiddenField::create('ID', '', $this->ID),
717
                HiddenField::create('IsSubsite', '', 1)
718
            ]);
719
720
            // If there are any themes available, add the dropdown
721
            $themes = $this->allowedThemes();
722
            if (!empty($themes)) {
723
                $fields->addFieldToTab(
724
                    'Root.Main',
725
                    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...
726
                        ->setEmptyString(_t(__CLASS__ . '.ThemeFieldEmptyString', '-')),
727
                    'PageTypeBlacklistToggle'
728
                );
729
            }
730
731
            // Targetted by the XHR PJAX JavaScript to reload the subsite list in the CMS
732
            $fields->fieldByName('Root.Main')->addExtraClass('subsite-model');
733
734
            // We don't need the Groups many many tab
735
            $fields->removeByName('Groups');
736
737
            // Rename the main tab to configuration
738
            $fields->fieldByName('Root.Main')->setTitle(_t(__CLASS__ . '.ConfigurationTab', 'Configuration'));
739
        });
740
741
        return parent::getCMSFields();
742
    }
743
744
    /**
745
     * Return a list of the different page types available to the CMS
746
     *
747
     * @return array
748
     */
749
    public function getPageTypeMap()
750
    {
751
        $pageTypeMap = [];
752
753
        $pageTypes = SiteTree::page_type_classes();
754
        foreach ($pageTypes as $pageType) {
755
            $pageTypeMap[$pageType] = singleton($pageType)->i18n_singular_name();
756
        }
757
758
        asort($pageTypeMap);
759
760
        return $pageTypeMap;
761
    }
762
763
    /**
764
     *
765
     * @param boolean $includerelations
766
     * @return array
767
     */
768
    public function fieldLabels($includerelations = true)
769
    {
770
        $labels = parent::fieldLabels($includerelations);
771
        $labels['Title'] = _t('Subsites.TitleFieldLabel', 'Subsite Name');
772
        $labels['RedirectURL'] = _t('Subsites.RedirectURLFieldLabel', 'Redirect URL');
773
        $labels['DefaultSite'] = _t('Subsites.DefaultSiteFieldLabel', 'Default site');
774
        $labels['Theme'] = _t('Subsites.ThemeFieldLabel', 'Theme');
775
        $labels['Language'] = _t('Subsites.LanguageFieldLabel', 'Language');
776
        $labels['IsPublic.Nice'] = _t('Subsites.IsPublicFieldLabel', 'Enable public access');
777
        $labels['PageTypeBlacklist'] = _t('Subsites.PageTypeBlacklistFieldLabel', 'Page Type Blacklist');
778
        $labels['Domains.Domain'] = _t('Subsites.DomainFieldLabel', 'Domain');
779
        $labels['PrimaryDomain'] = _t('Subsites.PrimaryDomainFieldLabel', 'Primary Domain');
780
781
        return $labels;
782
    }
783
784
    /**
785
     * Return the themes that can be used with this subsite, as an array of themecode => description
786
     *
787
     * @return array
788
     */
789
    public function allowedThemes()
790
    {
791
        if (($themes = self::$allowed_themes) || ($themes = ThemeResolver::singleton()->getCustomThemeOptions())) {
792
            return ArrayLib::valuekey($themes);
793
        }
794
795
        $themes = [];
796
        if (is_dir(THEMES_PATH)) {
797
            foreach (scandir(THEMES_PATH) as $theme) {
798
                if ($theme[0] == '.') {
799
                    continue;
800
                }
801
                $theme = strtok($theme, '_');
802
                $themes[$theme] = $theme;
803
            }
804
            ksort($themes);
805
        }
806
        return $themes;
807
    }
808
809
    /**
810
     * @return string Current locale of the subsite
811
     */
812
    public function getLanguage()
813
    {
814
        if ($this->getField('Language')) {
815
            return $this->getField('Language');
816
        }
817
818
        return i18n::get_locale();
819
    }
820
821
    /**
822
     *
823
     * @return \SilverStripe\ORM\ValidationResult
824
     */
825
    public function validate()
826
    {
827
        $result = parent::validate();
828
        if (!$this->Title) {
829
            $result->addError(_t(__CLASS__ . '.ValidateTitle', 'Please add a "Title"'));
830
        }
831
        return $result;
832
    }
833
834
    /**
835
     * Whenever a Subsite is written, rewrite the hostmap and create some default pages
836
     *
837
     * @return void
838
     */
839
    public function onAfterWrite()
840
    {
841
        Subsite::writeHostMap();
842
        if ($this->isChanged('ID')) {
843
            $this->createDefaultPages();
844
        }
845
        parent::onAfterWrite();
846
    }
847
848
    /**
849
     * Automatically create default pages for new subsites
850
     */
851
    protected function createDefaultPages()
852
    {
853
        SubsiteState::singleton()->withState(function (SubsiteState $newState) {
854
            $newState->setSubsiteId($this->ID);
855
856
            // Silence DB schema output
857
            DB::quiet();
858
            $siteTree = new SiteTree();
859
            $siteTree->requireDefaultRecords();
860
        });
861
    }
862
863
    /**
864
     * Return the primary domain of this site. Tries to "normalize" the domain name,
865
     * by replacing potential wildcards.
866
     *
867
     * @return string The full domain name of this subsite (without protocol prefix)
868
     */
869
    public function domain()
870
    {
871
        // Get best SubsiteDomain object
872
        $domainObject = $this->getPrimarySubsiteDomain();
873
        if ($domainObject) {
0 ignored issues
show
introduced by Werner M. Krauß
$domainObject is of type SilverStripe\Subsites\Model\SubsiteDomain, thus it always evaluated to true.
Loading history...
874
            return $domainObject->SubstitutedDomain;
875
        }
876
877
        // If there are no objects, default to the current hostname
878
        return Director::host();
879
    }
880
881
    /**
882
     * Finds the primary {@see SubsiteDomain} object for this subsite
883
     *
884
     * @return SubsiteDomain
885
     */
886
    public function getPrimarySubsiteDomain()
887
    {
888
        return $this
889
            ->Domains()
890
            ->sort('"IsPrimary" DESC')
891
            ->first();
892
    }
893
894
    /**
895
     *
896
     * @return string - The full domain name of this subsite (without protocol prefix)
897
     */
898
    public function getPrimaryDomain()
899
    {
900
        return $this->domain();
901
    }
902
903
    /**
904
     * Get the absolute URL for this subsite
905
     * @return string
906
     */
907
    public function absoluteBaseURL()
908
    {
909
        // Get best SubsiteDomain object
910
        $domainObject = $this->getPrimarySubsiteDomain();
911
        if ($domainObject) {
0 ignored issues
show
introduced by Werner M. Krauß
$domainObject is of type SilverStripe\Subsites\Model\SubsiteDomain, thus it always evaluated to true.
Loading history...
912
            return $domainObject->absoluteBaseURL();
913
        }
914
915
        // Fall back to the current base url
916
        return Director::absoluteBaseURL();
917
    }
918
919
    /**
920
     * Javascript admin action to duplicate this subsite
921
     *
922
     * @return string - javascript
923
     */
924
    public function adminDuplicate()
925
    {
926
        $newItem = $this->duplicate();
927
        $message = _t(
928
            __CLASS__ . '.CopyMessage',
929
            'Created a copy of {title}',
930
            ['title' => Convert::raw2js($this->Title)]
931
        );
932
933
        return <<<JS
934
            statusMessage($message, 'good');
935
            $('Form_EditForm').loadURLFromServer('admin/subsites/show/$newItem->ID');
936
JS;
937
    }
938
939
    /**
940
     * Make this subsite the current one
941
     */
942
    public function activate()
943
    {
944
        Subsite::changeSubsite($this);
945
    }
946
947
    /**
948
     *
949
     * @param array $permissionCodes
950
     * @return DataList
951
     */
952
    public function getMembersByPermission($permissionCodes = ['ADMIN'])
953
    {
954
        if (!is_array($permissionCodes)) {
0 ignored issues
show
introduced by Werner M. Krauß
The condition is_array($permissionCodes) is always true.
Loading history...
955
            user_error('Permissions must be passed to Subsite::getMembersByPermission as an array', E_USER_ERROR);
956
        }
957
        $SQL_permissionCodes = Convert::raw2sql($permissionCodes);
958
959
        $SQL_permissionCodes = join("','", $SQL_permissionCodes);
960
961
        return DataObject::get(
962
            Member::class,
963
            "\"Group\".\"SubsiteID\" = $this->ID AND \"Permission\".\"Code\" IN ('$SQL_permissionCodes')",
964
            '',
965
            'LEFT JOIN "Group_Members" ON "Member"."ID" = "Group_Members"."MemberID"
966
            LEFT JOIN "Group" ON "Group"."ID" = "Group_Members"."GroupID"
967
            LEFT JOIN "Permission" ON "Permission"."GroupID" = "Group"."ID"'
968
        );
969
    }
970
971
    /**
972
     * Duplicate this subsite
973
     * @param bool $doWrite
974
     * @param string $manyMany
975
     * @return DataObject
976
     */
977
    public function duplicate($doWrite = true, $manyMany = 'many_many')
978
    {
979
        $duplicate = parent::duplicate($doWrite);
980
981
        $oldSubsiteID = SubsiteState::singleton()->getSubsiteId();
982
        self::changeSubsite($this->ID);
983
984
        /*
985
         * Copy data from this object to the given subsite. Does this using an iterative depth-first search.
986
         * This will make sure that the new parents on the new subsite are correct, and there are no funny
987
         * issues with having to check whether or not the new parents have been added to the site tree
988
         * when a page, etc, is duplicated
989
         */
990
        $stack = [[0, 0]];
991
        while (count($stack) > 0) {
992
            list($sourceParentID, $destParentID) = array_pop($stack);
993
            $children = Versioned::get_by_stage('Page', 'Live', "\"ParentID\" = $sourceParentID", '');
994
995
            if ($children) {
996
                foreach ($children as $child) {
997
                    self::changeSubsite($duplicate->ID); //Change to destination subsite
998
999
                    $childClone = $child->duplicateToSubsite($duplicate, false);
1000
                    $childClone->ParentID = $destParentID;
1001
                    $childClone->writeToStage('Stage');
1002
                    $childClone->copyVersionToStage('Stage', 'Live');
1003
1004
                    self::changeSubsite($this->ID); //Change Back to this subsite
1005
1006
                    array_push($stack, [$child->ID, $childClone->ID]);
1007
                }
1008
            }
1009
        }
1010
1011
        self::changeSubsite($oldSubsiteID);
1012
1013
        return $duplicate;
1014
    }
1015
}
1016