├── CNAME ├── wav ├── 0.wav ├── 1.wav ├── 2.wav ├── 3.wav ├── 4.wav ├── 5.wav ├── 6.wav ├── 7.wav ├── 8.wav ├── 9.wav ├── hash.wav ├── star.wav └── silence.wav ├── favicon.ico ├── mp3 └── ring.mp3 ├── img ├── bg_333.jpg └── notification.png ├── _config.yml ├── resources └── switch │ └── conf │ └── dialplan │ ├── 012_saraphone_is_caller_callee_saraphone.xml │ ├── 470_saraphone_parking.xml │ ├── 280_saraphone_unhold.xml │ ├── 300_saraphone_hold.xml │ ├── 310_saraphone_att_xfer.xml │ └── 300_saraphone_dx.xml ├── patch.README ├── app_defaults.php ├── app_menu.php ├── css ├── style.css ├── style2.css └── high2.css ├── doc └── INSTALL.txt ├── root.php ├── patch.diff ├── app_config.php ├── saraphone.lua ├── js └── md5.js ├── app_languages.php ├── README.md ├── contacts.php ├── LICENSE ├── saraphone.js └── saraphone.html /CNAME: -------------------------------------------------------------------------------- 1 | saraphone.org -------------------------------------------------------------------------------- /wav/0.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/wav/0.wav -------------------------------------------------------------------------------- /wav/1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/wav/1.wav -------------------------------------------------------------------------------- /wav/2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/wav/2.wav -------------------------------------------------------------------------------- /wav/3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/wav/3.wav -------------------------------------------------------------------------------- /wav/4.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/wav/4.wav -------------------------------------------------------------------------------- /wav/5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/wav/5.wav -------------------------------------------------------------------------------- /wav/6.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/wav/6.wav -------------------------------------------------------------------------------- /wav/7.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/wav/7.wav -------------------------------------------------------------------------------- /wav/8.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/wav/8.wav -------------------------------------------------------------------------------- /wav/9.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/wav/9.wav -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/favicon.ico -------------------------------------------------------------------------------- /mp3/ring.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/mp3/ring.mp3 -------------------------------------------------------------------------------- /wav/hash.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/wav/hash.wav -------------------------------------------------------------------------------- /wav/star.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/wav/star.wav -------------------------------------------------------------------------------- /img/bg_333.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/img/bg_333.jpg -------------------------------------------------------------------------------- /wav/silence.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/wav/silence.wav -------------------------------------------------------------------------------- /img/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmaruzz/saraphone/HEAD/img/notification.png -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate 2 | descripion: "SaraPhone is an open source SIP WebRTC phone, complete with HotDesking, Redial, BLFs, MWI, DND, PhoneBook, Hold, Mute, Notifications.|SaraPhone is fully integrated with FusionPBX. |Based on SIP.js, SaraPhone works with all WebRTC compliant servers: FreeSWITCH, Asterisk, OpenSIPS, Kamailio, etc. |SaraPhone gets its name from Giovanni's wife, Sara." 3 | -------------------------------------------------------------------------------- /resources/switch/conf/dialplan/012_saraphone_is_caller_callee_saraphone.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/switch/conf/dialplan/470_saraphone_parking.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/switch/conf/dialplan/280_saraphone_unhold.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/switch/conf/dialplan/300_saraphone_hold.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/switch/conf/dialplan/310_saraphone_att_xfer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /patch.README: -------------------------------------------------------------------------------- 1 | presence (BLFs) not working via wss #398 2 | ( https://github.com/signalwire/freeswitch/issues/398 ) 3 | 4 | quick and dirty patch 5 | 6 | ======= 7 | 8 | this patch is for: 9 | LATEST FreeSWITCH MASTER branch ( commit ae0444e9cbccdee55a80467d605e1e8c3363a36d 2020-04-06 ) 10 | or 11 | LATEST FreeSWITCH 1.10 branch ( commit f7bdd3845a91c77ff6c776a70ae2fc9d7f7d3f86 2019-12-31 ) 12 | 13 | ======= 14 | 15 | apply as: 16 | 17 | cd /usr/src/ 18 | git clone https://github.com/signalwire/freeswitch.git freeswitch ## OR git clone https://github.com/signalwire/freeswitch.git -bv1.10 freeswitch 19 | cd freeswitch 20 | git apply patch.diff 21 | 22 | ======= 23 | 24 | -giovanni 25 | -------------------------------------------------------------------------------- /app_defaults.php: -------------------------------------------------------------------------------- 1 | 20 | Portions created by the Initial Developer are Copyright (C) 2008-2016 21 | the Initial Developer. All Rights Reserved. 22 | 23 | Contributor(s): 24 | Mark J Crane 25 | */ 26 | 27 | if ($domains_processed == 1) { 28 | 29 | //do nothing 30 | 31 | } 32 | 33 | ?> 34 | -------------------------------------------------------------------------------- /resources/switch/conf/dialplan/300_saraphone_dx.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app_menu.php: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | display: block; 3 | text-align: center; 4 | } 5 | 6 | body { 7 | background: #f5f7f7; 8 | font-family: sans-serif; 9 | } 10 | 11 | #target { 12 | width: 25em; 13 | } 14 | 15 | #hangup { 16 | background-color: red; 17 | } 18 | 19 | .call-control { 20 | height: 3em; 21 | } 22 | 23 | #dtmf-buttons button { 24 | display: inline-block; 25 | margin: 1em 0 0 0; 26 | height: 3.8em; 27 | width: 3.8em; 28 | background-color: white; 29 | border: 1px solid black; 30 | border-radius: 2px; 31 | font-size: 1em; 32 | } 33 | 34 | input { 35 | border-radius: 2px; 36 | border: 0px; 37 | height: 3em; 38 | } 39 | 40 | input[type=submit] { 41 | background-color: #35f659; 42 | border: 0px; 43 | } 44 | 45 | input[type=checkbox] { 46 | height: auto; 47 | } 48 | 49 | #chat-box { 50 | position: relative; 51 | border-radius: 2px; 52 | border: 1px solid black; 53 | width: 30em; 54 | height: 40em; 55 | height: 8em; 56 | } 57 | 58 | #message-input { 59 | width: 100%; 60 | position: absolute; 61 | #bottom: 1em; 62 | border-top: 1px solid black; 63 | #padding-top: 1em; 64 | } 65 | 66 | #message, #message-submit { 67 | display: inline-block; 68 | margin: 0px 0px 0px 0px; 69 | #padding: 1px 1px 1px 1px; 70 | } 71 | 72 | #message { 73 | width: 80%; 74 | margin-left: 5em; 75 | border-top-right-radius: 0px; 76 | border-bottom-right-radius: 0px; 77 | } 78 | 79 | #message-submit { 80 | margin-left: -4px; 81 | border-top-left-radius: 0px; 82 | border-bottom-left-radius: 0px; 83 | padding-left: 5px; 84 | padding-right: 5px; 85 | } 86 | 87 | #log-container { 88 | height: 35em; 89 | height: 5em; 90 | overflow: scroll; 91 | } 92 | 93 | #remote-media { 94 | border-radius: 2px; 95 | border: 1px black solid; 96 | } 97 | -------------------------------------------------------------------------------- /doc/INSTALL.txt: -------------------------------------------------------------------------------- 1 | wget -O - https://raw.githubusercontent.com/fusionpbx/fusionpbx-install.sh/master/debian/pre-install.sh | sh; 2 | 3 | cd /usr/src/fusionpbx-install.sh/debian 4 | 5 | 6 | vi resources/config.sh 7 | 8 | #========================= 9 | 10 | # FusionPBX Settings 11 | domain_name=fusion01-pkg.XXX.com # hostname, ip_address or a custom value 12 | system_username=superadmin # default username admin 13 | system_password=CiaoCiaoCiao1! # random or a custom value 14 | system_branch=master # master, stable 15 | 16 | # FreeSWITCH Settings 17 | switch_branch=stable # master, stable 18 | switch_source=false # true or false 19 | switch_package=true # true or false 20 | switch_version=1.10.1 # only for source 21 | switch_tls=true # true or false 22 | 23 | # Database Settings 24 | database_password=CiaoCiaoCiao1! # random or a custom value (safe characters A-Z, a-z, 0-9) 25 | database_repo=official # PostgreSQL official, system, 2ndquadrant 26 | database_version=latest # requires repo official 27 | database_host=127.0.0.1 # hostname or IP address 28 | database_port=5432 # port number 29 | database_backup=false # true or false 30 | 31 | # General Settings 32 | php_version=7.1 # PHP version 5.6 or 7.0, 7.1, 7.2 33 | letsencrypt_folder=true # true or false 34 | 35 | #========================= 36 | 37 | 38 | 39 | ./install.sh 40 | 41 | cd /usr/src/fusionpbx-install.sh/debian/resources/ 42 | ./letsencrypt.sh 43 | 44 | cat /etc/dehydrated/certs/fusion01-pkg.XXX.it/fullchain.pem /etc/dehydrated/certs/fusion01-pkg.XXX.it/privkey.pem > /etc/freeswitch/tls/wss.pem 45 | 46 | cd /var/www/fusionpbx/app 47 | git clone https://github.com/gmaruzz/saraphone.git 48 | chown -R www-data:www-data saraphone 49 | 50 | -------------------------------------------------------------------------------- /root.php: -------------------------------------------------------------------------------- 1 | 20 | Portions created by the Initial Developer are Copyright (C) 2008-2014 21 | the Initial Developer. All Rights Reserved. 22 | 23 | Contributor(s): 24 | Mark J Crane 25 | */ 26 | 27 | // make sure the PATH_SEPARATOR is defined 28 | if (!defined("PATH_SEPARATOR")) { 29 | if ( strpos( $_ENV[ "OS" ], "Win" ) !== false ) { define("PATH_SEPARATOR", ";"); } else { define("PATH_SEPARATOR", ":"); } 30 | } 31 | 32 | // make sure the document_root is set 33 | $_SERVER["SCRIPT_FILENAME"] = str_replace("\\", "/", $_SERVER["SCRIPT_FILENAME"]); 34 | $_SERVER["DOCUMENT_ROOT"] = str_replace($_SERVER["PHP_SELF"], "", $_SERVER["SCRIPT_FILENAME"]); 35 | $_SERVER["DOCUMENT_ROOT"] = realpath($_SERVER["DOCUMENT_ROOT"]); 36 | //echo "DOCUMENT_ROOT: ".$_SERVER["DOCUMENT_ROOT"]."
\n"; 37 | //echo "PHP_SELF: ".$_SERVER["PHP_SELF"]."
\n"; 38 | //echo "SCRIPT_FILENAME: ".$_SERVER["SCRIPT_FILENAME"]."
\n"; 39 | 40 | // if the project directory exists then add it to the include path otherwise add the document root to the include path 41 | if (is_dir($_SERVER["DOCUMENT_ROOT"].'/fusionpbx')){ 42 | if(!defined('PROJECT_PATH')) { define('PROJECT_PATH', '/fusionpbx'); } 43 | set_include_path( get_include_path() . PATH_SEPARATOR . $_SERVER["DOCUMENT_ROOT"].'/fusionpbx' ); 44 | } 45 | else { 46 | if(!defined('PROJECT_PATH')) { define('PROJECT_PATH', ''); } 47 | set_include_path( get_include_path() . PATH_SEPARATOR . $_SERVER['DOCUMENT_ROOT'] ); 48 | } 49 | 50 | ?> 51 | -------------------------------------------------------------------------------- /patch.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/mod/endpoints/mod_sofia/sofia_glue.c b/src/mod/endpoints/mod_sofia/sofia_glue.c 2 | index 72c654fe46..dc784c5ac6 100644 3 | --- a/src/mod/endpoints/mod_sofia/sofia_glue.c 4 | +++ b/src/mod/endpoints/mod_sofia/sofia_glue.c 5 | @@ -291,6 +291,8 @@ sofia_transport_t sofia_glue_str2transport(const char *str) 6 | { 7 | if (!strncasecmp(str, "udp", 3)) { 8 | return SOFIA_TRANSPORT_UDP; 9 | + } else if (!strncasecmp(str, "wss", 3)) { 10 | + return SOFIA_TRANSPORT_WSS; 11 | } else if (!strncasecmp(str, "tcp", 3)) { 12 | return SOFIA_TRANSPORT_TCP; 13 | } else if (!strncasecmp(str, "sctp", 4)) { 14 | diff --git a/src/mod/endpoints/mod_sofia/sofia_presence.c b/src/mod/endpoints/mod_sofia/sofia_presence.c 15 | index be27121cd1..d73a26107e 100644 16 | --- a/src/mod/endpoints/mod_sofia/sofia_presence.c 17 | +++ b/src/mod/endpoints/mod_sofia/sofia_presence.c 18 | @@ -2235,6 +2235,23 @@ static void _send_presence_notify(sofia_profile_t *profile, 19 | sofia_transport_t transport = sofia_glue_str2transport(tp); 20 | 21 | switch (transport) { 22 | + char * colon; 23 | + char * at; 24 | + char * username; 25 | + int username_lenght; 26 | + 27 | + case SOFIA_TRANSPORT_WSS: 28 | + switch_strdup(username, full_from); 29 | + colon = strchr(username, ':'); 30 | + at = strchr(username, '@'); 31 | + username_lenght = at - colon; 32 | + username = colon; 33 | + username[username_lenght] = '\0'; 34 | + contact = switch_mprintf("sip%s@%s:%s;transport=wss", username, remote_ip, remote_port); 35 | + 36 | + contact_str = profile->public_url; 37 | + 38 | + break; 39 | case SOFIA_TRANSPORT_TCP: 40 | contact_str = profile->tcp_public_contact; 41 | break; 42 | @@ -2250,6 +2267,23 @@ static void _send_presence_notify(sofia_profile_t *profile, 43 | } else { 44 | sofia_transport_t transport = sofia_glue_str2transport(tp); 45 | switch (transport) { 46 | + char * colon; 47 | + char * at; 48 | + char * username; 49 | + int username_lenght; 50 | + 51 | + case SOFIA_TRANSPORT_WSS: 52 | + switch_strdup(username, full_from); 53 | + colon = strchr(username, ':'); 54 | + at = strchr(username, '@'); 55 | + username_lenght = at - colon; 56 | + username = colon; 57 | + username[username_lenght] = '\0'; 58 | + contact = switch_mprintf("sip%s@%s:%s;transport=wss", username, remote_ip, remote_port); 59 | + 60 | + contact_str = profile->url; 61 | + 62 | + break; 63 | case SOFIA_TRANSPORT_TCP: 64 | contact_str = profile->tcp_contact; 65 | break; 66 | -------------------------------------------------------------------------------- /app_config.php: -------------------------------------------------------------------------------- 1 | 62 | -------------------------------------------------------------------------------- /saraphone.lua: -------------------------------------------------------------------------------- 1 | -- Giovanni Maruzzelli 2 | 3 | --prepare the api object 4 | api = freeswitch.API(); 5 | 6 | local loglevel = "debug" 7 | session:setAutoHangup(false); 8 | 9 | ------------------------------------------------------------------------ 10 | ------------------------------------------------------------------------ 11 | ------------------------------------------------------------------------ 12 | 13 | 14 | --connect to the database 15 | local Database = require "resources.functions.database"; 16 | local dbh = Database.new('switch') 17 | 18 | 19 | --get the variables 20 | local uuid = session:getVariable("uuid"); 21 | local domain_name = session:getVariable("domain_name"); 22 | local destination_number = session:getVariable("destination_number"); 23 | local caller_id_number = session:getVariable("caller_id_number"); 24 | local username = session:getVariable("username"); 25 | if(destination_number == nil) then destination_number = '0' end 26 | if(caller_id_number == nil) then caller_id_number = '0' end 27 | local saraphone_destination_user_agent = api:execute("sofia_presence_data", "user_agent internal/"..destination_number.."@"..domain_name); 28 | local saraphone_caller_user_agent = api:execute("sofia_presence_data", "user_agent internal/"..username.."@"..domain_name); 29 | 30 | local saraphone_bind = session:getVariable("saraphone_bind"); 31 | if(saraphone_bind == nil) then saraphone_bind = "false" end 32 | 33 | local saraphone_ringback = session:getVariable("us-ring"); 34 | if(saraphone_ringback == nil) then saraphone_ringback = "%(2000,4000,440,480)" end 35 | 36 | freeswitch.consoleLog(loglevel, uuid .. " ------------ BEGIN ----------------------------------------------------------\n") 37 | 38 | freeswitch.consoleLog(loglevel, uuid .. " domain_name: " .. domain_name .. "\n"); 39 | freeswitch.consoleLog(loglevel, uuid .. " destination_number: " .. destination_number .. "\n"); 40 | freeswitch.consoleLog(loglevel, uuid .. " caller_id_number: " .. caller_id_number .. "\n"); 41 | freeswitch.consoleLog(loglevel, uuid .. " username: " .. username .. "\n"); 42 | freeswitch.consoleLog(loglevel, uuid .. " saraphone_destination_user_agent: " .. saraphone_destination_user_agent .. "\n"); 43 | freeswitch.consoleLog(loglevel, uuid .. " saraphone_caller_user_agent: " .. saraphone_caller_user_agent .. "\n"); 44 | freeswitch.consoleLog(loglevel, uuid .. " saraphone_bind: " .. saraphone_bind .. "\n"); 45 | 46 | if(saraphone_bind == "false") then 47 | 48 | session:execute("export","saraphone_bind=true"); 49 | 50 | local saraphone_is_caller = string.find(saraphone_caller_user_agent, "SaraPhone"); 51 | if(saraphone_is_caller) then 52 | saraphone_is_caller = "true" 53 | session:execute("export","ignore_early_media=false"); 54 | session:setVariable("ringback", saraphone_ringback); 55 | session:setVariable("instant_ringback", "true"); 56 | session:answer() 57 | api:execute("msleep", "1000"); 58 | end 59 | 60 | session:execute("export","saraphone_is_both=true"); 61 | 62 | session:execute("bind_digit_action","saraphone_local,*299,exec:execute_extension,saraphone_hold XML ${context},aleg,bleg"); 63 | session:execute("bind_digit_action","saraphone_local,*399,exec:execute_extension,saraphone_hold XML ${context},peer,peer"); 64 | session:execute("bind_digit_action","saraphone_local,*499,exec:execute_extension,saraphone_dx XML ${context},aleg,bleg"); 65 | session:execute("bind_digit_action","saraphone_local,*599,exec:execute_extension,saraphone_dx XML ${context},peer,peer"); 66 | session:execute("bind_digit_action","saraphone_local,*699,exec:execute_extension,saraphone_att_xfer XML ${context},aleg,bleg"); 67 | session:execute("bind_digit_action","saraphone_local,*799,exec:execute_extension,saraphone_att_xfer XML ${context},peer,peer"); 68 | session:execute("digit_action_set_realm","saraphone_local"); 69 | end 70 | 71 | freeswitch.consoleLog(loglevel, uuid .. " ------------ END ----------------------------------------------------------\n") 72 | 73 | 74 | ------------------------------------------------------------------------ 75 | ------------------------------------------------------------------------ 76 | ------------------------------------------------------------------------ 77 | 78 | -------------------------------------------------------------------------------- /css/style2.css: -------------------------------------------------------------------------------- 1 | /* background */ 2 | html, body{ 3 | background: -moz-linear-gradient(top, #6c89b5 0%, #144794 100%); 4 | /*background: rgb(108,137,181);*/ 5 | background: rgba(20,71,148,1); 6 | background: linear-gradient(180deg, rgba(108,137,181,1) 0%, rgba(20,71,148,1) 100%); 7 | background-repeat: no-repeat; 8 | background-attachment: fixed; 9 | webkit-background-size:cover; 10 | -moz-background-size:cover; 11 | -o-background-size:cover; 12 | background-size:cover; 13 | background-attachment: fixed !important; 14 | } 15 | 16 | body { 17 | text-align: center; 18 | color: rgb(255, 255, 255); 19 | position: relative; 20 | /*min-height: 150px;*/ 21 | } 22 | 23 | /* Custom default button */ 24 | .btn-default, 25 | .btn-default:hover, 26 | .btn-default:focus { 27 | color: #333; 28 | text-shadow: none; /* Prevent inheritence from `body` */ 29 | background-color: #fff; 30 | border: 1px solid #fff; 31 | } 32 | 33 | .form-signin { 34 | max-width: 800px; 35 | margin: 0 auto; 36 | } 37 | 38 | .form-signin .form-signin-heading, 39 | .form-signin .checkbox { 40 | margin-top: 5px; 41 | } 42 | .form-signin .form-signin-content{ 43 | margin-top: 5px; 44 | } 45 | 46 | input[type='number'] { 47 | -moz-appearance:textfield; 48 | } 49 | 50 | input::-webkit-outer-spin-button, 51 | input::-webkit-inner-spin-button { 52 | -webkit-appearance: none; 53 | } 54 | 55 | /* main menu container */ 56 | nav.navbar { 57 | display: none !important; 58 | } 59 | 60 | /* main menu logo */ 61 | img.navbar-logo { 62 | display: none !important; 63 | } 64 | 65 | /* keypad */ 66 | input.btn, input.button, button.btn-default { 67 | padding: 5px 8px; 68 | border: 1px solid #242424; 69 | border-radius: 5px 5px 5px 5px; 70 | background: #4f4f4f; 71 | background-image: -webkit-linear-gradient(top, #4f4f4f 0%, #000000 100%); 72 | text-align: center; 73 | text-transform: uppercase; 74 | color: #ffffff; 75 | vertical-align: middle; 76 | } 77 | input.btn:hover, input.btn:active, input.btn:focus, input.button:hover, input.button:active, input.button:focus, button.btn-default:hover, button.btn-default:active, button.btn-default:focus { 78 | color: #ffffff !important; 79 | } 80 | 81 | /* non usato... */ 82 | #keypad_button{ 83 | margin-bottom: 5px; 84 | margin-right: 3px; 85 | width: 48px !important; 86 | height: 48px !important; 87 | font-size: 20px; 88 | font-weight: bold; 89 | } 90 | 91 | /* webphone */ 92 | #webphone_body{ 93 | text-shadow:0 0px 0px rgba(0,0,0,.5); 94 | border-style: solid; 95 | border-width: 1px; 96 | border-radius: 15px; 97 | border-color: #292929; 98 | background: #333; 99 | padding: 15px; 100 | margin-bottom: 10px; 101 | } 102 | #webphone_display{ 103 | background-color: #808080; 104 | margin-bottom: 10px; 105 | padding-top: 5px; 106 | border-style: solid; 107 | border-width: 1px; 108 | border-color: #292929; 109 | border-radius: 15px; 110 | height: 120px; 111 | } 112 | #webphone_keypad{ 113 | border-style: inset; 114 | border-width: 1px; 115 | border-radius: 15px; 116 | border-color: #292929; 117 | background: -moz-linear-gradient(top, #616161 0%, #333 100%); 118 | background: rgb(97,97,97); 119 | background: linear-gradient(180deg, rgba(97,97,97,1) 0%, rgba(51,51,51,1) 100%); 120 | padding: 20px 0px 0px 0px; 121 | margin:0px 5px 5px 5px; 122 | } 123 | #webphone_keypad_right{ 124 | border-style: inset; 125 | border-width: 1px; 126 | border-radius: 15px; 127 | border-color: #292929; 128 | background: -moz-linear-gradient(top, #616161 0%, #333 100%); 129 | background: rgb(97,97,97); 130 | background: linear-gradient(180deg, rgba(97,97,97,1) 0%, rgba(51,51,51,1) 100%); 131 | padding: 20px 0px 5px 0px; 132 | margin: 0px 5px 5px 5px; 133 | } 134 | #webphone_blf{ 135 | text-shadow:0 0px 0px rgba(0,0,0,.5); 136 | border-style: solid; 137 | border-width: 1px; 138 | border-radius: 15px; 139 | border-color: #292929; 140 | background: #333; 141 | padding: 20px; 142 | } 143 | 144 | #hideAll 145 | { 146 | position: fixed; 147 | left: 0px; 148 | right: 0px; 149 | top: 0px; 150 | bottom: 0px; 151 | background-color: white; 152 | z-index: 99; /* Higher than anything else in the document */ 153 | } 154 | -------------------------------------------------------------------------------- /css/high2.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | /* Links */ 6 | a, 7 | a:focus, 8 | a:hover { 9 | color: #fff; 10 | } 11 | 12 | /* Custom default button */ 13 | .btn-default, 14 | .btn-default:hover, 15 | .btn-default:focus { 16 | color: #333; 17 | text-shadow: none; /* Prevent inheritence from `body` */ 18 | background-color: #fff; 19 | border: 1px solid #fff; 20 | } 21 | 22 | 23 | /* 24 | * Base structure 25 | */ 26 | 27 | /*html,*/ 28 | /*body {*/ 29 | /*height: 100%;*/ 30 | /*background-color: #333;*/ 31 | /*background-color: #004a98;*/ 32 | /*}*/ 33 | 34 | body { 35 | color: #fff; 36 | text-align: center; 37 | text-shadow: 0 1px 3px rgba(0,0,0,.5); 38 | color: rgb(255, 255, 255); 39 | position: relative; 40 | min-height: 372px; 41 | } 42 | 43 | /* Extra markup and styles for table-esque vertical and horizontal centering */ 44 | .site-wrapper { 45 | display: table; 46 | width: 100%; 47 | height: 100%; /* For at least Firefox */ 48 | min-height: 100%; 49 | -webkit-box-shadow: inset 0 0 100px rgba(0,0,0,.5); 50 | box-shadow: inset 0 0 100px rgba(0,0,0,.5); 51 | } 52 | .site-wrapper-inner { 53 | display: table-cell; 54 | vertical-align: top; 55 | } 56 | .cover-container { 57 | margin-right: auto; 58 | margin-left: auto; 59 | } 60 | 61 | /* Padding for spacing */ 62 | .inner { 63 | padding: 3px; 64 | } 65 | 66 | 67 | /* 68 | * Header 69 | */ 70 | .masthead-brand { 71 | margin-top: 10px; 72 | margin-bottom: 10px; 73 | } 74 | 75 | .masthead-nav > li { 76 | display: inline-block; 77 | } 78 | .masthead-nav > li + li { 79 | margin-left: 20px; 80 | } 81 | .masthead-nav > li > a { 82 | padding-right: 0; 83 | padding-left: 0; 84 | font-size: 16px; 85 | font-weight: bold; 86 | color: #fff; /* IE8 proofing */ 87 | color: rgba(255,255,255,.75); 88 | border-bottom: 2px solid transparent; 89 | } 90 | .masthead-nav > li > a:hover, 91 | .masthead-nav > li > a:focus { 92 | background-color: transparent; 93 | border-bottom-color: #a9a9a9; 94 | border-bottom-color: rgba(255,255,255,.25); 95 | } 96 | .masthead-nav > .active > a, 97 | .masthead-nav > .active > a:hover, 98 | .masthead-nav > .active > a:focus { 99 | color: #fff; 100 | border-bottom-color: #fff; 101 | } 102 | 103 | @media (min-width: 768px) { 104 | .masthead-brand { 105 | float: left; 106 | } 107 | .masthead-nav { 108 | float: right; 109 | } 110 | } 111 | 112 | 113 | /* 114 | * Cover 115 | */ 116 | 117 | .cover { 118 | position: relative; 119 | padding: 0 0; 120 | } 121 | .cover .btn-lg { 122 | padding: 10px 20px; 123 | font-weight: bold; 124 | } 125 | 126 | .explanation { 127 | position: relative; 128 | padding: 0 0; 129 | } 130 | .explanation .btn-lg { 131 | padding: 10px 20px; 132 | font-weight: bold; 133 | } 134 | 135 | 136 | /* 137 | * Footer 138 | */ 139 | 140 | .mastfoot { 141 | position: relative; 142 | color: #999; /* IE8 proofing */ 143 | color: rgba(255,255,255,.5); 144 | } 145 | 146 | 147 | /* 148 | * Affix and center 149 | */ 150 | 151 | @media (min-width: 768px) { 152 | /* Pull out the header and footer */ 153 | .masthead { 154 | position: fixed; 155 | top: 0; 156 | } 157 | .mastfoot { 158 | position: fixed; 159 | bottom: 0; 160 | } 161 | /* Start the vertical centering */ 162 | .site-wrapper-inner { 163 | vertical-align: middle; 164 | } 165 | /* Handle the widths */ 166 | .masthead, 167 | .mastfoot, 168 | .cover-container { 169 | width: 100%; /* Must be percentage or pixels for horizontal alignment */ 170 | } 171 | } 172 | 173 | @media (min-width: 992px) { 174 | .masthead, 175 | .mastfoot, 176 | .cover-container { 177 | width: 700px; 178 | } 179 | } 180 | 181 | .form-signin { 182 | max-width: 800px; 183 | margin: 0 auto; 184 | } 185 | 186 | .form-signin .form-signin-heading, 187 | .form-signin .checkbox { 188 | margin-top: 5px; 189 | } 190 | .form-signin .form-signin-content{ 191 | margin-top: 5px; 192 | } 193 | 194 | 195 | input[type='number'] { 196 | -moz-appearance:textfield; 197 | } 198 | 199 | input::-webkit-outer-spin-button, 200 | input::-webkit-inner-spin-button { 201 | -webkit-appearance: none; 202 | } 203 | 204 | #hideAll 205 | { 206 | position: fixed; 207 | left: 0px; 208 | right: 0px; 209 | top: 0px; 210 | bottom: 0px; 211 | background-color: white; 212 | z-index: 99; /* Higher than anything else in the document */ 213 | } 214 | 215 | /* 216 | video::-webkit-media-controls { 217 | display:none !important; 218 | } 219 | */ 220 | -------------------------------------------------------------------------------- /js/md5.js: -------------------------------------------------------------------------------- 1 | /* 2 | CryptoJS v3.1.2 3 | code.google.com/p/crypto-js 4 | (c) 2009-2013 by Jeff Mott. All rights reserved. 5 | code.google.com/p/crypto-js/wiki/License 6 | */ 7 | var CryptoJS=CryptoJS||function(s,p){var m={},l=m.lib={},n=function(){},r=l.Base={extend:function(b){n.prototype=this;var h=new n;b&&h.mixIn(b);h.hasOwnProperty("init")||(h.init=function(){h.$super.init.apply(this,arguments)});h.init.prototype=h;h.$super=this;return h},create:function(){var b=this.extend();b.init.apply(b,arguments);return b},init:function(){},mixIn:function(b){for(var h in b)b.hasOwnProperty(h)&&(this[h]=b[h]);b.hasOwnProperty("toString")&&(this.toString=b.toString)},clone:function(){return this.init.prototype.extend(this)}}, 8 | q=l.WordArray=r.extend({init:function(b,h){b=this.words=b||[];this.sigBytes=h!=p?h:4*b.length},toString:function(b){return(b||t).stringify(this)},concat:function(b){var h=this.words,a=b.words,j=this.sigBytes;b=b.sigBytes;this.clamp();if(j%4)for(var g=0;g>>2]|=(a[g>>>2]>>>24-8*(g%4)&255)<<24-8*((j+g)%4);else if(65535>>2]=a[g>>>2];else h.push.apply(h,a);this.sigBytes+=b;return this},clamp:function(){var b=this.words,h=this.sigBytes;b[h>>>2]&=4294967295<< 9 | 32-8*(h%4);b.length=s.ceil(h/4)},clone:function(){var b=r.clone.call(this);b.words=this.words.slice(0);return b},random:function(b){for(var h=[],a=0;a>>2]>>>24-8*(j%4)&255;g.push((k>>>4).toString(16));g.push((k&15).toString(16))}return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j>>3]|=parseInt(b.substr(j, 10 | 2),16)<<24-4*(j%8);return new q.init(g,a/2)}},a=v.Latin1={stringify:function(b){var a=b.words;b=b.sigBytes;for(var g=[],j=0;j>>2]>>>24-8*(j%4)&255));return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j>>2]|=(b.charCodeAt(j)&255)<<24-8*(j%4);return new q.init(g,a)}},u=v.Utf8={stringify:function(b){try{return decodeURIComponent(escape(a.stringify(b)))}catch(g){throw Error("Malformed UTF-8 data");}},parse:function(b){return a.parse(unescape(encodeURIComponent(b)))}}, 11 | g=l.BufferedBlockAlgorithm=r.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(b){"string"==typeof b&&(b=u.parse(b));this._data.concat(b);this._nDataBytes+=b.sigBytes},_process:function(b){var a=this._data,g=a.words,j=a.sigBytes,k=this.blockSize,m=j/(4*k),m=b?s.ceil(m):s.max((m|0)-this._minBufferSize,0);b=m*k;j=s.min(4*b,j);if(b){for(var l=0;l>>32-j)+k}function m(a,k,b,h,l,j,m){a=a+(k&h|b&~h)+l+m;return(a<>>32-j)+k}function l(a,k,b,h,l,j,m){a=a+(k^b^h)+l+m;return(a<>>32-j)+k}function n(a,k,b,h,l,j,m){a=a+(b^(k|~h))+l+m;return(a<>>32-j)+k}for(var r=CryptoJS,q=r.lib,v=q.WordArray,t=q.Hasher,q=r.algo,a=[],u=0;64>u;u++)a[u]=4294967296*s.abs(s.sin(u+1))|0;q=q.MD5=t.extend({_doReset:function(){this._hash=new v.init([1732584193,4023233417,2562383102,271733878])}, 15 | _doProcessBlock:function(g,k){for(var b=0;16>b;b++){var h=k+b,w=g[h];g[h]=(w<<8|w>>>24)&16711935|(w<<24|w>>>8)&4278255360}var b=this._hash.words,h=g[k+0],w=g[k+1],j=g[k+2],q=g[k+3],r=g[k+4],s=g[k+5],t=g[k+6],u=g[k+7],v=g[k+8],x=g[k+9],y=g[k+10],z=g[k+11],A=g[k+12],B=g[k+13],C=g[k+14],D=g[k+15],c=b[0],d=b[1],e=b[2],f=b[3],c=p(c,d,e,f,h,7,a[0]),f=p(f,c,d,e,w,12,a[1]),e=p(e,f,c,d,j,17,a[2]),d=p(d,e,f,c,q,22,a[3]),c=p(c,d,e,f,r,7,a[4]),f=p(f,c,d,e,s,12,a[5]),e=p(e,f,c,d,t,17,a[6]),d=p(d,e,f,c,u,22,a[7]), 16 | c=p(c,d,e,f,v,7,a[8]),f=p(f,c,d,e,x,12,a[9]),e=p(e,f,c,d,y,17,a[10]),d=p(d,e,f,c,z,22,a[11]),c=p(c,d,e,f,A,7,a[12]),f=p(f,c,d,e,B,12,a[13]),e=p(e,f,c,d,C,17,a[14]),d=p(d,e,f,c,D,22,a[15]),c=m(c,d,e,f,w,5,a[16]),f=m(f,c,d,e,t,9,a[17]),e=m(e,f,c,d,z,14,a[18]),d=m(d,e,f,c,h,20,a[19]),c=m(c,d,e,f,s,5,a[20]),f=m(f,c,d,e,y,9,a[21]),e=m(e,f,c,d,D,14,a[22]),d=m(d,e,f,c,r,20,a[23]),c=m(c,d,e,f,x,5,a[24]),f=m(f,c,d,e,C,9,a[25]),e=m(e,f,c,d,q,14,a[26]),d=m(d,e,f,c,v,20,a[27]),c=m(c,d,e,f,B,5,a[28]),f=m(f,c, 17 | d,e,j,9,a[29]),e=m(e,f,c,d,u,14,a[30]),d=m(d,e,f,c,A,20,a[31]),c=l(c,d,e,f,s,4,a[32]),f=l(f,c,d,e,v,11,a[33]),e=l(e,f,c,d,z,16,a[34]),d=l(d,e,f,c,C,23,a[35]),c=l(c,d,e,f,w,4,a[36]),f=l(f,c,d,e,r,11,a[37]),e=l(e,f,c,d,u,16,a[38]),d=l(d,e,f,c,y,23,a[39]),c=l(c,d,e,f,B,4,a[40]),f=l(f,c,d,e,h,11,a[41]),e=l(e,f,c,d,q,16,a[42]),d=l(d,e,f,c,t,23,a[43]),c=l(c,d,e,f,x,4,a[44]),f=l(f,c,d,e,A,11,a[45]),e=l(e,f,c,d,D,16,a[46]),d=l(d,e,f,c,j,23,a[47]),c=n(c,d,e,f,h,6,a[48]),f=n(f,c,d,e,u,10,a[49]),e=n(e,f,c,d, 18 | C,15,a[50]),d=n(d,e,f,c,s,21,a[51]),c=n(c,d,e,f,A,6,a[52]),f=n(f,c,d,e,q,10,a[53]),e=n(e,f,c,d,y,15,a[54]),d=n(d,e,f,c,w,21,a[55]),c=n(c,d,e,f,v,6,a[56]),f=n(f,c,d,e,D,10,a[57]),e=n(e,f,c,d,t,15,a[58]),d=n(d,e,f,c,B,21,a[59]),c=n(c,d,e,f,r,6,a[60]),f=n(f,c,d,e,z,10,a[61]),e=n(e,f,c,d,j,15,a[62]),d=n(d,e,f,c,x,21,a[63]);b[0]=b[0]+c|0;b[1]=b[1]+d|0;b[2]=b[2]+e|0;b[3]=b[3]+f|0},_doFinalize:function(){var a=this._data,k=a.words,b=8*this._nDataBytes,h=8*a.sigBytes;k[h>>>5]|=128<<24-h%32;var l=s.floor(b/ 19 | 4294967296);k[(h+64>>>9<<4)+15]=(l<<8|l>>>24)&16711935|(l<<24|l>>>8)&4278255360;k[(h+64>>>9<<4)+14]=(b<<8|b>>>24)&16711935|(b<<24|b>>>8)&4278255360;a.sigBytes=4*(k.length+1);this._process();a=this._hash;k=a.words;for(b=0;4>b;b++)h=k[b],k[b]=(h<<8|h>>>24)&16711935|(h<<24|h>>>8)&4278255360;return a},clone:function(){var a=t.clone.call(this);a._hash=this._hash.clone();return a}});r.MD5=t._createHelper(q);r.HmacMD5=t._createHmacHelper(q)})(Math); 20 | -------------------------------------------------------------------------------- /app_languages.php: -------------------------------------------------------------------------------- 1 | 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | What is SaraPhone? 2 | -------------------------------------- 3 | 4 | SaraPhone is an open source bare bone SIP WebRTC office phone (no video), complete with most features real companies want to use in real world: HotDesking, Redial, BLFs, MWI, DND, PhoneBook, Hold, Transfer, Mute, Attended Transfer, Notifications, running on all Browsers both on Desktop and SmartPhone. 5 | 6 | SaraPhone is fully integrated with FusionPBX, the full-featured domain based multi-tenant PBX and voice switch for FreeSwitch. 7 | 8 | Based on SIP.js, SaraPhone works with all WebRTC compliant SIP proxies, gateways, and servers (FreeSWITCH, Asterisk, OpenSIPS, Kamailio, etc). 9 | 10 | Initial author is Giovanni Maruzzelli, and SaraPhone gets its name from Giovanni's wife, Sara. 11 | 12 | 13 | In addition to providing all of the usual DeskPhone functionality, SaraPhone got: 14 | 15 | - Desktop Notification for Incoming Calls 16 | - Live MWI update 17 | - Real Time BLFs status update 18 | - BLF click to call 19 | - Caller Name and Number Display 20 | - Call Error Cause Display 21 | - AutoAnswer 22 | - Network Disconnect Reload 23 | - Show and Set Caller-ID (incoming-outbound) 24 | 25 | Software Requirements 26 | -------------------------------------- 27 | 28 | - FusionPBX 29 | - or 30 | - WSS SIP Server (FreeSWITCH, Asterisk, OpenSIPS, Kamailio, etc) + Web Server (Apache, Nginx, etc) 31 | 32 | How to Install SaraPhone on FusionPBX 33 | ---------------------------- 34 | 35 | YOU **REALLY** NEED TO DO **ALL** FOLLOWING STEPS 36 | 37 | **ALL THOSE BORING FOLLOWING STEPS** 38 | 39 | **SAD, BUT TRUE** 40 | 41 | **1 As root do the following:** 42 | 43 | ``` 44 | cd /var/www/fusionpbx/app; 45 | git clone https://github.com/gmaruzz/saraphone.git; 46 | chown -R www-data:www-data saraphone; 47 | ``` 48 | 49 | **2 Login as superadmin to your FusionPBX Web GUI,** 50 | 51 | Menu->Advanced->Upgrade, check: 52 | - App Defaults 53 | - Menu Defaults 54 | - Permission Defaults 55 | 56 | then click "Execute" 57 | 58 | **3 Then, go to** 59 | 60 | Menu->Advanced->Default Settings 61 | SaraPhone settings: 62 | - wss_proxy SIP external IP Address of FusionPBX server 63 | 64 | then click "Reload" 65 | 66 | **4 Go to Menu->Advanced->Sip Profiles** 67 | 68 | click on "internal", then: 69 | - liberal-dtmf true true 70 | - send-message-query-on-register true true 71 | - send-presence-on-register true true 72 | - wss-binding :7443 true 73 | 74 | **5 Go to menu->status->sipstatus** 75 | 76 | - click fluchcache 77 | - click reloadxml 78 | 79 | 80 | **6 You NEED well working letsencrypt SSL certificates:** 81 | 82 | ``` 83 | cd /usr/src/fusionpbx-install.sh/debian/resources/ 84 | ./letsencrypt.sh 85 | 86 | cat /etc/dehydrated/certs/XXX/fullchain.pem /etc/dehydrated/certs/XXX/privkey.pem > /etc/freeswitch/tls/wss.pem 87 | 88 | ``` 89 | **7 then restart FreeSWITCH:** 90 | 91 | ``` 92 | systemctl restart freeswitch; 93 | ``` 94 | 95 | **8 For well working MWI:** 96 | 97 | edit /etc/freeswitch/autoload_configs/lua.conf.xml, uncomment line: 98 | ``` 99 | 100 | ``` 101 | 102 | **9 then restart FreeSWITCH:** 103 | 104 | ``` 105 | systemctl restart freeswitch; 106 | ``` 107 | 108 | **10 check USERs, EXTENSIONs, DEVICEs** 109 | 110 | User **MUST** have one or more **EXTENSION** assigned to her, and at least one of such extensions **MUST** be assigned to a **DEVICE** (you can create a fake device making up the macaddress). 111 | 112 | SaraPhone will get its config from the **DEVICE** so, you want to configure the BLFs in the DEVICE page (menu->Accounts->Devices). 113 | 114 | Saraphone will not care about "Port" and "Transport" settings in the DEVICE page. Saraphone will always use WSS transport, and the port defined in menu->Advanced->Default Settings->saraphone. 115 | 116 | (optional: for best looking results, in the menu->Accounts->Extensions extension page, set effective-caller-id-name) 117 | 118 | 119 | **11 Logout from FusionPBX and login as a normal user, you will find:** 120 | 121 | Menu->Apps->SaraPhone 122 | 123 | 124 | **12 Desktop Notifications of incoming calls** 125 | 126 | To allow for desktop notifications of incoming calls, click on "Allow Notification" on the bottom of SaraPhone web page 127 | 128 | 129 | **13 Upgrading After Install** 130 | 131 | ``` 132 | cd /var/www/fusionpbx/app/saraphone; 133 | git stash; git pull; git stash apply 134 | ``` 135 | often, and you will get latest features/bigfixes, and maintain your own modifications 136 | 137 | 138 | How to Install SaraPhone on WSS SIP Server + Web Server 139 | ---------------------------- 140 | 141 | * As root go into HTML directory of your webserver, and: 142 | 143 | ``` 144 | git clone https://github.com/gmaruzz/saraphone.git; 145 | chown -R www-data:www-data saraphone; 146 | ``` 147 | then edit saraphone.html to preset WSS proxy address and port, and the SIP domain. 148 | 149 | You can then access SaraPhone at: 150 | ``` 151 | https://your.webserver.address/saraphone/saraphone.html 152 | ``` 153 | 166 | 167 | 168 | DON'T: Self -Signed SSL certs 169 | ---------------------------- 170 | 171 | DON'T: To authorize self-signed certificates (only for test) for WSS, from your browser (works on Opera and FireFox, Chrome does not accept self signed WSS at all) go to: 172 | ``` 173 | https://your.fusionpbx.address:7443/ 174 | ``` 175 | and force the browser to accept (I understand the risks, etc) 176 | 177 | 178 | FAQs, PROBLEMs, Troubleshooting 179 | ---------------------------- 180 | 181 | **Q:** There is a sensible delay in establishing audio after call is connected 182 | 183 | **A:** Check if you have two network interfaces (eg: Ethernet and VPN on PCs, or WiFi and Data on Cells) active at same moment. ICE gathering is confused by two Net interfaces. Disable "Data always on" on smartphones, so you will have either WiFi OR Data at each single moment. 184 | 185 | **Q:** In FusionPBX, I want to click on VoiceMail/Messages button and go straight to my messages, no login no password 186 | 187 | **A:** Into saraphone.js, edit the lines: 188 | ``` 189 | $("#checkvmailbtn").click(function() { 190 | $("#extstarbtn").click(); 191 | $("#ext9btn").click(); 192 | $("#ext8btn").click(); 193 | $("#callbtn").click(); 194 | }); 195 | 196 | ``` 197 | to become: 198 | ``` 199 | $("#checkvmailbtn").click(function() { 200 | $("#extstarbtn").click(); 201 | $("#ext9btn").click(); 202 | $("#ext7btn").click(); 203 | $("#callbtn").click(); 204 | }); 205 | 206 | ``` 207 | 208 | eg, it will call *97 instead of *98 209 | 210 | then edit the dialplan extension named vmain_user (*97) and add: 211 | 212 | ``` 213 | action set voicemail_authorized=true 214 | ``` 215 | 216 | at order 37 (before app.lua voicemail.lua) 217 | 218 | **Q:** I want to use SaraPhone with multiple "Internel" SIP Profiles in FusionPBX 219 | 220 | **A:** You must edit BOTH your SIP Profiles AND your Domains: 221 | 222 | SIP Profiles: 223 | 224 | menu->Advanced->Sip Profiles 225 | 226 | for each "internal" Sip Profile: 227 | 228 | wss-binding :74XX True 229 | 230 | #note the colon in the port value, sao is colon then portnumber, XX is a number 231 | 232 | DOMAINS: 233 | 234 | menu->advanced->domains 235 | 236 | click on a domainname 237 | 238 | for each domainname 239 | 240 | go at bottom right of page 241 | 242 | click on Add (domain setting) 243 | 244 | Category: saraphone 245 | 246 | Subcategory: wss_port 247 | 248 | Type: text 249 | 250 | Value: the port number (no colon) you assigned to the profile of this domain 251 | 252 | Enabled: True 253 | 254 | 255 | 256 | SCREENSHOTS ! 257 | ---------------------------- 258 | 259 | ![saraphone_01](https://user-images.githubusercontent.com/331862/79241436-5758d600-7e73-11ea-92ce-7522db44fe63.jpg) 260 | ![saraphone_02](https://user-images.githubusercontent.com/331862/79241434-5627a900-7e73-11ea-8196-549379e603ec.jpg) 261 | ![saraphone_03](https://user-images.githubusercontent.com/331862/79241430-558f1280-7e73-11ea-9994-c8b9d48a587d.jpg) 262 | -------------------------------------------------------------------------------- /contacts.php: -------------------------------------------------------------------------------- 1 | 20 | Portions created by the Initial Developer are Copyright (C) 2008-2019 21 | the Initial Developer. All Rights Reserved. 22 | 23 | Contributor(s): 24 | Mark J Crane 25 | Giovanni Maruzzelli 26 | */ 27 | 28 | //includes 29 | require_once "root.php"; 30 | require_once "resources/require.php"; 31 | require_once "resources/check_auth.php"; 32 | require_once "resources/paging.php"; 33 | 34 | //check permissions 35 | if (permission_exists('contact_view')) { 36 | //access granted 37 | } 38 | else { 39 | echo "access denied"; 40 | exit; 41 | } 42 | 43 | //add multi-lingual support 44 | $language = new text; 45 | $text = $language->get(); 46 | 47 | //get posted data 48 | if (is_array($_POST['contacts'])) { 49 | $action = $_POST['action']; 50 | $search = $_POST['search']; 51 | $contacts = $_POST['contacts']; 52 | } 53 | 54 | //process the http post data by action 55 | if ($action != '' && is_array($contacts) && @sizeof($contacts) != 0) { 56 | switch ($action) { 57 | case 'delete': 58 | if (permission_exists('contact_delete')) { 59 | $obj = new contacts; 60 | $obj->delete($contacts); 61 | } 62 | break; 63 | } 64 | 65 | header('Location: contacts.php'.($search != '' ? '?search='.urlencode($search) : null)); 66 | exit; 67 | } 68 | 69 | //retrieve current user's assigned groups (uuids) 70 | foreach ($_SESSION['groups'] as $group_data) { 71 | $user_group_uuids[] = $group_data['group_uuid']; 72 | } 73 | 74 | //add user's uuid to group uuid list to include private (non-shared) contacts 75 | $user_group_uuids[] = $_SESSION["user_uuid"]; 76 | 77 | //get contact settings - sync sources 78 | $sql = "select "; 79 | $sql .= "contact_uuid, "; 80 | $sql .= "contact_setting_value "; 81 | $sql .= "from "; 82 | $sql .= "v_contact_settings "; 83 | $sql .= "where "; 84 | $sql .= "domain_uuid = :domain_uuid "; 85 | $sql .= "and contact_setting_category = 'sync' "; 86 | $sql .= "and contact_setting_subcategory = 'source' "; 87 | $sql .= "and contact_setting_name = 'array' "; 88 | $sql .= "and contact_setting_value <> '' "; 89 | $sql .= "and contact_setting_value is not null "; 90 | if (!(if_group("superadmin") || if_group("admin"))) { 91 | $sql .= "and ( "; //only contacts assigned to current user's group(s) and those not assigned to any group 92 | $sql .= " contact_uuid in ( "; 93 | $sql .= " select contact_uuid from v_contact_groups "; 94 | $sql .= " where "; 95 | if (is_array($user_group_uuids) && @sizeof($user_group_uuids) != 0) { 96 | foreach ($user_group_uuids as $index => $user_group_uuid) { 97 | if (is_uuid($user_group_uuid)) { 98 | $sql_where_or[] = "group_uuid = :group_uuid_".$index; 99 | $parameters['group_uuid_'.$index] = $user_group_uuid; 100 | } 101 | } 102 | if (is_array($sql_where_or) && @sizeof($sql_where_or) != 0) { 103 | $sql .= " ( ".implode(' or ', $sql_where_or)." ) "; 104 | } 105 | unset($sql_where_or, $index, $user_group_uuid); 106 | } 107 | $sql .= " and domain_uuid = :domain_uuid "; 108 | $sql .= " ) "; 109 | $sql .= " or "; 110 | $sql .= " contact_uuid not in ( "; 111 | $sql .= " select contact_uuid from v_contact_groups "; 112 | $sql .= " where group_uuid = :group_uuid "; 113 | $sql .= " and domain_uuid = :domain_uuid "; 114 | $sql .= " ) "; 115 | $sql .= ") "; 116 | } 117 | $parameters['domain_uuid'] = $_SESSION['domain_uuid']; 118 | $parameters['group_uuid'] = $_SESSION['group_uuid']; 119 | $database = new database; 120 | $result = $database->select($sql, $parameters, 'all'); 121 | if (is_array($result) && @sizeof($result) != 0) { 122 | foreach($result as $row) { 123 | $contact_sync_sources[$row['contact_uuid']][] = $row['contact_setting_value']; 124 | } 125 | } 126 | unset($sql, $parameters, $result); 127 | 128 | //get variables used to control the order 129 | $order_by = $_GET["order_by"]; 130 | $order = $_GET["order"]; 131 | $user_extension = $_GET["user_extension"]; 132 | 133 | //add the search term 134 | $search = strtolower($_GET["search"]); 135 | if (strlen($search) > 0) { 136 | if (is_numeric($search)) { 137 | $sql_search .= "and contact_uuid in ( "; 138 | $sql_search .= " select contact_uuid from v_contact_phones "; 139 | $sql_search .= " where phone_number like :search "; 140 | $sql_search .= ") "; 141 | } 142 | else { 143 | $sql_search .= "and contact_uuid in ( "; 144 | $sql_search .= " select contact_uuid from v_contacts "; 145 | $sql_search .= " where domain_uuid = :domain_uuid "; 146 | $sql_search .= " and ( "; 147 | $sql_search .= " lower(contact_organization) like :search or "; 148 | $sql_search .= " lower(contact_name_given) like :search or "; 149 | $sql_search .= " lower(contact_name_family) like :search or "; 150 | $sql_search .= " lower(contact_nickname) like :search or "; 151 | $sql_search .= " lower(contact_title) like :search or "; 152 | $sql_search .= " lower(contact_category) like :search or "; 153 | $sql_search .= " lower(contact_role) like :search or "; 154 | $sql_search .= " lower(contact_url) like :search or "; 155 | $sql_search .= " lower(contact_time_zone) like :search or "; 156 | $sql_search .= " lower(contact_note) like :search or "; 157 | $sql_search .= " lower(contact_type) like :search "; 158 | $sql_search .= " ) "; 159 | $sql_search .= ") "; 160 | } 161 | $parameters['search'] = '%'.$search.'%'; 162 | } 163 | 164 | //build query for paging and list 165 | $sql = "select count(*) "; 166 | $sql .= "from v_contacts as c "; 167 | $sql .= "where domain_uuid = :domain_uuid "; 168 | if (!(if_group("superadmin") || if_group("admin"))) { 169 | $sql .= "and ( "; //only contacts assigned to current user's group(s) and those not assigned to any group 170 | $sql .= " contact_uuid in ( "; 171 | $sql .= " select contact_uuid from v_contact_groups "; 172 | $sql .= " where "; 173 | if (is_array($user_group_uuids) && @sizeof($user_group_uuids) != 0) { 174 | foreach ($user_group_uuids as $index => $user_group_uuid) { 175 | if (is_uuid($user_group_uuid)) { 176 | $sql_where_or[] = "group_uuid = :group_uuid_".$index; 177 | $parameters['group_uuid_'.$index] = $user_group_uuid; 178 | } 179 | } 180 | if (is_array($sql_where_or) && @sizeof($sql_where_or) != 0) { 181 | $sql .= " ( ".implode(' or ', $sql_where_or)." ) "; 182 | } 183 | unset($sql_where_or, $index, $user_group_uuid); 184 | } 185 | $sql .= " and domain_uuid = :domain_uuid "; 186 | $sql .= " ) "; 187 | $sql .= " or contact_uuid in ( "; 188 | $sql .= " select contact_uuid from v_contact_users "; 189 | $sql .= " where user_uuid = :user_uuid "; 190 | $sql .= " and domain_uuid = :domain_uuid "; 191 | $sql .= ""; 192 | $sql .= " ) "; 193 | $sql .= ") "; 194 | $parameters['user_uuid'] = $_SESSION['user_uuid']; 195 | } 196 | $sql .= $sql_search; 197 | $parameters['domain_uuid'] = $_SESSION['domain_uuid']; 198 | $database = new database; 199 | $num_rows = $database->select($sql, $parameters, 'column'); 200 | 201 | //prepare to page the results 202 | $rows_per_page = ($_SESSION['domain']['paging']['numeric'] != '') ? $_SESSION['domain']['paging']['numeric'] : 50; 203 | $param = "&search=".$search; 204 | $page = $_GET['page']; 205 | if (strlen($page) == 0) { $page = 0; $_GET['page'] = 0; } 206 | list($paging_controls, $rows_per_page) = paging($num_rows, $param, $rows_per_page); //bottom 207 | list($paging_controls_mini, $rows_per_page) = paging($num_rows, $param, $rows_per_page, true); //top 208 | $offset = $rows_per_page * $page; 209 | 210 | //get the list 211 | $sql = str_replace('count(*)', '*, (select a.contact_attachment_uuid from v_contact_attachments as a where a.contact_uuid = c.contact_uuid and a.attachment_primary = 1) as contact_attachment_uuid', $sql); 212 | if ($order_by != '') { 213 | $sql .= order_by($order_by, $order); 214 | $sql .= ", contact_organization asc "; 215 | } 216 | else { 217 | $contact_default_sort_column = $_SESSION['contacts']['default_sort_column']['text'] != '' ? $_SESSION['contacts']['default_sort_column']['text'] : "last_mod_date"; 218 | $contact_default_sort_order = $_SESSION['contacts']['default_sort_order']['text'] != '' ? $_SESSION['contacts']['default_sort_order']['text'] : "desc"; 219 | 220 | $sql .= order_by($contact_default_sort_column, $contact_default_sort_order); 221 | if ($db_type == "pgsql") { 222 | $sql .= " nulls last "; 223 | } 224 | } 225 | $sql .= limit_offset($rows_per_page, $offset); 226 | $database = new database; 227 | $contacts = $database->select($sql, $parameters, 'all'); 228 | unset($sql, $parameters); 229 | 230 | //get the contact list 231 | $sql = "select * from v_contact_phones "; 232 | $sql .= "where domain_uuid = :domain_uuid "; 233 | $sql .= "and contact_uuid = :contact_uuid "; 234 | $sql .= "order by phone_primary desc, phone_label asc "; 235 | $parameters['domain_uuid'] = $domain_uuid; 236 | $parameters['contact_uuid'] = $contact_uuid; 237 | $database = new database; 238 | $contact_phones = $database->select($sql, $parameters, 'all'); 239 | unset($sql, $parameters); 240 | 241 | 242 | //create token 243 | $object = new token; 244 | $token = $object->create($_SERVER['PHP_SELF']); 245 | 246 | //includes and title 247 | $document['title'] = $text['title-contacts']; 248 | 249 | //show the content 250 | echo " \n"; 251 | echo " \n"; 252 | echo " \n"; 253 | echo " \n"; 254 | echo " \n"; 255 | echo " \n"; 256 | echo " \n"; 257 | echo " \n"; 258 | echo "\n"; 259 | echo " \n"; 260 | 261 | //contact attachment layer 262 | echo "\n"; 274 | echo "\n"; 279 | 280 | echo "\n"; 281 | 282 | //javascript function: send_cmd 283 | echo "\n"; 295 | 296 | 297 | 298 | //echo "
"; 299 | echo "
\n"; 300 | 301 | 302 | //show the content 303 | echo "
\n"; 304 | echo "
Contacts".$text['header-contacts']." (".$num_rows.")
\n"; 305 | echo "
\n"; 306 | echo "\n"; 314 | echo "
\n"; 315 | echo "
\n"; 316 | echo "
\n"; 317 | 318 | echo $text['description-contacts']."\n"; 319 | // echo "

