blob: 48f253fa837f5e2046b2228713385fcee9afd151 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace Adldap\Query;
4
5use Closure;
6use Adldap\Adldap;
7use Adldap\Utilities;
8use Adldap\Models\Model;
9use Illuminate\Support\Arr;
10use InvalidArgumentException;
11use Adldap\Schemas\ActiveDirectory;
12use Adldap\Schemas\SchemaInterface;
13use Adldap\Query\Events\QueryExecuted;
14use Adldap\Models\ModelNotFoundException;
15use Adldap\Connections\ConnectionInterface;
16
17class Builder
18{
19 /**
20 * The selected columns to retrieve on the query.
21 *
22 * @var array
23 */
24 public $columns = ['*'];
25
26 /**
27 * The query filters.
28 *
29 * @var array
30 */
31 public $filters = [
32 'and' => [],
33 'or' => [],
34 'raw' => [],
35 ];
36
37 /**
38 * The size limit of the query.
39 *
40 * @var int
41 */
42 public $limit = 0;
43
44 /**
45 * Determines whether the current query is paginated.
46 *
47 * @var bool
48 */
49 public $paginated = false;
50
51 /**
52 * The field to sort search results by.
53 *
54 * @var string
55 */
56 protected $sortByField = '';
57
58 /**
59 * The direction to sort the results by.
60 *
61 * @var string
62 */
63 protected $sortByDirection = '';
64
65 /**
66 * The sort flags for sorting query results.
67 *
68 * @var int
69 */
70 protected $sortByFlags;
71
72 /**
73 * The distinguished name to perform searches upon.
74 *
75 * @var string|null
76 */
77 protected $dn;
78
79 /**
80 * The default query type.
81 *
82 * @var string
83 */
84 protected $type = 'search';
85
86 /**
87 * Determines whether or not to return LDAP results in their raw array format.
88 *
89 * @var bool
90 */
91 protected $raw = false;
92
93 /**
94 * Determines whether the query is nested.
95 *
96 * @var bool
97 */
98 protected $nested = false;
99
100 /**
101 * Determines whether the query should be cached.
102 *
103 * @var bool
104 */
105 protected $caching = false;
106
107 /**
108 * How long the query should be cached until.
109 *
110 * @var \DateTimeInterface|null
111 */
112 protected $cacheUntil = null;
113
114 /**
115 * Determines whether the query cache must be flushed.
116 *
117 * @var bool
118 */
119 protected $flushCache = false;
120
121 /**
122 * The current connection instance.
123 *
124 * @var ConnectionInterface
125 */
126 protected $connection;
127
128 /**
129 * The current grammar instance.
130 *
131 * @var Grammar
132 */
133 protected $grammar;
134
135 /**
136 * The current schema instance.
137 *
138 * @var SchemaInterface
139 */
140 protected $schema;
141
142 /**
143 * The current cache instance.
144 *
145 * @var Cache|null
146 */
147 protected $cache;
148
149 /**
150 * Constructor.
151 *
152 * @param ConnectionInterface $connection
153 * @param Grammar|null $grammar
154 * @param SchemaInterface|null $schema
155 */
156 public function __construct(ConnectionInterface $connection, Grammar $grammar = null, SchemaInterface $schema = null)
157 {
158 $this->setConnection($connection)
159 ->setGrammar($grammar)
160 ->setSchema($schema);
161 }
162
163 /**
164 * Sets the current connection.
165 *
166 * @param ConnectionInterface $connection
167 *
168 * @return Builder
169 */
170 public function setConnection(ConnectionInterface $connection)
171 {
172 $this->connection = $connection;
173
174 return $this;
175 }
176
177 /**
178 * Sets the current filter grammar.
179 *
180 * @param Grammar|null $grammar
181 *
182 * @return Builder
183 */
184 public function setGrammar(Grammar $grammar = null)
185 {
186 $this->grammar = $grammar ?: new Grammar();
187
188 return $this;
189 }
190
191 /**
192 * Sets the current schema.
193 *
194 * @param SchemaInterface|null $schema
195 *
196 * @return Builder
197 */
198 public function setSchema(SchemaInterface $schema = null)
199 {
200 $this->schema = $schema ?: new ActiveDirectory();
201
202 return $this;
203 }
204
205 /**
206 * Returns the current schema.
207 *
208 * @return SchemaInterface
209 */
210 public function getSchema()
211 {
212 return $this->schema;
213 }
214
215 /**
216 * Sets the cache to store query results.
217 *
218 * @param Cache|null $cache
219 */
220 public function setCache(Cache $cache = null)
221 {
222 $this->cache = $cache;
223
224 return $this;
225 }
226
227 /**
228 * Returns a new Query Builder instance.
229 *
230 * @param string $baseDn
231 *
232 * @return Builder
233 */
234 public function newInstance($baseDn = null)
235 {
236 // We'll set the base DN of the new Builder so
237 // developers don't need to do this manually.
238 $dn = is_null($baseDn) ? $this->getDn() : $baseDn;
239
240 return (new static($this->connection, $this->grammar, $this->schema))
241 ->setDn($dn);
242 }
243
244 /**
245 * Returns a new nested Query Builder instance.
246 *
247 * @param Closure|null $closure
248 *
249 * @return $this
250 */
251 public function newNestedInstance(Closure $closure = null)
252 {
253 $query = $this->newInstance()->nested();
254
255 if ($closure) {
256 call_user_func($closure, $query);
257 }
258
259 return $query;
260 }
261
262 /**
263 * Returns the current query.
264 *
265 * @return Collection|array
266 */
267 public function get()
268 {
269 // We'll mute any warnings / errors here. We just need to
270 // know if any query results were returned.
271 return @$this->query($this->getQuery());
272 }
273
274 /**
275 * Compiles and returns the current query string.
276 *
277 * @return string
278 */
279 public function getQuery()
280 {
281 // We need to ensure we have at least one filter, as
282 // no query results will be returned otherwise.
283 if (count(array_filter($this->filters)) === 0) {
284 $this->whereHas($this->schema->objectClass());
285 }
286
287 return $this->grammar->compile($this);
288 }
289
290 /**
291 * Returns the unescaped query.
292 *
293 * @return string
294 */
295 public function getUnescapedQuery()
296 {
297 return Utilities::unescape($this->getQuery());
298 }
299
300 /**
301 * Returns the current Grammar instance.
302 *
303 * @return Grammar
304 */
305 public function getGrammar()
306 {
307 return $this->grammar;
308 }
309
310 /**
311 * Returns the current Connection instance.
312 *
313 * @return ConnectionInterface
314 */
315 public function getConnection()
316 {
317 return $this->connection;
318 }
319
320 /**
321 * Returns the builders DN to perform searches upon.
322 *
323 * @return string
324 */
325 public function getDn()
326 {
327 return $this->dn;
328 }
329
330 /**
331 * Sets the DN to perform searches upon.
332 *
333 * @param string|Model|null $dn
334 *
335 * @return Builder
336 */
337 public function setDn($dn = null)
338 {
339 $this->dn = $dn instanceof Model ? $dn->getDn() : $dn;
340
341 return $this;
342 }
343
344 /**
345 * Alias for setting the base DN of the query.
346 *
347 * @param string|Model|null $dn
348 *
349 * @return Builder
350 */
351 public function in($dn = null)
352 {
353 return $this->setDn($dn);
354 }
355
356 /**
357 * Sets the size limit of the current query.
358 *
359 * @param int $limit
360 *
361 * @return Builder
362 */
363 public function limit($limit = 0)
364 {
365 $this->limit = $limit;
366
367 return $this;
368 }
369
370 /**
371 * Performs the specified query on the current LDAP connection.
372 *
373 * @param string $query
374 *
375 * @return \Adldap\Query\Collection|array
376 */
377 public function query($query)
378 {
379 $start = microtime(true);
380
381 // Here we will create the execution callback. This allows us
382 // to only execute an LDAP request if caching is disabled
383 // or if no cache of the given query exists yet.
384 $callback = function () use ($query) {
385 return $this->parse($this->run($query));
386 };
387
388 // If caching is enabled and we have a cache instance available,
389 // we will try to retrieve the cached results instead.
390 // Otherwise, we will simply execute the callback.
391 if ($this->caching && $this->cache) {
392 $results = $this->getCachedResponse($this->getCacheKey($query), $callback);
393 } else {
394 $results = $callback();
395 }
396
397 // Log the query.
398 $this->logQuery($this, $this->type, $this->getElapsedTime($start));
399
400 // Process & return the results.
401 return $this->newProcessor()->process($results);
402 }
403
404 /**
405 * Paginates the current LDAP query.
406 *
407 * @param int $perPage
408 * @param int $currentPage
409 * @param bool $isCritical
410 *
411 * @return Paginator
412 */
413 public function paginate($perPage = 1000, $currentPage = 0, $isCritical = true)
414 {
415 $this->paginated = true;
416
417 $start = microtime(true);
418
419 $query = $this->getQuery();
420
421 // Here we will create the pagination callback. This allows us
422 // to only execute an LDAP request if caching is disabled
423 // or if no cache of the given query exists yet.
424 $callback = function () use ($query, $perPage, $isCritical) {
425 return $this->runPaginate($query, $perPage, $isCritical);
426 };
427
428 // If caching is enabled and we have a cache instance available,
429 // we will try to retrieve the cached results instead.
430 if ($this->caching && $this->cache) {
431 $pages = $this->getCachedResponse($this->getCacheKey($query), $callback);
432 } else {
433 $pages = $callback();
434 }
435
436 // Log the query.
437 $this->logQuery($this, 'paginate', $this->getElapsedTime($start));
438
439 // Process & return the results.
440 return $this->newProcessor()->processPaginated($pages, $perPage, $currentPage);
441 }
442
443 /**
444 * Get the cached response or execute and cache the callback value.
445 *
446 * @param string $key
447 * @param Closure $callback
448 *
449 * @return mixed
450 */
451 protected function getCachedResponse($key, Closure $callback)
452 {
453 if ($this->flushCache) {
454 $this->cache->delete($key);
455 }
456
457 return $this->cache->remember($key, $this->cacheUntil, $callback);
458 }
459
460 /**
461 * Runs the query operation with the given filter.
462 *
463 * @param string $filter
464 *
465 * @return resource
466 */
467 protected function run($filter)
468 {
469 return $this->connection->{$this->type}(
470 $this->getDn(),
471 $filter,
472 $this->getSelects(),
473 $onlyAttributes = false,
474 $this->limit
475 );
476 }
477
478 /**
479 * Runs the paginate operation with the given filter.
480 *
481 * @param string $filter
482 * @param int $perPage
483 * @param bool $isCritical
484 *
485 * @return array
486 */
487 protected function runPaginate($filter, $perPage, $isCritical)
488 {
489 return $this->connection->supportsServerControlsInMethods() ?
490 $this->compatiblePaginationCallback($filter, $perPage, $isCritical) :
491 $this->deprecatedPaginationCallback($filter, $perPage, $isCritical);
492 }
493
494 /**
495 * Create a deprecated pagination callback compatible with PHP 7.2.
496 *
497 * @param string $filter
498 * @param int $perPage
499 * @param bool $isCritical
500 *
501 * @return array
502 */
503 protected function deprecatedPaginationCallback($filter, $perPage, $isCritical)
504 {
505 $pages = [];
506
507 $cookie = '';
508
509 do {
510 $this->connection->controlPagedResult($perPage, $isCritical, $cookie);
511
512 if (! $resource = $this->run($filter)) {
513 break;
514 }
515
516 // If we have been given a valid resource, we will retrieve the next
517 // pagination cookie to send for our next pagination request.
518 $this->connection->controlPagedResultResponse($resource, $cookie);
519
520 $pages[] = $this->parse($resource);
521 } while (!empty($cookie));
522
523 // Reset paged result on the current connection. We won't pass in the current $perPage
524 // parameter since we want to reset the page size to the default '1000'. Sending '0'
525 // eliminates any further opportunity for running queries in the same request,
526 // even though that is supposed to be the correct usage.
527 $this->connection->controlPagedResult();
528
529 return $pages;
530 }
531
532 /**
533 * Create a compatible pagination callback compatible with PHP 7.3 and greater.
534 *
535 * @param string $filter
536 * @param int $perPage
537 * @param bool $isCritical
538 *
539 * @return array
540 */
541 protected function compatiblePaginationCallback($filter, $perPage, $isCritical)
542 {
543 $pages = [];
544
545 // Setup our paged results control.
546 $controls = [
547 LDAP_CONTROL_PAGEDRESULTS => [
548 'oid' => LDAP_CONTROL_PAGEDRESULTS,
549 'isCritical' => $isCritical,
550 'value' => [
551 'size' => $perPage,
552 'cookie' => '',
553 ],
554 ],
555 ];
556
557 do {
558 // Update the server controls.
559 $this->connection->setOption(LDAP_OPT_SERVER_CONTROLS, $controls);
560
561 if (! $resource = $this->run($filter)) {
562 break;
563 }
564
565 $errorCode = $dn = $errorMessage = $refs = null;
566
567 // Update the server controls with the servers response.
568 $this->connection->parseResult($resource, $errorCode, $dn, $errorMessage, $refs, $controls);
569
570 $pages[] = $this->parse($resource);
571
572 // Reset paged result on the current connection. We won't pass in the current $perPage
573 // parameter since we want to reset the page size to the default '1000'. Sending '0'
574 // eliminates any further opportunity for running queries in the same request,
575 // even though that is supposed to be the correct usage.
576 $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['size'] = $perPage;
577 } while (!empty($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']));
578
579 // After running the query, we will clear the LDAP server controls. This
580 // allows the controls to be automatically reset before each new query
581 // that is conducted on the same connection during each request.
582 $this->connection->setOption(LDAP_OPT_SERVER_CONTROLS, []);
583
584 return $pages;
585 }
586
587 /**
588 * Parses the given LDAP resource by retrieving its entries.
589 *
590 * @param resource $resource
591 *
592 * @return array
593 */
594 protected function parse($resource)
595 {
596 // Normalize entries. Get entries returns false on failure.
597 // We'll always want an array in this situation.
598 $entries = $this->connection->getEntries($resource) ?: [];
599
600 // Free up memory.
601 if (is_resource($resource)) {
602 $this->connection->freeResult($resource);
603 }
604
605 return $entries;
606 }
607
608 /**
609 * Returns the cache key.
610 *
611 * @param string $query
612 *
613 * @return string
614 */
615 protected function getCacheKey($query)
616 {
617 $key = $this->connection->getHost()
618 .$this->type
619 .$this->getDn()
620 .$query
621 .implode('', $this->getSelects())
622 .$this->limit
623 .$this->paginated;
624
625 return md5($key);
626 }
627
628 /**
629 * Returns the first entry in a search result.
630 *
631 * @param array|string $columns
632 *
633 * @return Model|array|null
634 */
635 public function first($columns = [])
636 {
637 $results = $this->select($columns)->limit(1)->get();
638
639 // Since results may be returned inside an array if `raw()`
640 // is specified, then we'll use our array helper
641 // to retrieve the first result.
642 return Arr::get($results, 0);
643 }
644
645 /**
646 * Returns the first entry in a search result.
647 *
648 * If no entry is found, an exception is thrown.
649 *
650 * @param array|string $columns
651 *
652 * @throws ModelNotFoundException
653 *
654 * @return Model|array
655 */
656 public function firstOrFail($columns = [])
657 {
658 $record = $this->first($columns);
659
660 if (!$record) {
661 throw (new ModelNotFoundException())
662 ->setQuery($this->getUnescapedQuery(), $this->getDn());
663 }
664
665 return $record;
666 }
667
668 /**
669 * Finds a record by the specified attribute and value.
670 *
671 * @param string $attribute
672 * @param string $value
673 * @param array|string $columns
674 *
675 * @return Model|array|false
676 */
677 public function findBy($attribute, $value, $columns = [])
678 {
679 try {
680 return $this->findByOrFail($attribute, $value, $columns);
681 } catch (ModelNotFoundException $e) {
682 return;
683 }
684 }
685
686 /**
687 * Finds a record by the specified attribute and value.
688 *
689 * If no record is found an exception is thrown.
690 *
691 * @param string $attribute
692 * @param string $value
693 * @param array|string $columns
694 *
695 * @throws ModelNotFoundException
696 *
697 * @return Model|array
698 */
699 public function findByOrFail($attribute, $value, $columns = [])
700 {
701 return $this->whereEquals($attribute, $value)->firstOrFail($columns);
702 }
703
704 /**
705 * Finds a record using ambiguous name resolution.
706 *
707 * @param string|array $value
708 * @param array|string $columns
709 *
710 * @return Model|array|null
711 */
712 public function find($value, $columns = [])
713 {
714 if (is_array($value)) {
715 return $this->findMany($value, $columns);
716 }
717
718 // If we're not using ActiveDirectory, we can't use ANR. We'll make our own query.
719 if (!is_a($this->schema, ActiveDirectory::class)) {
720 return $this->prepareAnrEquivalentQuery($value)->first($columns);
721 }
722
723 return $this->findBy($this->schema->anr(), $value, $columns);
724 }
725
726 /**
727 * Finds multiple records using ambiguous name resolution.
728 *
729 * @param array $values
730 * @param array $columns
731 *
732 * @return \Adldap\Query\Collection|array
733 */
734 public function findMany(array $values = [], $columns = [])
735 {
736 $this->select($columns);
737
738 if (!is_a($this->schema, ActiveDirectory::class)) {
739 $query = $this;
740
741 foreach ($values as $value) {
742 $query->prepareAnrEquivalentQuery($value);
743 }
744
745 return $query->get();
746 }
747
748 return $this->findManyBy($this->schema->anr(), $values);
749 }
750
751 /**
752 * Creates an ANR equivalent query for LDAP distributions that do not support ANR.
753 *
754 * @param string $value
755 *
756 * @return Builder
757 */
758 protected function prepareAnrEquivalentQuery($value)
759 {
760 return $this->orFilter(function (self $query) use ($value) {
761 $locateBy = [
762 $this->schema->name(),
763 $this->schema->email(),
764 $this->schema->userId(),
765 $this->schema->lastName(),
766 $this->schema->firstName(),
767 $this->schema->commonName(),
768 $this->schema->displayName(),
769 ];
770
771 foreach ($locateBy as $attribute) {
772 $query->whereEquals($attribute, $value);
773 }
774 });
775 }
776
777 /**
778 * Finds many records by the specified attribute.
779 *
780 * @param string $attribute
781 * @param array $values
782 * @param array $columns
783 *
784 * @return \Adldap\Query\Collection|array
785 */
786 public function findManyBy($attribute, array $values = [], $columns = [])
787 {
788 $query = $this->select($columns);
789
790 foreach ($values as $value) {
791 $query->orWhere([$attribute => $value]);
792 }
793
794 return $query->get();
795 }
796
797 /**
798 * Finds a record using ambiguous name resolution.
799 *
800 * If a record is not found, an exception is thrown.
801 *
802 * @param string $value
803 * @param array|string $columns
804 *
805 * @throws ModelNotFoundException
806 *
807 * @return Model|array
808 */
809 public function findOrFail($value, $columns = [])
810 {
811 $entry = $this->find($value, $columns);
812
813 // Make sure we check if the result is an entry or an array before
814 // we throw an exception in case the user wants raw results.
815 if (!$entry instanceof Model && !is_array($entry)) {
816 throw (new ModelNotFoundException())
817 ->setQuery($this->getUnescapedQuery(), $this->getDn());
818 }
819
820 return $entry;
821 }
822
823 /**
824 * Finds a record by its distinguished name.
825 *
826 * @param string $dn
827 * @param array|string $columns
828 *
829 * @return bool|Model
830 */
831 public function findByDn($dn, $columns = [])
832 {
833 try {
834 return $this->findByDnOrFail($dn, $columns);
835 } catch (ModelNotFoundException $e) {
836 return;
837 }
838 }
839
840 /**
841 * Finds a record by its distinguished name.
842 *
843 * Fails upon no records returned.
844 *
845 * @param string $dn
846 * @param array|string $columns
847 *
848 * @throws ModelNotFoundException
849 *
850 * @return Model|array
851 */
852 public function findByDnOrFail($dn, $columns = [])
853 {
854 // Since we're setting our base DN to be able to retrieve a model
855 // by its distinguished name, we need to set it back to
856 // our configured base so it is not overwritten.
857 $base = $this->getDn();
858
859 $model = $this->setDn($dn)
860 ->read()
861 ->whereHas($this->schema->objectClass())
862 ->firstOrFail($columns);
863
864 // Reset the models query builder (in case a model is returned).
865 // Otherwise, we must be requesting a raw result.
866 if ($model instanceof Model) {
867 $model->setQuery($this->in($base));
868 }
869
870 return $model;
871 }
872
873 /**
874 * Finds a record by its string GUID.
875 *
876 * @param string $guid
877 * @param array|string $columns
878 *
879 * @return Model|array|false
880 */
881 public function findByGuid($guid, $columns = [])
882 {
883 try {
884 return $this->findByGuidOrFail($guid, $columns);
885 } catch (ModelNotFoundException $e) {
886 return;
887 }
888 }
889
890 /**
891 * Finds a record by its string GUID.
892 *
893 * Fails upon no records returned.
894 *
895 * @param string $guid
896 * @param array|string $columns
897 *
898 * @throws ModelNotFoundException
899 *
900 * @return Model|array
901 */
902 public function findByGuidOrFail($guid, $columns = [])
903 {
904 if ($this->schema->objectGuidRequiresConversion()) {
905 $guid = Utilities::stringGuidToHex($guid);
906 }
907
908 return $this->select($columns)->whereRaw([
909 $this->schema->objectGuid() => $guid,
910 ])->firstOrFail();
911 }
912
913 /**
914 * Finds a record by its Object SID.
915 *
916 * @param string $sid
917 * @param array|string $columns
918 *
919 * @return Model|array|false
920 */
921 public function findBySid($sid, $columns = [])
922 {
923 try {
924 return $this->findBySidOrFail($sid, $columns);
925 } catch (ModelNotFoundException $e) {
926 return;
927 }
928 }
929
930 /**
931 * Finds a record by its Object SID.
932 *
933 * Fails upon no records returned.
934 *
935 * @param string $sid
936 * @param array|string $columns
937 *
938 * @throws ModelNotFoundException
939 *
940 * @return Model|array
941 */
942 public function findBySidOrFail($sid, $columns = [])
943 {
944 return $this->findByOrFail($this->schema->objectSid(), $sid, $columns);
945 }
946
947 /**
948 * Finds the Base DN of your domain controller.
949 *
950 * @return string|bool
951 */
952 public function findBaseDn()
953 {
954 $result = $this->setDn(null)
955 ->read()
956 ->raw()
957 ->whereHas($this->schema->objectClass())
958 ->first();
959
960 $key = $this->schema->defaultNamingContext();
961
962 if (is_array($result) && array_key_exists($key, $result)) {
963 if (array_key_exists(0, $result[$key])) {
964 return $result[$key][0];
965 }
966 }
967
968 return false;
969 }
970
971 /**
972 * Adds the inserted fields to query on the current LDAP connection.
973 *
974 * @param array|string $columns
975 *
976 * @return Builder
977 */
978 public function select($columns = [])
979 {
980 $columns = is_array($columns) ? $columns : func_get_args();
981
982 if (!empty($columns)) {
983 $this->columns = $columns;
984 }
985
986 return $this;
987 }
988
989 /**
990 * Adds a raw filter to the current query.
991 *
992 * @param array|string $filters
993 *
994 * @return Builder
995 */
996 public function rawFilter($filters = [])
997 {
998 $filters = is_array($filters) ? $filters : func_get_args();
999
1000 foreach ($filters as $filter) {
1001 $this->filters['raw'][] = $filter;
1002 }
1003
1004 return $this;
1005 }
1006
1007 /**
1008 * Adds a nested 'and' filter to the current query.
1009 *
1010 * @param Closure $closure
1011 *
1012 * @return Builder
1013 */
1014 public function andFilter(Closure $closure)
1015 {
1016 $query = $this->newNestedInstance($closure);
1017
1018 $filter = $this->grammar->compileAnd($query->getQuery());
1019
1020 return $this->rawFilter($filter);
1021 }
1022
1023 /**
1024 * Adds a nested 'or' filter to the current query.
1025 *
1026 * @param Closure $closure
1027 *
1028 * @return Builder
1029 */
1030 public function orFilter(Closure $closure)
1031 {
1032 $query = $this->newNestedInstance($closure);
1033
1034 $filter = $this->grammar->compileOr($query->getQuery());
1035
1036 return $this->rawFilter($filter);
1037 }
1038
1039 /**
1040 * Adds a nested 'not' filter to the current query.
1041 *
1042 * @param Closure $closure
1043 *
1044 * @return Builder
1045 */
1046 public function notFilter(Closure $closure)
1047 {
1048 $query = $this->newNestedInstance($closure);
1049
1050 $filter = $this->grammar->compileNot($query->getQuery());
1051
1052 return $this->rawFilter($filter);
1053 }
1054
1055 /**
1056 * Adds a where clause to the current query.
1057 *
1058 * @param string|array $field
1059 * @param string $operator
1060 * @param string $value
1061 * @param string $boolean
1062 * @param bool $raw
1063 *
1064 * @throws InvalidArgumentException
1065 *
1066 * @return Builder
1067 */
1068 public function where($field, $operator = null, $value = null, $boolean = 'and', $raw = false)
1069 {
1070 if (is_array($field)) {
1071 // If the column is an array, we will assume it is an array of
1072 // key-value pairs and can add them each as a where clause.
1073 return $this->addArrayOfWheres($field, $boolean, $raw);
1074 }
1075
1076 // We'll bypass the 'has' and 'notHas' operator since they
1077 // only require two arguments inside the where method.
1078 $bypass = [Operator::$has, Operator::$notHas];
1079
1080 // Here we will make some assumptions about the operator. If only
1081 // 2 values are passed to the method, we will assume that
1082 // the operator is 'equals' and keep going.
1083 if (func_num_args() === 2 && in_array($operator, $bypass) === false) {
1084 list($value, $operator) = [$operator, '='];
1085 }
1086
1087 if (!in_array($operator, Operator::all())) {
1088 throw new InvalidArgumentException("Invalid where operator: {$operator}");
1089 }
1090
1091 // We'll escape the value if raw isn't requested.
1092 $value = $raw ? $value : $this->escape($value);
1093
1094 $field = $this->escape($field, $ignore = null, 3);
1095
1096 $this->addFilter($boolean, compact('field', 'operator', 'value'));
1097
1098 return $this;
1099 }
1100
1101 /**
1102 * Adds a raw where clause to the current query.
1103 *
1104 * Values given to this method are not escaped.
1105 *
1106 * @param string|array $field
1107 * @param string $operator
1108 * @param string $value
1109 *
1110 * @return Builder
1111 */
1112 public function whereRaw($field, $operator = null, $value = null)
1113 {
1114 return $this->where($field, $operator, $value, 'and', true);
1115 }
1116
1117 /**
1118 * Adds a 'where equals' clause to the current query.
1119 *
1120 * @param string $field
1121 * @param string $value
1122 *
1123 * @return Builder
1124 */
1125 public function whereEquals($field, $value)
1126 {
1127 return $this->where($field, Operator::$equals, $value);
1128 }
1129
1130 /**
1131 * Adds a 'where not equals' clause to the current query.
1132 *
1133 * @param string $field
1134 * @param string $value
1135 *
1136 * @return Builder
1137 */
1138 public function whereNotEquals($field, $value)
1139 {
1140 return $this->where($field, Operator::$doesNotEqual, $value);
1141 }
1142
1143 /**
1144 * Adds a 'where approximately equals' clause to the current query.
1145 *
1146 * @param string $field
1147 * @param string $value
1148 *
1149 * @return Builder
1150 */
1151 public function whereApproximatelyEquals($field, $value)
1152 {
1153 return $this->where($field, Operator::$approximatelyEquals, $value);
1154 }
1155
1156 /**
1157 * Adds a 'where has' clause to the current query.
1158 *
1159 * @param string $field
1160 *
1161 * @return Builder
1162 */
1163 public function whereHas($field)
1164 {
1165 return $this->where($field, Operator::$has);
1166 }
1167
1168 /**
1169 * Adds a 'where not has' clause to the current query.
1170 *
1171 * @param string $field
1172 *
1173 * @return Builder
1174 */
1175 public function whereNotHas($field)
1176 {
1177 return $this->where($field, Operator::$notHas);
1178 }
1179
1180 /**
1181 * Adds a 'where contains' clause to the current query.
1182 *
1183 * @param string $field
1184 * @param string $value
1185 *
1186 * @return Builder
1187 */
1188 public function whereContains($field, $value)
1189 {
1190 return $this->where($field, Operator::$contains, $value);
1191 }
1192
1193 /**
1194 * Adds a 'where contains' clause to the current query.
1195 *
1196 * @param string $field
1197 * @param string $value
1198 *
1199 * @return Builder
1200 */
1201 public function whereNotContains($field, $value)
1202 {
1203 return $this->where($field, Operator::$notContains, $value);
1204 }
1205
1206 /**
1207 * Query for entries that match any of the values provided for the given field.
1208 *
1209 * @param string $field
1210 * @param array $values
1211 *
1212 * @return Builder
1213 */
1214 public function whereIn($field, array $values)
1215 {
1216 return $this->orFilter(function (self $query) use ($field, $values) {
1217 foreach ($values as $value) {
1218 $query->whereEquals($field, $value);
1219 }
1220 });
1221 }
1222
1223 /**
1224 * Adds a 'between' clause to the current query.
1225 *
1226 * @param string $field
1227 * @param array $values
1228 *
1229 * @return Builder
1230 */
1231 public function whereBetween($field, array $values)
1232 {
1233 return $this->where([
1234 [$field, '>=', $values[0]],
1235 [$field, '<=', $values[1]],
1236 ]);
1237 }
1238
1239 /**
1240 * Adds a 'where starts with' clause to the current query.
1241 *
1242 * @param string $field
1243 * @param string $value
1244 *
1245 * @return Builder
1246 */
1247 public function whereStartsWith($field, $value)
1248 {
1249 return $this->where($field, Operator::$startsWith, $value);
1250 }
1251
1252 /**
1253 * Adds a 'where *not* starts with' clause to the current query.
1254 *
1255 * @param string $field
1256 * @param string $value
1257 *
1258 * @return Builder
1259 */
1260 public function whereNotStartsWith($field, $value)
1261 {
1262 return $this->where($field, Operator::$notStartsWith, $value);
1263 }
1264
1265 /**
1266 * Adds a 'where ends with' clause to the current query.
1267 *
1268 * @param string $field
1269 * @param string $value
1270 *
1271 * @return Builder
1272 */
1273 public function whereEndsWith($field, $value)
1274 {
1275 return $this->where($field, Operator::$endsWith, $value);
1276 }
1277
1278 /**
1279 * Adds a 'where *not* ends with' clause to the current query.
1280 *
1281 * @param string $field
1282 * @param string $value
1283 *
1284 * @return Builder
1285 */
1286 public function whereNotEndsWith($field, $value)
1287 {
1288 return $this->where($field, Operator::$notEndsWith, $value);
1289 }
1290
1291 /**
1292 * Adds a enabled filter to the current query.
1293 *
1294 * @return Builder
1295 */
1296 public function whereEnabled()
1297 {
1298 return $this->rawFilter($this->schema->filterEnabled());
1299 }
1300
1301 /**
1302 * Adds a disabled filter to the current query.
1303 *
1304 * @return Builder
1305 */
1306 public function whereDisabled()
1307 {
1308 return $this->rawFilter($this->schema->filterDisabled());
1309 }
1310
1311 /**
1312 * Adds a 'member of' filter to the current query.
1313 *
1314 * @param string $dn
1315 *
1316 * @return Builder
1317 */
1318 public function whereMemberOf($dn)
1319 {
1320 return $this->whereEquals($this->schema->memberOfRecursive(), $dn);
1321 }
1322
1323 /**
1324 * Adds an 'or where' clause to the current query.
1325 *
1326 * @param array|string $field
1327 * @param string|null $operator
1328 * @param string|null $value
1329 *
1330 * @return Builder
1331 */
1332 public function orWhere($field, $operator = null, $value = null)
1333 {
1334 return $this->where($field, $operator, $value, 'or');
1335 }
1336
1337 /**
1338 * Adds a raw or where clause to the current query.
1339 *
1340 * Values given to this method are not escaped.
1341 *
1342 * @param string $field
1343 * @param string $operator
1344 * @param string $value
1345 *
1346 * @return Builder
1347 */
1348 public function orWhereRaw($field, $operator = null, $value = null)
1349 {
1350 return $this->where($field, $operator, $value, 'or', true);
1351 }
1352
1353 /**
1354 * Adds an 'or where has' clause to the current query.
1355 *
1356 * @param string $field
1357 *
1358 * @return Builder
1359 */
1360 public function orWhereHas($field)
1361 {
1362 return $this->orWhere($field, Operator::$has);
1363 }
1364
1365 /**
1366 * Adds a 'where not has' clause to the current query.
1367 *
1368 * @param string $field
1369 *
1370 * @return Builder
1371 */
1372 public function orWhereNotHas($field)
1373 {
1374 return $this->orWhere($field, Operator::$notHas);
1375 }
1376
1377 /**
1378 * Adds an 'or where equals' clause to the current query.
1379 *
1380 * @param string $field
1381 * @param string $value
1382 *
1383 * @return Builder
1384 */
1385 public function orWhereEquals($field, $value)
1386 {
1387 return $this->orWhere($field, Operator::$equals, $value);
1388 }
1389
1390 /**
1391 * Adds an 'or where not equals' clause to the current query.
1392 *
1393 * @param string $field
1394 * @param string $value
1395 *
1396 * @return Builder
1397 */
1398 public function orWhereNotEquals($field, $value)
1399 {
1400 return $this->orWhere($field, Operator::$doesNotEqual, $value);
1401 }
1402
1403 /**
1404 * Adds a 'or where approximately equals' clause to the current query.
1405 *
1406 * @param string $field
1407 * @param string $value
1408 *
1409 * @return Builder
1410 */
1411 public function orWhereApproximatelyEquals($field, $value)
1412 {
1413 return $this->orWhere($field, Operator::$approximatelyEquals, $value);
1414 }
1415
1416 /**
1417 * Adds an 'or where contains' clause to the current query.
1418 *
1419 * @param string $field
1420 * @param string $value
1421 *
1422 * @return Builder
1423 */
1424 public function orWhereContains($field, $value)
1425 {
1426 return $this->orWhere($field, Operator::$contains, $value);
1427 }
1428
1429 /**
1430 * Adds an 'or where *not* contains' clause to the current query.
1431 *
1432 * @param string $field
1433 * @param string $value
1434 *
1435 * @return Builder
1436 */
1437 public function orWhereNotContains($field, $value)
1438 {
1439 return $this->orWhere($field, Operator::$notContains, $value);
1440 }
1441
1442 /**
1443 * Adds an 'or where starts with' clause to the current query.
1444 *
1445 * @param string $field
1446 * @param string $value
1447 *
1448 * @return Builder
1449 */
1450 public function orWhereStartsWith($field, $value)
1451 {
1452 return $this->orWhere($field, Operator::$startsWith, $value);
1453 }
1454
1455 /**
1456 * Adds an 'or where *not* starts with' clause to the current query.
1457 *
1458 * @param string $field
1459 * @param string $value
1460 *
1461 * @return Builder
1462 */
1463 public function orWhereNotStartsWith($field, $value)
1464 {
1465 return $this->orWhere($field, Operator::$notStartsWith, $value);
1466 }
1467
1468 /**
1469 * Adds an 'or where ends with' clause to the current query.
1470 *
1471 * @param string $field
1472 * @param string $value
1473 *
1474 * @return Builder
1475 */
1476 public function orWhereEndsWith($field, $value)
1477 {
1478 return $this->orWhere($field, Operator::$endsWith, $value);
1479 }
1480
1481 /**
1482 * Adds an 'or where *not* ends with' clause to the current query.
1483 *
1484 * @param string $field
1485 * @param string $value
1486 *
1487 * @return Builder
1488 */
1489 public function orWhereNotEndsWith($field, $value)
1490 {
1491 return $this->orWhere($field, Operator::$notEndsWith, $value);
1492 }
1493
1494 /**
1495 * Adds an 'or where member of' filter to the current query.
1496 *
1497 * @param string $dn
1498 *
1499 * @return Builder
1500 */
1501 public function orWhereMemberOf($dn)
1502 {
1503 return $this->orWhereEquals($this->schema->memberOfRecursive(), $dn);
1504 }
1505
1506 /**
1507 * Adds a filter onto the current query.
1508 *
1509 * @param string $type The type of filter to add.
1510 * @param array $bindings The bindings of the filter.
1511 *
1512 * @throws InvalidArgumentException
1513 *
1514 * @return $this
1515 */
1516 public function addFilter($type, array $bindings)
1517 {
1518 // Here we will ensure we have been given a proper filter type.
1519 if (!array_key_exists($type, $this->filters)) {
1520 throw new InvalidArgumentException("Invalid filter type: {$type}.");
1521 }
1522
1523 // The required filter key bindings.
1524 $required = ['field', 'operator', 'value'];
1525
1526 // Here we will ensure the proper key bindings are given.
1527 if (count(array_intersect_key(array_flip($required), $bindings)) !== count($required)) {
1528 // Retrieve the keys that are missing in the bindings array.
1529 $missing = implode(', ', array_diff($required, array_flip($bindings)));
1530
1531 throw new InvalidArgumentException("Invalid filter bindings. Missing: {$missing} keys.");
1532 }
1533
1534 $this->filters[$type][] = $bindings;
1535
1536 return $this;
1537 }
1538
1539 /**
1540 * Clear the query builders filters.
1541 *
1542 * @return $this
1543 */
1544 public function clearFilters()
1545 {
1546 foreach ($this->filters as $type => $filters) {
1547 $this->filters[$type] = [];
1548 }
1549
1550 return $this;
1551 }
1552
1553 /**
1554 * Returns true / false depending if the current object
1555 * contains selects.
1556 *
1557 * @return bool
1558 */
1559 public function hasSelects()
1560 {
1561 return count($this->getSelects()) > 0;
1562 }
1563
1564 /**
1565 * Returns the current selected fields to retrieve.
1566 *
1567 * @return array
1568 */
1569 public function getSelects()
1570 {
1571 $selects = $this->columns;
1572
1573 // If the asterisk is not provided in the selected columns, we need to
1574 // ensure we always select the object class and category, as these
1575 // are used for constructing models. The asterisk indicates that
1576 // we want all attributes returned for LDAP records.
1577 if (!in_array('*', $selects)) {
1578 $selects[] = $this->schema->objectCategory();
1579 $selects[] = $this->schema->objectClass();
1580 }
1581
1582 return $selects;
1583 }
1584
1585 /**
1586 * Sorts the LDAP search results by the specified field and direction.
1587 *
1588 * @param string $field
1589 * @param string $direction
1590 * @param int|null $flags
1591 *
1592 * @return Builder
1593 */
1594 public function sortBy($field, $direction = 'asc', $flags = null)
1595 {
1596 $this->sortByField = $field;
1597
1598 // Normalize direction.
1599 $direction = strtolower($direction);
1600
1601 if ($direction === 'asc' || $direction === 'desc') {
1602 $this->sortByDirection = $direction;
1603 }
1604
1605 if (is_null($flags)) {
1606 $this->sortByFlags = SORT_NATURAL + SORT_FLAG_CASE;
1607 }
1608
1609 return $this;
1610 }
1611
1612 /**
1613 * Set the query to search on the base distinguished name.
1614 *
1615 * This will result in one record being returned.
1616 *
1617 * @return Builder
1618 */
1619 public function read()
1620 {
1621 $this->type = 'read';
1622
1623 return $this;
1624 }
1625
1626 /**
1627 * Set the query to search one level on the base distinguished name.
1628 *
1629 * @return Builder
1630 */
1631 public function listing()
1632 {
1633 $this->type = 'listing';
1634
1635 return $this;
1636 }
1637
1638 /**
1639 * Sets the query to search the entire directory on the base distinguished name.
1640 *
1641 * @return Builder
1642 */
1643 public function recursive()
1644 {
1645 $this->type = 'search';
1646
1647 return $this;
1648 }
1649
1650 /**
1651 * Whether to return the LDAP results in their raw format.
1652 *
1653 * @param bool $raw
1654 *
1655 * @return Builder
1656 */
1657 public function raw($raw = true)
1658 {
1659 $this->raw = (bool) $raw;
1660
1661 return $this;
1662 }
1663
1664 /**
1665 * Whether the current query is nested.
1666 *
1667 * @param bool $nested
1668 *
1669 * @return Builder
1670 */
1671 public function nested($nested = true)
1672 {
1673 $this->nested = (bool) $nested;
1674
1675 return $this;
1676 }
1677
1678 /**
1679 * Enables caching on the current query until the given date.
1680 *
1681 * If flushing is enabled, the query cache will be flushed and then re-cached.
1682 *
1683 * @param \DateTimeInterface $until When to expire the query cache.
1684 * @param bool $flush Whether to force-flush the query cache.
1685 *
1686 * @return $this
1687 */
1688 public function cache(\DateTimeInterface $until = null, $flush = false)
1689 {
1690 $this->caching = true;
1691 $this->cacheUntil = $until;
1692 $this->flushCache = $flush;
1693
1694 return $this;
1695 }
1696
1697 /**
1698 * Returns an escaped string for use in an LDAP filter.
1699 *
1700 * @param string $value
1701 * @param string $ignore
1702 * @param int $flags
1703 *
1704 * @return string
1705 */
1706 public function escape($value, $ignore = '', $flags = 0)
1707 {
1708 return ldap_escape($value, $ignore, $flags);
1709 }
1710
1711 /**
1712 * Returns the query builders sort by field.
1713 *
1714 * @return string
1715 */
1716 public function getSortByField()
1717 {
1718 return $this->sortByField;
1719 }
1720
1721 /**
1722 * Returns the query builders sort by direction.
1723 *
1724 * @return string
1725 */
1726 public function getSortByDirection()
1727 {
1728 return $this->sortByDirection;
1729 }
1730
1731 /**
1732 * Returns the query builders sort by flags.
1733 *
1734 * @return int
1735 */
1736 public function getSortByFlags()
1737 {
1738 return $this->sortByFlags;
1739 }
1740
1741 /**
1742 * Returns true / false if the current query is nested.
1743 *
1744 * @return bool
1745 */
1746 public function isNested()
1747 {
1748 return $this->nested === true;
1749 }
1750
1751 /**
1752 * Returns bool that determines whether the current
1753 * query builder will return raw results.
1754 *
1755 * @return bool
1756 */
1757 public function isRaw()
1758 {
1759 return $this->raw;
1760 }
1761
1762 /**
1763 * Returns bool that determines whether the current
1764 * query builder will return paginated results.
1765 *
1766 * @return bool
1767 */
1768 public function isPaginated()
1769 {
1770 return $this->paginated;
1771 }
1772
1773 /**
1774 * Returns bool that determines whether the current
1775 * query builder will return sorted results.
1776 *
1777 * @return bool
1778 */
1779 public function isSorted()
1780 {
1781 return $this->sortByField ? true : false;
1782 }
1783
1784 /**
1785 * Handle dynamic method calls on the query builder object to be directed to the query processor.
1786 *
1787 * @param string $method
1788 * @param array $parameters
1789 *
1790 * @return mixed
1791 */
1792 public function __call($method, $parameters)
1793 {
1794 // We'll check if the beginning of the method being called contains
1795 // 'where'. If so, we'll assume it's a dynamic 'where' clause.
1796 if (substr($method, 0, 5) === 'where') {
1797 return $this->dynamicWhere($method, $parameters);
1798 }
1799
1800 return call_user_func_array([$this->newProcessor(), $method], $parameters);
1801 }
1802
1803 /**
1804 * Handles dynamic "where" clauses to the query.
1805 *
1806 * @param string $method
1807 * @param array $parameters
1808 *
1809 * @return Builder
1810 */
1811 public function dynamicWhere($method, $parameters)
1812 {
1813 $finder = substr($method, 5);
1814
1815 $segments = preg_split('/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE);
1816
1817 // The connector variable will determine which connector will be used for the
1818 // query condition. We will change it as we come across new boolean values
1819 // in the dynamic method strings, which could contain a number of these.
1820 $connector = 'and';
1821
1822 $index = 0;
1823
1824 foreach ($segments as $segment) {
1825 // If the segment is not a boolean connector, we can assume it is a column's name
1826 // and we will add it to the query as a new constraint as a where clause, then
1827 // we can keep iterating through the dynamic method string's segments again.
1828 if ($segment != 'And' && $segment != 'Or') {
1829 $this->addDynamic($segment, $connector, $parameters, $index);
1830
1831 $index++;
1832 }
1833
1834 // Otherwise, we will store the connector so we know how the next where clause we
1835 // find in the query should be connected to the previous ones, meaning we will
1836 // have the proper boolean connector to connect the next where clause found.
1837 else {
1838 $connector = $segment;
1839 }
1840 }
1841
1842 return $this;
1843 }
1844
1845 /**
1846 * Adds an array of wheres to the current query.
1847 *
1848 * @param array $wheres
1849 * @param string $boolean
1850 * @param bool $raw
1851 *
1852 * @return Builder
1853 */
1854 protected function addArrayOfWheres($wheres, $boolean, $raw)
1855 {
1856 foreach ($wheres as $key => $value) {
1857 if (is_numeric($key) && is_array($value)) {
1858 // If the key is numeric and the value is an array, we'll
1859 // assume we've been given an array with conditionals.
1860 list($field, $condition) = $value;
1861
1862 // Since a value is optional for some conditionals, we will
1863 // try and retrieve the third parameter from the array,
1864 // but is entirely optional.
1865 $value = Arr::get($value, 2);
1866
1867 $this->where($field, $condition, $value, $boolean);
1868 } else {
1869 // If the value is not an array, we will assume an equals clause.
1870 $this->where($key, Operator::$equals, $value, $boolean, $raw);
1871 }
1872 }
1873
1874 return $this;
1875 }
1876
1877 /**
1878 * Add a single dynamic where clause statement to the query.
1879 *
1880 * @param string $segment
1881 * @param string $connector
1882 * @param array $parameters
1883 * @param int $index
1884 *
1885 * @return void
1886 */
1887 protected function addDynamic($segment, $connector, $parameters, $index)
1888 {
1889 // We'll format the 'where' boolean and field here to avoid casing issues.
1890 $bool = strtolower($connector);
1891 $field = strtolower($segment);
1892
1893 $this->where($field, '=', $parameters[$index], $bool);
1894 }
1895
1896 /**
1897 * Logs the given executed query information by firing its query event.
1898 *
1899 * @param Builder $query
1900 * @param string $type
1901 * @param null|float $time
1902 */
1903 protected function logQuery($query, $type, $time = null)
1904 {
1905 $args = [$query, $time];
1906
1907 switch ($type) {
1908 case 'listing':
1909 $event = new Events\Listing(...$args);
1910 break;
1911 case 'read':
1912 $event = new Events\Read(...$args);
1913 break;
1914 case 'paginate':
1915 $event = new Events\Paginate(...$args);
1916 break;
1917 default:
1918 $event = new Events\Search(...$args);
1919 break;
1920 }
1921
1922 $this->fireQueryEvent($event);
1923 }
1924
1925 /**
1926 * Fires the given query event.
1927 *
1928 * @param QueryExecuted $event
1929 */
1930 protected function fireQueryEvent(QueryExecuted $event)
1931 {
1932 Adldap::getEventDispatcher()->fire($event);
1933 }
1934
1935 /**
1936 * Get the elapsed time since a given starting point.
1937 *
1938 * @param int $start
1939 *
1940 * @return float
1941 */
1942 protected function getElapsedTime($start)
1943 {
1944 return round((microtime(true) - $start) * 1000, 2);
1945 }
1946
1947 /**
1948 * Returns a new query Processor instance.
1949 *
1950 * @return Processor
1951 */
1952 protected function newProcessor()
1953 {
1954 return new Processor($this);
1955 }
1956}