diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d817908 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.buildpath +.project +.settings* diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e56b29 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +Overview +======= + +This is a sample application showing how to deploy a Tropo WebAPI aplication to Orchestra.io + +Tropo - http://www.tropo.com + +Orchestra.io - http://www.orchestra.io/ + diff --git a/classes/limonade.php b/classes/limonade.php new file mode 100755 index 0000000..bc67487 --- /dev/null +++ b/classes/limonade.php @@ -0,0 +1,2585 @@ + $v) + if(array_key_exists($k, $GLOBALS)) unset($GLOBALS[$k]); +} + +if(ini_get('register_globals')) +{ + unregister_globals( '_POST', '_GET', '_COOKIE', '_REQUEST', '_SERVER', + '_ENV', '_FILES'); + ini_set('register_globals', 0); +} + +# B. removing magic quotes + +/** + * @access private + * @param string $array + * @return array + */ +function remove_magic_quotes($array) +{ + foreach ($array as $k => $v) + $array[$k] = is_array($v) ? remove_magic_quotes($v) : stripslashes($v); + return $array; +} + +if (get_magic_quotes_gpc()) +{ + $_GET = remove_magic_quotes($_GET); + $_POST = remove_magic_quotes($_POST); + $_COOKIE = remove_magic_quotes($_COOKIE); + ini_set('magic_quotes_gpc', 0); +} + +if(function_exists('set_magic_quotes_runtime') && get_magic_quotes_runtime()) set_magic_quotes_runtime(false); + +# C. Disable error display +# by default, no error reporting; it will be switched on later in run(). +# ini_set('display_errors', 1); must be called explicitly in app file +# if you want to show errors before running app +ini_set('display_errors', 0); + +## SETTING INTERNAL ROUTES _____________________________________________________ + +dispatch(array("/_lim_css/*.css", array('_lim_css_filename')), 'render_limonade_css'); + /** + * Internal controller that responds to route /_lim_css/*.css + * + * @access private + * @return string + */ + function render_limonade_css() + { + option('views_dir', file_path(option('limonade_public_dir'), 'css')); + $fpath = file_path(params('_lim_css_filename').".css"); + return css($fpath, null); // with no layout + } + +dispatch(array("/_lim_public/**", array('_lim_public_file')), 'render_limonade_file'); + /** + * Internal controller that responds to route /_lim_public/** + * + * @access private + * @return void + */ + function render_limonade_file() + { + $fpath = file_path(option('limonade_public_dir'), params('_lim_public_file')); + return render_file($fpath, true); + } + + + + + # # # + + + + +# ============================================================================ # +# 1. BASE # +# ============================================================================ # + +## ABSTRACTS ___________________________________________________________________ + +# Abstract methods that might be redefined by user: +# +# - function configure(){} +# - function initialize(){} +# - function autoload_controller($callback){} +# - function before($route){} +# - function after($output, $route){} +# - function not_found($errno, $errstr, $errfile=null, $errline=null){} +# - function server_error($errno, $errstr, $errfile=null, $errline=null){} +# - function route_missing($request_method, $request_uri){} +# - function before_exit(){} +# - function before_render($content_or_func, $layout, $locals, $view_path){} +# - function autorender($route){} +# +# See abstract.php for more details. + + +## MAIN PUBLIC FUNCTIONS _______________________________________________________ + +/** + * Set and returns options values + * + * If multiple values are provided, set $name option with an array of those values. + * If there is only one value, set $name option with the provided $values + * + * @param string $name + * @param mixed $values,... + * @return mixed option value for $name if $name argument is provided, else return all options + */ +function option($name = null, $values = null) +{ + static $options = array(); + $args = func_get_args(); + $name = array_shift($args); + if(is_null($name)) return $options; + if(!empty($args)) + { + $options[$name] = count($args) > 1 ? $args : $args[0]; + } + if(array_key_exists($name, $options)) return $options[$name]; + return; +} + +/** + * Set and returns params + * + * Depending on provided arguments: + * + * * Reset params if first argument is null + * + * * If first argument is an array, merge it with current params + * + * * If there is a second argument $value, set param $name (first argument) with $value + * + * params('name', 'Doe') // set 'name' => 'Doe' + * + * * If there is more than 2 arguments, set param $name (first argument) value with + * an array of next arguments + * + * params('months', 'jan', 'feb', 'mar') // set 'month' => array('months', 'jan', 'feb', 'mar') + * + * + * @param mixed $name_or_array_or_null could be null || array of params || name of a param (optional) + * @param mixed $value,... for the $name param (optional) + * @return mixed all params, or one if a first argument $name is provided + */ +function params($name_or_array_or_null = null, $value = null) +{ + static $params = array(); + $args = func_get_args(); + + if(func_num_args() > 0) + { + $name = array_shift($args); + if(is_null($name)) + { + # Reset params + $params = array(); + return $params; + } + if(is_array($name)) + { + $params = array_merge($params, $name); + return $params; + } + $nargs = count($args); + if($nargs > 0) + { + $value = $nargs > 1 ? $args : $args[0]; + $params[$name] = $value; + } + return array_key_exists($name,$params) ? $params[$name] : null; + } + + return $params; +} + +/** + * Set and returns template variables + * + * If multiple values are provided, set $name variable with an array of those values. + * If there is only one value, set $name variable with the provided $values + * + * @param string $name + * @param mixed $values,... + * @return mixed variable value for $name if $name argument is provided, else return all variables + */ +function set($name = null, $values = null) +{ + static $vars = array(); + $args = func_get_args(); + $name = array_shift($args); + if(is_null($name)) return $vars; + if(!empty($args)) + { + $vars[$name] = count($args) > 1 ? $args : $args[0]; + } + if(array_key_exists($name, $vars)) return $vars[$name]; + return $vars; +} + +/** + * Sets a template variable with a value or a default value if value is empty + * + * @param string $name + * @param string $value + * @param string $default + * @return mixed setted value + */ +function set_or_default($name, $value, $default) +{ + return set($name, value_or_default($value, $default)); +} + +/** + * Running application + * + * @param string $env + * @return void + */ +function run($env = null) +{ + if(is_null($env)) $env = env(); + + # 0. Set default configuration + $root_dir = dirname(app_file()); + $base_path = dirname(file_path($env['SERVER']['SCRIPT_NAME'])); + $base_file = basename($env['SERVER']['SCRIPT_NAME']); + $base_uri = file_path($base_path, (($base_file == 'index.php') ? '?' : $base_file.'?')); + $lim_dir = dirname(__FILE__); + option('root_dir', $root_dir); + option('base_path', $base_path); + option('base_uri', $base_uri); // set it manually if you use url_rewriting + option('limonade_dir', file_path($lim_dir)); + option('limonade_views_dir', file_path($lim_dir, 'limonade', 'views')); + option('limonade_public_dir',file_path($lim_dir, 'limonade', 'public')); + option('public_dir', file_path($root_dir, 'public')); + option('views_dir', file_path($root_dir, 'views')); + option('controllers_dir', file_path($root_dir, 'controllers')); + option('lib_dir', file_path($root_dir, 'lib')); + option('error_views_dir', option('limonade_views_dir')); + option('env', ENV_PRODUCTION); + option('debug', true); + option('session', LIM_SESSION_NAME); // true, false or the name of your session + option('encoding', 'utf-8'); + option('signature', LIM_NAME); // X-Limonade header value or false to hide it + option('gzip', false); + option('x-sendfile', 0); // 0: disabled, + // X-SENDFILE: for Apache and Lighttpd v. >= 1.5, + // X-LIGHTTPD-SEND-FILE: for Apache and Lighttpd v. < 1.5 + + # 1. Set handlers + # 1.1 Set error handling + ini_set('display_errors', 1); + set_error_handler('error_handler_dispatcher', E_ALL ^ E_NOTICE); + + # 1.2 Register shutdown function + register_shutdown_function('stop_and_exit'); + + # 2. Set user configuration + call_if_exists('configure'); + + # 2.1 Set gzip compression if defined + if(is_bool(option('gzip')) && option('gzip')) + { + ini_set('zlib.output_compression', '1'); + } + + # 2.2 Set X-Limonade header + if($signature = option('signature')) header("X-Limonade: $signature"); + + # 3. Loading libs + require_once_dir(option('lib_dir')); + + # 4. Starting session + if(!defined('SID') && option('session')) + { + if(!is_bool(option('session'))) session_name(option('session')); + if(!session_start()) trigger_error("An error occured while trying to start the session", E_USER_WARNING); + } + + # 5. Set some default methods if needed + if(!function_exists('after')) + { + function after($output) + { + return $output; + } + } + if(!function_exists('route_missing')) + { + function route_missing($request_method, $request_uri) + { + halt(NOT_FOUND, "($request_method) $request_uri"); + } + } + + call_if_exists('initialize'); + + # 6. Check request + if($rm = request_method($env)) + { + if(request_is_head($env)) ob_start(); // then no output + + if(!request_method_is_allowed($rm)) + halt(HTTP_NOT_IMPLEMENTED, "The requested method '$rm' is not implemented"); + + # 6.1 Check matching route + if($route = route_find($rm, request_uri($env))) + { + params($route['params']); + + # 6.2 Load controllers dir + if(!function_exists('autoload_controller')) + { + function autoload_controller($callback) + { + require_once_dir(option('controllers_dir')); + } + } + autoload_controller($route['callback']); + + if(is_callable($route['callback'])) + { + # 6.3 Call before function + call_if_exists('before', $route); + + # 6.4 Call matching controller function and output result + $output = call_user_func_array($route['callback'], array_values($route['params'])); + if(is_null($output)) $output = call_if_exists('autorender', $route); + echo after(error_notices_render() . $output, $route); + } + else halt(SERVER_ERROR, "Routing error: undefined function '{$route['callback']}'", $route); + } + else route_missing($rm, request_uri($env)); + + } + else halt(HTTP_NOT_IMPLEMENTED, "The requested method '$rm' is not implemented"); +} + +/** + * Stop and exit limonade application + * + * @access private + * @param boolean exit or not + * @return void + */ +function stop_and_exit($exit = true) +{ + call_if_exists('before_exit', $exit); + $flash_sweep = true; + $headers = headers_list(); + foreach($headers as $header) + { + // If a Content-Type header exists, flash_sweep only if is text/html + // Else if there's no Content-Type header, flash_sweep by default + if(stripos($header, 'Content-Type:') === 0) + { + $flash_sweep = stripos($header, 'Content-Type: text/html') === 0; + break; + } + } + if($flash_sweep) flash_sweep(); + if(defined('SID')) session_write_close(); + if(request_is_head()) ob_end_clean(); + if($exit) exit; +} + +/** + * Returns limonade environment variables: + * + * 'SERVER', 'FILES', 'REQUEST', 'SESSION', 'ENV', 'COOKIE', + * 'GET', 'POST', 'PUT', 'DELETE' + * + * If a null argument is passed, reset and rebuild environment + * + * @param null @reset reset and rebuild environment + * @return array + */ +function env($reset = null) +{ + static $env = array(); + if(func_num_args() > 0) + { + $args = func_get_args(); + if(is_null($args[0])) $env = array(); + } + + if(empty($env)) + { + if(empty($GLOBALS['_SERVER'])) + { + // Fixing empty $GLOBALS['_SERVER'] bug + // http://sofadesign.lighthouseapp.com/projects/29612-limonade/tickets/29-env-is-empty + $GLOBALS['_SERVER'] =& $_SERVER; + $GLOBALS['_FILES'] =& $_FILES; + $GLOBALS['_REQUEST'] =& $_REQUEST; + $GLOBALS['_SESSION'] =& $_SESSION; + $GLOBALS['_ENV'] =& $_ENV; + $GLOBALS['_COOKIE'] =& $_COOKIE; + } + + $glo_names = array('SERVER', 'FILES', 'REQUEST', 'SESSION', 'ENV', 'COOKIE'); + + $vars = array_merge($glo_names, request_methods()); + foreach($vars as $var) + { + $varname = "_$var"; + if(!array_key_exists($varname, $GLOBALS)) $GLOBALS[$varname] = array(); + $env[$var] =& $GLOBALS[$varname]; + } + + $method = request_method($env); + if($method == 'PUT' || $method == 'DELETE') + { + $varname = "_$method"; + if(array_key_exists('_method', $_POST) && $_POST['_method'] == $method) + { + foreach($_POST as $k => $v) + { + if($k == "_method") continue; + $GLOBALS[$varname][$k] = $v; + } + } + else + { + parse_str(file_get_contents('php://input'), $GLOBALS[$varname]); + } + } + } + return $env; +} + +/** + * Returns application root file path + * + * @return string + */ +function app_file() +{ + static $file; + if(empty($file)) + { + $debug_backtrace = debug_backtrace(); + $stacktrace = array_pop($debug_backtrace); + $file = $stacktrace['file']; + } + return file_path($file); +} + + + + + # # # + + + + +# ============================================================================ # +# 2. ERROR # +# ============================================================================ # + +/** + * Associate a function with error code(s) and return all associations + * + * @param string $errno + * @param string $function + * @return array + */ +function error($errno = null, $function = null) +{ + static $errors = array(); + if(func_num_args() > 0) + { + $errors[] = array('errno'=>$errno, 'function'=> $function); + } + return $errors; +} + +/** + * Raise an error, passing a given error number and an optional message, + * then exit. + * Error number should be a HTTP status code or a php user error (E_USER...) + * $errno and $msg arguments can be passsed in any order + * If no arguments are passed, default $errno is SERVER_ERROR (500) + * + * @param int,string $errno Error number or message string + * @param string,string $msg Message string or error number + * @param mixed $debug_args extra data provided for debugging + * @return void + */ +function halt($errno = SERVER_ERROR, $msg = '', $debug_args = null) +{ + $args = func_get_args(); + $error = array_shift($args); + + # switch $errno and $msg args + # TODO cleanup / refactoring + if(is_string($errno)) + { + $msg = $errno; + $oldmsg = array_shift($args); + $errno = empty($oldmsg) ? SERVER_ERROR : $oldmsg; + } + else if(!empty($args)) $msg = array_shift($args); + + if(empty($msg) && $errno == NOT_FOUND) $msg = request_uri(); + if(empty($msg)) $msg = ""; + if(!empty($args)) $debug_args = $args; + set('_lim_err_debug_args', $debug_args); + + error_handler_dispatcher($errno, $msg, null, null); + +} + +/** + * Internal error handler dispatcher + * Find and call matching error handler and exit + * If no match found, call default error handler + * + * @access private + * @param int $errno + * @param string $errstr + * @param string $errfile + * @param string $errline + * @return void + */ +function error_handler_dispatcher($errno, $errstr, $errfile, $errline) +{ + $back_trace = debug_backtrace(); + while($trace = array_shift($back_trace)) + { + if($trace['function'] == 'halt') + { + $errfile = $trace['file']; + $errline = $trace['line']; + break; + } + } + + # Notices and warning won't halt execution + if(error_wont_halt_app($errno)) + { + error_notice($errno, $errstr, $errfile, $errline); + return; + } + else + { + # Other errors will stop application + static $handlers = array(); + if(empty($handlers)) + { + error(E_LIM_PHP, 'error_default_handler'); + $handlers = error(); + } + + $is_http_err = http_response_status_is_valid($errno); + while($handler = array_shift($handlers)) + { + $e = is_array($handler['errno']) ? $handler['errno'] : array($handler['errno']); + while($ee = array_shift($e)) + { + if($ee == $errno || $ee == E_LIM_PHP || ($ee == E_LIM_HTTP && $is_http_err)) + { + echo call_if_exists($handler['function'], $errno, $errstr, $errfile, $errline); + exit; + } + } + } + } +} + + +/** + * Default error handler + * + * @param string $errno + * @param string $errstr + * @param string $errfile + * @param string $errline + * @return string error output + */ +function error_default_handler($errno, $errstr, $errfile, $errline) +{ + $is_http_err = http_response_status_is_valid($errno); + $http_error_code = $is_http_err ? $errno : SERVER_ERROR; + + status($http_error_code); + + return $http_error_code == NOT_FOUND ? + error_not_found_output($errno, $errstr, $errfile, $errline) : + error_server_error_output($errno, $errstr, $errfile, $errline); +} + +/** + * Returns not found error output + * + * @access private + * @param string $msg + * @return string + */ +function error_not_found_output($errno, $errstr, $errfile, $errline) +{ + if(!function_exists('not_found')) + { + /** + * Default not found error output + * + * @param string $errno + * @param string $errstr + * @param string $errfile + * @param string $errline + * @return string + */ + function not_found($errno, $errstr, $errfile=null, $errline=null) + { + option('views_dir', option('error_views_dir')); + $msg = h(rawurldecode($errstr)); + return html("

