Completed
Push — master ( 5cf2d8...46bcff )
by Damian
13s
created

code/model/Subsite.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace SilverStripe\Subsites\Model;
4
5
use SilverStripe\Admin\CMSMenu;
6
use SilverStripe\CMS\Model\SiteTree;
7
use SilverStripe\Control\Controller;
8
use SilverStripe\Control\Director;
9
use SilverStripe\Control\Session;
10
use SilverStripe\Core\Convert;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\Forms\CheckboxField;
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\HeaderField;
19
use SilverStripe\Forms\HiddenField;
20
use SilverStripe\Forms\LiteralField;
21
use SilverStripe\Forms\Tab;
22
use SilverStripe\Forms\TabSet;
23
use SilverStripe\Forms\TextField;
24
use SilverStripe\i18n\Data\Intl\IntlLocales;
25
use SilverStripe\i18n\i18n;
26
use SilverStripe\ORM\ArrayLib;
27
use SilverStripe\ORM\ArrayList;
28
use SilverStripe\ORM\DataList;
29
use SilverStripe\ORM\DataObject;
30
use SilverStripe\ORM\DB;
31
use SilverStripe\ORM\SS_List;
32
use SilverStripe\Security\Group;
33
use SilverStripe\Security\Member;
34
use SilverStripe\Security\Permission;
35
use SilverStripe\Versioned\Versioned;
36
use UnexpectedValueException;
37
38
/**
39
 * A dynamically created subsite. SiteTree objects can now belong to a subsite.
40
 * You can simulate subsite access without setting up virtual hosts by appending ?SubsiteID=<ID> to the request.
41
 *
42
 * @package subsites
43
 */