\n"; 320 | // echo "
\n"; 321 | 322 | echo "
\n"; 323 | echo "\n"; 324 | echo "\n"; 325 | 326 | echo "\n"; 327 | echo "\n"; 328 | echo th_order_by('contact_organization', "Company", $order_by, $order); 329 | echo th_order_by('contact_name_given', "Name ", $order_by, $order); 330 | echo th_order_by('contact_name_family', "Surname", $order_by, $order); 331 | echo th_order_by('contact_name_family', "Numbers", $order_by, $order); 332 | echo "\n"; 333 | 334 | if (is_array($contacts) && @sizeof($contacts) != 0) { 335 | $x = 0; 336 | foreach($contacts as $row) { 337 | echo "\n"; 338 | echo " \n"; 339 | echo " \n"; 340 | echo " \n"; 341 | /***************************************************************************/ 342 | //get the contact list 343 | $sql9 = "select * from v_contact_phones "; 344 | $sql9 .= "where domain_uuid = :domain_uuid "; 345 | $sql9 .= "and contact_uuid = :contact_uuid "; 346 | $sql9 .= "order by phone_primary desc, phone_label asc "; 347 | $parameters9['domain_uuid'] = $_SESSION['domain_uuid']; 348 | $parameters9['contact_uuid'] = $row['contact_uuid']; 349 | $database9 = new database; 350 | $contact_phones9 = $database9->select($sql9, $parameters9, 'all'); 351 | unset($sql9, $parameters9); 352 | echo " \n"; 360 | /***************************************************************************/ 361 | echo "\n"; 362 | $x++; 363 | } 364 | unset($contacts); 365 | } 366 | 367 | echo "
".escape($row['contact_organization'])."   ".escape($row['contact_name_given'])."   ".escape($row['contact_name_family'])."   
\n"; 368 | echo "
\n"; 369 | echo "
".$paging_controls."
\n"; 370 | 371 | echo "\n"; 372 | 373 | echo "
\n"; 374 | 375 | //javascript 376 | echo "\n"; 383 | 384 | echo "
\n"; 385 | echo "\n"; 386 | ?> 387 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /saraphone.js: -------------------------------------------------------------------------------- 1 | /* 2 | SaraPhone 3 | Version: MPL 1.1 4 | 5 | The contents of this file are subject to Mozilla Public License Version 6 | 1.1 (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | http://www.mozilla.org/MPL/ 9 | 10 | Software distributed under the License is distributed on an "AS IS" basis, 11 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 12 | for the specific language governing rights and limitations under the 13 | License. 14 | 15 | The Original Code is SaraPhone 16 | 17 | The Initial Developer of the Original Code is 18 | Giovanni Maruzzelli 19 | Portions created by the Initial Developer are Copyright (C) 2020 20 | the Initial Developer. All Rights Reserved. 21 | 22 | SaraPhone gets its name from Giovanni's wife, Sara. 23 | 24 | Author(s): 25 | Giovanni Maruzzelli 26 | Danilo Volpinari 27 | Luca Mularoni 28 | */ 29 | 30 | 'use strict'; 31 | 32 | var cur_call = null; 33 | var ua; 34 | var which_server; 35 | var isAndroid = false; 36 | var isIOS = false; 37 | var isOnMute = false; 38 | var isOnHold = false; 39 | var clicklogin = "no"; 40 | var isRecording = false; 41 | var isDnd = false; 42 | var isNoRing = false; 43 | var isAutoAnswer = false; 44 | var isRegistered = false; 45 | var vmail_subscription = false; 46 | var presence_array = new Array(); 47 | var incomingsession = null; 48 | var audioElement = document.createElement('audio'); 49 | var callTimer; 50 | var oldext = false; 51 | var gotopanel = false; 52 | var isIncomingCall = false; 53 | var isOutboundCall = false; 54 | 55 | var dtmf_options = { 56 | 'duration': 100, 57 | 'interToneGap': 100 58 | }; 59 | 60 | //http://jsfiddle.net/55Kfu/1506/ 61 | //https://stackoverflow.com/posts/13194087/revisions 62 | var beep = (function() { 63 | var ctxClass = window.audioContext || window.AudioContext || window.AudioContext || window.webkitAudioContext 64 | var ctx = new ctxClass(); 65 | return function(duration, type, finishedCallback) { 66 | 67 | duration = +duration; 68 | 69 | // Only 0-4 are valid types. 70 | type = (type % 5) || 0; 71 | 72 | if (typeof finishedCallback != "function") { 73 | finishedCallback = function() {}; 74 | } 75 | 76 | var osc = ctx.createOscillator(); 77 | 78 | //osc.type = type; 79 | osc.type = "sine"; 80 | 81 | osc.connect(ctx.destination); 82 | if (osc.noteOn) osc.noteOn(0); // old browsers 83 | if (osc.start) osc.start(); // new browsers 84 | 85 | setTimeout(function() { 86 | if (osc.noteOff) osc.noteOff(0); // old browsers 87 | if (osc.stop) osc.stop(); // new browsers 88 | finishedCallback(); 89 | }, duration); 90 | 91 | }; 92 | })(); 93 | 94 | function tempAlert(msg,duration) 95 | { 96 | var el = document.createElement("div"); 97 | el.setAttribute("style","position:absolute;top:1%;left:1%;background-color:red;foreground-color:black;"); 98 | el.innerHTML = msg; 99 | setTimeout(function(){ 100 | el.parentNode.removeChild(el); 101 | location.reload(true); 102 | },duration); 103 | document.body.appendChild(el); 104 | console.error("TEMPALERT"); 105 | } 106 | 107 | 108 | 109 | function onCancelled() { 110 | audioElement.pause(); 111 | console.log('cancelled'); 112 | $("#isIncomingcall").hide(); 113 | $("#isNotIncomingcall").show(); 114 | incomingsession = null; 115 | var span = document.getElementById('calling'); 116 | $("#calling_input").val(""); 117 | span.innerText = "..."; 118 | } 119 | 120 | function onTerminated() { 121 | audioElement.pause(); 122 | console.log('Onterminated'); 123 | $("#signin").hide(); 124 | $("#dial").show(); 125 | $("#incall").hide(); 126 | $("#ext").val(""); 127 | if (cur_call) { 128 | cur_call.terminate(); 129 | cur_call = null; 130 | resetOptionsTimer(); 131 | } 132 | isOnMute = false; 133 | 134 | incomingsession = null; 135 | 136 | var span = document.getElementById('calling'); 137 | $("#calling_input").val(""); 138 | span.innerText = "..."; 139 | } 140 | 141 | function onTerminated2() { 142 | console.log('Onterminated2'); 143 | cur_call = null; 144 | incomingsession = null; 145 | } 146 | 147 | function onAccepted() { 148 | audioElement.pause(); 149 | 150 | $("#signin").hide(); 151 | $("#dial").hide(); 152 | $("#incall").show(); 153 | 154 | isOnMute = false; 155 | $("#mutebtn").removeClass('btn-danger').addClass('btn-warning'); 156 | 157 | } 158 | 159 | $("#asknotificationpermission").click(function() { 160 | if (isIOS) { 161 | //do nothing 162 | } else { 163 | // Let's check if the browser supports notifications 164 | if (!("Notification" in window)) { 165 | alert("This browser does not support desktop notification"); 166 | } 167 | 168 | // Otherwise, we need to ask the user for permission 169 | // Note, Chrome does not implement the permission static property 170 | // So we have to check for NOT 'denied' instead of 'default' 171 | else if (Notification.permission !== 'denied') { 172 | Notification.requestPermission(function(permission) { 173 | 174 | // Whatever the user answers, we make sure we store the information 175 | if (!('permission' in Notification)) { 176 | Notification.permission = permission; 177 | } 178 | 179 | // If the user is okay, let's create a notification 180 | if (permission === "granted") { 181 | console.log("Notification Permission Granted!"); 182 | var notification = new Notification("Notification Permission Granted!"); 183 | $("#asknotificationpermission").hide(); 184 | } 185 | }); 186 | } else { 187 | alert(`Permission is ${Notification.permission}`); 188 | } 189 | 190 | } 191 | }); 192 | 193 | 194 | function notifyMe(msg) { 195 | if (isIOS) { 196 | //do nothing 197 | } else { 198 | if (Notification.permission === "granted") { 199 | console.log(msg); 200 | let img = 'img/notification.png'; 201 | let notification = new Notification('WebPhone', { 202 | body: msg, 203 | icon: img 204 | }); 205 | notification.onclick = function() { 206 | parent.focus(); 207 | window.focus(); 208 | this.close(); 209 | }; 210 | notification.onclose = function() { 211 | parent.focus(); 212 | window.focus(); 213 | this.close(); 214 | }; 215 | notification.onerror = function() { 216 | parent.focus(); 217 | window.focus(); 218 | this.close(); 219 | }; 220 | } 221 | } 222 | } 223 | 224 | 225 | function onRegistered() { 226 | if (isIOS) { 227 | //do nothing 228 | } else { 229 | if (Notification.permission === "granted") { 230 | $("#asknotificationpermission").hide(); 231 | } 232 | } 233 | 234 | $("#signin").hide(); 235 | $("#dial").show(); 236 | $("#incall").hide(); 237 | $("#ext").val(""); 238 | var span = document.getElementById('calling'); 239 | $("#calling_input").val(""); 240 | span.innerText = "..."; 241 | 242 | var span = document.getElementById('whoami'); 243 | var txt = document.createTextNode($("#login").val()); 244 | span.innerText = txt.textContent + " (" + $("#yourname").val() + ")"; 245 | 246 | isRegistered = true; 247 | 248 | 249 | var countpres = 1; 250 | 251 | while (countpres < 61) { 252 | if ($("#pres" + countpres).val()) { 253 | presence_array[countpres] = ua.subscribe($("#pres" + countpres).val(), 'presence', { 254 | expires: 120 255 | }); 256 | 257 | const mycountpres = countpres; 258 | presence_array[countpres].on('notify', function(notification) { 259 | //console.log(notification.request.body); 260 | 261 | var presence = notification.request.body.match(/(.*)<\/dm:note>/i); 262 | if (presence) { 263 | var ispresent = presence[1]; 264 | 265 | if (ispresent.match(/unregistered/i)) { 266 | $("#pres" + mycountpres + "btn").removeClass('btn-success btn-warning btn-default btn-danger').addClass('btn-danger'); 267 | } else { 268 | if (ispresent.match(/available/i) || ispresent.match(/closed/i)) { 269 | $("#pres" + mycountpres + "btn").removeClass('btn-success btn-warning btn-default btn-danger').addClass('btn-success'); 270 | 271 | } else { 272 | $("#pres" + mycountpres + "btn").removeClass('btn-success btn-warning btn-default btn-danger').addClass('btn-warning'); 273 | } 274 | } 275 | 276 | var span = document.getElementById('ispresent' + mycountpres); 277 | $("#pres" + mycountpres + "_label").val($("#pres" + mycountpres + "_label").val().substr(0, 10)); 278 | if (ispresent.match(/available/i) || ispresent.match(/closed/i)) { 279 | span.innerText = $("#pres" + mycountpres + "_label").val(); 280 | } else { 281 | span.innerText = $("#pres" + mycountpres + "_label").val() + ": " + ispresent; 282 | } 283 | } 284 | 285 | }); 286 | 287 | 288 | 289 | $("#pres" + mycountpres + "btn").click(function() { 290 | $("#ext").val($("#pres" + mycountpres).val()); 291 | oldext=$("#ext").val(); 292 | docall(); 293 | }); 294 | 295 | 296 | 297 | } else { 298 | 299 | $("#pres" + countpres + "btn").remove(); 300 | 301 | } 302 | countpres++; 303 | } 304 | 305 | $("#webphone_blf").show(); 306 | 307 | 308 | // Once subscribed, receive notifications and handle 309 | vmail_subscription = ua.subscribe($("#login").val() + '@' + $("#domain").val(), 'message-summary', { 310 | extraHeaders: ['Accept: application/simple-message-summary'], 311 | expires: 120 312 | }); 313 | vmail_subscription.on('notify', handleNotify); 314 | 315 | if (isAndroid || isIOS) { 316 | $("#calling_input").hide(); 317 | } 318 | } 319 | 320 | $("#checkvmailbtn").click(function() { 321 | $("#extstarbtn").click(); 322 | $("#ext9btn").click(); 323 | $("#ext8btn").click(); 324 | $("#callbtn").click(); 325 | }); 326 | 327 | $("#gotopanel1").click(function() { 328 | gotopanel = true; 329 | console.error("GOTOPANEL1"); 330 | window.location.assign('/'); 331 | }); 332 | 333 | $("#gotopanel2").click(function() { 334 | gotopanel = true; 335 | console.error("GOTOPANEL2"); 336 | window.location.assign('/'); 337 | }); 338 | 339 | $("#gotopanel3").click(function() { 340 | gotopanel = true; 341 | console.error("GOTOPANEL3"); 342 | window.location.assign('/'); 343 | }); 344 | 345 | function handleNotify(r) { 346 | //console.log(r.request.method); 347 | //console.log(r.request.body); 348 | 349 | var newMessages = 0; 350 | var oldMessages = 0; 351 | var span = document.getElementById('vmailcount'); 352 | var gotmsg = r.request.body.match(/voice-message:\s*(\d+)\/(\d+)/i); 353 | if (gotmsg) { 354 | newMessages = parseInt(gotmsg[1]); 355 | oldMessages = parseInt(gotmsg[2]); 356 | if (newMessages) { 357 | $("#checkvmailbtn").removeClass('btn-info').addClass('btn-warning'); 358 | 359 | } else { 360 | $("#checkvmailbtn").removeClass('btn-warning').addClass('btn-info'); 361 | 362 | } 363 | span.innerText = newMessages + "/" + oldMessages; 364 | } 365 | 366 | 367 | } 368 | 369 | 370 | $("#anscallbtn").click(function() { 371 | audioElement.pause(); 372 | incomingsession.accept({ 373 | media: { 374 | constraints: { 375 | audio: { 376 | deviceId: { 377 | ideal: $("#selectmic").val() 378 | } 379 | }, 380 | video: false 381 | }, 382 | render: { 383 | remote: document.getElementById('audio') 384 | } 385 | } 386 | }); 387 | console.log('answered'); 388 | 389 | $("#isIncomingcall").hide(); 390 | $("#isNotIncomingcall").show(); 391 | cur_call = incomingsession; 392 | var span = document.getElementById('speakingwith'); 393 | var txt = document.createTextNode(cur_call.remoteIdentity.displayName.toString()); 394 | span.innerText = txt.textContent + " (" + cur_call.remoteIdentity.uri.user.toString() + ")"; 395 | 396 | cur_call.on('accepted', onAccepted.bind(cur_call)); 397 | cur_call.once('bye', onTerminated.bind(cur_call)); 398 | cur_call.once('failed', onTerminated.bind(cur_call)); 399 | cur_call.once('cancel', onTerminated.bind(cur_call)); 400 | cur_call.once('terminated', onTerminated2.bind(cur_call)); 401 | }); 402 | 403 | 404 | $("#rejcallbtn").click(function() { 405 | audioElement.pause(); 406 | incomingsession.reject({ 407 | statusCode: '486', 408 | reasonPhrase: 'Busy Here 1' 409 | }); 410 | console.log('rejected'); 411 | $("#isIncomingcall").hide(); 412 | $("#isNotIncomingcall").show(); 413 | var span = document.getElementById('calling'); 414 | $("#calling_input").val(""); 415 | span.innerText = "..."; 416 | incomingsession = null; 417 | }); 418 | 419 | 420 | 421 | function handleInvite(s) { 422 | if (cur_call) { 423 | s.reject({ 424 | statusCode: '486', 425 | reasonPhrase: 'Busy Here 2' 426 | }); 427 | } 428 | if (isDnd) { 429 | s.reject({ 430 | statusCode: '486', 431 | reasonPhrase: 'Busy Here 3' 432 | }); 433 | } else { 434 | if (!cur_call) { 435 | var span = document.getElementById('calling'); 436 | var txt = "---"; 437 | isIncomingCall = true; 438 | isOutboundCall = false; 439 | if(s.remoteIdentity.displayName && s.remoteIdentity.displayName.toString()) { 440 | txt = document.createTextNode(s.remoteIdentity.displayName.toString()); 441 | } 442 | span.innerText = "CALL FROM: " + txt.textContent + " (" + s.remoteIdentity.uri.user.toString() + ")"; 443 | incomingsession = s; 444 | $("#isIncomingcall").show(); 445 | $("#isNotIncomingcall").hide(); 446 | incomingsession.once('cancel', onCancelled.bind(incomingsession)); 447 | 448 | if (isIOS) { 449 | //do nothing 450 | } else { 451 | notifyMe("CALL FROM: " + txt.textContent + " (" + s.remoteIdentity.uri.user.toString() + ")"); 452 | } 453 | 454 | if (isNoRing == false) { 455 | audioElement.currentTime = 0; 456 | audioElement.play(); 457 | } 458 | if (isAutoAnswer == true) { 459 | $("#anscallbtn").trigger("click"); 460 | } 461 | } 462 | } 463 | } 464 | 465 | 466 | function docall() { 467 | 468 | if (cur_call) { 469 | cur_call.terminate(); 470 | cur_call = null; 471 | resetOptionsTimer(); 472 | } 473 | 474 | isIncomingCall = false; 475 | isOutboundCall = true; 476 | 477 | cur_call = ua.invite($("#ext").val(), { 478 | media: { 479 | constraints: { 480 | audio: { 481 | deviceId: { 482 | ideal: $("#selectmic").val() 483 | } 484 | }, 485 | video: false 486 | }, 487 | render: { 488 | remote: document.getElementById('audio') 489 | } 490 | } 491 | }); 492 | 493 | cur_call.on('accepted', onAccepted.bind(cur_call)); 494 | cur_call.once('failed', function(response, cause) { 495 | if (cause != "null") { 496 | console.log(cause); 497 | } else { 498 | cause = "N/A"; 499 | } 500 | var span = document.getElementById('calling'); 501 | onTerminated(cur_call); 502 | var txt = document.createTextNode(response.status_code + ": " + cause); 503 | span.innerText = txt.textContent; 504 | }) 505 | 506 | cur_call.once('bye', function(request) { 507 | if (request.headers.Reason && !(request.headers.Reason["0"].raw.toString().match(/cause=16/)) && !(request.headers.Reason["0"].raw.toString().match(/cause=31/))) { 508 | console.log(request); 509 | var span = document.getElementById('calling'); 510 | onTerminated(cur_call); 511 | var regex = /.*text="(.*)".*/; 512 | var txt = document.createTextNode(request.headers.Reason["0"].raw.toString().replace(regex, "$1")); 513 | span.innerText = txt.textContent; 514 | } else { 515 | onTerminated(cur_call); 516 | } 517 | }) 518 | cur_call.once('cancel', onTerminated.bind(cur_call)); 519 | 520 | var span = document.getElementById('speakingwith'); 521 | var txt = document.createTextNode($("#ext").val()); 522 | span.innerText = txt.textContent; 523 | } 524 | 525 | 526 | $("#dialctrlbtn").click(function() { 527 | var x = document.getElementById('dialadv1'); 528 | if (x.style.display === 'none') { 529 | x.style.display = 'block'; 530 | } else { 531 | x.style.display = 'none'; 532 | } 533 | x = document.getElementById('dialadv2'); 534 | if (x.style.display === 'none') { 535 | x.style.display = 'block'; 536 | } else { 537 | x.style.display = 'none'; 538 | } 539 | 540 | 541 | }); 542 | 543 | $("#signinctrlbtn").click(function() { 544 | var x = document.getElementById('signinadv1'); 545 | if (x.style.display === 'none') { 546 | x.style.display = 'block'; 547 | } else { 548 | x.style.display = 'none'; 549 | } 550 | }); 551 | 552 | 553 | 554 | $("#callbtn").click(function() { 555 | if ($("#ext").val()) { 556 | var regex1 = /#/g; 557 | var new_ext = $("#ext").val().replace(regex1, "_"); 558 | $("#ext").val(new_ext); 559 | oldext=$("#ext").val(); 560 | docall(); 561 | } 562 | }); 563 | 564 | $("#delcallbtn").click(function() { 565 | $("#ext").val(""); 566 | $("#calling_input").val(""); 567 | var span = document.getElementById('calling'); 568 | span.innerText = "..."; 569 | 570 | $("#hangupbtn").trigger("click"); 571 | }); 572 | 573 | 574 | $("#hangupbtn").click(function() { 575 | if (cur_call) { 576 | cur_call.terminate(); 577 | cur_call = null; 578 | resetOptionsTimer(); 579 | } 580 | $("#br").show(); 581 | $("#ext").show(); 582 | $("#calling_input").val(""); 583 | var span = document.getElementById('calling'); 584 | span.innerText = "..."; 585 | }); 586 | 587 | 588 | $("#loginbtn").click(function() { 589 | init(); 590 | }); 591 | 592 | $("#xferbtn").click(function() { 593 | if(isOutboundCall==true){ 594 | cur_call.dtmf("*499", dtmf_options); 595 | }else{ 596 | cur_call.dtmf("*599", dtmf_options); 597 | } 598 | }); 599 | 600 | $("#attxbtn").click(function() { 601 | if(isOutboundCall==true){ 602 | cur_call.dtmf("*699", dtmf_options); 603 | }else{ 604 | cur_call.dtmf("*799", dtmf_options); 605 | } 606 | }); 607 | 608 | $("#mutebtn").click(function() { 609 | if (isOnMute) { 610 | cur_call.unmute(); 611 | isOnMute = false; 612 | $(this).removeClass('btn-danger').addClass('btn-warning'); 613 | } else { 614 | cur_call.mute(); 615 | isOnMute = true; 616 | 617 | $(this).removeClass('btn-warning').addClass('btn-danger'); 618 | } 619 | }); 620 | 621 | $("#holdbtn").click(function() { 622 | if (isOnHold==false){ 623 | isOnHold = true; 624 | if(isOutboundCall==true){ 625 | cur_call.dtmf("*299", dtmf_options); 626 | }else{ 627 | cur_call.dtmf("*399", dtmf_options); 628 | } 629 | $("#unholdbtn").show(); 630 | console.error("HOLD begins"); 631 | } 632 | }); 633 | 634 | $("#unholdbtn").click(function() { 635 | if (isOnHold == true){ 636 | isOnHold = false; 637 | $("#extstarbtn").click(); 638 | $("#ext6btn").click(); 639 | $("#ext5btn").click(); 640 | $("#ext5btn").click(); 641 | $("#callbtn").click(); 642 | $("#unholdbtn").hide(); 643 | console.error("HOLD ends"); 644 | } 645 | }); 646 | 647 | $("#redialbtn").click(function() { 648 | audioElement.pause(); 649 | $("#ext").val(oldext); 650 | $("#callbtn").click(); 651 | }); 652 | 653 | $("#callbackbtn").click(function() { 654 | audioElement.pause(); 655 | $("#extstarbtn").click(); 656 | $("#ext6btn").click(); 657 | $("#ext9btn").click(); 658 | $("#callbtn").click(); 659 | }); 660 | 661 | $("#recordcallbtn").click(function() { 662 | if (isRecording) { 663 | cur_call.dtmf("*"); 664 | cur_call.dtmf("2"); 665 | isRecording = false; 666 | $(this).removeClass('btn-danger').addClass('btn-warning'); 667 | } else { 668 | cur_call.dtmf("*"); 669 | cur_call.dtmf("2"); 670 | isRecording = true; 671 | 672 | $(this).removeClass('btn-warning').addClass('btn-danger'); 673 | } 674 | }); 675 | 676 | $("#dndbtn").click(function() { 677 | if (isDnd) { 678 | isDnd = false; 679 | $(this).removeClass('btn-danger').addClass('btn-warning'); 680 | } else { 681 | isDnd = true; 682 | $(this).removeClass('btn-warning').addClass('btn-danger'); 683 | } 684 | }); 685 | 686 | $("#ringbtn").click(function() { 687 | if (isNoRing) { 688 | isNoRing = false; 689 | $(this).removeClass('btn-danger').addClass('btn-warning'); 690 | } else { 691 | isNoRing = true; 692 | $(this).removeClass('btn-warning').addClass('btn-danger'); 693 | } 694 | }); 695 | 696 | $("#autoanswerbtn").click(function() { 697 | if (isAutoAnswer) { 698 | isAutoAnswer = false; 699 | $(this).removeClass('btn-danger').addClass('btn-warning'); 700 | } else { 701 | isAutoAnswer = true; 702 | $(this).removeClass('btn-warning').addClass('btn-danger'); 703 | } 704 | }); 705 | 706 | 707 | 708 | $("#ext1btn").click(function() { 709 | $("#ext").val($("#ext").val() + "1"); 710 | var span = document.getElementById('calling'); 711 | var txt = document.createTextNode($("#ext").val()); 712 | span.innerText = "DIALING: " + txt.textContent; 713 | }); 714 | 715 | $("#ext2btn").click(function() { 716 | $("#ext").val($("#ext").val() + "2"); 717 | var span = document.getElementById('calling'); 718 | var txt = document.createTextNode($("#ext").val()); 719 | span.innerText = "DIALING: " + txt.textContent; 720 | }); 721 | 722 | $("#ext3btn").click(function() { 723 | $("#ext").val($("#ext").val() + "3"); 724 | var span = document.getElementById('calling'); 725 | var txt = document.createTextNode($("#ext").val()); 726 | span.innerText = "DIALING: " + txt.textContent; 727 | }); 728 | 729 | $("#ext4btn").click(function() { 730 | $("#ext").val($("#ext").val() + "4"); 731 | var span = document.getElementById('calling'); 732 | var txt = document.createTextNode($("#ext").val()); 733 | span.innerText = "DIALING: " + txt.textContent; 734 | }); 735 | 736 | $("#ext5btn").click(function() { 737 | $("#ext").val($("#ext").val() + "5"); 738 | var span = document.getElementById('calling'); 739 | var txt = document.createTextNode($("#ext").val()); 740 | span.innerText = "DIALING: " + txt.textContent; 741 | }); 742 | 743 | $("#ext6btn").click(function() { 744 | $("#ext").val($("#ext").val() + "6"); 745 | var span = document.getElementById('calling'); 746 | var txt = document.createTextNode($("#ext").val()); 747 | span.innerText = "DIALING: " + txt.textContent; 748 | }); 749 | 750 | $("#ext7btn").click(function() { 751 | $("#ext").val($("#ext").val() + "7"); 752 | var span = document.getElementById('calling'); 753 | var txt = document.createTextNode($("#ext").val()); 754 | span.innerText = "DIALING: " + txt.textContent; 755 | }); 756 | 757 | $("#ext8btn").click(function() { 758 | $("#ext").val($("#ext").val() + "8"); 759 | var span = document.getElementById('calling'); 760 | var txt = document.createTextNode($("#ext").val()); 761 | span.innerText = "DIALING: " + txt.textContent; 762 | }); 763 | 764 | $("#ext9btn").click(function() { 765 | $("#ext").val($("#ext").val() + "9"); 766 | var span = document.getElementById('calling'); 767 | var txt = document.createTextNode($("#ext").val()); 768 | span.innerText = "DIALING: " + txt.textContent; 769 | }); 770 | 771 | $("#ext0btn").click(function() { 772 | $("#ext").val($("#ext").val() + "0"); 773 | var span = document.getElementById('calling'); 774 | var txt = document.createTextNode($("#ext").val()); 775 | span.innerText = "DIALING: " + txt.textContent; 776 | }); 777 | 778 | $("#extstarbtn").click(function() { 779 | $("#ext").val($("#ext").val() + "*"); 780 | var span = document.getElementById('calling'); 781 | var txt = document.createTextNode($("#ext").val()); 782 | span.innerText = "DIALING: " + txt.textContent; 783 | }); 784 | 785 | $("#extpoundbtn").click(function() { 786 | $("#ext").val($("#ext").val() + "#"); 787 | var span = document.getElementById('calling'); 788 | var txt = document.createTextNode($("#ext").val()); 789 | span.innerText = "DIALING: " + txt.textContent; 790 | }); 791 | 792 | $("#dtmf1btn").click(function() { 793 | cur_call.dtmf("1", dtmf_options); 794 | }); 795 | 796 | $("#dtmf2btn").click(function() { 797 | cur_call.dtmf("2", dtmf_options); 798 | }); 799 | 800 | $("#dtmf3btn").click(function() { 801 | cur_call.dtmf("3", dtmf_options); 802 | }); 803 | 804 | $("#dtmf4btn").click(function() { 805 | cur_call.dtmf("4", dtmf_options); 806 | }); 807 | 808 | $("#dtmf5btn").click(function() { 809 | cur_call.dtmf("5", dtmf_options); 810 | }); 811 | 812 | $("#dtmf6btn").click(function() { 813 | cur_call.dtmf("6", dtmf_options); 814 | }); 815 | 816 | $("#dtmf7btn").click(function() { 817 | cur_call.dtmf("7", dtmf_options); 818 | }); 819 | 820 | $("#dtmf8btn").click(function() { 821 | cur_call.dtmf("8", dtmf_options); 822 | }); 823 | 824 | $("#dtmf9btn").click(function() { 825 | cur_call.dtmf("9", dtmf_options); 826 | }); 827 | 828 | $("#dtmf0btn").click(function() { 829 | cur_call.dtmf("0", dtmf_options); 830 | }); 831 | 832 | $("#dtmfstarbtn").click(function() { 833 | cur_call.dtmf("*", dtmf_options); 834 | }); 835 | 836 | $("#dtmfpoundbtn").click(function() { 837 | cur_call.dtmf("#", dtmf_options); 838 | }); 839 | 840 | 841 | function resetOptionsTimer() { 842 | /* 843 | window.clearTimeout(callTimer); 844 | 845 | callTimer = window.setTimeout(function() { 846 | console.error("NETWORK DISCONNECT, NO OPTIONS SINCE 25000 msec"); 847 | beep(1000, 2); 848 | if (cur_call) { 849 | alert("NETWORK DISCONNECT, CLICK OK TO PROCEED"); 850 | } 851 | $("#hangupbtn").trigger("click"); 852 | if (gotopanel == false){ 853 | location.reload(); 854 | } 855 | }, 25000); 856 | */ 857 | } 858 | 859 | function init() { 860 | 861 | var nameDomain; 862 | var nameProxy; 863 | var uri; 864 | var password; 865 | var login; 866 | var yourname; 867 | var wssport; 868 | 869 | cur_call = null; 870 | resetOptionsTimer(); 871 | yourname = $("#yourname").val(); 872 | nameDomain = $("#domain").val(); 873 | nameProxy = $("#proxy").val(); 874 | wssport = $("#port").val(); 875 | which_server = "wss://" + nameProxy + ":" + wssport; 876 | 877 | if (yourname === "") { 878 | yourname = $("#login").val(); 879 | } 880 | 881 | login = $("#login").val(); 882 | password = $("#passwd").val(); 883 | 884 | uri = login + "@" + nameDomain; 885 | 886 | //console.error("uri: " + uri); 887 | 888 | ua = new SIP.UA({ 889 | wsServers: which_server, 890 | uri: uri, 891 | password: password, 892 | userAgentString: 'SIP.js/0.7.8 SaraPhone 04', 893 | traceSip: true, 894 | displayName: yourname, 895 | iceCheckingTimeout: 1000, 896 | registerExpires: 120, 897 | allowLegacyNotifications: true, 898 | hackWssInTransport: true, 899 | wsServerMaxReconnection: 5000, 900 | wsServerReconnectionTimeout: 1, 901 | connectionRecoveryMaxInterval: 3, 902 | connectionRecoveryMinInterval: 2, 903 | log: { 904 | level: 2, 905 | connector: function(level, category, label, content) { 906 | var str = content; 907 | var patt2 = new RegExp("WebSocket abrupt disconnection"); 908 | var res2 = patt2.exec(str); 909 | /* 910 | var patt = new RegExp("OPTIONS sip"); 911 | var res = patt.exec(str); 912 | 913 | if (res) { 914 | resetOptionsTimer(); 915 | } 916 | */ 917 | if (res2) { 918 | if (gotopanel == false){ 919 | console.error('WebSocket ABRUPT DISCONNECTION'); 920 | tempAlert("- WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - WebSocket ABRUPT DISCONNECTION - ",10000); 921 | } 922 | } 923 | }, 924 | } 925 | }); 926 | 927 | ua.on('notify', handleNotify); 928 | ua.on('invite', handleInvite); 929 | ua.on('disconnected', function() { 930 | console.error('DISCONNECTED'); 931 | //alert("DO YOU HAVE AUTHORIZED SSL CERTS FOR PORT 7443 ???? - READ THE README! :) - NETWORK DISCONNECT, CLICK OK TO PROCEED"); 932 | if (gotopanel == false){ 933 | tempAlert("- NETWORK DISCONNECTED - NETWORK DISCONNECTED - NETWORK DISCONNECTED - NETWORK DISCONNECTED - DO YOU HAVE WSS PORT OPEN ON FIREWALL? DO YOU HAVE AUTHORIZED SSL CERTS? AND YOUR WSS CERTS, ARE AUTHORIZED? - READ THE README! :) - NETWORK DISCONNECTED - NETWORK DISCONNECTED - NETWORK DISCONNECTED - NETWORK DISCONNECTED - ",60000); 934 | } 935 | }); 936 | 937 | $("#isIncomingcall").hide(); 938 | 939 | $(document).keyup(function(event) { 940 | if (event.keyCode == 13 && !event.shiftKey) { 941 | if (isRegistered) { 942 | if (cur_call) {} else { 943 | $("#callbtn").trigger("click"); 944 | } 945 | } 946 | } 947 | }); 948 | 949 | $(document).keypress(function(event) { 950 | var key = String.fromCharCode(event.keyCode || event.charCode); 951 | var i = parseInt(key); 952 | var tag = event.target.tagName.toLowerCase(); 953 | if (isRegistered) { 954 | if (cur_call) { 955 | if (key === "#" || key === "*" || key === "0" || (i > 0 && i <= 9)) { 956 | cur_call.dtmf(key, dtmf_options); 957 | } 958 | } else { 959 | 960 | if (key === "#" || key === "*" || key === "0" || (i > 0 && i <= 9)) { 961 | 962 | if (key === "0") $("#ext0btn").click(); 963 | if (key === "1") $("#ext1btn").click(); 964 | if (key === "2") $("#ext2btn").click(); 965 | if (key === "3") $("#ext3btn").click(); 966 | if (key === "4") $("#ext4btn").click(); 967 | if (key === "5") $("#ext5btn").click(); 968 | if (key === "6") $("#ext6btn").click(); 969 | if (key === "7") $("#ext7btn").click(); 970 | if (key === "8") $("#ext8btn").click(); 971 | if (key === "9") $("#ext9btn").click(); 972 | if (key === "*") $("#extstarbtn").click(); 973 | if (key === "#") $("#extpoundbtn").click(); 974 | } 975 | } 976 | } 977 | }); 978 | 979 | ua.once('registered', onRegistered.bind(cur_call)); 980 | ua.on('unregistered', function() { 981 | console.error('UNREGISTERED'); 982 | if (gotopanel == false){ 983 | tempAlert("- UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - UNREGISTERED - ",3000); 984 | } 985 | }); 986 | } 987 | 988 | $("#calling_input").keyup(function(event) { 989 | if (event.keyCode == 13 && !event.shiftKey) { 990 | $("#ext").val($("#calling_input").val()); 991 | $("#callbtn").trigger("click"); 992 | } 993 | }); 994 | 995 | /* 996 | window.onbeforeunload = function(e) { 997 | e = e || window.event; 998 | 999 | console.log("closing window"); 1000 | 1001 | // For IE and Firefox prior to version 4 1002 | if (e) { 1003 | e.returnValue = "Sure?"; 1004 | } 1005 | 1006 | return "Sure?"; 1007 | }; 1008 | */ 1009 | 1010 | $(window).load(function() { 1011 | cur_call = null; 1012 | resetOptionsTimer(); 1013 | isAndroid = (navigator.userAgent.toLowerCase().indexOf('android') > -1); 1014 | isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); 1015 | 1016 | console.log("The doctor is in"); 1017 | console.log("Is something troubling you?"); 1018 | 1019 | 1020 | var url_string = window.location.href; //window.location.href 1021 | var url = new URL(url_string); 1022 | 1023 | clicklogin = url.searchParams.get("clicklogin"); 1024 | 1025 | $("#signin").hide(); 1026 | $("#dial").hide(); 1027 | $("#incall").hide(); 1028 | 1029 | $("#controls").hide(); 1030 | $("#dialadv1").hide(); 1031 | $("#dialadv2").hide(); 1032 | $("#unholdbtn").hide(); 1033 | 1034 | $("#yourname").keyup(function(event) { 1035 | if (event.keyCode == 13 && !event.shiftKey) { 1036 | $("#loginbtn").trigger("click"); 1037 | } 1038 | }); 1039 | 1040 | $("#passwd").keyup(function(event) { 1041 | if (event.keyCode == 13 && !event.shiftKey) { 1042 | $("#loginbtn").trigger("click"); 1043 | } 1044 | }); 1045 | $("#login").keyup(function(event) { 1046 | if (event.keyCode == 13 && !event.shiftKey) { 1047 | $("#loginbtn").trigger("click"); 1048 | } 1049 | }); 1050 | $("#ext").keyup(function(event) { 1051 | if (event.keyCode == 13 && !event.shiftKey) { 1052 | $("#callbtn").trigger("click"); 1053 | } 1054 | }); 1055 | 1056 | 1057 | // Safari requires the user to grant device access before providing 1058 | // all necessary device info, so do that first. 1059 | var constraints = { 1060 | audio: true, 1061 | video: false, 1062 | }; 1063 | navigator.mediaDevices.getUserMedia(constraints); 1064 | 1065 | navigator.mediaDevices.enumerateDevices() 1066 | .then(function(devices) { 1067 | var i = 1; 1068 | var div = document.querySelector("#listmic"), 1069 | frag = document.createDocumentFragment(), 1070 | selectmic = document.createElement("select"); 1071 | 1072 | while (div.firstChild) { 1073 | div.removeChild(div.firstChild); 1074 | } 1075 | i = 1; 1076 | selectmic.id = "selectmic"; 1077 | selectmic.style = "background-color: black;"; 1078 | 1079 | devices.forEach(function(device) { 1080 | 1081 | 1082 | if (device.kind === 'audioinput') { 1083 | 1084 | selectmic.options.add(new Option('Microphone: ' + (device.label ? device.label : (i)), device.deviceId)); 1085 | i++; 1086 | 1087 | } 1088 | }); 1089 | 1090 | frag.appendChild(selectmic); 1091 | 1092 | div.appendChild(frag); 1093 | 1094 | }) 1095 | .catch(function(err) { 1096 | console.log(err.name + ": " + err.message); 1097 | }); 1098 | 1099 | document.getElementById("hideAll").style.display = "none"; 1100 | $("#signin").show(); 1101 | $("#signinadv1").hide(); 1102 | 1103 | $("#webphone_blf").hide(); 1104 | 1105 | if (clicklogin === "yes") { 1106 | $("#loginbtn").trigger("click"); 1107 | } 1108 | }); 1109 | 1110 | 1111 | $(document).ready(function() { 1112 | audioElement.setAttribute('src', 'mp3/ring.mp3'); 1113 | setupCacheHandler(); 1114 | }); 1115 | 1116 | $("#phonebookbtn").click(function() { 1117 | var x = document.getElementById('phonebook'); 1118 | if (x.style.display === 'none') { 1119 | x.style.display = 'block'; 1120 | } else { 1121 | x.style.display = 'none'; 1122 | } 1123 | }); 1124 | 1125 | var cacheItems = ['login', 'passwd', 'yourname', 'domain', 'proxy', 'port', 1126 | 'pres1', 'pres1_label', 1127 | 'pres2', 'pres2_label', 1128 | 'pres3', 'pres3_label', 1129 | 'pres4', 'pres4_label', 1130 | 'pres5', 'pres5_label', 1131 | 'pres6', 'pres6_label', 1132 | 'pres7', 'pres7_label', 1133 | 'pres8', 'pres8_label', 1134 | 'pres9', 'pres9_label', 1135 | 'pres10', 'pres10_label', 1136 | 1137 | ]; 1138 | 1139 | function setupCacheHandler() { 1140 | for(var i = 0; i < cacheItems.length; i++) { 1141 | var key = cacheItems[i]; 1142 | var value = localStorage.getItem("saraphone." + key); 1143 | if (value) document.getElementById(key).value = value; 1144 | $("#" + key).change(function(e) {localStorage.setItem("saraphone." + e.target.id, e.target.value);}); 1145 | } 1146 | } 1147 | -------------------------------------------------------------------------------- /saraphone.html: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | SaraPhone WebRTC 42 | 43 | 44 | 45 | 46 | 47 | 62 |
63 |
64 |

Wait please...

65 |
66 |
67 | 68 | 474 |
475 | 476 | 477 | 478 | 479 | 483 |
484 | 485 | 486 | --------------------------------------------------------------------------------