web2ldap  1.7.7
About: web2ldap is a full-featured web-based LDAPv3 client.
  Fossies Dox: web2ldap-1.7.7.tar.gz  ("unofficial" and yet experimental doxygen-generated source code documentation)  

search.py
Go to the documentation of this file.
1# -*- coding: ascii -*-
2"""
3web2ldap.app.search: do a search and return results in several formats
4
5web2ldap - a web-based LDAP Client,
6see https://www.web2ldap.de for details
7
8(C) 1998-2022 by Michael Stroeder <michael@stroeder.com>
9
10This software is distributed under the terms of the
11Apache License Version 2.0 (Apache-2.0)
12https://www.apache.org/licenses/LICENSE-2.0
13"""
14
15import time
16import csv
17import urllib.parse
18import binascii
19
20import xlwt
21
22import ldap0
23import ldap0.cidict
24import ldap0.filter
25import ldap0.schema.models
26from ldap0.controls.openldap import SearchNoOpControl
27from ldap0.schema.models import AttributeType
28from ldap0.base import decode_list
29from ldap0.res import SearchReference, SearchResultEntry
30from ldap0.openldap import ldapsearch_cmd
31
32from ..__about__ import __version__
33from ..web import escape_html
34from ..ldaputil import asynch
35from ..ldaputil import has_subordinates
36from ..ldaputil.extldapurl import ExtendedLDAPUrl
37from ..ldapsession import LDAPLimitErrors
38from ..msbase import GrabKeys, CaseinsensitiveStringKeyDict
39from ..web.wsgi import WSGIBytesWrapper
40from . import ErrorExit
41
42from .form import ExportFormatSelect, InclOpAttrsCheckbox
43from .entry import DisplayEntry
44from .schema.syntaxes import syntax_registry
45from .searchform import (
46 w2l_searchform,
47 SEARCH_OPT_ATTR_EXISTS,
48 SEARCH_OPT_ATTR_NOT_EXISTS,
49 SEARCH_SCOPE_STR_ONELEVEL,
50)
51from .gui import (
52 footer,
53 gen_headers,
54 main_menu,
55 top_section,
56)
57
58SEARCH_NOOP_TIMEOUT = 5.0
59
60SEARCH_BOOKMARK_TMPL = """
61<a
62 href="{baseUrl}?{ldapUrl}"
63 target="_blank"
64 rel="bookmark"
65 title="Bookmark for these search results"
66>
67 Bookmark
68</a>
69"""
70
71PAGE_COMMAND_TMPL = """
72<nav><table>
73 <tr>
74 <td width="20%">{0}</td>
75 <td width="20%">{1}</td>
76 <td width="20%">{2}</td>
77 <td width="20%">{3}</td>
78 <td width="20%">{4}</td>
79 </tr>
80</table></nav>
81"""
82
83LDAPERROR_SIZELIMIT_MSG = """
84<p class="ErrorMessage">
85 <strong>
86 Only partial results received. Try to refine search.
87 </strong><br>
88 {error_msg}
89</p>
90"""
91
92LDIF1_HEADER = r"""########################################################################
93# LDIF export by web2ldap %s, see https://www.web2ldap.de
94# Date and time: %s
95# Bind-DN: %s
96# LDAP-URL of search:
97# %s
98########################################################################
99version: 1
100
101"""
102
103is_search_result = {
104 ldap0.RES_SEARCH_ENTRY,
105 ldap0.RES_SEARCH_RESULT,
106}
107
108is_search_reference = {
109 ldap0.RES_SEARCH_REFERENCE,
110}
111
112
113class ExcelSemicolon(csv.excel):
114 """Describe the usual properties of Excel-generated TAB-delimited files."""
115 delimiter = ';'
116
117csv.register_dialect('excel-semicolon', ExcelSemicolon)
118
119
120class LDIFWriter(asynch.LDIFWriter):
121
122 def pre_processing(self):
123 return
124
125 def after_first(self):
126 self._ldif_writer._output_file.set_headers(
128 content_type='text/plain',
129 charset='utf-8',
130 more_headers=[
131 ('Content-Disposition', 'inline; filename=web2ldap-export.ldif'),
132 ]
133 )
134 )
135 asynch.LDIFWriter.pre_processing(self)
136
137
138class PrintableHTMLWriter(asynch.List):
139 """
140 Class for writing a stream LDAP search results to a printable file
141 """
142 _entryResultTypes = is_search_result
143
144 def __init__(self, app, dn, sub_schema, print_template_str_dict):
145 asynch.List.__init__(self, app.ls.l)
146 self._app = app
147 self._dn = dn
148 self._s = sub_schema
149 self._p = print_template_str_dict
150
151 def process_results(self, ignoreResultsNumber=0, processResultsCount=0):
152 asynch.List.process_results(self)
153 #self.allResults.sort()
154 # This should speed up things
155 s2d = self._app.form.s2d
156 print_cols = self._app.cfg_param('print_cols', '4')
157 table = []
158 for res in self.allResults:
159 if not isinstance(res, SearchResultEntry):
160 continue
161 objectclasses = res.entry_s.get('objectclass', res.entry_s.get('objectClass', []))
162 template_oc = list(
163 {ocl.lower() for ocl in objectclasses} & {s.lower() for s in self._p.keys()}
164 )
165 if template_oc:
166 tableentry = CaseinsensitiveStringKeyDict(default='')
167 attr_list = res.entry_s.keys()
168 for attr in attr_list:
169 tableentry[attr] = ', '.join([
170 s2d(attr_value)
171 for attr_value in res.entry_s[attr]
172 ])
173 table.append(self._p[template_oc[0]] % (tableentry))
174 # Output search results as pretty-printable table without buttons
175 top_section(self._app, 'Printable Search Results', [])
176 self._app.outf.write(
177 """
178 <table
179 class="PrintSearchResults"
180 rules="rows"
181 id="PrintTable"
182 summary="Table with search results formatted for printing">
183 """
184 )
185 for i in range(0, len(table), print_cols):
186 td_list = [
187 '<td>%s</td>' % (tc)
188 for tc in table[i:i+print_cols]
189 ]
190 self._app.outf.write('<tr>\n%s</tr>\n' % ('\n'.join(td_list)))
191 self._app.outf.write('</table>\n')
192 footer(self._app)
193 # end of process_results()
194
195
196class CSVWriter(asynch.AsyncSearchHandler):
197 """
198 Class for writing a stream LDAP search results to a CSV file
199 """
200 _entryResultTypes = is_search_result
201 _formular_prefixes = frozenset('@+-=|%')
202
203 def __init__(self, l, f, sub_schema, attr_types, ldap_charset='utf-8'):
204 asynch.AsyncSearchHandler.__init__(self, l)
206 self._csv_writer = csv.writer(f, dialect='excel-semicolon')
207 self._s = sub_schema
208 self._attr_types = attr_types
209 self._ldap_charset = ldap_charset
210
211 def after_first(self):
212 self._output_file.set_headers(
214 content_type='text/csv',
215 charset='utf-8',
216 more_headers=[
217 ('Content-Disposition', 'inline; filename=web2ldap-export.csv'),
218 ]
219 )
220 )
221 self._csv_writer.writerow(self._attr_types)
222
223 def _process_result(self, resultItem):
224 if not isinstance(resultItem, SearchResultEntry):
225 return
226 entry = ldap0.schema.models.Entry(self._s, resultItem.dn_s, resultItem.entry_as)
227 csv_row_list = []
228 for attr_type in self._attr_types:
229 csv_col_value_list = []
230 for attr_value in entry.get(attr_type, [b'']):
231 try:
232 csv_col_value = attr_value.decode(self._ldap_charset)
233 except UnicodeError:
234 csv_col_value = binascii.b2a_base64(attr_value).decode('ascii').replace('\r', '').replace('\n', '')
235 if csv_col_value and csv_col_value[0] in self._formular_prefixes:
236 csv_col_value_list.append("'"+csv_col_value)
237 else:
238 csv_col_value_list.append(csv_col_value)
239 csv_row_list.append('|'.join(csv_col_value_list))
240 self._csv_writer.writerow(csv_row_list)
241
242
243class ExcelWriter(asynch.AsyncSearchHandler):
244 """
245 Class for writing a stream LDAP search results to a Excel file
246 """
247 _entryResultTypes = is_search_result
248
249 def __init__(self, l, f, sub_schema, attr_types, ldap_charset='utf-8'):
250 asynch.AsyncSearchHandler.__init__(self, l)
251 self._f = f
252 self._s = sub_schema
253 self._attr_types = attr_types
254 self._ldap_charset = ldap_charset
255 self._workbook = xlwt.Workbook(encoding='cp1251')
256 self._worksheet = self._workbook.add_sheet('web2ldap_export')
258
259 def after_first(self):
260 self._f.set_headers(
262 content_type='application/vnd.ms-excel',
263 charset='utf-8',
264 more_headers=[
265 ('Content-Disposition', 'inline; filename=web2ldap-export.xls'),
266 ]
267 )
268 )
269 for col in range(len(self._attr_types)):
270 self._worksheet.write(0, col, self._attr_types[col])
271 self._row_counter += 1
272
274 self._workbook.save(self._f)
275
276 def _process_result(self, resultItem):
277 if not isinstance(resultItem, SearchResultEntry):
278 return
279 entry = ldap0.schema.models.Entry(self._s, resultItem.dn_s, resultItem.entry_as)
280 csv_row_list = []
281 for attr_type in self._attr_types:
282 csv_col_value_list = []
283 for attr_value in entry.get(attr_type, [b'']):
284 try:
285 csv_col_value = attr_value.decode(self._ldap_charset)
286 except UnicodeError:
287 csv_col_value = binascii.b2a_base64(attr_value).decode('ascii').replace('\r', '').replace('\n', '')
288 csv_col_value_list.append(csv_col_value)
289 csv_row_list.append('\r\n'.join(csv_col_value_list))
290 for col, val in enumerate(csv_row_list):
291 self._worksheet.write(self._row_counter, col, val)
292 self._row_counter += 1
293
294
295def w2l_search(app):
296 """
297 Search for entries and output results as table, pretty-printable output
298 or LDIF formatted
299 """
300
301 def page_appl_anchor(
302 app,
303 link_text,
304 search_root, filterstr, search_output,
305 search_resminindex, search_resnumber,
306 search_lastmod,
307 num_result_all,
308 link_rel=None,
309 ):
310 display_start_num = search_resminindex+1
311 display_end_num = search_resminindex + search_resnumber
312 if num_result_all is not None:
313 display_end_num = min(display_end_num, num_result_all)
314 if not search_resnumber:
315 link_title = 'Display all search results'
316 else:
317 link_title = 'Display search results %d to %d' % (display_start_num, display_end_num)
318 return app.anchor(
319 'search',
320 link_text.format(display_start_num, display_end_num),
321 [
322 ('dn', app.dn),
323 ('search_root', search_root),
324 ('filterstr', filterstr),
325 ('search_output', search_output),
326 ('search_resminindex', str(search_resminindex)),
327 ('search_resnumber', str(search_resnumber)),
328 ('search_lastmod', str(search_lastmod)),
329 ('scope', str(scope)),
330 ('search_attrs', ','.join(search_attrs)),
331 ],
332 title=link_title,
333 rel=link_rel,
334 )
335 # end of page_appl_anchor()
336
337 scope = app.ldap_url.scope
338 filterstr = app.ldap_url.filterstr
339
340 search_submit = app.form.getInputValue('search_submit', ['Search'])[0]
341 searchform_mode = app.form.getInputValue('searchform_mode', ['exp'])[0]
342
343 if search_submit != 'Search' and searchform_mode == 'adv':
345 app,
346 msg='',
347 filterstr='',
348 scope=scope
349 )
350 return
351
352 # This should speed up things
353 s2d = app.form.s2d
354
355 search_output = app.form.getInputValue('search_output', ['table'])[0]
356 search_opattrs = app.form.getInputValue('search_opattrs', ['no'])[0] == 'yes'
357 search_root = app.form.getInputValue('search_root', [app.dn])[0]
358
359 if scope is None:
360 scope = ldap0.SCOPE_SUBTREE
361
362 search_filter = app.form.getInputValue('filterstr', [filterstr])
363
364 search_mode = app.form.getInputValue('search_mode', ['(&%s)'])[0]
365 search_option = app.form.getInputValue('search_option', [])
366 search_attr = app.form.getInputValue('search_attr', [])
367 search_mr = app.form.getInputValue('search_mr', [None]*len(search_attr))
368 search_string = app.form.getInputValue('search_string', [])
369
370 if not len(search_option) == len(search_attr) == len(search_mr) == len(search_string):
371 raise ErrorExit('Invalid search form data.')
372
373 # If one search value without search attribute was provided
374 # we assume this is used as value for a template in filter string
375 if (
376 search_filter and search_filter[0]
377 and len(search_string) == 1 and search_string[0]
378 and not search_attr[0]
379 ):
380 search_filter = [search_filter[0].format(av=ldap0.filter.escape_str(search_string[0]))]
381
382 # Build LDAP search filter from input data of advanced search form
383 for i in range(len(search_attr)):
384 if not search_attr[i]:
385 # Ignore null-string attribute types
386 continue
387 search_av_string = search_string[i]
388 if not '*' in search_option[i]:
389 # If an exact assertion value is needed we can normalize via plugin class
390 attr_instance = syntax_registry.get_at(
391 app, app.dn, app.schema, search_attr[i], None, entry=None
392 )
393 search_av_string = attr_instance.sanitize(search_av_string.encode(app.ls.charset)).decode(app.ls.charset)
394 if search_mr[i]:
395 search_mr_string = ':%s:' % (search_mr[i])
396 else:
397 search_mr_string = ''
398 if search_av_string or \
399 search_option[i] in {SEARCH_OPT_ATTR_EXISTS, SEARCH_OPT_ATTR_NOT_EXISTS}:
400 search_filter.append(search_option[i].format(
401 at=''.join((search_attr[i], search_mr_string)),
402 av=ldap0.filter.escape_str(search_av_string)
403 ))
404
405 # Wipe out all nullable search_filter list items
406 search_filter = list(filter(None, search_filter))
407
408 if not search_filter:
410 app,
411 msg='Empty search values.',
412 filterstr='',
413 scope=scope
414 )
415 return
416 if len(search_filter) == 1:
417 filterstr = search_filter[0]
418 elif len(search_filter) > 1:
419 filterstr = search_mode % (''.join(search_filter))
420
421 search_resminindex = int(app.form.getInputValue('search_resminindex', ['0'])[0])
422 search_resnumber = int(
423 app.form.getInputValue(
424 'search_resnumber',
425 [str(app.cfg_param('search_resultsperpage', 10))]
426 )[0]
427 )
428
429 search_lastmod = int(app.form.getInputValue('search_lastmod', [-1])[0])
430 if search_lastmod > 0:
431 timestamp_str = time.strftime('%Y%m%d%H%M%S', time.gmtime(time.time()-search_lastmod))
432 if '1.2.840.113556.1.2.2' in app.schema.sed[AttributeType] and \
433 '1.2.840.113556.1.2.3' in app.schema.sed[AttributeType]:
434 # Assume we're searching MS Active Directory
435 filterstr2 = '(&(|(whenCreated>=%s.0Z)(whenChanged>=%s.0Z))%s)' % (
436 timestamp_str, timestamp_str, filterstr,
437 )
438 else:
439 # Assume standard LDAPv3 attributes
440 filterstr2 = '(&(|(createTimestamp>=%sZ)(modifyTimestamp>=%sZ))%s)' % (
441 timestamp_str, timestamp_str, filterstr,
442 )
443 else:
444 filterstr2 = filterstr
445
446 requested_attrs = app.cfg_param('requested_attrs', [])
447
448 search_attrs = [
449 a.strip()
450 for a in app.form.getInputValue(
451 'search_attrs',
452 [','.join(app.ldap_url.attrs or [])]
453 )[0].split(',')
454 if a.strip()
455 ]
456
457 search_attr_set = ldap0.schema.models.SchemaElementOIDSet(app.schema, AttributeType, search_attrs)
458 search_attrs = search_attr_set.names
459
460 search_ldap_url = app.ls.ldap_url(dn=search_root or str(app.naming_context))
461 search_ldap_url.filterstr = filterstr2
462 search_ldap_url.scope = scope
463 search_ldap_url.attrs = search_attrs
464
465 ldap_search_command = ldapsearch_cmd(search_ldap_url)
466
467 read_attr_set = ldap0.schema.models.SchemaElementOIDSet(app.schema, AttributeType, search_attrs)
468 if search_output in {'table', 'print'}:
469 read_attr_set.add('objectClass')
470
471 if search_output == 'print':
472 print_template_filenames_dict = app.cfg_param('print_template', None)
473 if print_template_filenames_dict is None:
474 raise ErrorExit('No templates for printing defined.')
475 print_template_str_dict = CaseinsensitiveStringKeyDict()
476 for ocl in print_template_filenames_dict.keys():
477 try:
478 with open(print_template_filenames_dict[ocl], 'r') as template_file:
479 print_template_str_dict[ocl] = template_file.read()
480 except IOError:
481 pass
482 else:
483 read_attr_set.update(GrabKeys(print_template_str_dict[ocl]).keys)
484 read_attrs = read_attr_set.names
485 result_handler = PrintableHTMLWriter(app, search_root, app.schema, print_template_str_dict)
486
487 elif search_output in {'table', 'raw'}:
488
489 search_tdtemplate = ldap0.cidict.CIDict(app.cfg_param('search_tdtemplate', {}))
490 search_tdtemplate_keys = search_tdtemplate.keys()
491 search_tdtemplate_attrs_lower = ldap0.cidict.CIDict()
492 for ocl in search_tdtemplate_keys:
493 search_tdtemplate_attrs_lower[ocl] = GrabKeys(search_tdtemplate[ocl]).keys
494
495 # Start with operational attributes used to determine subordinate
496 # entries existence/count
497 read_attr_set.update([
498 'subschemaSubentry', 'displayName', 'description', 'structuralObjectClass',
499 'hasSubordinates', 'subordinateCount',
500 'numSubordinates',
501 'numAllSubordinates', # Siemens DirX
502 'countImmSubordinates', 'countTotSubordinates', # Critical Path Directory Server
503 'msDS-Approx-Immed-Subordinates' # MS Active Directory
504 ])
505
506 # Extend with list of attributes to read for displaying results with templates
507 if search_output == 'table':
508 for ocl in search_tdtemplate_keys:
509 read_attr_set.update(GrabKeys(search_tdtemplate[ocl]).keys)
510 read_attr_set.discard('entryDN')
511 read_attrs = read_attr_set.names
512
513 # Create async search handler instance
514 result_handler = asynch.List(app.ls.l)
515
516 elif search_output in {'ldif', 'ldif1'}:
517 # read all attributes
518 read_attrs = (
519 search_attrs
520 or {False:('*',), True:('*', '+')}[app.ls.supports_allop_attr and search_opattrs]+requested_attrs
521 or None
522 )
523 result_handler = LDIFWriter(app.ls.l, WSGIBytesWrapper(app.outf))
524 if search_output == 'ldif1':
525 result_handler.header = LDIF1_HEADER % (
526 __version__,
527 time.strftime(
528 '%A, %Y-%m-%d %H:%M:%S GMT',
529 time.gmtime(time.time())
530 ),
531 repr(app.ls.who),
532 str(search_ldap_url),
533 )
534
535 elif search_output in {'csv', 'excel'}:
536
537 read_attrs = [a for a in search_attrs if not a in {'*', '+'}]
538 if not read_attrs:
539 if searchform_mode == 'base':
540 searchform_mode = 'adv'
542 app,
543 msg='For table-structured export you have to define the attributes to be read!',
544 filterstr=filterstr,
545 scope=scope,
546 search_root=search_root,
547 searchform_mode=searchform_mode,
548 )
549 return
550 if search_output == 'csv':
551 result_handler = CSVWriter(app.ls.l, app.outf, app.schema, read_attrs, ldap_charset=app.ls.charset)
552 elif search_output == 'excel':
553 result_handler = ExcelWriter(app.ls.l, WSGIBytesWrapper(app.outf), app.schema, read_attrs, ldap_charset=app.ls.charset)
554
555 if search_resnumber:
556 search_size_limit = search_resminindex+search_resnumber
557 else:
558 search_size_limit = -1
559
560 try:
561 # Start the search
562 result_handler.start_search(
563 search_root,
564 scope,
565 filterstr2,
566 attrList=read_attrs or None,
567 sizelimit=search_size_limit
568 )
569 except (
570 ldap0.FILTER_ERROR,
571 ldap0.INAPPROPRIATE_MATCHING,
572 ) as err:
573 # Give the user a chance to edit his bad search filter
575 app,
576 msg=' '.join((
577 app.ldap_error_msg(err),
578 s2d(filterstr2),
579 )),
580 filterstr=filterstr,
581 scope=scope
582 )
583 return
584 except ldap0.NO_SUCH_OBJECT as err:
585 if app.dn:
586 raise err
587
588 if search_output in {'table', 'raw'}:
589
590 search_warning = ''
591 max_result_msg = ''
592 num_all_search_results, num_all_search_continuations = None, None
593 num_result_all = None
594 partial_results = 0
595
596 try:
597 result_handler.process_results(
598 search_resminindex, search_resnumber+int(search_resnumber > 0)
599 )
600 except (ldap0.SIZELIMIT_EXCEEDED, ldap0.ADMINLIMIT_EXCEEDED) as err:
601 if search_size_limit < 0 or result_handler.endResultBreak < search_size_limit:
602 search_warning = app.ldap_error_msg(err, template=LDAPERROR_SIZELIMIT_MSG)
603 partial_results = 1
604 resind = result_handler.endResultBreak
605 # Retrieve the overall number of search results by resending the
606 # search request without size limit but with the SearchNoOpControl attached
607 if SearchNoOpControl.controlType in app.ls.supportedControl:
608 try:
609 num_all_search_results, num_all_search_continuations = app.ls.l.noop_search(
610 search_root,
611 scope,
612 filterstr=filterstr2,
613 timeout=SEARCH_NOOP_TIMEOUT,
614 )
615 if num_all_search_results is not None and num_all_search_continuations is not None:
616 num_result_all = num_all_search_results + num_all_search_continuations
617 max_result_msg = '(of %d / %d) ' % (num_all_search_results, num_all_search_continuations)
618 except LDAPLimitErrors:
619 pass
620 except (ldap0.FILTER_ERROR, ldap0.INAPPROPRIATE_MATCHING) as err:
621 # Give the user a chance to edit his bad search filter
623 app,
624 msg=app.ldap_error_msg(err),
625 filterstr=filterstr,
626 scope=scope
627 )
628 return
629 except (ldap0.NO_SUCH_OBJECT, ldap0.UNWILLING_TO_PERFORM) as err:
630 resind = result_handler.endResultBreak
631 if search_root or scope != ldap0.SCOPE_ONELEVEL:
632 # Give the user a chance to edit his bad search filter
634 app,
635 msg=app.ldap_error_msg(err),
636 filterstr=filterstr,
637 scope=scope
638 )
639 return
640 else:
641 partial_results = search_size_limit >= 0 and result_handler.endResultBreak > search_size_limit
642 resind = result_handler.endResultBreak
643
644 search_resminindex = result_handler.beginResultsDropped
645 result_dnlist = result_handler.allResults
646
647 # HACK! Searching the root level the namingContexts is
648 # appended if not already received in search result
649 if not search_root and not result_dnlist and scope == ldap0.SCOPE_ONELEVEL:
650 result_dnlist.extend([
651 SearchResultEntry(result_dn.encode(app.ls.charset), {})
652 for result_dn in app.ls.namingContexts
653 ])
654 resind = len(result_dnlist)
655
656# result_dnlist.sort(key=lambda x: x.dn_s)
657
658 ctx_menu_items = [
659 app.anchor(
660 'searchform', 'Edit Filter',
661 [
662 ('dn', app.dn),
663 ('searchform_mode', 'exp'),
664 ('search_root', search_root),
665 ('filterstr', filterstr),
666 ('search_lastmod', str(search_lastmod)),
667 ('search_attrs', ','.join(search_attrs)),
668 ('scope', str(scope)),
669 ],
670 ),
671 app.anchor(
672 'search', 'Negate search',
673 [
674 ('dn', app.dn),
675 ('search_root', search_root),
676 ('search_output', {False:'raw', True:'table'}[search_output == 'table']),
677 ('scope', str(scope)),
678 ('filterstr', ldap0.filter.negate_filter(filterstr)),
679 ('search_resminindex', str(search_resminindex)),
680 ('search_resnumber', str(search_resnumber)),
681 ('search_lastmod', str(search_lastmod)),
682 ('search_attrs', ','.join(search_attrs)),
683 ],
684 title='Search with negated search filter',
685 ),
686 ]
687
688 if searchform_mode in {'base', 'adv'}:
689 ctx_menu_items.append(
690 app.anchor(
691 'searchform', 'Modify Search',
692 app.form.allInputFields(
693 fields=[
694 ('dn', app.dn),
695 ('searchform_mode', 'adv')
696 ],
697 ignore_fields=('dn', 'searchform_mode'),
698 ),
699 title='Modify search parameters',
700 )
701 )
702
703 search_param_html = """
704 <table>
705 <tr>
706 <td>Scope:</td>
707 <td>%s</td>
708 </tr>
709 <tr>
710 <td>Base DN:</td>
711 <td>%s</td>
712 </tr>
713 <tr>
714 <td>Filter string:</td>
715 <td>%s</td>
716 </tr>
717 </table>
718 """ % (
719 ldap0.ldapurl.SEARCH_SCOPE_STR[scope],
720 s2d(search_root),
721 s2d(filterstr2),
722 )
723
724 if not result_dnlist:
725
726 # Empty search results
727 #--------------------------------------------------
728 app.simple_message(
729 'No Search Results',
730 '<p class="WarningMessage">No entries found.</p>%s' % (search_param_html),
731 main_menu_list=main_menu(app),
732 context_menu_list=ctx_menu_items
733 )
734
735 else:
736
737 # There are search results to be displayed
738 #--------------------------------------------------
739
740 page_command_list = None
741
742 ctx_menu_items.extend([
743 app.anchor(
744 'search',
745 {False:'Raw', True:'Table'}[search_output == 'raw'],
746 [
747 ('dn', app.dn),
748 ('search_root', search_root),
749 ('search_output', {False:'raw', True:'table'}[search_output == 'raw']),
750 ('scope', str(scope)),
751 ('filterstr', filterstr),
752 ('search_resminindex', str(search_resminindex)),
753 ('search_resnumber', str(search_resnumber)),
754 ('search_lastmod', str(search_lastmod)),
755 ('search_attrs', ','.join(search_attrs)),
756 ],
757 title='Display %s of search results' % (
758 {False:'distinguished names', True:'attributes'}[search_output == 'raw']
759 ),
760 ),
761 app.anchor(
762 'delete', 'Delete',
763 [
764 ('dn', search_root),
765 ('filterstr', filterstr2),
766 ('scope', str(scope)),
767 ],
768 ),
769 app.anchor(
770 'bulkmod', 'Bulk modify',
771 [
772 ('dn', search_root),
773 ('filterstr', filterstr2),
774 ('scope', str(scope)),
775 ],
776 ),
777 ])
778
779 if (partial_results and search_size_limit > 0) or search_resminindex:
780
781 page_command_list = 5 * ['&nbsp;']
782 prev_resminindex = max(0, search_resminindex-search_resnumber)
783
784 if search_resminindex > search_resnumber:
785 page_command_list[0] = page_appl_anchor(
786 app,
787 '|&larr;{0}\u2026{1}',
788 search_root, filterstr, search_output,
789 0, search_resnumber,
790 search_lastmod, num_result_all,
791 link_rel='first',
792 )
793
794 if search_resminindex > 0:
795 page_command_list[1] = page_appl_anchor(
796 app,
797 '&larr;{0}\u2026{1}',
798 search_root, filterstr, search_output,
799 max(0, prev_resminindex), search_resnumber,
800 search_lastmod, num_result_all,
801 link_rel='prev',
802 )
803
804 page_command_list[2] = page_appl_anchor(
805 app,
806 'all',
807 search_root, filterstr, search_output,
808 0, 0,
809 search_lastmod, num_result_all,
810 )
811
812 if partial_results:
813
814 page_next_link = page_appl_anchor(
815 app,
816 '{0}\u2026{1}&rarr;',
817 search_root, filterstr, search_output,
818 search_resminindex+search_resnumber, search_resnumber,
819 search_lastmod, num_result_all,
820 link_rel='next',
821 )
822
823 if num_result_all is not None and resind < num_result_all:
824 page_command_list[3] = page_next_link
825 page_command_list[4] = page_appl_anchor(
826 app,
827 '{0}\u2026{1}&rarr;|',
828 search_root, filterstr, search_output,
829 num_result_all-search_resnumber, search_resnumber,
830 search_lastmod, num_result_all,
831 link_rel='last',
832 )
833 elif search_resminindex+search_resnumber <= resind:
834 page_command_list[3] = page_next_link
835
836 search_bookmark = SEARCH_BOOKMARK_TMPL.format(
837 baseUrl=escape_html(app.form.script_name),
838 ldapUrl=str(search_ldap_url),
839 )
840 result_message = '\n<p>Search results %d - %d %s / <a href="#params" title="See search parameters and export options">Params</a> / %s</p>\n' % (
841 search_resminindex+1,
842 resind,
843 max_result_msg,
844 search_bookmark,
845 )
846
847 top_section(app, 'Search Results', main_menu(app), context_menu_list=ctx_menu_items)
848
849 export_field = ExportFormatSelect()
850 export_field.charset = app.form.accept_charset
851
852 app.outf.write('\n'.join((search_warning, result_message)))
853
854 if search_resminindex == 0 and not partial_results:
855 mailtolist = set()
856 for res in result_dnlist:
857 if isinstance(res, SearchResultEntry):
858 mailtolist.update(res.entry_as.get('mail', res.entry_as.get('rfc822Mailbox', [])))
859 if mailtolist:
860 mailtolist = [urllib.parse.quote(m.decode(app.ls.charset)) for m in mailtolist]
861 app.outf.write('Mail to all <a href="mailto:%s?cc=%s">Cc:-ed</a> - <a href="mailto:?bcc=%s">Bcc:-ed</a>' % (
862 mailtolist[0],
863 ','.join(mailtolist[1:]),
864 ','.join(mailtolist)
865 ))
866
867 if page_command_list:
868 # output the paging links
869 app.outf.write(PAGE_COMMAND_TMPL.format(*page_command_list))
870
871 app.outf.write('<table id="SrchResList">\n')
872
873 for res in result_dnlist[0:resind]:
874
875 if isinstance(res, SearchReference):
876
877 # Display a search continuation (search reference)
878 entry = ldap0.cidict.CIDict({})
879 try:
880 ref_url = ExtendedLDAPUrl(res.ref_url_strings[0])
881 except ValueError:
882 command_table = []
883 result_dd_str = 'Search reference (NON-LDAP-URI) =&gt; %s' % (s2d(str(res[1][1][0])))
884 else:
885 result_dd_str = 'Search reference =&gt; %s' % (ref_url.htmlHREF(hrefTarget=None))
886 if scope == ldap0.SCOPE_SUBTREE:
887 ref_url.scope = ref_url.scope or scope
888 ref_url.filterstr = ((ref_url.filterstr or '') or filterstr)
889 command_table = [
890 app.anchor(
891 'search', 'Continue search',
892 [('ldapurl', ref_url.unparse())],
893 title='Follow this search continuation',
894 )
895 ]
896 else:
897 command_table = []
898 ref_url.filterstr = filterstr
899 ref_url.scope = ldap0.SCOPE_BASE
900 command_table.append(app.anchor(
901 'read', 'Read',
902 [('ldapurl', ref_url.unparse())],
903 title='Display single entry following search continuation',
904 ))
905 ref_url.scope = ldap0.SCOPE_ONELEVEL
906 command_table.append(app.anchor(
907 'search', 'Down',
908 [('ldapurl', ref_url.unparse())],
909 title='Descend into tree following search continuation',
910 ))
911
912 elif isinstance(res, SearchResultEntry):
913
914 # Display a search result with entry's data
915 res_dn_s = res.dn_s
916 entry = ldap0.schema.models.Entry(app.schema, res_dn_s, res.entry_as)
917
918 if search_output == 'raw':
919
920 # Output DN
921 result_dd_str = s2d(res_dn_s)
922
923 else:
924
925 oc_set = ldap0.schema.models.SchemaElementOIDSet(
926 app.schema,
927 ldap0.schema.models.ObjectClass,
928 decode_list(entry.get('objectClass', []), encoding='ascii'),
929 )
930 tdtemplate_oc = oc_set.intersection(search_tdtemplate_keys).names
931 tableentry_attrs = None
932
933 if tdtemplate_oc:
934 template_attrs = ldap0.schema.models.SchemaElementOIDSet(
935 app.schema,
936 AttributeType,
937 [],
938 )
939 for ocl in tdtemplate_oc:
940 template_attrs.update(search_tdtemplate_attrs_lower[ocl])
941 tableentry_attrs = template_attrs.intersection(entry.keys())
942
943 if tableentry_attrs:
944 # Output entry with the help of pre-defined templates
945 tableentry = DisplayEntry(
946 app, res_dn_s, app.schema, entry, 'search_sep', False
947 )
948 tdlist = []
949 for ocl in tdtemplate_oc:
950 tdlist.append(search_tdtemplate[ocl] % tableentry)
951 result_dd_str = '<br>\n'.join(tdlist)
952
953 elif 'displayName' in entry:
954 result_dd_str = s2d(app.ls.uc_decode(entry['displayName'][0])[0])
955
956 else:
957 # Output DN
958 result_dd_str = s2d(res_dn_s)
959
960 # Build the list for link table
961 command_table = []
962
963 # A [Read] link is added in any case
964 command_table.append(
965 app.anchor(
966 'read', 'Read',
967 [('dn', res_dn_s)],
968 )
969 )
970
971 # If subordinates or unsure a [Down] link is added
972 if has_subordinates(entry, default=True):
973
974 down_title_list = ['List direct subordinates of %s' % (res_dn_s)]
975
976 # Determine number of direct subordinates
977 try:
978 num_subordinates = int(
979 entry.get(
980 'numSubOrdinates',
981 entry.get(
982 'subordinateCount',
983 entry.get(
984 'countImmSubordinates',
985 entry['msDS-Approx-Immed-Subordinates'])))[0]
986 )
987 except (KeyError, ValueError):
988 pass
989 else:
990 down_title_list.append('direct: %d' % (num_subordinates))
991 # Determine total number of subordinates
992 try:
993 num_all_subordinates = int(
994 entry.get(
995 'numAllSubOrdinates',
996 entry['countTotSubordinates']
997 )[0]
998 )
999 except (KeyError, ValueError):
1000 pass
1001 else:
1002 down_title_list.append('total: %d' % (num_all_subordinates))
1003
1004 command_table.append(app.anchor(
1005 'search', 'Down',
1006 (
1007 ('dn', res_dn_s),
1008 ('scope', SEARCH_SCOPE_STR_ONELEVEL),
1009 ('searchform_mode', 'adv'),
1010 ('search_attr', 'objectClass'),
1011 ('search_option', SEARCH_OPT_ATTR_EXISTS),
1012 ('search_string', ''),
1013 ),
1014 title='\r\n'.join(down_title_list),
1015 ))
1016
1017 else:
1018 raise ValueError('LDAP result of invalid type: %r' % (res,))
1019
1020 # write the search result table row
1021 app.outf.write(
1022 '<tr><td class="CT">{0}</td><td>{1}</td></tr>\n'.format(
1023 ''.join(command_table),
1024 result_dd_str
1025 )
1026 )
1027
1028 app.outf.write(
1029 """
1030 </table>
1031 <a id="params"></a>
1032 %s
1033 <h3>Export to other formats</h3>
1034 <p>%s &nbsp; Include operational attributes %s</p>
1035 <p><input type="submit" value="Export"></p>
1036 </form>
1037 """ % (
1038 '\n'.join((
1039 app.begin_form('search', 'GET', target='web2ldapexport'),
1040 app.form.hidden_field_html('dn', app.dn, ''),
1041 app.form.hidden_field_html('search_root', search_root, ''),
1042 app.form.hidden_field_html('scope', str(scope), ''),
1043 app.form.hidden_field_html('filterstr', filterstr, ''),
1044 app.form.hidden_field_html('search_lastmod', str(search_lastmod), ''),
1045 app.form.hidden_field_html('search_resnumber', '0', ''),
1046 app.form.hidden_field_html('search_attrs', ','.join(search_attrs), ''),
1047 )),
1048 export_field.input_html(),
1049 InclOpAttrsCheckbox().input_html(),
1050 )
1051 )
1052
1053 app.outf.write(
1054 """
1055 <h3>Search parameters used</h3>
1056 %s
1057 <p>
1058 Equivalent OpenLDAP command:<br>
1059 <input value="%s" size="60" readonly>
1060 </p>
1061 """ % (
1062 search_param_html,
1063 s2d(ldap_search_command),
1064 )
1065 )
1066
1067 footer(app)
1068
1069
1070 else:
1071
1072 try:
1073 result_handler.process_results()
1074 except (
1075 ldap0.SIZELIMIT_EXCEEDED,
1076 ldap0.ADMINLIMIT_EXCEEDED,
1077 ):
1078 result_handler.post_processing()
def __init__(self, l, f, sub_schema, attr_types, ldap_charset='utf-8')
Definition: search.py:203
def _process_result(self, resultItem)
Definition: search.py:223
def _process_result(self, resultItem)
Definition: search.py:276
def __init__(self, l, f, sub_schema, attr_types, ldap_charset='utf-8')
Definition: search.py:249
def __init__(self, app, dn, sub_schema, print_template_str_dict)
Definition: search.py:144
def process_results(self, ignoreResultsNumber=0, processResultsCount=0)
Definition: search.py:151
def top_section(app, title, main_menu_list, context_menu_list=None, main_div_id='Message')
Definition: gui.py:352
def main_menu(app)
Definition: gui.py:234
def gen_headers(content_type, charset, more_headers=None)
Definition: gui.py:442
def footer(app)
Definition: gui.py:477
def w2l_search(app)
Definition: search.py:295
def w2l_searchform(app, msg='', filterstr='', scope=ldap0.SCOPE_SUBTREE, search_root=None, searchform_mode=None)
Definition: searchform.py:213
bool has_subordinates(entry, default=True)
Definition: __init__.py:45
str escape_html(str val)
Definition: __init__.py:18