2 * Asterisk -- An open source telephony toolkit.
4 * Copyright (C) 1999 - 2005, Digium, Inc.
6 * Mark Spencer <markster@digium.com>
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.
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.
21 * \brief Provide a directory of extensions
23 * \author Mark Spencer <markster@digium.com>
25 * \ingroup applications
29 <depend>app_voicemail</depend>
30 <support_level>core</support_level>
34 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
38 #include "asterisk/paths.h" /* use ast_config_AST_SPOOL_DIR */
39 #include "asterisk/file.h"
40 #include "asterisk/pbx.h"
41 #include "asterisk/module.h"
42 #include "asterisk/say.h"
43 #include "asterisk/app.h"
44 #include "asterisk/utils.h"
47 <application name="Directory" language="en_US">
49 Provide directory of voicemail extensions.
52 <parameter name="vm-context">
53 <para>This is the context within voicemail.conf to use for the Directory. If not
54 specified and <literal>searchcontexts=no</literal> in
55 <filename>voicemail.conf</filename>, then <literal>default</literal>
56 will be assumed.</para>
58 <parameter name="dial-context" required="false">
59 <para>This is the dialplan context to use when looking for an
60 extension that the user has selected, or when jumping to the
61 <literal>o</literal> or <literal>a</literal> extension. If not
62 specified, the current context will be used.</para>
64 <parameter name="options" required="false">
67 <para>In addition to the name, also read the extension number to the
68 caller before presenting dialing options.</para>
71 <para>Allow the caller to enter the first name of a user in the
72 directory instead of using the last name. If specified, the
73 optional number argument will be used for the number of
74 characters the user should enter.</para>
75 <argument name="n" required="true" />
78 <para>Allow the caller to enter the last name of a user in the
79 directory. This is the default. If specified, the
80 optional number argument will be used for the number of
81 characters the user should enter.</para>
82 <argument name="n" required="true" />
85 <para> Allow the caller to enter either the first or the last name
86 of a user in the directory. If specified, the optional number
87 argument will be used for the number of characters the user should enter.</para>
88 <argument name="n" required="true" />
91 <para>Instead of reading each name sequentially and asking for
92 confirmation, create a menu of up to 8 names.</para>
95 <para>Read digits even if the channel is not answered.</para>
98 <para>Pause for n milliseconds after the digits are typed. This is
99 helpful for people with cellphones, who are not holding the
100 receiver to their ear while entering DTMF.</para>
101 <argument name="n" required="true" />
104 <note><para>Only one of the <replaceable>f</replaceable>, <replaceable>l</replaceable>, or <replaceable>b</replaceable>
105 options may be specified. <emphasis>If more than one is specified</emphasis>, then Directory will act as
106 if <replaceable>b</replaceable> was specified. The number
107 of characters for the user to type defaults to <literal>3</literal>.</para></note>
111 <para>This application will present the calling channel with a directory of extensions from which they can search
112 by name. The list of names and corresponding extensions is retrieved from the
113 voicemail configuration file, <filename>voicemail.conf</filename>.</para>
114 <para>This application will immediately exit if one of the following DTMF digits are
115 received and the extension to jump to exists:</para>
116 <para><literal>0</literal> - Jump to the 'o' extension, if it exists.</para>
117 <para><literal>*</literal> - Jump to the 'a' extension, if it exists.</para>
122 static const char app[] = "Directory";
124 /* For simplicity, I'm keeping the format compatible with the voicemail config,
125 but i'm open to suggestions for isolating it */
127 #define VOICEMAIL_CONFIG "voicemail.conf"
130 OPT_LISTBYFIRSTNAME = (1 << 0),
131 OPT_SAYEXTENSION = (1 << 1),
132 OPT_FROMVOICEMAIL = (1 << 2),
133 OPT_SELECTFROMMENU = (1 << 3),
134 OPT_LISTBYLASTNAME = (1 << 4),
135 OPT_LISTBYEITHER = OPT_LISTBYFIRSTNAME | OPT_LISTBYLASTNAME,
136 OPT_PAUSE = (1 << 5),
137 OPT_NOANSWER = (1 << 6),
141 OPT_ARG_FIRSTNAME = 0,
142 OPT_ARG_LASTNAME = 1,
145 /* This *must* be the last value in this enum! */
146 OPT_ARG_ARRAY_SIZE = 4,
149 struct directory_item {
150 char exten[AST_MAX_EXTENSION + 1];
151 char name[AST_MAX_EXTENSION + 1];
152 char context[AST_MAX_CONTEXT + 1];
153 char key[50]; /* Text to order items. Either lastname+firstname or firstname+lastname */
155 AST_LIST_ENTRY(directory_item) entry;
158 AST_APP_OPTIONS(directory_app_options, {
159 AST_APP_OPTION_ARG('f', OPT_LISTBYFIRSTNAME, OPT_ARG_FIRSTNAME),
160 AST_APP_OPTION_ARG('l', OPT_LISTBYLASTNAME, OPT_ARG_LASTNAME),
161 AST_APP_OPTION_ARG('b', OPT_LISTBYEITHER, OPT_ARG_EITHER),
162 AST_APP_OPTION_ARG('p', OPT_PAUSE, OPT_ARG_PAUSE),
163 AST_APP_OPTION('e', OPT_SAYEXTENSION),
164 AST_APP_OPTION('v', OPT_FROMVOICEMAIL),
165 AST_APP_OPTION('m', OPT_SELECTFROMMENU),
166 AST_APP_OPTION('n', OPT_NOANSWER),
169 static int compare(const char *text, const char *template)
173 if (ast_strlen_zero(text)) {
178 digit = toupper(*text++);
242 if (*template++ != digit)
249 static int goto_exten(struct ast_channel *chan, const char *dialcontext, char *ext)
251 if (!ast_goto_if_exists(chan, S_OR(dialcontext, ast_channel_context(chan)), ext, 1) ||
252 (!ast_strlen_zero(ast_channel_macrocontext(chan)) &&
253 !ast_goto_if_exists(chan, ast_channel_macrocontext(chan), ext, 1))) {
256 ast_log(LOG_WARNING, "Can't find extension '%s' in current context. "
257 "Not Exiting the Directory!\n", ext);
262 /* play name of mailbox owner.
263 * returns: -1 for bad or missing extension
264 * '1' for selected entry from directory
265 * '*' for skipped entry from directory
267 static int play_mailbox_owner(struct ast_channel *chan, const char *context,
268 const char *ext, const char *name, struct ast_flags *flags)
271 if ((res = ast_app_sayname(chan, ext, context)) >= 0) {
272 ast_stopstream(chan);
273 /* If Option 'e' was specified, also read the extension number with the name */
274 if (ast_test_flag(flags, OPT_SAYEXTENSION)) {
275 ast_stream_and_wait(chan, "vm-extension", AST_DIGIT_ANY);
276 res = ast_say_character_str(chan, ext, AST_DIGIT_ANY, ast_channel_language(chan));
279 res = ast_say_character_str(chan, S_OR(name, ext), AST_DIGIT_ANY, ast_channel_language(chan));
280 if (!ast_strlen_zero(name) && ast_test_flag(flags, OPT_SAYEXTENSION)) {
281 ast_stream_and_wait(chan, "vm-extension", AST_DIGIT_ANY);
282 res = ast_say_character_str(chan, ext, AST_DIGIT_ANY, ast_channel_language(chan));
289 static int select_entry(struct ast_channel *chan, const char *dialcontext, const struct directory_item *item, struct ast_flags *flags)
291 ast_debug(1, "Selecting '%s' - %s@%s\n", item->name, item->exten, S_OR(dialcontext, item->context));
293 if (ast_test_flag(flags, OPT_FROMVOICEMAIL)) {
294 /* We still want to set the exten though */
295 ast_channel_exten_set(chan, item->exten);
296 } else if (ast_goto_if_exists(chan, S_OR(dialcontext, item->context), item->exten, 1)) {
298 "Can't find extension '%s' in context '%s'. "
299 "Did you pass the wrong context to Directory?\n",
300 item->exten, S_OR(dialcontext, item->context));
307 static int select_item_pause(struct ast_channel *chan, struct ast_flags *flags, char *opts[])
309 int res = 0, opt_pause = 0;
311 if (ast_test_flag(flags, OPT_PAUSE) && !ast_strlen_zero(opts[OPT_ARG_PAUSE])) {
312 opt_pause = atoi(opts[OPT_ARG_PAUSE]);
313 if (opt_pause > 3000) {
316 res = ast_waitfordigit(chan, opt_pause);
321 static int select_item_seq(struct ast_channel *chan, struct directory_item **items, int count, const char *dialcontext, struct ast_flags *flags, char *opts[])
323 struct directory_item *item, **ptr;
326 /* option p(n): cellphone pause option */
327 /* allow early press of selection key */
328 res = select_item_pause(chan, flags, opts);
330 for (ptr = items, i = 0; i < count; i++, ptr++) {
333 for (loop = 3 ; loop > 0; loop--) {
335 res = play_mailbox_owner(chan, item->context, item->exten, item->name, flags);
337 res = ast_stream_and_wait(chan, "dir-instr", AST_DIGIT_ANY);
339 res = ast_waitfordigit(chan, 3000);
340 ast_stopstream(chan);
342 if (res == '0') { /* operator selected */
343 goto_exten(chan, dialcontext, "o");
345 } else if (res == '1') { /* Name selected */
346 return select_entry(chan, dialcontext, item, flags) ? -1 : 1;
347 } else if (res == '*') {
348 /* Skip to next match in list */
350 } else if (res == '#') {
351 /* Exit reading, continue in dialplan */
363 /* Nothing was selected */
367 static int select_item_menu(struct ast_channel *chan, struct directory_item **items, int count, const char *dialcontext, struct ast_flags *flags, char *opts[])
369 struct directory_item **block, *item;
370 int i, limit, res = 0;
373 /* option p(n): cellphone pause option */
374 select_item_pause(chan, flags, opts);
376 for (block = items; count; block += limit, count -= limit) {
381 for (i = 0; i < limit && !res; i++) {
384 snprintf(buf, sizeof(buf), "digits/%d", i + 1);
385 /* Press <num> for <name>, [ extension <ext> ] */
386 res = ast_streamfile(chan, "dir-multi1", ast_channel_language(chan));
388 res = ast_waitstream(chan, AST_DIGIT_ANY);
390 res = ast_streamfile(chan, buf, ast_channel_language(chan));
392 res = ast_waitstream(chan, AST_DIGIT_ANY);
394 res = ast_streamfile(chan, "dir-multi2", ast_channel_language(chan));
396 res = ast_waitstream(chan, AST_DIGIT_ANY);
398 res = play_mailbox_owner(chan, item->context, item->exten, item->name, flags);
400 res = ast_waitstream(chan, AST_DIGIT_ANY);
402 res = ast_waitfordigit(chan, 800);
405 /* Press "9" for more names. */
406 if (!res && count > limit) {
407 res = ast_streamfile(chan, "dir-multi9", ast_channel_language(chan));
409 res = ast_waitstream(chan, AST_DIGIT_ANY);
413 res = ast_waitfordigit(chan, 3000);
416 if (res && res > '0' && res < '1' + limit) {
417 return select_entry(chan, dialcontext, block[res - '1'], flags) ? -1 : 1;
426 /* Nothing was selected */
430 static struct ast_config *realtime_directory(char *context)
432 struct ast_config *cfg;
433 struct ast_config *rtdata;
434 struct ast_category *cat;
435 struct ast_variable *var;
437 const char *fullname;
438 const char *hidefromdir, *searchcontexts = NULL;
440 struct ast_flags config_flags = { 0 };
442 /* Load flat file config. */
443 cfg = ast_config_load(VOICEMAIL_CONFIG, config_flags);
446 /* Loading config failed. */
447 ast_log(LOG_WARNING, "Loading config failed.\n");
449 } else if (cfg == CONFIG_STATUS_FILEINVALID) {
450 ast_log(LOG_ERROR, "Config file %s is in an invalid format. Aborting.\n", VOICEMAIL_CONFIG);
454 /* Get realtime entries, categorized by their mailbox number
455 and present in the requested context */
456 if (ast_strlen_zero(context) && (searchcontexts = ast_variable_retrieve(cfg, "general", "searchcontexts"))) {
457 if (ast_true(searchcontexts)) {
458 rtdata = ast_load_realtime_multientry("voicemail", "mailbox LIKE", "%", SENTINEL);
461 rtdata = ast_load_realtime_multientry("voicemail", "mailbox LIKE", "%", "context", "default", SENTINEL);
465 rtdata = ast_load_realtime_multientry("voicemail", "mailbox LIKE", "%", "context", context, SENTINEL);
468 /* if there are no results, just return the entries from the config file */
474 while ( (mailbox = ast_category_browse(rtdata, mailbox)) ) {
475 const char *ctx = ast_variable_retrieve(rtdata, mailbox, "context");
477 fullname = ast_variable_retrieve(rtdata, mailbox, "fullname");
478 hidefromdir = ast_variable_retrieve(rtdata, mailbox, "hidefromdir");
479 if (ast_true(hidefromdir)) {
483 snprintf(tmp, sizeof(tmp), "no-password,%s", S_OR(fullname, ""));
485 /* Does the context exist within the config file? If not, make one */
486 if (!(cat = ast_category_get(cfg, ctx))) {
487 if (!(cat = ast_category_new(ctx, "", 99999))) {
488 ast_log(LOG_WARNING, "Out of memory\n");
489 ast_config_destroy(cfg);
491 ast_config_destroy(rtdata);
495 ast_category_append(cfg, cat);
498 if ((var = ast_variable_new(mailbox, tmp, ""))) {
499 ast_variable_append(cat, var);
501 ast_log(LOG_WARNING, "Out of memory adding mailbox '%s'\n", mailbox);
504 ast_config_destroy(rtdata);
509 static int check_match(struct directory_item **result, const char *item_context, const char *item_fullname, const char *item_ext, const char *pattern_ext, int use_first_name)
511 struct directory_item *item;
512 const char *key = NULL;
515 if (ast_strlen_zero(item_fullname)) {
519 /* Set key to last name or first name depending on search mode */
521 key = strchr(item_fullname, ' ');
528 if (compare(key, pattern_ext))
531 ast_debug(1, "Found match %s@%s\n", item_ext, item_context);
534 item = ast_calloc(1, sizeof(*item));
537 ast_copy_string(item->context, item_context, sizeof(item->context));
538 ast_copy_string(item->name, item_fullname, sizeof(item->name));
539 ast_copy_string(item->exten, item_ext, sizeof(item->exten));
541 ast_copy_string(item->key, key, sizeof(item->key));
542 if (key != item_fullname) {
543 /* Key is the last name. Append first name to key in order to sort Last,First */
544 namelen = key - item_fullname - 1;
545 if (namelen > sizeof(item->key) - strlen(item->key) - 1)
546 namelen = sizeof(item->key) - strlen(item->key) - 1;
547 strncat(item->key, item_fullname, namelen);
554 typedef AST_LIST_HEAD_NOLOCK(, directory_item) itemlist;
556 static int search_directory_sub(const char *context, struct ast_config *vmcfg, struct ast_config *ucfg, const char *ext, struct ast_flags flags, itemlist *alist)
558 struct ast_variable *v;
559 char buf[AST_MAX_EXTENSION + 1], *pos, *bufptr, *cat;
560 struct directory_item *item;
563 ast_debug(2, "Pattern: %s\n", ext);
565 for (v = ast_variable_browse(vmcfg, context); v; v = v->next) {
568 if (strcasestr(v->value, "hidefromdir=yes"))
571 ast_copy_string(buf, v->value, sizeof(buf));
574 /* password,Full Name,email,pager,options */
575 strsep(&bufptr, ",");
576 pos = strsep(&bufptr, ",");
578 /* No name to compare against */
579 if (ast_strlen_zero(pos)) {
584 if (ast_test_flag(&flags, OPT_LISTBYLASTNAME)) {
585 res = check_match(&item, context, pos, v->name, ext, 0 /* use_first_name */);
587 if (!res && ast_test_flag(&flags, OPT_LISTBYFIRSTNAME)) {
588 res = check_match(&item, context, pos, v->name, ext, 1 /* use_first_name */);
596 AST_LIST_INSERT_TAIL(alist, item, entry);
600 for (cat = ast_category_browse(ucfg, NULL); cat ; cat = ast_category_browse(ucfg, cat)) {
601 const char *position;
602 if (!strcasecmp(cat, "general"))
604 if (!ast_true(ast_config_option(ucfg, cat, "hasdirectory")))
607 /* Find all candidate extensions */
608 position = ast_variable_retrieve(ucfg, cat, "fullname");
613 if (ast_test_flag(&flags, OPT_LISTBYLASTNAME)) {
614 res = check_match(&item, context, position, cat, ext, 0 /* use_first_name */);
616 if (!res && ast_test_flag(&flags, OPT_LISTBYFIRSTNAME)) {
617 res = check_match(&item, context, position, cat, ext, 1 /* use_first_name */);
625 AST_LIST_INSERT_TAIL(alist, item, entry);
631 static int search_directory(const char *context, struct ast_config *vmcfg, struct ast_config *ucfg, const char *ext, struct ast_flags flags, itemlist *alist)
633 const char *searchcontexts = ast_variable_retrieve(vmcfg, "general", "searchcontexts");
634 if (ast_strlen_zero(context)) {
635 if (!ast_strlen_zero(searchcontexts) && ast_true(searchcontexts)) {
636 /* Browse each context for a match */
639 for (catg = ast_category_browse(vmcfg, NULL); catg; catg = ast_category_browse(vmcfg, catg)) {
640 if (!strcmp(catg, "general") || !strcmp(catg, "zonemessages")) {
644 if ((res = search_directory_sub(catg, vmcfg, ucfg, ext, flags, alist))) {
650 ast_debug(1, "Searching by category default\n");
651 return search_directory_sub("default", vmcfg, ucfg, ext, flags, alist);
654 /* Browse only the listed context for a match */
655 ast_debug(1, "Searching by category %s\n", context);
656 return search_directory_sub(context, vmcfg, ucfg, ext, flags, alist);
660 static void sort_items(struct directory_item **sorted, int count)
663 struct directory_item **ptr, *tmp;
668 /* Bubble-sort items by the key */
671 for (ptr = sorted, i = 0; i < count - 1; i++, ptr++) {
672 if (strcasecmp(ptr[0]->key, ptr[1]->key) > 0) {
682 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, char *opts[])
684 /* Read in the first three digits.. "digit" is the first digit, already read */
686 itemlist alist = AST_LIST_HEAD_NOLOCK_INIT_VALUE;
687 struct directory_item *item, **ptr, **sorted = NULL;
691 if (digit == '0' && !goto_exten(chan, dialcontext, "o")) {
695 if (digit == '*' && !goto_exten(chan, dialcontext, "a")) {
700 if (ast_readstring(chan, ext + 1, digits - 1, 3000, 3000, "#") < 0)
703 res = search_directory(context, vmcfg, ucfg, ext, *flags, &alist);
707 /* Count items in the list */
709 AST_LIST_TRAVERSE(&alist, item, entry) {
714 res = ast_streamfile(chan, "dir-nomatch", ast_channel_language(chan));
719 /* Create plain array of pointers to items (for sorting) */
720 sorted = ast_calloc(count, sizeof(*sorted));
723 AST_LIST_TRAVERSE(&alist, item, entry) {
728 sort_items(sorted, count);
731 ast_debug(2, "Listing matching entries:\n");
732 for (ptr = sorted, i = 0; i < count; i++, ptr++) {
733 ast_debug(2, "%s: %s\n", ptr[0]->exten, ptr[0]->name);
737 if (ast_test_flag(flags, OPT_SELECTFROMMENU)) {
738 /* Offer multiple entries at the same time */
739 res = select_item_menu(chan, sorted, count, dialcontext, flags, opts);
741 /* Offer entries one by one */
742 res = select_item_seq(chan, sorted, count, dialcontext, flags, opts);
746 res = ast_streamfile(chan, "dir-nomore", ast_channel_language(chan));
753 while ((item = AST_LIST_REMOVE_HEAD(&alist, entry)))
759 static int directory_exec(struct ast_channel *chan, const char *data)
761 int res = 0, digit = 3;
762 struct ast_config *cfg, *ucfg;
763 const char *dirintro;
764 char *parse, *opts[OPT_ARG_ARRAY_SIZE] = { 0, };
765 struct ast_flags flags = { 0 };
766 struct ast_flags config_flags = { 0 };
767 enum { FIRST, LAST, BOTH } which = LAST;
768 char digits[9] = "digits/3";
769 AST_DECLARE_APP_ARGS(args,
770 AST_APP_ARG(vmcontext);
771 AST_APP_ARG(dialcontext);
772 AST_APP_ARG(options);
775 parse = ast_strdupa(data);
777 AST_STANDARD_APP_ARGS(args, parse);
779 if (args.options && ast_app_parse_options(directory_app_options, &flags, opts, args.options))
782 if (!(cfg = realtime_directory(args.vmcontext))) {
783 ast_log(LOG_ERROR, "Unable to read the configuration data!\n");
787 if ((ucfg = ast_config_load("users.conf", config_flags)) == CONFIG_STATUS_FILEINVALID) {
788 ast_log(LOG_ERROR, "Config file users.conf is in an invalid format. Aborting.\n");
792 dirintro = ast_variable_retrieve(cfg, args.vmcontext, "directoryintro");
793 if (ast_strlen_zero(dirintro))
794 dirintro = ast_variable_retrieve(cfg, "general", "directoryintro");
795 /* the above prompts probably should be modified to include 0 for dialing operator
796 and # for exiting (continues in dialplan) */
798 if (ast_test_flag(&flags, OPT_LISTBYFIRSTNAME) && ast_test_flag(&flags, OPT_LISTBYLASTNAME)) {
799 if (!ast_strlen_zero(opts[OPT_ARG_EITHER])) {
800 digit = atoi(opts[OPT_ARG_EITHER]);
803 } else if (ast_test_flag(&flags, OPT_LISTBYFIRSTNAME)) {
804 if (!ast_strlen_zero(opts[OPT_ARG_FIRSTNAME])) {
805 digit = atoi(opts[OPT_ARG_FIRSTNAME]);
809 if (!ast_strlen_zero(opts[OPT_ARG_LASTNAME])) {
810 digit = atoi(opts[OPT_ARG_LASTNAME]);
815 /* If no options specified, search by last name */
816 if (!ast_test_flag(&flags, OPT_LISTBYFIRSTNAME) && !ast_test_flag(&flags, OPT_LISTBYLASTNAME)) {
817 ast_set_flag(&flags, OPT_LISTBYLASTNAME);
823 } else if (digit < 1) {
826 digits[7] = digit + '0';
828 if (ast_channel_state(chan) != AST_STATE_UP) {
829 if (!ast_test_flag(&flags, OPT_NOANSWER)) {
830 /* Otherwise answer unless we're supposed to read while on-hook */
831 res = ast_answer(chan);
835 if (!ast_strlen_zero(dirintro) && !res) {
836 res = ast_stream_and_wait(chan, dirintro, AST_DIGIT_ANY);
838 /* Stop playing sounds as soon as we have a digit. */
839 res = ast_stream_and_wait(chan, "dir-welcome", AST_DIGIT_ANY);
841 res = ast_stream_and_wait(chan, "dir-pls-enter", AST_DIGIT_ANY);
844 res = ast_stream_and_wait(chan, digits, AST_DIGIT_ANY);
847 res = ast_stream_and_wait(chan,
848 which == FIRST ? "dir-first" :
849 which == LAST ? "dir-last" :
850 "dir-firstlast", AST_DIGIT_ANY);
853 res = ast_stream_and_wait(chan, "dir-usingkeypad", AST_DIGIT_ANY);
856 ast_stopstream(chan);
858 res = ast_waitfordigit(chan, 5000);
863 res = do_directory(chan, cfg, ucfg, args.vmcontext, args.dialcontext, res, digit, &flags, opts);
867 res = ast_waitstream(chan, AST_DIGIT_ANY);
868 ast_stopstream(chan);
875 ast_config_destroy(ucfg);
876 ast_config_destroy(cfg);
878 return res < 0 ? -1 : 0;
881 static int unload_module(void)
884 res = ast_unregister_application(app);
888 static int load_module(void)
890 return ast_register_application_xml(app, directory_exec);
893 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Extension Directory");