44
class Subsite extends DataObject
45
{
46
47
    private static $table_name = 'Subsite';
48
49
    /**
50
     * @var $use_session_subsiteid Boolean Set to TRUE when using the CMS and FALSE
51
     * when browsing the frontend of a website.
52
     *
53
     * @todo Remove flag once the Subsite CMS works without session state,
54
     * similarly to the Translatable module.
55
     */
56
    public static $use_session_subsiteid = false;
57
58
    /**
59
     * @var boolean $disable_subsite_filter If enabled, bypasses the query decoration
60
     * to limit DataObject::get*() calls to a specific subsite. Useful for debugging.
61
     */
62
    public static $disable_subsite_filter = false;
63
64
    /**
65
     * Allows you to force a specific subsite ID, or comma separated list of IDs.
66
     * Only works for reading. An object cannot be written to more than 1 subsite.
67
     */
68
    public static $force_subsite = null;
69
70
    /**
71
     *
72
     * @var boolean
73
     */
74
    public static $write_hostmap = true;
75
76
    /**
77
     * Memory cache of accessible sites
78
     *
79
     * @array
80
     */
81
    private static $_cache_accessible_sites = [];
82
83
    /**
84
     * Memory cache of subsite id for domains
85
     *
86
     * @var array
87
     */
88
    private static $_cache_subsite_for_domain = [];
89
90
    /**
91
     * @var array $allowed_themes Numeric array of all themes which are allowed to be selected for all subsites.
92
     * Corresponds to subfolder names within the /themes folder. By default, all themes contained in this folder
93
     * are listed.
94
     */
95
    private static $allowed_themes = [];
96
97
    /**
98
     * @var Boolean If set to TRUE, don't assume 'www.example.com' and 'example.com' are the same.
99
     * Doesn't affect wildcard matching, so '*.example.com' will match 'www.example.com' (but not 'example.com')
100
     * in both TRUE or FALSE setting.
101
     */
102
    public static $strict_subdomain_matching = false;
103
104
    /**
105
     * @var boolean Respects the IsPublic flag when retrieving subsites
106
     */
107
    public static $check_is_public = true;
108
109
    /*** @return array
110
     */
111
    private static $summary_fields = [
112
        'Title',
113
        'PrimaryDomain',
114
        'IsPublic'
115
    ];
116
117
    /**
118
     * Set allowed themes
119
     *
120
     * @param array $themes - Numeric array of all themes which are allowed to be selected for all subsites.
121
     */
122
    public static function set_allowed_themes($themes)
123
    {
124
        self::$allowed_themes = $themes;
125
    }
126
127
    /**
128
     * Gets the subsite currently set in the session.
129
     *
130
     * @uses ControllerSubsites->controllerAugmentInit()
131
     * @return DataObject The current Subsite
132
     */
133
    public static function currentSubsite()
134
    {
135
        return Subsite::get()->byID(self::currentSubsiteID());
136
    }
137
138
    /**
139
     * This function gets the current subsite ID from the session. It used in the backend so Ajax requests
140
     * use the correct subsite. The frontend handles subsites differently. It calls getSubsiteIDForDomain
141
     * directly from ModelAsController::getNestedController. Only gets Subsite instances which have their
142
     * {@link IsPublic} flag set to TRUE.
143
     *
144
     * You can simulate subsite access without creating virtual hosts by appending ?SubsiteID=<ID> to the request.
145
     *
146
     * @todo Pass $request object from controller so we don't have to rely on $_GET
147
     *
148
     * @return int ID of the current subsite instance
149
     */
150
    public static function currentSubsiteID()
0 ignored issues
show
currentSubsiteID uses the super-global variable $_GET which is generally not recommended.

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

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

// Better
class Router
{
    private $host;

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

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

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

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
151
    {
152
        $id = null;
153
154
        if (isset($_GET['SubsiteID'])) {
155
            $id = (int)$_GET['SubsiteID'];
156
        } elseif (Subsite::$use_session_subsiteid) {
157
            $id = Controller::curr()->getRequest()->getSession()->get('SubsiteID');
158
        }
159
160
        if ($id === null) {
161
            $id = self::getSubsiteIDForDomain();
162
        }
163
164
        return (int)$id;
165
    }
166
167
    /**
168
     * Switch to another subsite through storing the subsite identifier in the current PHP session.
169
     * Only takes effect when {@link Subsite::$use_session_subsiteid} is set to TRUE.
170
     *
171
     * @param int|Subsite $subsite Either the ID of the subsite, or the subsite object itself
172
     */
173
    public static function changeSubsite($subsite)
174
    {
175
        // Session subsite change only meaningful if the session is active.
176
        // Otherwise we risk setting it to wrong value, e.g. if we rely on currentSubsiteID.
177
        if (!Subsite::$use_session_subsiteid) {
178
            return;
179
        }
180
181
        if (is_object($subsite)) {
182
            $subsiteID = $subsite->ID;
183
        } else {
184
            $subsiteID = $subsite;
185
        }
186
187
        Controller::curr()->getRequest()->getSession()->set('SubsiteID', (int)$subsiteID);
188
189
        // Set locale
190
        if (is_object($subsite) && $subsite->Language !== '') {
191
            $locale = (new IntlLocales())->localeFromLang($subsite->Language);
192
            if ($locale) {
193
                i18n::set_locale($locale);
194
            }
195
        }
196
197
        Permission::reset();
198
    }
199
200
    /**
201
     * Get a matching subsite for the given host, or for the current HTTP_HOST.
202
     * Supports "fuzzy" matching of domains by placing an asterisk at the start of end of the string,
203
     * for example matching all subdomains on *.example.com with one subsite,
204
     * and all subdomains on *.example.org on another.
205
     *
206
     * @param $host string The host to find the subsite for.  If not specified, $_SERVER['HTTP_HOST'] is used.
207
     * @param bool $checkPermissions
208
     * @return int Subsite ID
209
     */
210
    public static function getSubsiteIDForDomain($host = null, $checkPermissions = true)
211
    {
212
        if ($host == null && isset($_SERVER['HTTP_HOST'])) {
213
            $host = $_SERVER['HTTP_HOST'];
214
        }
215
216
        $matchingDomains = null;
217
        $cacheKey = null;
218
        if ($host) {
219
            if (!self::$strict_subdomain_matching) {
220
                $host = preg_replace('/^www\./', '', $host);
221
            }
222
223
            $cacheKey = implode('_', [$host, Member::currentUserID(), self::$check_is_public]);
224
            if (isset(self::$_cache_subsite_for_domain[$cacheKey])) {
225
                return self::$_cache_subsite_for_domain[$cacheKey];
226
            }
227
228
            $SQL_host = Convert::raw2sql($host);
229
            $matchingDomains = DataObject::get(
230
                SubsiteDomain::class,
231
                "'$SQL_host' LIKE replace(\"SubsiteDomain\".\"Domain\",'*','%')",
232
                '"IsPrimary" DESC'
233
            )->innerJoin(
234
                'Subsite',
235
                '"Subsite"."ID" = "SubsiteDomain"."SubsiteID" AND "Subsite"."IsPublic"=1'
236
            );
237
        }
238
239
        if ($matchingDomains && $matchingDomains->count()) {
240
            $subsiteIDs = array_unique($matchingDomains->column('SubsiteID'));
241
            $subsiteDomains = array_unique($matchingDomains->column('Domain'));
242
            if (sizeof($subsiteIDs) > 1) {
243
                throw new UnexpectedValueException(sprintf(
244
                    "Multiple subsites match on '%s': %s",
245
                    $host,
246
                    implode(',', $subsiteDomains)
247
                ));
248
            }
249
250
            $subsiteID = $subsiteIDs[0];
251
        } else {
252
            if ($default = DataObject::get_one(Subsite::class, '"DefaultSite" = 1')) {
253
                // Check for a 'default' subsite
254
                $subsiteID = $default->ID;
255
            } else {
256
                // Default subsite id = 0, the main site
257
                $subsiteID = 0;
258
            }
259
        }
260
261
        if ($cacheKey) {
262
            self::$_cache_subsite_for_domain[$cacheKey] = $subsiteID;
263
        }
264
265
        return $subsiteID;
266
    }
267
268
    /**
269
     *
270
     * @param string $className
271
     * @param string $filter
272
     * @param string $sort
273
     * @param string $join
274
     * @param string $limit
275
     * @return DataList
276
     */
277
    public static function get_from_all_subsites($className, $filter = '', $sort = '', $join = '', $limit = '')
278
    {
279
        $result = DataObject::get($className, $filter, $sort, $join, $limit);
280
        $result = $result->setDataQueryParam('Subsite.filter', false);
281
        return $result;
282
    }
283
284
    /**
285
     * Disable the sub-site filtering; queries will select from all subsites
286
     * @param bool $disabled
287
     */
288
    public static function disable_subsite_filter($disabled = true)
289
    {
290
        self::$disable_subsite_filter = $disabled;
291
    }
292
293
    /**
294
     * Flush caches on database reset
295
     */
296
    public static function on_db_reset()
297
    {
298
        self::$_cache_accessible_sites = [];
299
        self::$_cache_subsite_for_domain = [];
300
    }
301
302
    /**
303
     * Return all subsites, regardless of permissions (augmented with main site).
304
     *
305
     * @param bool $includeMainSite
306
     * @param string $mainSiteTitle
307
     * @return SS_List List of <a href='psi_element://Subsite'>Subsite</a> objects (DataList or ArrayList).
308
     * objects (DataList or ArrayList).
309
     */
310
    public static function all_sites($includeMainSite = true, $mainSiteTitle = 'Main site')
311
    {
312
        $subsites = Subsite::get();
313
314 View Code Duplication
        if ($includeMainSite) {
315
            $subsites = $subsites->toArray();
316
317
            $mainSite = new Subsite();
318
            $mainSite->Title = $mainSiteTitle;
319
            array_unshift($subsites, $mainSite);
320
321
            $subsites = ArrayList::create($subsites);
322
        }
323
324
        return $subsites;
325
    }
326
327
    /*
328
     * Returns an ArrayList of the subsites accessible to the current user.
329
     * It's enough for any section to be accessible for the site to be included.
330
     *
331
     * @return ArrayList of {@link Subsite} instances.
332
     */
333
    public static function all_accessible_sites($includeMainSite = true, $mainSiteTitle = 'Main site', $member = null)
334
    {
335
        // Rationalise member arguments
336
        if (!$member) {
337
            $member = Member::currentUser();
338
        }
339
        if (!$member) {
340
            return new ArrayList();
341
        }
342
        if (!is_object($member)) {
343
            $member = DataObject::get_by_id(Member::class, $member);
344
        }
345
346
        $subsites = new ArrayList();
347
348
        // Collect subsites for all sections.
349
        $menu = CMSMenu::get_viewable_menu_items();
350
        foreach ($menu as $candidate) {
351
            if ($candidate->controller) {
352
                $accessibleSites = singleton($candidate->controller)->sectionSites(
353
                    $includeMainSite,
354
                    $mainSiteTitle,
355
                    $member
356
                );
357
358
                // Replace existing keys so no one site appears twice.
359
                $subsites->merge($accessibleSites);
360
            }
361
        }
362
363
        $subsites->removeDuplicates();
364
365
        return $subsites;
366
    }
367
368
    /**
369
     * Return the subsites that the current user can access by given permission.
370
     * Sites will only be included if they have a Title.
371
     *
372
     * @param $permCode array|string Either a single permission code or an array of permission codes.
373
     * @param $includeMainSite bool If true, the main site will be included if appropriate.
374
     * @param $mainSiteTitle string The label to give to the main site
375
     * @param $member int|Member The member attempting to access the sites
376
     * @return DataList|ArrayList of {@link Subsite} instances
377
     */
378
    public static function accessible_sites(
379
        $permCode,
380
        $includeMainSite = true,
381
        $mainSiteTitle = 'Main site',
382
        $member = null
383
    ) {
384
    
385
        // Rationalise member arguments
386
        if (!$member) {
387
            $member = Member::currentUser();
388
        }
389
        if (!$member) {
390
            return new ArrayList();
391
        }
392
        if (!is_object($member)) {
393
            $member = DataObject::get_by_id(Member::class, $member);
394
        }
395
396
        // Rationalise permCode argument
397
        if (is_array($permCode)) {
398
            $SQL_codes = "'" . implode("', '", Convert::raw2sql($permCode)) . "'";
399
        } else {
400
            $SQL_codes = "'" . Convert::raw2sql($permCode) . "'";
401
        }
402
403
        // Cache handling
404
        $cacheKey = $SQL_codes . '-' . $member->ID . '-' . $includeMainSite . '-' . $mainSiteTitle;
405
        if (isset(self::$_cache_accessible_sites[$cacheKey])) {
406
            return self::$_cache_accessible_sites[$cacheKey];
407
        }
408
409
        $subsites = DataList::create(Subsite::class)
410
            ->where("\"Subsite\".\"Title\" != ''")
411
            ->leftJoin('Group_Subsites', '"Group_Subsites"."SubsiteID" = "Subsite"."ID"')
412
            ->innerJoin(
413
                'Group',
414
                '"Group"."ID" = "Group_Subsites"."GroupID" OR "Group"."AccessAllSubsites" = 1'
415
            )
416
            ->innerJoin(
417
                'Group_Members',
418
                "\"Group_Members\".\"GroupID\"=\"Group\".\"ID\" AND \"Group_Members\".\"MemberID\" = $member->ID"
419
            )
420
            ->innerJoin(
421
                'Permission',
422
                "\"Group\".\"ID\"=\"Permission\".\"GroupID\" AND \"Permission\".\"Code\" IN ($SQL_codes, 'CMS_ACCESS_LeftAndMain', 'ADMIN')"
423
            );
424
425
        if (!$subsites) {
426
            $subsites = new ArrayList();
427
        }
428
429
        /** @var DataList $rolesSubsites */
430
        $rolesSubsites = DataList::create(Subsite::class)
431
            ->where("\"Subsite\".\"Title\" != ''")
432
            ->leftJoin('Group_Subsites', '"Group_Subsites"."SubsiteID" = "Subsite"."ID"')
433
            ->innerJoin(
434
                'Group',
435
                '"Group"."ID" = "Group_Subsites"."GroupID" OR "Group"."AccessAllSubsites" = 1'
436
            )
437
            ->innerJoin(
438
                'Group_Members',
439
                "\"Group_Members\".\"GroupID\"=\"Group\".\"ID\" AND \"Group_Members\".\"MemberID\" = $member->ID"
440
            )
441
            ->innerJoin('Group_Roles', '"Group_Roles"."GroupID"="Group"."ID"')
442
            ->innerJoin('PermissionRole', '"Group_Roles"."PermissionRoleID"="PermissionRole"."ID"')
443
            ->innerJoin(
444
                'PermissionRoleCode',
445
                "\"PermissionRole\".\"ID\"=\"PermissionRoleCode\".\"RoleID\" AND \"PermissionRoleCode\".\"Code\" IN ($SQL_codes, 'CMS_ACCESS_LeftAndMain', 'ADMIN')"
446
            );
447
448
        if (!$subsites && $rolesSubsites) {
449
            return $rolesSubsites;
450
        }
451
452
        $subsites = new ArrayList($subsites->toArray());
453
454
        if ($rolesSubsites) {
455
            foreach ($rolesSubsites as $subsite) {
456
                if (!$subsites->find('ID', $subsite->ID)) {
457
                    $subsites->push($subsite);
458
                }
459
            }
460
        }
461
462
        if ($includeMainSite) {
463
            if (!is_array($permCode)) {
464
                $permCode = [$permCode];
465
            }
466 View Code Duplication
            if (self::hasMainSitePermission($member, $permCode)) {
467
                $subsites = $subsites->toArray();
468
469
                $mainSite = new Subsite();
470
                $mainSite->Title = $mainSiteTitle;
471
                array_unshift($subsites, $mainSite);
472
                $subsites = ArrayList::create($subsites);
473
            }
474
        }
475
476
        self::$_cache_accessible_sites[$cacheKey] = $subsites;
477
478
        return $subsites;
479
    }
480
481
    /**
482
     * Write a host->domain map to subsites/host-map.php
483
     *
484
     * This is used primarily when using subsites in conjunction with StaticPublisher
485
     *
486
     * @param string $file - filepath of the host map to be written
487
     * @return void
488
     */
489
    public static function writeHostMap($file = null)
490
    {
491
        if (!self::$write_hostmap) {
492
            return;
493
        }
494
495
        if (!$file) {
496
            $file = Director::baseFolder() . '/subsites/host-map.php';
497
        }
498
        $hostmap = [];
499
500
        $subsites = DataObject::get(Subsite::class);
501
502
        if ($subsites) {
503
            foreach ($subsites as $subsite) {
504
                $domains = $subsite->Domains();
505
                if ($domains) {
506
                    foreach ($domains as $domain) {
507
                        $domainStr = $domain->Domain;
508
                        if (!self::$strict_subdomain_matching) {
509
                            $domainStr = preg_replace('/^www\./', '', $domainStr);
510
                        }
511
                        $hostmap[$domainStr] = $subsite->domain();
512
                    }
513
                }
514
                if ($subsite->DefaultSite) {
515
                    $hostmap['default'] = $subsite->domain();
516
                }
517
            }
518
        }
519
520
        $data = "<?php \n";
521
        $data .= "// Generated by Subsite::writeHostMap() on " . date('d/M/y') . "\n";
522
        $data .= '$subsiteHostmap = ' . var_export($hostmap, true) . ';';
523
524
        if (is_writable(dirname($file)) || is_writable($file)) {
525
            file_put_contents($file, $data);
526
        }
527
    }
528
529
    /**
530
     * Checks if a member can be granted certain permissions, regardless of the subsite context.
531
     * Similar logic to {@link Permission::checkMember()}, but only returns TRUE
532
     * if the member is part of a group with the "AccessAllSubsites" flag set.
533
     * If more than one permission is passed to the method, at least one of them must
534
     * be granted for if to return TRUE.
535
     *
536
     * @todo Allow permission inheritance through group hierarchy.
537
     *
538
     * @param Member Member to check against. Defaults to currently logged in member
539
     * @param array $permissionCodes
540
     * @return bool
541
     */
542
    public static function hasMainSitePermission($member = null, $permissionCodes = ['ADMIN'])
543
    {
544
        if (!is_array($permissionCodes)) {
545
            user_error('Permissions must be passed to Subsite::hasMainSitePermission as an array', E_USER_ERROR);
546
        }
547
548
        if (!$member && $member !== false) {
549
            $member = Member::currentUser();
550
        }
551
552
        if (!$member) {
553
            return false;
554
        }
555
556
        if (!in_array('ADMIN', $permissionCodes)) {
557
            $permissionCodes[] = 'ADMIN';
558
        }
559
560
        $SQLa_perm = Convert::raw2sql($permissionCodes);
561
        $SQL_perms = join("','", $SQLa_perm);
562
        $memberID = (int)$member->ID;
563
564
        // Count this user's groups which can access the main site
565
        $groupCount = DB::query("
566
            SELECT COUNT(\"Permission\".\"ID\")
567
            FROM \"Permission\"
568
            INNER JOIN \"Group\" ON \"Group\".\"ID\" = \"Permission\".\"GroupID\" AND \"Group\".\"AccessAllSubsites\" = 1
569
            INNER JOIN \"Group_Members\" ON \"Group_Members\".\"GroupID\" = \"Permission\".\"GroupID\"
570
            WHERE \"Permission\".\"Code\" IN ('$SQL_perms')
571
            AND \"Group_Members\".\"MemberID\" = {$memberID}
572
        ")->value();
573
574
        // Count this user's groups which have a role that can access the main site
575
        $roleCount = DB::query("
576
            SELECT COUNT(\"PermissionRoleCode\".\"ID\")
577
            FROM \"Group\"
578
            INNER JOIN \"Group_Members\" ON \"Group_Members\".\"GroupID\" = \"Group\".\"ID\"
579
            INNER JOIN \"Group_Roles\" ON \"Group_Roles\".\"GroupID\"=\"Group\".\"ID\"
580
            INNER JOIN \"PermissionRole\" ON \"Group_Roles\".\"PermissionRoleID\"=\"PermissionRole\".\"ID\"
581
            INNER JOIN \"PermissionRoleCode\" ON \"PermissionRole\".\"ID\"=\"PermissionRoleCode\".\"RoleID\"
582
            WHERE \"PermissionRoleCode\".\"Code\" IN ('$SQL_perms')
583
            AND \"Group\".\"AccessAllSubsites\" = 1
584
            AND \"Group_Members\".\"MemberID\" = {$memberID}
585
        ")->value();
586
587
        // There has to be at least one that allows access.
588
        return ($groupCount + $roleCount > 0);
589
    }
590
591
    /**
592
     * @var array
593
     */
594
    private static $db = [
595
        'Title' => 'Varchar(255)',
596
        'RedirectURL' => 'Varchar(255)',
597
        'DefaultSite' => 'Boolean',
598
        'Theme' => 'Varchar',
599
        'Language' => 'Varchar(6)',
600
601
        // Used to hide unfinished/private subsites from public view.
602
        // If unset, will default to true
603
        'IsPublic' => 'Boolean',
604
605
        // Comma-separated list of disallowed page types
606
        'PageTypeBlacklist' => 'Text',
607
    ];
608
609
    /**
610
     * @var array
611
     */
612
    private static $has_many = [
613
        'Domains' => SubsiteDomain::class,
614
    ];
615
616
    /**
617
     * @var array
618
     */
619
    private static $belongs_many_many = [
620
        'Groups' => Group::class,
621
    ];
622
623
    /**
624
     * @var array
625
     */
626
    private static $defaults = [
627
        'IsPublic' => 1
628
    ];
629
630
    /**
631
     * @var array
632
     */
633
    private static $searchable_fields = [
634
        'Title',
635
        'Domains.Domain',
636
        'IsPublic',
637
    ];
638
639
    /**
640
     * @var string
641
     */
642
    private static $default_sort = '"Title" ASC';
643
644
    /**
645
     * @todo Possible security issue, don't grant edit permissions to everybody.
646
     * @param bool $member
647
     * @return bool
648
     */
649
    public function canEdit($member = false)
650
    {
651
        return true;
652
    }
653
654
    /**
655
     * Show the configuration fields for each subsite
656
     *
657
     * @return FieldList
658
     */
659
    public function getCMSFields()
660
    {
661
        if ($this->ID != 0) {
662
            $domainTable = new GridField(
663
                'Domains',
664
                _t('Subsite.DomainsListTitle', 'Domains'),
665
                $this->Domains(),
666
                GridFieldConfig_RecordEditor::create(10)
667
            );
668
        } else {
669
            $domainTable = new LiteralField(
670
                'Domains',
671
                '<p>' . _t(
672
                    'Subsite.DOMAINSAVEFIRST',
673
                    'You can only add domains after saving for the first time'
674
                ) . '</p>'
675
            );
676
        }
677
678
        $languageSelector = new DropdownField(
679
            'Language',
680
            $this->fieldLabel('Language'),
681
            Injector::inst()->get(IntlLocales::class)->getLocales()
682
        );
683
684
        $pageTypeMap = [];
685
        $pageTypes = SiteTree::page_type_classes();
686
        foreach ($pageTypes as $pageType) {
687
            $pageTypeMap[$pageType] = singleton($pageType)->i18n_singular_name();
688
        }
689
        asort($pageTypeMap);
690
691
        $fields = new FieldList(
692
            $subsiteTabs = new TabSet(
693
                'Root',
694
                new Tab(
695
                    'Configuration',
696
                    _t('Subsite.TabTitleConfig', 'Configuration'),
697
                    HeaderField::create('ConfigForSubsiteHeaderField', 'Subsite Configuration'),
698
                    TextField::create('Title', $this->fieldLabel('Title'), $this->Title),
699
                    HeaderField::create(
700
                        'DomainsForSubsiteHeaderField',
701
                        _t('Subsite.DomainsHeadline', 'Domains for this subsite')
702
                    ),
703
                    $domainTable,
704
                    $languageSelector,
705
                    // new TextField('RedirectURL', 'Redirect to URL', $this->RedirectURL),
706
                    CheckboxField::create('DefaultSite', $this->fieldLabel('DefaultSite'), $this->DefaultSite),
707
                    CheckboxField::create('IsPublic', $this->fieldLabel('IsPublic'), $this->IsPublic),
708
                    LiteralField::create(
709
                        'PageTypeBlacklistToggle',
710
                        sprintf(
711
                            '<div class="field"><a href="#" id="PageTypeBlacklistToggle">%s</a></div>',
712
                            _t('Subsite.PageTypeBlacklistField', 'Disallow page types?')
713
                        )
714
                    ),
715
                    CheckboxSetField::create(
716
                        'PageTypeBlacklist',
717
                        false,
718
                        $pageTypeMap
719
                    )
720
                )
721
            ),
722
            HiddenField::create('ID', '', $this->ID),
723
            HiddenField::create('IsSubsite', '', 1)
724
        );
725
726
        // If there are any themes available, add the dropdown
727
        $themes = $this->allowedThemes();
728
        if (!empty($themes)) {
729
            $fields->addFieldToTab(
730
                'Root.Configuration',
731
                DropdownField::create('Theme', $this->fieldLabel('Theme'), $this->allowedThemes(), $this->Theme)
732
                ->setEmptyString(_t('Subsite.ThemeFieldEmptyString', '-')),
733
                'PageTypeBlacklistToggle'
734
            );
735
        }
736
737
        $subsiteTabs->addExtraClass('subsite-model');
738
739
        $this->extend('updateCMSFields', $fields);
740
        return $fields;
741
    }
742
743
    /**
744
     *
745
     * @param boolean $includerelations
746
     * @return array
747
     */
748
    public function fieldLabels($includerelations = true)
749
    {
750
        $labels = parent::fieldLabels($includerelations);
751
        $labels['Title'] = _t('Subsites.TitleFieldLabel', 'Subsite Name');
752
        $labels['RedirectURL'] = _t('Subsites.RedirectURLFieldLabel', 'Redirect URL');
753
        $labels['DefaultSite'] = _t('Subsites.DefaultSiteFieldLabel', 'Default site');
754
        $labels['Theme'] = _t('Subsites.ThemeFieldLabel', 'Theme');
755
        $labels['Language'] = _t('Subsites.LanguageFieldLabel', 'Language');
756
        $labels['IsPublic'] = _t('Subsites.IsPublicFieldLabel', 'Enable public access');
757
        $labels['PageTypeBlacklist'] = _t('Subsites.PageTypeBlacklistFieldLabel', 'Page Type Blacklist');
758
        $labels['Domains.Domain'] = _t('Subsites.DomainFieldLabel', 'Domain');
759
        $labels['PrimaryDomain'] = _t('Subsites.PrimaryDomainFieldLabel', 'Primary Domain');
760
761
        return $labels;
762
    }
763
764
    /**
765
     * Return the themes that can be used with this subsite, as an array of themecode => description
766
     *
767
     * @return array
768
     */
769
    public function allowedThemes()
770
    {
771
        if ($themes = $this->stat('allowed_themes')) {
772
            return ArrayLib::valuekey($themes);
773
        }
774
775
        $themes = [];
776
        if (is_dir(THEMES_PATH)) {
777
            foreach (scandir(THEMES_PATH) as $theme) {
778
                if ($theme[0] == '.') {
779
                    continue;
780
                }
781
                $theme = strtok($theme, '_');
782
                $themes[$theme] = $theme;
783
            }
784
            ksort($themes);
785
        }
786
        return $themes;
787
    }
788
789
    /**
790
     * @return string Current locale of the subsite
791
     */
792
    public function getLanguage()
793
    {
794
        if ($this->getField('Language')) {
795
            return $this->getField('Language');
796
        }
797
798
        return i18n::get_locale();
799
    }
800
801
    /**
802
     *
803
     * @return \SilverStripe\ORM\ValidationResult
804
     */
805
    public function validate()
806
    {
807
        $result = parent::validate();
808
        if (!$this->Title) {
809
            $result->addError(_t('Subsite.ValidateTitle', 'Please add a "Title"'));
810
        }
811
        return $result;
812
    }
813
814
    /**
815
     * Whenever a Subsite is written, rewrite the hostmap
816
     *
817
     * @return void
818
     */
819
    public function onAfterWrite()
820
    {
821
        Subsite::writeHostMap();
822
        parent::onAfterWrite();
823
    }
824
825
    /**
826
     * Return the primary domain of this site. Tries to "normalize" the domain name,
827
     * by replacing potential wildcards.
828
     *
829
     * @return string The full domain name of this subsite (without protocol prefix)
830
     */
831
    public function domain()
832
    {
833
        // Get best SubsiteDomain object
834
        $domainObject = $this->getPrimarySubsiteDomain();
835
        if ($domainObject) {
836
            return $domainObject->SubstitutedDomain;
837
        }
838
839
        // If there are no objects, default to the current hostname
840
        return $_SERVER['HTTP_HOST'];
841
    }
842
843
    /**
844
     * Finds the primary {@see SubsiteDomain} object for this subsite
845
     *
846
     * @return SubsiteDomain
847
     */
848
    public function getPrimarySubsiteDomain()
849
    {
850
        return $this
851
            ->Domains()
852
            ->sort('"IsPrimary" DESC')
853
            ->first();
854
    }
855
856
    /**
857
     *
858
     * @return string - The full domain name of this subsite (without protocol prefix)
859
     */
860
    public function getPrimaryDomain()
861
    {
862
        return $this->domain();
863
    }
864
865
    /**
866
     * Get the absolute URL for this subsite
867
     * @return string
868
     */
869
    public function absoluteBaseURL()
870
    {
871
        // Get best SubsiteDomain object
872
        $domainObject = $this->getPrimarySubsiteDomain();
873
        if ($domainObject) {
874
            return $domainObject->absoluteBaseURL();
875
        }
876
877
        // Fall back to the current base url
878
        return Director::absoluteBaseURL();
879
    }
880
881
    /**
882
     * Javascript admin action to duplicate this subsite
883
     *
884
     * @return string - javascript
885
     */
886
    public function adminDuplicate()
887
    {
888
        $newItem = $this->duplicate();
889
        $message = _t(
890
            'Subsite.CopyMessage',
891
            'Created a copy of {title}',
892
            ['title' => Convert::raw2js($this->Title)]
893
        );
894
895
        return <<<JS
896
            statusMessage($message, 'good');
897
            $('Form_EditForm').loadURLFromServer('admin/subsites/show/$newItem->ID');
898
JS;
899
    }
900
901
    /**
902
     * Make this subsite the current one
903
     */
904
    public function activate()
905
    {
906
        Subsite::changeSubsite($this);
907
    }
908
909
    /**
910
     *
911
     * @param array $permissionCodes
912
     * @return DataList
913
     */
914
    public function getMembersByPermission($permissionCodes = ['ADMIN'])
915
    {
916
        if (!is_array($permissionCodes)) {
917
            user_error('Permissions must be passed to Subsite::getMembersByPermission as an array', E_USER_ERROR);
918
        }
919
        $SQL_permissionCodes = Convert::raw2sql($permissionCodes);
920
921
        $SQL_permissionCodes = join("','", $SQL_permissionCodes);
922
923
        return DataObject::get(
924
            Member::class,
925
            "\"Group\".\"SubsiteID\" = $this->ID AND \"Permission\".\"Code\" IN ('$SQL_permissionCodes')",
926
            '',
927
            'LEFT JOIN "Group_Members" ON "Member"."ID" = "Group_Members"."MemberID"
928
            LEFT JOIN "Group" ON "Group"."ID" = "Group_Members"."GroupID"
929
            LEFT JOIN "Permission" ON "Permission"."GroupID" = "Group"."ID"'
930
        );
931
    }
932
933
    /**
934
     * Duplicate this subsite
935
     * @param bool $doWrite
936
     * @param string $manyMany
937
     * @return DataObject
938
     */
939
    public function duplicate($doWrite = true, $manyMany = 'many_many')
940
    {
941
        $duplicate = parent::duplicate($doWrite);
942
943
        $oldSubsiteID = Session::get('SubsiteID');
944
        self::changeSubsite($this->ID);
945
946
        /*
947
         * Copy data from this object to the given subsite. Does this using an iterative depth-first search.
948
         * This will make sure that the new parents on the new subsite are correct, and there are no funny
949
         * issues with having to check whether or not the new parents have been added to the site tree
950
         * when a page, etc, is duplicated
951
         */
952
        $stack = [[0, 0]];
953
        while (count($stack) > 0) {
954
            list($sourceParentID, $destParentID) = array_pop($stack);
955
            $children = Versioned::get_by_stage('Page', 'Live', "\"ParentID\" = $sourceParentID", '');
956
957
            if ($children) {
958
                foreach ($children as $child) {
959
                    self::changeSubsite($duplicate->ID); //Change to destination subsite
960
961
                    $childClone = $child->duplicateToSubsite($duplicate, false);
962
                    $childClone->ParentID = $destParentID;
963
                    $childClone->writeToStage('Stage');
964
                    $childClone->copyVersionToStage('Stage', 'Live');
965
966
                    self::changeSubsite($this->ID); //Change Back to this subsite
967
968
                    array_push($stack, [$child->ID, $childClone->ID]);
969
                }
970
            }
971
        }
972
973
        self::changeSubsite($oldSubsiteID);
974
975
        return $duplicate;
976
    }
977
}
978