21
21
*/
22
22
class Query extends AbstractQuery
23
23
{
24
+ // As of PHP 7.2, we can use LDAP_CONTROL_PAGEDRESULTS instead of this
25
+ const PAGINATION_OID = '1.2.840.113556.1.4.319 ' ;
26
+
24
27
/** @var Connection */
25
28
protected $ connection ;
26
29
27
- /** @var resource */
28
- private $ search ;
30
+ /** @var resource[] */
31
+ private $ results ;
29
32
30
33
public function __construct (Connection $ connection , string $ dn , string $ query , array $ options = [])
31
34
{
@@ -37,29 +40,33 @@ public function __destruct()
37
40
$ con = $ this ->connection ->getResource ();
38
41
$ this ->connection = null ;
39
42
40
- if (null === $ this ->search || false === $ this -> search ) {
43
+ if (null === $ this ->results ) {
41
44
return ;
42
45
}
43
46
44
- $ success = ldap_free_result ($ this ->search );
45
- $ this ->search = null ;
46
-
47
- if (!$ success ) {
48
- throw new LdapException (sprintf ('Could not free results: %s. ' , ldap_error ($ con )));
47
+ foreach ($ this ->results as $ result ) {
48
+ if (false === $ result || null === $ result ) {
49
+ continue ;
50
+ }
51
+ if (!ldap_free_result ($ result )) {
52
+ throw new LdapException (sprintf ('Could not free results: %s. ' , ldap_error ($ con )));
53
+ }
49
54
}
55
+ $ this ->results = null ;
50
56
}
51
57
52
58
/**
53
59
* {@inheritdoc}
54
60
*/
55
61
public function execute ()
56
62
{
57
- if (null === $ this ->search ) {
63
+ if (null === $ this ->results ) {
58
64
// If the connection is not bound, throw an exception. Users should use an explicit bind call first.
59
65
if (!$ this ->connection ->isBound ()) {
60
66
throw new NotBoundException ('Query execution is not possible without binding the connection first. ' );
61
67
}
62
68
69
+ $ this ->results = [];
63
70
$ con = $ this ->connection ->getResource ();
64
71
65
72
switch ($ this ->options ['scope ' ]) {
@@ -76,39 +83,126 @@ public function execute()
76
83
throw new LdapException (sprintf ('Could not search in scope "%s". ' , $ this ->options ['scope ' ]));
77
84
}
78
85
79
- $ this ->search = @$ func (
80
- $ con ,
81
- $ this ->dn ,
82
- $ this ->query ,
83
- $ this ->options ['filter ' ],
84
- $ this ->options ['attrsOnly ' ],
85
- $ this ->options ['maxItems ' ],
86
- $ this ->options ['timeout ' ],
87
- $ this ->options ['deref ' ]
88
- );
89
- }
90
-
91
- if (false === $ this ->search ) {
92
- $ ldapError = '' ;
93
- if ($ errno = ldap_errno ($ con )) {
94
- $ ldapError = sprintf (' LDAP error was [%d] %s ' , $ errno , ldap_error ($ con ));
86
+ $ itemsLeft = $ maxItems = $ this ->options ['maxItems ' ];
87
+ $ pageSize = $ this ->options ['pageSize ' ];
88
+ // Deal with the logic to handle maxItems properly. If we can satisfy it in
89
+ // one request based on pageSize, we don't need to bother sending page control
90
+ // to the server so that it can determine what we already know.
91
+ if (0 !== $ maxItems && $ pageSize > $ maxItems ) {
92
+ $ pageSize = 0 ;
93
+ } elseif (0 !== $ maxItems ) {
94
+ $ pageSize = min ($ maxItems , $ pageSize );
95
+ }
96
+ $ pageControl = $ this ->options ['scope ' ] != static ::SCOPE_BASE && $ pageSize > 0 ;
97
+ $ cookie = '' ;
98
+ do {
99
+ if ($ pageControl ) {
100
+ ldap_control_paged_result ($ con , $ pageSize , true , $ cookie );
101
+ }
102
+ $ sizeLimit = $ itemsLeft ;
103
+ if ($ pageSize > 0 && $ sizeLimit >= $ pageSize ) {
104
+ $ sizeLimit = 0 ;
105
+ }
106
+ $ search = @$ func (
107
+ $ con ,
108
+ $ this ->dn ,
109
+ $ this ->query ,
110
+ $ this ->options ['filter ' ],
111
+ $ this ->options ['attrsOnly ' ],
112
+ $ sizeLimit ,
113
+ $ this ->options ['timeout ' ],
114
+ $ this ->options ['deref ' ]
115
+ );
116
+
117
+ if (false === $ search ) {
118
+ $ ldapError = '' ;
119
+ if ($ errno = ldap_errno ($ con )) {
120
+ $ ldapError = sprintf (' LDAP error was [%d] %s ' , $ errno , ldap_error ($ con ));
121
+ }
122
+ if ($ pageControl ) {
123
+ $ this ->resetPagination ();
124
+ }
125
+
126
+ throw new LdapException (sprintf ('Could not complete search with dn "%s", query "%s" and filters "%s".%s ' , $ this ->dn , $ this ->query , implode (', ' , $ this ->options ['filter ' ]), $ ldapError ));
127
+ }
128
+
129
+ $ this ->results [] = $ search ;
130
+ $ itemsLeft -= min ($ itemsLeft , $ pageSize );
131
+
132
+ if (0 !== $ maxItems && 0 === $ itemsLeft ) {
133
+ break ;
134
+ }
135
+ if ($ pageControl ) {
136
+ ldap_control_paged_result_response ($ con , $ search , $ cookie );
137
+ }
138
+ } while (null !== $ cookie && '' !== $ cookie );
139
+
140
+ if ($ pageControl ) {
141
+ $ this ->resetPagination ();
95
142
}
96
-
97
- throw new LdapException (sprintf ('Could not complete search with dn "%s", query "%s" and filters "%s".%s ' , $ this ->dn , $ this ->query , implode (', ' , $ this ->options ['filter ' ]), $ ldapError ));
98
143
}
99
144
100
145
return new Collection ($ this ->connection , $ this );
101
146
}
102
147
103
148
/**
104
- * Returns a LDAP search resource.
149
+ * Returns a LDAP search resource. If this query resulted in multiple searches, only the first
150
+ * page will be returned.
105
151
*
106
152
* @return resource
107
153
*
108
154
* @internal
109
155
*/
110
- public function getResource ()
156
+ public function getResource ($ idx = 0 )
111
157
{
112
- return $ this ->search ;
158
+ if (null === $ this ->results || $ idx >= \count ($ this ->results )) {
159
+ return null ;
160
+ }
161
+
162
+ return $ this ->results [$ idx ];
163
+ }
164
+
165
+ /**
166
+ * Returns all LDAP search resources.
167
+ *
168
+ * @return resource[]
169
+ *
170
+ * @internal
171
+ */
172
+ public function getResources ()
173
+ {
174
+ return $ this ->results ;
175
+ }
176
+
177
+ /**
178
+ * Resets pagination on the current connection.
179
+ *
180
+ * @internal
181
+ */
182
+ private function resetPagination ()
183
+ {
184
+ $ con = $ this ->connection ->getResource ();
185
+ ldap_control_paged_result ($ con , 0 );
186
+
187
+ // This is a workaround for a bit of a bug in the above invocation
188
+ // of ldap_control_paged_result. Instead of indicating to extldap that
189
+ // we no longer wish to page queries on this link, this invocation sets
190
+ // the LDAP_CONTROL_PAGEDRESULTS OID with a page size of 0. This isn't
191
+ // well defined by RFC 2696 if there is no cookie present, so some servers
192
+ // will interpret it differently and do the wrong thing. Forcefully remove
193
+ // the OID for now until a fix can make its way through the versions of PHP
194
+ // the we support.
195
+ //
196
+ // This is not supported in PHP < 7.2, so these versions will remain broken.
197
+ $ ctl = [];
198
+ ldap_get_option ($ con , LDAP_OPT_SERVER_CONTROLS , $ ctl );
199
+ if (!empty ($ ctl )) {
200
+ foreach ($ ctl as $ idx => $ info ) {
201
+ if (static ::PAGINATION_OID == $ info ['oid ' ]) {
202
+ unset($ ctl [$ idx ]);
203
+ }
204
+ }
205
+ ldap_set_option ($ con , LDAP_OPT_SERVER_CONTROLS , $ ctl );
206
+ }
113
207
}
114
208
}
0 commit comments