Completed
Push — master ( cde41c...73943a )
by Daniel
21s
created

src/Model/Subsite.php (1 issue)

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\Core\Manifest\ModuleLoader;
12
use SilverStripe\Dev\Deprecation;
13
use SilverStripe\Forms\CheckboxSetField;
14
use SilverStripe\Forms\DropdownField;
15
use SilverStripe\Forms\FieldList;
16
use SilverStripe\Forms\GridField\GridField;
17
use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor;
18
use SilverStripe\Forms\HiddenField;
19
use SilverStripe\Forms\Tab;
20
use SilverStripe\Forms\ToggleCompositeField;
21
use SilverStripe\ORM\ArrayLib;
22
use SilverStripe\ORM\ArrayList;
23
use SilverStripe\ORM\DB;
24
use SilverStripe\ORM\DataList;
25
use SilverStripe\ORM\DataObject;
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 SilverStripe\i18n\Data\Intl\IntlLocales;
34
use SilverStripe\i18n\i18n;
35
use UnexpectedValueException;
36
37
/**
38
 * A dynamically created subsite. SiteTree objects can now belong to a subsite.
39
 * You can simulate subsite access without setting up virtual hosts by appending ?SubsiteID=<ID> to the request.
40
 *
41
 * @package subsites
42
 */
43
class Subsite extends DataObject
44
{
45
46
    private static $table_name = 'Subsite';
47
48
    /**
49
     * @var boolean $disable_subsite_filter If enabled, bypasses the query decoration
50
     * to limit DataObject::get*() calls to a specific subsite. Useful for debugging. Note that
51
     * for now this is left as a public static property to avoid having to nest and mutate the
52
     * configuration manifest.
53
     */
54
    public static $disable_subsite_filter = false;
55
56
    /**
57
     * Allows you to force a specific subsite ID, or comma separated list of IDs.
58
     * Only works for reading. An object cannot be written to more than 1 subsite.
59
     *
60
     * @deprecated 2.0.0..3.0.0 Use SubsiteState::singleton()->withState() instead.
61
     */
62
    public static $force_subsite = null;
63
64
    /**
65
     * Whether to write a host-map.php file
66
     *
67
     * @config
68
     * @var boolean
69
     */
70
    private static $write_hostmap = true;
71
72
    /**
73
     * Memory cache of accessible sites
74
     *
75
     * @array
76
     */
77
    protected static $cache_accessible_sites = [];
78
79
    /**
80
     * Memory cache of subsite id for domains
81
     *
82
     * @var array
83
     */
84
    protected static $cache_subsite_for_domain = [];
85
86
    /**
87
     * Numeric array of all themes which are allowed to be selected for all subsites.
88
     * Corresponds to subfolder names within the /themes folder. By default, all themes contained in this folder
89
     * are listed.
90
     *
91
     * @var array
92
     */
93
    protected static $allowed_themes = [];
94
95
    /**
96
     * If set to TRUE, don't assume 'www.example.com' and 'example.com' are the same.
97
     * Doesn't affect wildcard matching, so '*.example.com' will match 'www.example.com' (but not 'example.com')
98
     * in both TRUE or FALSE setting.
99
     *
100
     * @config
101
     * @var boolean
102
     */
103
    private static $strict_subdomain_matching = false;
104
105
    /**
106
     * Respects the IsPublic flag when retrieving subsites
107
     *
108
     * @config
109
     * @var boolean
110
     */
111
    private static $check_is_public = true;
112
113
    /*** @return array
114
     */
115
    private static $summary_fields = [
116
        'Title',
117
        'PrimaryDomain',
118
        'IsPublic'
119
    ];
120
121
    /**
122
     * @var array
123
     */
124
    private static $db = [
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
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...
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 = [
143
        'Domains' => SubsiteDomain::class,
144
    ];
145
146
    /**
147
     * @var array
148
     */
149
    private static $belongs_many_many = [
150
        'Groups' => Group::class,
151
    ];
152
153
    /**
154
     * @var array
155
     */
156
    private static $defaults = [
157
        'IsPublic' => 1
158
    ];
159
160
    /**
161
     * @var array
162
     */
163
    private static $searchable_fields = [
164
        'Title',
165
        'Domains.Domain',
166
        'IsPublic',
167
    ];
168
169
    /**
170
     * @var string
171
     */
172
    private static $default_sort = '"Title" ASC';
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
     * @uses ControllerSubsites->controllerAugmentInit()
188
     * @return DataObject The current Subsite
189
     */
190
    public static function currentSubsite()
191
    {
192
        return Subsite::get()->byID(SubsiteState::singleton()->getSubsiteId());
193
    }
194
195
    /**
196
     * This function gets the current subsite ID from the session. It used in the backend so Ajax requests
197
     * use the correct subsite. The frontend handles subsites differently. It calls getSubsiteIDForDomain
198
     * directly from ModelAsController::getNestedController. Only gets Subsite instances which have their
199
     * {@link IsPublic} flag set to TRUE.
200
     *
201
     * You can simulate subsite access without creating virtual hosts by appending ?SubsiteID=<ID> to the request.
202
     *
203
     * @return int ID of the current subsite instance
204
     *
205
     * @deprecated 2.0..3.0 Use SubsiteState::singleton()->getSubsiteId() instead
206
     */
207
    public static function currentSubsiteID()
208
    {
209
        Deprecation::notice('3.0', 'Use SubsiteState::singleton()->getSubsiteId() instead');
210
        return SubsiteState::singleton()->getSubsiteId();
211
    }
212
213
    /**
214
     * Switch to another subsite through storing the subsite identifier in the current PHP session.
215
     * Only takes effect when {@link SubsiteState::singleton()->getUseSessions()} is set to TRUE.
216
     *
217
     * @param int|Subsite $subsite Either the ID of the subsite, or the subsite object itself
218
     */
219
    public static function changeSubsite($subsite)
220
    {
221
        // Session subsite change only meaningful if the session is active.
222
        // Otherwise we risk setting it to wrong value, e.g. if we rely on currentSubsiteID.
223
        if (!SubsiteState::singleton()->getUseSessions()) {
224
            return;
225
        }
226
227
        if (is_object($subsite)) {
228
            $subsiteID = $subsite->ID;
229
        } else {
230
            $subsiteID = $subsite;
231
        }
232
233
        SubsiteState::singleton()->setSubsiteId($subsiteID);
234
235
        // Set locale
236
        if (is_object($subsite) && $subsite->Language !== '') {
237
            $locale = (new IntlLocales())->localeFromLang($subsite->Language);
238
            if ($locale) {
239
                i18n::set_locale($locale);
240
            }
241
        }
242
243
        Permission::reset();
244
    }
245
246
    /**
247
     * Get a matching subsite for the given host, or for the current HTTP_HOST.
248
     * Supports "fuzzy" matching of domains by placing an asterisk at the start of end of the string,
249
     * for example matching all subdomains on *.example.com with one subsite,
250
     * and all subdomains on *.example.org on another.
251
     *
252
     * @param $host string The host to find the subsite for.  If not specified, $_SERVER['HTTP_HOST'] is used.
253
     * @param bool $checkPermissions
254
     * @return int Subsite ID
255
     */
256
    public static function getSubsiteIDForDomain($host = null, $checkPermissions = true)
257
    {
258
        if ($host == null && isset($_SERVER['HTTP_HOST'])) {
259
            $host = $_SERVER['HTTP_HOST'];
260
        }
261
262
        $matchingDomains = null;
263
        $cacheKey = null;
264
        if ($host) {
265
            if (!static::config()->get('strict_subdomain_matching')) {
266
                $host = preg_replace('/^www\./', '', $host);
267
            }
268
269
            $currentUserId = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0;
270
            $cacheKey = implode('_', [$host, $currentUserId, static::config()->get('check_is_public')]);
271
            if (isset(self::$cache_subsite_for_domain[$cacheKey])) {
272
                return self::$cache_subsite_for_domain[$cacheKey];
273
            }
274
275
            $SQL_host = Convert::raw2sql($host);
276
277
            $schema = DataObject::getSchema();
278
279
            /** @skipUpgrade */
280
            $domainTableName = $schema->tableName(SubsiteDomain::class);
281
            if (!in_array($domainTableName, DB::table_list())) {
282
                // Table hasn't been created yet. Might be a dev/build, skip.
283
                return 0;
284
            }
285
286
            $subsiteTableName = $schema->tableName(__CLASS__);
287
            /** @skipUpgrade */
288
            $matchingDomains = DataObject::get(
289
                SubsiteDomain::class,
290
                "'$SQL_host' LIKE replace(\"{$domainTableName}\".\"Domain\",'*','%')",
291
                '"IsPrimary" DESC'
292
            )->innerJoin(
293
                $subsiteTableName,
294
                '"' . $subsiteTableName . '"."ID" = "SubsiteDomain"."SubsiteID" AND "'
295
                    . $subsiteTableName . '"."IsPublic"=1'
296
            );
297
        }
298
299
        if ($matchingDomains && $matchingDomains->count()) {
300
            $subsiteIDs = array_unique($matchingDomains->column('SubsiteID'));
301
            $subsiteDomains = array_unique($matchingDomains->column('Domain'));
302
            if (sizeof($subsiteIDs) > 1) {
303
                throw new UnexpectedValueException(sprintf(
304
                    "Multiple subsites match on '%s': %s",
305
                    $host,
306
                    implode(',', $subsiteDomains)
307
                ));
308
            }
309
310
            $subsiteID = $subsiteIDs[0];
311
        } else {
312
            if ($default = DataObject::get_one(Subsite::class, '"DefaultSite" = 1')) {
313
                // Check for a 'default' subsite
314
                $subsiteID = $default->ID;
315
            } else {
316
                // Default subsite id = 0, the main site
317
                $subsiteID = 0;
318
            }
319
        }
320
321
        if ($cacheKey) {
322
            self::$cache_subsite_for_domain[$cacheKey] = $subsiteID;
323
        }
324
325
        return $subsiteID;
326
    }
327
328
    /**
329
     *
330
     * @param string $className
331
     * @param string $filter
332
     * @param string $sort
333
     * @param string $join
334
     * @param string $limit
335
     * @return DataList
336
     */
337
    public static function get_from_all_subsites($className, $filter = '', $sort = '', $join = '', $limit = '')
338
    {
339
        $result = DataObject::get($className, $filter, $sort, $join, $limit);
340
        $result = $result->setDataQueryParam('Subsite.filter', false);
341
        return $result;
342
    }
343
344
    /**
345
     * Disable the sub-site filtering; queries will select from all subsites
346
     * @param bool $disabled
347
     */
348
    public static function disable_subsite_filter($disabled = true)
349
    {
350
        self::$disable_subsite_filter = $disabled;
351
    }
352
353
    /**
354
     * Flush caches on database reset
355
     */
356
    public static function on_db_reset()
357
    {
358
        self::$cache_accessible_sites = [];
359
        self::$cache_subsite_for_domain = [];
360
    }
361
362
    /**
363
     * Return all subsites, regardless of permissions (augmented with main site).
364
     *
365
     * @param bool $includeMainSite
366
     * @param string $mainSiteTitle
367
     * @return SS_List List of <a href='psi_element://Subsite'>Subsite</a> objects (DataList or ArrayList).
368
     * objects (DataList or ArrayList).
369
     */
370
    public static function all_sites($includeMainSite = true, $mainSiteTitle = 'Main site')
371
    {
372
        $subsites = Subsite::get();
373
374
        if ($includeMainSite) {
375
            $subsites = $subsites->toArray();
376
377
            $mainSite = new Subsite();
378
            $mainSite->Title = $mainSiteTitle;
379
            array_unshift($subsites, $mainSite);
380
381
            $subsites = ArrayList::create($subsites);
382
        }
383
384
        return $subsites;
385
    }
386
387
    /*
388
     * Returns an ArrayList of the subsites accessible to the current user.
389
     * It's enough for any section to be accessible for the site to be included.
390
     *
391
     * @return ArrayList of {@link Subsite} instances.
392
     */
393
    public static function all_accessible_sites($includeMainSite = true, $mainSiteTitle = 'Main site', $member = null)
394
    {
395
        // Rationalise member arguments
396
        if (!$member) {
397
            $member = Security::getCurrentUser();
398
        }
399
        if (!$member) {
400
            return ArrayList::create();
401
        }
402
        if (!is_object($member)) {
403
            $member = DataObject::get_by_id(Member::class, $member);
404
        }
405
406
        $subsites = ArrayList::create();
407
408
        // Collect subsites for all sections.
409
        $menu = CMSMenu::get_viewable_menu_items();
410
        foreach ($menu as $candidate) {
411
            if ($candidate->controller) {
412
                $accessibleSites = singleton($candidate->controller)->sectionSites(
413
                    $includeMainSite,
414
                    $mainSiteTitle,
415
                    $member
416
                );
417
418
                // Replace existing keys so no one site appears twice.
419
                $subsites->merge($accessibleSites);
420
            }
421
        }
422
423
        $subsites->removeDuplicates();
424
425
        return $subsites;
426
    }
427
428
    /**
429
     * Return the subsites that the current user can access by given permission.
430
     * Sites will only be included if they have a Title.
431
     *
432
     * @param $permCode array|string Either a single permission code or an array of permission codes.
433
     * @param $includeMainSite bool If true, the main site will be included if appropriate.
434
     * @param $mainSiteTitle string The label to give to the main site
435
     * @param $member int|Member The member attempting to access the sites
436
     * @return DataList|ArrayList of {@link Subsite} instances
437
     */
438
    public static function accessible_sites(
439
        $permCode,
440
        $includeMainSite = true,
441
        $mainSiteTitle = 'Main site',
442
        $member = null
443
    ) {
444
445
        // Rationalise member arguments
446
        if (!$member) {
447
            $member = Member::currentUser();
448
        }
449
        if (!$member) {
450
            return new ArrayList();
451
        }
452
        if (!is_object($member)) {
453
            $member = DataObject::get_by_id(Member::class, $member);
454
        }
455
456
        // Rationalise permCode argument
457
        if (is_array($permCode)) {
458
            $SQL_codes = "'" . implode("', '", Convert::raw2sql($permCode)) . "'";
459
        } else {
460
            $SQL_codes = "'" . Convert::raw2sql($permCode) . "'";
461
        }
462
463
        // Cache handling
464
        $cacheKey = $SQL_codes . '-' . $member->ID . '-' . $includeMainSite . '-' . $mainSiteTitle;
465
        if (isset(self::$cache_accessible_sites[$cacheKey])) {
466
            return self::$cache_accessible_sites[$cacheKey];
467
        }
468
469
        /** @skipUpgrade */
470
        $subsites = DataList::create(Subsite::class)
471
            ->where("\"Subsite\".\"Title\" != ''")
472
            ->leftJoin('Group_Subsites', '"Group_Subsites"."SubsiteID" = "Subsite"."ID"')
473
            ->innerJoin(
474
                'Group',
475
                '"Group"."ID" = "Group_Subsites"."GroupID" OR "Group"."AccessAllSubsites" = 1'
476
            )
477
            ->innerJoin(
478
                'Group_Members',
479
                "\"Group_Members\".\"GroupID\"=\"Group\".\"ID\" AND \"Group_Members\".\"MemberID\" = $member->ID"
480
            )
481
            ->innerJoin(
482
                'Permission',
483
                "\"Group\".\"ID\"=\"Permission\".\"GroupID\" 
484
                AND \"Permission\".\"Code\" 
485
                IN ($SQL_codes, 'CMS_ACCESS_LeftAndMain', 'ADMIN')"
486
            );
487
488
        if (!$subsites) {
489
            $subsites = new ArrayList();
490
        }
491
492
        /** @var DataList $rolesSubsites */
493
        /** @skipUpgrade */
494
        $rolesSubsites = DataList::create(Subsite::class)
495
            ->where("\"Subsite\".\"Title\" != ''")
496
            ->leftJoin('Group_Subsites', '"Group_Subsites"."SubsiteID" = "Subsite"."ID"')
497
            ->innerJoin(
498
                'Group',
499
                '"Group"."ID" = "Group_Subsites"."GroupID" OR "Group"."AccessAllSubsites" = 1'
500
            )
501
            ->innerJoin(
502
                'Group_Members',
503
                "\"Group_Members\".\"GroupID\"=\"Group\".\"ID\" AND \"Group_Members\".\"MemberID\" = $member->ID"
504
            )
505
            ->innerJoin('Group_Roles', '"Group_Roles"."GroupID"="Group"."ID"')
506
            ->innerJoin('PermissionRole', '"Group_Roles"."PermissionRoleID"="PermissionRole"."ID"')
507
            ->innerJoin(
508
                'PermissionRoleCode',
509
                "\"PermissionRole\".\"ID\"=\"PermissionRoleCode\".\"RoleID\" 
510
                AND \"PermissionRoleCode\".\"Code\" 
511
                IN ($SQL_codes, 'CMS_ACCESS_LeftAndMain', 'ADMIN')"
512
            );
513
514
        if (!$subsites && $rolesSubsites) {
515
            return $rolesSubsites;
516
        }
517
518
        $subsites = new ArrayList($subsites->toArray());
519
520
        if ($rolesSubsites) {
521
            foreach ($rolesSubsites as $subsite) {
522
                if (!$subsites->find('ID', $subsite->ID)) {
523
                    $subsites->push($subsite);
524
                }
525
            }
526
        }
527
528
        if ($includeMainSite) {
529
            if (!is_array($permCode)) {
530
                $permCode = [$permCode];
531
            }
532
            if (self::hasMainSitePermission($member, $permCode)) {
533
                $subsites = $subsites->toArray();
534
535
                $mainSite = new Subsite();
536
                $mainSite->Title = $mainSiteTitle;
537
                array_unshift($subsites, $mainSite);
538
                $subsites = ArrayList::create($subsites);
539
            }
540
        }
541
542
        self::$cache_accessible_sites[$cacheKey] = $subsites;
543
544
        return $subsites;
545
    }
546
547
    /**
548
     * Write a host->domain map to subsites/host-map.php
549
     *
550
     * This is used primarily when using subsites in conjunction with StaticPublisher
551
     *
552
     * @param string $file - filepath of the host map to be written
553
     * @return void
554
     */
555
    public static function writeHostMap($file = null)
556
    {
557
        if (!static::config()->get('write_hostmap')) {
558
            return;
559
        }
560
561
        if (!$file) {
562
            $subsitesPath = ModuleLoader::getModule('silverstripe/subsites')->getRelativePath();
563
            $file = Director::baseFolder() . $subsitesPath . '/host-map.php';
564
        }
565
        $hostmap = [];
566
567
        $subsites = DataObject::get(Subsite::class);
568
569
        if ($subsites) {
570
            foreach ($subsites as $subsite) {
571
                $domains = $subsite->Domains();
572
                if ($domains) {
573
                    foreach ($domains as $domain) {
574
                        $domainStr = $domain->Domain;
575
                        if (!static::config()->get('strict_subdomain_matching')) {
576
                            $domainStr = preg_replace('/^www\./', '', $domainStr);
577
                        }
578
                        $hostmap[$domainStr] = $subsite->domain();
579
                    }
580
                }
581
                if ($subsite->DefaultSite) {
582
                    $hostmap['default'] = $subsite->domain();
583
                }
584
            }
585
        }
586
587
        $data = "<?php \n";
588
        $data .= "// Generated by Subsite::writeHostMap() on " . date('d/M/y') . "\n";
589
        $data .= '$subsiteHostmap = ' . var_export($hostmap, true) . ';';
590
591
        if (is_writable(dirname($file)) || is_writable($file)) {
592
            file_put_contents($file, $data);
593
        }
594
    }
595
596
    /**
597
     * Checks if a member can be granted certain permissions, regardless of the subsite context.
598
     * Similar logic to {@link Permission::checkMember()}, but only returns TRUE
599
     * if the member is part of a group with the "AccessAllSubsites" flag set.
600
     * If more than one permission is passed to the method, at least one of them must
601
     * be granted for if to return TRUE.
602
     *
603
     * @todo Allow permission inheritance through group hierarchy.
604
     *
605
     * @param Member Member to check against. Defaults to currently logged in member
606
     * @param array $permissionCodes
607
     * @return bool
608
     */
609
    public static function hasMainSitePermission($member = null, $permissionCodes = ['ADMIN'])
610
    {
611
        if (!is_array($permissionCodes)) {
612
            user_error('Permissions must be passed to Subsite::hasMainSitePermission as an array', E_USER_ERROR);
613
        }
614
615
        if (!$member && $member !== false) {
616
            $member = Security::getCurrentUser();
617
        }
618
619
        if (!$member) {
620
            return false;
621
        }
622
623
        if (!in_array('ADMIN', $permissionCodes)) {
624
            $permissionCodes[] = 'ADMIN';
625
        }
626
627
        $SQLa_perm = Convert::raw2sql($permissionCodes);
628
        $SQL_perms = join("','", $SQLa_perm);
629
        $memberID = (int)$member->ID;
630
631
        // Count this user's groups which can access the main site
632
        $groupCount = DB::query("
633
            SELECT COUNT(\"Permission\".\"ID\")
634
            FROM \"Permission\"
635
            INNER JOIN \"Group\"
636
            ON \"Group\".\"ID\" = \"Permission\".\"GroupID\" AND \"Group\".\"AccessAllSubsites\" = 1
637
            INNER JOIN \"Group_Members\"
638
            ON \"Group_Members\".\"GroupID\" = \"Permission\".\"GroupID\"
639
            WHERE \"Permission\".\"Code\"
640
            IN ('$SQL_perms') AND \"Group_Members\".\"MemberID\" = {$memberID}
641
        ")->value();
642
643
        // Count this user's groups which have a role that can access the main site
644
        $roleCount = DB::query("
645
            SELECT COUNT(\"PermissionRoleCode\".\"ID\")
646
            FROM \"Group\"
647
            INNER JOIN \"Group_Members\" ON \"Group_Members\".\"GroupID\" = \"Group\".\"ID\"
648
            INNER JOIN \"Group_Roles\" ON \"Group_Roles\".\"GroupID\"=\"Group\".\"ID\"
649
            INNER JOIN \"PermissionRole\" ON \"Group_Roles\".\"PermissionRoleID\"=\"PermissionRole\".\"ID\"
650
            INNER JOIN \"PermissionRoleCode\" ON \"PermissionRole\".\"ID\"=\"PermissionRoleCode\".\"RoleID\"
651
            WHERE \"PermissionRoleCode\".\"Code\" IN ('$SQL_perms')
652
            AND \"Group\".\"AccessAllSubsites\" = 1
653
            AND \"Group_Members\".\"MemberID\" = {$memberID}
654
        ")->value();
655
656
        // There has to be at least one that allows access.
657
        return ($groupCount + $roleCount > 0);
658
    }
659
660
    /**
661
     * @todo Possible security issue, don't grant edit permissions to everybody.
662
     * @param bool $member
663
     * @return bool
664
     */
665
    public function canEdit($member = false)
666
    {
667
        $extended = $this->extendedCan(__FUNCTION__, $member);
668
        if ($extended !== null) {
669
            return $extended;
670
        }
671
672
        return true;
673
    }
674
675
    /**
676
     * Show the configuration fields for each subsite
677
     *
678
     * @return FieldList
679
     */
680
    public function getCMSFields()
681
    {
682
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
683
            if ($this->exists()) {
684
                // Add a GridField for domains to a new tab if the subsite has already been created
685
                $fields->addFieldsToTab('Root.Domains', [
686
                    GridField::create(
687
                        'Domains',
688
                        '',
689
                        $this->Domains(),
690
                        GridFieldConfig_RecordEditor::create(10)
691
                    )
692
                ]);
693
            }
694
695
            // Remove the default scaffolded blacklist field, we replace it with a checkbox set field
696
            // in a wrapper further down. The RedirectURL field is currently not in use.
697
            $fields->removeByName(['PageTypeBlacklist', 'RedirectURL']);
698
699
            $fields->addFieldToTab('Root.Main', DropdownField::create(
700
                'Language',
701
                $this->fieldLabel('Language'),
702
                Injector::inst()->get(IntlLocales::class)->getLocales()
703
            ), 'DefaultSite');
704
705
            $fields->addFieldsToTab('Root.Main', [
706
                ToggleCompositeField::create(
707
                    'PageTypeBlacklistToggle',
708
                    _t(__CLASS__ . '.PageTypeBlacklistField', 'Disallow page types?'),
709
                    [
710
                        CheckboxSetField::create('PageTypeBlacklist', '', $this->getPageTypeMap())
711
                    ]
712
                )->setHeadingLevel(4),
713
                HiddenField::create('ID', '', $this->ID),
714
                HiddenField::create('IsSubsite', '', 1)
715
            ]);
716
717
            // If there are any themes available, add the dropdown
718
            $themes = $this->allowedThemes();
719
            if (!empty($themes)) {
720
                $fields->addFieldToTab(
721
                    'Root.Main',
722
                    DropdownField::create('Theme', $this->fieldLabel('Theme'), $this->allowedThemes(), $this->Theme)
723
                        ->setEmptyString(_t(__CLASS__ . '.ThemeFieldEmptyString', '-')),
724
                    'PageTypeBlacklistToggle'
725
                );
726
            }
727
728
            // Targetted by the XHR PJAX JavaScript to reload the subsite list in the CMS
729
            $fields->fieldByName('Root.Main')->addExtraClass('subsite-model');
730
731
            // We don't need the Groups many many tab
732
            $fields->removeByName('Groups');
733
734
            // Rename the main tab to configuration
735
            $fields->fieldByName('Root.Main')->setTitle(_t(__CLASS__ . '.ConfigurationTab', 'Configuration'));
736
        });
737
738
        return parent::getCMSFields();
739
    }
740
741
    /**
742
     * Return a list of the different page types available to the CMS
743
     *
744
     * @return array
745
     */
746
    public function getPageTypeMap()
747
    {
748
        $pageTypeMap = [];
749
750
        $pageTypes = SiteTree::page_type_classes();
751
        foreach ($pageTypes as $pageType) {
752
            $pageTypeMap[$pageType] = singleton($pageType)->i18n_singular_name();
753
        }
754
755
        asort($pageTypeMap);
756
757
        return $pageTypeMap;
758
    }
759
760
    /**
761
     *
762
     * @param boolean $includerelations
763
     * @return array
764
     */
765
    public function fieldLabels($includerelations = true)
766
    {
767
        $labels = parent::fieldLabels($includerelations);
768
        $labels['Title'] = _t('Subsites.TitleFieldLabel', 'Subsite Name');
769
        $labels['RedirectURL'] = _t('Subsites.RedirectURLFieldLabel', 'Redirect URL');
770
        $labels['DefaultSite'] = _t('Subsites.DefaultSiteFieldLabel', 'Default site');
771
        $labels['Theme'] = _t('Subsites.ThemeFieldLabel', 'Theme');
772
        $labels['Language'] = _t('Subsites.LanguageFieldLabel', 'Language');
773
        $labels['IsPublic'] = _t('Subsites.IsPublicFieldLabel', 'Enable public access');
774
        $labels['PageTypeBlacklist'] = _t('Subsites.PageTypeBlacklistFieldLabel', 'Page Type Blacklist');
775
        $labels['Domains.Domain'] = _t('Subsites.DomainFieldLabel', 'Domain');
776
        $labels['PrimaryDomain'] = _t('Subsites.PrimaryDomainFieldLabel', 'Primary Domain');
777
778
        return $labels;
779
    }
780
781
    /**
782
     * Return the themes that can be used with this subsite, as an array of themecode => description
783
     *
784
     * @return array
785
     */
786
    public function allowedThemes()
787
    {
788
        if ($themes = self::$allowed_themes) {
789
            return ArrayLib::valuekey($themes);
790
        }
791
792
        $themes = [];
793
        if (is_dir(THEMES_PATH)) {
794
            foreach (scandir(THEMES_PATH) as $theme) {
795
                if ($theme[0] == '.') {
796
                    continue;
797
                }
798
                $theme = strtok($theme, '_');
799
                $themes[$theme] = $theme;
800
            }
801
            ksort($themes);
802
        }
803
        return $themes;
804
    }
805
806
    /**
807
     * @return string Current locale of the subsite
808
     */
809
    public function getLanguage()
810
    {
811
        if ($this->getField('Language')) {
812
            return $this->getField('Language');
813
        }
814
815
        return i18n::get_locale();
816
    }
817
818
    /**
819
     *
820
     * @return \SilverStripe\ORM\ValidationResult
821
     */
822
    public function validate()
823
    {
824
        $result = parent::validate();
825
        if (!$this->Title) {
826
            $result->addError(_t(__CLASS__ . '.ValidateTitle', 'Please add a "Title"'));
827
        }
828
        return $result;
829
    }
830
831
    /**
832
     * Whenever a Subsite is written, rewrite the hostmap
833
     *
834
     * @return void
835
     */
836
    public function onAfterWrite()
837
    {
838
        Subsite::writeHostMap();
839
        parent::onAfterWrite();
840
    }
841
842
    /**
843
     * Return the primary domain of this site. Tries to "normalize" the domain name,
844
     * by replacing potential wildcards.
845
     *
846
     * @return string The full domain name of this subsite (without protocol prefix)
847
     */
848
    public function domain()
849
    {
850
        // Get best SubsiteDomain object
851
        $domainObject = $this->getPrimarySubsiteDomain();
852
        if ($domainObject) {
853
            return $domainObject->SubstitutedDomain;
854
        }
855
856
        // If there are no objects, default to the current hostname
857
        return $_SERVER['HTTP_HOST'];
858
    }
859
860
    /**
861
     * Finds the primary {@see SubsiteDomain} object for this subsite
862
     *
863
     * @return SubsiteDomain
864
     */
865
    public function getPrimarySubsiteDomain()
866
    {
867
        return $this
868
            ->Domains()
869
            ->sort('"IsPrimary" DESC')
870
            ->first();
871
    }
872
873
    /**
874
     *
875
     * @return string - The full domain name of this subsite (without protocol prefix)
876
     */
877
    public function getPrimaryDomain()
878
    {
879
        return $this->domain();
880
    }
881
882
    /**
883
     * Get the absolute URL for this subsite
884
     * @return string
885
     */
886
    public function absoluteBaseURL()
887
    {
888
        // Get best SubsiteDomain object
889
        $domainObject = $this->getPrimarySubsiteDomain();
890
        if ($domainObject) {
891
            return $domainObject->absoluteBaseURL();
892
        }
893
894
        // Fall back to the current base url
895
        return Director::absoluteBaseURL();
896
    }
897
898
    /**
899
     * Javascript admin action to duplicate this subsite
900
     *
901
     * @return string - javascript
902
     */
903
    public function adminDuplicate()
904
    {
905
        $newItem = $this->duplicate();
906
        $message = _t(
907
            __CLASS__ . '.CopyMessage',
908
            'Created a copy of {title}',
909
            ['title' => Convert::raw2js($this->Title)]
910
        );
911
912
        return <<<JS
913
            statusMessage($message, 'good');
914
            $('Form_EditForm').loadURLFromServer('admin/subsites/show/$newItem->ID');
915
JS;
916
    }
917
918
    /**
919
     * Make this subsite the current one
920
     */
921
    public function activate()
922
    {
923
        Subsite::changeSubsite($this);
924
    }
925
926
    /**
927
     *
928
     * @param array $permissionCodes
929
     * @return DataList
930
     */
931
    public function getMembersByPermission($permissionCodes = ['ADMIN'])
932
    {
933
        if (!is_array($permissionCodes)) {
934
            user_error('Permissions must be passed to Subsite::getMembersByPermission as an array', E_USER_ERROR);
935
        }
936
        $SQL_permissionCodes = Convert::raw2sql($permissionCodes);
937
938
        $SQL_permissionCodes = join("','", $SQL_permissionCodes);
939
940
        return DataObject::get(
941
            Member::class,
942
            "\"Group\".\"SubsiteID\" = $this->ID AND \"Permission\".\"Code\" IN ('$SQL_permissionCodes')",
943
            '',
944
            'LEFT JOIN "Group_Members" ON "Member"."ID" = "Group_Members"."MemberID"
945
            LEFT JOIN "Group" ON "Group"."ID" = "Group_Members"."GroupID"
946
            LEFT JOIN "Permission" ON "Permission"."GroupID" = "Group"."ID"'
947
        );
948
    }
949
950
    /**
951
     * Duplicate this subsite
952
     * @param bool $doWrite
953
     * @param string $manyMany
954
     * @return DataObject
955
     */
956
    public function duplicate($doWrite = true, $manyMany = 'many_many')
957
    {
958
        $duplicate = parent::duplicate($doWrite);
959
960
        $oldSubsiteID = SubsiteState::singleton()->getSubsiteId();
961
        self::changeSubsite($this->ID);
962
963
        /*
964
         * Copy data from this object to the given subsite. Does this using an iterative depth-first search.
965
         * This will make sure that the new parents on the new subsite are correct, and there are no funny
966
         * issues with having to check whether or not the new parents have been added to the site tree
967
         * when a page, etc, is duplicated
968
         */
969
        $stack = [[0, 0]];
970
        while (count($stack) > 0) {
971
            list($sourceParentID, $destParentID) = array_pop($stack);
972
            $children = Versioned::get_by_stage('Page', 'Live', "\"ParentID\" = $sourceParentID", '');
973
974
            if ($children) {
975
                foreach ($children as $child) {
976
                    self::changeSubsite($duplicate->ID); //Change to destination subsite
977
978
                    $childClone = $child->duplicateToSubsite($duplicate, false);
979
                    $childClone->ParentID = $destParentID;
980
                    $childClone->writeToStage('Stage');
981
                    $childClone->copyVersionToStage('Stage', 'Live');
982
983
                    self::changeSubsite($this->ID); //Change Back to this subsite
984
985
                    array_push($stack, [$child->ID, $childClone->ID]);
986
                }
987
            }
988
        }
989
990
        self::changeSubsite($oldSubsiteID);
991
992
        return $duplicate;
993
    }
994
}
995