public function run($request)
 {
     // get all groups from LDAP, but only get the attributes we need.
     // this is useful to avoid holding onto too much data in memory
     // especially in the case where getGroups() would return a lot of groups
     $ldapGroups = $this->ldapService->getGroups(false, array('objectguid', 'samaccountname', 'dn', 'name', 'description'), 'objectguid');
     $start = time();
     $count = 0;
     foreach ($ldapGroups as $data) {
         $group = Group::get()->filter('GUID', $data['objectguid'])->limit(1)->first();
         if (!($group && $group->exists())) {
             // create the initial Group with some internal fields
             $group = new Group();
             $group->GUID = $data['objectguid'];
             $this->log(sprintf('Creating new Group (ID: %s, GUID: %s, sAMAccountName: %s)', $group->ID, $data['objectguid'], $data['samaccountname']));
         } else {
             $this->log(sprintf('Updating existing Group "%s" (ID: %s, GUID: %s, sAMAccountName: %s)', $group->getTitle(), $group->ID, $data['objectguid'], $data['samaccountname']));
         }
         // Synchronise specific guaranteed fields.
         $group->Code = $data['samaccountname'];
         if (!empty($data['name'])) {
             $group->Title = $data['name'];
         } else {
             $group->Title = $data['samaccountname'];
         }
         if (!empty($data['description'])) {
             $group->Description = $data['description'];
         }
         $group->DN = $data['dn'];
         $group->LastSynced = (string) SS_Datetime::now();
         $group->IsImportedFromLDAP = true;
         $group->write();
         // Mappings on this group are automatically maintained to contain just the group's DN.
         // First, scan through existing mappings and remove ones that are not matching (in case the group moved).
         $hasCorrectMapping = false;
         foreach ($group->LDAPGroupMappings() as $mapping) {
             if ($mapping->DN === $data['dn']) {
                 // This is the correct mapping we want to retain.
                 $hasCorrectMapping = true;
             } else {
                 $this->log(sprintf('Deleting invalid mapping %s on %s.', $mapping->DN, $group->getTitle()));
                 $mapping->delete();
             }
         }
         // Second, if the main mapping was not found, add it in.
         if (!$hasCorrectMapping) {
             $this->log(sprintf('Setting up missing group mapping from %s to %s', $group->getTitle(), $data['dn']));
             $mapping = new LDAPGroupMapping();
             $mapping->DN = $data['dn'];
             $mapping->write();
             $group->LDAPGroupMappings()->add($mapping);
         }
         // cleanup object from memory
         $group->destroy();
         $count++;
     }
     // remove Group records that were previously imported, but no longer exist in the directory
     // NOTE: DB::query() here is used for performance and so we don't run out of memory
     if ($this->config()->destructive) {
         foreach (DB::query('SELECT "ID", "GUID" FROM "Group" WHERE "IsImportedFromLDAP" = 1') as $record) {
             if (!isset($ldapGroups[$record['GUID']])) {
                 $group = Group::get()->byId($record['ID']);
                 // Cascade into mappings, just to clean up behind ourselves.
                 foreach ($group->LDAPGroupMappings() as $mapping) {
                     $mapping->delete();
                 }
                 $group->delete();
                 $this->log(sprintf('Removing Group "%s" (GUID: %s) that no longer exists in LDAP.', $group->Title, $group->GUID));
                 // cleanup object from memory
                 $group->destroy();
             }
         }
     }
     $end = time() - $start;
     $this->log(sprintf('Done. Processed %s records. Duration: %s seconds', $count, round($end, 0)));
 }
 /**
  * Update the current Member record with data from LDAP.
  *
  * Constraints:
  * - Member *must* be in the database before calling this as it will need the ID to be mapped to a {@link Group}.
  * - GUID of the member must have already been set, for integrity reasons we don't allow it to change here.
  *
  * @param Member
  * @param array|null $data If passed, this is pre-existing AD attribute data to update the Member with.
  *            If not given, the data will be looked up by the user's GUID.
  * @return bool
  */
 public function updateMemberFromLDAP($member, $data = null)
 {
     // don't attempt to do this if there's no LDAP configured
     if (!Config::inst()->get('LDAPGateway', 'options')) {
         return false;
     }
     if (!$member->GUID) {
         SS_Log::log(sprintf('Cannot update Member ID %s, GUID not set', $member->ID), SS_Log::WARN);
         return false;
     }
     if (!$data) {
         $data = $this->getUserByGUID($member->GUID);
         if (!$data) {
             SS_Log::log(sprintf('Could not retrieve data for user. GUID: %s', $member->GUID), SS_Log::WARN);
             return false;
         }
     }
     $member->IsExpired = ($data['useraccountcontrol'] & 2) == 2;
     $member->LastSynced = (string) SS_Datetime::now();
     $member->IsImportedFromLDAP = true;
     foreach ($member->config()->ldap_field_mappings as $attribute => $field) {
         if (!isset($data[$attribute])) {
             SS_Log::log(sprintf('Attribute %s configured in Member.ldap_field_mappings, but no available attribute in AD data (GUID: %s, Member ID: %s)', $attribute, $data['objectguid'], $member->ID), SS_Log::WARN);
             continue;
         }
         if ($attribute == 'thumbnailphoto') {
             $imageClass = $member->getRelationClass($field);
             if ($imageClass !== 'Image' && !is_subclass_of($imageClass, 'Image')) {
                 SS_Log::log(sprintf('Member field %s configured for thumbnailphoto AD attribute, but it isn\'t a valid relation to an Image class', $field), SS_Log::WARN);
                 continue;
             }
             $filename = sprintf('thumbnailphoto-%s.jpg', $data['samaccountname']);
             $path = ASSETS_DIR . '/' . $member->config()->ldap_thumbnail_path;
             $absPath = BASE_PATH . '/' . $path;
             if (!file_exists($absPath)) {
                 Filesystem::makeFolder($absPath);
             }
             // remove existing record if it exists
             $existingObj = $member->getComponent($field);
             if ($existingObj && $existingObj->exists()) {
                 $existingObj->delete();
             }
             // The image data is provided in raw binary.
             file_put_contents($absPath . '/' . $filename, $data[$attribute]);
             $record = new $imageClass();
             $record->Name = $filename;
             $record->Filename = $path . '/' . $filename;
             $record->write();
             $relationField = $field . 'ID';
             $member->{$relationField} = $record->ID;
         } else {
             $member->{$field} = $data[$attribute];
         }
     }
     // if a default group was configured, ensure the user is in that group
     if ($this->config()->default_group) {
         $group = Group::get()->filter('Code', $this->config()->default_group)->limit(1)->first();
         if (!($group && $group->exists())) {
             SS_Log::log(sprintf('LDAPService.default_group misconfiguration! There is no such group with Code = \'%s\'', $this->config()->default_group), SS_Log::WARN);
         } else {
             $group->Members()->add($member, array('IsImportedFromLDAP' => '1'));
         }
     }
     // this is to keep track of which groups the user gets mapped to
     // and we'll use that later to remove them from any groups that they're no longer mapped to
     $mappedGroupIDs = array();
     // ensure the user is in any mapped groups
     if (isset($data['memberof'])) {
         $ldapGroups = is_array($data['memberof']) ? $data['memberof'] : array($data['memberof']);
         foreach ($ldapGroups as $groupDN) {
             foreach (LDAPGroupMapping::get() as $mapping) {
                 if (!$mapping->DN) {
                     SS_Log::log(sprintf('LDAPGroupMapping ID %s is missing DN field. Skipping', $mapping->ID), SS_Log::WARN);
                     continue;
                 }
                 // the user is a direct member of group with a mapping, add them to the SS group.
                 if ($mapping->DN == $groupDN) {
                     $mapping->Group()->Members()->add($member, array('IsImportedFromLDAP' => '1'));
                     $mappedGroupIDs[] = $mapping->GroupID;
                 }
                 // the user *might* be a member of a nested group provided the scope of the mapping
                 // is to include the entire subtree. Check all those mappings and find the LDAP child groups
                 // to see if they are a member of one of those. If they are, add them to the SS group
                 if ($mapping->Scope == 'Subtree') {
                     $childGroups = $this->getNestedGroups($mapping->DN, array('dn'));
                     if (!$childGroups) {
                         continue;
                     }
                     foreach ($childGroups as $childGroupDN => $childGroupRecord) {
                         if ($childGroupDN == $groupDN) {
                             $mapping->Group()->Members()->add($member, array('IsImportedFromLDAP' => '1'));
                             $mappedGroupIDs[] = $mapping->GroupID;
                         }
                     }
                 }
             }
         }
     }
     // remove the user from any previously mapped groups, where the mapping has since been removed
     $groupRecords = DB::query(sprintf('SELECT "GroupID" FROM "Group_Members" WHERE "IsImportedFromLDAP" = 1 AND "MemberID" = %s', $member->ID));
     foreach ($groupRecords as $groupRecord) {
         if (!in_array($groupRecord['GroupID'], $mappedGroupIDs)) {
             $group = Group::get()->byId($groupRecord['GroupID']);
             // Some groups may no longer exist. SilverStripe does not clean up join tables.
             if ($group) {
                 $group->Members()->remove($member);
             }
         }
     }
     // This will throw an exception if there are two distinct GUIDs with the same email address.
     // We are happy with a raw 500 here at this stage.
     $member->write();
 }
 /**
  * Sync a specific Group by updating it with LDAP data.
  *
  * @param Group $group An existing Group or a new Group object
  * @param array $data LDAP group object data
  *
  * @return bool
  */
 public function updateGroupFromLDAP(Group $group, $data)
 {
     if (!$this->enabled()) {
         return false;
     }
     // Synchronise specific guaranteed fields.
     $group->Code = $data['samaccountname'];
     if (!empty($data['name'])) {
         $group->Title = $data['name'];
     } else {
         $group->Title = $data['samaccountname'];
     }
     if (!empty($data['description'])) {
         $group->Description = $data['description'];
     }
     $group->DN = $data['dn'];
     $group->LastSynced = (string) SS_Datetime::now();
     $group->IsImportedFromLDAP = true;
     $group->write();
     // Mappings on this group are automatically maintained to contain just the group's DN.
     // First, scan through existing mappings and remove ones that are not matching (in case the group moved).
     $hasCorrectMapping = false;
     foreach ($group->LDAPGroupMappings() as $mapping) {
         if ($mapping->DN === $data['dn']) {
             // This is the correct mapping we want to retain.
             $hasCorrectMapping = true;
         } else {
             $mapping->delete();
         }
     }
     // Second, if the main mapping was not found, add it in.
     if (!$hasCorrectMapping) {
         $mapping = new LDAPGroupMapping();
         $mapping->DN = $data['dn'];
         $mapping->write();
         $group->LDAPGroupMappings()->add($mapping);
     }
 }
 public function MappedGroups()
 {
     return LDAPGroupMapping::get();
 }