Completed
Push — master ( aab69e...f7bdc5 )
by Daniel
12s
created

code/model/Subsite.php (10 issues)

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