Passed
Push — master ( 7115d0...94e88b )
by Robbie
05:00
created

Subsite::domain()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 10
rs 9.4285
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\State\SubsiteState;
32
use SilverStripe\Versioned\Versioned;
33
use UnexpectedValueException;
34
35
/**
36
 * A dynamically created subsite. SiteTree objects can now belong to a subsite.
37
 * You can simulate subsite access without setting up virtual hosts by appending ?SubsiteID=<ID> to the request.
38
 *
39
 * @package subsites
40
 */
41
class Subsite extends DataObject
42
{
43
44
    private static $table_name = 'Subsite';
0 ignored issues
show
introduced by
The private property $table_name is not used, and could be removed.
Loading history...
45
46
    /**
47
     * @var boolean $disable_subsite_filter If enabled, bypasses the query decoration
48
     * to limit DataObject::get*() calls to a specific subsite. Useful for debugging. Note that
49
     * for now this is left as a public static property to avoid having to nest and mutate the
50
     * configuration manifest.
51
     */
52
    public static $disable_subsite_filter = false;
53
54
    /**
55
     * Allows you to force a specific subsite ID, or comma separated list of IDs.
56
     * Only works for reading. An object cannot be written to more than 1 subsite.
57
     *
58
     * @deprecated 2.0.0..3.0.0 Use SubsiteState::singleton()->withState() instead.
59
     */
60
    public static $force_subsite = null;
61
62
    /**
63
     * Whether to write a host-map.php file
64
     *
65
     * @config
66
     * @var boolean
67
     */
68
    private static $write_hostmap = true;
0 ignored issues
show
introduced by
The private property $write_hostmap is not used, and could be removed.
Loading history...
69
70
    /**
71
     * Memory cache of accessible sites
72
     *
73
     * @array
74
     */
75
    protected static $cache_accessible_sites = [];
76
77
    /**
78
     * Memory cache of subsite id for domains
79
     *
80
     * @var array
81
     */
82
    protected static $cache_subsite_for_domain = [];
83
84
    /**
85
     * Numeric array of all themes which are allowed to be selected for all subsites.
86
     * Corresponds to subfolder names within the /themes folder. By default, all themes contained in this folder
87
     * are listed.
88
     *
89
     * @var array
90
     */
91
    protected static $allowed_themes = [];
92
93
    /**
94
     * If set to TRUE, don't assume 'www.example.com' and 'example.com' are the same.
95
     * Doesn't affect wildcard matching, so '*.example.com' will match 'www.example.com' (but not 'example.com')
96
     * in both TRUE or FALSE setting.
97
     *
98
     * @config
99
     * @var boolean
100
     */
101
    private static $strict_subdomain_matching = false;
0 ignored issues
show
introduced by
The private property $strict_subdomain_matching is not used, and could be removed.
Loading history...
102
103
    /**
104
     * Respects the IsPublic flag when retrieving subsites
105
     *
106
     * @config
107
     * @var boolean
108
     */
109
    private static $check_is_public = true;
0 ignored issues
show
introduced by
The private property $check_is_public is not used, and could be removed.
Loading history...
110
111
    /*** @return array
112
     */
113
    private static $summary_fields = [
0 ignored issues
show
introduced by
The private property $summary_fields is not used, and could be removed.
Loading history...
114
        'Title',
115
        'PrimaryDomain',
116
        'IsPublic'
117
    ];
118
119
    /**
120
     * @var array
121
     */
122
    private static $db = [
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
123
        'Title' => 'Varchar(255)',
124
        'RedirectURL' => 'Varchar(255)',
125
        'DefaultSite' => 'Boolean',
126
        'Theme' => 'Varchar',
127
        'Language' => 'Varchar(6)',
128
129
        // Used to hide unfinished/private subsites from public view.
130
        // If unset, will default to true
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
131
        'IsPublic' => 'Boolean',
132
133
        // Comma-separated list of disallowed page types
134
        'PageTypeBlacklist' => 'Text',
135
    ];
136
137
    /**
138
     * @var array
139
     */
140
    private static $has_many = [
0 ignored issues
show
introduced by
The private property $has_many is not used, and could be removed.
Loading history...
141
        'Domains' => SubsiteDomain::class,
142
    ];
143
144
    /**
145
     * @var array
146
     */
147
    private static $belongs_many_many = [
0 ignored issues
show
introduced by
The private property $belongs_many_many is not used, and could be removed.
Loading history...
148
        'Groups' => Group::class,
149
    ];
150
151
    /**
152
     * @var array
153
     */
154
    private static $defaults = [
0 ignored issues
show
introduced by
The private property $defaults is not used, and could be removed.
Loading history...
155
        'IsPublic' => 1
156
    ];
157
158
    /**
159
     * @var array
160
     */
161
    private static $searchable_fields = [
0 ignored issues
show
introduced by
The private property $searchable_fields is not used, and could be removed.
Loading history...
162
        'Title',
163
        'Domains.Domain',
164
        'IsPublic',
165
    ];
166
167
    /**
168
     * @var string
169
     */
170
    private static $default_sort = '"Title" ASC';
0 ignored issues
show
introduced by
The private property $default_sort is not used, and could be removed.
Loading history...
171
172
    /**
173
     * Set allowed themes
174
     *
175
     * @param array $themes - Numeric array of all themes which are allowed to be selected for all subsites.
176
     */
177
    public static function set_allowed_themes($themes)
178
    {
179
        self::$allowed_themes = $themes;
180
    }
181
182
    /**
183
     * Gets the subsite currently set in the session.
184
     *
185
     * @uses ControllerSubsites->controllerAugmentInit()
186
     * @return DataObject The current Subsite
187
     */
188
    public static function currentSubsite()
189
    {
190
        return Subsite::get()->byID(SubsiteState::singleton()->getSubsiteId());
191
    }
192
193
    /**
194
     * This function gets the current subsite ID from the session. It used in the backend so Ajax requests
195
     * use the correct subsite. The frontend handles subsites differently. It calls getSubsiteIDForDomain
196
     * directly from ModelAsController::getNestedController. Only gets Subsite instances which have their
197
     * {@link IsPublic} flag set to TRUE.
198
     *
199
     * You can simulate subsite access without creating virtual hosts by appending ?SubsiteID=<ID> to the request.
200
     *
201
     * @return int ID of the current subsite instance
202
     *
203
     * @deprecated 2.0..3.0 Use SubsiteState::singleton()->getSubsiteId() instead
204
     */
205
    public static function currentSubsiteID()
206
    {
207
        Deprecation::notice('3.0', 'Use SubsiteState::singleton()->getSubsiteId() instead');
208
        return SubsiteState::singleton()->getSubsiteId();
209
    }
210
211
    /**
212
     * Switch to another subsite through storing the subsite identifier in the current PHP session.
213
     * Only takes effect when {@link SubsiteState::singleton()->getUseSessions()} is set to TRUE.
214
     *
215
     * @param int|Subsite $subsite Either the ID of the subsite, or the subsite object itself
216
     */
217
    public static function changeSubsite($subsite)
218
    {
219
        // Session subsite change only meaningful if the session is active.
220
        // Otherwise we risk setting it to wrong value, e.g. if we rely on currentSubsiteID.
221
        if (!SubsiteState::singleton()->getUseSessions()) {
222
            return;
223
        }
224
225
        if (is_object($subsite)) {
226
            $subsiteID = $subsite->ID;
227
        } else {
228
            $subsiteID = $subsite;
229
        }
230
231
        SubsiteState::singleton()->setSubsiteId($subsiteID);
232
233
        // Set locale
234
        if (is_object($subsite) && $subsite->Language !== '') {
0 ignored issues
show
Bug Best Practice introduced by
The property Language does not exist on SilverStripe\Subsites\Model\Subsite. Since you implemented __get, consider adding a @property annotation.
Loading history...
235
            $locale = (new IntlLocales())->localeFromLang($subsite->Language);
236
            if ($locale) {
237
                i18n::set_locale($locale);
238
            }
239
        }
240
241
        Permission::reset();
242
    }
243
244
    /**
245
     * Get a matching subsite for the given host, or for the current HTTP_HOST.
246
     * Supports "fuzzy" matching of domains by placing an asterisk at the start of end of the string,
247
     * for example matching all subdomains on *.example.com with one subsite,
248
     * and all subdomains on *.example.org on another.
249
     *
250
     * @param $host string The host to find the subsite for.  If not specified, $_SERVER['HTTP_HOST'] is used.
251
     * @param bool $checkPermissions
252
     * @return int Subsite ID
253
     */
254
    public static function getSubsiteIDForDomain($host = null, $checkPermissions = true)
255
    {
256
        if ($host == null && isset($_SERVER['HTTP_HOST'])) {
257
            $host = $_SERVER['HTTP_HOST'];
258
        }
259
260
        $matchingDomains = null;
261
        $cacheKey = null;
262
        if ($host) {
263
            if (!static::config()->get('strict_subdomain_matching')) {
264
                $host = preg_replace('/^www\./', '', $host);
265
            }
266
267
            $currentUserId = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0;
268
            $cacheKey = implode('_', [$host, $currentUserId, static::config()->get('check_is_public')]);
269
            if (isset(self::$cache_subsite_for_domain[$cacheKey])) {
270
                return self::$cache_subsite_for_domain[$cacheKey];
271
            }
272
273
            $SQL_host = Convert::raw2sql($host);
274
275
            $schema = DataObject::getSchema();
276
277
            /** @skipUpgrade */
278
            $domainTableName = $schema->tableName(SubsiteDomain::class);
279
            if (!in_array($domainTableName, DB::table_list())) {
280
                // Table hasn't been created yet. Might be a dev/build, skip.
281
                return 0;
282
            }
283
284
            $subsiteTableName = $schema->tableName(__CLASS__);
285
            /** @skipUpgrade */
286
            $matchingDomains = DataObject::get(
287
                SubsiteDomain::class,
288
                "'$SQL_host' LIKE replace(\"{$domainTableName}\".\"Domain\",'*','%')",
289
                '"IsPrimary" DESC'
290
            )->innerJoin(
291
                $subsiteTableName,
292
                '"' . $subsiteTableName . '"."ID" = "SubsiteDomain"."SubsiteID" AND "'
293
                    . $subsiteTableName . '"."IsPublic"=1'
294
            );
295
        }
296
297
        if ($matchingDomains && $matchingDomains->count()) {
298
            $subsiteIDs = array_unique($matchingDomains->column('SubsiteID'));
299
            $subsiteDomains = array_unique($matchingDomains->column('Domain'));
300
            if (sizeof($subsiteIDs) > 1) {
301
                throw new UnexpectedValueException(sprintf(
302
                    "Multiple subsites match on '%s': %s",
303
                    $host,
304
                    implode(',', $subsiteDomains)
305
                ));
306
            }
307
308
            $subsiteID = $subsiteIDs[0];
309
        } else {
310
            if ($default = DataObject::get_one(Subsite::class, '"DefaultSite" = 1')) {
311
                // Check for a 'default' subsite
312
                $subsiteID = $default->ID;
313
            } else {
314
                // Default subsite id = 0, the main site
315
                $subsiteID = 0;
316
            }
317
        }
318
319
        if ($cacheKey) {
320
            self::$cache_subsite_for_domain[$cacheKey] = $subsiteID;
321
        }
322
323
        return $subsiteID;
324
    }
325
326
    /**
327
     *
328
     * @param string $className
329
     * @param string $filter
330
     * @param string $sort
331
     * @param string $join
332
     * @param string $limit
333
     * @return DataList
334
     */
335
    public static function get_from_all_subsites($className, $filter = '', $sort = '', $join = '', $limit = '')
336
    {
337
        $result = DataObject::get($className, $filter, $sort, $join, $limit);
338
        $result = $result->setDataQueryParam('Subsite.filter', false);
339
        return $result;
340
    }
341
342
    /**
343
     * Disable the sub-site filtering; queries will select from all subsites
344
     * @param bool $disabled
345
     */
346
    public static function disable_subsite_filter($disabled = true)
347
    {
348
        self::$disable_subsite_filter = $disabled;
349
    }
350
351
    /**
352
     * Flush caches on database reset
353
     */
354
    public static function on_db_reset()
355
    {
356
        self::$cache_accessible_sites = [];
357
        self::$cache_subsite_for_domain = [];
358
    }
359
360
    /**
361
     * Return all subsites, regardless of permissions (augmented with main site).
362
     *
363
     * @param bool $includeMainSite
364
     * @param string $mainSiteTitle
365
     * @return SS_List List of <a href='psi_element://Subsite'>Subsite</a> objects (DataList or ArrayList).
366
     * objects (DataList or ArrayList).
367
     */
368
    public static function all_sites($includeMainSite = true, $mainSiteTitle = 'Main site')
369
    {
370
        $subsites = Subsite::get();
371
372
        if ($includeMainSite) {
373
            $subsites = $subsites->toArray();
374
375
            $mainSite = new Subsite();
376
            $mainSite->Title = $mainSiteTitle;
377
            array_unshift($subsites, $mainSite);
378
379
            $subsites = ArrayList::create($subsites);
380
        }
381
382
        return $subsites;
383
    }
384
385
    /*
386
     * Returns an ArrayList of the subsites accessible to the current user.
387
     * It's enough for any section to be accessible for the site to be included.
388
     *
389
     * @return ArrayList of {@link Subsite} instances.
390
     */
391
    public static function all_accessible_sites($includeMainSite = true, $mainSiteTitle = 'Main site', $member = null)
392
    {
393
        // Rationalise member arguments
394
        if (!$member) {
395
            $member = Security::getCurrentUser();
396
        }
397
        if (!$member) {
398
            return ArrayList::create();
399
        }
400
        if (!is_object($member)) {
401
            $member = DataObject::get_by_id(Member::class, $member);
402
        }
403
404
        $subsites = ArrayList::create();
405
406
        // Collect subsites for all sections.
407
        $menu = CMSMenu::get_viewable_menu_items();
408
        foreach ($menu as $candidate) {
409
            if ($candidate->controller) {
410
                $accessibleSites = singleton($candidate->controller)->sectionSites(
411
                    $includeMainSite,
412
                    $mainSiteTitle,
413
                    $member
414
                );
415
416
                // Replace existing keys so no one site appears twice.
417
                $subsites->merge($accessibleSites);
418
            }
419
        }
420
421
        $subsites->removeDuplicates();
422
423
        return $subsites;
424
    }
425
426
    /**
427
     * Return the subsites that the current user can access by given permission.
428
     * Sites will only be included if they have a Title.
429
     *
430
     * @param $permCode array|string Either a single permission code or an array of permission codes.
431
     * @param $includeMainSite bool If true, the main site will be included if appropriate.
432
     * @param $mainSiteTitle string The label to give to the main site
433
     * @param $member int|Member The member attempting to access the sites
434
     * @return DataList|ArrayList of {@link Subsite} instances
435
     */
436
    public static function accessible_sites(
437
        $permCode,
438
        $includeMainSite = true,
439
        $mainSiteTitle = 'Main site',
440
        $member = null
441
    ) {
442
443
        // Rationalise member arguments
444
        if (!$member) {
445
            $member = Member::currentUser();
0 ignored issues
show
Deprecated Code introduced by
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

445
            $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...
446
        }
447
        if (!$member) {
448
            return new ArrayList();
449
        }
450
        if (!is_object($member)) {
451
            $member = DataObject::get_by_id(Member::class, $member);
452
        }
453
454
        // Rationalise permCode argument
455
        if (is_array($permCode)) {
456
            $SQL_codes = "'" . implode("', '", Convert::raw2sql($permCode)) . "'";
457
        } else {
458
            $SQL_codes = "'" . Convert::raw2sql($permCode) . "'";
0 ignored issues
show
Bug introduced by
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

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

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

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