app_record: Resolve some absolute vs. relative filename bugs
[asterisk/asterisk.git] / apps / app_record.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 1999 - 2005, Digium, Inc.
5  *
6  * Matthew Fredrickson <creslin@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 Trivial application to record a sound file
22  *
23  * \author Matthew Fredrickson <creslin@digium.com>
24  *
25  * \ingroup applications
26  */
27
28 /*** MODULEINFO
29         <support_level>core</support_level>
30  ***/
31  
32 #include "asterisk.h"
33
34 #include "asterisk/file.h"
35 #include "asterisk/pbx.h"
36 #include "asterisk/module.h"
37 #include "asterisk/app.h"
38 #include "asterisk/channel.h"
39 #include "asterisk/dsp.h"       /* use dsp routines for silence detection */
40 #include "asterisk/format_cache.h"
41 #include "asterisk/paths.h"
42
43 /*** DOCUMENTATION
44         <application name="Record" language="en_US">
45                 <synopsis>
46                         Record to a file.
47                 </synopsis>
48                 <syntax>
49                         <parameter name="filename" required="true" argsep=".">
50                                 <argument name="filename" required="true" />
51                                 <argument name="format" required="true">
52                                         <para>Is the format of the file type to be recorded (wav, gsm, etc).</para>
53                                 </argument>
54                         </parameter>
55                         <parameter name="silence">
56                                 <para>Is the number of seconds of silence to allow before returning.</para>
57                         </parameter>
58                         <parameter name="maxduration">
59                                 <para>Is the maximum recording duration in seconds. If missing
60                                 or 0 there is no maximum.</para>
61                         </parameter>
62                         <parameter name="options">
63                                 <optionlist>
64                                         <option name="a">
65                                                 <para>Append to existing recording rather than replacing.</para>
66                                         </option>
67                                         <option name="n">
68                                                 <para>Do not answer, but record anyway if line not yet answered.</para>
69                                         </option>
70                                         <option name="o">
71                                                 <para>Exit when 0 is pressed, setting the variable <variable>RECORD_STATUS</variable>
72                                                 to <literal>OPERATOR</literal> instead of <literal>DTMF</literal></para>
73                                         </option>
74                                         <option name="q">
75                                                 <para>quiet (do not play a beep tone).</para>
76                                         </option>
77                                         <option name="s">
78                                                 <para>skip recording if the line is not yet answered.</para>
79                                         </option>
80                                         <option name="t">
81                                                 <para>use alternate '*' terminator key (DTMF) instead of default '#'</para>
82                                         </option>
83                                         <option name="u">
84                                                 <para>Don't truncate recorded silence.</para>
85                                         </option>
86                                         <option name="x">
87                                                 <para>Ignore all terminator keys (DTMF) and keep recording until hangup.</para>
88                                         </option>
89                                         <option name="k">
90                                                 <para>Keep recorded file upon hangup.</para>
91                                         </option>
92                                         <option name="y">
93                                                 <para>Terminate recording if *any* DTMF digit is received.</para>
94                                         </option>
95                                 </optionlist>
96                         </parameter>
97                 </syntax>
98                 <description>
99                         <para>If filename contains <literal>%d</literal>, these characters will be replaced with a number
100                         incremented by one each time the file is recorded.
101                         Use <astcli>core show file formats</astcli> to see the available formats on your system
102                         User can press <literal>#</literal> to terminate the recording and continue to the next priority.
103                         If the user hangs up during a recording, all data will be lost and the application will terminate.</para>
104                         <variablelist>
105                                 <variable name="RECORDED_FILE">
106                                         <para>Will be set to the final filename of the recording, without an extension.</para>
107                                 </variable>
108                                 <variable name="RECORD_STATUS">
109                                         <para>This is the final status of the command</para>
110                                         <value name="DTMF">A terminating DTMF was received ('#' or '*', depending upon option 't')</value>
111                                         <value name="SILENCE">The maximum silence occurred in the recording.</value>
112                                         <value name="SKIP">The line was not yet answered and the 's' option was specified.</value>
113                                         <value name="TIMEOUT">The maximum length was reached.</value>
114                                         <value name="HANGUP">The channel was hung up.</value>
115                                         <value name="ERROR">An unrecoverable error occurred, which resulted in a WARNING to the logs.</value>
116                                 </variable>
117                         </variablelist>
118                 </description>
119         </application>
120
121  ***/
122
123 #define OPERATOR_KEY '0'
124
125 static char *app = "Record";
126
127 enum {
128         OPTION_APPEND = (1 << 0),
129         OPTION_NOANSWER = (1 << 1),
130         OPTION_QUIET = (1 << 2),
131         OPTION_SKIP = (1 << 3),
132         OPTION_STAR_TERMINATE = (1 << 4),
133         OPTION_IGNORE_TERMINATE = (1 << 5),
134         OPTION_KEEP = (1 << 6),
135         OPTION_ANY_TERMINATE = (1 << 7),
136         OPTION_OPERATOR_EXIT = (1 << 8),
137         OPTION_NO_TRUNCATE = (1 << 9),
138 };
139
140 AST_APP_OPTIONS(app_opts,{
141         AST_APP_OPTION('a', OPTION_APPEND),
142         AST_APP_OPTION('k', OPTION_KEEP),
143         AST_APP_OPTION('n', OPTION_NOANSWER),
144         AST_APP_OPTION('o', OPTION_OPERATOR_EXIT),
145         AST_APP_OPTION('q', OPTION_QUIET),
146         AST_APP_OPTION('s', OPTION_SKIP),
147         AST_APP_OPTION('t', OPTION_STAR_TERMINATE),
148         AST_APP_OPTION('u', OPTION_NO_TRUNCATE),
149         AST_APP_OPTION('y', OPTION_ANY_TERMINATE),
150         AST_APP_OPTION('x', OPTION_IGNORE_TERMINATE),
151 });
152
153 /*!
154  * \internal
155  * \brief Used to determine what action to take when DTMF is received while recording
156  * \since 13.0.0
157  *
158  * \param chan channel being recorded
159  * \param flags option flags in use by the record application
160  * \param dtmf_integer the integer value of the DTMF key received
161  * \param terminator key currently set to be pressed for normal termination
162  *
163  * \retval 0 do not exit
164  * \retval -1 do exit
165  */
166 static int record_dtmf_response(struct ast_channel *chan, struct ast_flags *flags, int dtmf_integer, int terminator)
167 {
168         if ((dtmf_integer == OPERATOR_KEY) &&
169                 (ast_test_flag(flags, OPTION_OPERATOR_EXIT))) {
170                 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "OPERATOR");
171                 return -1;
172         }
173
174         if ((dtmf_integer == terminator) ||
175                 (ast_test_flag(flags, OPTION_ANY_TERMINATE))) {
176                 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "DTMF");
177                 return -1;
178         }
179
180         return 0;
181 }
182
183 static int create_destination_directory(const char *path)
184 {
185         int res;
186         char directory[PATH_MAX], *file_sep;
187
188         if (!(file_sep = strrchr(path, '/'))) {
189                 /* No directory to create */
190                 return 0;
191         }
192
193         /* Overwrite temporarily */
194         *file_sep = '\0';
195
196         /* Absolute path? */
197         if (path[0] == '/') {
198                 res = ast_mkdir(path, 0777);
199                 *file_sep = '/';
200                 return res;
201         }
202
203         /* Relative path */
204         res = snprintf(directory, sizeof(directory), "%s/sounds/%s",
205                                    ast_config_AST_DATA_DIR, path);
206
207         *file_sep = '/';
208
209         if (res >= sizeof(directory)) {
210                 /* We truncated, so we fail */
211                 return -1;
212         }
213
214         return ast_mkdir(directory, 0777);
215 }
216
217 static int record_exec(struct ast_channel *chan, const char *data)
218 {
219         int res = 0;
220         char *ext = NULL, *opts[0];
221         char *parse;
222         int i = 0;
223         char tmp[PATH_MAX];
224
225         struct ast_filestream *s = NULL;
226         struct ast_frame *f = NULL;
227         
228         struct ast_dsp *sildet = NULL;          /* silence detector dsp */
229         int totalsilence = 0;
230         int dspsilence = 0;
231         int silence = 0;                /* amount of silence to allow */
232         int gotsilence = 0;             /* did we timeout for silence? */
233         int truncate_silence = 1;       /* truncate on complete silence recording */
234         int maxduration = 0;            /* max duration of recording in milliseconds */
235         int gottimeout = 0;             /* did we timeout for maxduration exceeded? */
236         int terminator = '#';
237         RAII_VAR(struct ast_format *, rfmt, NULL, ao2_cleanup);
238         int ioflags;
239         struct ast_silence_generator *silgen = NULL;
240         struct ast_flags flags = { 0, };
241         AST_DECLARE_APP_ARGS(args,
242                 AST_APP_ARG(filename);
243                 AST_APP_ARG(silence);
244                 AST_APP_ARG(maxduration);
245                 AST_APP_ARG(options);
246         );
247         int ms;
248         struct timeval start;
249
250         /* The next few lines of code parse out the filename and header from the input string */
251         if (ast_strlen_zero(data)) { /* no data implies no filename or anything is present */
252                 ast_log(LOG_WARNING, "Record requires an argument (filename)\n");
253                 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
254                 return -1;
255         }
256
257         parse = ast_strdupa(data);
258         AST_STANDARD_APP_ARGS(args, parse);
259         if (args.argc == 4)
260                 ast_app_parse_options(app_opts, &flags, opts, args.options);
261
262         if (!ast_strlen_zero(args.filename)) {
263                 ext = strrchr(args.filename, '.'); /* to support filename with a . in the filename, not format */
264                 if (!ext)
265                         ext = strchr(args.filename, ':');
266                 if (ext) {
267                         *ext = '\0';
268                         ext++;
269                 }
270         }
271         if (!ext) {
272                 ast_log(LOG_WARNING, "No extension specified to filename!\n");
273                 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
274                 return -1;
275         }
276         if (args.silence) {
277                 if ((sscanf(args.silence, "%30d", &i) == 1) && (i > -1)) {
278                         silence = i * 1000;
279                 } else if (!ast_strlen_zero(args.silence)) {
280                         ast_log(LOG_WARNING, "'%s' is not a valid silence duration\n", args.silence);
281                 }
282         }
283
284         if (ast_test_flag(&flags, OPTION_NO_TRUNCATE))
285                 truncate_silence = 0;
286
287         if (args.maxduration) {
288                 if ((sscanf(args.maxduration, "%30d", &i) == 1) && (i > -1))
289                         /* Convert duration to milliseconds */
290                         maxduration = i * 1000;
291                 else if (!ast_strlen_zero(args.maxduration))
292                         ast_log(LOG_WARNING, "'%s' is not a valid maximum duration\n", args.maxduration);
293         }
294
295         if (ast_test_flag(&flags, OPTION_STAR_TERMINATE))
296                 terminator = '*';
297         if (ast_test_flag(&flags, OPTION_IGNORE_TERMINATE))
298                 terminator = '\0';
299
300         /*
301           If a '%d' is specified as part of the filename, we replace that token with
302           sequentially incrementing numbers until we find a unique filename.
303         */
304         if (strchr(args.filename, '%')) {
305                 size_t src, dst, count = 0;
306                 size_t src_len = strlen(args.filename);
307                 size_t dst_len = sizeof(tmp) - 1;
308
309                 do {
310                         for (src = 0, dst = 0; src < src_len && dst < dst_len; src++) {
311                                 if (!strncmp(&args.filename[src], "%d", 2)) {
312                                         int s = snprintf(&tmp[dst], PATH_MAX - dst, "%zu", count);
313                                         if (s >= PATH_MAX - dst) {
314                                                 /* We truncated, so we need to bail */
315                                                 ast_log(LOG_WARNING, "Failed to create unique filename from template: %s\n", args.filename);
316                                                 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
317                                                 return -1;
318                                         }
319                                         dst += s;
320                                         src++;
321                                 } else {
322                                         tmp[dst] = args.filename[src];
323                                         tmp[++dst] = '\0';
324                                 }
325                         }
326                         count++;
327                 } while (ast_fileexists(tmp, ext, ast_channel_language(chan)) > 0);
328         } else
329                 ast_copy_string(tmp, args.filename, sizeof(tmp));
330
331         pbx_builtin_setvar_helper(chan, "RECORDED_FILE", tmp);
332
333         if (ast_channel_state(chan) != AST_STATE_UP) {
334                 if (ast_test_flag(&flags, OPTION_SKIP)) {
335                         /* At the user's option, skip if the line is not up */
336                         pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "SKIP");
337                         return 0;
338                 } else if (!ast_test_flag(&flags, OPTION_NOANSWER)) {
339                         /* Otherwise answer unless we're supposed to record while on-hook */
340                         res = ast_answer(chan);
341                 }
342         }
343
344         if (res) {
345                 ast_log(LOG_WARNING, "Could not answer channel '%s'\n", ast_channel_name(chan));
346                 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
347                 goto out;
348         }
349
350         if (!ast_test_flag(&flags, OPTION_QUIET)) {
351                 /* Some code to play a nice little beep to signify the start of the record operation */
352                 res = ast_streamfile(chan, "beep", ast_channel_language(chan));
353                 if (!res) {
354                         res = ast_waitstream(chan, "");
355                 } else {
356                         ast_log(LOG_WARNING, "ast_streamfile failed on %s\n", ast_channel_name(chan));
357                 }
358                 ast_stopstream(chan);
359         }
360
361         /* The end of beep code.  Now the recording starts */
362
363         if (silence > 0) {
364                 rfmt = ao2_bump(ast_channel_readformat(chan));
365                 res = ast_set_read_format(chan, ast_format_slin);
366                 if (res < 0) {
367                         ast_log(LOG_WARNING, "Unable to set to linear mode, giving up\n");
368                         pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
369                         return -1;
370                 }
371                 sildet = ast_dsp_new();
372                 if (!sildet) {
373                         ast_log(LOG_WARNING, "Unable to create silence detector :(\n");
374                         pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
375                         return -1;
376                 }
377                 ast_dsp_set_threshold(sildet, ast_dsp_get_threshold_from_settings(THRESHOLD_SILENCE));
378         } 
379
380         if (create_destination_directory(tmp)) {
381                 ast_log(LOG_WARNING, "Could not create directory for file %s\n", args.filename);
382                 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
383                 goto out;
384         }
385
386         ioflags = ast_test_flag(&flags, OPTION_APPEND) ? O_CREAT|O_APPEND|O_WRONLY : O_CREAT|O_TRUNC|O_WRONLY;
387         s = ast_writefile(tmp, ext, NULL, ioflags, 0, AST_FILE_MODE);
388
389         if (!s) {
390                 ast_log(LOG_WARNING, "Could not create file %s\n", args.filename);
391                 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
392                 goto out;
393         }
394
395         if (ast_opt_transmit_silence)
396                 silgen = ast_channel_start_silence_generator(chan);
397
398         /* Request a video update */
399         ast_indicate(chan, AST_CONTROL_VIDUPDATE);
400
401         if (maxduration <= 0)
402                 maxduration = -1;
403
404         start = ast_tvnow();
405         while ((ms = ast_remaining_ms(start, maxduration))) {
406                 ms = ast_waitfor(chan, ms);
407                 if (ms < 0) {
408                         break;
409                 }
410
411                 if (maxduration > 0 && ms == 0) {
412                         break;
413                 }
414
415                 f = ast_read(chan);
416                 if (!f) {
417                         res = -1;
418                         break;
419                 }
420                 if (f->frametype == AST_FRAME_VOICE) {
421                         res = ast_writestream(s, f);
422
423                         if (res) {
424                                 ast_log(LOG_WARNING, "Problem writing frame\n");
425                                 ast_frfree(f);
426                                 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
427                                 break;
428                         }
429
430                         if (silence > 0) {
431                                 dspsilence = 0;
432                                 ast_dsp_silence(sildet, f, &dspsilence);
433                                 if (dspsilence) {
434                                         totalsilence = dspsilence;
435                                 } else {
436                                         totalsilence = 0;
437                                 }
438                                 if (totalsilence > silence) {
439                                         /* Ended happily with silence */
440                                         ast_frfree(f);
441                                         gotsilence = 1;
442                                         pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "SILENCE");
443                                         break;
444                                 }
445                         }
446                 } else if (f->frametype == AST_FRAME_VIDEO) {
447                         res = ast_writestream(s, f);
448
449                         if (res) {
450                                 ast_log(LOG_WARNING, "Problem writing frame\n");
451                                 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
452                                 ast_frfree(f);
453                                 break;
454                         }
455                 } else if (f->frametype == AST_FRAME_DTMF) {
456                         if (record_dtmf_response(chan, &flags, f->subclass.integer, terminator)) {
457                                 ast_frfree(f);
458                                 break;
459                         }
460                 }
461                 ast_frfree(f);
462         }
463
464         if (maxduration > 0 && !ms) {
465                 gottimeout = 1;
466                 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "TIMEOUT");
467         }
468
469         if (!f) {
470                 ast_debug(1, "Got hangup\n");
471                 res = -1;
472                 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "HANGUP");
473                 if (!ast_test_flag(&flags, OPTION_KEEP)) {
474                         ast_filedelete(args.filename, NULL);
475                 }
476         }
477
478         if (gotsilence && truncate_silence) {
479                 ast_stream_rewind(s, silence - 1000);
480                 ast_truncstream(s);
481         } else if (!gottimeout && f) {
482                 /*
483                  * Strip off the last 1/4 second of it, if we didn't end because of a timeout,
484                  * or a hangup.  This must mean we ended because of a DTMF tone and while this
485                  * 1/4 second stripping is very old code the most likely explanation is that it
486                  * relates to stripping a partial DTMF tone.
487                  */
488                 ast_stream_rewind(s, 250);
489                 ast_truncstream(s);
490         }
491         ast_closestream(s);
492
493         if (silgen)
494                 ast_channel_stop_silence_generator(chan, silgen);
495
496 out:
497         if ((silence > 0) && rfmt) {
498                 res = ast_set_read_format(chan, rfmt);
499                 if (res) {
500                         ast_log(LOG_WARNING, "Unable to restore read format on '%s'\n", ast_channel_name(chan));
501                 }
502         }
503
504         if (sildet) {
505                 ast_dsp_free(sildet);
506         }
507         return res;
508 }
509
510 static int unload_module(void)
511 {
512         return ast_unregister_application(app);
513 }
514
515 static int load_module(void)
516 {
517         return ast_register_application_xml(app, record_exec);
518 }
519
520 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Trivial Record Application");