I was struck by the same problem when developing Zend_Ldap for the Zend Framework. I will try to explain what the real problem is, but to make it short: before PHP 5.4, it was not possible to use paged results from Active Directory with an unsupported version of PHP ( ext/ldap ) due to the restriction in this extension .
Try to figure it all out ... Microsoft Active Directory uses the so-called server-side control to make a server-side paging. This control is described in RFC 2696, “Extending LDAP Controls for Simple Paging Manipulation” .
ext/php offers access to LDAP control extensions through ldap_set_option() and LDAP_OPT_SERVER_CONTROLS and LDAP_OPT_CLIENT_CONTROLS respectively. To set up a paged control, you need an oid control that is 1.2.840.113556.1.4.319 , and we need to know how to encode the control value (this is described in the RFC ). The value is an octet string wrapping a BER-encoded version of the following SEQUENCE (copied from RFC):
realSearchControlValue ::= SEQUENCE { size INTEGER (0..maxInt), -- requested page size from client -- result set size estimate from server cookie OCTET STRING }
Thus, we can establish the appropriate server control before executing the LDAP request:
$pageSize = 100; $pageControl = array( 'oid' => '1.2.840.113556.1.4.319', // the control-oid 'iscritical' => true, // the operation should fail if the server is not able to support this control 'value' => sprintf ("%c%c%c%c%c%c%c", 48, 5, 2, 1, $pageSize, 4, 0) // the required BER-encoded control-value );
This allows us to send a page request to the LDAP / AD server. But how do we know if there are more pages, and how do we indicate with what control value we should send our next request?
Here we are stuck ... The server responds with a result set that includes the necessary paging information, but PHP does not have a method for retrieving this information from the result set. PHP provides a wrapper for the LDAP API function ldap_parse_result() , but the last serverctrlsp parameter serverctrlsp not exposed to the PHP function, so there is no way to get the required information. A bug report has been filed for this problem, but since 2005 there has been no response. If the ldap_parse_result() function provided the required parameter, using the paged results will work as
$l = ldap_connect('somehost.mydomain.com'); $pageSize = 100; $pageControl = array( 'oid' => '1.2.840.113556.1.4.319', 'iscritical' => true, 'value' => sprintf ("%c%c%c%c%c%c%c", 48, 5, 2, 1, $pageSize, 4, 0) ); $controls = array($pageControl); ldap_set_option($l, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_bind($l, 'CN=bind-user,OU=my-users,DC=mydomain,DC=com', 'bind-user-password'); $continue = true; while ($continue) { ldap_set_option($l, LDAP_OPT_SERVER_CONTROLS, $controls); $sr = ldap_search($l, 'OU=some-ou,DC=mydomain,DC=com', 'cn=*', array('sAMAccountName'), null, null, null, null); ldap_parse_result ($l, $sr, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls); // (*) if (isset($serverctrls)) { foreach ($serverctrls as $i) { if ($i["oid"] == '1.2.840.113556.1.4.319') { $i["value"]{8} = chr($pageSize); $i["iscritical"] = true; $controls = array($i); break; } } } $info = ldap_get_entries($l, $sr); if ($info["count"] < $pageSize) { $continue = false; } for ($entry = ldap_first_entry($l, $sr); $entry != false; $entry = ldap_next_entry($l, $entry)) { $dn = ldap_get_dn($l, $entry); } }
As you can see, there is one line of code (*) that makes all of this useless. On my way, although rare information on this subject, I found a patch against PHP 4.3.10 ext/ldap from Iñaki Arenaza, but I have not tried it either, and I don’t know if the patch can be applied to PHP5 ext/ldap . The patch extends ldap_parse_result() to show the 7th parameter for PHP:
--- ldap.c 2004-06-01 23: 05: 33.000000000 +0200
+++ /usr/src/php4/php4-4.3.10/ext/ldap/ldap.c 2005-09-03 17: 02: 03.000000000 +0200
@@ -74.7 +74.7 @@
ZEND_DECLARE_MODULE_GLOBALS (ldap)
static unsigned char third_argument_force_ref [] = {3, BYREF_NONE, BYREF_NONE, BYREF_FORCE};
-static unsigned char arg3to6of6_force_ref [] = {6, BYREF_NONE, BYREF_NONE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE};
+ static unsigned char arg3to7of7_force_ref [] = {7, BYREF_NONE, BYREF_NONE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE};
static int le_link, le_result, le_result_entry, le_ber_entry;
@@ -124.7 +124.7 @@
#if (LDAP_API_VERSION> 2000) || HAVE_NSLDAP
PHP_FE (ldap_get_option, third_argument_force_ref)
PHP_FE (ldap_set_option, NULL)
- PHP_FE (ldap_parse_result, arg3to6of6_force_ref)
+ PHP_FE (ldap_parse_result, arg3to7of7_force_ref)
PHP_FE (ldap_first_reference, NULL)
PHP_FE (ldap_next_reference, NULL)
#ifdef HAVE_LDAP_PARSE_REFERENCE
@@ -1775.14 +1775.15 @@
Extract information from result * /
PHP_FUNCTION (ldap_parse_result)
{
- pval ** link, ** result, ** errcode, ** matcheddn, ** errmsg, ** referrals;
+ pval ** link, ** result, ** errcode, ** matcheddn, ** errmsg, ** referrals, ** serverctrls;
ldap_linkdata * ld;
LDAPMessage * ldap_result;
+ LDAPControl ** lserverctrls, ** ctrlp, * ctrl;
char ** lreferrals, ** refp;
char * lmatcheddn, * lerrmsg;
int rc, lerrcode, myargcount = ZEND_NUM_ARGS ();
- if (myargcount 6 || zend_get_parameters_ex (myargcount, & link, & result, & errcode, & matcheddn, & errmsg, & referrals) == FAILURE) {
+ if (myargcount 7 || zend_get_parameters_ex (myargcount, & link, & result, & errcode, & matcheddn, & errmsg, & referrals, & serverctrls) == FAILURE) {
WRONG_PARAM_COUNT;
}
@@ -1793.7 +1794.7 @@
myargcount> 3? & lmatcheddn: NULL,
myargcount> 4? & lerrmsg: NULL,
myargcount> 5? & lreferrals: NULL,
- NULL / * & serverctrls * /,
+ myargcount> 6? & lserverctrls: NULL,
0);
if (rc! = LDAP_SUCCESS) {
php_error (E_WARNING, "% s (): Unable to parse result:% s", get_active_function_name (TSRMLS_C), ldap_err2string (rc));
@@ -1805.6 +1806.29 @@
/ * Reverse -> fall through * /
switch (myargcount) {
+ case 7:
+ zval_dtor (* serverctrls);
+
+ if (lserverctrls! = NULL) {
+ array_init (* serverctrls);
+ ctrlp = lserverctrls;
+
+ while (* ctrlp! = NULL) {
+ zval * ctrl_array;
+
+ ctrl = * ctrlp;
+ MAKE_STD_ZVAL (ctrl_array);
+ array_init (ctrl_array);
+
+ add_assoc_string (ctrl_array, "oid", ctrl-> ldctl_oid, 1);
+ add_assoc_bool (ctrl_array, "iscritical", ctrl-> ldctl_iscritical);
+ add_assoc_stringl (ctrl_array, "value", ctrl-> ldctl_value.bv_val,
+ ctrl-> ldctl_value.bv_len, 1);
+ add_next_index_zval (* serverctrls, ctrl_array);
+ ctrlp ++;
+}
+ ldap_controls_free (lserverctrls);
+}
case 6:
zval_dtor (* referrals);
if (array_init (* referrals) == FAILURE) { In fact, the only remaining option would be to change the Active Directory configuration and increase the maximum result limit. The corresponding option is called MaxPageSize and can be changed using ntdsutil.exe - see "How to View and Install an LDAP Policy in Active Directory Using Ntdsutil.exe" .
EDIT (link to COM):
Or you can go the other way around and use the COM approach through ADODB as suggested in the link provided by eykanal .