03f8dbbe2fd6e3d2651c62de1eb527767ba886d6
[asterisk/asterisk.git] / apps / app_directory.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 1999 - 2005, Digium, Inc.
5  *
6  * Mark Spencer <markster@digium.com>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*! \file
20  *
21  * \brief Provide a directory of extensions
22  *
23  * \author Mark Spencer <markster@digium.com>
24  *
25  * \ingroup applications
26  */
27
28 #include "asterisk.h"
29
30 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
31
32 #include <ctype.h>
33
34 #include "asterisk/paths.h" /* use ast_config_AST_SPOOL_DIR */
35 #include "asterisk/file.h"
36 #include "asterisk/pbx.h"
37 #include "asterisk/module.h"
38 #include "asterisk/say.h"
39 #include "asterisk/app.h"
40 #include "asterisk/utils.h"
41
42 #ifdef ODBC_STORAGE
43 #include <sys/mman.h>
44 #include "asterisk/res_odbc.h"
45
46 static char odbc_database[80] = "asterisk";
47 static char odbc_table[80] = "voicemessages";
48 static char vmfmts[80] = "wav";
49 #endif
50
51 static char *app = "Directory";
52
53 static char *synopsis = "Provide directory of voicemail extensions";
54 static char *descrip =
55 "  Directory(vm-context[,dial-context[,options]]): This application will present\n"
56 "the calling channel with a directory of extensions from which they can search\n"
57 "by name. The list of names and corresponding extensions is retrieved from the\n"
58 "voicemail configuration file, voicemail.conf.\n"
59 "  This application will immediately exit if one of the following DTMF digits are\n"
60 "received and the extension to jump to exists:\n"
61 "    0 - Jump to the 'o' extension, if it exists.\n"
62 "    * - Jump to the 'a' extension, if it exists.\n\n"
63 "  Parameters:\n"
64 "    vm-context   - This is the context within voicemail.conf to use for the\n"
65 "                   Directory.\n"
66 "    dial-context - This is the dialplan context to use when looking for an\n"
67 "                   extension that the user has selected, or when jumping to the\n"
68 "                   'o' or 'a' extension.\n\n"
69 "  Options:\n"
70 "    e           In addition to the name, also read the extension number to the\n"
71 "              caller before presenting dialing options.\n"
72 "    f[(<n>)]    Allow the caller to enter the first name of a user in the\n"
73 "              directory instead of using the last name.  If specified, the\n"
74 "              optional number argument will be used for the number of\n"
75 "              characters the user should enter.\n"
76 "    l[(<n>)]    Allow the caller to enter the last name of a user in the\n"
77 "              directory.  This is the default.  If specified, the\n"
78 "              optional number argument will be used for the number of\n"
79 "              characters the user should enter.\n"
80 "    b[(<n>)]    Allow the caller to enter either the first or the last name\n"
81 "              of a user in the directory.  If specified, the optional number\n"
82 "              argument will be used for the number of characters the user\n"
83 "              should enter.\n"
84 "    m           Instead of reading each name sequentially and asking for\n"
85 "              confirmation, create a menu of up to 8 names.\n"
86 "    p(<n>)      Pause for n milliseconds after the digits are typed.  This is\n"
87 "              helpful for people with cellphones, who are not holding the\n"
88 "              receiver to their ear while entering DTMF.\n"
89 "\n"
90 "    Only one of the f, l, or b options may be specified.  If more than one is\n"
91 "    specified, then Directory will act as if 'b' was specified.  The number\n"
92 "    of characters for the user to type defaults to 3.\n";
93
94 /* For simplicity, I'm keeping the format compatible with the voicemail config,
95    but i'm open to suggestions for isolating it */
96
97 #define VOICEMAIL_CONFIG "voicemail.conf"
98
99 enum {
100         OPT_LISTBYFIRSTNAME = (1 << 0),
101         OPT_SAYEXTENSION =    (1 << 1),
102         OPT_FROMVOICEMAIL =   (1 << 2),
103         OPT_SELECTFROMMENU =  (1 << 3),
104         OPT_LISTBYLASTNAME =  (1 << 4),
105         OPT_LISTBYEITHER =    OPT_LISTBYFIRSTNAME | OPT_LISTBYLASTNAME,
106         OPT_PAUSE =           (1 << 5),
107 } directory_option_flags;
108
109 enum {
110         OPT_ARG_FIRSTNAME =   0,
111         OPT_ARG_LASTNAME =    1,
112         OPT_ARG_EITHER =      2,
113         OPT_ARG_PAUSE =       3,
114         /* This *must* be the last value in this enum! */
115         OPT_ARG_ARRAY_SIZE =  4,
116 };
117
118 struct directory_item {
119         char exten[AST_MAX_EXTENSION + 1];
120         char name[AST_MAX_EXTENSION + 1];
121         char key[50]; /* Text to order items. Either lastname+firstname or firstname+lastname */
122
123         AST_LIST_ENTRY(directory_item) entry;
124 };
125
126 AST_APP_OPTIONS(directory_app_options, {
127         AST_APP_OPTION_ARG('f', OPT_LISTBYFIRSTNAME, OPT_ARG_FIRSTNAME),
128         AST_APP_OPTION_ARG('l', OPT_LISTBYLASTNAME, OPT_ARG_LASTNAME),
129         AST_APP_OPTION_ARG('b', OPT_LISTBYEITHER, OPT_ARG_EITHER),
130         AST_APP_OPTION_ARG('p', OPT_PAUSE, OPT_ARG_PAUSE),
131         AST_APP_OPTION('e', OPT_SAYEXTENSION),
132         AST_APP_OPTION('v', OPT_FROMVOICEMAIL),
133         AST_APP_OPTION('m', OPT_SELECTFROMMENU),
134 });
135
136 #ifdef ODBC_STORAGE
137 struct generic_prepare_struct {
138         const char *sql;
139         const char *param;
140 };
141
142 static SQLHSTMT generic_prepare(struct odbc_obj *obj, void *data)
143 {
144         struct generic_prepare_struct *gps = data;
145         SQLHSTMT stmt;
146         int res;
147
148         res = SQLAllocHandle(SQL_HANDLE_STMT, obj->con, &stmt);
149         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
150                 ast_log(LOG_WARNING, "SQL Alloc Handle failed!\n");
151                 return NULL;
152         }
153
154         res = SQLPrepare(stmt, (unsigned char *)gps->sql, SQL_NTS);
155         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
156                 ast_log(LOG_WARNING, "SQL Prepare failed![%s]\n", (char *)gps->sql);
157                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
158                 return NULL;
159         }
160
161         if (!ast_strlen_zero(gps->param))
162                 SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, strlen(gps->param), 0, (void *)gps->param, 0, NULL);
163
164         return stmt;
165 }
166
167 static void retrieve_file(char *dir)
168 {
169         int x = 0;
170         int res;
171         int fd=-1;
172         size_t fdlen = 0;
173         void *fdm = MAP_FAILED;
174         SQLHSTMT stmt;
175         char sql[256];
176         char fmt[80]="", empty[10] = "";
177         char *c;
178         SQLLEN colsize;
179         char full_fn[256];
180         struct odbc_obj *obj;
181         struct generic_prepare_struct gps = { .sql = sql, .param = dir };
182
183         obj = ast_odbc_request_obj(odbc_database, 1);
184         if (obj) {
185                 do {
186                         ast_copy_string(fmt, vmfmts, sizeof(fmt));
187                         c = strchr(fmt, '|');
188                         if (c)
189                                 *c = '\0';
190                         if (!strcasecmp(fmt, "wav49"))
191                                 strcpy(fmt, "WAV");
192                         snprintf(full_fn, sizeof(full_fn), "%s.%s", dir, fmt);
193                         snprintf(sql, sizeof(sql), "SELECT recording FROM %s WHERE dir=? AND msgnum=-1", odbc_table);
194                         stmt = ast_odbc_prepare_and_execute(obj, generic_prepare, &gps);
195
196                         if (!stmt) {
197                                 ast_log(LOG_WARNING, "SQL Execute error!\n[%s]\n\n", sql);
198                                 break;
199                         }
200                         res = SQLFetch(stmt);
201                         if (res == SQL_NO_DATA) {
202                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
203                                 break;
204                         } else if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
205                                 ast_log(LOG_WARNING, "SQL Fetch error!\n[%s]\n\n", sql);
206                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
207                                 break;
208                         }
209                         fd = open(full_fn, O_RDWR | O_CREAT | O_TRUNC, AST_FILE_MODE);
210                         if (fd < 0) {
211                                 ast_log(LOG_WARNING, "Failed to write '%s': %s\n", full_fn, strerror(errno));
212                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
213                                 break;
214                         }
215
216                         res = SQLGetData(stmt, 1, SQL_BINARY, empty, 0, &colsize);
217                         fdlen = colsize;
218                         if (fd > -1) {
219                                 char tmp[1]="";
220                                 lseek(fd, fdlen - 1, SEEK_SET);
221                                 if (write(fd, tmp, 1) != 1) {
222                                         close(fd);
223                                         fd = -1;
224                                         break;
225                                 }
226                                 if (fd > -1)
227                                         fdm = mmap(NULL, fdlen, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
228                         }
229                         if (fdm != MAP_FAILED) {
230                                 memset(fdm, 0, fdlen);
231                                 res = SQLGetData(stmt, x + 1, SQL_BINARY, fdm, fdlen, &colsize);
232                                 if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
233                                         ast_log(LOG_WARNING, "SQL Get Data error!\n[%s]\n\n", sql);
234                                         SQLFreeHandle(SQL_HANDLE_STMT, stmt);
235                                         break;
236                                 }
237                         }
238                         SQLFreeHandle(SQL_HANDLE_STMT, stmt);
239                 } while (0);
240                 ast_odbc_release_obj(obj);
241         } else
242                 ast_log(LOG_WARNING, "Failed to obtain database object for '%s'!\n", odbc_database);
243         if (fdm != MAP_FAILED)
244                 munmap(fdm, fdlen);
245         if (fd > -1)
246                 close(fd);
247         return;
248 }
249 #endif
250
251 static int compare(const char *text, const char *template)
252 {
253         char digit;
254
255         while (*template) {
256                 digit = toupper(*text++);
257                 switch (digit) {
258                 case 0:
259                         return -1;
260                 case '1':
261                         digit = '1';
262                         break;
263                 case '2':
264                 case 'A':
265                 case 'B':
266                 case 'C':
267                         digit = '2';
268                         break;
269                 case '3':
270                 case 'D':
271                 case 'E':
272                 case 'F':
273                         digit = '3';
274                         break;
275                 case '4':
276                 case 'G':
277                 case 'H':
278                 case 'I':
279                         digit = '4';
280                         break;
281                 case '5':
282                 case 'J':
283                 case 'K':
284                 case 'L':
285                         digit = '5';
286                         break;
287                 case '6':
288                 case 'M':
289                 case 'N':
290                 case 'O':
291                         digit = '6';
292                         break;
293                 case '7':
294                 case 'P':
295                 case 'Q':
296                 case 'R':
297                 case 'S':
298                         digit = '7';
299                         break;
300                 case '8':
301                 case 'T':
302                 case 'U':
303                 case 'V':
304                         digit = '8';
305                         break;
306                 case '9':
307                 case 'W':
308                 case 'X':
309                 case 'Y':
310                 case 'Z':
311                         digit = '9';
312                         break;
313
314                 default:
315                         if (digit > ' ')
316                                 return -1;
317                         continue;
318                 }
319
320                 if (*template++ != digit)
321                         return -1;
322         }
323
324         return 0;
325 }
326
327 /* play name of mailbox owner.
328  * returns:  -1 for bad or missing extension
329  *           '1' for selected entry from directory
330  *           '*' for skipped entry from directory
331  */
332 static int play_mailbox_owner(struct ast_channel *chan, const char *context,
333         const char *ext, const char *name, struct ast_flags *flags)
334 {
335         int res = 0;
336         char fn[256];
337
338         /* Check for the VoiceMail2 greeting first */
339         snprintf(fn, sizeof(fn), "%s/voicemail/%s/%s/greet",
340                 ast_config_AST_SPOOL_DIR, context, ext);
341 #ifdef ODBC_STORAGE
342         retrieve_file(fn);
343 #endif
344
345         if (ast_fileexists(fn, NULL, chan->language) <= 0) {
346                 /* no file, check for an old-style Voicemail greeting */
347                 snprintf(fn, sizeof(fn), "%s/vm/%s/greet",
348                         ast_config_AST_SPOOL_DIR, ext);
349         }
350 #ifdef ODBC_STORAGE
351         retrieve_file(fn);
352 #endif
353
354         if (ast_fileexists(fn, NULL, chan->language) > 0) {
355                 res = ast_stream_and_wait(chan, fn, AST_DIGIT_ANY);
356                 ast_stopstream(chan);
357                 /* If Option 'e' was specified, also read the extension number with the name */
358                 if (ast_test_flag(flags, OPT_SAYEXTENSION)) {
359                         ast_stream_and_wait(chan, "vm-extension", AST_DIGIT_ANY);
360                         res = ast_say_character_str(chan, ext, AST_DIGIT_ANY, chan->language);
361                 }
362         } else {
363                 res = ast_say_character_str(chan, S_OR(name, ext), AST_DIGIT_ANY, chan->language);
364                 if (!ast_strlen_zero(name) && ast_test_flag(flags, OPT_SAYEXTENSION)) {
365                         ast_stream_and_wait(chan, "vm-extension", AST_DIGIT_ANY);
366                         res = ast_say_character_str(chan, ext, AST_DIGIT_ANY, chan->language);
367                 }
368         }
369 #ifdef ODBC_STORAGE
370         ast_filedelete(fn, NULL);
371 #endif
372
373         return res;
374 }
375
376 static int select_entry(struct ast_channel *chan, const char *context, const char *dialcontext, const struct directory_item *item, struct ast_flags *flags)
377 {
378         ast_debug(1, "Selecting '%s' - %s@%s\n", item->name, item->exten, dialcontext);
379
380         if (ast_test_flag(flags, OPT_FROMVOICEMAIL)) {
381                 /* We still want to set the exten though */
382                 ast_copy_string(chan->exten, item->exten, sizeof(chan->exten));
383         } else if (ast_goto_if_exists(chan, dialcontext, item->exten, 1)) {
384                 ast_log(LOG_WARNING,
385                         "Can't find extension '%s' in context '%s'.  "
386                         "Did you pass the wrong context to Directory?\n",
387                         item->exten, dialcontext);
388                 return -1;
389         }
390
391         return 0;
392 }
393
394 static int select_item_seq(struct ast_channel *chan, struct directory_item **items, int count, const char *context, const char *dialcontext, struct ast_flags *flags)
395 {
396         struct directory_item *item, **ptr;
397         int i, res, loop;
398
399         for (ptr = items, i = 0; i < count; i++, ptr++) {
400                 item = *ptr;
401
402                 for (loop = 3 ; loop > 0; loop--) {
403                         res = play_mailbox_owner(chan, context, item->exten, item->name, flags);
404
405                         if (!res)
406                                 res = ast_stream_and_wait(chan, "dir-instr", AST_DIGIT_ANY);
407                         if (!res)
408                                 res = ast_waitfordigit(chan, 3000);
409                         ast_stopstream(chan);
410         
411                         if (res == '1') { /* Name selected */
412                                 return select_entry(chan, context, dialcontext, item, flags) ? -1 : 1;
413                         } else if (res == '*') {
414                                 /* Skip to next match in list */
415                                 break;
416                         }
417
418                         if (res < 0)
419                                 return -1;
420
421                         res = 0;
422                 }
423         }
424
425         /* Nothing was selected */
426         return 0;
427 }
428
429 static int select_item_menu(struct ast_channel *chan, struct directory_item **items, int count, const char *context, const char *dialcontext, struct ast_flags *flags)
430 {
431         struct directory_item **block, *item;
432         int i, limit, res = 0;
433         char buf[9];
434
435         for (block = items; count; block += limit, count -= limit) {
436                 limit = count;
437                 if (limit > 8)
438                         limit = 8;
439
440                 for (i = 0; i < limit && !res; i++) {
441                         item = block[i];
442
443                         snprintf(buf, sizeof(buf), "digits/%d", i + 1);
444                         /* Press <num> for <name>, [ extension <ext> ] */
445                         res = ast_streamfile(chan, "dir-multi1", chan->language);
446                         if (!res)
447                                 res = ast_waitstream(chan, AST_DIGIT_ANY);
448                         if (!res)
449                                 res = ast_streamfile(chan, buf, chan->language);
450                         if (!res)
451                                 res = ast_waitstream(chan, AST_DIGIT_ANY);
452                         if (!res)
453                                 res = ast_streamfile(chan, "dir-multi2", chan->language);
454                         if (!res)
455                                 res = ast_waitstream(chan, AST_DIGIT_ANY);
456                         if (!res)
457                                 res = play_mailbox_owner(chan, context, item->exten, item->name, flags);
458                         if (!res)
459                                 res = ast_waitstream(chan, AST_DIGIT_ANY);
460                         if (!res)
461                                 res = ast_waitfordigit(chan, 800);
462                 }
463
464                 /* Press "9" for more names. */
465                 if (!res && count > limit) {
466                         res = ast_streamfile(chan, "dir-multi9", chan->language);
467                         if (!res)
468                                 res = ast_waitstream(chan, AST_DIGIT_ANY);
469                 }
470
471                 if (!res) {
472                         res = ast_waitfordigit(chan, 3000);
473                 }
474
475                 if (res && res > '0' && res < '1' + limit) {
476                         return select_entry(chan, context, dialcontext, block[res - '1'], flags) ? -1 : 1;
477                 }
478
479                 if (res < 0)
480                         return -1;
481
482                 res = 0;
483         }
484
485         /* Nothing was selected */
486         return 0;
487 }
488
489 static struct ast_config *realtime_directory(char *context)
490 {
491         struct ast_config *cfg;
492         struct ast_config *rtdata;
493         struct ast_category *cat;
494         struct ast_variable *var;
495         char *mailbox;
496         const char *fullname;
497         const char *hidefromdir;
498         char tmp[100];
499         struct ast_flags config_flags = { 0 };
500
501         /* Load flat file config. */
502         cfg = ast_config_load(VOICEMAIL_CONFIG, config_flags);
503
504         if (!cfg) {
505                 /* Loading config failed. */
506                 ast_log(LOG_WARNING, "Loading config failed.\n");
507                 return NULL;
508         }
509
510         /* Get realtime entries, categorized by their mailbox number
511            and present in the requested context */
512         rtdata = ast_load_realtime_multientry("voicemail", "mailbox LIKE", "%", "context", context, NULL);
513
514         /* if there are no results, just return the entries from the config file */
515         if (!rtdata)
516                 return cfg;
517
518         /* Does the context exist within the config file? If not, make one */
519         cat = ast_category_get(cfg, context);
520         if (!cat) {
521                 cat = ast_category_new(context, "", 99999);
522                 if (!cat) {
523                         ast_log(LOG_WARNING, "Out of memory\n");
524                         ast_config_destroy(cfg);
525                         return NULL;
526                 }
527                 ast_category_append(cfg, cat);
528         }
529
530         mailbox = NULL;
531         while ( (mailbox = ast_category_browse(rtdata, mailbox)) ) {
532                 fullname = ast_variable_retrieve(rtdata, mailbox, "fullname");
533                 if (ast_true((hidefromdir = ast_variable_retrieve(rtdata, mailbox, "hidefromdir")))) {
534                         /* Skip hidden */
535                         continue;
536                 }
537                 snprintf(tmp, sizeof(tmp), "no-password,%s", S_OR(fullname, ""));
538                 var = ast_variable_new(mailbox, tmp, "");
539                 if (var)
540                         ast_variable_append(cat, var);
541                 else
542                         ast_log(LOG_WARNING, "Out of memory adding mailbox '%s'\n", mailbox);
543         }
544         ast_config_destroy(rtdata);
545
546         return cfg;
547 }
548
549 static int check_match(struct directory_item **result, const char *item_fullname, const char *item_ext, const char *pattern_ext, int use_first_name)
550 {
551         struct directory_item *item;
552         const char *key = NULL;
553         int namelen;
554
555
556         /* Set key to last name or first name depending on search mode */
557         if (!use_first_name)
558                 key = strchr(item_fullname, ' ');
559
560         if (key)
561                 key++;
562         else
563                 key = item_fullname;
564
565         if (compare(key, pattern_ext))
566                 return 0;
567
568         /* Match */
569         item = ast_calloc(1, sizeof(*item));
570         if (!item)
571                 return -1;
572         ast_copy_string(item->name, item_fullname, sizeof(item->name));
573         ast_copy_string(item->exten, item_ext, sizeof(item->exten));
574
575         ast_copy_string(item->key, key, sizeof(item->key));
576         if (key != item_fullname) {
577                 /* Key is the last name. Append first name to key in order to sort Last,First */
578                 namelen = key - item_fullname - 1;
579                 if (namelen > sizeof(item->key) - strlen(item->key) - 1)
580                         namelen = sizeof(item->key) - strlen(item->key) - 1;
581                 strncat(item->key, item_fullname, namelen);
582         }
583
584         *result = item;
585         return 1;
586 }
587
588 typedef AST_LIST_HEAD_NOLOCK(, directory_item) itemlist;
589
590 static int search_directory(const char *context, struct ast_config *vmcfg, struct ast_config *ucfg, const char *ext, struct ast_flags flags, itemlist *alist)
591 {
592         struct ast_variable *v;
593         char buf[AST_MAX_EXTENSION + 1], *pos, *bufptr, *cat;
594         struct directory_item *item;
595         int res;
596
597         ast_debug(2, "Pattern: %s\n", ext);
598
599         for (v = ast_variable_browse(vmcfg, context); v; v = v->next) {
600
601                 /* Ignore hidden */
602                 if (strcasestr(v->value, "hidefromdir=yes"))
603                         continue;
604
605                 ast_copy_string(buf, v->value, sizeof(buf));
606                 bufptr = buf;
607
608                 /* password,Full Name,email,pager,options */
609                 strsep(&bufptr, ",");
610                 pos = strsep(&bufptr, ",");
611
612                 res = 0;
613                 if (ast_test_flag(&flags, OPT_LISTBYLASTNAME)) {
614                         res = check_match(&item, pos, v->name, ext, 0 /* use_first_name */);
615                 }
616                 if (!res && ast_test_flag(&flags, OPT_LISTBYFIRSTNAME)) {
617                         res = check_match(&item, pos, v->name, ext, 1 /* use_first_name */);
618                 }
619
620                 if (!res)
621                         continue;
622                 else if (res < 0)
623                         return -1;
624
625                 AST_LIST_INSERT_TAIL(alist, item, entry);
626         }
627
628
629         if (ucfg) {
630                 for (cat = ast_category_browse(ucfg, NULL); cat ; cat = ast_category_browse(ucfg, cat)) {
631                         const char *pos;
632                         if (!strcasecmp(cat, "general"))
633                                 continue;
634                         if (!ast_true(ast_config_option(ucfg, cat, "hasdirectory")))
635                                 continue;
636
637                         /* Find all candidate extensions */
638                         pos = ast_variable_retrieve(ucfg, cat, "fullname");
639                         if (!pos)
640                                 continue;
641
642                         res = 0;
643                         if (ast_test_flag(&flags, OPT_LISTBYLASTNAME)) {
644                                 res = check_match(&item, pos, v->name, ext, 0 /* use_first_name */);
645                         }
646                         if (!res && ast_test_flag(&flags, OPT_LISTBYFIRSTNAME)) {
647                                 res = check_match(&item, pos, v->name, ext, 1 /* use_first_name */);
648                         }
649
650                         if (!res)
651                                 continue;
652                         else if (res < 0)
653                                 return -1;
654
655                         AST_LIST_INSERT_TAIL(alist, item, entry);
656                 }
657         }
658
659         return 0;
660 }
661
662 static void sort_items(struct directory_item **sorted, int count)
663 {
664         int reordered, i;
665         struct directory_item **ptr, *tmp;
666
667         if (count < 2)
668                 return;
669
670         /* Bubble-sort items by the key */
671         do {
672                 reordered = 0;
673                 for (ptr = sorted, i = 0; i < count - 1; i++, ptr++) {
674                         if (strcasecmp(ptr[0]->key, ptr[1]->key) > 0) {
675                                 tmp = ptr[0];
676                                 ptr[0] = ptr[1];
677                                 ptr[1] = tmp;
678                                 reordered++;
679                         }
680                 }
681         } while (reordered);
682 }
683
684 static int goto_exten(struct ast_channel *chan, const char *dialcontext, char *ext)
685 {
686         if (!ast_goto_if_exists(chan, dialcontext, ext, 1) ||
687                 (!ast_strlen_zero(chan->macrocontext) &&
688                 !ast_goto_if_exists(chan, chan->macrocontext, ext, 1))) {
689                 return 0;
690         } else {
691                 ast_log(LOG_WARNING, "Can't find extension '%s' in current context.  "
692                         "Not Exiting the Directory!\n", ext);
693                 return -1;
694         }
695 }
696
697 static int do_directory(struct ast_channel *chan, struct ast_config *vmcfg, struct ast_config *ucfg, char *context, char *dialcontext, char digit, int digits, struct ast_flags *flags)
698 {
699         /* Read in the first three digits..  "digit" is the first digit, already read */
700         int res = 0;
701         itemlist alist = AST_LIST_HEAD_NOLOCK_INIT_VALUE;
702         struct directory_item *item, **ptr, **sorted = NULL;
703         int count, i;
704         char ext[10] = "";
705
706         if (ast_strlen_zero(context)) {
707                 ast_log(LOG_WARNING,
708                         "Directory must be called with an argument "
709                         "(context in which to interpret extensions)\n");
710                 return -1;
711         }
712
713         if (digit == '0' && !goto_exten(chan, dialcontext, "o")) {
714                 return 0;
715         }
716
717         if (digit == '*' && !goto_exten(chan, dialcontext, "a")) {
718                 return 0;
719         }
720
721         ext[0] = digit;
722         if (ast_readstring(chan, ext + 1, digits - 1, 3000, 3000, "#") < 0)
723                 return -1;
724
725         res = search_directory(context, vmcfg, ucfg, ext, *flags, &alist);
726         if (res)
727                 goto exit;
728
729         /* Count items in the list */
730         count = 0;
731         AST_LIST_TRAVERSE(&alist, item, entry) {
732                 count++;
733         }
734
735         if (count < 1) {
736                 res = ast_streamfile(chan, "dir-nomatch", chan->language);
737                 goto exit;
738         }
739
740
741         /* Create plain array of pointers to items (for sorting) */
742         sorted = ast_calloc(count, sizeof(*sorted));
743
744         ptr = sorted;
745         AST_LIST_TRAVERSE(&alist, item, entry) {
746                 *ptr++ = item;
747         }
748
749         /* Sort items */
750         sort_items(sorted, count);
751
752         if (option_debug) {
753                 ast_debug(2, "Listing matching entries:\n");
754                 for (ptr = sorted, i = 0; i < count; i++, ptr++) {
755                         ast_log(LOG_DEBUG, "%s: %s\n", ptr[0]->exten, ptr[0]->name);
756                 }
757         }
758
759         if (ast_test_flag(flags, OPT_SELECTFROMMENU)) {
760                 /* Offer multiple entries at the same time */
761                 res = select_item_menu(chan, sorted, count, context, dialcontext, flags);
762         } else {
763                 /* Offer entries one by one */
764                 res = select_item_seq(chan, sorted, count, context, dialcontext, flags);
765         }
766
767         if (!res) {
768                 res = ast_streamfile(chan, "dir-nomore", chan->language);
769         }
770
771 exit:
772         if (sorted)
773                 ast_free(sorted);
774
775         while ((item = AST_LIST_REMOVE_HEAD(&alist, entry)))
776                 ast_free(item);
777
778         return res;
779 }
780
781 static int directory_exec(struct ast_channel *chan, void *data)
782 {
783         int res = 0, digit = 3;
784         struct ast_config *cfg, *ucfg;
785         const char *dirintro;
786         char *parse, *opts[OPT_ARG_ARRAY_SIZE];
787         struct ast_flags flags = { 0 };
788         struct ast_flags config_flags = { 0 };
789         enum { FIRST, LAST, BOTH } which = LAST;
790         char digits[9] = "digits/3";
791         AST_DECLARE_APP_ARGS(args,
792                 AST_APP_ARG(vmcontext);
793                 AST_APP_ARG(dialcontext);
794                 AST_APP_ARG(options);
795         );
796
797         if (ast_strlen_zero(data)) {
798                 ast_log(LOG_WARNING, "Directory requires an argument (context[,dialcontext])\n");
799                 return -1;
800         }
801
802         parse = ast_strdupa(data);
803
804         AST_STANDARD_APP_ARGS(args, parse);
805
806         if (args.options && ast_app_parse_options(directory_app_options, &flags, opts, args.options))
807                 return -1;
808
809         if (ast_strlen_zero(args.dialcontext))
810                 args.dialcontext = args.vmcontext;
811
812         cfg = realtime_directory(args.vmcontext);
813         if (!cfg) {
814                 ast_log(LOG_ERROR, "Unable to read the configuration data!\n");
815                 return -1;
816         }
817
818         ucfg = ast_config_load("users.conf", config_flags);
819
820         dirintro = ast_variable_retrieve(cfg, args.vmcontext, "directoryintro");
821         if (ast_strlen_zero(dirintro))
822                 dirintro = ast_variable_retrieve(cfg, "general", "directoryintro");
823
824         if (ast_test_flag(&flags, OPT_LISTBYFIRSTNAME) && ast_test_flag(&flags, OPT_LISTBYLASTNAME)) {
825                 if (!ast_strlen_zero(opts[OPT_ARG_EITHER])) {
826                         digit = atoi(opts[OPT_ARG_EITHER]);
827                 }
828                 which = BOTH;
829         } else if (ast_test_flag(&flags, OPT_LISTBYFIRSTNAME)) {
830                 if (!ast_strlen_zero(opts[OPT_ARG_FIRSTNAME])) {
831                         digit = atoi(opts[OPT_ARG_FIRSTNAME]);
832                 }
833                 which = FIRST;
834         } else {
835                 if (!ast_strlen_zero(opts[OPT_ARG_LASTNAME])) {
836                         digit = atoi(opts[OPT_ARG_LASTNAME]);
837                 }
838                 which = LAST;
839         }
840
841         /* If no options specified, search by last name */
842         if (!ast_test_flag(&flags, OPT_LISTBYFIRSTNAME) && !ast_test_flag(&flags, OPT_LISTBYLASTNAME)) {
843                 ast_set_flag(&flags, OPT_LISTBYLASTNAME);
844                 which = LAST;
845         }
846
847         if (digit > 9) {
848                 digit = 9;
849         } else if (digit < 1) {
850                 digit = 3;
851         }
852         digits[7] = digit + '0';
853
854         if (chan->_state != AST_STATE_UP)
855                 res = ast_answer(chan);
856
857         for (;;) {
858                 if (!ast_strlen_zero(dirintro) && !res) {
859                         res = ast_stream_and_wait(chan, dirintro, AST_DIGIT_ANY);
860                 } else if (!res) {
861                         res = ast_stream_and_wait(chan, "dir-welcome", AST_DIGIT_ANY) ||
862                                 ast_stream_and_wait(chan, "dir-pls-enter", AST_DIGIT_ANY) ||
863                                 ast_stream_and_wait(chan, digits, AST_DIGIT_ANY) ||
864                                 ast_stream_and_wait(chan, 
865                                         which == FIRST ? "dir-first" :
866                                         which == LAST ? "dir-last" :
867                                         "dir-firstlast", AST_DIGIT_ANY) ||
868                                 ast_stream_and_wait(chan, "dir-usingkeypad", AST_DIGIT_ANY);
869                 }
870                 ast_stopstream(chan);
871                 if (!res)
872                         res = ast_waitfordigit(chan, 5000);
873
874                 if (res <= 0)
875                         break;
876
877                 res = do_directory(chan, cfg, ucfg, args.vmcontext, args.dialcontext, res, digit, &flags);
878                 if (res)
879                         break;
880
881                 res = ast_waitstream(chan, AST_DIGIT_ANY);
882                 ast_stopstream(chan);
883
884                 if (res)
885                         break;
886         }
887
888         if (ucfg)
889                 ast_config_destroy(ucfg);
890         ast_config_destroy(cfg);
891
892         return res < 0 ? -1 : 0;
893 }
894
895 static int unload_module(void)
896 {
897         int res;
898         res = ast_unregister_application(app);
899         return res;
900 }
901
902 static int load_module(void)
903 {
904 #ifdef ODBC_STORAGE
905         struct ast_flags config_flags = { 0 };
906         struct ast_config *cfg = ast_config_load(VOICEMAIL_CONFIG, config_flags);
907         const char *tmp;
908
909         if (cfg) {
910                 if ((tmp = ast_variable_retrieve(cfg, "general", "odbcstorage"))) {
911                         ast_copy_string(odbc_database, tmp, sizeof(odbc_database));
912                 }
913                 if ((tmp = ast_variable_retrieve(cfg, "general", "odbctable"))) {
914                         ast_copy_string(odbc_table, tmp, sizeof(odbc_table));
915                 }
916                 if ((tmp = ast_variable_retrieve(cfg, "general", "format"))) {
917                         ast_copy_string(vmfmts, tmp, sizeof(vmfmts));
918                 }
919                 ast_config_destroy(cfg);
920         } else
921                 ast_log(LOG_WARNING, "Unable to load " VOICEMAIL_CONFIG " - ODBC defaults will be used\n");
922 #endif
923
924         return ast_register_application(app, directory_exec, synopsis, descrip);
925 }
926
927 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Extension Directory");