Version: current

      1 <?php
      2 /**
      3  * A dynamically created subsite. SiteTree objects can now belong to a subsite.
      4  * You can simulate subsite access without setting up virtual hosts by appending ?SubsiteID=<ID> to the request.
      5  *
      6  * @package subsites
      7  */
      8 class Subsite extends DataObject implements PermissionProvider {
      9
     10     /**
     11      * @var boolean $disable_subsite_filter If enabled, bypasses the query decoration
     12      * to limit DataObject::get*() calls to a specific subsite. Useful for debugging.
     13      */
     14     static $disable_subsite_filter = false;
     15
     16     /**
     17      * Allows you to force a specific subsite ID, or comma separated list of IDs.
     18      * Only works for reading. An object cannot be written to more than 1 subsite.
     19      */
     20     static $force_subsite = null;
     21
     22     static $write_hostmap = true;
     23     static $default_sort = "\"Title\" ASC";
     24
     25     static $db = array(
     26         'Title' => 'Varchar(255)',
     27         'RedirectURL' => 'Varchar(255)',
     28         'DefaultSite' => 'Boolean',
     29         'Theme' => 'Varchar',
     30         'Language' => 'Varchar(6)',
     31
     32         // Used to hide unfinished/private subsites from public view.
     33         // If unset, will default to true
     34         'IsPublic' => 'Boolean'
     35     );
     36
     37     static $has_one = array(
     38     );
     39
     40     static $has_many = array(
     41         'Domains' => 'SubsiteDomain',
     42     );
     43
     44     static $belongs_many_many = array(
     45         "Groups" => "Group",
     46     );
     47
     48     static $defaults = array(
     49         'IsPublic' => 1
     50     );
     51
     52     static $searchable_fields = array(
     53         'Title' => array(
     54             'title' => 'Subsite Name'
     55         ),
     56         'Domains.Domain' => array(
     57             'title' => 'Domain name'
     58         ),
     59         'IsPublic' => array(
     60             'title' => 'Active subsite',
     61         ),
     62     );
     63
     64     static $summary_fields = array(
     65         'Title' => 'Subsite Name',
     66         'PrimaryDomain' => 'Primary Domain',
     67         'IsPublic' => 'Active subsite',
     68     );
     69
     70     // язык основного сайта, можно менять в _00config.php
     71     protected static $mainsite_language = 'ru_RU';
     72
     73     static function set_mainsite_language($val) {
     74         self::$mainsite_language = $val;
     75     }
     76
     77     static function get_mainsite_language() {
     78         return self::$mainsite_language;
     79     }
     80
     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     protected static $allowed_themes = array();
     88
     89         /**
     90          * @var string
     91          */
     92         static $mainsite_domain = 'localhost';
     93
     94         static function set_mainsite_domain($domain){
     95             self::$mainsite_domain = $domain;
     96         }
     97         static function get_mainsite_domain() {
     98             return self::$mainsite_domain;
     99         }
    100
    101     static function set_allowed_domains($domain){
    102         user_error('Subsite::set_allowed_domains() is deprecated; it is no longer necessary '
    103             . 'because users can now enter any domain name', E_USER_NOTICE);
    104     }
    105
    106     static function set_allowed_themes($themes) {
    107         self::$allowed_themes = $themes;
    108     }
    109
    110     /**
    111      * Return the themes that can be used with this subsite, as an array of themecode => description
    112      */
    113     function allowedThemes() {
    114         if($themes = $this->stat('allowed_themes')) {
    115             return ArrayLib::valuekey($themes);
    116         } else {
    117             $themes = array();
    118             $themes[''] = _t('Subsite.NotSelected');
    119             if(is_dir('../themes/')) {
    120                 foreach(scandir('../themes/') as $theme) {
    121                     if($theme[0] == '.') continue;
    122                     $theme = strtok($theme,'_');
    123                     $themes[$theme] = $theme;
    124                 }
    125                 ksort($themes);
    126             }
    127             return $themes;
    128         }
    129     }
    130
    131     public function getLanguage() {
    132         if($this->getField('Language')) {
    133             return $this->getField('Language');
    134         } else {
    135             return i18n::get_locale();
    136         }
    137     }
    138
    139     function onBeforeDelete() {
    140         parent::onBeforeDelete();
    141
    142         if ($this->Domains()->Count()) {
    143             foreach($this->Domains() as $domain) {
    144                 $domain->delete();
    145             }
    146         }
    147     }
    148
    149     /**
    150      * Whenever a Subsite is written, rewrite the hostmap
    151      *
    152      * @return void
    153      */
    154     public function onAfterWrite() {
    155         Subsite::writeHostMap();
    156     }
    157
    158     /**
    159      * Return the domain of this site
    160      *
    161      * @return string The full domain name of this subsite (without protocol prefix)
    162      */
    163     function domain() {
    164         if($this->ID) {
    165             $domains = DataObject::get("SubsiteDomain", "\"SubsiteID\" = $this->ID", "\"IsPrimary\" DESC",
    166                 "", 1);
    167             if($domains) {
    168                 $domain = $domains->First()->Domain;
    169                 // If there are wildcards in the primary domain (not recommended), make some
    170                 // educated guesses about what to replace them with
    171                 $domain = preg_replace("/\\.\\*\$/",".$_SERVER[HTTP_HOST]", $domain);
    172                 $domain = preg_replace("/^\\*\\./","subsite.", $domain);
    173                 $domain = str_replace('.www.','.', $domain);
    174                 return $domain;
    175             }
    176
    177         // SubsiteID = 0 is often used to refer to the main site, just return $_SERVER['HTTP_HOST']
    178         } else {
    179             return $_SERVER['HTTP_HOST'];
    180         }
    181     }
    182
    183     function getPrimaryDomain() {
    184         return $this->domain();
    185     }
    186
    187     function absoluteBaseURL() {
    188         return "http://" . $this->domain() . Director::baseURL();
    189     }
    190
    191     /**
    192      * Show the configuration fields for each subsite
    193      */
    194     function getCMSFields() {
    195         $domainTable = new TableField("Domains", "SubsiteDomain",
    196             array("Domain" => "Domain (use * as a wildcard)", "IsPrimary" => "Primary domain?"),
    197             array("Domain" => "TextField", "IsPrimary" => "CheckboxField"),
    198             "SubsiteID", $this->ID);
    199
    200         $languageSelector = new DropdownField('Language', 'Language', i18n::get_common_locales());
    201
    202         $fields = new FieldSet(
    203             new TabSet('Root',
    204                 new Tab('Configuration',
    205                     new HeaderField($this->getClassName() . ' configuration', 2),
    206                     new TextField('Title', 'Name of subsite:', $this->Title),
    207
    208                     new HeaderField("Domains for this subsite"),
    209                     $domainTable,
    210                     $languageSelector,
    211                     // new TextField('RedirectURL', 'Redirect to URL', $this->RedirectURL),
    212                     new CheckboxField('DefaultSite', 'Default site', $this->DefaultSite),
    213                     new CheckboxField('IsPublic', 'Enable public access', $this->IsPublic),
    214
    215                     new DropdownField('Theme','Theme', $this->allowedThemes(), $this->Theme)
    216                 )
    217             ),
    218             new HiddenField('ID', '', $this->ID),
    219             new HiddenField('IsSubsite', '', 1)
    220         );
    221
    222         $this->extend('updateCMSFields', $fields);
    223         return $fields;
    224     }
    225
    226     /**
    227      * @todo getClassName is redundant, already stored as a database field?
    228      */
    229     function getClassName() {
    230         return $this->class;
    231     }
    232
    233     function getCMSActions() {
    234         return new FieldSet(
    235             new FormAction('callPageMethod', "Create copy", null, 'adminDuplicate')
    236         );
    237     }
    238
    239     function adminDuplicate() {
    240         $newItem = $this->duplicate();
    241         $JS_title = Convert::raw2js($this->Title);
    242         return <<<JS
    243             statusMessage('Created a copy of $JS_title', 'good');
    244             $('Form_EditForm').loadURLFromServer('admin/subsites/show/$newItem->ID');
    245 JS;
    246     }
    247
    248     /**
    249      * Gets the subsite currently set in the session.
    250      *
    251      * @uses ControllerSubsites->controllerAugmentInit()
    252      *
    253      * @return Subsite
    254      */
    255     static function currentSubsite() {
    256         // get_by_id handles caching so we don't have to
    257         return DataObject::get_by_id('Subsite', self::currentSubsiteID());
    258     }
    259
    260     /**
    261      * This function gets the current subsite ID from the session. It used in the backend so Ajax requests
    262      * use the correct subsite. The frontend handles subsites differently. It calls getSubsiteIDForDomain
    263      * directly from ModelAsController::getNestedController. Only gets Subsite instances which have their
    264      * {@link IsPublic} flag set to TRUE.
    265      *
    266      * You can simulate subsite access without creating virtual hosts by appending ?SubsiteID=<ID> to the request.
    267      *
    268      * @todo Pass $request object from controller so we don't have to rely on $_REQUEST
    269      *
    270      * @param boolean $cache
    271      * @return int ID of the current subsite instance
    272      */
    273     static function currentSubsiteID() {
    274         if(isset($_REQUEST['SubsiteID'])) $id = (int)$_REQUEST['SubsiteID'];
    275         else $id = Session::get('SubsiteID');
    276
    277         if($id === NULL) {
    278             $id = self::getSubsiteIDForDomain();
    279             Session::set('SubsiteID', $id);
    280         }
    281
    282         return (int)$id;
    283     }
    284
    285     /**
    286      * Switch to another subsite.
    287      *
    288      * @param int|Subsite $subsite Either the ID of the subsite, or the subsite object itself
    289      */
    290     static function changeSubsite($subsite) {
    291         if(is_object($subsite)) $subsiteID = $subsite->ID;
    292         else $subsiteID = $subsite;
    293
    294         Session::set('SubsiteID', (int)$subsiteID);
    295
    296         // Set locale
    297         if (is_object($subsite) && $subsite->Language != '') {
    298             if (array_key_exists($subsite->Language, i18n::$common_locales)) {
    299                 i18n::set_locale($subsite->Language);
    300             }
    301             elseif (isset(i18n::$likely_subtags[$subsite->Language])) {
    302                 i18n::set_locale(i18n::$likely_subtags[$subsite->Language]);
    303             }
    304         }
    305
    306         // Only bother flushing caches if we've actually changed
    307         if($subsiteID != self::currentSubsiteID()) Permission::flush_permission_cache();
    308     }
    309
    310     /**
    311      * Make this subsite the current one
    312      */
    313     public function activate() {
    314         Subsite::changeSubsite($this);
    315     }
    316
    317     /**
    318      * @todo Possible security issue, don't grant edit permissions to everybody.
    319      */
    320     function canEdit() {
    321         return true;
    322     }
    323
    324     public function Lang() {
    325         return i18n::get_lang_from_locale($this->Language);
    326     }
    327
    328     /**
    329      * Get a matching subsite for the given host, or for the current HTTP_HOST.
    330      *
    331      * @param $host The host to find the subsite for.  If not specified, $_SERVER['HTTP_HOST']
    332      * is used.
    333      *
    334      * @return int Subsite ID
    335      */
    336     static function getSubsiteIDForDomain($host = null, $returnMainIfNotFound = true) {
    337         if($host == null) $host = $_SERVER['HTTP_HOST'];
    338
    339         $host = str_replace('www.','',$host);
    340         $SQL_host = Convert::raw2sql($host);
    341
    342         $matchingDomains = DataObject::get("SubsiteDomain", "'$SQL_host' LIKE replace(\"SubsiteDomain\".\"Domain\",'*','%')",
    343             "\"IsPrimary\" DESC", "INNER JOIN \"Subsite\" ON \"Subsite\".\"ID\" = \"SubsiteDomain\".\"SubsiteID\" AND
    344             \"Subsite\".\"IsPublic\"=1");
    345
    346         if($matchingDomains) {
    347             $subsiteIDs = array_unique($matchingDomains->column('SubsiteID'));
    348             if(sizeof($subsiteIDs) > 1) user_error("Multiple subsites match '$host'", E_USER_WARNING);
    349             return $subsiteIDs[0];
    350         }
    351
    352         // Check for a 'default' subsite
    353         if ($default = DataObject::get_one('Subsite', "\"DefaultSite\" = 1")) {
    354             return $default->ID;
    355         }
    356
    357         // Default subsite id = 0, the main site
    358         return 0;
    359     }
    360
    361     function getMembersByPermission($permissionCodes = array('ADMIN')){
    362         if(!is_array($permissionCodes))
    363             user_error('Permissions must be passed to Subsite::getMembersByPermission as an array', E_USER_ERROR);
    364         $SQL_permissionCodes = Convert::raw2sql($permissionCodes);
    365
    366         $SQL_permissionCodes = join("','", $SQL_permissionCodes);
    367
    368         return DataObject::get(
    369             'Member',
    370             "\"Group\".\"SubsiteID\" = $this->ID AND \"Permission\".\"Code\" IN ('$SQL_permissionCodes')",
    371             '',
    372             "LEFT JOIN \"Group_Members\" ON \"Member\".\"ID\" = \"Group_Members\".\"MemberID\"
    373             LEFT JOIN \"Group\" ON \"Group\".\"ID\" = \"Group_Members\".\"GroupID\"
    374             LEFT JOIN \"Permission\" ON \"Permission\".\"GroupID\" = \"Group\".\"ID\""
    375         );
    376
    377     }
    378
    379     static function hasMainSitePermission($member = null, $permissionCodes = array('ADMIN')) {
    380         if(!is_array($permissionCodes))
    381             user_error('Permissions must be passed to Subsite::hasMainSitePermission as an array', E_USER_ERROR);
    382
    383         if(!$member && $member !== FALSE) $member = Member::currentMember();
    384
    385         if(!$member) return false;
    386
    387         if(!in_array("ADMIN", $permissionCodes)) $permissionCodes[] = "ADMIN";
    388
    389         $SQLa_perm = Convert::raw2sql($permissionCodes);
    390         $SQL_perms = join("','", $SQLa_perm);
    391         $memberID = (int)$member->ID;
    392
    393         $groupCount = DB::query("
    394             SELECT COUNT(\"Permission\".\"ID\")
    395             FROM \"Permission\"
    396             INNER JOIN \"Group\" ON \"Group\".\"ID\" = \"Permission\".\"GroupID\" AND \"Group\".\"AccessAllSubsites\" = 1
    397             INNER JOIN \"Group_Members\" ON \"Group_Members\".\"GroupID\" = \"Permission\".\"GroupID\"
    398             WHERE \"Permission\".\"Code\" IN ('$SQL_perms')
    399             AND \"MemberID\" = {$memberID}
    400         ")->value();
    401
    402         return ($groupCount > 0);
    403
    404     }
    405
    406     /**
    407      * Duplicate this subsite
    408      */
    409     function duplicate() {
    410         $newTemplate = parent::duplicate();
    411
    412         $oldSubsiteID = Session::get('SubsiteID');
    413         self::changeSubsite($this->ID);
    414
    415         /*
    416          * Copy data from this template to the given subsite. Does this using an iterative depth-first search.
    417          * This will make sure that the new parents on the new subsite are correct, and there are no funny
    418          * issues with having to check whether or not the new parents have been added to the site tree
    419          * when a page, etc, is duplicated
    420          */
    421         $stack = array(array(0,0));
    422         while(count($stack) > 0) {
    423             list($sourceParentID, $destParentID) = array_pop($stack);
    424
    425             $children = Versioned::get_by_stage('Page', 'Live', "\"ParentID\" = $sourceParentID", '');
    426
    427             if($children) {
    428                 foreach($children as $child) {
    429                     $childClone = $child->duplicateToSubsite($newTemplate, false);
    430                     $childClone->ParentID = $destParentID;
    431                     $childClone->writeToStage('Stage');
    432                     $childClone->publish('Stage', 'Live');
    433                     array_push($stack, array($child->ID, $childClone->ID));
    434                 }
    435             }
    436         }
    437
    438         self::changeSubsite($oldSubsiteID);
    439
    440         return $newTemplate;
    441     }
    442
    443
    444     /**
    445      * Return the subsites that the current user can access.
    446      * Look for one of the given permission codes on the site.
    447      *
    448      * Sites and Templates will only be included if they have a Title
    449      *
    450      * @param $permCode array|string Either a single permission code or an array of permission codes.
    451      * @param $includeMainSite If true, the main site will be included if appropriate.
    452      * @param $mainSiteTitle The label to give to the main site
    453      */
    454     function accessible_sites($permCode, $includeMainSite = false, $mainSiteTitle = "Main site", $member = null) {
    455         // Rationalise member arguments
    456         if(!$member) $member = Member::currentUser();
    457         if(!$member) return new DataObjectSet();
    458         if(!is_object($member)) $member = DataObject::get_by_id('Member', $member);
    459
    460         if(is_array($permCode)) $SQL_codes = "'" . implode("', '", Convert::raw2sql($permCode)) . "'";
    461         else $SQL_codes = "'" . Convert::raw2sql($permCode) . "'";
    462
    463         $templateClassList = "'" . implode("', '", ClassInfo::subclassesFor("Subsite_Template")) . "'";
    464
    465         $subsites = DataObject::get(
    466             'Subsite',
    467             "\"Subsite\".\"Title\" != ''",
    468             '',
    469             "LEFT JOIN \"Group_Subsites\"
    470                 ON \"Group_Subsites\".\"SubsiteID\" = \"Subsite\".\"ID\"
    471             INNER JOIN \"Group\" ON \"Group\".\"ID\" = \"Group_Subsites\".\"GroupID\"
    472                 OR \"Group\".\"AccessAllSubsites\" = 1
    473             INNER JOIN \"Group_Members\"
    474                 ON \"Group_Members\".\"GroupID\"=\"Group\".\"ID\"
    475                 AND \"Group_Members\".\"MemberID\" = $member->ID
    476             INNER JOIN \"Permission\"
    477                 ON \"Group\".\"ID\"=\"Permission\".\"GroupID\"
    478                 AND \"Permission\".\"Code\" IN ($SQL_codes, 'ADMIN')"
    479         );
    480
    481         $rolesSubsites = DataObject::get(
    482             'Subsite',
    483             "\"Subsite\".\"Title\" != ''",
    484             '',
    485             "LEFT JOIN \"Group_Subsites\"
    486                 ON \"Group_Subsites\".\"SubsiteID\" = \"Subsite\".\"ID\"
    487             INNER JOIN \"Group\" ON \"Group\".\"ID\" = \"Group_Subsites\".\"GroupID\"
    488                 OR \"Group\".\"AccessAllSubsites\" = 1
    489             INNER JOIN \"Group_Members\"
    490                 ON \"Group_Members\".\"GroupID\"=\"Group\".\"ID\"
    491                 AND \"Group_Members\".\"MemberID\" = $member->ID
    492             INNER JOIN \"Group_Roles\"
    493                 ON \"Group_Roles\".\"GroupID\"=\"Group\".\"ID\"
    494             INNER JOIN \"PermissionRole\"
    495                 ON \"Group_Roles\".\"PermissionRoleID\"=\"PermissionRole\".\"ID\"
    496             INNER JOIN \"PermissionRoleCode\"
    497                 ON \"PermissionRole\".\"ID\"=\"PermissionRoleCode\".\"RoleID\"
    498                 AND \"PermissionRoleCode\".\"Code\" IN ($SQL_codes, 'ADMIN')"
    499         );
    500
    501         if(!$subsites && $rolesSubsites) return $rolesSubsites;
    502
    503         if($rolesSubsites) foreach($rolesSubsites as $subsite) {
    504             if(!$subsites->containsIDs(array($subsite->ID))) {
    505                 $subsites->push($subsite);
    506             }
    507         }
    508
    509         // Include the main site
    510         if(!$subsites) $subsites = new DataObjectSet();
    511         if($includeMainSite) {
    512             if(!is_array($permCode)) $permCode = array($permCode);
    513             if(self::hasMainSitePermission($member, $permCode)) {
    514                 $mainSite = new Subsite();
    515                 $mainSite->Title = $mainSiteTitle;
    516                 $subsites->insertFirst($mainSite);
    517             }
    518         }
    519
    520         return $subsites;
    521     }
    522
    523     /**
    524      * Write a host->domain map to assets/_host-map.php
    525      *
    526      * This is used primarily when using subsites in conjunction with StaticPublisher
    527      *
    528      * @return void
    529      */
    530     static function writeHostMap($file = null) {
    531         if (!self::$write_hostmap) return;
    532
    533         if (!$file) $file = Director::baseFolder().'/assets/_host-map.php';
    534         $hostmap = array();
    535
    536         $subsites = DataObject::get('Subsite');
    537
    538         if ($subsites) foreach($subsites as $subsite) {
    539             $domains = $subsite->Domains();
    540             if ($domains) foreach($domains as $domain) {
    541                 $hostmap[str_replace('www.', '', $domain->Domain)] = $subsite->domain();
    542             }
    543             if ($subsite->DefaultSite) $hostmap['default'] = $subsite->domain();
    544         }
    545
    546         $data = "<?php \n";
    547         $data .= "// Generated by Subsite::writeHostMap() on " . date('d/M/y') . "\n";
    548         $data .= '$subsiteHostmap = ' . var_export($hostmap, true) . ';';
    549
    550         if (is_writable(dirname($file)) || is_writable($file)) {
    551             file_put_contents($file, $data);
    552         }
    553     }
    554
    555     //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    556     // CMS ADMINISTRATION HELPERS
    557     //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    558
    559     /**
    560      * Return the FieldSet that will build the search form in the CMS
    561      */
    562     function adminSearchFields() {
    563         return new FieldSet(
    564             new TextField('Name', 'Sub-site name')
    565         );
    566     }
    567
    568     function providePermissions() {
    569         return array(
    570             'SUBSITE_ASSETS_CREATE_SUBSITE' => array(
    571                 'name' => _t('Subsite.MANAGE_ASSETS', 'Manage assets for subsites'),
    572                 'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
    573                 'help' => _t('Subsite.MANAGE_ASSETS_HELP', 'Ability to select the subsite to which an asset folder belongs. Requires "Access to Files & Images."'),
    574                 'sort' => 300
    575             )
    576         );
    577     }
    578
    579     static function get_from_all_subsites($className, $filter = "", $sort = "", $join = "", $limit = "") {
    580         $oldState = self::$disable_subsite_filter;
    581         self::$disable_subsite_filter = true;
    582         $result = DataObject::get($className, $filter, $sort, $join, $limit);
    583         self::$disable_subsite_filter = $oldState;
    584         return $result;
    585     }
    586
    587     /**
    588      * Disable the sub-site filtering; queries will select from all subsites
    589      */
    590     static function disable_subsite_filter($disabled = true) {
    591         self::$disable_subsite_filter = $disabled;
    592     }
    593
    594     function importDomain($domain, $record) {
    595         if (!$this->ID) {
    596             $this->write();
    597         }
    598         if (!$d = DataObject::get_one('SubsiteDomain', "Domain='" . Convert::raw2sql($domain) . "'")) {
    599             $d = new SubsiteDomain();
    600         }
    601         $d->Domain = $domain;
    602         $d->IsPrimary = 1;
    603         $d->SubsiteID = $this->ID;
    604         $d->write();
    605     }
    606 }
    607
    608 /**
    609  * An instance of subsite that can be duplicated to provide a quick way to create new subsites.
    610  *
    611  * @package subsites
    612  */
    613 class Subsite_Template extends Subsite {
    614     /**
    615      * Create an instance of this template, with the given title & domain
    616      */
    617     function createInstance($title, $domain = null) {
    618         $intranet = Object::create('Subsite');
    619         $intranet->Title = $title;
    620         $intranet->TemplateID = $this->ID;
    621         $intranet->write();
    622
    623         if($domain) {
    624             $intranetDomain = Object::create('SubsiteDomain');
    625             $intranetDomain->SubsiteID = $intranet->ID;
    626             $intranetDomain->Domain = $domain;
    627             $intranetDomain->write();
    628         }
    629
    630         $oldSubsiteID = Session::get('SubsiteID');
    631         self::changeSubsite($this->ID);
    632
    633         /*
    634          * Copy site content from this template to the given subsite. Does this using an iterative depth-first search.
    635          * This will make sure that the new parents on the new subsite are correct, and there are no funny
    636          * issues with having to check whether or not the new parents have been added to the site tree
    637          * when a page, etc, is duplicated
    638          */
    639         $stack = array(array(0,0));
    640         while(count($stack) > 0) {
    641             list($sourceParentID, $destParentID) = array_pop($stack);
    642
    643             $children = Versioned::get_by_stage('SiteTree', 'Live', "\"ParentID\" = $sourceParentID", '');
    644
    645             if($children) {
    646                 foreach($children as $child) {
    647                     $childClone = $child->duplicateToSubsite($intranet);
    648                     $childClone->ParentID = $destParentID;
    649                     $childClone->writeToStage('Stage');
    650                     $childClone->publish('Stage', 'Live');
    651                     array_push($stack, array($child->ID, $childClone->ID));
    652                 }
    653             }
    654         }
    655
    656         self::changeSubsite($oldSubsiteID);
    657
    658         return $intranet;
    659     }
    660 }
    661 ?>
    662

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -