2005-11-18 [cleroy] 1.9.100cvs18
[claws.git] / src / ldapquery.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 2003-2004 Match Grun
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  */
19
20 /*
21  * Functions necessary to define and perform LDAP queries.
22  */
23
24 #ifdef HAVE_CONFIG_H
25 #  include "config.h"
26 #endif
27
28 #ifdef USE_LDAP
29
30 #include <glib.h>
31 #include <sys/time.h>
32 #include <string.h>
33 #include <lber.h>
34
35 #include "ldapquery.h"
36 #include "ldapctrl.h"
37 #include "mgutils.h"
38
39 #include "addritem.h"
40 #include "addrcache.h"
41
42 #include "ldapquery.h"
43
44 /*
45  * Key for thread specific data.
46  */
47 static pthread_key_t _queryThreadKey_;
48 static gboolean _queryThreadInit_ = FALSE;
49
50 gboolean callbackend (gpointer data)
51 {
52         LdapQuery *qry = (LdapQuery *)data;
53         qry->callBackEnd( qry, ADDRQUERY_ID(qry), ADDRQUERY_RETVAL(qry), qry->data );
54         return FALSE;
55 }
56
57
58 /**
59  * Create new LDAP query object.
60  * \return Initialized query object.
61  */
62 LdapQuery *ldapqry_create( void ) {
63         LdapQuery *qry;
64
65         qry = g_new0( LdapQuery, 1 );
66         ADDRQUERY_TYPE(qry) = ADDRQUERY_LDAP;
67         ADDRQUERY_ID(qry) = 0;
68         ADDRQUERY_SEARCHTYPE(qry) = ADDRSEARCH_NONE;
69         ADDRQUERY_NAME(qry) = NULL;
70         ADDRQUERY_RETVAL(qry) = LDAPRC_SUCCESS;
71         ADDRQUERY_FOLDER(qry) = NULL;
72         ADDRQUERY_SEARCHVALUE(qry) = NULL;
73         qry->control = NULL;
74         qry->server = NULL;
75         qry->entriesRead = 0;
76         qry->elapsedTime = 0;
77         qry->stopFlag = FALSE;
78         qry->busyFlag = FALSE;
79         qry->agedFlag = FALSE;
80         qry->completed = FALSE;
81         qry->thread = NULL;
82         qry->callBackEntry = NULL;
83         qry->callBackEnd = NULL;
84         qry->ldap = NULL;
85         qry->data = NULL;
86
87         /* Mutex to protect stop and busy flags */
88         qry->mutexStop = g_malloc0( sizeof( pthread_mutex_t ) );
89         pthread_mutex_init( qry->mutexStop, NULL );
90         qry->mutexBusy = g_malloc0( sizeof( pthread_mutex_t ) );
91         pthread_mutex_init( qry->mutexBusy, NULL );
92
93         /* Mutex to protect critical section */
94         qry->mutexEntry = g_malloc0( sizeof( pthread_mutex_t ) );
95         pthread_mutex_init( qry->mutexEntry, NULL );
96
97         return qry;
98 }
99
100 /**
101  * Specify the reference to control data that will be used for the query. The calling
102  * module should be responsible for creating and destroying this control object.
103  * \param qry Query object.
104  * \param ctl Control object.
105  */
106 void ldapqry_set_control( LdapQuery *qry, LdapControl *ctl ) {
107         g_return_if_fail( qry != NULL );
108         qry->control = ctl;
109 }
110
111 /**
112  * Specify query name to be used.
113  * \param qry   Query object.
114  * \param value Name.
115  */
116 void ldapqry_set_name( LdapQuery* qry, const gchar *value ) {
117         ADDRQUERY_NAME(qry) = mgu_replace_string( ADDRQUERY_NAME(qry), value );
118         g_strstrip( ADDRQUERY_NAME(qry) );
119 }
120
121 /**
122  * Specify search value to be used.
123  * \param qry Query object.
124  * \param value 
125  */
126 void ldapqry_set_search_value( LdapQuery *qry, const gchar *value ) {
127         ADDRQUERY_SEARCHVALUE(qry) = mgu_replace_string( ADDRQUERY_SEARCHVALUE(qry), value );
128         g_strstrip( ADDRQUERY_SEARCHVALUE(qry) );
129 }
130
131 /**
132  * Specify error/status.
133  * \param qry   Query object.
134  * \param value Status.
135  */
136 void ldapqry_set_error_status( LdapQuery* qry, const gint value ) {
137         ADDRQUERY_RETVAL(qry) = value;
138 }
139
140 /**
141  * Specify query type.
142  * \param qry Query object.
143  * \param value Query type, either:
144  * <ul>
145  * <li><code>LDAPQUERY_NONE</code></li>
146  * <li><code>LDAPQUERY_STATIC</code></li>
147  * <li><code>LDAPQUERY_DYNAMIC</code></li>
148  * </ul>
149  */
150 /*
151 void ldapqry_set_query_type( LdapQuery* qry, const gint value ) {
152         ADDRQUERY_TYPE(qry) = value;
153 }
154 */
155
156 /**
157  * Specify search type.
158  * \param qry   Query object.
159  * \param value Type.
160  */
161 void ldapqry_set_search_type( LdapQuery *qry, const AddrSearchType value ) {
162         g_return_if_fail( qry != NULL );
163         ADDRQUERY_SEARCHTYPE(qry) = value;
164 }
165
166 /**
167  * Specify query ID.
168  * \param qry Query object.
169  * \param value ID for the query.
170  */
171 void ldapqry_set_query_id( LdapQuery* qry, const gint value ) {
172         ADDRQUERY_ID(qry) = value;
173 }
174
175 /**
176  * Specify maximum number of LDAP entries to retrieve.
177  * \param qry Query object.
178  * \param value Entries to read.
179  */
180 void ldapqry_set_entries_read( LdapQuery* qry, const gint value ) {
181         if( value > 0 ) {
182                 qry->entriesRead = value;
183         }
184         else {
185                 qry->entriesRead = 0;
186         }
187 }
188
189 /**
190  * Register a callback function that will be executed when each entry
191  * has been read and processed. When called, the function will be passed
192  * this query object and a GList of ItemEMail objects as arguments. An
193  * example of typical usage is shown below.
194  *
195  * <pre>
196  * ------------------------------------------------------------
197  * void myCallbackEntry( LdapQuery *qry, GList *listEMail ) {
198  *   GList *node;
199  *
200  *   node = listEMail;
201  *   while( node ) {
202  *     ItemEMail *email = node->data;
203  *     ... process email object ...
204  *     node = g_list_next( node );
205  *   }
206  *   g_list_free( listEMail );
207  * }
208  * ...
209  * ...
210  * ldapqry_set_callback_entry( qry, myCallbackEntry );
211  * ------------------------------------------------------------
212  * </pre>
213  *
214  * \param qry Query object.
215  * \param func Function.
216  */
217 void ldapqry_set_callback_entry( LdapQuery *qry, void *func ) {
218         pthread_mutex_lock( qry->mutexEntry );
219         qry->callBackEntry = func;
220         pthread_mutex_unlock( qry->mutexEntry );
221 }
222
223 /**
224  * Register a callback function that will be executed when the search
225  * is complete. When called, the function will be passed this query
226  * object as an argument.
227  * \param qry Query object.
228  * \param func Function.
229  */
230 void ldapqry_set_callback_end( LdapQuery *qry, void *func ) {
231         qry->callBackEnd = func;
232 }
233
234 /**
235  * Notify query to start/stop executing. This method should be called with a
236  * value if <i>TRUE</i> to terminate an existing running query.
237  *
238  * \param qry Query object.
239  * \param value Value of stop flag.
240  */
241 void ldapqry_set_stop_flag( LdapQuery *qry, const gboolean value ) {
242         g_return_if_fail( qry != NULL );
243
244         pthread_mutex_lock( qry->mutexStop );
245         qry->stopFlag = value;
246         pthread_mutex_unlock( qry->mutexStop );
247 }
248
249 /**
250  * Test value of stop flag. This method should be used to determine whether a
251  * query has stopped running.
252  * \param qry Query object.
253  * \return Value of stop flag.
254  */
255 gboolean ldapqry_get_stop_flag( LdapQuery *qry ) {
256         gboolean value;
257         g_return_val_if_fail( qry != NULL, TRUE );
258
259         pthread_mutex_lock( qry->mutexStop );
260         value = qry->stopFlag;
261         pthread_mutex_unlock( qry->mutexStop );
262         return value;
263 }
264
265 /**
266  * Set busy flag.
267  * \param qry Query object.
268  * \param value Value of busy flag.
269  */
270 void ldapqry_set_busy_flag( LdapQuery *qry, const gboolean value ) {
271         g_return_if_fail( qry != NULL );
272
273         pthread_mutex_lock( qry->mutexBusy );
274         qry->busyFlag = value;
275         pthread_mutex_unlock( qry->mutexBusy );
276 }
277
278 /**
279  * Test value of busy flag. This method will return a value of <i>FALSE</i>
280  * when a query has completed running.
281  * \param qry Query object.
282  * \return Value of busy flag.
283  */
284 gboolean ldapqry_get_busy_flag( LdapQuery *qry ) {
285         gboolean value;
286         g_return_val_if_fail( qry != NULL, FALSE );
287
288         pthread_mutex_lock( qry->mutexBusy );
289         value = qry->busyFlag;
290         pthread_mutex_unlock( qry->mutexBusy );
291         return value;
292 }
293
294 /**
295  * Set query aged flag.
296  * \param qry Query object.
297  * \param value Value of aged flag.
298  */
299 void ldapqry_set_aged_flag( LdapQuery *qry, const gboolean value ) {
300         g_return_if_fail( qry != NULL );
301         qry->agedFlag = value;
302 }
303
304 /**
305  * Test value of aged flag.
306  * \param qry Query object.
307  * \return <i>TRUE</i> if query has been marked as aged (and can be retired).
308  */
309 gboolean ldapqry_get_aged_flag( LdapQuery *qry ) {
310         g_return_val_if_fail( qry != NULL, TRUE );
311         return qry->agedFlag;
312 }
313
314 /**
315  * Specify user data for query.
316  * \param qry Query object.
317  * \param value Data to set.
318  */
319 void ldapqry_set_data( LdapQuery *qry, const gpointer value ) {
320         g_return_if_fail( qry != NULL );
321         qry->data = value;
322 }
323
324 /**
325  * Retrieve user data associated with query.
326  * \param qry Query object.
327  * \return Data.
328  */
329 gpointer ldapqry_get_data( LdapQuery *qry ) {
330         g_return_val_if_fail( qry != NULL, NULL );
331         return qry->data;
332 }
333
334 /**
335  * Release the LDAP control data associated with the query.
336  * \param qry Query object to process.
337  */
338 void ldapqry_release_control( LdapQuery *qry ) {
339         g_return_if_fail( qry != NULL );
340         if( qry->control != NULL ) {
341                 ldapctl_free( qry->control );
342         }
343         qry->control = NULL;
344 }
345
346 /**
347  * Clear LDAP query member variables.
348  * \param qry Query object.
349  */
350 void ldapqry_clear( LdapQuery *qry ) {
351         g_return_if_fail( qry != NULL );
352
353         /* Free internal stuff */
354         g_free( ADDRQUERY_NAME(qry) );
355         g_free( ADDRQUERY_SEARCHVALUE(qry) );
356
357         /* Clear pointers and value */
358         ADDRQUERY_NAME(qry) = NULL;
359         ADDRQUERY_SEARCHVALUE(qry) = NULL;
360         ADDRQUERY_ID(qry) = 0;
361         ADDRQUERY_SEARCHTYPE(qry) = ADDRSEARCH_NONE;
362         ADDRQUERY_RETVAL(qry) = LDAPRC_SUCCESS;
363         qry->entriesRead = 0;
364         qry->elapsedTime = 0;
365         qry->stopFlag = FALSE;
366         qry->busyFlag = FALSE;
367         qry->agedFlag = FALSE;
368         qry->completed = FALSE;
369         qry->callBackEntry = NULL;
370         qry->callBackEnd = NULL;
371         qry->ldap = NULL;
372         qry->data = NULL;
373 }
374
375 /**
376  * Free up LDAP query object by releasing internal memory. Note that
377  * the thread object will be freed by the OS.
378  * \param qry Query object to process.
379  */
380 void ldapqry_free( LdapQuery *qry ) {
381         g_return_if_fail( qry != NULL );
382
383         /* Clear out internal members */
384         ADDRQUERY_TYPE(qry) = ADDRQUERY_NONE;
385         ldapqry_clear( qry );
386
387         /* Free the mutex */
388         pthread_mutex_destroy( qry->mutexStop );
389         pthread_mutex_destroy( qry->mutexBusy );
390         pthread_mutex_destroy( qry->mutexEntry );
391         g_free( qry->mutexStop );
392         g_free( qry->mutexBusy );
393         g_free( qry->mutexEntry );
394         qry->mutexEntry = NULL;
395         qry->mutexBusy = NULL;
396         qry->mutexStop = NULL;
397
398         /* Do not free folder - parent server object should free */     
399         ADDRQUERY_FOLDER(qry) = NULL;
400
401         /* Do not free thread - thread should be terminated before freeing */
402         qry->thread = NULL;
403
404         /* Do not free LDAP control - should be destroyed before freeing */
405         qry->control = NULL;
406
407         /* Now release object */
408         g_free( qry );
409 }
410
411 /**
412  * Display object to specified stream.
413  * \param qry    Query object to process.
414  * \param stream Output stream.
415  */
416 void ldapqry_print( const LdapQuery *qry, FILE *stream ) {
417         g_return_if_fail( qry != NULL );
418
419         fprintf( stream, "LdapQuery:\n" );
420         fprintf( stream, "  control?: %s\n",   qry->control ? "yes" : "no" );
421         fprintf( stream, "err/status: %d\n",   ADDRQUERY_RETVAL(qry) );
422         fprintf( stream, "query type: %d\n",   ADDRQUERY_TYPE(qry) );
423         fprintf( stream, "searchType: %d\n",   ADDRQUERY_SEARCHTYPE(qry) );
424         fprintf( stream, "query name: '%s'\n", ADDRQUERY_NAME(qry) );
425         fprintf( stream, "search val: '%s'\n", ADDRQUERY_SEARCHVALUE(qry) );
426         fprintf( stream, "   queryID: %d\n",   ADDRQUERY_ID(qry) );
427         fprintf( stream, "   entries: %d\n",   qry->entriesRead );
428         fprintf( stream, "   elapsed: %d\n",   qry->elapsedTime );
429         fprintf( stream, " stop flag: %s\n",   qry->stopFlag  ? "yes" : "no" );
430         fprintf( stream, " busy flag: %s\n",   qry->busyFlag  ? "yes" : "no" );
431         fprintf( stream, " aged flag: %s\n",   qry->agedFlag  ? "yes" : "no" );
432         fprintf( stream, " completed: %s\n",   qry->completed ? "yes" : "no" );
433 }
434
435 /**
436  * Free linked lists of character strings.
437  * \param listName  List of common names.
438  * \param listAddr  List of addresses.
439  * \param listFirst List of first names.
440  * \param listLast  List of last names.
441  */
442 static void ldapqry_free_lists(
443                 GSList *listName, GSList *listAddr, GSList *listFirst,
444                 GSList *listLast )
445 {
446         mgu_free_list( listName );
447         mgu_free_list( listAddr );
448         mgu_free_list( listFirst );
449         mgu_free_list( listLast );
450 }
451
452 /**
453  * Add all LDAP attribute values to a list.
454  * \param ld LDAP handle.
455  * \param entry LDAP entry to process.
456  * \param attr  LDAP attribute.
457  * \return List of values.
458  */
459 static GSList *ldapqry_add_list_values(
460                 LDAP *ld, LDAPMessage *entry, char *attr )
461 {
462         GSList *list = NULL;
463         gint i;
464         gchar **vals;
465
466         if( ( vals = ldap_get_values( ld, entry, attr ) ) != NULL ) {
467                 for( i = 0; vals[i] != NULL; i++ ) {
468                         /* printf( "lv\t%s: %s\n", attr, vals[i] ); */
469                         list = g_slist_append( list, g_strdup( vals[i] ) );
470                 }
471         }
472         ldap_value_free( vals );
473         return list;
474 }
475
476 /**
477  * Add a single attribute value to a list.
478  * \param  ld    LDAP handle.
479  * \param  entry LDAP entry to process.
480  * \param  attr  LDAP attribute name to process.
481  * \return List of values; only one value will be present.
482  */
483 static GSList *ldapqry_add_single_value( LDAP *ld, LDAPMessage *entry, char *attr ) {
484         GSList *list = NULL;
485         gchar **vals;
486
487         if( ( vals = ldap_get_values( ld, entry, attr ) ) != NULL ) {
488                 if( vals[0] != NULL ) {
489                         /* printf( "sv\t%s: %s\n", attr, vals[0] ); */
490                         list = g_slist_append( list, g_strdup( vals[0] ) );
491                 }
492         }
493         ldap_value_free( vals );
494         return list;
495 }
496
497 /**
498  * Build an address list entry and append to list of address items. Name is formatted
499  * as "<first-name> <last-name>".
500  *
501  * \param  cache     Address cache to load.
502  * \param  qry Query object to process.
503  * \param  dn        DN for entry found on server.
504  * \param  listName  List of common names for entry; see notes below.
505  * \param  listAddr  List of EMail addresses for entry.
506  * \param  listFirst List of first names for entry.
507  * \param  listLast  List of last names for entry.
508  *
509  * \return List of ItemEMail objects.
510  *
511  * Notes:
512  * 1) Each LDAP server entry may have multiple LDAP attributes with the same
513  *    name. For example, a single entry for a person may have more than one
514  *    common name, email address, etc.
515 *
516  * 2) The DN for the entry is unique for the server.
517  */
518 static GList *ldapqry_build_items_fl(
519                 AddressCache *cache, LdapQuery *qry, gchar *dn,
520                 GSList *listName, GSList *listAddr, GSList *listFirst,
521                 GSList *listLast )
522 {
523         GSList *nodeAddress;
524         gchar *firstName = NULL, *lastName = NULL, *fullName = NULL;
525         gboolean allocated;
526         ItemPerson *person;
527         ItemEMail *email;
528         ItemFolder *folder;
529         GList *listReturn = NULL;
530
531         folder = ADDRQUERY_FOLDER(qry);
532         if( folder == NULL ) return listReturn;
533         if( listAddr == NULL ) return listReturn;
534
535         /* Find longest first name in list */
536         firstName = mgu_slist_longest_entry( listFirst );
537
538         /* Format last name */
539         if( listLast ) {
540                 lastName = listLast->data;
541         }
542
543         /* Find longest common name */
544         allocated = FALSE;
545         fullName = mgu_slist_longest_entry( listName );
546         if( fullName == NULL ) {
547                 /* Format a full name from first and last names */
548                 if( firstName ) {
549                         if( lastName ) {
550                                 fullName = g_strdup_printf( "%s %s", firstName, lastName );
551                         }
552                         else {
553                                 fullName = g_strdup_printf( "%s", firstName );
554                         }
555                 }
556                 else {
557                         if( lastName ) {
558                                 fullName = g_strdup_printf( "%s", lastName );
559                         }
560                 }
561                 if( fullName ) {
562                         g_strchug( fullName ); g_strchomp( fullName );
563                         allocated = TRUE;
564                 }
565         }
566
567         /* Add person into folder */            
568         person = addritem_create_item_person();
569         addritem_person_set_common_name( person, fullName );
570         addritem_person_set_first_name( person, firstName );
571         addritem_person_set_last_name( person, lastName );
572         addrcache_id_person( cache, person );
573         addritem_person_set_external_id( person, dn );
574         addrcache_folder_add_person( cache, ADDRQUERY_FOLDER(qry), person );
575
576         qry->entriesRead++;
577
578         /* Add each address item */
579         nodeAddress = listAddr;
580         while( nodeAddress ) {
581                 email = addritem_create_item_email();
582                 addritem_email_set_address( email, nodeAddress->data );
583                 addrcache_id_email( cache, email );
584                 addrcache_person_add_email( cache, person, email );
585                 addritem_person_add_email( person, email );
586                 listReturn = g_list_append( listReturn, email );
587                 nodeAddress = g_slist_next( nodeAddress );
588         }
589
590         /* Free any allocated memory */
591         if( allocated ) {
592                 g_free( fullName );
593         }
594         fullName = firstName = lastName = NULL;
595
596         return listReturn;
597 }
598
599 /**
600  * Process a single search entry.
601  * \param  cache Address cache to load.
602  * \param  qry   Query object to process.
603  * \param  ld    LDAP handle.
604  * \param  e     LDAP message.
605  * \return List of EMail objects found.
606  */
607 static GList *ldapqry_process_single_entry(
608                 AddressCache *cache, LdapQuery *qry, LDAP *ld, LDAPMessage *e )
609 {
610         char *dnEntry;
611         char *attribute;
612         LdapControl *ctl;
613         BerElement *ber;
614         GSList *listName = NULL, *listAddress = NULL;
615         GSList *listFirst = NULL, *listLast = NULL;
616         GList *listReturn;
617
618         listReturn = NULL;
619         ctl = qry->control;
620         dnEntry = ldap_get_dn( ld, e );
621         /* printf( "DN: %s\n", dnEntry ); */
622
623         /* Process all attributes */
624         for( attribute = ldap_first_attribute( ld, e, &ber ); attribute != NULL;
625                 attribute = ldap_next_attribute( ld, e, ber ) ) {
626
627                 if( strcasecmp( attribute, ctl->attribEMail ) == 0 ) {
628                         listAddress = ldapqry_add_list_values( ld, e, attribute );
629                 }
630                 else if( strcasecmp( attribute, ctl->attribCName ) == 0 ) {
631                         listName = ldapqry_add_list_values( ld, e, attribute );
632                 }
633                 else if( strcasecmp( attribute, ctl->attribFName ) == 0 ) {
634                         listFirst = ldapqry_add_list_values( ld, e, attribute );
635                 }
636                 else if( strcasecmp( attribute, ctl->attribLName ) == 0 ) {
637                         listLast = ldapqry_add_single_value( ld, e, attribute );
638                 }
639
640                 /* Free memory used to store attribute */
641                 ldap_memfree( attribute );
642         }
643
644         /* Format and add items to cache */
645         listReturn = ldapqry_build_items_fl(
646                 cache, qry, dnEntry, listName, listAddress, listFirst, listLast );
647
648         /* Free up */
649         ldapqry_free_lists( listName, listAddress, listFirst, listLast );
650         listName = listAddress = listFirst = listLast = NULL;
651
652         if( ber != NULL ) {
653                 ber_free( ber, 0 );
654         }
655         g_free( dnEntry );
656
657         return listReturn;
658 }
659
660 /**
661  * Check parameters that are required for a search. This should
662  * be called before performing a search.
663  * \param  qry Query object to process.
664  * \return <i>TRUE</i> if search criteria appear OK.
665  */
666 gboolean ldapqry_check_search( LdapQuery *qry ) {
667         LdapControl *ctl;
668         ADDRQUERY_RETVAL(qry) = LDAPRC_CRITERIA;
669
670         /* Test for control data */
671         ctl = qry->control;
672         if( ctl == NULL ) {
673                 return FALSE;
674         }
675
676         /* Test for search value */
677         if( ADDRQUERY_SEARCHVALUE(qry) == NULL ) {
678                 return FALSE;
679         }
680         if( strlen( ADDRQUERY_SEARCHVALUE(qry) ) < 1 ) {
681                 return FALSE;
682         }
683         ADDRQUERY_RETVAL(qry) = LDAPRC_SUCCESS;
684         return TRUE;
685 }
686
687 /**
688  * Touch the query. This nudges the touch time with the current time.
689  * \param qry Query object to process.
690  */
691 void ldapqry_touch( LdapQuery *qry ) {
692         qry->touchTime = time( NULL );
693         qry->agedFlag = FALSE;
694 }
695
696 /**
697  * Connect to LDAP server.
698  * \param  qry Query object to process.
699  * \return Error/status code.
700  */
701 static gint ldapqry_connect( LdapQuery *qry ) {
702         LdapControl *ctl;
703         LDAP *ld;
704         gint rc;
705         gint version;
706
707         /* Initialize connection */
708         /* printf( "===ldapqry_connect===\n" ); */
709         /* ldapqry_print( qry, stdout ); */
710         ctl = qry->control;
711         /* ldapctl_print( ctl, stdout ); */
712         /* printf( "======\n" ); */
713         ldapqry_touch( qry );
714         qry->startTime = qry->touchTime;
715         qry->elapsedTime = -1;
716         ADDRQUERY_RETVAL(qry) = LDAPRC_INIT;
717         if( ( ld = ldap_init( ctl->hostName, ctl->port ) ) == NULL ) {
718                 return ADDRQUERY_RETVAL(qry);
719         }
720         qry->ldap = ld;
721         ADDRQUERY_RETVAL(qry) = LDAPRC_STOP_FLAG;
722         if( ldapqry_get_stop_flag( qry ) ) {
723                 return ADDRQUERY_RETVAL(qry);
724         }
725         ldapqry_touch( qry );
726
727         /*
728         printf( "connected to LDAP host %s on port %d\n", ctl->hostName, ctl->port );
729         */
730
731 #ifdef USE_LDAP_TLS
732         /* Handle TLS */
733         version = LDAP_VERSION3;
734         rc = ldap_set_option( ld, LDAP_OPT_PROTOCOL_VERSION, &version );
735         if( rc == LDAP_OPT_SUCCESS ) {
736                 ctl->version = LDAP_VERSION3;
737         }
738
739         if( ctl->version == LDAP_VERSION3 ) {
740                 if( ctl->enableTLS ) {
741                         ADDRQUERY_RETVAL(qry) = LDAPRC_TLS;
742                         rc = ldap_start_tls_s( ld, NULL, NULL );
743                         /*
744                         printf( "rc=%d\n", rc );
745                         printf( "LDAP Status: set_option: %s\n", ldap_err2string( rc ) );
746                         */
747                         if( rc != LDAP_SUCCESS ) {
748                                 return ADDRQUERY_RETVAL(qry);
749                         }
750                 }
751         }
752 #endif
753
754         /* Bind to the server, if required */
755         ADDRQUERY_RETVAL(qry) = LDAPRC_BIND;
756         if( ctl->bindDN ) {
757                 if( * ctl->bindDN != '\0' ) {
758                         /* printf( "binding...\n" ); */
759                         rc = ldap_simple_bind_s( ld, ctl->bindDN, ctl->bindPass );
760                         /* printf( "rc=%d\n", rc ); */
761                         if( rc != LDAP_SUCCESS ) {
762                                 /*
763                                 printf( "LDAP Error: ldap_simple_bind_s: %s\n",
764                                         ldap_err2string( rc ) );
765                                 */
766                                 return ADDRQUERY_RETVAL(qry);
767                         }
768                 }
769         }
770         ADDRQUERY_RETVAL(qry) = LDAPRC_STOP_FLAG;
771         if( ldapqry_get_stop_flag( qry ) ) {
772                 return ADDRQUERY_RETVAL(qry);
773         }
774         ldapqry_touch( qry );
775
776         ADDRQUERY_RETVAL(qry) = LDAP_SUCCESS;
777
778         return ADDRQUERY_RETVAL(qry);
779 }
780
781 /**
782  * Connect to LDAP server.
783  * \param  qry Query object to process.
784  * \return Error/status code.
785  */
786 static gint ldapqry_disconnect( LdapQuery *qry ) {
787         /* Disconnect */
788         if( qry->ldap ) ldap_unbind( qry->ldap );
789         qry->ldap = NULL;
790
791         ldapqry_touch( qry );
792         qry->elapsedTime = qry->touchTime - qry->startTime;
793
794         return ADDRQUERY_RETVAL(qry);
795 }
796
797 /**
798  * Perform the LDAP search, reading LDAP entries into cache.
799  * Note that one LDAP entry can have multiple values for many of its
800  * attributes. If these attributes are E-Mail addresses; these are
801  * broken out into separate address items. For any other attribute,
802  * only the first occurrence is read.
803  * 
804  * \param  qry Query object to process.
805  * \return Error/status code.
806  */
807 static gint ldapqry_search_retrieve( LdapQuery *qry ) {
808         LdapControl *ctl;
809         LDAP *ld;
810         LDAPMessage *result, *e = NULL;
811         char **attribs;
812         gchar *criteria;
813         gboolean searchFlag;
814         gboolean entriesFound;
815         gboolean first;
816         struct timeval timeout;
817         gint rc;
818         AddressCache *cache;
819         GList *listEMail;
820
821         /* Initialize some variables */
822         ld = qry->ldap;
823         ctl = qry->control;
824         cache = qry->server->addressCache;
825         timeout.tv_sec = ctl->timeOut;
826         timeout.tv_usec = 0L;
827         entriesFound = FALSE;
828         ADDRQUERY_RETVAL(qry) = LDAPRC_SUCCESS;
829
830         /* Define all attributes we are interested in. */
831         attribs = ldapctl_attribute_array( ctl );
832
833         /* Create LDAP search string */
834         criteria = ldapctl_format_criteria( ctl, ADDRQUERY_SEARCHVALUE(qry) );
835         /* printf( "Search criteria ::%s::\n", criteria ); */
836
837         /*
838          * Execute the search - this step may take some time to complete
839          * depending on network traffic and server response time.
840          */
841         ADDRQUERY_RETVAL(qry) = LDAPRC_TIMEOUT;
842         rc = ldap_search_ext_s( ld, ctl->baseDN, LDAP_SCOPE_SUBTREE, criteria,
843                 attribs, 0, NULL, NULL, &timeout, 0, &result );
844         ldapctl_free_attribute_array( attribs );
845         g_free( criteria );
846         criteria = NULL;
847         if( rc == LDAP_TIMEOUT ) {
848                 return ADDRQUERY_RETVAL(qry);
849         }
850         ADDRQUERY_RETVAL(qry) = LDAPRC_SEARCH;
851
852         /* Test valid returns */
853         searchFlag = FALSE;
854         if( rc == LDAP_ADMINLIMIT_EXCEEDED ) {
855                 searchFlag = TRUE;
856         }
857         else if( rc == LDAP_SUCCESS ) {
858                 searchFlag = TRUE;
859         }
860         else if( rc == LDAP_PARTIAL_RESULTS ) {
861                 searchFlag = TRUE;
862         }
863         else {
864                 /*
865                 printf( "LDAP Error: ldap_search_st: %d\n", rc );
866                 printf( "LDAP Error: ldap_search_st: %s\n", ldap_err2string( rc ) );
867                 */
868                 return ADDRQUERY_RETVAL(qry);
869         }
870         ADDRQUERY_RETVAL(qry) = LDAPRC_STOP_FLAG;
871
872         /*
873         printf( "Total results are: %d\n", ldap_count_entries( ld, result ) );
874         */
875
876         /* Process results */
877         first = TRUE;
878         while( searchFlag ) {
879                 ldapqry_touch( qry );
880                 if( qry->entriesRead >= ctl->maxEntries ) break;                
881
882                 /* Test for stop */             
883                 if( ldapqry_get_stop_flag( qry ) ) {
884                         break;
885                 }
886
887                 /* Retrieve entry */            
888                 if( first ) {
889                         first = FALSE;
890                         e = ldap_first_entry( ld, result );
891                 }
892                 else {
893                         e = ldap_next_entry( ld, e );
894                 }
895                 if( e == NULL ) break;
896                 entriesFound = TRUE;
897
898                 /* Setup a critical section here */
899                 pthread_mutex_lock( qry->mutexEntry );
900
901                 /* Process entry */
902                 listEMail = ldapqry_process_single_entry( cache, qry, ld, e );
903
904                 /* Process callback */
905                 if( qry->callBackEntry ) {
906                         qry->callBackEntry( qry, ADDRQUERY_ID(qry), listEMail, qry->data );
907                 }
908                 else {
909                         g_list_free( listEMail );
910                 }
911                 pthread_mutex_unlock( qry->mutexEntry );
912         }
913
914         /* Free up and disconnect */
915         ldap_msgfree( result );
916
917         if( searchFlag ) {
918                 if( entriesFound ) {
919                         ADDRQUERY_RETVAL(qry) = LDAPRC_SUCCESS;
920                 }
921                 else {
922                         ADDRQUERY_RETVAL(qry) = LDAPRC_NOENTRIES;
923                 }
924         }
925
926         return ADDRQUERY_RETVAL(qry);
927 }
928
929 /**
930  * Connection, perform search and disconnect.
931  * \param  qry Query object to process.
932  * \return Error/status code.
933  */
934 static gint ldapqry_perform_search( LdapQuery *qry ) {
935         /* Check search criteria */     
936         if( ! ldapqry_check_search( qry ) ) {
937                 return ADDRQUERY_RETVAL(qry);
938         }
939
940         /* Connect */
941         qry->ldap = NULL;
942         ldapqry_connect( qry );
943         if( ADDRQUERY_RETVAL(qry) == LDAPRC_SUCCESS ) {
944                 /* Perform search */
945                 ldapqry_search_retrieve( qry );
946         }
947         /* Disconnect */
948         ldapqry_disconnect( qry );
949         qry->ldap = NULL;
950
951         return ADDRQUERY_RETVAL(qry);
952 }
953
954 static gint ldapqry_perform_locate( LdapQuery *qry );
955
956 /**
957  * Wrapper around search.
958  * \param  qry Query object to process.
959  * \return Error/status code.
960  */
961 gint ldapqry_search( LdapQuery *qry ) {
962         gint retVal;
963
964         g_return_val_if_fail( qry != NULL, -1 );
965         g_return_val_if_fail( qry->control != NULL, -1 );
966
967         ldapqry_touch( qry );
968         qry->completed = FALSE;
969
970         /* Setup pointer to thread specific area */
971         pthread_setspecific( _queryThreadKey_, qry );
972
973         pthread_detach( pthread_self() );
974         
975         /* Now perform the search */
976         qry->entriesRead = 0;
977         ADDRQUERY_RETVAL(qry) = LDAPRC_SUCCESS;
978         ldapqry_set_busy_flag( qry, TRUE );
979         ldapqry_set_stop_flag( qry, FALSE );
980         if( ADDRQUERY_SEARCHTYPE(qry) == ADDRSEARCH_LOCATE ) {
981                 retVal = ldapqry_perform_locate( qry );
982         }
983         else {
984                 retVal = ldapqry_perform_search( qry );
985         }
986         if( retVal == LDAPRC_SUCCESS ) {
987                 qry->server->addressCache->dataRead = TRUE;
988                 qry->server->addressCache->accessFlag = FALSE;
989                 if( ldapqry_get_stop_flag( qry ) ) {
990                         /*
991                         printf( "Search was terminated prematurely\n" );
992                         */
993                 }
994                 else {
995                         ldapqry_touch( qry );
996                         qry->completed = TRUE;
997                         /*
998                         printf( "Search ran to completion\n" );
999                         */
1000                 }
1001         }
1002         ldapqry_set_stop_flag( qry, TRUE );
1003         ldapqry_set_busy_flag( qry, FALSE );
1004
1005         /* Process callback */  
1006         if( qry->callBackEnd ) {
1007                 g_timeout_add(0, callbackend, qry);
1008         }
1009
1010         return ADDRQUERY_RETVAL(qry);
1011 }
1012
1013 /**
1014  * Read data into list using a background thread. Callback function will be
1015  * notified when search is complete.
1016  * \param  qry Query object to process.
1017  * \return Error/status code.
1018  */
1019 gint ldapqry_read_data_th( LdapQuery *qry ) {
1020         g_return_val_if_fail( qry != NULL, -1 );
1021         g_return_val_if_fail( qry->control != NULL, -1 );
1022
1023         ldapqry_set_stop_flag( qry, FALSE );
1024         ldapqry_touch( qry );
1025         if( ldapqry_check_search( qry ) ) {
1026                 if( ADDRQUERY_RETVAL(qry) == LDAPRC_SUCCESS ) {
1027                         /*
1028                         printf( "Starting LDAP search thread\n");
1029                         */
1030                         ldapqry_set_busy_flag( qry, TRUE );
1031                         qry->thread = g_malloc0( sizeof( pthread_t ) );
1032
1033                         /* Setup thread */                      
1034                         pthread_create( qry->thread, NULL,
1035                                 (void *) ldapqry_search, (void *) qry );
1036                 }
1037         }
1038         return ADDRQUERY_RETVAL(qry);
1039 }
1040
1041 /**
1042  * Cleanup LDAP thread data. This function will be called when each thread
1043  * exits. Note that the thread object will be freed by the kernel.
1044  * \param ptr Pointer to object being destroyed (a query object in this case).
1045  */
1046 static void ldapqry_destroyer( void * ptr ) {
1047         LdapQuery *qry;
1048
1049         qry = ( LdapQuery * ) ptr;
1050         /*
1051         printf( "ldapqry_destroyer::%d::%s\n", (int) pthread_self(), ADDRQUERY_NAME(qry) );
1052         */
1053
1054         /* Perform any destruction here */
1055         if( qry->control != NULL ) {
1056                 ldapctl_free( qry->control );
1057         }
1058         qry->control = NULL;
1059         qry->thread = NULL;
1060         ldapqry_set_busy_flag( qry, FALSE );
1061 }
1062
1063 /**
1064  * Cancel thread associated with query.
1065  * \param qry Query object to process.
1066  */
1067 void ldapqry_cancel( LdapQuery *qry ) {
1068         g_return_if_fail( qry != NULL );
1069
1070         /*
1071         printf( "cancelling::%d::%s\n", (int) pthread_self(), ADDRQUERY_NAME(qry) );
1072         */
1073         if( ldapqry_get_busy_flag( qry ) ) {
1074                 if( qry->thread ) {
1075                         /* printf( "calling pthread_cancel\n" ); */
1076                         pthread_cancel( * qry->thread );
1077                 }
1078         }
1079 }
1080
1081 /**
1082  * Initialize LDAP query. This function should be called once before executing
1083  * any LDAP queries to initialize thread specific data.
1084  */
1085 void ldapqry_initialize( void ) {
1086         /* printf( "ldapqry_initialize...\n" ); */
1087         if( ! _queryThreadInit_ ) {
1088                 /*
1089                 printf( "ldapqry_initialize::creating thread specific area\n" );
1090                 */
1091                 pthread_key_create( &_queryThreadKey_, ldapqry_destroyer );
1092                 _queryThreadInit_ = TRUE;
1093         }
1094         /* printf( "ldapqry_initialize... done!\n" ); */
1095 }
1096
1097 /**
1098  * Age the query based on LDAP control parameters.
1099  * \param qry    Query object to process.
1100  * \param maxAge Maximum age of query (in seconds).
1101  */
1102 void ldapqry_age( LdapQuery *qry, gint maxAge ) {
1103         gint age;
1104
1105         g_return_if_fail( qry != NULL );
1106
1107         /* Limit the time that queries can hang around */       
1108         if( maxAge < 1 ) maxAge = LDAPCTL_MAX_QUERY_AGE;
1109
1110         /* Check age of query */
1111         age = time( NULL ) - qry->touchTime;
1112         if( age > maxAge ) {
1113                 qry->agedFlag = TRUE;
1114         }
1115 }
1116
1117 /**
1118  * Delete folder associated with query results.
1119  * \param qry Query object to process.
1120  */
1121 void ldapqry_delete_folder( LdapQuery *qry ) {
1122         AddressCache *cache;
1123         ItemFolder *folder;
1124
1125         g_return_if_fail( qry != NULL );
1126
1127         folder = ADDRQUERY_FOLDER(qry);
1128         if( folder ) {
1129                 cache = qry->server->addressCache;
1130                 folder = addrcache_remove_folder_delete( cache, folder );
1131                 if( folder ) {
1132                         addritem_free_item_folder( folder );
1133                 }
1134                 ADDRQUERY_FOLDER(qry) = NULL;
1135         }
1136 }
1137
1138 /**
1139  * Create a name/value pair object.
1140  * \param n Name.
1141  * \param v Value.
1142  * \return Initialized object.
1143  */
1144 static NameValuePair *ldapqry_create_name_value( const gchar *n, const gchar *v ) {
1145         NameValuePair *nvp = g_new0( NameValuePair, 1 );
1146
1147         nvp->name = g_strdup( n );
1148         nvp->value = g_strdup( v );
1149         return nvp;
1150 }
1151
1152 /**
1153  * Free up name/value pair object.
1154  * \param nvp Name/value object.
1155  */
1156 void ldapqry_free_name_value( NameValuePair *nvp ) {
1157         if( nvp ) {
1158                 g_free( nvp->name );
1159                 g_free( nvp->value );
1160                 nvp->name = nvp->value = NULL;
1161                 g_free( nvp );
1162         }
1163 }
1164
1165 /**
1166  * Print name/value pair object for debug.
1167  * \param nvp    Name/value object.
1168  * \param stream Output stream.
1169  */
1170 void ldapqry_print_name_value( NameValuePair *nvp, FILE *stream ) {
1171         if( nvp ) {
1172                 fprintf( stream, "n/v ::%s::%s::\n", nvp->name, nvp->value );
1173         }
1174 }
1175
1176 /**
1177  * Free up a list name/value pair objects.
1178  * \param list List of name/value objects.
1179  */
1180 void ldapqry_free_list_name_value( GList *list ) {
1181         GList *node;
1182
1183         node = list;
1184         while( node ) {
1185                 NameValuePair *nvp = ( NameValuePair * ) node->data;
1186                 ldapqry_free_name_value( nvp );
1187                 node->data = NULL;
1188                 node = g_list_next( node );
1189         }
1190         g_list_free( list );
1191 }
1192
1193 /**
1194  * Load a list of name/value pairs from LDAP attributes.
1195  * \param  ld          LDAP handle.
1196  * \param  e          LDAP message.
1197  * \param  attr       Attribute name.
1198  * \param  listValues List to populate.
1199  * \return List of attribute name/value pairs.
1200  */
1201 static GList *ldapqry_load_attrib_values(
1202                 LDAP *ld, LDAPMessage *entry, char *attr,
1203                 GList *listValues )
1204 {
1205         GList *list = NULL;
1206         gint i;
1207         gchar **vals;
1208         NameValuePair *nvp;
1209
1210         list = listValues;
1211         if( ( vals = ldap_get_values( ld, entry, attr ) ) != NULL ) {
1212                 for( i = 0; vals[i] != NULL; i++ ) {
1213                         nvp = ldapqry_create_name_value( attr, vals[i] );
1214                         list = g_list_append( list, nvp );
1215                 }
1216         }
1217         ldap_value_free( vals );
1218         return list;
1219 }
1220
1221 /**
1222  * Fetch a list of all attributes.
1223  * \param  ld    LDAP handle.
1224  * \param  e     LDAP message.
1225  * \return List of attribute name/value pairs.
1226  */
1227 static GList *ldapqry_fetch_attribs( LDAP *ld, LDAPMessage *e )
1228 {
1229         char *attribute;
1230         BerElement *ber;
1231         GList *listValues = NULL;
1232
1233         /* Process all attributes */
1234         for( attribute = ldap_first_attribute( ld, e, &ber ); attribute != NULL;
1235                 attribute = ldap_next_attribute( ld, e, ber ) ) {
1236                 listValues = ldapqry_load_attrib_values( ld, e, attribute, listValues );
1237                 ldap_memfree( attribute );
1238         }
1239
1240         /* Free up */
1241         if( ber != NULL ) {
1242                 ber_free( ber, 0 );
1243         }
1244         return listValues;
1245 }
1246
1247 #define CRITERIA_SINGLE "(objectclass=*)"
1248
1249 /**
1250  * Perform the data retrieval for a specific LDAP record.
1251  * 
1252  * \param  qry Query object to process.
1253  * \return Error/status code.
1254  */
1255 static gint ldapqry_locate_retrieve( LdapQuery *qry ) {
1256         LdapControl *ctl;
1257         LDAP *ld;
1258         LDAPMessage *result, *e = NULL;
1259         gboolean entriesFound;
1260         gboolean first;
1261         struct timeval timeout;
1262         gint rc;
1263         gchar *dn;
1264         GList *listValues;
1265
1266         /* Initialize some variables */
1267         ld = qry->ldap;
1268         ctl = qry->control;
1269         dn = ADDRQUERY_SEARCHVALUE(qry);
1270         timeout.tv_sec = ctl->timeOut;
1271         timeout.tv_usec = 0L;
1272         entriesFound = FALSE;
1273
1274         /*
1275          * Execute the search - this step may take some time to complete
1276          * depending on network traffic and server response time.
1277          */
1278         ADDRQUERY_RETVAL(qry) = LDAPRC_TIMEOUT;
1279         rc = ldap_search_ext_s( ld, dn, LDAP_SCOPE_BASE, CRITERIA_SINGLE,
1280                 NULL, 0, NULL, NULL, &timeout, 0, &result );
1281         if( rc == LDAP_TIMEOUT ) {
1282                 return ADDRQUERY_RETVAL(qry);
1283         }
1284         ADDRQUERY_RETVAL(qry) = LDAPRC_SEARCH;
1285         if( rc != LDAP_SUCCESS ) {
1286                 /*
1287                 printf( "LDAP Error: ldap_search_st: %s\n", ldap_err2string( rc ) );
1288                 */
1289                 return ADDRQUERY_RETVAL(qry);
1290         }
1291
1292         /*
1293         printf( "Total results are: %d\n", ldap_count_entries( ld, result ) );
1294         */
1295
1296         /* Process results */
1297         ADDRQUERY_RETVAL(qry) = LDAPRC_STOP_FLAG;
1298         first = TRUE;
1299         while( TRUE ) {
1300                 ldapqry_touch( qry );
1301                 if( qry->entriesRead >= ctl->maxEntries ) break;                
1302
1303                 /* Test for stop */
1304                 if( ldapqry_get_stop_flag( qry ) ) {
1305                         break;
1306                 }
1307
1308                 /* Retrieve entry */            
1309                 if( first ) {
1310                         first = FALSE;
1311                         e = ldap_first_entry( ld, result );
1312                 }
1313                 else {
1314                         e = ldap_next_entry( ld, e );
1315                 }
1316                 if( e == NULL ) break;
1317
1318                 entriesFound = TRUE;
1319
1320                 /* Setup a critical section here */
1321                 pthread_mutex_lock( qry->mutexEntry );
1322
1323                 /* Process entry */
1324                 listValues = ldapqry_fetch_attribs( ld, e );
1325
1326                 /* Process callback */
1327                 if( qry->callBackEntry ) {
1328                         qry->callBackEntry( qry, ADDRQUERY_ID(qry), listValues, qry->data );
1329                 }
1330                 ldapqry_free_list_name_value( listValues );
1331                 listValues = NULL;
1332
1333                 pthread_mutex_unlock( qry->mutexEntry );
1334         }
1335
1336         /* Free up and disconnect */
1337         ldap_msgfree( result );
1338
1339         if( entriesFound ) {
1340                 ADDRQUERY_RETVAL(qry) = LDAPRC_SUCCESS;
1341         }
1342         else {
1343                 ADDRQUERY_RETVAL(qry) = LDAPRC_NOENTRIES;
1344         }
1345
1346         return ADDRQUERY_RETVAL(qry);
1347 }
1348
1349 /**
1350  * Perform the search to locate a specific LDAP record identified by
1351  * distinguished name (dn).
1352  * 
1353  * \param  qry Query object to process.
1354  * \return Error/status code.
1355  */
1356 static gint ldapqry_perform_locate( LdapQuery *qry ) {
1357         /* Connect */
1358         qry->ldap = NULL;
1359         ldapqry_connect( qry );
1360         if( ADDRQUERY_RETVAL(qry) == LDAPRC_SUCCESS ) {
1361                 /* Perform search */
1362                 ldapqry_locate_retrieve( qry );
1363         }
1364         /* Disconnect */
1365         ldapqry_disconnect( qry );
1366         qry->ldap = NULL;
1367
1368         /* Process callback */  
1369         if( qry->callBackEnd ) {
1370                 g_timeout_add(0, callbackend, qry);
1371         }
1372
1373         return ADDRQUERY_RETVAL(qry);
1374 }
1375
1376 /**
1377  * Remove results (folder and data) for specified LDAP query.
1378  * \param  qry Query object to process.
1379  * \return TRUE if folder deleted successfully.
1380  */
1381 gboolean ldapquery_remove_results( LdapQuery *qry ) {
1382         gboolean retVal = FALSE;
1383
1384         ldapqry_set_aged_flag( qry, TRUE );
1385
1386         if( ldapqry_get_busy_flag( qry ) ) {
1387                 ldapqry_set_stop_flag( qry, TRUE );
1388         }
1389         else {
1390                 LdapServer *server = qry->server;
1391                 server->listQuery = g_list_remove(server->listQuery, qry);
1392
1393                 retVal = TRUE;
1394         }
1395         return retVal;
1396 }
1397
1398 #endif  /* USE_LDAP */
1399
1400 /*
1401  * End of Source.
1402  */
1403
1404