#!/usr/bin/env perl
##
## Perl-version ldapsearch(1) clone
## Copyright (c) 2007 SATOH Fumiyasu @ OSS Technology Co., Japan
##                    <http://www.osstech.co.jp/>
##
## License: GNU General Public License version 3
## Date: 2007-07-13, since 2007-07-13
##
## This clone command supports LDAPv3 Paged results control (RFC2696),
## but the original ldapsearch(1) from OpenLDAP does not.
## MS Active Directory supports RFC2696 and force LDAP clients to
## use paging to retrieve large number of entires (>1000 by default).
## See also http://support.microsoft.com/default.aspx?scid=kb;en-us;315071,
## `perldoc Net::LDAP` and `perldoc Net::LDAP::Control::Paged`.
## -- fumiyas, 2007-07-13
##

## FIXME: Support ranged attributes. e.g.:
## dn: CN=student,CN=Users,DC=example,DC=jp
## objectClass: top
## objectClass: group
## cn: student
## member;range=0-1499: CN=user1,OU=Users,DC=example,DC=jp
## member;range=0-1499: CN=user2,OU=Users,DC=example,DC=jp
## ...
## member;range=0-1499: CN=user1500,OU=Users,DC=example,DC=jp
## distinguishedName: CN=student,CN=Users,DC=example,DC=jp
## ...

use strict;
use warnings;

use Net::LDAP;
use Net::LDAP::LDIF;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant qw(LDAP_CONTROL_PAGED);

my $server = undef;
my $port = undef;
my $bind_dn = undef;
my $bind_pass = undef;
my $base = undef;
my $scope = 'sub';
my $filter = '(objectClass=*)';
my @attrs = ();
my $page_size = 100;
my $ignore;

my $cmd_usage = "Usage: $0 [OPTIONS] [FILTER [ATTRIBUTE ...]]

Options:
 -H LDAPURI
    Specify URI referring to the ldap server
 -D BINDDN
    Use the Distinguished Name BINDDN to bind to the LDAP directory
 -w PASSWORD
    Use PASSWORD as the password for simple authentication
 -b SEARCHBASE
    Use SEARCHBASE as the starting point for the search
 -s SCOPE
    Specify the scope of the search to be one of 'base', 'one',
    'sub', or 'children'
";

use Getopt::Long;
Getopt::Long::Configure('bundling');
Getopt::Long::Configure('no_ignore_case');
Getopt::Long::Configure('no_auto_abbrev');
GetOptions(
  'h=s' =>	\$server,
  'p=s' =>	\$port,
  'H=s' =>	\$server,
  'x' =>	\$ignore,
  'D=s' =>	\$bind_dn,
  'w=s' =>	\$bind_pass,
  'b=s' =>	\$base,
  's=s' =>	\$scope,
  'L' =>	\$ignore,
);

if (@ARGV) {
  ($filter, @attrs) = @ARGV;
}

if (!defined($server)) {
  die "$0: option '-H SERVER' required\n";
}

if (defined($port) && $server !~ m#^\w+://#) {
  $server .= ":$port";
}
if (!defined($base) && defined($bind_dn) && $bind_dn =~ /,(dc=.*$)/i) {
  $base = $1;
}

my $ldap = Net::LDAP->new($server) || die "$@";
if (defined($bind_dn)) {
  $ldap->bind($bind_dn, 'password' => $bind_pass);
}
else {
  $ldap->bind();
}

my $page = Net::LDAP::Control::Paged->new(size => $page_size);
my @args = (
  'base' =>	$base,
  'scope' =>	$scope,
  'filter' =>	$filter,
  'attrs' =>	\@attrs,
  'control' =>	[$page],
);

my $ldif = Net::LDAP::LDIF->new('-', 'w');
my $cookie;
while (1) {
  my $mesg = $ldap->search(@args);
  $mesg->code && die $mesg->error;

  $ldif->write($mesg->entries);

  my ($resp) = $mesg->control(LDAP_CONTROL_PAGED) or last;
  $cookie = $resp->cookie or last;
  $page->cookie($cookie);
}

if ($cookie) {
  $page->cookie($cookie);
  $page->size(0);
  my $mesg = $ldap->search(@args);
  $ldif->write($mesg->entries);
}