Page not found:

{$msg}

", error_layout()); + } + } + return not_found($errno, $errstr, $errfile, $errline); +} + +/** + * Returns server error output + * + * @access private + * @param int $errno + * @param string $errstr + * @param string $errfile + * @param string $errline + * @return string + */ +function error_server_error_output($errno, $errstr, $errfile, $errline) +{ + if(!function_exists('server_error')) + { + /** + * Default server error output + * + * @param string $errno + * @param string $errstr + * @param string $errfile + * @param string $errline + * @return string + */ + function server_error($errno, $errstr, $errfile=null, $errline=null) + { + $is_http_error = http_response_status_is_valid($errno); + $args = compact('errno', 'errstr', 'errfile', 'errline', 'is_http_error'); + option('views_dir', option('limonade_views_dir')); + $html = render('error.html.php', null, $args); + option('views_dir', option('error_views_dir')); + return html($html, error_layout(), $args); + } + } + return server_error($errno, $errstr, $errfile, $errline); +} + +/** + * Set and returns error output layout + * + * @param string $layout + * @return string + */ +function error_layout($layout = false) +{ + static $o_layout = 'default_layout.php'; + if($layout !== false) + { + option('error_views_dir', option('views_dir')); + $o_layout = $layout; + } + return $o_layout; +} + + +/** + * Set a notice if arguments are provided + * Returns all stored notices. + * If $errno argument is null, reset the notices array + * + * @access private + * @param string, null $str + * @return array + */ +function error_notice($errno = false, $errstr = null, $errfile = null, $errline = null) +{ + static $notices = array(); + if($errno) $notices[] = compact('errno', 'errstr', 'errfile', 'errline'); + else if(is_null($errno)) $notices = array(); + return $notices; +} + +/** + * Returns notices output rendering and reset notices + * + * @return string + */ +function error_notices_render() +{ + if(option('debug') && option('env') > ENV_PRODUCTION) + { + $notices = error_notice(); + error_notice(null); // reset notices + $c_view_dir = option('views_dir'); // keep for restore after render + option('views_dir', option('limonade_views_dir')); + $o = render('_notices.html.php', null, array('notices' => $notices)); + option('views_dir', $c_view_dir); // restore current views dir + + return $o; + } +} + +/** + * Checks if an error is will halt application execution. + * Notices and warnings will not. + * + * @access private + * @param string $num error code number + * @return boolean + */ +function error_wont_halt_app($num) +{ + return $num == E_NOTICE || + $num == E_WARNING || + $num == E_CORE_WARNING || + $num == E_COMPILE_WARNING || + $num == E_USER_WARNING || + $num == E_USER_NOTICE || + $num == E_DEPRECATED || + $num == E_USER_DEPRECATED || + $num == E_LIM_DEPRECATED; +} + + + +/** + * return error code name for a given code num, or return all errors names + * + * @param string $num + * @return mixed + */ +function error_type($num = null) +{ + $types = array ( + E_ERROR => 'ERROR', + E_WARNING => 'WARNING', + E_PARSE => 'PARSING ERROR', + E_NOTICE => 'NOTICE', + E_CORE_ERROR => 'CORE ERROR', + E_CORE_WARNING => 'CORE WARNING', + E_COMPILE_ERROR => 'COMPILE ERROR', + E_COMPILE_WARNING => 'COMPILE WARNING', + E_USER_ERROR => 'USER ERROR', + E_USER_WARNING => 'USER WARNING', + E_USER_NOTICE => 'USER NOTICE', + E_STRICT => 'STRICT NOTICE', + E_RECOVERABLE_ERROR => 'RECOVERABLE ERROR', + E_DEPRECATED => 'DEPRECATED WARNING', + E_USER_DEPRECATED => 'USER DEPRECATED WARNING', + E_LIM_DEPRECATED => 'LIMONADE DEPRECATED WARNING' + ); + return is_null($num) ? $types : $types[$num]; +} + +/** + * Returns http response status for a given error number + * + * @param string $errno + * @return int + */ +function error_http_status($errno) +{ + $code = http_response_status_is_valid($errno) ? $errno : SERVER_ERROR; + return http_response_status($code); +} + + # # # + + + + +# ============================================================================ # +# 3. REQUEST # +# ============================================================================ # + +/** + * Returns current request method for a given environment or current one + * + * @param string $env + * @return string + */ +function request_method($env = null) +{ + if(is_null($env)) $env = env(); + $m = array_key_exists('REQUEST_METHOD', $env['SERVER']) ? $env['SERVER']['REQUEST_METHOD'] : null; + if($m == "POST" && array_key_exists('_method', $env['POST'])) + $m = strtoupper($env['POST']['_method']); + if(!in_array(strtoupper($m), request_methods())) + { + trigger_error("'$m' request method is unknown or unavailable.", E_USER_WARNING); + $m = false; + } + return $m; +} + +/** + * Checks if a request method or current one is allowed + * + * @param string $m + * @return bool + */ +function request_method_is_allowed($m = null) +{ + if(is_null($m)) $m = request_method(); + return in_array(strtoupper($m), request_methods()); +} + +/** + * Checks if request method is GET + * + * @param string $env + * @return bool + */ +function request_is_get($env = null) +{ + return request_method($env) == "GET"; +} + +/** + * Checks if request method is POST + * + * @param string $env + * @return bool + */ +function request_is_post($env = null) +{ + return request_method($env) == "POST"; +} + +/** + * Checks if request method is PUT + * + * @param string $env + * @return bool + */ +function request_is_put($env = null) +{ + return request_method($env) == "PUT"; +} + +/** + * Checks if request method is DELETE + * + * @param string $env + * @return bool + */ +function request_is_delete($env = null) +{ + return request_method($env) == "DELETE"; +} + +/** + * Checks if request method is HEAD + * + * @param string $env + * @return bool + */ +function request_is_head($env = null) +{ + return request_method($env) == "HEAD"; +} + +/** + * Returns allowed request methods + * + * @return array + */ +function request_methods() +{ + return array("GET","POST","PUT","DELETE", "HEAD"); +} + +/** + * Returns current request uri (the path that will be compared with routes) + * + * (Inspired from codeigniter URI::_fetch_uri_string method) + * + * @return string + */ +function request_uri($env = null) +{ + static $uri = null; + if(is_null($env)) + { + if(!is_null($uri)) return $uri; + $env = env(); + } + + if(array_key_exists('uri', $env['GET'])) + { + $uri = $env['GET']['uri']; + } + else if(array_key_exists('u', $env['GET'])) + { + $uri = $env['GET']['u']; + } + // bug: dot are converted to _... so we can't use it... + // else if (count($env['GET']) == 1 && trim(key($env['GET']), '/') != '') + // { + // $uri = key($env['GET']); + // } + else + { + $app_file = app_file(); + $path_info = isset($env['SERVER']['PATH_INFO']) ? $env['SERVER']['PATH_INFO'] : @getenv('PATH_INFO'); + $query_string = isset($env['SERVER']['QUERY_STRING']) ? $env['SERVER']['QUERY_STRING'] : @getenv('QUERY_STRING'); + + // Is there a PATH_INFO variable? + // Note: some servers seem to have trouble with getenv() so we'll test it two ways + if (trim($path_info, '/') != '' && $path_info != "/".$app_file) + { + if(strpos($path_info, '&') !== 0) + { + # exclude GET params + $params = explode('&', $path_info); + $path_info = array_shift($params); + # populate $_GET + foreach($params as $param) + { + if(strpos($param, '=') > 0) + { + list($k, $v) = explode('=', $param); + $env['GET'][$k] = $v; + } + } + } + $uri = $path_info; + } + // No PATH_INFO?... What about QUERY_STRING? + elseif (trim($query_string, '/') != '') + { + $uri = $query_string; + $get = $env['GET']; + if(count($get) > 0) + { + # exclude GET params + $keys = array_keys($get); + $first = array_shift($keys); + if(strpos($query_string, $first) === 0) $uri = $first; + } + } + elseif(array_key_exists('REQUEST_URI', $env['SERVER']) && !empty($env['SERVER']['REQUEST_URI'])) + { + $request_uri = rtrim(rawurldecode($env['SERVER']['REQUEST_URI']), '?/').'/'; + $base_path = $env['SERVER']['SCRIPT_NAME']; + + if($request_uri."index.php" == $base_path) $request_uri .= "index.php"; + $uri = str_replace($base_path, '', $request_uri); + } + elseif($env['SERVER']['argc'] > 1 && trim($env['SERVER']['argv'][1], '/') != '') + { + $uri = $env['SERVER']['argv'][1]; + } + } + + $uri = rtrim($uri, "/"); # removes ending / + if(empty($uri)) + { + $uri = '/'; + } + else if($uri[0] != '/') + { + $uri = '/' . $uri; # add a leading slash + } + return rawurldecode($uri); +} + + + + + # # # + + + + +# ============================================================================ # +# 4. ROUTER # +# ============================================================================ # + +/** + * An alias of {@link dispatch_get()} + * + * @return void + */ +function dispatch($path_or_array, $callback, $options = array()) +{ + dispatch_get($path_or_array, $callback, $options); +} + +/** + * Add a GET route. Also automatically defines a HEAD route. + * + * @param string $path_or_array + * @param string $callback + * @param array $options (optional). See {@link route()} for available options. + * @return void + */ +function dispatch_get($path_or_array, $callback, $options = array()) +{ + route("GET", $path_or_array, $callback, $options); + route("HEAD", $path_or_array, $callback, $options); +} + +/** + * Add a POST route + * + * @param string $path_or_array + * @param string $callback + * @param array $options (optional). See {@link route()} for available options. + * @return void + */ +function dispatch_post($path_or_array, $callback, $options = array()) +{ + route("POST", $path_or_array, $callback, $options); +} + +/** + * Add a PUT route + * + * @param string $path_or_array + * @param string $callback + * @param array $options (optional). See {@link route()} for available options. + * @return void + */ +function dispatch_put($path_or_array, $callback, $options = array()) +{ + route("PUT", $path_or_array, $callback, $options); +} + +/** + * Add a DELETE route + * + * @param string $path_or_array + * @param string $callback + * @param array $options (optional). See {@link route()} for available options. + * @return void + */ +function dispatch_delete($path_or_array, $callback, $options = array()) +{ + route("DELETE", $path_or_array, $callback, $options); +} + + +/** + * Add route if required params are provided. + * Delete all routes if null is passed as a unique argument + * Return all routes + * + * @see route_build() + * @access private + * @param string $method + * @param string|array $path_or_array + * @param callback $func + * @param array $options (optional). Available options: + * - 'params' key with an array of parameters: for parametrized routes. + * those parameters will be merged with routes parameters. + * @return array + */ +function route() +{ + static $routes = array(); + $nargs = func_num_args(); + if( $nargs > 0) + { + $args = func_get_args(); + if($nargs === 1 && is_null($args[0])) $routes = array(); + else if($nargs < 3) trigger_error("Missing arguments for route()", E_USER_ERROR); + else + { + $method = $args[0]; + $path_or_array = $args[1]; + $func = $args[2]; + $options = $nargs > 3 ? $args[3] : array(); + + $routes[] = route_build($method, $path_or_array, $func, $options); + } + } + return $routes; +} + +/** + * An alias of route(null): reset all routes + * + * @access private + * @return void + */ +function route_reset() +{ + route(null); +} + +/** + * Build a route and return it + * + * @access private + * @param string $method allowed http method (one of those returned by {@link request_methods()}) + * @param string|array $path_or_array + * @param callback $callback callback called when route is found. It can be + * a function, an object method, a static method or a closure. + * See {@link http://php.net/manual/en/language.pseudo-types.php#language.types.callback php documentation} + * to learn more about callbacks. + * @param array $options (optional). Available options: + * - 'params' key with an array of parameters: for parametrized routes. + * those parameters will be merged with routes parameters. + * @return array array with keys "method", "pattern", "names", "callback", "options" + */ +function route_build($method, $path_or_array, $callback, $options = array()) +{ + $method = strtoupper($method); + if(!in_array($method, request_methods())) + trigger_error("'$method' request method is unkown or unavailable.", E_USER_WARNING); + + if(is_array($path_or_array)) + { + $path = array_shift($path_or_array); + $names = $path_or_array[0]; + } + else + { + $path = $path_or_array; + $names = array(); + } + + $single_asterisk_subpattern = "(?:/([^\/]*))?"; + $double_asterisk_subpattern = "(?:/(.*))?"; + $optionnal_slash_subpattern = "(?:/*?)"; + $no_slash_asterisk_subpattern = "(?:([^\/]*))?"; + + if($path[0] == "^") + { + if($path{strlen($path) - 1} != "$") $path .= "$"; + $pattern = "#".$path."#i"; + } + else if(empty($path) || $path == "/") + { + $pattern = "#^".$optionnal_slash_subpattern."$#"; + } + else + { + $parsed = array(); + $elts = explode('/', $path); + + $parameters_count = 0; + + foreach($elts as $elt) + { + if(empty($elt)) continue; + + $name = null; + + # extracting double asterisk ** + if($elt == "**"): + $parsed[] = $double_asterisk_subpattern; + $name = $parameters_count; + + # extracting single asterisk * + elseif($elt == "*"): + $parsed[] = $single_asterisk_subpattern; + $name = $parameters_count; + + # extracting named parameters :my_param + elseif($elt[0] == ":"): + if(preg_match('/^:([^\:]+)$/', $elt, $matches)) + { + $parsed[] = $single_asterisk_subpattern; + $name = $matches[1]; + }; + + elseif(strpos($elt, '*') !== false): + $sub_elts = explode('*', $elt); + $parsed_sub = array(); + foreach($sub_elts as $sub_elt) + { + $parsed_sub[] = preg_quote($sub_elt, "#"); + $name = $parameters_count; + } + // + $parsed[] = "/".implode($no_slash_asterisk_subpattern, $parsed_sub); + + else: + $parsed[] = "/".preg_quote($elt, "#"); + + endif; + + /* set parameters names */ + if(is_null($name)) continue; + if(!array_key_exists($parameters_count, $names) || is_null($names[$parameters_count])) + $names[$parameters_count] = $name; + $parameters_count++; + } + + $pattern = "#^".implode('', $parsed).$optionnal_slash_subpattern."?$#i"; + } + + return array( "method" => $method, + "pattern" => $pattern, + "names" => $names, + "callback" => $callback, + "options" => $options ); +} + +/** + * Find a route and returns it. + * Parameters values extracted from the path are added and merged + * with the default 'params' option of the route + * If not found, returns false. + * Routes are checked from first added to last added. + * + * @access private + * @param string $method + * @param string $path + * @return array,false route array has same keys as route returned by + * {@link route_build()} ("method", "pattern", "names", "callback", "options") + * + the processed "params" key + */ +function route_find($method, $path) +{ + $routes = route(); + $method = strtoupper($method); + foreach($routes as $route) + { + if($method == $route["method"] && preg_match($route["pattern"], $path, $matches)) + { + $options = $route["options"]; + $params = array_key_exists('params', $options) ? $options["params"] : array(); + if(count($matches) > 1) + { + array_shift($matches); + $n_matches = count($matches); + $names = array_values($route["names"]); + $n_names = count($names); + if( $n_matches < $n_names ) + { + $a = array_fill(0, $n_names - $n_matches, null); + $matches = array_merge($matches, $a); + } + else if( $n_matches > $n_names ) + { + $names = range($n_names, $n_matches - 1); + } + $params = array_replace($params, array_combine($names, $matches)); + } + $route["params"] = $params; + return $route; + } + } + return false; +} + + + + + +# ============================================================================ # +# 5. OUTPUT AND RENDERING # +# ============================================================================ # + +/** + * Returns a string to output + * + * It might use a template file, a function, or a formatted string (like {@link sprintf()}). + * It could be embraced by a layout or not. + * Local vars can be passed in addition to variables made available with the {@link set()} + * function. + * + * @param string $content_or_func + * @param string $layout + * @param string $locals + * @return string + */ +function render($content_or_func, $layout = '', $locals = array()) +{ + $args = func_get_args(); + $content_or_func = array_shift($args); + $layout = count($args) > 0 ? array_shift($args) : layout(); + $view_path = file_path(option('views_dir'),$content_or_func); + + if(function_exists('before_render')) + list($content_or_func, $layout, $locals, $view_path) = before_render($content_or_func, $layout, $locals, $view_path); + + $vars = array_merge(set(), $locals); + + $flash = flash_now(); + if(array_key_exists('flash', $vars)) trigger_error('A $flash variable is already passed to view. Flash messages will only be accessible through flash_now()', E_USER_NOTICE); + else if(!empty($flash)) $vars['flash'] = $flash; + + $infinite_loop = false; + + # Avoid infinite loop: this function is in the backtrace ? + if(function_exists($content_or_func)) + { + $back_trace = debug_backtrace(); + while($trace = array_shift($back_trace)) + { + if($trace['function'] == strtolower($content_or_func)) + { + $infinite_loop = true; + break; + } + } + } + + if(function_exists($content_or_func) && !$infinite_loop) + { + ob_start(); + call_user_func($content_or_func, $vars); + $content = ob_get_clean(); + } + elseif(file_exists($view_path)) + { + ob_start(); + extract($vars); + include $view_path; + $content = ob_get_clean(); + } + else + { + if(substr_count($content_or_func, '%') !== count($vars)) $content = $content_or_func; + else $content = vsprintf($content_or_func, $vars); + } + + if(empty($layout)) return $content; + + return render($layout, null, array('content' => $content)); +} + +/** + * Returns a string to output + * + * Shortcut to render with no layout. + * + * @param string $content_or_func + * @param string $locals + * @return string + */ +function partial($content_or_func, $locals = array()) +{ + return render($content_or_func, null, $locals); +} + +/** + * Returns html output with proper http headers + * + * @param string $content_or_func + * @param string $layout + * @param string $locals + * @return string + */ +function html($content_or_func, $layout = '', $locals = array()) +{ + if(!headers_sent()) header('Content-Type: text/html; charset='.strtolower(option('encoding'))); + $args = func_get_args(); + return call_user_func_array('render', $args); +} + +/** + * Set and return current layout + * + * @param string $function_or_file + * @return string + */ +function layout($function_or_file = null) +{ + static $layout = null; + if(func_num_args() > 0) $layout = $function_or_file; + return $layout; +} + +/** + * Returns xml output with proper http headers + * + * @param string $content_or_func + * @param string $layout + * @param string $locals + * @return string + */ +function xml($data) +{ + if(!headers_sent()) header('Content-Type: text/xml; charset='.strtolower(option('encoding'))); + $args = func_get_args(); + return call_user_func_array('render', $args); +} + +/** + * Returns css output with proper http headers + * + * @param string $content_or_func + * @param string $layout + * @param string $locals + * @return string + */ +function css($content_or_func, $layout = '', $locals = array()) +{ + if(!headers_sent()) header('Content-Type: text/css; charset='.strtolower(option('encoding'))); + $args = func_get_args(); + return call_user_func_array('render', $args); +} + +/** + * Returns javacript output with proper http headers + * + * @param string $content_or_func + * @param string $layout + * @param string $locals + * @return string + */ +function js($content_or_func, $layout = '', $locals = array()) +{ + if(!headers_sent()) header('Content-Type: application/javascript; charset='.strtolower(option('encoding'))); + $args = func_get_args(); + return call_user_func_array('render', $args); +} + +/** + * Returns txt output with proper http headers + * + * @param string $content_or_func + * @param string $layout + * @param string $locals + * @return string + */ +function txt($content_or_func, $layout = '', $locals = array()) +{ + if(!headers_sent()) header('Content-Type: text/plain; charset='.strtolower(option('encoding'))); + $args = func_get_args(); + return call_user_func_array('render', $args); +} + +/** + * Returns json representation of data with proper http headers + * + * @param string $data + * @param int $json_option + * @return string + */ +function json($data, $json_option = 0) +{ + if(!headers_sent()) header('Content-Type: application/json; charset='.strtolower(option('encoding'))); + return version_compare(PHP_VERSION, '5.3.0', '>=') ? json_encode($data, $json_option) : json_encode($data); +} + +/** + * undocumented function + * + * @param string $filename + * @param string $return + * @return mixed number of bytes delivered or file output if $return = true + */ +function render_file($filename, $return = false) +{ + # TODO implements X-SENDFILE headers + // if($x-sendfile = option('x-sendfile')) + // { + // // add a X-Sendfile header for apache and Lighttpd >= 1.5 + // if($x-sendfile > X-SENDFILE) // add a X-LIGHTTPD-send-file header + // + // } + // else + // { + // + // } + $filename = str_replace('../', '', $filename); + if(file_exists($filename)) + { + $content_type = mime_type(file_extension($filename)); + $header = 'Content-type: '.$content_type; + if(file_is_text($filename)) $header .= '; charset='.strtolower(option('encoding')); + if(!headers_sent()) header($header); + return file_read($filename, $return); + } + else halt(NOT_FOUND, "unknown filename $filename"); +} + + + + + + + # # # + + + + +# ============================================================================ # +# 6. HELPERS # +# ============================================================================ # + +/** + * Returns an url composed of params joined with / + * A param can be a string or an array. + * If param is an array, its members will be added at the end of the return url + * as GET parameters "&key=value". + * + * @param string or array $param1, $param2 ... + * @return string + */ +function url_for($params = null) +{ + $paths = array(); + $params = func_get_args(); + $GET_params = array(); + foreach($params as $param) + { + if(is_array($param)) + { + $GET_params = array_merge($GET_params, $param); + continue; + } + if(filter_var_url($param)) + { + $paths[] = $param; + continue; + } + $p = explode('/',$param); + foreach($p as $v) + { + if($v != "") $paths[] = str_replace('%23', '#', rawurlencode($v)); + } + } + + $path = rtrim(implode('/', $paths), '/'); + + if(!filter_var_url($path)) + { + # it's a relative URL or an URL without a schema + $base_uri = option('base_uri'); + $path = file_path($base_uri, $path); + } + + if(!empty($GET_params)) + { + $is_first_qs_param = true; + $path_as_no_question_mark = strpos($path, '?') === false; + + foreach($GET_params as $k => $v) + { + $qs_separator = $is_first_qs_param && $path_as_no_question_mark ? + '?' : '&'; + $path .= $qs_separator . rawurlencode($k) . '=' . rawurlencode($v); + $is_first_qs_param = false; + } + } + + if(DIRECTORY_SEPARATOR != '/') $path = str_replace(DIRECTORY_SEPARATOR, '/', $path); + + return $path; +} + +/** + * An alias of {@link htmlspecialchars()}. + * If no $charset is provided, uses option('encoding') value + * + * @param string $str + * @param string $quote_style + * @param string $charset + * @return void + */ +function h($str, $quote_style = ENT_NOQUOTES, $charset = null) +{ + if(is_null($charset)) $charset = strtoupper(option('encoding')); + return htmlspecialchars($str, $quote_style, $charset); +} + +/** + * Set and returns flash messages that will be available in the next action + * via the {@link flash_now()} function or the view variable $flash. + * + * If multiple values are provided, set $name variable with an array of those values. + * If there is only one value, set $name variable with the provided $values + * or if it's $name is an array, merge it with current messages. + * + * @param string, array $name + * @param mixed $values,... + * @return mixed variable value for $name if $name argument is provided, else return all variables + */ +function flash($name = null, $value = null) +{ + if(!defined('SID')) trigger_error("Flash messages can't be used because session isn't enabled", E_USER_WARNING); + static $messages = array(); + $args = func_get_args(); + $name = array_shift($args); + if(is_null($name)) return $messages; + if(is_array($name)) return $messages = array_merge($messages, $name); + if(!empty($args)) + { + $messages[$name] = count($args) > 1 ? $args : $args[0]; + } + if(!array_key_exists($name, $messages)) return null; + else return $messages[$name]; + return $messages; +} + +/** + * Set and returns flash messages available for the current action, included those + * defined in the previous action with {@link flash()} + * Those messages will also be passed to the views and made available in the + * $flash variable. + * + * If multiple values are provided, set $name variable with an array of those values. + * If there is only one value, set $name variable with the provided $values + * or if it's $name is an array, merge it with current messages. + * + * @param string, array $name + * @param mixed $values,... + * @return mixed variable value for $name if $name argument is provided, else return all variables + */ +function flash_now($name = null, $value = null) +{ + static $messages = null; + if(is_null($messages)) + { + $fkey = LIM_SESSION_FLASH_KEY; + $messages = array(); + if(defined('SID') && array_key_exists($fkey, $_SESSION)) $messages = $_SESSION[$fkey]; + } + $args = func_get_args(); + $name = array_shift($args); + if(is_null($name)) return $messages; + if(is_array($name)) return $messages = array_merge($messages, $name); + if(!empty($args)) + { + $messages[$name] = count($args) > 1 ? $args : $args[0]; + } + if(!array_key_exists($name, $messages)) return null; + else return $messages[$name]; + return $messages; +} + +/** + * Delete current flash messages in session, and set new ones stored with + * flash function. + * Called before application exit. + * + * @access private + * @return void + */ +function flash_sweep() +{ + if(defined('SID')) + { + $fkey = LIM_SESSION_FLASH_KEY; + $_SESSION[$fkey] = flash(); + } +} + +/** + * Starts capturing block of text + * + * Calling without params stops capturing (same as end_content_for()). + * After capturing the captured block is put into a variable + * named $name for later use in layouts. If second parameter + * is supplied, its content will be used instead of capturing + * a block of text. + * + * @param string $name + * @param string $content + * @return void + */ +function content_for($name = null, $content = null) +{ + static $_name = null; + if(is_null($name) && !is_null($_name)) + { + set($_name, ob_get_clean()); + $_name = null; + } + elseif(!is_null($name) && !isset($content)) + { + $_name = $name; + ob_start(); + } + elseif(isset($name, $content)) + { + set($name, $content); + } +} + +/** + * Stops capturing block of text + * + * @return void + */ +function end_content_for() +{ + content_for(); +} + +/** + * Shows current memory and execution time of the application. + * + * @access public + * @return array + */ +function benchmark() +{ + $current_mem_usage = memory_get_usage(); + $execution_time = microtime() - LIM_START_MICROTIME; + + return array( + 'current_memory' => $current_mem_usage, + 'start_memory' => LIM_START_MEMORY, + 'average_memory' => (LIM_START_MEMORY + $current_mem_usage) / 2, + 'execution_time' => $execution_time + ); +} + + + + + # # # + + + + +# ============================================================================ # +# 7. UTILS # +# ============================================================================ # + +/** + * Calls a function if exists + * + * @param callback $callback a function stored in a string variable, + * or an object and the name of a method within the object + * See {@link http://php.net/manual/en/language.pseudo-types.php#language.types.callback php documentation} + * to learn more about callbacks. + * @param mixed $arg,.. (optional) + * @return mixed + */ +function call_if_exists($callback) +{ + $args = func_get_args(); + $callback = array_shift($args); + if(is_callable($callback)) return call_user_func_array($callback, $args); + return; +} + +/** + * Define a constant unless it already exists + * + * @param string $name + * @param string $value + * @return void + */ +function define_unless_exists($name, $value) +{ + if(!defined($name)) define($name, $value); +} + +/** + * Return a default value if provided value is empty + * + * @param mixed $value + * @param mixed $default default value returned if $value is empty + * @return mixed + */ +function value_or_default($value, $default) +{ + return empty($value) ? $default : $value; +} + +/** + * An alias of {@link value_or_default()} + * + * + * @param mixed $value + * @param mixed $default + * @return mixed + */ +function v($value, $default) +{ + return value_or_default($value, $default); +} + +/** + * Load php files with require_once in a given dir + * + * @param string $path Path in which are the file to load + * @param string $pattern a regexp pattern that filter files to load + * @param bool $prevents_output security option that prevents output + * @return array paths of loaded files + */ +function require_once_dir($path, $pattern = "*.php", $prevents_output = true) +{ + if($path[strlen($path) - 1] != "/") $path .= "/"; + $filenames = glob($path.$pattern); + if(!is_array($filenames)) $filenames = array(); + if($prevents_output) ob_start(); + foreach($filenames as $filename) require_once $filename; + if($prevents_output) ob_end_clean(); + return $filenames; +} + +/** + * Dumps a variable into inspectable format + * + * @param anything $var the variable to debug + * @param bool $output_as_html sets whether to wrap output in
 tags. default: true
