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: /**
156: * @param mixed $mReturn
157: * @return mixed
158: */
159: private function validateLdapErrorOnFalse($mReturn)
160: {
161: if (false === $mReturn) {
162: if ($this->rLink) {
163: \Aurora\System\Api::Log('LDAP: error #' . @ldap_errno($this->rLink) . ': ' . @ldap_error($this->rLink), \Aurora\System\Enums\LogLevel::Error);
164: } else {
165: \Aurora\System\Api::Log('LDAP: unknown ldap error', \Aurora\System\Enums\LogLevel::Error);
166: }
167: }
168:
169: return $mReturn;
170: }
171:
172: /**
173: * @param string $sObjectFilter
174: * @return bool
175: */
176: public function Search($sObjectFilter)
177: {
178: $result = false;
179: if ($this->rSearch && $this->sLastRequest === $this->sSearchDN . $sObjectFilter) {
180: \Aurora\System\Api::Log('LDAP: search repeat = "' . $this->sSearchDN . '" / ' . $sObjectFilter);
181:
182: $this->validateLdapErrorOnFalse($this->rSearch);
183: $result = is_resource($this->rSearch) || is_object($this->rSearch);
184: } else {
185: \Aurora\System\Api::Log('LDAP: search = "' . $this->sSearchDN . '" / ' . $sObjectFilter);
186: $this->rSearch = @ldap_search($this->rLink, $this->sSearchDN, $sObjectFilter, array('*'), 0, 3000);
187:
188: $this->validateLdapErrorOnFalse($this->rSearch);
189:
190: $this->sLastRequest = $this->sSearchDN . $sObjectFilter;
191: $result = is_resource($this->rSearch) || is_object($this->rSearch);
192: }
193:
194: return $result;
195: }
196:
197: /**
198: * @return bool
199: */
200: public function Add($sNewDn, $aEntry)
201: {
202: \Aurora\System\Api::Log('ldap_add = ' . ((empty($sNewDn) ? '' : $sNewDn . ',') . $this->sSearchDN));
203: \Aurora\System\Api::LogObject($aEntry);
204:
205: $bResult = !!@ldap_add($this->rLink, (empty($sNewDn) ? '' : $sNewDn . ',') . $this->sSearchDN, $aEntry);
206: $this->validateLdapErrorOnFalse($bResult);
207:
208: return $bResult;
209: }
210:
211: /**
212: * @return bool
213: */
214: public function Delete($sDeleteDn)
215: {
216: $bResult = false;
217: if (!empty($sDeleteDn)) {
218: \Aurora\System\Api::Log('ldap_delete = ' . ($sDeleteDn . ',' . $this->sSearchDN));
219: $bResult = !!@ldap_delete($this->rLink, $sDeleteDn . ',' . $this->sSearchDN);
220: $this->validateLdapErrorOnFalse($bResult);
221: }
222:
223: return $bResult;
224: }
225:
226: /**
227: * @return bool
228: */
229: public function Modify($sModifyDn, $aModifyEntry)
230: {
231: $bResult = false;
232: if (!empty($sModifyDn)) {
233: if (!empty($this->sSearchDN)) {
234: $sModifyDn = $sModifyDn . ',' . $this->sSearchDN;
235: }
236:
237: \Aurora\System\Api::Log('ldap_modify = ' . $sModifyDn);
238: \Aurora\System\Api::LogObject($aModifyEntry);
239:
240: $bResult = !!@ldap_modify($this->rLink, $sModifyDn, $aModifyEntry);
241: $this->validateLdapErrorOnFalse($bResult);
242: }
243:
244: return $bResult;
245: }
246:
247: /**
248: * @return int
249: */
250: public function ResultCount()
251: {
252: $iResult = 0;
253:
254: $iCount = ldap_count_entries($this->rLink, $this->rSearch);
255: $this->validateLdapErrorOnFalse($iCount);
256: if (false !== $iCount) {
257: $iResult = $iCount;
258: }
259:
260: return $iResult;
261: }
262:
263: /**
264: * @return mixed
265: */
266: public function ResultItem()
267: {
268: $mResult = false;
269:
270: $aResurn = @ldap_get_entries($this->rLink, $this->rSearch);
271: $this->validateLdapErrorOnFalse($aResurn);
272: if (false !== $aResurn && isset($aResurn[0]) && is_array($aResurn[0])) {
273: $mResult = $aResurn[0];
274: }
275:
276: return $mResult;
277: }
278:
279: /**
280: * @param string $sSortField
281: * @param bool $bAsc 'asc' or 'desc'
282: * @param int $iOffset = null
283: * @param int $iRequestLimit = null
284: * @return array
285: */
286: public function SortPaginate($sSortField, $bAsc = true, $iOffset = null, $iRequestLimit = null)
287: {
288: $iTotalEntries = @ldap_count_entries($this->rLink, $this->rSearch);
289:
290: $iEnd = 0;
291: $iStart = 0;
292: if ($iOffset === null || $iRequestLimit === null) {
293: $iStart = 0;
294: $iEnd = $iTotalEntries - 1;
295: } else {
296: $iStart = $iOffset;
297: $iStart = ($iStart < 0) ? 0 : $iStart;
298:
299: $iEnd = $iStart + $iRequestLimit;
300: $iEnd = ($iEnd > $iTotalEntries) ? $iTotalEntries : $iEnd;
301: }
302:
303: if (0 < strlen($sSortField)) {
304: if (function_exists('ldap_sort')) {
305: @\ldap_sort($this->rLink, $this->rSearch, $sSortField);
306: }
307: }
308:
309: $aList = array();
310: $iCurrent = 0;
311: $rEntry = ldap_first_entry($this->rLink, $this->rSearch);
312: do {
313: if ($iCurrent >= $iStart) {
314: array_push($aList, ldap_get_attributes($this->rLink, $rEntry));
315: }
316: $rEntry = ldap_next_entry($this->rLink, $rEntry);
317: $iCurrent++;
318: } while ($iCurrent < $iEnd && (is_resource($rEntry) || is_object($rEntry)));
319:
320: return $bAsc ? $aList : array_reverse($aList);
321: }
322: }
323: