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