"|(objectclass=inetorgperson)(objectclass=organizationalperson)(objectclass=person)(objectclass=user)", "group" => "|(objectclass=posixgroup)(objectclass=group)(objectclass=groupofuniquenames)", "department" => "|(objectclass=organizationalunit)" ]; private $arrayAttributes = [ "ldap" => ["uid" => "uid", "member" => "memberuid"], //OpenLDAP "ad" => ["uid" => "samaccountname", "member" => "member"], //Active Directory "ds" => ["uid" => "uid", "member" => "uniquemember"] //389 DS ]; private $arrayAttributesForUser = ["dn", "uid", "samaccountname", "givenname", "sn", "cn", "mail", "userprincipalname", "useraccountcontrol", "accountexpires", "manager"]; public function ldapConnection($authSourceData) { $pass = explode('_', $authSourceData['AUTH_SOURCE_PASSWORD']); // Removing sensitive data $loggableAuthSource = $authSourceData; unset($loggableAuthSource['AUTH_SOURCE_PASSWORD']); foreach ($pass as $index => $value) { if ($value == '2NnV3ujj3w') { $authSourceData['AUTH_SOURCE_PASSWORD'] = G::decrypt($pass[0], $authSourceData['AUTH_SOURCE_SERVER_NAME']); } } $ldapcnn = ldap_connect($authSourceData['AUTH_SOURCE_SERVER_NAME'], $authSourceData['AUTH_SOURCE_PORT']); $this->stdLog($ldapcnn, 'ldap_connect', $loggableAuthSource); $ldapServer = $authSourceData['AUTH_SOURCE_SERVER_NAME'] . ':' . $authSourceData['AUTH_SOURCE_PORT']; ldap_set_option($ldapcnn, LDAP_OPT_PROTOCOL_VERSION, 3); $this->stdLog($ldapcnn, 'ldap_set_option', $loggableAuthSource); ldap_set_option($ldapcnn, LDAP_OPT_REFERRALS, 0); $this->stdLog($ldapcnn, 'ldap_set_option', $loggableAuthSource); $resultLDAPStartTLS = true; if (isset($authSourceData['AUTH_SOURCE_ENABLED_TLS']) && $authSourceData['AUTH_SOURCE_ENABLED_TLS']) { $resultLDAPStartTLS = @ldap_start_tls($ldapcnn); $this->stdLog($ldapcnn, 'ldap_start_tls', $loggableAuthSource); $ldapServer = 'TLS ' . $ldapServer; } if ($authSourceData['AUTH_ANONYMOUS'] == '1') { $bBind = ldap_bind($ldapcnn); $this->log($ldapcnn, 'bind $ldapServer like anonymous user'); } else { $bBind = ldap_bind($ldapcnn, $authSourceData['AUTH_SOURCE_SEARCH_USER'], $authSourceData['AUTH_SOURCE_PASSWORD']); $this->log($ldapcnn, 'bind $ldapServer with user ' . $loggableAuthSource['AUTH_SOURCE_SEARCH_USER']); } $this->stdLog($ldapcnn, 'ldap_bind', $loggableAuthSource); $this->getDiagnosticMessage($ldapcnn); if (!$bBind) { $message = 'Unable to bind to server: ' . $ldapServer . 'LDAP-Errno: ' . ldap_errno($ldapcnn) . ' : ' . ldap_error($ldapcnn) . " \n"; throw new Exception($message); } return ['connection' =>$ldapcnn, 'startTLS' => $resultLDAPStartTLS]; } public function searchGroups() { try { $arrayGroup = []; $rbac = RBAC::getSingleton(); if (is_null($rbac->authSourcesObj)) { $rbac->authSourcesObj = new AuthenticationSource(); } $arrayAuthenticationSourceData = $rbac->authSourcesObj->load($this->authSourceUid); if (is_null($this->ldapcnn)) { $ldapcnn = $this->ldapConnection($arrayAuthenticationSourceData); $this->ldapcnn = $ldapcnn['connection']; } $ldapcnn = $this->ldapcnn; // Get Groups $limit = $this->getPageSizeLimitByData($arrayAuthenticationSourceData); $flagError = false; $filter = '(' . $this->arrayObjectClassFilter['group'] . ')'; $this->log($ldapcnn, 'search groups with Filter: ' . $filter); $cookie = ''; do { $searchResult = @ldap_search( $ldapcnn, $arrayAuthenticationSourceData['AUTH_SOURCE_BASE_DN'], $filter, ['dn', 'cn'], 0, -1, -1, LDAP_DEREF_NEVER, [['oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => ['size' => $limit, 'cookie' => $cookie]]] ); ldap_parse_result($ldapcnn, $searchResult, $errcode, $matcheddn, $errmsg, $referrals, $controls); $this->stdLog($ldapcnn, "ldap_search", ["filter" => $filter, "attributes" => ['dn', 'cn']]); $context = [ "baseDN" => $arrayAuthenticationSourceData['AUTH_SOURCE_BASE_DN'], "filter" => $filter, "attributes" => ['dn', 'cn'] ]; $this->stdLog($ldapcnn, "ldap_search", $context); if ($error = ldap_errno($ldapcnn)) { $this->log($ldapcnn, 'Error in Search'); $flagError = true; } else { if ($searchResult) { //Get groups from the ldap entries $countEntries = ldap_count_entries($ldapcnn, $searchResult); $this->stdLog($ldapcnn, "ldap_count_entries"); if ($countEntries > 0) { $entry = ldap_first_entry($ldapcnn, $searchResult); $this->stdLog($ldapcnn, "ldap_first_entry"); do { $arrayEntryData = $this->ldapGetAttributes($ldapcnn, $entry); if (isset($arrayEntryData['cn']) && !is_array($arrayEntryData['cn'])) { $arrayGroup[] = [ 'dn' => $arrayEntryData['dn'], 'cn' => trim($arrayEntryData['cn']), 'users' => 0, ]; } } while ($entry = ldap_next_entry($ldapcnn, $entry)); } } } if (!$flagError) { if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { // You need to pass the cookie from the last call to the next one $cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; } else { $cookie = ''; } } // Empty cookie means last page } while (!empty($cookie) && !$flagError); $str = ''; foreach ($arrayGroup as $group) { $str .= ' ' . $group['cn']; } $this->log($ldapcnn, 'found ' . count($arrayGroup) . ' groups: ' . $str); return $arrayGroup; } catch (Exception $e) { throw $e; } } public function searchDepartments() { try { $arrayDepartment = []; $filters = ['conditions' => ['AUTH_SOURCE_UID'=> $this->authSourceUid]]; $rbacAuthenticationSource = new RbacAuthenticationSource(); $authSourceReturn = $rbacAuthenticationSource->show($filters); $authenticationSourceData = $authSourceReturn['data'][0]; if (is_null($this->ldapcnn)) { $ldapcnn = $this->ldapConnection($authenticationSourceData); $this->ldapcnn = $ldapcnn['connection']; } $this->terminatedOu = $attributes['AUTH_SOURCE_RETIRED_OU'] ?? ''; $ldapcnn = $this->ldapcnn; //Get Departments $limit = $this->getPageSizeLimitByData($authenticationSourceData); $flagError = false; $filter = '(' . $this->arrayObjectClassFilter['department'] . ')'; $this->log($ldapcnn, 'search Departments with Filter: ' . $filter); $unitsBase = $this->custom_ldap_explode_dn($authenticationSourceData['AUTH_SOURCE_BASE_DN']); $cookie = ''; do { $searchResult = @ldap_search( $ldapcnn, $authenticationSourceData['AUTH_SOURCE_BASE_DN'], $filter, ['dn', 'ou'], 0, -1, -1, LDAP_DEREF_NEVER, [['oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => ['size' => $limit, 'cookie' => $cookie]]] ); ldap_parse_result($ldapcnn, $searchResult, $errcode, $matcheddn, $errmsg, $referrals, $controls); $this->stdLog($ldapcnn, "ldap_search", ["filter" => $filter, "attributes" => ['dn', 'ou']]); $context = [ "baseDN" => $authenticationSourceData['AUTH_SOURCE_BASE_DN'], "filter" => $filter, "attributes" => ['dn', 'ou'] ]; $this->stdLog($ldapcnn, "ldap_search", $context); if ($error = ldap_errno($ldapcnn)) { $this->log($ldapcnn, 'Error in Search'); $flagError = true; } else { if ($searchResult) { //The first node is root if (empty($arrayDepartment)) { $arrayDepartment[] = [ 'dn' => $authenticationSourceData['AUTH_SOURCE_BASE_DN'], 'parent' => '', 'ou' => 'ROOT', 'users' => 0 ]; } //Get departments from the ldap entries if (ldap_count_entries($ldapcnn, $searchResult) > 0) { $this->stdLog($ldapcnn, "ldap_count_entries"); $entry = ldap_first_entry($ldapcnn, $searchResult); $this->stdLog($ldapcnn, "ldap_first_entry", $context); do { $arrayEntryData = $this->ldapGetAttributes($ldapcnn, $entry); $unitsEqual = $this->custom_ldap_explode_dn($arrayEntryData['dn']); if (count($unitsEqual) == 1 && $unitsEqual[0] == '') { continue; } if (count($unitsEqual) > count($unitsBase)) { unset($unitsEqual[0]); } if (isset($arrayEntryData['ou']) && !is_array($arrayEntryData['ou'])) { $arrayDepartment[] = [ 'dn' => $arrayEntryData['dn'], 'parent' => (isset($unitsEqual[1])) ? implode(',', $unitsEqual) : '', 'ou' => trim($arrayEntryData['ou']), 'users' => 0 ]; } } while ($entry = ldap_next_entry($ldapcnn, $entry)); } } } if (!$flagError) { if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { // You need to pass the cookie from the last call to the next one $cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; } else { $cookie = ''; } } // Empty cookie means last page } while (!empty($cookie) && !$flagError); $str = ''; foreach ($arrayDepartment as $dep) { $str .= ' ' . $dep['ou']; } $this->log($ldapcnn, 'found ' . count($arrayDepartment) . ' departments: ' . $str); return $arrayDepartment; } catch (Exception $e) { throw $e; } } public function getPageSizeLimit($ldapcnn, $baseDn = '') { try { $limit = 1000; if ($ldapcnn === false) { return $limit; } $searchResult = @ldap_search($ldapcnn, $baseDn, '(|(objectclass=*))', ['dn']); $context = [ 'baseDN' => $baseDn, 'filter' => '(|(objectclass=*))', 'attributes' => ['dn'] ]; $this->stdLog($ldapcnn, 'ldap_search', $context); if ($searchResult) { $countEntries = ldap_count_entries($ldapcnn, $searchResult); $this->stdLog($ldapcnn, 'ldap_count_entries'); if ($countEntries > 0) { $limit = ($countEntries > $limit) ? $limit : $countEntries; } } return $limit; } catch (Exception $e) { throw $e; } } private function getDiagnosticMessage($linkIdentifier) { //specific message $keysError = [ [ 'key' => 'USER_NOT_FOUND', 'code' => 525, 'message' => G::LoadTranslation('ID_LDAP_USER_NOT_FOUND_INVALID'), ], [ 'key' => 'NOT_PERMITTED_TO_LOGON_AT_THIS_TIME', 'code' => 530, 'message' => G::LoadTranslation('ID_LDAP_NOT_PERMITTED_TO_LOGON_AT_THIS_TIME'), ], [ 'key' => 'RESTRICTED_TO_SPECIFIC_MACHINES', 'code' => 531, 'message' => G::LoadTranslation('ID_LDAP_RESTRICTED_TO_SPECIFIC_MACHINES'), ], [ 'key' => 'PASSWORD_EXPIRED', 'code' => 532, 'message' => G::LoadTranslation('ID_LDAP_PASSWORD_EXPIRED'), ], [ 'key' => 'ACCOUNT_DISABLED', 'code' => 533, 'message' => G::LoadTranslation('ID_LDAP_ACCOUNT_DISABLED'), ], [ 'key' => 'ACCOUNT_EXPIRED', 'code' => 701, 'message' => G::LoadTranslation('ID_LDAP_ACCOUNT_EXPIRED'), ], [ 'key' => 'USER_MUST_RESET_PASSWORD', 'code' => 773, 'message' => G::LoadTranslation('ID_LDAP_USER_MUST_RESET_PASSWORD'), ] ]; $message = ''; ldap_get_option($linkIdentifier, LDAP_OPT_DIAGNOSTIC_MESSAGE, $messageError); $this->stdLog($linkIdentifier, 'ldap_get_option', ['error' => $messageError]); foreach ($keysError as $key => $value) { if (strpos($messageError, (string) $value['code']) !== false) { $message = $value['message']; break; } } //standard message if (empty($message)) { $errorNumber = ldap_errno($linkIdentifier); $message = ldap_err2str($errorNumber) . '.'; } if (empty($message)) { $message = G::LoadTranslation('ID_LDAP_ERROR_CONNECTION'); } Cache::put('ldapMessageError', $message, 120); //laravel 8.x the time parameter is in seconds. $this->log($linkIdentifier, $messageError); } private function log($link, $text) { $logFile = PATH_DATA . 'logs/ldap.log'; if (!file_exists($logFile)) { file_put_contents($logFile, "Start\n"); @chmod($logFile, 0644); } if (!is_writable($logFile)) { error_log('Log file is not writable: ' . $logFile); throw new Exception('Log file is not writable: ' . $logFile); } $fpt = fopen($logFile, 'a'); $ldapErrorMsg = ''; $ldapErrorNr = 0; if ($link != null) { $ldapErrorNr = ldap_errno($link); if ($ldapErrorNr != 0) { $ldapErrorMsg = ldap_error($link); $text = $ldapErrorMsg . ' : ' . $text; } } // Log format: date hour ipaddress workspace ldapErrorNr fwrite($fpt, sprintf("%s %s %s %s %s \n", date('Y-m-d H:i:s'), getenv('REMOTE_ADDR'), config('system.workspace'), $ldapErrorNr, $text)); fclose($fpt); } private function stdLog($link, $message = "", $context = [], $level = "info") { try { if (empty($link)) { switch ($level) { case "error": Log::channel(':ldapAdvanced')->error($message, Bootstrap::context($context)); break; case "info": default: Log::channel(':ldapAdvanced')->info($message, Bootstrap::context($context)); break; } return; } $code = ldap_errno($link); $detail = ldap_err2str($code); $context["detail"] = $detail; if ($code === 0) { Log::channel(':ldapAdvanced')->info($message, Bootstrap::context($context)); } else { Log::channel(':ldapAdvanced')->error($message, Bootstrap::context($context)); } } catch (Exception $exception) { return ['success' => false, 'message' => $exception->getMessage()]; } } public function searchUsersLdap($keyword, $start = null, $limit = null) { $arrayUser = []; $totalUser = 0; $countUser = 0; $paged = !is_null($start) && !is_null($limit); $filters = [ 'conditions' => ['AUTH_SOURCE_UID' => $this->authSourceUid], ]; $rbacAuthenticationSource = new RbacAuthenticationSource(); $authSourceReturn = $rbacAuthenticationSource->show($filters); $arrayAuthenticationSourceData = $authSourceReturn['data'][0]; $arrayAuthenticationSourceData['AUTH_SOURCE_DATA'] = json_decode($arrayAuthenticationSourceData['AUTH_SOURCE_DATA'], true); $attributeUserSet = []; $attributeSetAdd = []; if ( isset($arrayAuthenticationSourceData['AUTH_SOURCE_DATA']['AUTH_SOURCE_GRID_ATTRIBUTE']) && !empty($arrayAuthenticationSourceData['AUTH_SOURCE_DATA']['AUTH_SOURCE_GRID_ATTRIBUTE']) ) { foreach ($arrayAuthenticationSourceData['AUTH_SOURCE_DATA']['AUTH_SOURCE_GRID_ATTRIBUTE'] as $value) { $attributeSetAdd[] = $value['attributeLdap']; $attributeUserSet[$value['attributeUser']] = $value['attributeLdap']; } } $ldapcnn = $this->ldapConnection($arrayAuthenticationSourceData); $ldapcnn = $ldapcnn['connection']; //Get Users if (!isset($arrayAuthenticationSourceData['AUTH_SOURCE_DATA']['AUTH_SOURCE_USERS_FILTER'])) { $arrayAuthenticationSourceData['AUTH_SOURCE_DATA']['AUTH_SOURCE_USERS_FILTER'] = ''; } $uidUserIdentifier = (isset($arrayAuthenticationSourceData['AUTH_SOURCE_DATA']['AUTH_SOURCE_IDENTIFIER_FOR_USER'])) ? $arrayAuthenticationSourceData['AUTH_SOURCE_DATA']['AUTH_SOURCE_IDENTIFIER_FOR_USER'] : 'uid'; $filterUsers = trim($arrayAuthenticationSourceData['AUTH_SOURCE_DATA']['AUTH_SOURCE_USERS_FILTER']); $filter = ($filterUsers != '') ? $filterUsers : '(' . $this->arrayObjectClassFilter['user'] . ')'; $filter = "(&$filter(|(dn=$keyword)(uid=$keyword)(samaccountname=$keyword)(givenname=$keyword)(sn=$keyword)(cn=$keyword)(mail=$keyword)(userprincipalname=$keyword)))"; $oSearch = @ldap_search($ldapcnn, $arrayAuthenticationSourceData['AUTH_SOURCE_BASE_DN'], $filter, array_merge($this->arrayAttributesForUser, $attributeSetAdd)); $context = [ 'baseDN' => $arrayAuthenticationSourceData['AUTH_SOURCE_BASE_DN'], 'filter' => $filter, 'attribute' => array_merge($this->arrayAttributesForUser, $attributeSetAdd) ]; $this->stdLog($ldapcnn, 'ldap_search', $context); if ($oError = ldap_errno($ldapcnn)) { $this->log($ldapcnn, 'Error in Search users'); } else { if ($oSearch) { $entries = ldap_count_entries($ldapcnn, $oSearch); $this->stdLog($ldapcnn, 'ldap_count_entries'); $totalUser = $entries; if ($entries > 0) { $oEntry = ldap_first_entry($ldapcnn, $oSearch); $this->stdLog($ldapcnn, 'ldap_first_entry'); $countEntries = 0; $flagNextRecord = true; do { $aAttr = $this->ldapGetAttributes($ldapcnn, $oEntry); $sUsername = (isset($aAttr[$uidUserIdentifier])) ? $aAttr[$uidUserIdentifier] : ''; if ((is_array($sUsername) && !empty($sUsername)) || trim($sUsername) != '') { $countUser++; /* Active Directory userAccountControl Values Normal Day to Day Values: 512 - Enable Account 514 - Disable account 544 - Account Enabled - Require user to change password at first logon 4096 - Workstation/server 66048 - Enabled, password never expires 66050 - Disabled, password never expires 262656 - Smart Card Logon Required 532480 - Domain controller 1 - script 2 - accountdisable 8 - homedir_required 16 - lockout 32 - passwd_notreqd 64 - passwd_cant_change 128 - encrypted_text_pwd_allowed 256 - temp_duplicate_account 512 - normal_account 2048 - interdomain_trust_account 4096 - workstation_trust_account 8192 - server_trust_account 65536 - dont_expire_password 131072 - mns_logon_account 262144 - smartcard_required 524288 - trusted_for_delegation 1048576 - not_delegated 2097152 - use_des_key_only 4194304 - dont_req_preauth 8388608 - password_expired 16777216 - trusted_to_auth_for_delegation */ $userCountControl = ''; //Active Directory, openLdap if (isset($aAttr['useraccountcontrol'])) { switch ($aAttr['useraccountcontrol']) { case '512': case '544': case '66048': case '66080': $userCountControl = 'ACTIVE'; break; case '514': case '546': case '66050': case '66082': case '2': case '16': case '8388608': default: $userCountControl = 'INACTIVE'; break; } } //apache ldap if (isset($aAttr['status'])) { $userCountControl = strtoupper($aAttr['status']); } $aUserAttributes = []; foreach ($attributeUserSet as $key => $value) { if ($key == 'USR_STATUS') { $aUserAttributes[$key] = ($userCountControl != '') ? $userCountControl : 'ACTIVE'; } elseif (isset($aAttr[$value])) { $aUserAttributes[$key] = $aAttr[$value]; } } if ($paged) { if ($countUser - 1 <= $start + $limit - 1) { if ($start <= $countUser - 1) { $arrayUser[] = array_merge($this->getUserDataFromAttribute($sUsername, $aAttr), $aUserAttributes); } } else { $flagNextRecord = false; } } else { $arrayUser[] = array_merge($this->getUserDataFromAttribute($sUsername, $aAttr), $aUserAttributes); } $countEntries++; } } while (($oEntry = ldap_next_entry($ldapcnn, $oEntry)) && $flagNextRecord); } } } return ($paged) ? ['numRecTotal' => $totalUser, 'data' => $arrayUser] : $arrayUser; } private function getUserDataFromAttribute($username, array $arrayAttributes) { try { $keyMail = (isset($arrayAttributes['mail'])) ? 'mail' : ((isset($arrayAttributes['userprincipalname'])) ? 'userprincipalname' : 'nomail'); return [ 'sUsername' => trim((is_array($username)) ? $username[0] : $username), 'sPassword' => trim((isset($arrayAttributes['userpassword'])) ? ((is_array($arrayAttributes['userpassword'])) ? $arrayAttributes['userpassword'][0] : $arrayAttributes['userpassword']) : ''), 'sFullname' => trim((isset($arrayAttributes['cn'])) ? ((is_array($arrayAttributes['cn'])) ? $arrayAttributes['cn'][0] : $arrayAttributes['cn']) : ''), 'sFirstname' => trim((isset($arrayAttributes['givenname'])) ? ((is_array($arrayAttributes['givenname'])) ? $arrayAttributes['givenname'][0] : $arrayAttributes['givenname']) : ''), 'sLastname' => trim((isset($arrayAttributes['sn'])) ? ((is_array($arrayAttributes['sn'])) ? $arrayAttributes['sn'][0] : $arrayAttributes['sn']) : ''), 'sEmail' => trim((isset($arrayAttributes[$keyMail])) ? ((is_array($arrayAttributes[$keyMail])) ? $arrayAttributes[$keyMail][0] : $arrayAttributes[$keyMail]) : ''), 'sDN' => trim($arrayAttributes['dn']), 'sManagerDN' => trim((isset($arrayAttributes['manager'])) ? ((is_array($arrayAttributes['manager'])) ? $arrayAttributes['manager'][0] : $arrayAttributes['manager']) : '') ]; } catch (Exception $e) { throw $e; } } private function getPageSizeLimitByData(array $arrayAuthSourceData) { if (isset($arrayAuthSourceData['AUTH_SOURCE_DATA']['LDAP_PAGE_SIZE_LIMIT'])) { return $arrayAuthSourceData['AUTH_SOURCE_DATA']['LDAP_PAGE_SIZE_LIMIT']; } else { return $this->getPageSizeLimit(false); } } private function ldapGetAttributes($ldapcnn, $entry) { try { $arrayAttributes = []; $arrayAttributes['dn'] = ldap_get_dn($ldapcnn, $entry); $this->stdLog($ldapcnn, 'ldap_get_dn'); $arrayAux = ldap_get_attributes($ldapcnn, $entry); $this->stdLog($ldapcnn, 'ldap_get_attributes'); for ($i = 0; $i <= $arrayAux['count'] - 1; $i++) { $key = strtolower($arrayAux[$i]); switch ($arrayAux[$arrayAux[$i]]['count']) { case 0: $arrayAttributes[$key] = ''; break; case 1: $arrayAttributes[$key] = $arrayAux[$arrayAux[$i]][0]; break; default: $arrayAttributes[$key] = $arrayAux[$arrayAux[$i]]; unset($arrayAttributes[$key]['count']); break; } } if (!isset($arrayAttributes['mail']) && isset($arrayAttributes['userprincipalname'])) { $arrayAttributes['mail'] = $arrayAttributes['userprincipalname']; } return $arrayAttributes; } catch (Exception $e) { throw $e; } } public function custom_ldap_explode_dn($dn) { $dn = trim($dn, ','); $result = ldap_explode_dn($dn, 0); $this->stdLog(null, "ldap_explode_dn", ["dn" => $dn]); if (is_array($result)) { unset($result['count']); foreach ($result as $key => $value) { $result[$key] = addcslashes(preg_replace_callback("/\\\([0-9A-Fa-f]{2})/", function ($m) { return chr(hexdec($m[1])); }, $value), '<>,"'); } } return $result; } }