# Copyright (c) 1997-2004 Graham Barr <gbarr@pobox.com>. All rights reserved.
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
package Net::LDAP::Entry;
use strict;
use Net::LDAP::ASN qw(LDAPEntry);
use Net::LDAP::Constant qw(LDAP_LOCAL_ERROR LDAP_OTHER);
use constant CHECK_UTF8 => $] > 5.007;
BEGIN {
require Encode
if (CHECK_UTF8);
}
our $VERSION = '0.28';
sub new {
my $self = shift;
my $type = ref($self) || $self;
my $entry = bless { changetype => 'add', changes => [] }, $type;
@_ and $entry->dn( shift );
@_ and $entry->add( @_ );
return $entry;
}
sub clone {
my $self = shift;
my $clone = $self->new();
$clone->dn($self->dn());
foreach ($self->attributes()) {
$clone->add($_ => [$self->get_value($_)]);
}
$clone->{changetype} = $self->{changetype};
my @changes = @{$self->{changes}};
while (my($action, $cmd) = splice(@changes, 0, 2)) {
my @new_cmd;
my @cmd = @$cmd;
while (my($type, $val) = splice(@cmd, 0, 2)) {
push @new_cmd, $type, [ @$val ];
}
push @{$clone->{changes}}, $action, \@new_cmd;
}
$clone;
}
# Build attrs cache, created when needed
sub _build_attrs {
+{ map { (lc($_->{type}), $_->{vals}) } @{$_[0]->{asn}{attributes}} };
}
# If we are passed an ASN structure we really do nothing
sub decode {
my $self = shift;
my $result = ref($_[0]) ? shift : $LDAPEntry->decode(shift)
or return;
my %arg = @_;
%{$self} = ( asn => $result, changetype => 'modify', changes => []);
if (CHECK_UTF8 && $arg{raw}) {
$result->{objectName} = Encode::decode_utf8($result->{objectName})
if ('dn' !~ /$arg{raw}/);
foreach my $elem (@{$self->{asn}{attributes}}) {
map { $_ = Encode::decode_utf8($_) } @{$elem->{vals}}
if ($elem->{type} !~ /$arg{raw}/);
}
}
$self;
}
sub encode {
$LDAPEntry->encode( shift->{asn} );
}
sub dn {
my $self = shift;
@_ ? ($self->{asn}{objectName} = shift) : $self->{asn}{objectName};
}
sub get_attribute {
require Carp;
Carp::carp('->get_attribute deprecated, use ->get_value') if $^W;
shift->get_value(@_, asref => !wantarray);
}
sub get {
require Carp;
Carp::carp('->get deprecated, use ->get_value') if $^W;
shift->get_value(@_, asref => !wantarray);
}
sub exists {
my $self = shift;
my $type = lc(shift);
my $attrs = $self->{attrs} ||= _build_attrs($self);
exists $attrs->{$type};
}
sub get_value {
my $self = shift;
my $type = lc(shift);
my %opt = @_;
if ($opt{alloptions}) {
my %ret = map {
$_->{type} =~ /^\Q$type\E((?:;.*)?)$/i ? (lc($1), $_->{vals}) : ()
} @{$self->{asn}{attributes}};
return %ret ? \%ret : undef;
}
my $attrs = $self->{attrs} ||= _build_attrs($self);
my $attr = $attrs->{$type} or return;
return $opt{asref}
? $attr
: wantarray
? @{$attr}
: $attr->[0];
}
sub changetype {
my $self = shift;
return $self->{changetype} unless @_;
$self->{changes} = [];
$self->{changetype} = shift;
return $self;
}
sub add {
my $self = shift;
my $cmd = $self->{changetype} eq 'modify' ? [] : undef;
my $attrs = $self->{attrs} ||= _build_attrs($self);
while (my($type, $val) = splice(@_, 0, 2)) {
my $lc_type = lc $type;
push @{$self->{asn}{attributes}}, { type => $type, vals => ($attrs->{$lc_type}=[])}
unless exists $attrs->{$lc_type};
push @{$attrs->{$lc_type}}, ref($val) ? @$val : $val;
push @$cmd, $type, [ ref($val) ? @$val : $val ]
if $cmd;
}
push(@{$self->{changes}}, 'add', $cmd) if $cmd;
return $self;
}
sub replace {
my $self = shift;
my $cmd = $self->{changetype} eq 'modify' ? [] : undef;
my $attrs = $self->{attrs} ||= _build_attrs($self);
while (my($type, $val) = splice(@_, 0, 2)) {
my $lc_type = lc $type;
if (defined($val) and (!ref($val) or @$val)) {
push @{$self->{asn}{attributes}}, { type => $type, vals => ($attrs->{$lc_type}=[])}
unless exists $attrs->{$lc_type};
@{$attrs->{$lc_type}} = ref($val) ? @$val : ($val);
push @$cmd, $type, [ ref($val) ? @$val : $val ]
if $cmd;
}
else {
delete $attrs->{$lc_type};
@{$self->{asn}{attributes}}
= grep { $lc_type ne lc($_->{type}) } @{$self->{asn}{attributes}};
push @$cmd, $type, []
if $cmd;
}
}
push(@{$self->{changes}}, 'replace', $cmd) if $cmd;
return $self;
}
sub delete {
my $self = shift;
unless (@_) {
$self->changetype('delete');
return $self;
}
my $cmd = $self->{changetype} eq 'modify' ? [] : undef;
my $attrs = $self->{attrs} ||= _build_attrs($self);
while (my($type, $val) = splice(@_, 0, 2)) {
my $lc_type = lc $type;
if (defined($val) and (!ref($val) or @$val)) {
my %values;
@values{(ref($val) ? @$val : $val)} = ();
unless (@{$attrs->{$lc_type}}
= grep { !exists $values{$_} } @{$attrs->{$lc_type}})
{
delete $attrs->{$lc_type};
@{$self->{asn}{attributes}}
= grep { $lc_type ne lc($_->{type}) } @{$self->{asn}{attributes}};
}
push @$cmd, $type, [ ref($val) ? @$val : $val ]
if $cmd;
}
else {
delete $attrs->{$lc_type};
@{$self->{asn}{attributes}}
= grep { $lc_type ne lc($_->{type}) } @{$self->{asn}{attributes}};
push @$cmd, $type, [] if $cmd;
}
}
push(@{$self->{changes}}, 'delete', $cmd) if $cmd;
return $self;
}
sub update {
my $self = shift;
my $target = shift; # a Net::LDAP or a Net::LDAP::LDIF object
my %opt = @_;
my $mesg;
my $user_cb = delete $opt{callback};
my $cb = sub { $self->changetype('modify') unless $_[0]->code;
$user_cb->(@_) if $user_cb };
if (eval { $target->isa('Net::LDAP') }) {
if ($self->{changetype} eq 'add') {
$mesg = $target->add($self, callback => $cb, %opt);
}
elsif ($self->{changetype} eq 'delete') {
$mesg = $target->delete($self, callback => $cb, %opt);
}
elsif ($self->{changetype} =~ /modr?dn/o) {
my @args = (newrdn => $self->get_value('newrdn') || undef,
deleteoldrdn => $self->get_value('deleteoldrdn') || undef);
my $newsuperior = $self->get_value('newsuperior');
push(@args, newsuperior => $newsuperior) if $newsuperior;
$mesg = $target->moddn($self, @args, callback => $cb, %opt);
}
elsif (@{$self->{changes}}) {
$mesg = $target->modify($self, changes => $self->{changes}, callback => $cb, %opt);
}
else {
require Net::LDAP::Message;
$mesg = Net::LDAP::Message->new( $target );
$mesg->set_error(LDAP_LOCAL_ERROR, 'No attributes to update');
}
}
elsif (eval { $target->isa('Net::LDAP::LDIF') }) {
require Net::LDAP::Message;
$target->write_entry($self, %opt);
$mesg = Net::LDAP::Message::Dummy->new();
$mesg->set_error(LDAP_OTHER, $target->error())
if ($target->error());
}
else {
$mesg = Net::LDAP::Message::Dummy->new();
$mesg->set_error(LDAP_OTHER, 'illegal update target');
}
return $mesg;
}
sub ldif {
my $self = shift;
my %opt = @_;
require Net::LDAP::LDIF;
open(my $fh, '>', \my $buffer);
my $change = exists $opt{change} ? $opt{change} : $self->changes ? 1 : 0;
my $ldif = Net::LDAP::LDIF->new($fh, 'w', %opt, version => 0, change => $change);
$ldif->write_entry($self);
return $buffer;
}
# Just for debugging
sub dump {
my $self = shift;
no strict 'refs'; # select may return a GLOB name
my $fh = @_ ? shift : select;
my $asn = $self->{asn};
print $fh '-' x 72, "\n";
print $fh 'dn:', $asn->{objectName}, "\n\n" if $asn->{objectName};
my $l = 0;
for (keys %{ $self->{attrs} ||= _build_attrs($self) }) {
$l = length if length > $l;
}
my $spc = "\n " . ' ' x $l;
foreach my $attr (@{$asn->{attributes}}) {
my $val = $attr->{vals};
printf $fh "%${l}s: ", $attr->{type};
my $i = 0;
foreach my $v (@$val) {
print $fh $spc if $i++;
print $fh $v;
}
print $fh "\n";
}
}
sub attributes {
my $self = shift;
my %opt = @_;
if ($opt{nooptions}) {
my %done;
return map {
$_->{type} =~ /^([^;]+)/;
$done{lc $1}++ ? () : ($1);
} @{$self->{asn}{attributes}};
}
else {
return map { $_->{type} } @{$self->{asn}{attributes}};
}
}
sub asn {
shift->{asn}
}
sub changes {
my $ref = shift->{changes};
$ref ? @$ref : ();
}
1;