+ * @return string the variable with output
+ */
+function debug($var, $output_as_html = true)
+{ 
+  if ( is_null($var) ) { return '[NULL]'; };
+  $out = '';
+  switch ($var) 
+  { 
+    case empty($var):
+      $out = '[empty value]';
+      break;
+    
+    case is_array($var):
+      $out = var_export($var, true);
+      break;
+    
+    case is_object($var):
+      $out = var_export($var, true);
+      break;
+      
+    case is_string($var):
+      $out = $var;
+      break;
+    
+    default:
+      $out = var_export($var, true);
+      break;
+  }
+  if ($output_as_html) { $out = h($out);  }
+  return "
\n" . $out ."
"; +} + + +## HTTP utils _________________________________________________________________ + + +### Constants: HTTP status codes + +define( 'HTTP_CONTINUE', 100 ); +define( 'HTTP_SWITCHING_PROTOCOLS', 101 ); +define( 'HTTP_PROCESSING', 102 ); +define( 'HTTP_OK', 200 ); +define( 'HTTP_CREATED', 201 ); +define( 'HTTP_ACCEPTED', 202 ); +define( 'HTTP_NON_AUTHORITATIVE', 203 ); +define( 'HTTP_NO_CONTENT', 204 ); +define( 'HTTP_RESET_CONTENT', 205 ); +define( 'HTTP_PARTIAL_CONTENT', 206 ); +define( 'HTTP_MULTI_STATUS', 207 ); + +define( 'HTTP_MULTIPLE_CHOICES', 300 ); +define( 'HTTP_MOVED_PERMANENTLY', 301 ); +define( 'HTTP_MOVED_TEMPORARILY', 302 ); +define( 'HTTP_SEE_OTHER', 303 ); +define( 'HTTP_NOT_MODIFIED', 304 ); +define( 'HTTP_USE_PROXY', 305 ); +define( 'HTTP_TEMPORARY_REDIRECT', 307 ); + +define( 'HTTP_BAD_REQUEST', 400 ); +define( 'HTTP_UNAUTHORIZED', 401 ); +define( 'HTTP_PAYMENT_REQUIRED', 402 ); +define( 'HTTP_FORBIDDEN', 403 ); +define( 'HTTP_NOT_FOUND', 404 ); +define( 'HTTP_METHOD_NOT_ALLOWED', 405 ); +define( 'HTTP_NOT_ACCEPTABLE', 406 ); +define( 'HTTP_PROXY_AUTHENTICATION_REQUIRED', 407 ); +define( 'HTTP_REQUEST_TIME_OUT', 408 ); +define( 'HTTP_CONFLICT', 409 ); +define( 'HTTP_GONE', 410 ); +define( 'HTTP_LENGTH_REQUIRED', 411 ); +define( 'HTTP_PRECONDITION_FAILED', 412 ); +define( 'HTTP_REQUEST_ENTITY_TOO_LARGE', 413 ); +define( 'HTTP_REQUEST_URI_TOO_LARGE', 414 ); +define( 'HTTP_UNSUPPORTED_MEDIA_TYPE', 415 ); +define( 'HTTP_RANGE_NOT_SATISFIABLE', 416 ); +define( 'HTTP_EXPECTATION_FAILED', 417 ); +define( 'HTTP_UNPROCESSABLE_ENTITY', 422 ); +define( 'HTTP_LOCKED', 423 ); +define( 'HTTP_FAILED_DEPENDENCY', 424 ); +define( 'HTTP_UPGRADE_REQUIRED', 426 ); + +define( 'HTTP_INTERNAL_SERVER_ERROR', 500 ); +define( 'HTTP_NOT_IMPLEMENTED', 501 ); +define( 'HTTP_BAD_GATEWAY', 502 ); +define( 'HTTP_SERVICE_UNAVAILABLE', 503 ); +define( 'HTTP_GATEWAY_TIME_OUT', 504 ); +define( 'HTTP_VERSION_NOT_SUPPORTED', 505 ); +define( 'HTTP_VARIANT_ALSO_VARIES', 506 ); +define( 'HTTP_INSUFFICIENT_STORAGE', 507 ); +define( 'HTTP_NOT_EXTENDED', 510 ); + +/** + * Output proper HTTP header for a given HTTP code + * + * @param string $code + * @return void + */ +function status($code = 500) +{ + if(!headers_sent()) + { + $str = http_response_status_code($code); + header($str); + } +} + +/** + * Http redirection + * + * Same use as {@link url_for()} + * By default HTTP status code is 302, but a different code can be specified + * with a status key in array parameter. + * + * + * redirecto('new','url'); # 302 HTTP_MOVED_TEMPORARILY by default + * redirecto('new','url', array('status' => HTTP_MOVED_PERMANENTLY)); + * + * + * @param string or array $param1, $param2... + * @return void + */ +function redirect_to($params) +{ + # [NOTE]: (from php.net) HTTP/1.1 requires an absolute URI as argument to » Location: + # including the scheme, hostname and absolute path, but some clients accept + # relative URIs. You can usually use $_SERVER['HTTP_HOST'], + # $_SERVER['PHP_SELF'] and dirname() to make an absolute URI from a relative + # one yourself. + + # TODO make absolute uri + if(!headers_sent()) + { + $status = HTTP_MOVED_TEMPORARILY; # default for a redirection in PHP + $params = func_get_args(); + $n_params = array(); + # extract status param if exists + foreach($params as $param) + { + if(is_array($param)) + { + if(array_key_exists('status', $param)) + { + $status = $param['status']; + unset($param['status']); + } + } + $n_params[] = $param; + } + $uri = call_user_func_array('url_for', $n_params); + stop_and_exit(false); + header('Location: '.$uri, true, $status); + exit; + } +} + +/** + * Http redirection + * + * @deprecated deprecated since version 0.4. Please use {@link redirect_to()} instead. + * @param string $url + * @return void + */ +function redirect($uri) +{ + # halt('redirect() is deprecated. Please use redirect_to() instead.', E_LIM_DEPRECATED); + # halt not necesary... it won't be visible because of http redirection... + redirect_to($uri); +} + +/** + * Returns HTTP response status for a given code. + * If no code provided, return an array of all status + * + * @param string $num + * @return string,array + */ +function http_response_status($num = null) +{ + $status = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 226 => 'IM Used', + + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Reserved', + 307 => 'Temporary Redirect', + + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 426 => 'Upgrade Required', + + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 510 => 'Not Extended' + ); + if(is_null($num)) return $status; + return array_key_exists($num, $status) ? $status[$num] : ''; +} + +/** + * Checks if an HTTP response code is valid + * + * @param string $num + * @return bool + */ +function http_response_status_is_valid($num) +{ + $r = http_response_status($num); + return !empty($r); +} + +/** + * Returns an HTTP response status string for a given code + * + * @param string $num + * @return string + */ +function http_response_status_code($num) +{ + $protocole = empty($_SERVER["SERVER_PROTOCOL"]) ? "HTTP/1.1" : $_SERVER["SERVER_PROTOCOL"]; + if($str = http_response_status($num)) return "$protocole $num $str"; +} + +/** + * Check if the _Accept_ header is present, and includes the given `type`. + * + * When the _Accept_ header is not present `true` is returned. Otherwise + * the given `type` is matched by an exact match, and then subtypes. You + * may pass the subtype such as "html" which is then converted internally + * to "text/html" using the mime lookup table. + * + * @param string $type + * @param string $env + * @return bool + */ +function http_ua_accepts($type, $env = null) +{ + if(is_null($env)) $env = env(); + $accept = array_key_exists('HTTP_ACCEPT', $env['SERVER']) ? $env['SERVER']['HTTP_ACCEPT'] : null; + + if(!$accept || $accept === '*/*') return true; + + if($type) + { + // Allow "html" vs "text/html" etc + if(!strpos($type, '/')) $type = mime_type($type); + + // Check if we have a direct match + if(strpos($accept, $type) > -1) return true; + + // Check if we have type/* + $type_parts = explode('/', $type); + $type = $type_parts[0].'/*'; + return (strpos($accept, $type) > -1); + } + + return false; +} + +## FILE utils _________________________________________________________________ + +/** + * Returns mime type for a given extension or if no extension is provided, + * all mime types in an associative array, with extensions as keys. + * (extracted from Orbit source http://orbit.luaforge.net/) + * + * @param string $ext + * @return string, array + */ +function mime_type($ext = null) +{ + $types = array( + 'ai' => 'application/postscript', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'asc' => 'text/plain', + 'atom' => 'application/atom+xml', + 'atom' => 'application/atom+xml', + 'au' => 'audio/basic', + 'avi' => 'video/x-msvideo', + 'bcpio' => 'application/x-bcpio', + 'bin' => 'application/octet-stream', + 'bmp' => 'image/bmp', + 'cdf' => 'application/x-netcdf', + 'cgm' => 'image/cgm', + 'class' => 'application/octet-stream', + 'cpio' => 'application/x-cpio', + 'cpt' => 'application/mac-compactpro', + 'csh' => 'application/x-csh', + 'css' => 'text/css', + 'csv' => 'text/csv', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'djv' => 'image/vnd.djvu', + 'djvu' => 'image/vnd.djvu', + 'dll' => 'application/octet-stream', + 'dmg' => 'application/octet-stream', + 'dms' => 'application/octet-stream', + 'doc' => 'application/msword', + 'dtd' => 'application/xml-dtd', + 'dvi' => 'application/x-dvi', + 'dxr' => 'application/x-director', + 'eps' => 'application/postscript', + 'etx' => 'text/x-setext', + 'exe' => 'application/octet-stream', + 'ez' => 'application/andrew-inset', + 'gif' => 'image/gif', + 'gram' => 'application/srgs', + 'grxml' => 'application/srgs+xml', + 'gtar' => 'application/x-gtar', + 'hdf' => 'application/x-hdf', + 'hqx' => 'application/mac-binhex40', + 'htm' => 'text/html', + 'html' => 'text/html', + 'ice' => 'x-conference/x-cooltalk', + 'ico' => 'image/x-icon', + 'ics' => 'text/calendar', + 'ief' => 'image/ief', + 'ifb' => 'text/calendar', + 'iges' => 'model/iges', + 'igs' => 'model/iges', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'js' => 'application/x-javascript', + 'json' => 'application/json', + 'kar' => 'audio/midi', + 'latex' => 'application/x-latex', + 'lha' => 'application/octet-stream', + 'lzh' => 'application/octet-stream', + 'm3u' => 'audio/x-mpegurl', + 'man' => 'application/x-troff-man', + 'mathml' => 'application/mathml+xml', + 'me' => 'application/x-troff-me', + 'mesh' => 'model/mesh', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mif' => 'application/vnd.mif', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpga' => 'audio/mpeg', + 'ms' => 'application/x-troff-ms', + 'msh' => 'model/mesh', + 'mxu' => 'video/vnd.mpegurl', + 'nc' => 'application/x-netcdf', + 'oda' => 'application/oda', + 'ogg' => 'application/ogg', + 'pbm' => 'image/x-portable-bitmap', + 'pdb' => 'chemical/x-pdb', + 'pdf' => 'application/pdf', + 'pgm' => 'image/x-portable-graymap', + 'pgn' => 'application/x-chess-pgn', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'ppm' => 'image/x-portable-pixmap', + 'ppt' => 'application/vnd.ms-powerpoint', + 'ps' => 'application/postscript', + 'qt' => 'video/quicktime', + 'ra' => 'audio/x-pn-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'ras' => 'image/x-cmu-raster', + 'rdf' => 'application/rdf+xml', + 'rgb' => 'image/x-rgb', + 'rm' => 'application/vnd.rn-realmedia', + 'roff' => 'application/x-troff', + 'rss' => 'application/rss+xml', + 'rtf' => 'text/rtf', + 'rtx' => 'text/richtext', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'sh' => 'application/x-sh', + 'shar' => 'application/x-shar', + 'silo' => 'model/mesh', + 'sit' => 'application/x-stuffit', + 'skd' => 'application/x-koan', + 'skm' => 'application/x-koan', + 'skp' => 'application/x-koan', + 'skt' => 'application/x-koan', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'snd' => 'audio/basic', + 'so' => 'application/octet-stream', + 'spl' => 'application/x-futuresplash', + 'src' => 'application/x-wais-source', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'swf' => 'application/x-shockwave-flash', + 't' => 'application/x-troff', + 'tar' => 'application/x-tar', + 'tcl' => 'application/x-tcl', + 'tex' => 'application/x-tex', + 'texi' => 'application/x-texinfo', + 'texinfo' => 'application/x-texinfo', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'tr' => 'application/x-troff', + 'tsv' => 'text/tab-separated-values', + 'txt' => 'text/plain', + 'ustar' => 'application/x-ustar', + 'vcd' => 'application/x-cdlink', + 'vrml' => 'model/vrml', + 'vxml' => 'application/voicexml+xml', + 'wav' => 'audio/x-wav', + 'wbmp' => 'image/vnd.wap.wbmp', + 'wbxml' => 'application/vnd.wap.wbxml', + 'wml' => 'text/vnd.wap.wml', + 'wmlc' => 'application/vnd.wap.wmlc', + 'wmls' => 'text/vnd.wap.wmlscript', + 'wmlsc' => 'application/vnd.wap.wmlscriptc', + 'wrl' => 'model/vrml', + 'xbm' => 'image/x-xbitmap', + 'xht' => 'application/xhtml+xml', + 'xhtml' => 'application/xhtml+xml', + 'xls' => 'application/vnd.ms-excel', + 'xml' => 'application/xml', + 'xpm' => 'image/x-xpixmap', + 'xsl' => 'application/xml', + 'xslt' => 'application/xslt+xml', + 'xul' => 'application/vnd.mozilla.xul+xml', + 'xwd' => 'image/x-xwindowdump', + 'xyz' => 'chemical/x-xyz', + 'zip' => 'application/zip' + ); + return is_null($ext) ? $types : $types[strtolower($ext)]; +} + +/** + * Detect MIME Content-type for a file + * + * @param string $filename Path to the tested file. + * @return string + */ +function file_mime_content_type($filename) +{ + $ext = file_extension($filename); /* strtolower isn't necessary */ + if($mime = mime_type($ext)) return $mime; + elseif (function_exists('finfo_open')) + { + if($finfo = finfo_open(FILEINFO_MIME)) + { + if($mime = finfo_file($finfo, $filename)) + { + finfo_close($finfo); + return $mime; + } + } + } + return 'application/octet-stream'; +} + + +/** + * Read and output file content and return filesize in bytes or status after + * closing file. + * This function is very efficient for outputing large files without timeout + * nor too expensive memory use + * + * @param string $filename + * @param string $retbytes + * @return bool, int + */ +function file_read_chunked($filename, $retbytes = true) +{ + $chunksize = 1*(1024*1024); // how many bytes per chunk + $buffer = ''; + $cnt = 0; + $handle = fopen($filename, 'rb'); + if ($handle === false) return false; + + ob_start(); + while (!feof($handle)) { + $buffer = fread($handle, $chunksize); + echo $buffer; + ob_flush(); + flush(); + if ($retbytes) $cnt += strlen($buffer); + set_time_limit(0); + } + ob_end_flush(); + + $status = fclose($handle); + if ($retbytes && $status) return $cnt; // return num. bytes delivered like readfile() does. + return $status; +} + +/** + * Create a file path by concatenation of given arguments. + * Windows paths with backslash directory separators are normalized in *nix paths. + * + * @param string $path, ... + * @return string normalized path + */ +function file_path($path) +{ + $args = func_get_args(); + $ds = '/'; + $win_ds = '\\'; + $n_path = count($args) > 1 ? implode($ds, $args) : $path; + if(strpos($n_path, $win_ds) !== false) $n_path = str_replace( $win_ds, $ds, $n_path ); + $n_path = preg_replace( "#$ds+#", $ds, $n_path); + + return $n_path; +} + +/** + * Returns file extension or false if none + * + * @param string $filename + * @return string, false + */ +function file_extension($filename) +{ + $pos = strrpos($filename, '.'); + if($pos !== false) return substr($filename, $pos + 1); + return false; +} + +/** + * Checks if $filename is a text file + * + * @param string $filename + * @return bool + */ +function file_is_text($filename) +{ + if($mime = file_mime_content_type($filename)) return substr($mime,0,5) == "text/"; + return null; +} + +/** + * Checks if $filename is a binary file + * + * @param string $filename + * @return void + */ +function file_is_binary($filename) +{ + $is_text = file_is_text($filename); + return is_null($is_text) ? null : !$is_text; +} + +/** + * Return or output file content + * + * @return string, int + * + **/ + +function file_read($filename, $return = false) +{ + if(!file_exists($filename)) trigger_error("$filename doesn't exists", E_USER_ERROR); + if($return) return file_get_contents($filename); + return file_read_chunked($filename); +} + +/** + * Returns an array of files contained in a directory + * + * @param string $dir + * @return array + */ +function file_list_dir($dir) +{ + $files = array(); + if ($handle = opendir($dir)) + { + while (false !== ($file = readdir($handle))) + { + if ($file[0] != "." && $file != "..") $files[] = $file; + } + closedir($handle); + } + return $files; +} + +## Extra utils ________________________________________________________________ + +if(!function_exists('array_replace')) +{ + /** + * For PHP 5 < 5.3.0 (backward compatibility) + * (from {@link http://www.php.net/manual/fr/function.array-replace.php#92549 this php doc. note}) + * + * @see array_replace() + * @param string $array + * @param string $array1 + * @return $array + */ + function array_replace( array &$array, array &$array1 ) + { + $args = func_get_args(); + $count = func_num_args(); + + for ($i = 0; $i < $count; ++$i) + { + if(is_array($args[$i])) + { + foreach ($args[$i] as $key => $val) $array[$key] = $val; + } + else + { + trigger_error( + __FUNCTION__ . '(): Argument #' . ($i+1) . ' is not an array', + E_USER_WARNING + ); + return null; + } + } + return $array; + } +} + +/** + * Check if a string is an url + * + * This implementation no longer requires + * {@link http://www.php.net/manual/en/book.filter.php the filter extenstion}, + * so it will improve compatibility with older PHP versions. + * + * @param string $str + * @return false, str the string if true, false instead + */ +function filter_var_url($str) +{ + $regexp = '@^https?://([-[:alnum:]]+\.)+[a-zA-Z]{2,6}(:[0-9]+)?(.*)?$@'; + $options = array( "options" => array("regexp" => $regexp )); + return preg_match($regexp, $str) ? $str : false; +} + + + + + +# ================================= END ================================== # diff --git a/classes/tropo.class.php b/classes/tropo.class.php new file mode 100644 index 0000000..cec277a --- /dev/null +++ b/classes/tropo.class.php @@ -0,0 +1,1766 @@ +tropo = array(); + } + + /** + * Set a default voice for use with all Text To Speech. + * + * Tropo's text to speech engine can pronounce your text with + * a variety of voices in different languages. All elements where + * you can create text to speech (TTS) accept a voice parameter. + * Tropo's default is "Allison" but you can set a default for this + * script here. + * + * @param string $voice + */ + public function setVoice($voice) { + $this->_voice = $voice; + } + + /** + * Set a default language to use in speech recognition. + * + * When recognizing spoken input, Tropo allows you to set a language + * to let the platform know which language is being spoken and which + * recognizer to use. The default is en-us (US English), but you can + * set a different default to be used in your application here. + * + * @param string $language + */ + public function setLanguage($language) { + $this->_language = $language; + } + + /** + * Sends a prompt to the user and optionally waits for a response. + * + * The ask method allows for collecting input using either speech + * recognition or DTMF (also known as Touch Tone). You can either + * pass in a fully-formed Ask object or a string to use as the + * prompt and an array of parameters. + * + * @param string|Ask $ask + * @param array $params + * @see https://www.tropo.com/docs/webapi/ask.htm + */ + public function ask($ask, Array $params=NULL) { + if(!is_object($ask)) { + $p = array('as','event','voice','attempts', 'bargein', 'minConfidence', 'name', 'required', 'timeout', 'allowSignals'); + foreach ($p as $option) { + $$option = null; + if (is_array($params) && array_key_exists($option, $params)) { + $$option = $params[$option]; + } + } + $say[] = new Say($ask, $as, null, $voice); + if (is_array($event)) { + // If an event was passed in, add the events to the Ask + foreach ($event as $e => $val){ + $say[] = new Say($val, $as, $e, $voice); + } + } + $params["mode"] = isset($params["mode"]) ? $params["mode"] : null; + $params["dtmf"] = isset($params["dtmf"]) ? $params["dtmf"] : null; + $voice = isset($this->_voice) ? $this->_voice : null; + $choices = isset($params["choices"]) ? new Choices($params["choices"], $params["mode"], $params["terminator"]) : null; + $ask = new Ask($attempts, $bargein, $choices, $minConfidence, $name, $required, $say, $timeout, $voice, $allowSignals); + } + $this->ask = sprintf($ask); + } + + /** + * Places a call or sends an an IM, Twitter, or SMS message. To start a call, use the Session API to tell Tropo to launch your code. + * + * @param string|Call $call + * @param array $params + * @see https://www.tropo.com/docs/webapi/call.htm + */ + public function call($call, Array $params=NULL) { + if(!is_object($call)) { + $p = array('to', 'from', 'network', 'channel', 'answerOnMedia', 'timeout', 'headers', 'recording', 'allowSignals'); + foreach ($p as $option) { + $$option = null; + if (is_array($params) && array_key_exists($option, $params)) { + $$option = $params[$option]; + } + } + $call = new Call($call, $from, $network, $channel, $answerOnMedia, $timeout, $headers, $recording, $allowSignals); + } + $this->call = sprintf($call); + } + + /** + * This object allows multiple lines in separate sessions to be conferenced together so that the parties on each line can talk to each other simultaneously. + * This is a voice channel only feature. + * + * @param string|Conference $conference + * @param array $params + * @see https://www.tropo.com/docs/webapi/conference.htm + */ + public function conference($conference, Array $params=NULL) { + if(!is_object($conference)) { + $p = array('name', 'id', 'mute', 'on', 'playTones', 'required', 'terminator', 'allowSignals'); + foreach ($p as $option) { + $$option = null; + if (is_array($params) && array_key_exists($option, $params)) { + $$option = $params[$option]; + } + } + $conference = new Conference($name, $id, $mute, $on, $playTones, $required, $terminator, $allowSignals); + } + $this->conference = sprintf($conference); + } + + /** + * This function instructs Tropo to "hang-up" or disconnect the session associated with the current session. + * @see https://www.tropo.com/docs/webapi/hangup.htm + */ + public function hangup() { + $hangup = new Hangup(); + $this->hangup = sprintf($hangup); + } + + /** + * A shortcut method to create a session, say something, and hang up, all in one step. This is particularly useful for sending out a quick SMS or IM. + * + * @param string|Message $message + * @param array $params + * @see https://www.tropo.com/docs/webapi/message.htm + */ + public function message($message, Array $params=null) { + if(!is_object($message)) { + $say = new Say($message); + $to = $params["to"]; + $p = array('channel', 'network', 'from', 'voice', 'timeout', 'answerOnMedia','headers'); + foreach ($p as $option) { + $$option = null; + if (is_array($params) && array_key_exists($option, $params)) { + $$option = $params[$option]; + } + } + $message = new Message($say, $to, $channel, $network, $from, $voice, $timeout, $answerOnMedia, $headers); + } + $this->message = sprintf($message); + } + + /** + * Adds an event callback so that your application may be notified when a particular event occurs. + * Possible events are: "continue", "error", "incomplete" and "hangup". + * + * @param array $params + * @see https://www.tropo.com/docs/webapi/on.htm + */ + public function on($on) { + if (!is_object($on) && is_array($on)) { + $params = $on; + $say = (array_key_exists('say', $params)) ? new Say($params["say"]) : null; + $next = (array_key_exists('next', $params)) ? $params["next"] : null; + $on = new On($params["event"], $next, $say); + } + $this->on = sprintf($on); + } + + /** + * Plays a prompt (audio file or text to speech) and optionally waits for a response from the caller that is recorded. + * If collected, responses may be in the form of DTMF or speech recognition using a simple grammar format defined below. + * The record funtion is really an alias of the prompt function, but one which forces the record option to true regardless of how it is (or is not) initially set. + * At the conclusion of the recording, the audio file may be automatically sent to an external server via FTP or an HTTP POST/Multipart Form. + * If specified, the audio file may also be transcribed and the text returned to you via an email address or HTTP POST/Multipart Form. + * + * @param array|Record $record + * @see https://www.tropo.com/docs/webapi/record.htm + */ + public function record($record) { + if(!is_object($record) && is_array($record)) { + $params = $record; + $choices = isset($params["choices"]) ? new Choices($params["choices"]) : null; + $say = new Say($params["say"], $params["as"], null, $params["voice"]); + if (is_array($params['transcription'])) { + $p = array('url', 'id', 'emailFormat'); + foreach ($p as $option) { + $$option = null; + if (!is_array($params["transcription"]) || !array_key_exists($option, $params["transcription"])) { + $params["transcription"][$option] = null; + } + } + $transcription = new Transcription($params["transcription"]["url"],$params["transcription"]["id"],$params["transcription"]["emailFormat"]); + } else { + $transcription = $params["transcription"]; + } + $p = array('attempts', 'allowSignals', 'bargein', 'beep', 'format', 'maxTime', 'maxSilence', 'method', 'password', 'required', 'timeout', 'username', 'url', 'voice'); + foreach ($p as $option) { + $$option = null; + if (is_array($params) && array_key_exists($option, $params)) { + $$option = $params[$option]; + } + } + $record = new Record($attempts, $allowSignals, $bargein, $beep, $choices, $format, $maxSilence, $maxTime, $method, $password, $required, $say, $timeout, $transcription, $username, $url, $voice); + } + $this->record = sprintf($record); + } + + /** + * The redirect function forwards an incoming call to another destination / phone number before answering it. + * The redirect function must be called before answer is called; redirect expects that a call be in the ringing or answering state. + * Use transfer when working with active answered calls. + * + * @param string|Redirect $redirect + * @param array $params + * @see https://www.tropo.com/docs/webapi/redirect.htm + */ + public function redirect($redirect, Array $params=NULL) { + if(!is_object($redirect)) { + $to = isset($params["to"]) ? $params["to"]: null; + $from = isset($params["from"]) ? $params["from"] : null; + $redirect = new Redirect($to, $from); + } + $this->redirect = sprintf($redirect); + } + + /** + * Allows Tropo applications to reject incoming sessions before they are answered. + * For example, an application could inspect the callerID variable to determine if the user is known, and then use the reject call accordingly. + * + * @see https://www.tropo.com/docs/webapi/reject.htm + * + */ + public function reject() { + $reject = new Reject(); + $this->reject = sprintf($reject); + } + + /** + * When the current session is a voice channel this key will either play a message or an audio file from a URL. + * In the case of an text channel it will send the text back to the user via i nstant messaging or SMS. + * + * @param string|Say $say + * @param array $params + * @see https://www.tropo.com/docs/webapi/say.htm + */ + public function say($say, Array $params=NULL) { + if(!is_object($say)) { + $p = array('as', 'format', 'event','voice', 'allowSignals'); + $value = $say; + foreach ($p as $option) { + $$option = null; + if (is_array($params) && array_key_exists($option, $params)) { + $$option = $params[$option]; + } + } + $voice = isset($voice) ? $voice : $this->_voice; + $say = new Say($value, $as, $event, $voice, $allowSignals); + } + $this->say = array(sprintf($say)); + } + + /** + * Allows Tropo applications to begin recording the current session. + * The resulting recording may then be sent via FTP or an HTTP POST/Multipart Form. + * + * @param array|StartRecording $startRecording + * @see https://www.tropo.com/docs/webapi/startrecording.htm + */ + public function startRecording($startRecording) { + if(!is_object($startRecording) && is_array($startRecording)) { + $params = $startRecording; + $p = array('format', 'method', 'password', 'url', 'username'); + foreach ($p as $option) { + $$option = null; + if (is_array($params) && array_key_exists($option, $params)) { + $$option = $params[$option]; + } + } + $startRecording = new StartRecording($format, $method, $password, $url, $username); + } + $this->startRecording = sprintf($startRecording); + } + + /** + * Stops a previously started recording. + * + * @see https://www.tropo.com/docs/webapi/stoprecording.htm + */ + public function stopRecording() { + $stopRecording = new stopRecording(); + $this->stopRecording = sprintf($stopRecording); + } + + /** + * Transfers an already answered call to another destination / phone number. + * Call may be transferred to another phone number or SIP address, which is set through the "to" parameter and is in URL format. + * + * @param string|Transfer $transfer + * @param array $params + * @see https://www.tropo.com/docs/webapi/transfer.htm + */ + public function transfer($transfer, Array $params=NULL) { + if(!is_object($transfer)) { + $choices = isset($params["choices"]) ? new Choices($params["choices"]) : null; + $to = isset($params["to"]) ? $params["to"] : $transfer; + $p = array('answerOnMedia', 'ringRepeat', 'timeout', 'from', 'on', 'allowSignals'); + foreach ($p as $option) { + $$option = null; + if (is_array($params) && array_key_exists($option, $params)) { + $$option = $params[$option]; + } + } + $transfer = new Transfer($to, $answerOnMedia, $choices, $from, $ringRepeat, $timeout, $on, $allowSignals); + } + $this->transfer = sprintf($transfer); + } + + /** + * Launches a new session with the Tropo Session API. + * (Pass through to SessionAPI class.) + * + * @param string $token Your outbound session token from Tropo + * @param array $params An array of key value pairs that will be added as query string parameters + * @return bool True if the session was launched successfully + */ + public function createSession($token, Array $params=NULL) { + try { + $session = new SessionAPI(); + $result = $session->createSession($token, $params); + return $result; + } + // If an exception occurs, wrap it in a TropoException and rethrow. + catch (Exception $ex) { + throw new TropoException($ex->getMessage(), $ex->getCode()); + } + } + + public function sendEvent($session_id, $value) { + try { + $event = new EventAPI(); + $result = $event->sendEvent($session_id, $value); + return $result; + } + catch (Exception $ex) { + throw new TropoException($ex->getMessage(), $ex->getCode()); + } + } + + /** + * Creates a new Tropo Application + * (Pass through to ProvisioningAPI class). + * + * @param string $userid + * @param string $password + * @param array $params + * @return string JSON + */ + public function createApplication($userid, $password, Array $params) { + $p = array('href', 'name', 'voiceUrl', 'messagingUrl', 'platform', 'partition'); + foreach ($p as $property) { + $$property = null; + if (is_array($params) && array_key_exists($property, $params)) { + $$property = $params[$property]; + } + } + try { + $provision = new ProvisioningAPI($userid, $password); + $result = $provision->createApplication($href, $name, $voiceUrl, $messagingUrl, $platform, $partition); + return $result; + } + // If an exception occurs, wrap it in a TropoException and rethrow. + catch (Exception $ex) { + throw new TropoException($ex->getMessage(), $ex->getCode()); + } + } + + /** + * Add/Update an address (phone number, IM address or token) for an existing Tropo application. + * (Pass through to ProvisioningAPI class). + * + * @param string $userid + * @param string $password + * @param string $applicationID + * @param array $params + * @return string JSON + */ + public function updateApplicationAddress($userid, $passwd, $applicationID, Array $params) { + $p = array('type', 'prefix', 'number', 'city', 'state', 'channel', 'username', 'password', 'token'); + foreach ($p as $property) { + $$property = null; + if (is_array($params) && array_key_exists($property, $params)) { + $$property = $params[$property]; + } + } + try { + $provision = new ProvisioningAPI($userid, $passwd); + $result = $provision->updateApplicationAddress($applicationID, $type, $prefix, $number, $city, $state, $channel, $username, $password, $token); + return $result; + } + // If an exception occurs, wrap it in a TropoException and rethrow. + catch (Exception $ex) { + throw new TropoException($ex->getMessage(), $ex->getCode()); + } + } + + /** + * Update a property (name, URL, platform, etc.) for an existing Tropo application. + * (Pass through to ProvisioningAPI class). + * + * @param string $userid + * @param string $password + * @param string $applicationID + * @param array $params + * @return string JSON + */ + public function updateApplicationProperty($userid, $password, $applicationID, Array $params) { + $p = array('href', 'name', 'voiceUrl', 'messagingUrl', 'platform', 'partition'); + foreach ($p as $property) { + $$property = null; + if (is_array($params) && array_key_exists($property, $params)) { + $$property = $params[$property]; + } + } + try { + $provision = new ProvisioningAPI($userid, $password); + $result = $provision->updateApplicationProperty($applicationID, $href, $name, $voiceUrl, $messagingUrl, $platform, $partition); + return $result; + } + // If an exception occurs, wrap it in a TropoException and rethrow. + catch (Exception $ex) { + throw new TropoException($ex->getMessage(), $ex->getCode()); + } + } + + /** + * Delete an existing Tropo application. + * (Pass through to ProvisioningAPI class). + * + * @param string $userid + * @param string $password + * @param string $applicationID + * @return string JSON + */ + public function deleteApplication($userid, $password, $applicationID) { + $provision = new ProvisioningAPI($userid, $password); + return $provision->deleteApplication($applicationID); + } + + /** + * Delete an address for an existing Tropo application. + * (Pass through to ProvisioningAPI class). + * + * @param string $userid + * @param string $password + * @param string $applicationID + * @param string $number + * @return string JSON + */ + public function deleteApplicationAddress($userid, $password, $applicationID, $addresstype, $address) { + $provision = new ProvisioningAPI($userid, $password); + return $provision->deleteApplicationAddress($applicationID, $addresstype, $address); + } + + /** + * View a list of Tropo applications. + * (Pass through to ProvisioningAPI class). + * + * @param string $userid + * @param string $password + * @return string JSON + */ + public function viewApplications($userid, $password) { + $provision = new ProvisioningAPI($userid, $password); + return $provision->viewApplications(); + } + + /** + * View the details of a specific Tropo application. + * (Pass through to ProvisioningAPI class). + * + * @param string $userid + * @param string $password + * @param string $applicationID + * @return string JSON + */ + public function viewSpecificApplication($userid, $password, $applicationID) { + $provision = new ProvisioningAPI($userid, $password); + return $provision->viewSpecificApplication($applicationID); + } + + /** + * View the addresses for a specific Tropo application. + * (Pass through to ProvisioningAPI class). + * + * @param string $userid + * @param string $password + * @param string $applicationID + * @return string JSON + */ + public function viewAddresses($userid, $password, $applicationID) { + $provision = new ProvisioningAPI($userid, $password); + return $provision->viewAddresses($applicationID); + } + + /** + * View a list of available exchanges for assigning a number to a Tropo application. + * (Pass through to ProvisioningAPI class). + * + * @param string $userid + * @param string $password + * @return string JSON + */ + public function viewExchanges($userid, $password) { + $provision = new ProvisioningAPI($userid, $password); + return $provision->viewExchanges(); + } + + /** + * Renders the Tropo object as JSON. + * + */ + public function renderJSON() { + header('Content-type: application/json'); + echo $this; + } + + /** + * Allows undefined methods to be called. + * This method is invloked by Tropo class methods to add action items to the Tropo array. + * + * @param string $name + * @param mixed $value + * @access private + */ + public function __set($name, $value) { + array_push($this->tropo, array($name => $value)); + } + + /** + * Controls how JSON structure for the Tropo object is rendered. + * + * @return string + * @access private + */ + public function __toString() { + // Remove voice and language so they do not appear in the rednered JSON. + unset($this->_voice); + unset($this->_language); + + // Call the unescapeJSON() method in the parent class. + return parent::unescapeJSON(json_encode($this)); + } +} + +/** + * Base class for Tropo class and indvidual Tropo action classes. + * Derived classes must implement both a constructor and __toString() function. + * @package TropoPHP_Support + * @abstract BaseClass + */ + +abstract class BaseClass { + + /** + * Class constructor + * @abstract __construct() + */ + abstract public function __construct(); + + /** + * toString Function + * @abstract __toString() + */ + abstract public function __toString(); + + /** + * Allows derived classes to set Undeclared properties. + * + * @param mixed $attribute + * @param mixed $value + */ + public function __set($attribute, $value) { + $this->$attribute= $value; + } + + /** + * Removes escape characters from a JSON string. + * + * @param string $json + * @return string + */ + public function unescapeJSON($json) { + return str_replace(array("\\", "\"{", "}\""), array("", "{", "}"), $json); + } +} + +/** + * Base class for empty actions. + * @package TropoPHP_Support + * + */ +class EmptyBaseClass { + + final public function __toString() { + return json_encode(null); + } +} + + + +/** + * Action classes. Each specific object represents a specific Tropo action. + * + */ + +/** + * Sends a prompt to the user and optionally waits for a response. + * @package TropoPHP_Support + * + */ +class Ask extends BaseClass { + + private $_attempts; + private $_bargein; + private $_choices; + private $_minConfidence; + private $_name; + private $_required; + private $_say; + private $_timeout; + private $_voice; + private $_allowSignals; + + /** + * Class constructor + * + * @param int $attempts + * @param boolean $bargein + * @param Choices $choices + * @param float $minConfidence + * @param string $name + * @param boolean $required + * @param Say $say + * @param int $timeout + * @param string $voice + * @param string|array $allowSignals + */ + public function __construct($attempts=NULL, $bargein=NULL, Choices $choices=NULL, $minConfidence=NULL, $name=NULL, $required=NULL, $say=NULL, $timeout=NULL, $voice=NULL, $allowSignals=NULL) { + $this->_attempts = $attempts; + $this->_bargein = $bargein; + $this->_choices = isset($choices) ? sprintf($choices) : null ; + $this->_minConfidence = $minConfidence; + $this->_name = $name; + $this->_required = $required; + $this->_say = isset($say) ? $say : null; + $this->_timeout = $timeout; + $this->_voice = $voice; + $this->_allowSignals = $allowSignals; + } + + /** + * Renders object in JSON format. + * + */ + public function __toString() { + if(isset($this->_attempts)) { $this->attempts = $this->_attempts; } + if(isset($this->_bargein)) { $this->bargein = $this->_bargein; } + if(isset($this->_choices)) { $this->choices = $this->_choices; } + if(isset($this->_minConfidence)) { $this->minConfidence = $this->_minConfidence; } + if(isset($this->_name)) { $this->name = $this->_name; } + if(isset($this->_required)) { $this->required = $this->_required; } + if(isset($this->_say)) { $this->say = $this->_say; } + if (is_array($this->_say)) { + foreach ($this->_say as $k => $v) { + $this->_say[$k] = sprintf($v); + } + } + if(isset($this->_timeout)) { $this->timeout = $this->_timeout; } + if(isset($this->_voice)) { $this->voice = $this->_voice; } + if(isset($this->_allowSignals)) { $this->allowSignals = $this->_allowSignals; } + return $this->unescapeJSON(json_encode($this)); + } + + /** + * Adds an additional Say to the Ask + * + * Used to add events such as a prompt to say on timeout or nomatch + * + * @param Say $say A say object + */ + public function addEvent(Say $say) { + $this->_say[] = $say; + } +} + +/** + * This object allows Tropo to make an outbound call. The call can be over voice or one + * of the text channels. + * @package TropoPHP_Support + * + */ +class Call extends BaseClass { + + private $_to; + private $_from; + private $_network; + private $_channel; + private $_answerOnMedia; + private $_timeout; + private $_headers; + private $_recording; + private $_allowSignals; + + /** + * Class constructor + * + * @param string $to + * @param string $from + * @param string $network + * @param string $channel + * @param boolean $answerOnMedia + * @param int $timeout + * @param array $headers + * @param StartRecording $recording + * @param string|array $allowSignals + */ + public function __construct($to, $from=NULL, $network=NULL, $channel=NULL, $answerOnMedia=NULL, $timeout=NULL, Array $headers=NULL, StartRecording $recording=NULL, $allowSignals=NULL) { + $this->_to = $to; + $this->_from = $from; + $this->_network = $network; + $this->_channel = $channel; + $this->_answerOnMedia = $answerOnMedia; + $this->_timeout = $timeout; + $this->_headers = $headers; + $this->_recording = isset($recording) ? sprintf($recording) : null ; + $this->_allowSignals = $allowSignals; + } + + /** + * Renders object in JSON format. + * + */ + public function __toString() { + $this->to = $this->_to; + if(isset($this->_from)) { $this->from = $this->_from; } + if(isset($this->_network)) { $this->network = $this->_network; } + if(isset($this->_channel)) { $this->channel = $this->_channel; } + if(isset($this->_timeout)) { $this->timeout = $this->_timeout; } + if(isset($this->_answerOnMedia)) { $this->answerOnMedia = $this->_answerOnMedia; } + if(count($this->_headers)) { $this->headers = $this->_headers; } + if(isset($this->_recording)) { $this->recording = $this->_recording; } + if(isset($this->_allowSignals)) { $this->allowSignals = $this->_allowSignals; } + return $this->unescapeJSON(json_encode($this)); + } +} + +/** + * Defines the input to be collected from the user. + * @package TropoPHP_Support + */ +class Choices extends BaseClass { + + private $_value; + private $_mode; + private $_termChar; + + /** + * Class constructor + * + * @param string $value + * @param string $mode + * @param string $termChar + */ + public function __construct($value, $mode=NULL, $termChar=NULL) { + $this->_value = $value; + $this->_mode = $mode; + $this->_termChar = $termChar; + } + + /** + * Renders object in JSON format. + * + */ + public function __toString() { + $this->value = $this->_value; + if(isset($this->_mode)) { $this->mode = $this->_mode; } + if(isset($this->_termChar)) { $this->terminator = $this->_termChar; } + return $this->unescapeJSON(json_encode($this)); + } +} + +/** + * This object allows multiple lines in separate sessions to be conferenced together so that + * the parties on each line can talk to each other simultaneously. + * This is a voice channel only feature. + * + * TODO: Conference object should support multiple event handlers (e.g. join and leave). + * @package TropoPHP_Support + * + */ +class Conference extends BaseClass { + + private $_id; + private $_mute; + private $_name; + private $_on; + private $_playTones; + private $_required; + private $_terminator; + private $_allowSignals; + + + /** + * Class constructor + * + * @param int $id + * @param boolean $mute + * @param string $name + * @param On $on + * @param boolean $playTones + * @param boolean $required + * @param string $terminator + * @param string|array $allowSignals + */ + public function __construct($name, $id=NULL, $mute=NULL, On $on=NULL, $playTones=NULL, $required=NULL, $terminator=NULL, $allowSignals=NULL) { + $this->_name = $name; + $this->_id = $id; + $this->_mute = $mute; + $this->_on = isset($on) ? sprintf($on) : null; + $this->_playTones = $playTones; + $this->_required = $required; + $this->_terminator = $terminator; + $this->_allowSignals = $allowSignals; + } + + /** + * Renders object in JSON format. + * + */ + public function __toString() { + $this->name = $this->_name; + if(isset($this->_id)) { $this->id = $this->_id; } + if(isset($this->_mute)) { $this->mute = $this->_mute; } + if(isset($this->_on)) { $this->on = $this->_on; } + if(isset($this->_playTones)) { $this->playTones = $this->_playTones; } + if(isset($this->_required)) { $this->required = $this->_required; } + if(isset($this->_terminator)) { $this->terminator = $this->_terminator; } + if(isset($this->_allowSignals)) { $this->allowSignals = $this->_allowSignals; } + return $this->unescapeJSON(json_encode($this)); + } +} + +/** + * This function instructs Tropo to "hang-up" or disconnect the session associated with the current session. + * @package TropoPHP_Support + * + */ +class Hangup extends EmptyBaseClass { } + +/** + * This function instructs Tropo to send a message. + * @package TropoPHP_Support + * + */ +class Message extends BaseClass { + + private $_say; + private $_to; + private $_channel; + private $_network; + private $_from; + private $_voice; + private $_timeout; + private $_answerOnMedia; + private $_headers; + + /** + * Class constructor + * + * @param Say $say + * @param string $to + * @param string $channel + * @param string $network + * @param string $from + * @param string $voice + * @param integer $timeout + * @param boolean $answerOnMedia + * @param array $headers + */ + public function __construct(Say $say, $to, $channel=null, $network=null, $from=null, $voice=null, $timeout=null, $answerOnMedia=null, Array $headers=null) { + $this->_say = isset($say) ? sprintf($say) : null ; + $this->_to = $to; + $this->_channel = $channel; + $this->_network = $network; + $this->_from = $from; + $this->_voice = $voice; + $this->_timeout = $timeout; + $this->_answerOnMedia = $answerOnMedia; + $this->_headers = $headers; + } + + /** + * Renders object in JSON format. + * + */ + public function __toString() { + $this->say = $this->_say; + $this->to = $this->_to; + if(isset($this->_channel)) { $this->channel = $this->_channel; } + if(isset($this->_network)) { $this->network = $this->_network; } + if(isset($this->_from)) { $this->from = $this->_from; } + if(isset($this->_voice)) { $this->voice = $this->_voice; } + if(isset($this->_timeout)) { $this->timeout = $this->_timeout; } + if(isset($this->_answerOnMedia)) { $this->answerOnMedia = $this->_answerOnMedia; } + if(count($this->_headers)) { $this->headers = $this->_headers; } + return $this->unescapeJSON(json_encode($this)); + } +} + +/** + * Adds an event callback so that your application may be notified when a particular event occurs. + * @package TropoPHP_Support + * + */ +class On extends BaseClass { + + private $_event; + private $_next; + private $_say; + + /** + * Class constructor + * + * @param string $event + * @param string $next + * @param Say $say + */ + public function __construct($event=NULL, $next=NULL, Say $say=NULL) { + $this->_event = $event; + $this->_next = $next; + $this->_say = isset($say) ? sprintf($say) : null ; + } + + /** + * Renders object in JSON format. + * + */ + public function __toString() { + if(isset($this->_event)) { $this->event = $this->_event; } + if(isset($this->_next)) { $this->next = $this->_next; } + if(isset($this->_say)) { $this->say = $this->_say; } + return $this->unescapeJSON(json_encode($this)); + } +} + +/** + * Plays a prompt (audio file or text to speech) and optionally waits for a response from the caller that is recorded. + * @package TropoPHP_Support + * + */ +class Record extends BaseClass { + + private $_attempts; + private $_allowSignals; + private $_bargein; + private $_beep; + private $_choices; + private $_format; + private $_maxSilence; + private $_maxTime; + private $_method; + private $_password; + private $_required; + private $_say; + private $_timeout; + private $_transcription; + private $_username; + private $_url; + private $_voice; + + + /** + * Class constructor + * + * @param int $attempts + * @param string|array $allowSignals + * @param boolean $bargein + * @param boolean $beep + * @param Choices $choices + * @param string $format + * @param int $maxSilence + * @param string $method + * @param string $password + * @param boolean $required + * @param Say $say + * @param int $timeout + * @param string $username + * @param string $url + * @param string $voice + */ + public function __construct($attempts=NULL, $allowSignals=NULL, $bargein=NULL, $beep=NULL, Choices $choices=NULL, $format=NULL, $maxSilence=NULL, $maxTime=NULL, $method=NULL, $password=NULL, $required=NULL, $say=NULL, $timeout=NULL, Transcription $transcription=NULL, $username=NULL, $url=NULL, $voice=NULL) { + $this->_attempts = $attempts; + $this->_allowSignals = $allowSignals; + $this->_bargein = $bargein; + $this->_beep = $beep; + $this->_choices = isset($choices) ? sprintf($choices) : null; + $this->_format = $format; + $this->_maxSilence = $maxSilence; + $this->_maxTime = $maxTime; + $this->_method = $method; + $this->_password = $password; + if (!is_object($say)) { + $say = new Say($say); + } + $this->_say = isset($say) ? sprintf($say) : null; + $this->_timeout = $timeout; + $this->_transcription = isset($transcription) ? sprintf($transcription) : null; + $this->_username = $username; + $this->_url = $url; + $this->_voice = $voice; + } + + /** + * Renders object in JSON format. + * + */ + public function __toString() { + if(isset($this->_attempts)) { $this->attempts = $this->_attempts; } + if(isset($this->_allowSignals)) { $this->allowSignals = $this->_allowSignals; } + if(isset($this->_bargein)) { $this->bargein = $this->_bargein; } + if(isset($this->_beep)) { $this->beep = $this->_beep; } + if(isset($this->_choices)) { $this->choices = $this->_choices; } + if(isset($this->_format)) { $this->format = $this->_format; } + if(isset($this->_maxSilence)) { $this->maxSilence = $this->_maxSilence; } + if(isset($this->_maxTime)) { $this->maxTime = $this->_maxTime; } + if(isset($this->_method)) { $this->method = $this->_method; } + if(isset($this->_password)) { $this->password = $this->_password; } + if(isset($this->_say)) { $this->say = $this->_say; } + if(isset($this->_timeout)) { $this->timeout = $this->_timeout; } + if(isset($this->_transcription)) { $this->transcription = $this->_transcription; } + if(isset($this->_username)) { $this->username = $this->_username; } + if(isset($this->_url)) { $this->url = $this->_url; } + if(isset($this->_voice)) { $this->voice = $this->_voice; } + return $this->unescapeJSON(json_encode($this)); + } +} + +/** + * The redirect function forwards an incoming call to another destination / phone number before answering it. + * @package TropoPHP_Support + * + */ +class Redirect extends BaseClass { + + private $_to; + private $_from; + + /** + * Class constructor + * + * @param Endpoint $to + * @param Endpoint $from + */ + public function __construct($to=NULL, $from=NULL) { + $this->_to = sprintf($to); + $this->_from = isset($from) ? sprintf($from) : null; + } + + /** + * Renders object in JSON format. + * + */ + public function __toString() { + $this->to = $this->_to; + if(isset($this->_from)) { $this->from = $this->_from; } + return $this->unescapeJSON(json_encode($this)); + } +} + +/** + * Allows Tropo applications to reject incoming sessions before they are answered. + * @package TropoPHP_Support + * + */ +class Reject extends EmptyBaseClass { } + +/** + * Returned anytime a request is made to the Tropo Web API. + * @package TropoPHP + * + */ +class Result { + + private $_sessionId; + private $_callId; + private $_state; + private $_sessionDuration; + private $_sequence; + private $_complete; + private $_error; + private $_actions; + private $_name; + private $_attempts; + private $_disposition; + private $_confidence; + private $_interpretation; + private $_concept; + private $_utterance; + private $_value; + + /** + * Class constructor + * + * @param string $json + */ + public function __construct($json=NULL) { + if(empty($json)) { + $json = file_get_contents("php://input"); + // if $json is still empty, there was nothing in + // the POST so throw an exception + if(empty($json)) { + throw new TropoException('No JSON available.'); + } + } + $result = json_decode($json); + if (!is_object($result) || !property_exists($result, "result")) { + throw new TropoException('Not a result object.'); + } + $this->_sessionId = $result->result->sessionId; + $this->_callId = $result->result->callId; + $this->_state = $result->result->state; + $this->_sessionDuration = $result->result->sessionDuration; + $this->_sequence = $result->result->sequence; + $this->_complete = $result->result->complete; + $this->_error = $result->result->error; + $this->_actions = $result->result->actions; + $this->_name = $result->result->actions->name; + $this->_attempts = $result->result->actions->attempts; + $this->_disposition = $result->result->actions->disposition; + $this->_confidence = $result->result->actions->confidence; + $this->_interpretation = $result->result->actions->interpretation; + $this->_utterance = $result->result->actions->utterance; + $this->_value = $result->result->actions->value; + $this->_concept = $result->result->concept->value; + + } + + function getSessionId() { + return $this->_sessionId; + } + + function getCallId() { + return $this->_callId; + } + + function getState() { + return $this->_state; + } + + function getSessionDuration() { + return $this->_sessionDuration; + } + + function getSequence() { + return $this->_sequence; + } + + function isComplete() { + return (bool) $this->_complete; + } + + function getError() { + return $this->_error; + } + + function getActions() { + return $this->_actions; + } + + function getName() { + return $this->_name; + } + + function getAttempts() { + return $this->_attempts; + } + + function getDisposition() { + return $this->_disposition; + } + + function getConfidence() { + return $this->_confidence; + } + + function getInterpretation() { + return $this->_interpretation; + } + + function getConcept() { + return $this->_concept; + } + + function getUtterance() { + return $this->_utterance; + } + + function getValue() { + return $this->_value; + } +} + +/** + * When the current session is a voice channel this key will either play a message or an audio file from a URL. + * In the case of an text channel it will send the text back to the user via instant messaging or SMS. + * @package TropoPHP_Support + * + */ +class Say extends BaseClass { + + private $_value; + private $_as; + private $_event; + private $_format; + private $_voice; + private $_allowSignals; + + /** + * Class constructor + * + * @param string $value + * @param SayAs $as + * @param string $event + * @param string $voice + * @param string|array $allowSignals + */ + public function __construct($value, $as=NULL, $event=NULL, $voice=NULL, $allowSignals=NULL) { + $this->_value = $value; + $this->_as = $as; + $this->_event = $event; + $this->_voice = $voice; + $this->_allowSignals = $allowSignals; + } + + /** + * Renders object in JSON format. + * + */ + public function __toString() { + if(isset($this->_event)) { $this->event = $this->_event; } + $this->value = $this->_value; + if(isset($this->_as)) { $this->as = $this->_as; } + if(isset($this->_voice)) { $this->voice = $this->_voice; } + if(isset($this->_allowSignals)) { $this->allowSignals = $this->_allowSignals; } + return $this->unescapeJSON(json_encode($this)); + } +} + +/** + * The payload sent as an HTTP POST to the web application when a new session arrives. + * + * TODO: Consider using associative array for To and From. + * TODO: Need to break out headers into a more accessible data structure. + * @package TropoPHP + */ +class Session { + + private $_id; + private $_accountID; + private $_timestamp; + private $_userType; + private $_initialText; + private $_to; + private $_from; + private $_headers; + private $_parameters; + + /** + * Class constructor + * + * @param string $json + */ + public function __construct($json=NULL) { + if(empty($json)) { + $json = file_get_contents("php://input"); + // if $json is still empty, there was nothing in + // the POST so throw exception + if(empty($json)) { + throw new TropoException('No JSON available.', 1); + } + } + $session = json_decode($json); + if (!is_object($session) || !property_exists($session, "session")) { + throw new TropoException('Not a session object.', 2); + } + $this->_id = $session->session->id; + $this->_accountId = $session->session->accountId; + $this->_timestamp = $session->session->timestamp; + $this->_userType = $session->session->userType; + $this->_initialText = $session->session->initialText; + $this->_to = array("id" => $session->session->to->id, "channel" => $session->session->to->channel, "name" => $session->session->to->name, "network" => $session->session->to->network); + $this->_from = array("id" => $session->session->from->id, "channel" => $session->session->from->channel, "name" => $session->session->from->name, "network" => $session->session->from->network); + $this->_headers = self::setHeaders($session->session->headers); + $this->_parameters = property_exists($session->session, 'parameters') ? (Array) $session->session->parameters : null; + } + + public function getId() { + return $this->_id; + } + + public function getAccountID() { + return $this->_accountId; + } + + public function getTimeStamp() { + return $this->_timestamp; + } + public function getUserType() { + return $this->_userType; + } + + public function getInitialText() { + return $this->_initialText; + } + + public function getTo() { + return $this->_to; + } + + public function getFrom() { + return $this->_from; + } + + public function getHeaders() { + return $this->_headers; + } + + /** + * Returns the query string parameters for the session api + * + * If an argument is provided, a string containing the value of a + * query string variable matching that string is returned or null + * if there is no match. If no argument is argument is provided, + * an array is returned with all query string variables or an empty + * array if there are no query string variables. + * + * @param string $name A specific parameter to return + * @return string|array $param + */ + public function getParameters($name = null) { + if (isset($name)) { + if (!is_array($this->_parameters)) { + // We've asked for a specific param, not there's no params set + // return a null. + return null; + } + if (isset($this->_parameters[$name])) { + return $this->_parameters[$name]; + } else { + return null; + } + } else { + // If the parameters field doesn't exist or isn't an array + // then return an empty array() + if (!is_array($this->_parameters)) { + return array(); + } + return $this->_parameters; + } + } + + public function setHeaders($headers) { + $formattedHeaders = new Headers(); + // headers don't exist on outboud calls + // so only do this if there are headers + if (is_array($headers)) { + foreach($headers as $name => $value) { + $formattedHeaders->$name = $value; + } + } + return $formattedHeaders; + } +} + +/** + * Allows Tropo applications to begin recording the current session. + * The resulting recording may then be sent via FTP or an HTTP POST/Multipart Form. + * @package TropoPHP_Support + * + */ +class StartRecording extends BaseClass { + + private $_name; + private $_format; + private $_method; + private $_password; + private $_url; + private $_username; + + /** + * Class constructor + * + * @param string $name + * @param string $format + * @param string $method + * @param string $password + * @param string $url + * @param string $username + */ + public function __construct($format=NULL, $method=NULL, $password=NULL, $url=NULL, $username=NULL) { + $this->_format = $format; + $this->_method = $method; + $this->_password = $password; + $this->_url = $url; + $this->_username = $username; + } + + /** + * Renders object in JSON format. + * + */ + public function __toString() { + if(isset($this->_format)) { $this->format = $this->_format; } + if(isset($this->_method)) { $this->method = $this->_method; } + if(isset($this->_password)) { $this->password = $this->_password; } + if(isset($this->_url)) { $this->url = $this->_url; } + if(isset($this->_username)) { $this->username = $this->_username; } + return $this->unescapeJSON(json_encode($this)); + } +} + +/** + * Stop an already started recording. + * @package TropoPHP_Support + * + */ +class StopRecording extends EmptyBaseClass { } + +/** + * Transcribes spoken text. + * @package TropoPHP_Support + * + */ +class Transcription extends BaseClass { + + private $_url; + private $_id; + private $_emailFormat; + + /** + * Class constructor + * + * @param string $url + * @param string $id + * @param string $emailFormat + */ + public function __construct($url, $id=NULL, $emailFormat=NULL) { + $this->_url = $url; + $this->_id = $id; + $this->_emailFormat = $emailFormat; + } + + /** + * Renders object in JSON format. + * + */ + public function __toString() { + if(isset($this->_id)) { $this->id = $this->_id; } + if(isset($this->_url)) { $this->url = $this->_url; } + if(isset($this->_emailFormat)) { $this->emailFormat = $this->_emailFormat; } + return $this->unescapeJSON(json_encode($this)); + } +} + +/** + * Transfers an already answered call to another destination / phone number. + * @package TropoPHP_Support + * + */ +class Transfer extends BaseClass { + + private $_answerOnMedia; + private $_choices; + private $_from; + private $_on; + private $_ringRepeat; + private $_timeout; + private $_to; + private $_allowSignals; + + /** + * Class constructor + * + * @param string $to + * @param boolean $answerOnMedia + * @param Choices $choices + * @param Endpoint $from + * @param On $on + * @param int $ringRepeat + * @param int $timeout + * @param string|array $allowSignals + */ + public function __construct($to, $answerOnMedia=NULL, Choices $choices=NULL, $from=NULL, $ringRepeat=NULL, $timeout=NULL, $on=NULL, $allowSignals=NULL) { + $this->_to = $to; + $this->_answerOnMedia = $answerOnMedia; + $this->_choices = isset($choices) ? sprintf($choices) : null; + $this->_from = $from; + $this->_ringRepeat = $ringRepeat; + $this->_timeout = $timeout; + $this->_on = isset($on) ? sprintf($on) : null; + $this->_allowSignals = $allowSignals; + } + + /** + * Renders object in JSON format. + * + */ + public function __toString() { + $this->to = $this->_to; + if(isset($this->_answerOnMedia)) { $this->answerOnMedia = $this->_answerOnMedia; } + if(isset($this->_choices)) { $this->choices = $this->_choices; } + if(isset($this->_from)) { $this->from = $this->_from; } + if(isset($this->_ringRepeat)) { $this->ringRepeat = $this->_ringRepeat; } + if(isset($this->_timeout)) { $this->timeout = $this->_timeout; } + if(isset($this->_on)) { $this->on = $this->_on; } + if(isset($this->_allowSignals)) { $this->allowSignals = $this->_allowSignals; } + return $this->unescapeJSON(json_encode($this)); + } +} + +/** + * Defnies an endoint for transfer and redirects. + * @package TropoPHP_Support + * + */ +class Endpoint extends BaseClass { + + private $_id; + private $_channel; + private $_name = 'unknown'; + private $_network; + + /** + * Class constructor + * + * @param string $id + * @param string $channel + * @param string $name + * @param string $network + */ + public function __construct($id, $channel=NULL, $name=NULL, $network=NULL) { + + $this->_id = $id; + $this->_channel = $channel; + $this->_name = $name; + $this->_network = $network; + } + + /** + * Renders object in JSON format. + * + */ + public function __toString() { + + if(isset($this->_id)) { $this->id = $this->_id; } + if(isset($this->_channel)) { $this->channel = $this->_channel; } + if(isset($this->_name)) { $this->name = $this->_name; } + if(isset($this->_network)) { $this->network = $this->_network; } + return $this->unescapeJSON(json_encode($this)); + } +} + +/** + * A helper class for wrapping exceptions. Can be modified for custom excpetion handling. + * + */ +class TropoException extends Exception { } + +/** + * Date Helper class. + * @package TropoPHP_Support + */ +class Date { + public static $monthDayYear = "mdy"; + public static $dayMonthYear = "dmy"; + public static $yearMonthDay = "ymd"; + public static $yearMonth = "ym"; + public static $monthYear = "my"; + public static $monthDay = "md"; + public static $year = "y"; + public static $month = "m"; + public static $day = "d"; +} + +/** + * Duration Helper class. + * @package TropoPHP_Support + */ +class Duration { + public static $hoursMinutesSeconds = "hms"; + public static $hoursMinutes = "hm"; + public static $hours = "h"; + public static $minutes = "m"; + public static $seconds = "s"; +} + +/** + * Event Helper class. + * @package TropoPHP_Support + */ +class Event { + + public static $continue = 'continue'; + public static $incomplete = 'incomplete'; + public static $error = 'error'; + public static $hangup = 'hangup'; + public static $join = 'join'; + public static $leave = 'leave'; + public static $ring = 'ring'; +} + +/** + * Format Helper class. + * @package TropoPHP_Support + */ +class Format { + public $date; + public $duration; + public static $ordinal = "ordinal"; + public static $digits = "digits"; + + public function __construct($date=NULL, $duration=NULL) { + $this->date = $date; + $this->duration = $duration; + } +} + +/** + * SayAs Helper class. + * @package TropoPHP_Support + */ +class SayAs { + public static $date = "DATE"; + public static $digits = "DIGITS"; + public static $number = "NUMBER"; +} + +/** + * Network Helper class. + * @package TropoPHP_Support + */ +class Network { + public static $pstn = "PSTN"; + public static $voip = "VOIP"; + public static $aim = "AIM"; + public static $gtalk = "GTALK"; + public static $jabber = "JABBER"; + public static $msn = "MSN"; + public static $sms = "SMS"; + public static $yahoo = "YAHOO"; + public static $twitter = "TWITTER"; +} + +/** + * Channel Helper class. + * @package TropoPHP_Support + */ +class Channel { + public static $voice = "VOICE"; + public static $text = "TEXT"; +} + +/** + * AudioFormat Helper class. + * @package TropoPHP_Support + */ +class AudioFormat { + public static $wav = "audio/wav"; + public static $mp3 = "audio/mp3"; +} + +/** + * Voice Helper class. + * @package TropoPHP_Support + */ +class Voice { + public static $Castilian_Spanish_male = "jorge"; + public static $Castilian_Spanish_female = "carmen"; + public static $French_male = "bernard"; + public static $French_female = "florence"; + public static $US_English_male = "dave"; + public static $US_English_female = "jill"; + public static $British_English_male = "dave"; + public static $British_English_female = "kate"; + public static $German_male = "stefan"; + public static $German_female = "katrin"; + public static $Italian_male = "luca"; + public static $Italian_female = "paola"; + public static $Dutch_male = "willem"; + public static $Dutch_female = "saskia"; + public static $Mexican_Spanish_male = "carlos"; + public static $Mexican_Spanish_female = "soledad"; +} + +/** + * SIP Headers Helper class. + * @package TropoPHP_Support + */ +class Headers { + + public function __set($name, $value) { + if(!strstr($name, "-")) { + $this->$name = $value; + } else { + $name = str_replace("-", "_", $name); + $this->$name = $value; + } + } + +} + +?> \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..41e82bc --- /dev/null +++ b/index.php @@ -0,0 +1,67 @@ +say("Welcome to the tropo orchestra example.", array("barge" => false)); + $tropo->ask("What is your favoirte programming language?", array("attempts" => 3,"choices" => "PHP, Ruby, JavaScript, Python", "name" => "language", "timeout" => 5)); + $tropo->on(array("event" => "continue", "next" => "index?uri=end", "say" => "Please hold.")); + $tropo->on(array("event" => "error", "next" => "index?uri=error", "say" => "An error has occured.")); + $tropo->renderJSON(); + +} + +// Route for the final caler dialog. +dispatch_post('end', 'tropoEnd'); +function tropoEnd() { + + $result = new Result(); + $selection = $result->getValue(); + + $tropo = new Tropo(); + + switch($selection) { + + case 'PHP': + $toSay = "PHP is the shiz nit."; + break; + + case 'Ruby': + $toSay = "I have nothing against Ruby."; + break; + + case 'Javascript': + $toSay = "Javascript is mighty fine."; + break; + + case 'Python': + $toSay = "Python is for lovers."; + + } + + $tropo->say("You chose, " . $selection); + $tropo->say($toSay); + $tropo->say("Thanks for playing along. Goodbye"); + $tropo->hangup(); + $tropo->renderJSON(); + +} + +// Route for an error dialog. +dispatch_post('error', 'tropoError'); +function tropoError() { + + $tropo = new Tropo(); + $tropo->say("Sorry, an error has occured. Please try again later"); + $tropo->hangup(); + $tropo->renderJSON(); + +} + +run(); \ No newline at end of file