/[packages]/cauldron/ejabberd/current/SOURCES/mod_vcard_ad.erl
ViewVC logotype

Contents of /cauldron/ejabberd/current/SOURCES/mod_vcard_ad.erl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 233035 - (show annotations) (download)
Mon Apr 23 18:15:51 2012 UTC (12 years ago) by mitya
File size: 17820 byte(s)
Import ejabberd 2.1.10

1
2 %%%----------------------------------------------------------------------
3 %%% File : mod_vcard_ad.erl
4 %%% Author : Stanislav Bogatyrev <realloc@realloc.spb.ru>
5 %%% Author : Alexey Shchepin <alexey@sevcom.net>
6 %%% Author : Alex <agent_007> Gorbachenko (agent_007@immo.ru)
7 %%% Purpose :
8 %%% Created : 2 Jan 2003 by Alexey Shchepin <alexey@sevcom.net>
9 %%% Id : $Id: mod_vcard_ad.erl 437 2005-11-19 01:20:05Z agent_007 $
10 %%%----------------------------------------------------------------------
11
12 -module(mod_vcard_ad).
13 -author('realloc@realloc.spb.ru').
14 -author('alexey@sevcom.net').
15 -author('agent_007@immo.ru').
16 -vsn('$Revision: 437 $ ').
17
18 -behaviour(gen_mod).
19
20 -export([start/2, init/3, stop/1,
21 get_sm_features/5,
22 process_local_iq/3,
23 process_sm_iq/3,
24 remove_user/1]).
25
26 -include("ejabberd.hrl").
27 -include("eldap/eldap.hrl").
28 -include("jlib.hrl").
29
30 -define(PROCNAME, ejabberd_mod_vcard_ad).
31
32 start(Host, Opts) ->
33 IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue),
34 gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD,
35 ?MODULE, process_local_iq, IQDisc),
36 gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_VCARD,
37 ?MODULE, process_sm_iq, IQDisc),
38 ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50),
39 LDAPServers = ejabberd_config:get_local_option({ad_servers, Host}),
40 RootDN = ejabberd_config:get_local_option({ad_rootdn, Host}),
41 Password = ejabberd_config:get_local_option({ad_password, Host}),
42 eldap:start_link("mod_vcard_ad", LDAPServers, 389, RootDN, Password),
43 MyHost = gen_mod:get_opt(host, Opts, "vjud." ++ Host),
44 Search = gen_mod:get_opt(search, Opts, true),
45 register(gen_mod:get_module_proc(Host, ?PROCNAME),
46 spawn(?MODULE, init, [MyHost, Host, Search])).
47
48 init(Host, ServerHost, Search) ->
49 case Search of
50 false ->
51 loop(Host, ServerHost);
52 _ ->
53 ejabberd_router:register_route(Host),
54 loop(Host, ServerHost)
55 end.
56
57 loop(Host, ServerHost) ->
58 receive
59 {route, From, To, Packet} ->
60 case catch do_route(ServerHost, From, To, Packet) of
61 {'EXIT', Reason} ->
62 ?ERROR_MSG("~p", [Reason]);
63 _ ->
64 ok
65 end,
66 loop(Host, ServerHost);
67 stop ->
68 ejabberd_router:unregister_route(Host),
69 ok;
70 _ ->
71 loop(Host, ServerHost)
72 end.
73
74 stop(Host) ->
75 gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD),
76 gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_VCARD),
77 ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50),
78 Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
79 Proc ! stop,
80 {wait, Proc}.
81
82 get_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) ->
83 Acc;
84 get_sm_features(Acc, _From, _To, Node, _Lang) ->
85 case Node of
86 [] ->
87 case Acc of
88 {result, Features} ->
89 {result, [?NS_VCARD | Features]};
90 empty ->
91 {result, [?NS_VCARD]}
92 end;
93 _ ->
94 Acc
95 end.
96
97 process_local_iq(_From, _To, #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) ->
98 case Type of
99 set ->
100 IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]};
101 get ->
102 IQ#iq{type = result,
103 sub_el = [{xmlelement, "vCard",
104 [{"xmlns", ?NS_VCARD}],
105 [{xmlelement, "FN", [],
106 [{xmlcdata, "ejabberd"}]},
107 {xmlelement, "URL", [],
108 [{xmlcdata,
109 "http://ejabberd.jabberstudio.org/"}]},
110 {xmlelement, "DESC", [],
111 [{xmlcdata,
112 translate:translate(
113 Lang,
114 "Erlang Jabber Server\n"
115 "Copyright (c) 2002-2005 Alexey Shchepin")}]},
116 {xmlelement, "BDAY", [],
117 [{xmlcdata, "2002-11-16"}]}
118 ]}]}
119 end.
120
121 find_ldap_user(Host, User) ->
122 Attr = ejabberd_config:get_local_option({ad_uidattr, Host}),
123 Filter = eldap:equalityMatch(Attr, User),
124 Base = ejabberd_config:get_local_option({ad_base, Host}),
125 case eldap:search("mod_vcard_ad", [{base, Base},
126 {filter, Filter},
127 {attributes, []}]) of
128 #eldap_search_result{entries = [E | _]} ->
129 E;
130 _ ->
131 false
132 end.
133
134 is_attribute_read_allowed(Name,From,To) ->
135 true.
136
137 ldap_attribute_to_vcard(Prefix,{Name,Values},From,To) ->
138 case is_attribute_read_allowed(Name,From,To) of
139 true ->
140 ldap_lca_to_vcard(Prefix,stringprep:tolower(Name),Values);
141 _ ->
142 none
143 end.
144
145 ldap_lca_to_vcard(vCard,"displayname",[Value|_]) ->
146 {xmlelement,"FN",[],[{xmlcdata,Value}]};
147
148 ldap_lca_to_vcard(vCard,"cn",[Value|_]) ->
149 {xmlelement,"NICKNAME",[],[{xmlcdata,Value}]};
150
151 ldap_lca_to_vcard(vCard,"title",[Value|_]) ->
152 {xmlelement,"TITLE",[],[{xmlcdata,Value}]};
153
154 ldap_lca_to_vcard(vCard,"wwwhomepage",[Value|_]) ->
155 {xmlelement,"URL",[],[{xmlcdata,Value}]};
156
157 ldap_lca_to_vcard(vCard,"description",[Value|_]) ->
158 {xmlelement,"DESC",[],[{xmlcdata,Value}]};
159
160 ldap_lca_to_vcard(vCard,"telephonenumber",[Value|_]) ->
161 {xmlelement,"TEL",[],[{xmlelement,"VOICE",[],[]},
162 {xmlelement,"WORK",[],[]},
163 {xmlelement,"NUMBER",[],[{xmlcdata,Value}]}]};
164
165 ldap_lca_to_vcard(vCard,"mail",[Value|_]) ->
166 {xmlelement,"EMAIL",[],[{xmlelement,"INTERNET",[],[]},
167 {xmlelement,"PREF",[],[]},
168 {xmlelement,"USERID",[],[{xmlcdata,Value}]}]};
169
170 ldap_lca_to_vcard(vCardN,"sn",[Value|_]) ->
171 {xmlelement,"FAMILY",[],[{xmlcdata,Value}]};
172
173 ldap_lca_to_vcard(vCardN,"givenname",[Value|_]) ->
174 {xmlelement,"GIVEN",[],[{xmlcdata,Value}]};
175
176 ldap_lca_to_vcard(vCardN,"initials",[Value|_]) ->
177 {xmlelement,"MIDDLE",[],[{xmlcdata,Value}]};
178
179 ldap_lca_to_vcard(vCardAdr,"streetaddress",[Value|_]) ->
180 {xmlelement,"STREET",[],[{xmlcdata,Value}]};
181 ldap_lca_to_vcard(vCardAdr,"co",[Value|_]) ->
182 {xmlelement,"CTRY",[],[{xmlcdata,Value}]};
183 ldap_lca_to_vcard(vCardAdr,"l",[Value|_]) ->
184 {xmlelement,"LOCALITY",[],[{xmlcdata,Value}]};
185 ldap_lca_to_vcard(vCardAdr,"st",[Value|_]) ->
186 {xmlelement,"REGION",[],[{xmlcdata,Value}]};
187 ldap_lca_to_vcard(vCardAdr,"postalcode",[Value|_]) ->
188 {xmlelement,"PCODE",[],[{xmlcdata,Value}]};
189 ldap_lca_to_vcard(vCardO,"company",[Value|_]) ->
190 {xmlelement,"ORGNAME",[],[{xmlcdata,Value}]};
191
192 ldap_lca_to_vcard(vCardO,"department",[Value|_]) ->
193 {xmlelement,"ORGUNIT",[],[{xmlcdata,Value}]};
194
195 ldap_lca_to_vcard(_,_,_) -> none.
196
197 ldap_attributes_to_vcard(Attributes,From,To) ->
198 Elts = lists:map(fun(Attr) ->
199 ldap_attribute_to_vcard(vCard,Attr,From,To)
200 end,Attributes),
201 FElts = [ X || X <- Elts, X /= none ],
202 NElts = lists:map(fun(Attr) ->
203 ldap_attribute_to_vcard(vCardN,Attr,From,To)
204 end,Attributes),
205 FNElts = [ X || X <- NElts, X /= none ],
206
207 ADRElts = lists:map(fun(Attr) ->
208 ldap_attribute_to_vcard(vCardAdr,Attr,From,To)
209 end,Attributes),
210 FADRElts = [ X || X <- ADRElts, X /= none ],
211
212 OElts = lists:map(fun(Attr) ->
213 ldap_attribute_to_vcard(vCardO,Attr,From,To)
214 end,Attributes),
215 FOElts = [ X || X <- OElts, X /= none ],
216 [{xmlelement, "vCard", [{"xmlns", ?NS_VCARD}],
217 lists:append(FElts,
218 [{xmlelement,"N",[],FNElts},
219 {xmlelement,"ADR",[],FADRElts},
220 {xmlelement,"ORG",[],FOElts}])
221 }].
222
223 process_sm_iq(From, To, #iq{type = Type, sub_el = SubEl} = IQ) ->
224 case Type of
225 set ->
226 IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]};
227 get ->
228 #jid{luser = LUser, lserver = LServer} = To,
229 case find_ldap_user(LServer, LUser) of
230 #eldap_entry{attributes = Attributes} ->
231 Vcard = ldap_attributes_to_vcard(Attributes,From,To),
232 IQ#iq{type = result, sub_el = Vcard};
233 _ ->
234 IQ#iq{type = result, sub_el = []}
235 end
236 end.
237
238 -define(TLFIELD(Type, Label, Var),
239 {xmlelement, "field", [{"type", Type},
240 {"label", translate:translate(Lang, Label)},
241 {"var", Var}], []}).
242
243
244 -define(FORM(JID),
245 [{xmlelement, "instructions", [],
246 [{xmlcdata, translate:translate(Lang, "You need an x:data capable client to search")}]},
247 {xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}],
248 [{xmlelement, "title", [],
249 [{xmlcdata, translate:translate(Lang, "Search users in ") ++
250 jlib:jid_to_string(JID)}]},
251 {xmlelement, "instructions", [],
252 [{xmlcdata, translate:translate(Lang, "Fill in fields to search "
253 "for any matching Jabber User")}]},
254 ?TLFIELD("text-single", "User", "user"),
255 ?TLFIELD("text-single", "Full Name", "fn"),
256 ?TLFIELD("text-single", "Given Name", "given"),
257 ?TLFIELD("text-single", "Middle Name", "middle"),
258 ?TLFIELD("text-single", "Family Name", "family"),
259 ?TLFIELD("text-single", "Nickname", "nickname"),
260 ?TLFIELD("text-single", "Birthday", "bday"),
261 ?TLFIELD("text-single", "Country", "ctry"),
262 ?TLFIELD("text-single", "City", "locality"),
263 ?TLFIELD("text-single", "email", "email"),
264 ?TLFIELD("text-single", "Organization Name", "orgname"),
265 ?TLFIELD("text-single", "Organization Unit", "orgunit")
266 ]}]).
267
268
269
270
271 do_route(ServerHost, From, To, Packet) ->
272 #jid{user = User, resource = Resource} = To,
273 if
274 (User /= "") or (Resource /= "") ->
275 Err = jlib:make_error_reply(Packet, ?ERR_SERVICE_UNAVAILABLE),
276 ejabberd_router ! {route, To, From, Err};
277 true ->
278 IQ = jlib:iq_query_info(Packet),
279 case IQ of
280 #iq{type = Type, xmlns = ?NS_SEARCH, lang = Lang, sub_el = SubEl} ->
281 case Type of
282 set ->
283 XDataEl = find_xdata_el(SubEl),
284 case XDataEl of
285 false ->
286 Err = jlib:make_error_reply(
287 Packet, ?ERR_BAD_REQUEST),
288 ejabberd_router:route(To, From, Err);
289 _ ->
290 XData = jlib:parse_xdata_submit(XDataEl),
291 case XData of
292 invalid ->
293 Err = jlib:make_error_reply(
294 Packet,
295 ?ERR_BAD_REQUEST),
296 ejabberd_router:route(To, From,
297 Err);
298 _ ->
299 ResIQ =
300 IQ#iq{
301 type = result,
302 sub_el =
303 [{xmlelement,
304 "query",
305 [{"xmlns", ?NS_SEARCH}],
306 [{xmlelement, "x",
307 [{"xmlns", ?NS_XDATA},
308 {"type", "result"}],
309 search_result(Lang, To, ServerHost, XData)
310 }]}]},
311 ejabberd_router:route(
312 To, From, jlib:iq_to_xml(ResIQ))
313 end
314 end;
315 get ->
316 ResIQ = IQ#iq{type = result,
317 sub_el = [{xmlelement,
318 "query",
319 [{"xmlns", ?NS_SEARCH}],
320 ?FORM(To)
321 }]},
322 ejabberd_router:route(To,
323 From,
324 jlib:iq_to_xml(ResIQ))
325 end;
326 #iq{type = Type, xmlns = ?NS_DISCO_INFO, sub_el = SubEl} ->
327 case Type of
328 set ->
329 Err = jlib:make_error_reply(
330 Packet, ?ERR_NOT_ALLOWED),
331 ejabberd_router:route(To, From, Err);
332 get ->
333 ResIQ =
334 IQ#iq{type = result,
335 sub_el = [{xmlelement,
336 "query",
337 [{"xmlns", ?NS_DISCO_INFO}],
338 [{xmlelement, "identity",
339 [{"category", "directory"},
340 {"type", "user"},
341 {"name",
342 "vCard User Search"}],
343 []},
344 {xmlelement, "feature",
345 [{"var", ?NS_SEARCH}], []},
346 {xmlelement, "feature",
347 [{"var", ?NS_VCARD}], []}
348 ]
349 }]},
350 ejabberd_router:route(To,
351 From,
352 jlib:iq_to_xml(ResIQ))
353 end;
354 #iq{type = Type, xmlns = ?NS_DISCO_ITEMS, sub_el = SubEl} ->
355 case Type of
356 set ->
357 Err = jlib:make_error_reply(
358 Packet, ?ERR_NOT_ALLOWED),
359 ejabberd_router:route(To, From, Err);
360 get ->
361 ResIQ =
362 IQ#iq{type = result,
363 sub_el = [{xmlelement,
364 "query",
365 [{"xmlns", ?NS_DISCO_ITEMS}],
366 []}]},
367 ejabberd_router:route(To,
368 From,
369 jlib:iq_to_xml(ResIQ))
370 end;
371 #iq{type = get, xmlns = ?NS_VCARD, lang = Lang} ->
372 ResIQ =
373 IQ#iq{type = result,
374 sub_el = [{xmlelement,
375 "vCard",
376 [{"xmlns", ?NS_VCARD}],
377 iq_get_vcard(Lang)}]},
378 ejabberd_router:route(To,
379 From,
380 jlib:iq_to_xml(ResIQ));
381 _ ->
382 Err = jlib:make_error_reply(Packet,
383 ?ERR_SERVICE_UNAVAILABLE),
384 ejabberd_router:route(To, From, Err)
385 end
386 end.
387
388 iq_get_vcard(Lang) ->
389 [{xmlelement, "FN", [],
390 [{xmlcdata, "ejabberd/mod_vcard"}]},
391 {xmlelement, "URL", [],
392 [{xmlcdata,
393 "http://ejabberd.jabberstudio.org/"}]},
394 {xmlelement, "DESC", [],
395 [{xmlcdata, translate:translate(
396 Lang,
397 "ejabberd vCard module\n"
398 "Copyright (c) 2003-2005 Alexey Shchepin")}]}].
399
400 find_xdata_el({xmlelement, _Name, _Attrs, SubEls}) ->
401 find_xdata_el1(SubEls).
402
403 find_xdata_el1([]) ->
404 false;
405 find_xdata_el1([{xmlelement, Name, Attrs, SubEls} | Els]) ->
406 case xml:get_attr_s("xmlns", Attrs) of
407 ?NS_XDATA ->
408 {xmlelement, Name, Attrs, SubEls};
409 _ ->
410 find_xdata_el1(Els)
411 end;
412 find_xdata_el1([_ | Els]) ->
413 find_xdata_el1(Els).
414
415 -define(LFIELD(Label, Var),
416 {xmlelement, "field", [{"label", translate:translate(Lang, Label)},
417 {"var", Var}], []}).
418
419 search_result(Lang, JID, ServerHost, Data) ->
420 [{xmlelement, "title", [],
421 [{xmlcdata, translate:translate(Lang, "Results of search in ") ++
422 jlib:jid_to_string(JID)}]},
423 {xmlelement, "reported", [],
424 [?LFIELD("JID", "jid"),
425 ?LFIELD("Full Name", "fn"),
426 ?LFIELD("Given Name", "given"),
427 ?LFIELD("Middle Name", "middle"),
428 ?LFIELD("Family Name", "family"),
429 ?LFIELD("Nickname", "nickname"),
430 ?LFIELD("Birthday", "bday"),
431 ?LFIELD("Country", "ctry"),
432 ?LFIELD("City", "locality"),
433 ?LFIELD("email", "email"),
434 ?LFIELD("Organization Name", "orgname"),
435 ?LFIELD("Organization Unit", "orgunit")
436 ]}] ++ lists:map(fun(E) ->
437 record_to_item(E#eldap_entry.attributes)
438 end, search(ServerHost, Data)).
439
440 -define(FIELD(Var, Val),
441 {xmlelement, "field", [{"var", Var}],
442 [{xmlelement, "value", [],
443 [{xmlcdata, Val}]}]}).
444
445 case_exact_compare(none,_) ->
446 false;
447 case_exact_compare(_,none) ->
448 false;
449 case_exact_compare(X,Y) ->
450 X > Y.
451
452 ldap_sort_entries(L) ->
453 lists:sort(fun(E1,E2) ->
454 case_exact_compare(ldap_get_value(E1,"cn"),ldap_get_value(E2,"cn"))
455 end,L).
456
457 ldap_get_value(E,Attribute) ->
458 #eldap_entry{attributes = Attributes} = E,
459 case lists:filter(fun({A,_}) ->
460 string:equal(A,Attribute)
461 end,Attributes) of
462 [{Attr,[Value|_]}] ->
463 Value;
464 _ ->
465 none
466 end.
467
468 ldap_attribute_to_item("samaccountname",Value) ->
469 [
470 ?FIELD("jid",Value ++ "@" ++ ?MYNAME),
471 ?FIELD("uid",Value)
472 ];
473
474 ldap_attribute_to_item("cn",Value) ->
475 [
476 ?FIELD("nickname",Value)
477 ];
478
479 ldap_attribute_to_item("displayname",Value) ->
480 [
481 ?FIELD("fn",Value)
482 ];
483
484 ldap_attribute_to_item("sn",Value) ->
485 [
486 ?FIELD("family",Value)
487 ];
488 ldap_attribute_to_item("co",Value) ->
489 [
490 ?FIELD("ctry",Value)
491 ];
492 ldap_attribute_to_item("l",Value) ->
493 [
494 ?FIELD("locality",Value)
495 ];
496
497 ldap_attribute_to_item("givenname",Value) ->
498 [
499 ?FIELD("given",Value)
500 ];
501
502 ldap_attribute_to_item("initials",Value) ->
503 [
504 ?FIELD("middle",Value)
505 ];
506
507 ldap_attribute_to_item("mail",Value) ->
508 [
509 ?FIELD("email",Value)
510 ];
511
512 ldap_attribute_to_item("company",Value) ->
513 [
514 ?FIELD("orgname",Value)
515 ];
516
517 ldap_attribute_to_item("department",Value) ->
518 [
519 ?FIELD("orgunit",Value)
520 ];
521
522 ldap_attribute_to_item(_,_) ->
523 [none].
524
525 record_to_item(Attributes) ->
526 List = lists:append(lists:map(fun({Attr,[Value|_]}) ->
527 ldap_attribute_to_item(stringprep:tolower(Attr),Value)
528 end,Attributes)),
529 FList = [X || X <- List, X /= none],
530 {xmlelement, "item", [],FList}.
531
532 search(LServer, Data) ->
533 AdGroup = ejabberd_config:get_local_option({ad_group, LServer}),
534 FilterDef = make_filter(Data),
535 FilterPerson = eldap:equalityMatch("objectCategory", "person"),
536 FilterComp = eldap:equalityMatch("objectClass", "computer"),
537 FilterHidden = eldap:equalityMatch("description", "hidden"),
538 FilterGroup = eldap:equalityMatch("memberOf", AdGroup),
539 FilterLive = eldap:equalityMatch("userAccountControl", "66050"),
540 Filter = eldap:'and'([
541 FilterDef,
542 FilterPerson,
543 FilterGroup,
544 eldap:'not'(FilterComp),
545 eldap:'not'(FilterHidden),
546 eldap:'not'(FilterLive)]),
547 Base = ejabberd_config:get_local_option({ad_base, LServer}),
548 UIDAttr = ejabberd_config:get_local_option({ad_uidattr, LServer}),
549 case eldap:search("mod_vcard_ad",[{base, Base},
550 {filter, Filter},
551 {attributes, []}]) of
552 #eldap_search_result{entries = E} ->
553 [X || X <- E,
554 ejabberd_auth:is_user_exists(
555 ldap_get_value(X, UIDAttr), LServer)];
556 Err ->
557 ?ERROR_MSG("Bad search: ~p", [[LServer, {base, Base},
558 {filter, Filter},
559 {attributes, []}]])
560 end.
561
562
563 make_filter(Data) ->
564 Filter = [X || X <- lists:map(fun(R) ->
565 make_assertion(R)
566 end, Data),
567 X /= none ],
568 case Filter of
569 [F] ->
570 F;
571 _ ->
572 eldap:'and'(Filter)
573 end.
574
575
576 make_assertion("givenName",Value) ->
577 eldap:substrings("givenName",[{any,Value}]);
578
579 make_assertion("cn",Value) ->
580 eldap:substrings("cn",[{any,Value}]);
581
582 make_assertion("sn",Value) ->
583 eldap:substrings("sn",[{any,Value}]);
584
585 make_assertion(Attr, Value) ->
586 eldap:equalityMatch(Attr,Value).
587
588 make_assertion({SVar, [Val]}) ->
589 LAttr = ldap_attribute(SVar),
590 case LAttr of
591 none ->
592 none;
593 _ ->
594 if
595 is_list(Val) and (Val /= "") ->
596 make_assertion(LAttr,Val);
597 true ->
598 none
599 end
600 end.
601
602 ldap_attribute("user") ->
603 "samaccountname";
604
605 ldap_attribute("fn") ->
606 "cn";
607
608 ldap_attribute("family") ->
609 "sn";
610
611 ldap_attribute("given") ->
612 "givenName";
613
614 ldap_attribute("middle") ->
615 "initials";
616
617 ldap_attribute("email") ->
618 "mail";
619
620 ldap_attribute("orgname") ->
621 "company";
622
623 ldap_attribute("orgunit") ->
624 "department";
625
626 ldap_attribute(_) ->
627 none.
628
629 remove_user(User) ->
630 true.
631

  ViewVC Help
Powered by ViewVC 1.1.30