1: <?php
2: /**
3: * This code is licensed under AGPLv3 license or Afterlogic Software License
4: * if commercial version of the product was purchased.
5: * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
6: */
7:
8: namespace Aurora\System\Utils;
9:
10: /**
11: * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
12: * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
13: * @copyright Copyright (c) 2019, Afterlogic Corp.
14: */
15: class Ldap
16: {
17: /**
18: * @var \LDAP\Connection
19: */
20: private $rLink;
21:
22: /**
23: * @var \LDAP\Result
24: */
25: private $rSearch;
26:
27: /**
28: * @var string
29: */
30: private $sSearchDN;
31:
32: /**
33: * @var string
34: */
35: private $sLastRequest;
36:
37: public function __construct($sSearchDN = '')
38: {
39: $this->rLink = null;
40: $this->rSearch = null;
41: $this->sSearchDN = $sSearchDN;
42: }
43:
44: public function Escape($sStr, $bForDn = false)
45: {
46: if ($bForDn) {
47: $aMetaChars = array(',', '=', '+', '<', '>', ';', '\\', '"', '#');
48: } else {
49: $aMetaChars = array('*', '(', ')', '\\', chr(0));
50: }
51:
52: $aQuotedMetaChars = array();
53: foreach ($aMetaChars as $iKey => $sValue) {
54: $aQuotedMetaChars[$iKey] = '\\'.str_pad(dechex(ord($sValue)), 2, '0');
55: }
56:
57: return str_replace($aMetaChars, $aQuotedMetaChars, $sStr);
58: }
59:
60: /**
61: * @param string $sSearchDN
62: * @return Ldap
63: */
64: public function SetSearchDN($sSearchDN)
65: {
66: $this->sSearchDN = $sSearchDN;
67: return $this;
68: }
69:
70: /**
71: * @return string
72: */
73: public function GetSearchDN()
74: {
75: return $this->sSearchDN;
76: }
77:
78: /**
79: * @param string $sHost
80: * @param int $iPort
81: * @param string $sBindDb = ''
82: * @param string $sBindPassword = ''
83: * @param string $sHostBack = ''
84: * @param int $iPortBack = null
85: * @return bool
86: */
87: public function Connect($sHost, $iPort, $sBindDb = '', $sBindPassword = '', $sHostBack = '', $iPortBack = null)
88: {
89: if (!extension_loaded('ldap')) {
90: \Aurora\System\Api::Log('LDAP: Can\'t load LDAP extension.', \Aurora\System\Enums\LogLevel::Error);
91: return false;
92: }
93:
94: if (!(is_resource($this->rLink) || is_object($this->rLink))) {
95: \Aurora\System\Api::Log('LDAP: connect to '.$sHost.':'.$iPort);
96:
97: $rLink = ldap_connect($sHost, $iPort);
98: if ($rLink) {
99: @ldap_set_option($rLink, LDAP_OPT_PROTOCOL_VERSION, 3);
100: @ldap_set_option($rLink, LDAP_OPT_REFERRALS, 0);
101:
102: \Aurora\System\Api::Log('LDAP: bind = "'.$sBindDb.'" / "'.$sBindPassword.'"');
103: if (0 < strlen($sBindDb) && 0 < strlen($sBindPassword) ?
104: !@ldap_bind($rLink, $sBindDb, $sBindPassword) : !@ldap_bind($rLink)
105: ) {
106: $this->rLink = $rLink;
107:
108: $this->validateLdapErrorOnFalse(false);
109:
110: $this->rLink = null;
111: if (0 < strlen($sHostBack)) {
112: return $this->Connect($sHostBack, $iPortBack, $sBindDb, $sBindPassword);
113: }
114:
115: return false;
116: } else {
117: // @register_shutdown_function(array(&$this, 'Disconnect'));
118: $this->rLink = $rLink;
119: }
120: } else {
121: $this->validateLdapErrorOnFalse(false);
122: if (0 < strlen($sHostBack)) {
123: return $this->Connect($sHostBack, $iPortBack, $sBindDb, $sBindPassword);
124: }
125:
126: return false;
127: }
128: }
129:
130: return true;
131: }
132:
133: /**
134: * @param string $sBindDb
135: * @param string $sBindPassword
136: * @return bool
137: */
138: public function ReBind($sBindDb, $sBindPassword)
139: {
140: if (is_resource($this->rLink) || is_object($this->rLink)) {
141: \Aurora\System\Api::Log('LDAP: rebind '.$sBindDb);
142:
143: if (!@ldap_bind($this->rLink, $sBindDb, $sBindPassword)) {
144: $this->validateLdapErrorOnFalse(false);
145: $this->rLink = null;
146: } else {
147: return true;
148: }
149: }
150:
151: return false;
152: }
153:
154: /**
155: * @param mixed $mReturn
156: * @return mixed
157: */
158: private function validateLdapErrorOnFalse($mReturn)
159: {
160: if (false === $mReturn) {
161: if ($this->rLink) {
162: \Aurora\System\Api::Log('LDAP: error #'.@ldap_errno($this->rLink).': '.@ldap_error($this->rLink), \Aurora\System\Enums\LogLevel::Error);
163: } else {
164: \Aurora\System\Api::Log('LDAP: unknown ldap error', \Aurora\System\Enums\LogLevel::Error);
165: }
166: }
167:
168: return $mReturn;
169: }
170:
171: /**
172: * @param string $sObjectFilter
173: * @return bool
174: */
175: public function Search($sObjectFilter)
176: {
177: if ($this->rSearch && $this->sLastRequest === $this->sSearchDN.$sObjectFilter) {
178: \Aurora\System\Api::Log('LDAP: search repeat = "'.$this->sSearchDN.'" / '.$sObjectFilter);
179:
180: $this->validateLdapErrorOnFalse($this->rSearch);
181: return is_resource($this->rSearch) || is_object($this->rSearch);
182: } else {
183: \Aurora\System\Api::Log('LDAP: search = "'.$this->sSearchDN.'" / '.$sObjectFilter);
184: $this->rSearch = @ldap_search($this->rLink, $this->sSearchDN, $sObjectFilter, array('*'), 0, 3000);
185:
186: $this->validateLdapErrorOnFalse($this->rSearch);
187:
188: $this->sLastRequest = $this->sSearchDN.$sObjectFilter;
189: return is_resource($this->rSearch) || is_object($this->rSearch);
190: }
191:
192: return false;
193: }
194:
195: /**
196: * @return bool
197: */
198: public function Add($sNewDn, $aEntry)
199: {
200: \Aurora\System\Api::Log('ldap_add = '.((empty($sNewDn) ? '' : $sNewDn.',').$this->sSearchDN));
201: \Aurora\System\Api::LogObject($aEntry);
202:
203: $bResult = !!@ldap_add($this->rLink, (empty($sNewDn) ? '' : $sNewDn.',').$this->sSearchDN, $aEntry);
204: $this->validateLdapErrorOnFalse($bResult);
205:
206: return $bResult;
207: }
208:
209: /**
210: * @return bool
211: */
212: public function Delete($sDeleteDn)
213: {
214: $bResult = false;
215: if (!empty($sDeleteDn)) {
216: \Aurora\System\Api::Log('ldap_delete = '.($sDeleteDn.','.$this->sSearchDN));
217: $bResult = !!@ldap_delete($this->rLink, $sDeleteDn.','.$this->sSearchDN);
218: $this->validateLdapErrorOnFalse($bResult);
219: }
220:
221: return $bResult;
222: }
223:
224: /**
225: * @return bool
226: */
227: public function Modify($sModifyDn, $aModifyEntry)
228: {
229: $bResult = false;
230: if (!empty($sModifyDn)) {
231: if (!empty($this->sSearchDN)) {
232: $sModifyDn = $sModifyDn.','.$this->sSearchDN;
233: }
234:
235: \Aurora\System\Api::Log('ldap_modify = '.$sModifyDn);
236: \Aurora\System\Api::LogObject($aModifyEntry);
237:
238: $bResult = !!@ldap_modify($this->rLink, $sModifyDn, $aModifyEntry);
239: $this->validateLdapErrorOnFalse($bResult);
240: }
241:
242: return $bResult;
243: }
244:
245: /**
246: * @return int
247: */
248: public function ResultCount()
249: {
250: $iResult = 0;
251:
252: $iCount = ldap_count_entries($this->rLink, $this->rSearch);
253: $this->validateLdapErrorOnFalse($iCount);
254: if (false !== $iCount) {
255: $iResult = $iCount;
256: }
257:
258: return $iResult;
259: }
260:
261: /**
262: * @return mixed
263: */
264: public function ResultItem()
265: {
266: $mResult = false;
267:
268: $aResurn = @ldap_get_entries($this->rLink, $this->rSearch);
269: $this->validateLdapErrorOnFalse($aResurn);
270: if (false !== $aResurn && isset($aResurn[0]) && is_array($aResurn[0])) {
271: $mResult = $aResurn[0];
272: }
273:
274: return $mResult;
275: }
276:
277: /**
278: * @param string $sSortField
279: * @param string $bAsc 'asc' or 'desc'
280: * @param int $iOffset = null
281: * @param int $iRequestLimit = null
282: * @return array
283: */
284: public function SortPaginate($sSortField, $bAsc = true, $iOffset = null, $iRequestLimit = null)
285: {
286: $iTotalEntries = @ldap_count_entries($this->rLink, $this->rSearch);
287:
288: $iEnd = 0;
289: $iStart = 0;
290: if ($iOffset === null || $iRequestLimit === null) {
291: $iStart = 0;
292: $iEnd = $iTotalEntries - 1;
293: } else {
294: $iStart = $iOffset;
295: $iStart = ($iStart < 0) ? 0 : $iStart;
296:
297: $iEnd = $iStart + $iRequestLimit;
298: $iEnd = ($iEnd > $iTotalEntries) ? $iTotalEntries : $iEnd;
299: }
300:
301: if (0 < strlen($sSortField)) {
302: if (function_exists('ldap_sort')) {
303: @\ldap_sort($this->rLink, $this->rSearch, $sSortField);
304: }
305: }
306:
307: $aList = array();
308: $iCurrent = 0;
309: $rEntry = ldap_first_entry($this->rLink, $this->rSearch);
310: do {
311: if ($iCurrent >= $iStart) {
312: array_push($aList, ldap_get_attributes($this->rLink, $rEntry));
313: }
314: $rEntry = ldap_next_entry($this->rLink, $rEntry);
315: $iCurrent++;
316: } while ($iCurrent < $iEnd && (is_resource($rEntry) || is_object($rEntry)));
317:
318: return $bAsc ? $aList : array_reverse($aList);
319: }
320: }
321: