1 /** 2 * @fileOverview CWIC is a jQuery plug-in to access the Cisco Web Communicator.<br> 3 * Audio and Video media require the Cisco Web Communicator add-on to be installed.<br> 4 * @version REL.MAJ.MIN.BUILD, Unified Communications System Release SYSMAJ.SYSMIN 5 */ 6 7 /* 8 CWIC is a jQuery plug-in to access the Cisco Web Communicator 9 10 CWIC uses jQuery features such as:<ul> 11 <li>'cwic' namespacing: jQuery.cwic();</li> 12 <li>custom events (.cwic namespace): conversationStart.cwic</li> 13 <li>attach data with the 'cwic' key: ui.data('cwic', conversation)</li> 14 </ul> 15 16 Audio and Video media require the Cisco Web Communicator add-on to be installed 17 18 */ 19 /** 20 * Global window namespace 21 * @name window 22 * @namespace 23 */ 24 25 (function ($) { 26 'use strict'; 27 /** @scope $.fn.cwic */ 28 29 // a global reference to the CWC native plugin API 30 var _plugin = null; 31 32 // client id to differentiate between cwic instances in separate tabs (generated only once per cwic instance) 33 var clientId; 34 35 // we have to keep the reference to one of the video objects for active conversation. 36 // This is neccessary for calling "removeWindowFromCall" after conversation is ended. 37 var activeConversation = { 38 videoObject: null, // first video object added to conversation 39 window: null, 40 lastId: -1 // see addWindowToCall() for explanation 41 }; 42 43 // jsdoc does not seem to like enumerating properties (fields) of objects it already considers as properties (fields). 44 /** cwic error object 45 * @name $.fn.cwic-errorMapEntry 46 * @namespace 47 * @property {String} code a unique error identifier 48 * @property {String} message the message associated with the error 49 * @property {Any} [propertyName] Additional properties that will be passed back when an error is raised. 50 */ 51 52 /** 53 * The error map used to build errors triggered by cwic. <br> 54 * Keys are error codes (numbers), values objects associated to codes. <br> 55 * By default the error object contains a single 'message' property. <br> 56 * The error map can be customized via the init function. <br> 57 * @namespace 58 */ 59 var errorMap = { 60 61 /** Unknown: unknown error or exception 62 * @type $.fn.cwic-errorMapEntry 63 */ 64 Unknown: { 65 code: 'Unknown', 66 message: 'Unknown error' 67 }, 68 69 /** PluginNotAvailable: plugin not available (not installed, not enabled or unable to load) 70 * @type $.fn.cwic-errorMapEntry 71 */ 72 PluginNotAvailable: { 73 code: 'PluginNotAvailable', 74 message: 'Plugin not available' 75 }, 76 77 /** BrowserNotSupported: browser not supported 78 * @type $.fn.cwic-errorMapEntry 79 */ 80 BrowserNotSupported: { 81 code: 'BrowserNotSupported', 82 message: 'Browser not supported' 83 }, 84 85 /** InvalidArguments: invalid arguments 86 * @type $.fn.cwic-errorMapEntry 87 */ 88 InvalidArguments: { 89 code: 'InvalidArguments', 90 message: 'Invalid arguments' 91 }, 92 93 /** InvalidState: invalid state for operation (e.g. startconversation when phone is not registered) 94 * @type $.fn.cwic-errorMapEntry 95 */ 96 InvalidState: { 97 code: 'InvalidState', 98 message: 'Invalid State' 99 }, 100 101 /** NativePluginError: plugin returned an error 102 * @type $.fn.cwic-errorMapEntry 103 */ 104 NativePluginError: { 105 code: 'NativePluginError', 106 message: 'Native plugin error' 107 }, 108 109 /** OperationNotSupported: operation not supported 110 * @type $.fn.cwic-errorMapEntry 111 */ 112 OperationNotSupported: { 113 code: 'OperationNotSupported', 114 message: 'Operation not supported' 115 }, 116 117 /** InvalidTFTPServer: The configured TFTP server is incorrect 118 * @type $.fn.cwic-errorMapEntry 119 */ 120 InvalidTFTPServer: { 121 code: 'InvalidTFTPServer', 122 message: 'The configured TFTP server is incorrect' 123 }, 124 /** InvalidCCMCIPServer: The configured CCMCIP server is incorrect 125 * @type $.fn.cwic-errorMapEntry 126 */ 127 InvalidCCMCIPServer: { 128 code: 'InvalidCCMCIPServer', 129 message: 'The configured CCMCIP server is incorrect' 130 }, 131 /** InvalidCTIServer: The configured CTI server is incorrect 132 * @type $.fn.cwic-errorMapEntry 133 */ 134 InvalidCTIServer: { 135 code: 'InvalidCTIServer', 136 message: 'The configured CTI server is incorrect' 137 }, 138 /** ReleaseMismatch: release mismatch 139 * @type $.fn.cwic-errorMapEntry 140 */ 141 ReleaseMismatch: { 142 code: 'ReleaseMismatch', 143 message: 'Release mismatch' 144 }, 145 146 /** NoDevicesFound: no devices found for supplied credentials 147 * @type $.fn.cwic-errorMapEntry 148 */ 149 NoDevicesFound: { 150 code: 'NoDevicesFound', 151 message: 'No devices found' 152 }, 153 154 /** TooManyPluginInstances: already logged in in another process (browser or window in internet explorer) 155 * @type $.fn.cwic-errorMapEntry 156 */ 157 TooManyPluginInstances: { 158 code: 'TooManyPluginInstances', 159 message: 'Too many plug-in instances' 160 }, 161 162 /** AuthenticationFailure: authentication failed - invalid or missing credentials/token or incorrect server parameters 163 * @type $.fn.cwic-errorMapEntry 164 */ 165 AuthenticationFailure: { 166 code: 'AuthenticationFailure', 167 message: 'Authentication failed' 168 }, 169 170 /** SignInError: other sign-in error 171 * @type $.fn.cwic-errorMapEntry 172 */ 173 SignInError: { 174 code: 'SignInError', 175 message: 'Sign-in Error' 176 }, 177 178 /** CallControlError: error performing call control operation 179 * @type $.fn.cwic-errorMapEntry 180 */ 181 CallControlError: { 182 code: 'CallControlError', 183 message: 'Call control error' 184 }, 185 186 /** PhoneConfigGenError: other phone configuration error 187 * @type $.fn.cwic-errorMapEntry 188 */ 189 PhoneConfigGenError: { 190 code: 'PhoneConfigGenError', 191 message: 'Phone configuration error' 192 }, 193 194 /** CreateCallError: error creating a new call. Possible causes:<br> 195 * - the device is not available anymore<br> 196 * - the maximum number of active calls configured on the user's line was reached<br> 197 * @type $.fn.cwic-errorMapEntry 198 */ 199 CreateCallError: { 200 code: 'CreateCallError', 201 message: 'Cannot create call' 202 }, 203 204 /** NetworkError: No network connection or SSL/TLS connection error 205 * @type $.fn.cwic-errorMapEntry 206 */ 207 NetworkError: { 208 code: 'NetworkError', 209 message: 'Network error' 210 }, 211 212 /** VideoWindowError: error modifying video association (e.g. removing non-attached window or adding non-existing window) 213 * @type $.fn.cwic-errorMapEntry 214 */ 215 VideoWindowError: { 216 code: 'VideoWindowError', 217 message: 'Video window error' 218 }, 219 220 /** CapabilityMissing: Capability missing (e.g. no capability to merge call to conference or to transfer a call) 221 * @type $.fn.cwic-errorMapEntry 222 */ 223 CapabilityMissing: { 224 code: 'CapabilityMissing', 225 message: 'Capability Missing' 226 }, 227 228 /** NotUserAuthorized: user did not authorize the add-on to run 229 * @type $.fn.cwic-errorMapEntry 230 */ 231 NotUserAuthorized: { 232 code: 'NotUserAuthorized', 233 message: 'User did not authorize access' 234 }, 235 236 /** OperationFailed: System not started or fully operational 237 * @type $.fn.cwic-errorMapEntry 238 */ 239 OperationFailed: { 240 code: 'OperationFailed', 241 message: 'Operation Failed' 242 }, 243 244 /** ExtensionNotAvailable: browser extension not available (not installed, not enabled or unable to load) 245 * @type $.fn.cwic-errorMapEntry 246 */ 247 ExtensionNotAvailable: { 248 code: 'ExtensionNotAvailable', 249 message: 'Browser extension not available' 250 }, 251 252 /** DockingCapabilitiesNotAvailable: External video docking capabilities not available 253 * @type $.fn.cwic-errorMapEntry 254 */ 255 DockingCapabilitiesNotAvailable: { 256 code: 'DockingCapabilitiesNotAvailable', 257 message: 'External video docking capabilities not available' 258 }, 259 260 /** DockArgumentNotHTMLElement: Dock needs to be called on an HTMLElement 261 * @type $.fn.cwic-errorMapEntry 262 */ 263 DockArgumentNotHTMLElement: { 264 code: 'DockArgumentNotHTMLElement', 265 message: 'Dock needs to be called on an HTMLElement' 266 }, 267 268 /** ServiceDiscoveryMissingOrInvalidCallback: Service discovery lifecycle error (no callback defined or exception occured within callback for some of lifecycle states) 269 * @type $.fn.cwic-errorMapEntry 270 */ 271 ServiceDiscoveryMissingOrInvalidCallback: { 272 code: 'ServiceDiscoveryMissingOrInvalidCallback', 273 message: 'Service Discovery Error - Callback not implemented or exception occured' 274 }, 275 276 /** SSOMissingOrInvalidRedirectURI: Single Sign On redirect uri is missing or invalid (OAuth2) 277 * @type $.fn.cwic-errorMapEntry 278 */ 279 SSOMissingOrInvalidRedirectURI: { 280 code: 'SSOMissingOrInvalidRedirectURI', 281 message: 'Redirect URI missing or invalid' 282 }, 283 284 /** InvalidUserInput: User input invalid (email, password, redirectUri, etc.) 285 * @type $.fn.cwic-errorMapEntry 286 */ 287 InvalidUserInput: { 288 code: 'InvalidUserInput', 289 message: 'Invalid user input' 290 }, 291 292 /** CertificateError: Cannot start a new session due to a certificate problem. 293 * @type $.fn.cwic-errorMapEntry 294 */ 295 CertificateError: { 296 code: 'CertificateError', 297 message: 'Certificate error' 298 }, 299 300 /** InvalidURLFragment: Invalid SSO URL fragment received from child window (popup or iFrame) 301 * @type $.fn.cwic-errorMapEntry 302 */ 303 InvalidURLFragment: { 304 code: 'InvalidURLFragment', 305 message: 'Invalid URL fragment received' 306 }, 307 308 /** ErrorReadingConfig: Attempt to load startup handler config failed. Check if StartupHandlerConfig.xml file is present in installation directory. 309 * @type $.fn.cwic-errorMapEntry 310 */ 311 ErrorReadingConfig: { 312 code: 'ErrorReadingConfig', 313 message: 'Error reading config' 314 }, 315 316 /** UnexpectedLifecycleState: Unexpected application lifecycle state. 317 * @type $.fn.cwic-errorMapEntry 318 */ 319 UnexpectedLifecycleState: { 320 code: 'UnexpectedLifecycleState', 321 message: 'Unexpected application lifecycle state' 322 }, 323 324 /** SSOStartSessionError: SSO start session error, probably because of missing token. 325 * @type $.fn.cwic-errorMapEntry 326 */ 327 SSOStartSessionError: { 328 code: 'SSOStartSessionError', 329 message: 'SSO start session error' 330 }, 331 332 /** SSOCanceled: SSO canceled. 333 * @type $.fn.cwic-errorMapEntry 334 */ 335 SSOCanceled: { 336 code: 'SSOCanceled', 337 message: 'SSO canceled' 338 }, 339 340 /** SSOInvalidUserSwitch: You have attempted to sign in as a different user. To switch user you must call resetData API. 341 * @type $.fn.cwic-errorMapEntry 342 */ 343 SSOInvalidUserSwitch: { 344 code: 'SSOInvalidUserSwitch', 345 message: 'Invalid user switch' 346 }, 347 348 /** SSOSessionExpired: SSO session has expired. 349 * @type $.fn.cwic-errorMapEntry 350 */ 351 SSOSessionExpired: { 352 code: 'SSOSessionExpired', 353 message: 'SSO session expired' 354 }, 355 356 /** ServiceDiscoveryFailure: Cannot find services automatically. Try to set up server addresses manually. 357 * @type $.fn.cwic-errorMapEntry 358 */ 359 ServiceDiscoveryFailure: { 360 code: 'ServiceDiscoveryFailure', 361 message: 'Cannot find services automatically' 362 }, 363 364 /** CannotConnectToServer: Cannot communicate with the server. 365 * @type $.fn.cwic-errorMapEntry 366 */ 367 CannotConnectToServer: { 368 code: 'CannotConnectToServer', 369 message: 'Cannot connect to CUCM server' 370 }, 371 372 /** SelectDeviceFailure: Connecting to phone device failed. 373 * @type $.fn.cwic-errorMapEntry 374 */ 375 SelectDeviceFailure: { 376 code: 'SelectDeviceFailure', 377 message: 'Connecting to phone device failed' 378 }, 379 380 /** NoError: no error (success) 381 * @type $.fn.cwic-errorMapEntry 382 */ 383 NoError: { 384 code: 'NoError', 385 message: 'No error' 386 } 387 }; 388 389 var errorMapAlias = { 390 //origin in plugin - ApiReturnCodeEnum 391 eCreateCallFailed: 'CreateCallError', 392 eCallOperationFailed: 'CallControlError', 393 eNoActiveDevice: 'PhoneConfigGenError', 394 eLoggedInLock: 'TooManyPluginInstances', 395 eLogoutFailed: 'SignInError', 396 eNoWindowExists: 'VideoWindowError', 397 eInvalidWindowIdOrObject: 'VideoWindowError', // for addPreviewWindow and removePreviewWindow (CSCuo00772 and CSCuo00654) 398 eWindowAlreadyExists: 'VideoWindowError', 399 eNoPhoneMode: 'InvalidArgument', 400 eInvalidArgument: 'InvalidArguments', 401 eOperationNotSupported: 'OperationNotSupported', 402 eCapabilityMissing: 'CapabilityMissing', 403 eNotUserAuthorized: 'NotUserAuthorized', 404 eSyntaxError: 'NativePluginError', 405 eOperationFailed: 'OperationFailed', 406 eInvalidCallId: 'InvalidArguments', 407 eInvalidState: 'InvalidState', 408 eNoError: 'NoError', 409 eUnknownServiceEvent: 'Unknown', // default for service events 410 411 Ok: 'NoError', 412 413 // Telephony service event codes 414 Unknown: 'Unknown', 415 DeviceRegNoDevicesFound: 'NoDevicesFound', 416 NoCredentialsConfiguredServerHealth: 'AuthenticationFailure', 417 InvalidCredential: 'AuthenticationFailure', 418 InvalidCredentialServerHealth: 'AuthenticationFailure', 419 InvalidCCMCIPServer: 'InvalidCCMCIPServer', 420 InvalidTFTPServer: 'InvalidTFTPServer', 421 InvalidCTIServer: 'InvalidCTIServer', 422 NoNetwork: 'NetworkError', 423 TLSFailure: 'NetworkError', 424 SSLConnectError: 'NetworkError', 425 ServerConnectionFailure: 'CannotConnectToServer', 426 ServerAuthenticationFailure: 'AuthenticationFailure', 427 SelectDeviceFailure: 'SelectDeviceFailure', 428 InValidConfig: 'AuthenticationFailure', //The last attempt at authentication with CUCM failed because of invalid configuration. The CCMIP server, port etc was incorrect. 429 ServerCertificateRejected: 'CertificateError', 430 InvalidToken: 'AuthenticationFailure', 431 InvalidAuthorisationTokenServerHealth: 'AuthenticationFailure', 432 433 // System service event codes 434 ErrorReadingConfig: 'ErrorReadingConfig', 435 InvalidStartupHandlerState: 'UnexpectedLifecycleState', 436 InvalidLifeCycleState: 'UnexpectedLifecycleState', // The Lifecycle was requested to Start or Stop while in an invalid state. 437 InvalidCertRejected: 'CertificateError', 438 SSOPageLoadError: 'UnexpectedLifecycleState', // wrong input provided to "OnNavigationCompleted" 439 SSOStartSessionError: 'SSOStartSessionError', 440 SSOUnknownError: 'UnexpectedLifecycleState', // wrong input provided to "OnNavigationCompleted" 441 SSOCancelled: 'SSOCancelled', 442 SSOCertificateError: 'CertificateError', 443 SSOInvalidUserSwitch: 'SSOInvalidUserSwitch', 444 SSOWhoAmIFailure: 'SSOStartSessionError', 445 SSOSessionExpired: 'SSOSessionExpired', 446 ServiceDiscoveryFailure: 'ServiceDiscoveryFailure', // Cannot find your services automatically. Click advanced settings to set up manually. 447 ServiceDiscoveryAuthenticationFailure: 'AuthenticationFailure', 448 ServiceDiscoveryCannotConnectToCucmServer: 'CannotConnectToServer', 449 ServiceDiscoveryNoCucmConfiguration: 'ServiceDiscoveryFailure', 450 ServiceDiscoveryNoSRVRecordsFound: 'ServiceDiscoveryFailure', 451 ServiceDiscoveryCannotConnectToEdge: 'CannotConnectToServer', 452 ServiceDiscoveryNoNetworkConnectivity: 'NetworkError', 453 ServiceDiscoveryUntrustedCertificate: 'CertificateError' 454 455 // Connection failure codes 456 // if something breaks connection during the session 457 // lot of connection failure codes have the same name as above codes, so not repeated here. 458 459 }; 460 461 var getError = function (key, backupkey) { 462 var errorMapKey = 'Unknown'; 463 if (errorMapAlias[key]) { 464 errorMapKey = errorMapAlias[key]; 465 } else if (errorMap[key]) { 466 errorMapKey = key; 467 } else if (backupkey && errorMapAlias[backupkey]) { 468 errorMapKey = errorMapAlias[backupkey]; 469 } else if (backupkey && errorMap[backupkey]) { 470 errorMapKey = backupkey; 471 } 472 return errorMap[errorMapKey]; 473 }; 474 475 /** cwic global settings, they can be overridden by passing options to init 476 * @namespace 477 */ 478 var settings = { 479 /** The handler to be called when the API is ready and authorized.<br> 480 * The values in the defaults parameter can be used when invoking registerPhone.<br> 481 * The API is ready when:<ul> 482 * <li>The document (DOM) is ready.</li> 483 * <li>The Cisco Web Communicator add-on was found and could be loaded.</li> 484 * <li>User authorization status is "UserAuthorized" (since 3.0.1).</li></ul> 485 * @type Function=null 486 * @param {Object} defaults An object containing default values retrieved from URL query parameters user and/or cucm <i>e.g: http://myserver/phone?user=foo&cucm=1.2.3.4 </i><br> 487 * @param {Boolean} registered Phone registration status - true if the phone is already registered (can be used when using SDK in multiple browser tabs), false otherwise 488 * @param {String} mode The phone's current call control mode - "SoftPhone" or "DeskPhone" 489 */ 490 ready: null, 491 /** Device prefix to use for default softphone device prediction algorithm. If not set, default prefix is 'ecp'. See also {@link $.fn.cwic-settings.predictDevice}. 492 * @type String='ecp' 493 */ 494 devicePrefix: 'ecp', 495 /** Callback function to predict softphone device name<br> 496 * Device prediction algorithm is used to predict softphone device name in switchPhoneMode API function. If device name is not provided in the form of non-empty string, predictDevice function is used to predict device name. If custom predictDevice is not provided, default implementation is to concatenate settings.devicePrefix + options.username, where options.username is the name of the currently logged-in user. 497 * @name $.fn.cwic-settings.predictDevice 498 * @type Function=null 499 * @function 500 * @param {Object} options 501 * @param {String} options.username 502 */ 503 /** A flag to indicate to cwic that it should log more messages. 504 * @type Boolean=false 505 */ 506 verbose: true, 507 /** Handler to be called when cwic needs to log information.<br> 508 * Default is to use console.log if available, otherwise do nothing. 509 * @function 510 * @param {String} msg the message 511 * @param {Object} [context] the context of the message 512 * @type Function 513 */ 514 log: function (msg, context) { 515 if (typeof console !== 'undefined' && console.log) { 516 console.log(msg); 517 if (context) { 518 console.log(context); 519 } 520 } 521 }, 522 /** The handler to be called if the API could not be initialized.<br> 523 * The basic properties of the error object are listed, however more may be added based on the context of the error.<br> 524 * If the triggered error originated from a caught exception, the original error properties are included in the error parameter.<br> 525 * An error with code 1 (PluginNotAvailable) can have an extra 'pluginDisabled' property set to true.<br> 526 * @type Function 527 * @param {Object} error see {@link $.fn.cwic-errorMap} 528 * @param {String} [error.message] message associated with the error. 529 * @param {Number} error.code code associated with the error 530 */ 531 error: function (error) { 532 _log('Error: ', error); 533 }, 534 /** 535 * Allows the application to extend the default error map.<br> 536 * This parameter is a map of error id to {@link $.fn.cwic-errorMapEntry} 537 * It may also be a map of error id to String 538 * By default error messages (String) are associated to error codes (map keys, Numbers).<br> 539 * The application can define new error codes, or associate a different message/object to a pre-defined error code. <br> 540 * Default error map: {@link $.fn.cwic-errorMap}<br> 541 * @name $.fn.cwic-settings.errorMap 542 * @type $.fn.cwic-errorMapEntry{} 543 */ 544 errorMap: {}, 545 /** 546 * A callback used to indicate that CWIC must show the user authorization dialog before the application can use 547 * the CWIC API. Can be used to display instructions to the user, etc. before the user authorization dialog is 548 * displayed to the user. If implemented, the application must call the {@link $.fn.cwic-showUserAuthorization} API to show the 549 * user authorization dialog and obtain authorization from the user before using the CWIC API. 550 * If null, the user authorization dialog will be displayed when the application calls CWIC 'init', unless 551 * the domain has been previously authorized through the authorization dialog by the user selecting the "Always 552 * Allow" button, or the applications domain has been allowed by administrative whitelist. 553 * @since 3.0.1 554 * @type Function=null 555 * @function 556 */ 557 delayedUserAuth: null, 558 /** 559 * A flag to indicate if service discovery based sign in is active. 560 * If discovery lifecycle callbacks are not implemented, set this value to false. 561 * @type Boolean=true 562 */ 563 serviceDiscovery: true, 564 /** 565 * OAuth2 redirect_uri parameter. An URL to which an OAuth token is sent. Required for SSO sign in scenario. 566 */ 567 redirectUri: '', 568 /** 569 * Discovery lifecycle callback for "User Profile Required" lifecycle event. Only happens on first use. 570 * After first use, plugin caches user email address/domain and callback won't be triggered again. 571 * Call resetData API to change email address. 572 * @param {Function} setEmail Callback to call to continue with sign-in. 573 * @param {string} cachedEmail Cached previous value of email which could be used to populate input field on UI. 574 */ 575 emailRequired: function (setEmail, cachedEmail) { 576 _log(false, 'emailRequired callback not implemented, cannot proceed with sign in'); 577 throw { 578 name: errorMap.ServiceDiscoveryMissingOrInvalidCallback.message, 579 message: "emailRequired not implemented", 580 toString: function () { 581 return this.name + ": " + this.message; 582 } 583 }; 584 }, 585 /** 586 * Discovery lifecycle callback for "Credentials Required" lifecycle event. 587 * @param {Function} setCredentials Callback to call to set credentials and to continue with sign-in. 588 * @param {string} cachedUser Cached username value which could be used to populate input field on UI. 589 */ 590 credentialsRequired: function (setCredentials, cachedUser) { 591 _log(false, 'credentialsRequired callback not implemented, cannot proceed with sign in'); 592 throw { 593 name: errorMap.ServiceDiscoveryMissingOrInvalidCallback.message, 594 message: "credentialsRequired not implemented", 595 toString: function () { 596 return this.name + ": " + this.message; 597 } 598 }; 599 }, 600 /** 601 * Discovery lifecycle callback for "SSO Signed In" lifecycle state. Only happens when the app has successfully been authenticated. 602 * This does not mean that phone is ready to use, telephony device connection flow is initiated afterwards. Implementation of signedIn callback is optional. 603 */ 604 signedIn: function () { 605 _log(false, 'default signedIn callback called...'); 606 } 607 }; 608 609 var defaultSettings = $.extend({}, settings); 610 611 function resetInitSettings() { 612 settings = defaultSettings; 613 } 614 615 // -------------------------------------------------------- 616 // Helper tools - BEGIN 617 // -------------------------------------------------------- 618 619 /** 620 * Helper function. Wraps a function of arity n into another anonymous function of arity 0. Used for passing a function with bound(pre-set) arguments as properties on event objects. 621 * @param {Function} f function to wrap 622 * @param {Any} arg f's argument 623 * @private 624 */ 625 function wrap(f) { 626 var fn = f, 627 args = [].slice.call(arguments, 1); 628 return function () { 629 return fn.apply(this, args); 630 }; 631 } 632 633 function debounce(fn, t) { 634 var timeout = null; 635 636 return function () { 637 var args = arguments, 638 self = this; 639 640 if (timeout) { 641 clearTimeout(timeout); 642 } 643 timeout = setTimeout(function () { 644 timeout = null; 645 fn.apply(self, args); 646 }, t); 647 }; 648 } 649 650 /** 651 * Function decorator which negates the result of passed function 652 * @param {Function} fn function to decorate 653 * @returns {Function} negated passed-in function 654 * @private 655 */ 656 function not(fn) { 657 return function () { 658 return !fn.apply(this, [].slice.call(arguments)); 659 }; 660 } 661 662 /** 663 * Check if some value is an object. Needed because of JS quirk "typeof null" returns "object" 664 * @param {Any} o value to check 665 * @returns {Boolean} result of check 666 * @private 667 */ 668 function isObject(o) { 669 return typeof o === 'object' && o !== null; 670 } 671 672 var isNotObject = not(isObject); 673 674 function isMac() { 675 return navigator.platform && navigator.platform.indexOf('Mac') !== -1; 676 } 677 678 /** 679 * send string to plugin for encryption 680 * @param {String} str 681 * @param {Function} cb(error, result) callback to be called with result/error 682 * @private 683 */ 684 function encrypt(str, cb) { 685 function encryptCb(res) { 686 if (res) { 687 cb(null, res); 688 } else { 689 cb('Empty response from plugin'); 690 } 691 } 692 693 if (str && typeof str === 'string') { 694 _sendClientRequest('encryptCucmPassword', str, encryptCb); 695 } else { 696 cb('Invalid input string'); 697 } 698 } 699 700 var validators = (function () { 701 var validatorsMap; 702 703 /** 704 * Basic email validation 705 * @param {String} email 706 * @returns {Boolean} validation result 707 * @private 708 */ 709 function validateEmail(email) { 710 // FUTURE 711 //var emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+$/, 712 // dotsRegex = /\.{2,}/; // detect 2 or more consecutive dots 713 // 714 //if (email && (typeof email === 'string') && email.length < 127) { 715 // return (emailRegex.test(email) && !dotsRegex.test(email)) ? true : false; 716 //} else { 717 // return false; 718 //} 719 return true; 720 } 721 722 /** 723 * Validate URL + check for sso related parameters 724 * @param {String} url 725 * @returns {Boolean} validation result 726 * @private 727 */ 728 function validateSSOUrl(url) { 729 // FUTURE 730 // var checkProtocolRegExp = /^(http|https):\/\//, 731 // clientId = 'C69908c4f345729af0a23cdfff1d255272de942193e7d39171ddd307bc488d7a1', 732 // responseType = 'token', 733 // tokenType = 'Bearer', 734 // scope = 'UnifiedCommunications:readwrite', 735 // params, 736 // paramsObj = {}; 737 738 // _log(true, 'Validator: validating SSO URL (navigate to): ', url); 739 740 // if (typeof url === 'string' && url.length < 500) { 741 // url = $.trim(url); 742 // } else { 743 // return false; 744 // } 745 746 // try { 747 // params = url.split('?')[1].split('&'); // split url to form ['param1=value', 'param2=value'] 748 // // convert array to object 749 // $.each(params, function (index, pair) { 750 // var param = pair.split('='); 751 // paramsObj[param[0]] = param[1]; 752 // }); 753 // } catch(e) { 754 // return false; 755 // } 756 757 // if (checkProtocolRegExp.test(url) && 758 // paramsObj['response_type'] === responseType && 759 // paramsObj['token_type'] === tokenType && 760 // paramsObj['scope'] === scope && 761 // paramsObj['client_id'] === clientId && 762 // paramsObj['redirect_uri'] ) { // for now just ensure that redirect_uri exists without validation 763 // return true; 764 // } else { 765 // return false; 766 // } 767 return true; 768 } 769 770 function validateUrl(url) { 771 // FUTURE 772 //_log(true, 'URL validator not implemented'); 773 return true; 774 } 775 776 function validateUsername(username) { 777 _log(true, 'Validator: validating username: ', username); 778 779 // FUTURE (cannot validate encrypted string) 780 //if (username && (typeof username === 'string') && username.length < 256) { 781 // return true; 782 //} 783 //return false; 784 785 return true; 786 } 787 788 function validatePassphrase(passphrase) { 789 //if (passphrase && (typeof passphrase === 'string') && passphrase.length < 1000) { 790 // return true; 791 //} 792 //return false; 793 return true; 794 } 795 796 /** 797 * Credentials validation - checking the length of passed string 798 * @param {String} usr username 799 * @param {String} pass passphrase 800 * @returns {Boolean} validation result 801 * @private 802 */ 803 function validateCredentials(usr, pass) { 804 //return (validateUsername(usr) && validatePassphrase(pass)) ? true : false; 805 return true; 806 } 807 808 /** 809 * Validate received URL fragment which supposed to have OAuth token inside. 810 * @param {String} urlFragmentWithToken URL fragment 811 * @private 812 */ 813 function validateSSOToken(urlFragmentWithToken) { 814 // FUTURE 815 // var token = ''; 816 817 // _log(true, 'Validator: validating URL fragment: ', urlFragmentWithToken); 818 819 // if (typeof urlFragmentWithToken === 'string') { 820 // token = urlFragmentWithToken.split('&')[0]; 821 // } 822 823 // if (token && token.indexOf('access_token') >= 0 && 824 // token.split('=')[1] && // token value 825 // urlFragmentWithToken.indexOf('token_type=Bearer') >= 0 && 826 // urlFragmentWithToken.indexOf('expires_in') >= 0) { 827 // return true; 828 // } 829 830 // return false; 831 return true; 832 } 833 834 validatorsMap = { 835 email: { 836 validate: validateEmail, 837 isValid: validateEmail, 838 isNotValid: not(validateEmail) 839 }, 840 ssourl: { 841 validate: validateSSOUrl, 842 isValid: validateSSOUrl, 843 isNotValid: not(validateSSOUrl) 844 }, 845 credentials: { 846 validate: validateCredentials, 847 isValid: validateCredentials, 848 isNotValid: not(validateCredentials) 849 }, 850 url: { 851 validate: validateUrl, 852 isValid: validateUrl, 853 isNotValid: not(validateUrl) 854 }, 855 passphrase: { 856 validate: validatePassphrase, 857 isValid: validatePassphrase, 858 isNotValid: not(validatePassphrase) 859 }, 860 username: { 861 validate: validateUsername, 862 isValid: validateUsername, 863 isNotValid: not(validateUsername) 864 }, 865 ssotoken: { 866 validate: validateSSOToken, 867 isValid: validateSSOToken, 868 isNotValid: not(validateSSOToken) 869 } 870 }; 871 872 return { 873 get: function getValidator(validatorName) { 874 var validator; 875 876 if (typeof validatorName !== 'string') { 877 throw new TypeError(validator + ' is not a string'); 878 } 879 880 validator = validatorsMap[$.trim(validatorName)]; 881 882 if (validator) { 883 return validator; 884 } else { 885 throw new Error(validatorName + ' is not defined'); 886 } 887 888 } 889 }; 890 }()); 891 892 // -------------------------------------------------------- 893 // Helper tools - END 894 // -------------------------------------------------------- 895 896 /** 897 * Registration object with properties of the currently logged in session <br> 898 * expanded below in _getRegistrationObject() and authenticateCcmcip() <br> 899 * @type Object 900 * @private 901 */ 902 903 var registration, 904 regGlobals = {}, 905 transferCallGlobals = { 906 endTransfer: function () { 907 this.inProgress = false; 908 this.completeBtn = null; 909 this.callId = null; 910 } 911 }, 912 SSOGlobals; 913 914 function resetGlobals() { 915 _log(true, 'resetGlobals: reseting ...'); 916 var user = regGlobals.user || '', 917 email = regGlobals.email || '', 918 manual = regGlobals.manual, 919 unregisterCb = regGlobals.unregisterCb || null, // cannot overwrite because it waits for 'SIGNEDOUT' state to occur! 920 errorState = regGlobals.errorState || '', // after error, logout is called. We must save this values for the next sign in attempt 921 lastAuthenticatorId = regGlobals.lastAuthenticatorId || null, 922 lastAuthStatus = regGlobals.lastAuthStatus || ''; 923 924 SSOGlobals = { 925 inProgress: false, 926 canCancel: false 927 //popup: null, [postponed] 928 //popupParams: {}, [postponed] 929 }; 930 931 transferCallGlobals.endTransfer(); 932 933 regGlobals = { 934 registeringPhone: false, 935 manual: manual, 936 signingOut: false, 937 switchingMode: false, 938 telephonyDevicesSet: false, 939 lastConnectedDevice: '', 940 lastAuthStatus: lastAuthStatus, 941 errorState: errorState, 942 lastAuthenticatorId: lastAuthenticatorId, 943 successCb: null, 944 errorCb: null, 945 CUCM: [], 946 user: user, 947 password: '', 948 email: email, 949 unregisterCb: unregisterCb, 950 authenticatedCallback: null, 951 credentialsRequiredCalled: false, 952 emailRequiredCalled: false, 953 ssoFailedCalled: false 954 }; 955 956 registration = { 957 devices: {} // map of available devices (key is device name) 958 }; 959 } 960 961 resetGlobals(); 962 963 //****************************************** 964 // SSO related work 965 //****************************************** 966 /** 967 * Initiates service discovery based sign-in. Before using this API, following callbacks must be defined in init API: 968 * <ul> 969 * <li>emailRequired</li> 970 * <li>credentialsRequired</li> 971 * <li>signedIn</li> 972 * </ul> 973 * After successfull service discovery either SSO or credentials based authentication occurs depending on UCM configuration. 974 * @param {Object} args A map with: 975 * @param {String} [args.mode] Register the phone in this mode. Available modes are "SoftPhone" or "DeskPhone". Default of intelligent guess is applied after a device is selected.<br> 976 * @param {Function} [args.devicesAvailable(devices, phoneMode, selectDeviceCb)] Callback called after successful authentication. 977 * If this callback is not specified, cwic applies the default device selection algorithm. An array of {@link device} objects is passed so the application can select the device.<br> 978 * To complete the device registration, call the selectDeviceCb(phoneMode, deviceName, lineDN) function that is passed in to devicesAvailable as the third parameter. 979 * lineDN argument is optional and it is valid only in deskphone mode. It is ignored in softphone mode. 980 * @param {Boolean} [args.forceRegistration] Specifies whether to forcibly unregister other softphone instances with CUCM. Default is false. See GracefulRegistration doc for more info. 981 * @param {Function} [args.success(registration)] Callback called when registration succeeds. A {@link registration} object is passed to the callback 982 * @param {Function} [args.error(errorMapEntry)] Callback called if the registration fails. 983 */ 984 function startDiscovery(args) { 985 var $this = this; 986 987 if (!args) { 988 _log(true, 'startDiscovery: empty arguments received'); 989 args = {}; 990 } 991 992 // todo: uncomment or remove 993 // if (SSOGlobals.inProgress === true) { 994 // _log(false, 'startDiscovery: Sign in already in progress, cannot send another request!'); 995 // return $this; 996 // } 997 998 _log(true, 'startDiscovery called with arguments: ', args); 999 1000 setRegGlobalsD(args, $this); 1001 1002 if (canProceed(args, $this) === false) { 1003 return $this; 1004 } 1005 1006 // start service discovery 1007 SSOGlobals.inProgress = true; 1008 _sendClientRequest('startSignIn', { 1009 manualSettings: false 1010 }); 1011 1012 return $this; 1013 } 1014 1015 // D for Discovery 1016 function setRegGlobalsD(args, $this) { 1017 var devicesAvailableCb = $.isFunction(args.devicesAvailable) ? args.devicesAvailable : null; 1018 1019 regGlobals.registeringPhone = true; 1020 regGlobals.manual = false; 1021 regGlobals.successCb = $.isFunction(args.success) ? args.success : null; 1022 regGlobals.errorCb = $.isFunction(args.error) ? args.error : null; 1023 regGlobals.CUCM = 'discovery based address'; 1024 1025 regGlobals.authenticatedCallback = getAuthenticatedCb(devicesAvailableCb, $this); 1026 1027 // reset global registration object 1028 registration = { 1029 mode: args.mode || 'SoftPhone', 1030 devices: {}, 1031 forceRegistration: args.forceRegistration || false 1032 }; 1033 _log(true, 'setRegGlobalsD: regGlobals set: ', regGlobals); 1034 _log(true, 'setRegGlobalsD: registration set: ', registration); 1035 } 1036 1037 // also triggers error as a side effect. (_triggerError does not stop the execution of current function, so it must be done manually in top function) 1038 function canProceed(args, $this) { 1039 if (!_plugin) { 1040 _triggerError($this, regGlobals.errorCb, errorMap.PluginNotAvailable, 'Plug-in is not available or has not been initialized', {registration: registration}); 1041 return false; 1042 } 1043 1044 if (args.mode && typeof args.mode === 'string' && !args.mode.match(/^(SoftPhone|DeskPhone)$/)) { 1045 _triggerError($this, regGlobals.errorCb, errorMap.InvalidArguments, 'Invalid phone mode "' + registration.mode + '"', {registration: registration}); 1046 return false; 1047 } 1048 1049 return true; 1050 } 1051 1052 // authenticatedCallback is called after receiving authenticationresult event with positive outcome 1053 // required both for manual and sd based sign in 1054 function getAuthenticatedCb(devicesAvailableCb, $this) { 1055 var deviceSelected = false; 1056 1057 function sendConnectMsg(deviceName, lineDN) { 1058 // because of stream like behavior of available devices list, availableDevicesCB could be triggered multiple times, 1059 // so we need to guard against calling connect multiple times for the same device 1060 // on failure, regGlobals.lastConnectedDevice will be reset, so it should not block connectiong to device again after failure 1061 if (!deviceName || deviceName === regGlobals.lastConnectedDevice) { 1062 _log(true, 'authenticatedCb: device name empty or already sent "connect" message for this device, returning...', deviceName); 1063 return; 1064 } 1065 1066 lineDN = lineDN || ''; 1067 regGlobals.lastConnectedDevice = deviceName; 1068 1069 _sendClientRequest('connect', 1070 { phoneMode: registration.mode, deviceName: deviceName, lineDN: lineDN, forceRegistration: registration.forceRegistration }, $.noop, 1071 function errorCb(error) { 1072 _triggerError($this, regGlobals.errorCb, getError(error), error, {registration: registration}); 1073 } 1074 ); 1075 } 1076 1077 return function (_devices) { 1078 var defaultDevice, 1079 i; 1080 1081 _log(true, 'Entering authenticatedCallback'); 1082 1083 if (_devices.length === 0) { 1084 _log(true, 'authenticatedCallback: devices list of zero length received'); 1085 } 1086 1087 if (devicesAvailableCb) { 1088 try { 1089 devicesAvailableCb(_devices, registration.mode, function (phoneMode, deviceName, lineDN) { 1090 if (deviceSelected) { 1091 _log(true, 'device selection could be done only once after sign in...use switchPhoneMode to select another device'); 1092 return; 1093 } 1094 1095 deviceSelected = true; 1096 // in softphone mode, lineDN is not allowed 1097 if (phoneMode === 'SoftPhone' || !lineDN) { 1098 lineDN = ''; 1099 } 1100 1101 _log(true, 'connecting to user selected device: ', deviceName); 1102 1103 sendConnectMsg(deviceName, lineDN); 1104 }); 1105 } catch (devicesAvailableException) { 1106 _log('Exception occurred in application devicesAvailable callback', devicesAvailableException); 1107 if (typeof console !== 'undefined' && console.trace) { 1108 console.trace(); 1109 } 1110 } 1111 } else { 1112 defaultDevice = {name: ''}; 1113 //...use the first available one 1114 for (i = 0; i < _devices.length; i++) { 1115 if (registration.mode === 'SoftPhone' && _devices[i].isSoftPhone) { 1116 // Note device objects retrieved from ECC will have device model description in device.modelDescription 1117 // This differs from csf node.js module 'phoneconfig' implementation which puts it in device.model - this is also removed from 11.0 1118 // device.model is removed from 11.0 release 1119 if (_devices[i].modelDescription.match(/^\s*Cisco\s+Unified\s+Client\s+Services\s+Framework\s*$/i)) { // On CUCM Product Type: Cisco Unified Client Services Framework 1120 defaultDevice = _devices[i]; 1121 break; 1122 } 1123 } 1124 if (registration.mode === 'DeskPhone' && _devices[i].isDeskPhone) { 1125 defaultDevice = _devices[i]; 1126 break; 1127 } 1128 } 1129 _log(true, 'connecting to discovered device: ', defaultDevice); 1130 1131 sendConnectMsg(defaultDevice.name); 1132 } 1133 }; 1134 } 1135 1136 1137 /** 1138 * Cancels ongoing Single Sign On procedure if internal 'canCancelSingleSignOn' capability is enabled. 1139 * 'canCancelSingleSignOn' is enabled immediately before 'ssoNavigateTo.cwic' event is triggered. It is disabled again after the token is acquired or after 'cancelSSO' call. 1140 * @since: 4.0.0 <br> 1141 */ 1142 function cancelSSO() { 1143 var $this = this; 1144 if (SSOGlobals.canCancel) { 1145 // FUTURE: Integrated popup/iframe feature postponed 1146 // close popup. TODO: remove iframe. 1147 //if (SSOGlobals.popup && SSOGlobals.popup.close) { 1148 // SSOGlobals.popup.close();ibn 1149 //} 1150 1151 // set to false early to prevent multiple calls to plugin. (It would be set to false anyway by plugin when Lifecycle state changes) 1152 SSOGlobals.canCancel = false; 1153 _sendClientRequest('cancelSingleSignOn', {}); 1154 } else { 1155 _log(false, 'SSO(cancelSSO): Not possible to cancel SSO in the current state'); 1156 } 1157 1158 return $this; 1159 } 1160 1161 /*TODO: document*/ 1162 function getSSOSessionTimeRemaining() { 1163 // maybe this could be internal function 1164 /* // TODO: implement 1165 function remainingTimeCb(time) { 1166 // do something with time 1167 } 1168 _sendClientRequest('getSSOSessionTimeRemaining', remainingTimeCb, {});*/ 1169 } // end getSSOSessionTimeRemaining 1170 1171 /** 1172 * Clears cached user and system data. Must be called before manual sign in if previous sign in type was discovery based. 1173 * Can only be called in signed out state. 1174 */ 1175 function resetData() { 1176 function resetCompleted() { 1177 // reload page after cache is cleared? window.location.reload(true) - bed idea, important information could be present in the 'patrent' web page 1178 // check if success cb means that reset is finished? Maybe call this on InvokeResetData event? 1179 } 1180 // TODO: when is it possible to call this function!? - Only in state LifeCycleStateEnum::SIGNEDOUT 1181 _sendClientRequest('resetData', {}, resetCompleted); 1182 1183 } 1184 1185 function restablishSSOSession() { 1186 _sendClientRequest('restablishSSOSession', {}); 1187 } 1188 1189 function suppressSSOSessionExpiryPrompt() { 1190 _sendClientRequest('suppressSSOSessionExpiryPrompt', {}); 1191 } 1192 1193 function unsuppressSSOSessionExpiryPrompt(val) { 1194 if (typeof val === 'boolean') { 1195 _sendClientRequest('unsuppressSSOSessionExpiryPrompt', {promptIfExpired: val}); 1196 } else { 1197 throw new TypeError(val + ' is not a boolean'); 1198 } 1199 } //end unsuppressSSOSessionExpiryPrompt 1200 1201 1202 /** 1203 * Handler for 'ssonavigateto' event. Constructs valid sso url and triggers public event 'ssoNavigateTo.cwic'. 1204 * Developers have to implement logic for redirecting to passed url (popup or iframe). 1205 * @param {jQuery Object} $this plugin 1206 * @param {String} url base url passed from JCF 1207 * @fires 'ssoNavigateTo.cwic' 1208 * @private 1209 */ 1210 function _triggerSSONavigateTo($this, url) { 1211 if (!settings.redirectUri) { 1212 return _triggerError($this, SSOGlobals.args.error, errorMap.SSOMissingOrInvalidRedirectURI); 1213 } 1214 1215 var newUrl, 1216 params, 1217 paramsObj = {}, 1218 urlParts = url.split('?'), 1219 server = urlParts[0]; 1220 1221 _log(true, 'Plugin sent URL: ' + url); 1222 1223 try { 1224 params = urlParts[1].split('&'); // split url to form ['param1=value', 'param2=value'] 1225 // convert array to object 1226 $.each(params, function (index, pair) { 1227 var param = pair.split('='); 1228 paramsObj[param[0]] = param[1]; 1229 }); 1230 } catch (e) { 1231 // TODO: leave invalid url check or not!? 1232 _log(true, 'Invalid SSO URL received', e); 1233 throw new Error('SSO authorization service URL invalid format'); 1234 } 1235 1236 paramsObj.client_id = 'C69908c4f345729af0a23cdfff1d255272de942193e7d39171ddd307bc488d7a1'; 1237 paramsObj.redirect_uri = settings.redirectUri; // encodeURIComponent(settings.redirectUri); - not needed, $ does it in param method 1238 1239 newUrl = server + '?' + $.param(paramsObj); 1240 1241 _log(true, 'New URL generated: ' + newUrl); 1242 1243 var event = $.Event('ssoNavigateTo.cwic'); 1244 event.url = newUrl; 1245 $this.trigger(event); 1246 1247 // FUTURE: Integrated popup/iframe feature postponed. TODO: provide mechanism for choosing between popup and iframe 1248 // var popup = SSOGlobals.popup, 1249 // popupParams = SSOGlobals.popupParams; 1250 // 1251 // TODO: implement on unload event and popup.close inside. When browser is refreshed reference to opened popup is lost, so it must be closed on unload! 1252 //popup && popup.close(); 1253 // TODO change hardcoded values with popupParams values 1254 //SSOGlobals.popup = window.open(url, '_blank', 'toolbar=no, location=no, scrollbars=yes, resizable=yes, top=100, left=100, width=400, height=600'); 1255 } 1256 1257 /** 1258 * Only happens: 1259 * 1. On first use if bootstrapped 1260 * 2. Provisioned keys have not been set to auto generate user profile (for Jabber) 1261 * 3. Discovery error occurs 1262 * @param {jQuery Object} $this jquery object on which SDK is initiated 1263 * @param {string[]} content.errors List of errors 1264 * @param {errorMapEntry} content.error First error from the list converted to errorMapEntry object 1265 * @private 1266 */ 1267 function _triggerEmailRequired($this, content) { 1268 if (!regGlobals.registeringPhone) { 1269 return; 1270 } 1271 var error = content.error, 1272 emailValidator = validators.get('email'), 1273 cachedEmail = ''; 1274 1275 function setEmailAddress(email) { 1276 if (emailValidator.isNotValid(email)) { 1277 _triggerEmailRequired($this, { 1278 error: getError('InvalidUserInput'), 1279 errors: ['Invalid email submited'] 1280 }); 1281 1282 return; 1283 } 1284 1285 registration.email = regGlobals.email = email; 1286 _sendClientRequest('setUserProfileEmailAddress', {email: email}); 1287 } 1288 1289 // if manual login, skip emailRequired callback calling and immediately set email with arbitrary value, it will be ignored anyway 1290 // if error is present, call error callback, defined in registerPhone args, and optionally stop lifecycle 1291 if (regGlobals.manual) { 1292 if (error) { 1293 stopSignInFromER($this, error); 1294 return; 1295 } 1296 return setEmailAddress('jabbersdk@any.domain'); 1297 } 1298 1299 if (error && regGlobals.emailReguiredCalled) { 1300 return stopSignInFromER($this, error); 1301 } 1302 1303 regGlobals.emailReguiredCalled = true; 1304 // manual == false 1305 if (regGlobals.email && typeof regGlobals.email === 'string') { 1306 cachedEmail = regGlobals.email; 1307 } else if (regGlobals.user && typeof regGlobals.user === 'string') { 1308 cachedEmail = regGlobals.user + '@'; 1309 } 1310 1311 try { 1312 settings.emailRequired(setEmailAddress, cachedEmail); 1313 } catch (e) { 1314 // not implemented 1315 return _triggerError($this, settings.error, errorMap.ServiceDiscoveryMissingOrInvalidCallback, e); 1316 } 1317 1318 } 1319 1320 1321 1322 /** 1323 * Handler for credentialsrequired event (Service discovery found home cluster without SSO enabled). Only happens when: 1324 * 1. Non SSO sign out 1325 * 2. Non SSO credentials not already set 1326 * 3. Non SSO authentication error 1327 * @param {Object} content event arguments object 1328 * @param {string[]} content.errors List of status or error messages 1329 * @param {errorMapEntry} content.error First error from the list converted to errorMapEntry object 1330 * @param {Number} content.authenticatorId Authenticator ID for which credentials are required 1331 * @private 1332 */ 1333 function _triggerCredentialsRequired($this, content) { 1334 if (!regGlobals.registeringPhone) { 1335 return; 1336 } 1337 1338 var error = content.error, 1339 credentialsValidator = validators.get('credentials'), 1340 authId = content.authenticatorId, 1341 cachedUser = ''; 1342 1343 // 1344 // -------------------------------------------- 1345 function provideManualCredentials($this) { 1346 if (regGlobals.user && regGlobals.passphrase) { 1347 return setCredentials(regGlobals.user, regGlobals.passphrase); 1348 } else { 1349 // should never happened, because credentials are checked in the beginning of registerPhone API 1350 // ... actually it happens after sign out, if the following credentialsRequired event is not ignored. 1351 stopSignInFromCR($this, getError('AuthenticationFailure'), authId); 1352 return; 1353 } 1354 } 1355 1356 function getUserFromCache() { 1357 if (regGlobals.user) { 1358 return regGlobals.user; 1359 } else if (regGlobals.email && typeof regGlobals.email === 'string') { 1360 return regGlobals.email.split('@')[0]; 1361 } else { 1362 return ''; 1363 } 1364 } 1365 1366 function callCredentialsRequiredCb($this) { 1367 if (error && regGlobals.credentialsRequiredCalled) { 1368 return stopSignInFromCR($this, error); 1369 } 1370 regGlobals.credentialsRequiredCalled = true; 1371 1372 try { 1373 settings.credentialsRequired(setCredentials, cachedUser); 1374 } catch (e) { 1375 // not implemented or exception occurred 1376 return _triggerError($this, settings.error, errorMap.ServiceDiscoveryMissingOrInvalidCallback, e); 1377 } 1378 } 1379 1380 function setCredentials(username, passphrase) { 1381 var isEncrypted = false; 1382 1383 function encryptCb(error, result) { 1384 if (error) { 1385 return _triggerError($this, settings.error, getError('NativePluginError'), error); 1386 } 1387 passphrase = result; 1388 1389 _sendClientRequest('setUserProfileCredentials', { 1390 username: username, 1391 password: passphrase, 1392 authenticator: authId 1393 }); 1394 } 1395 1396 if (passphrase && passphrase.encrypted) { 1397 passphrase = passphrase.encrypted; 1398 isEncrypted = true; 1399 } 1400 1401 if (credentialsValidator.isNotValid(username, passphrase)) { 1402 return _triggerCredentialsRequired($this, { 1403 errors: ['InvalidCredentials'], 1404 error: getError('AuthenticationFailure'), 1405 authenticatorId: authId 1406 }); 1407 } 1408 1409 registration.user = regGlobals.user = username; 1410 1411 if (isEncrypted === false) { 1412 encrypt(passphrase, encryptCb); 1413 } else { 1414 _sendClientRequest('setUserProfileCredentials', { 1415 username: username, 1416 password: passphrase, 1417 authenticator: authId 1418 }); 1419 } 1420 } 1421 // 1422 //------------------------------------------------------------- 1423 1424 // if manual login, skip credentialsRequired callback calling and immediately set credentials with user provided values 1425 // when logged in in manual mode, then signout, credentialsRequired event will be triggered, but we need to ignore it 1426 if (regGlobals.manual) { 1427 if (error) { 1428 _log(true, 'returning from credentialsRequired in manual mode, because of error...'); 1429 stopSignInFromCR($this, error, authId); 1430 return; 1431 } 1432 provideManualCredentials($this); 1433 } else { 1434 cachedUser = getUserFromCache(); 1435 1436 callCredentialsRequiredCb($this); 1437 } 1438 return; 1439 } 1440 1441 // CR for Credentials Required 1442 function stopSignInFromCR($this, error, authId) { 1443 regGlobals.errorState = 'credentialsRequired'; 1444 regGlobals.lastAuthenticatorId = authId; 1445 stopSignIn($this, error); 1446 } 1447 1448 // ER for Email Required 1449 function stopSignInFromER($this, error) { 1450 regGlobals.errorState = 'emailRequired'; 1451 stopSignIn($this, error); 1452 } 1453 1454 function stopSignIn($this, error) { 1455 _triggerError($this, regGlobals.errorCb, errorMap.AuthenticationFailure, error, {registration: registration}); 1456 1457 resetGlobals(); // will delete errorCb, don't call it before 1458 } 1459 1460 /** 1461 * Handler for ssosigninfailed event. Call startDiscovery to leave this state. Only happens when: 1462 * 1. SSO Sign Out todo: clean 1463 * 2. SSO credentials not already set 1464 * 3. SSO authentication error 1465 * @param {Object} content 1466 * @param {string[]} content.errors List of error or status messages 1467 * @param {errorMapEntry} content.error First error from the list converted to errorMapEntry object 1468 * @private 1469 */ 1470 function _triggerSSOSignInFailed($this, content) { 1471 if (!regGlobals.registeringPhone) { 1472 return; 1473 } 1474 1475 var error = content.error; 1476 SSOGlobals.inProgress = false; 1477 stopSignIn($this, error); 1478 //todo: clean 1479 // try { 1480 // settings.SSOSignInFailed(error); 1481 // } catch (e) { 1482 // // not implemented 1483 // return _triggerError($this, settings.error, errorMap.ServiceDiscoveryMissingOrInvalidCallback, e); 1484 // } 1485 } 1486 1487 /** 1488 * Only happens when the app has successfully been authenticated with the primary authenticator. Calls signedIn callback. 1489 * SignedIn state means that Lifecycle state is changed to 'SIGNEDIN'. Device connection is performed after this event is triggered. 1490 * @param {JQuery Object} $this 1491 * @private 1492 */ 1493 function _triggerSignedIn($this) { 1494 SSOGlobals.inProgress = false; 1495 _log(true, '_triggerSignedIn called'); 1496 _log(true, '_triggerSignedIn: authenticatedCB present: ', regGlobals.authenticatedCallback ? true : false); 1497 _log(true, '_triggerSignedIn: telephonyDevicesSet: ', regGlobals.telephonyDevicesSet); 1498 1499 if (regGlobals.authenticatedCallback && regGlobals.telephonyDevicesSet) { 1500 // regGlobals.telephonyDevicesSet = false; // it will be reset on sign out 1501 _log(true, 'Proceeding with device selection immediately after SignedIn event'); 1502 proceedWithDeviceSelection(); 1503 } else { 1504 // device selection will be done after devices are received... see _triggerTelephonyDevicesChange 1505 } 1506 1507 try { 1508 settings.signedIn(); // TODO: maybe this handler does not have to be public. It does not provide any valueable information for user? 1509 } catch (e) { 1510 // signedIn callback is optional 1511 } 1512 } 1513 1514 /** 1515 * Handler for telephonydeviceschange event. Triggered every time devices are changed. 1516 * When user is switched, this event will also triger with empty device list. That event should be filtered. 1517 * @param {Array} content.devices array of available devices 1518 * @private 1519 */ 1520 function _triggerTelephonyDevicesChange($this, content) { 1521 _log(true, '_triggerTelephonyDevicesChange called with following data: ', content); 1522 _log(true, '_triggerTelephonyDevicesChange: regGlobals: ', regGlobals); 1523 1524 if (content.devices && content.devices.length > 0 && regGlobals.registeringPhone) { 1525 // TelephonyDevicesChange event is triggered for each device in the list. We don't know when the last device is discovered. 1526 // calling getAvailableDevices after first TelephonyDevicesChange event is received seems to be good enough. 1527 // additionally we debounced TelephonyDevicesChange event, so this callback will be called only after timeout of 20ms expires between successive events 1528 regGlobals.telephonyDevicesSet = true; 1529 1530 // We now want to call devices available cb for every change event during registration. 1531 _log(true, 'Proceeding with device selection after waiting for TelephonyDevicesChange event.'); 1532 proceedWithDeviceSelection(); 1533 } 1534 } 1535 1536 var _triggerTelephonyDevicesChangeDebounced = debounce(_triggerTelephonyDevicesChange, 20); 1537 1538 function proceedWithDeviceSelection() { 1539 // var localAuthCb; 1540 // 1541 // // removing of authenticatedCallback is moved here from the authenticatedCallback itself, 1542 // // because 'authenticationresult' event is triggered multiple times, 1543 // // and _updateRegistration creates multiple references through closure before authenticatedCallback is invoked. 1544 // // In such a case, calling "delete regGlobals.authenticatedCallback" removes only one reference and practically don't have expected effect. 1545 // // Now we delete this authenticatedCallback just before it is passed to _updateRegistration for the first time. 1546 // if (regGlobals.authenticatedCallback) { 1547 // localAuthCb = regGlobals.authenticatedCallback; 1548 // regGlobals.authenticatedCallback = null; 1549 // 1550 // // _updateRegistration gets raw devices list and passes to authenticatedCallback 1551 // _updateRegistration(regGlobals.currState, localAuthCb); 1552 // } 1553 // TODO: clean this after we decide how to handle TelephonyDevicesChange event during signIn 1554 _log(true, 'proceedWithDeviceSelection called'); 1555 _updateRegistration(regGlobals.currState, regGlobals.authenticatedCallback); 1556 } 1557 1558 /** 1559 * Handler for invokeresetdata event. It should clear all cached information by browser. 1560 * In reality it can just clear/reset to default SSOGlobals object. 1561 * Candidate for removal. 1562 * @private 1563 */ 1564 function _triggerInvokeResetData() { 1565 // TODO: implement, clear SSOGlobals object? 1566 console.log('SSO: Invoke Reset Data Triggered. Not possible to implement in browser/JS!'); 1567 } 1568 1569 /** 1570 * Handler for ssoshowwindow event. It signals when the popup/iframe for SSO login should be shown/hidden during the 1571 * login phase. 1572 * This is Jabber specific. Jabber SDK does not send URL for every page HTML page, but only when token is acquired. 1573 * Candidate for removal. 1574 * @private 1575 */ 1576 function _triggerSSOShowWindow(show) { 1577 // TODO: implement 1578 _log(true, 'SSO: Show Window Triggered!'); 1579 } 1580 1581 /** 1582 * TODO: token renewal US 1583 * @private 1584 */ 1585 function _triggerSSOSessionExpiryPromptRequired() { 1586 // TODO: implement 1587 console.log('SSO: Session Expiry Triggered!'); 1588 } 1589 1590 /** 1591 * Handler for lifecyclestatechanged event. 1592 * @param {string} content New lifecycle state (CSFUnified::LifeCycleStateEnum::LifeCycleState) 1593 * Candidate for removal. 1594 * @private 1595 */ 1596 function _triggerLifecycleStateChanged($this, state) { 1597 _log(true, 'System lifecycle state changed. New state: ' + state); 1598 1599 if (regGlobals.signingOut && state === 'SIGNEDOUT') { 1600 // emulating eIdle connection state to force calling of 'unregisterCb' 1601 // because SIGNEDOUT lifecycle state comes a lot earlier than connection state update. 1602 // Registered phone is unavailable in between anyway. 1603 regGlobals.signingOut = false; 1604 _triggerProviderEvent($this, 'eIdle'); 1605 1606 } 1607 1608 if (state === 'SIGNINGOUT') { 1609 regGlobals.signingOut = true; 1610 } 1611 1612 } 1613 1614 /** 1615 * Handler for lifecyclessosessionchanged event. Used only for debugging. 1616 * @param {string} content New lifecycle session state 1617 * Candidate for removal. 1618 * @private 1619 */ 1620 function _triggerLifecycleSSOSessionChanged($this, content) { 1621 // TODO: implement or remove 1622 _log(true, 'SSO: Lifecycle session changed. New state: ' + content); 1623 } 1624 1625 /** 1626 * Handler for cancancelssochanged event. Signals if it is possible to cancel ongoing SSO login. 1627 * @param {Object} content 1628 * @param {Boolean} content.cancancel 1629 * @private 1630 */ 1631 function _triggerCanCancelSSOChanged($this, content) { 1632 _log(true, 'SSO: CanCancel property changed. New state: ' + content.cancancel); 1633 SSOGlobals.canCancel = content.cancancel; 1634 // TODO: trigger public event, so cancel button can be enabled/disabled 1635 // is it neccessary!? it is always enabled just before navigateTo event, so there is no posibility that navigateTo is emitted but cancelSSO api is disabled 1636 // var event = $.Event('canCancelSSO.cwic'); 1637 // event.canCancel = content.cancancel; 1638 // $this.trigger(event); 1639 } 1640 1641 /** 1642 * internal function to release all SSO related resources on 1643 * (window.unload event) and on invokeResetData 1644 * @private 1645 */ 1646 function _resetSSO() { 1647 // close popup window 1648 // if (SSOGlobals.popup) { 1649 // SSOGlobals.popup.close(); 1650 // } 1651 } 1652 1653 //****************************************** 1654 // END SSO related work 1655 //****************************************** 1656 1657 //****************************************** 1658 // Certificate validation 1659 //****************************************** 1660 // callback which submits user's choice to accept or reject the certificate 1661 // fp - string, accept - boolean 1662 function handleInvalidCert(fp, accept) { 1663 if (fp && (typeof fp === 'string') && (typeof accept === 'boolean')) { 1664 _log(true, 'handleInvalidCertificate sending response: ' + accept + ' (for fingerprint - ' + fp); 1665 _sendClientRequest('handleInvalidCertificate', {certFingerprint: fp, accept: accept}); 1666 1667 } else { 1668 throw new TypeError('handleInvalidCert: Wrong arguments!'); 1669 } 1670 } 1671 // Inalid certificate event handler 1672 function _triggerInvalidCertificate($this, content) { 1673 /* 1674 content properties: 1675 - certFingerprint 1676 - identifierToDisplay 1677 - certSubjectCN 1678 - referenceId 1679 - invalidReasons 1680 - subjectCertificateData 1681 - intermediateCACertificateData 1682 - allowUserToAccept 1683 - persistAcceptedDecision 1684 */ 1685 if ($.isArray(content.invalidReasons)) { 1686 content.invalidReasons = $.map(content.invalidReasons, function (elem) { 1687 return elem.invalidReason; 1688 }); 1689 } else { 1690 content.invalidReasons = []; 1691 } 1692 //Emit public event 1693 var event = $.Event('invalidCertificate.cwic'); 1694 event.info = content; 1695 event.respond = handleInvalidCert; 1696 1697 $this.trigger(event); 1698 } 1699 //****************************************** 1700 // END Certificate validation 1701 //****************************************** 1702 1703 // ------------------------------------------- 1704 // BEGIN: Call transfer related event handling 1705 // ------------------------------------------- 1706 1707 /** 1708 * Completes ongoing call transfer 1709 * @param {Number} callId conversation id of active conversation 1710 * @private 1711 */ 1712 function completeTransfer(callId) { 1713 var $this = this; 1714 if (transferCallGlobals.inProgress) { 1715 _sendClientRequest('completeTransfer', { 1716 callId: callId 1717 }, $.noop, function errorCb(error) { 1718 // TODO: what errors are returned? should the separate event be triggered for transfer and conference API 1719 _triggerError($this, getError(error, 'NativePluginError'), 'completeTransfer', error); 1720 }); 1721 transferCallGlobals.endTransfer(); 1722 return true; 1723 } else { 1724 _log(true, 'completeTransfer: transfer not in progress, returning from function...'); 1725 return false; 1726 } 1727 1728 } 1729 1730 /** 1731 * Enables button wrapped in jQuery object 1732 * @param {jQuery Object} $el selected button object 1733 * @returns {jQuery Object} button passed-in 1734 * @private 1735 */ 1736 function enable($el) { 1737 if (!($el instanceof jQuery)) { 1738 throw new TypeError('enable function accepts only jQuery objects'); 1739 } 1740 $el.attr('disabled', false); 1741 return $el; 1742 } 1743 1744 /** 1745 * Disables button wrapped in jQuery object 1746 * @param {jQuery Object} $el selected button object 1747 * @returns {jQuery Object} button passed-in 1748 * @private 1749 */ 1750 function disable($el) { 1751 if (!($el instanceof jQuery)) { 1752 throw new TypeError('disable function accepts only jQuery objects'); 1753 } 1754 $el.attr('disabled', true); 1755 return $el; 1756 } 1757 1758 1759 /** 1760 * Event handler for 'attendedtransferstatechange' event 1761 * @private 1762 */ 1763 function _triggerAttendedTransferStateChange($this, content) { 1764 //Emit public event 1765 var event = $.Event('callTransferInProgress.cwic'), 1766 callId = content.callId, 1767 $completeBtn = transferCallGlobals.completeBtn; 1768 1769 transferCallGlobals.callId = callId; 1770 1771 // add wrapped function to event object 1772 event.completeTransfer = wrap(completeTransfer, callId); 1773 1774 /**Helper function for completing call transfer. Calls passed-in function and disables complete and cancel buttons 1775 * @param {function} f function wrap and to call before disabling buttons 1776 * @returns {function} wrapped function, ready to be attached as event handler 1777 * @private 1778 */ 1779 function finishCallTransfer(fn) { 1780 return function (ev) { 1781 fn(); 1782 if ($completeBtn) { 1783 disable($completeBtn).unbind(); 1784 } 1785 }; 1786 } 1787 1788 if (!callId) { 1789 // TODO : trigger error 1790 //_triggerError($this, getError(error), error, 'cannot start conversation'); 1791 _log('_triggerAttendedTransferStateChange: No callId received from the plugin. Returning from the function...'); 1792 return; 1793 } 1794 _log(true, '_triggerAttendedTransferStateChange: Received callId: ' + callId); 1795 1796 // check if complete and cancel buttons are passed in 1797 if (typeof $completeBtn === 'string') { 1798 $completeBtn = $('#' + $completeBtn); 1799 } 1800 1801 // duck typing to check if passed-in buttons are jQuery objects that we need 1802 if ($completeBtn && 1803 (typeof $completeBtn.one === 'function') && 1804 $completeBtn.attr 1805 ) { 1806 enable($completeBtn).unbind().one('click', finishCallTransfer(event.completeTransfer)); 1807 1808 return; 1809 } else { 1810 $this.trigger(event); 1811 return; 1812 } 1813 } 1814 1815 // ------------------------------------------ 1816 // END: Call transfer related event handling 1817 // ------------------------------------------ 1818 1819 var isMultimediaStarted = false; 1820 1821 function _triggerCurrentRingtoneChanged($this, content) { 1822 var event = $.Event('ringtoneChange.cwic'); 1823 event.currentRingtone = content.ringtone; 1824 $this.trigger(event); 1825 } 1826 1827 function _triggerMultimediaCapabilitiesStarted($this, isMultimediaCapabilityStarted) { 1828 isMultimediaStarted = isMultimediaCapabilityStarted; 1829 1830 var event = $.Event('multimediaCapabilities.cwic'); 1831 event.multimediaCapability = isMultimediaCapabilityStarted; 1832 $this.trigger(event); 1833 1834 if (isMultimediaCapabilityStarted) { 1835 getAvailableRingtones($this); 1836 _sendClientRequest('getMultimediaDevices', function mmDevicesCb(content) { 1837 _triggerMMDeviceEvent($this, content); 1838 }); 1839 } 1840 } 1841 1842 1843 /** 1844 * an internal function to log messages 1845 * @param (Boolean) [isVerbose] indicates if msg should be logged in verbose mode only (configurable by the application). true - show this log only in verbose mode, false - always show this log <br> 1846 * @param (String) msg the message to be logged (to console.log by default, configurable by the application) <br> 1847 * @param (Object) [context] a context to be logged <br> 1848 */ 1849 function _log() { 1850 var isVerbose = typeof arguments[0] === 'boolean' ? arguments[0] : false; 1851 var msg = typeof arguments[0] === 'string' ? arguments[0] : arguments[1]; 1852 var context = typeof arguments[1] === 'object' ? arguments[1] : arguments[2]; 1853 1854 if ((!isVerbose || (isVerbose && settings.verbose)) && $.isFunction(settings.log)) { 1855 try { 1856 var current = new Date(); 1857 var timelog = current.getDate() + '/' + 1858 ('0' + (current.getMonth() + 1)).slice(-2) + '/' + 1859 current.getFullYear() + ' ' + 1860 ('0' + current.getHours()).slice(-2) + ':' + 1861 ('0' + current.getMinutes()).slice(-2) + ':' + 1862 ('0' + current.getSeconds()).slice(-2) + '.' + 1863 ('00' + current.getMilliseconds()).slice(-3) + ' '; 1864 settings.log('[cwic] ' + timelog + msg, context); 1865 } catch (e) { 1866 // Exceptions in application-define log functions can't really be logged 1867 } 1868 } 1869 } 1870 1871 // Helper function to check if plug-in is still available. 1872 // Related to DE3975. The CK advanced editor causes the overflow CSS attribute to change, which in turn 1873 // removes and replaces the plug-in during the reflow losing all state. 1874 var _doesPluginExist = function () { 1875 var ret = true; 1876 if (!_plugin || !_plugin.api) { 1877 _log(true, '_doesPluginExist failed basic existence check'); 1878 ret = false; 1879 } else if (typeof _plugin.api.sendRequest === 'undefined' && typeof _plugin.api.postMessage === 'undefined') { 1880 _log(true, '_doesPluginExist failed sendRequest/postMessage method check'); 1881 ret = false; 1882 } 1883 1884 return ret; 1885 }; 1886 1887 // support unit tests for IE 1888 // should be more transparent than this, but tell that to internet explorer - you can't override attachEvent... 1889 var _addListener = function (obj, type, handler) { 1890 try { 1891 // if the object has a method called _addListener, then we're running a unit test. 1892 if (obj._addListener) { 1893 obj._addListener(type, handler, false); 1894 } else if (obj.attachEvent) { 1895 obj.attachEvent('on' + type, handler); 1896 } else { 1897 obj.addEventListener(type, handler, false); 1898 } 1899 } catch (e) { 1900 _log('_addListener error: ', e); 1901 } 1902 }; 1903 var _removeListener = function (obj, type, handler) { 1904 try { 1905 // if the object has a method called _addListener, then we're running a unit test. 1906 if (obj._addListener) { 1907 return; 1908 //obj._removeListener(type,handler,false); 1909 } else if (obj.attachEvent) { 1910 obj.detachEvent('on' + type, handler); 1911 } else { 1912 obj.removeEventListener(type, handler, false); 1913 } 1914 } catch (e) { 1915 _log('_removeListener error: ', e); 1916 } 1917 }; 1918 1919 var _handlePluginMessage = function _handlePluginMessage(msg) { 1920 var $this = (_plugin) ? _plugin.scope : this, 1921 i; 1922 //in case of NPAPI, check if clientId matches 1923 //TODO: we did it in registerNPAPI handlers....check 1924 1925 if (msg.ciscoChannelServerMessage) { 1926 if (msg.ciscoChannelServerMessage.name === 'ChannelDisconnect') { 1927 _log('Extension channel disconnected', msg.ciscoChannelServerMessage); 1928 _plugin = null; 1929 clientRequestCallbacks.purge(); 1930 // TODO anything else we need to shutdown or clean up so that the app can re-init? 1931 _triggerError($this, settings.error, errorMap.ExtensionNotAvailable, 'Lost connection to browser extension'); 1932 } else if (msg.ciscoChannelServerMessage.name === 'HostDisconnect') { 1933 _log('Host application disconnected', msg.ciscoChannelServerMessage); 1934 _plugin = null; 1935 clientRequestCallbacks.purge(); 1936 // TODO anything else we need to shutdown or clean up so that the app can re-init? 1937 _triggerError($this, settings.error, errorMap.PluginNotAvailable, 'Lost connection to plugin'); 1938 } else { 1939 _log('ciscoChannelServerMessage unknown name: ' + msg.ciscoChannelServerMessage.name); 1940 } 1941 } else if (msg.ciscoSDKServerMessage) { 1942 var content = msg.ciscoSDKServerMessage.content; 1943 var error = msg.ciscoSDKServerMessage.error; 1944 var msgId = msg.ciscoSDKServerMessage.replyToMessageId; 1945 var name = msg.ciscoSDKServerMessage.name; 1946 1947 _log(true, '_handlePluginMessage: ' + name, msg.ciscoSDKServerMessage); 1948 1949 // ************************************************************* 1950 // Lifecycle errors customizations BEGIN 1951 // ************************************************************* 1952 // plugin sends an array of error codes. We never seen more then one error in that array, 1953 // so we will take only the first error and send it as a string to handlers instead of array. 1954 // We will log other errors if present 1955 1956 // content.errors is like [{error: 'err1'}, {error:'err2'}] 1957 // simplify it to array of strings. 1958 if (content && content.errors) { 1959 content.errors = $.map(content.errors, function (errorObj) { 1960 return errorObj.error !== '' ? errorObj.error : null; 1961 }); 1962 1963 _log(true, 'Lifecycle error list contains ' + content.errors.length + ' errors'); 1964 for (i = 0; i < content.errors.length; i++) { 1965 _log(true, 'Lifecycle error from the list', content.errors[i]); 1966 } 1967 1968 content.error = content.errors.length ? 1969 getError(content.errors[0]) : 1970 null; 1971 } 1972 1973 // empty string is passed when there is no error is JCF 1974 if (content && content.errors === '') { 1975 _log(true, 'Received empty string instead of lifecycle error list'); 1976 content.errors = []; 1977 content.error = null; 1978 } 1979 1980 // ************************************************************* 1981 // Lifecycle errors customizations END 1982 // ************************************************************* 1983 1984 1985 // first check if we have a callback waiting 1986 if (msgId) { 1987 clientRequestCallbacks.callCallback(msgId, error, content); 1988 } 1989 1990 // then trigger any other matching events 1991 switch (name) { 1992 case 'init': 1993 _cwic_onInitReceived(content); 1994 break; 1995 case 'userauthorized': 1996 _cwic_userAuthHandler(content); 1997 break; 1998 case 'connectionstatuschange': 1999 _triggerProviderEvent($this, content); 2000 break; 2001 case 'multimediacapabilitiesstarted': 2002 _triggerMultimediaCapabilitiesStarted($this, true); 2003 break; 2004 case 'multimediacapabilitiesstopped': 2005 _triggerMultimediaCapabilitiesStarted($this, false); 2006 break; 2007 case 'ringtonechanged': 2008 _triggerCurrentRingtoneChanged($this, content); 2009 break; 2010 case 'attendedtransferstatechange': 2011 _triggerAttendedTransferStateChange($this, content); 2012 break; 2013 case 'multimediadevicechange': 2014 //multimedia devices changed, now go get the new list 2015 if (isMultimediaStarted) { 2016 _sendClientRequest('getMultimediaDevices', function mmDevicesCb(content) { 2017 _triggerMMDeviceEvent($this, content); 2018 }); 2019 } 2020 break; 2021 case 'connectionfailure': 2022 // all errors should be handled through lifecycle states 2023 _log(true, 'Connection Failure event received with reason: ', content); 2024 var errorKey = getError(content, 'ServerConnectionFailure'); 2025 _triggerError($this, regGlobals.errorCb, errorKey, content, { registration: registration }); 2026 break; 2027 case 'authenticationresult': 2028 _triggerAuthResultAndStatus($this, content); 2029 break; 2030 case 'telephonydeviceschange': 2031 _triggerTelephonyDevicesChangeDebounced($this, content); 2032 break; 2033 case 'callcontrolmodechange': 2034 // TODO: this event is triggered all the time (oldMode->NoMode->newMode), so there is no real value from this event... 2035 // 2036 // Pass only documented values. filter 'ExtendAndConnect' for now! 2037 //_log(true, 'Phone mode changed. New mode: ' + content.phoneMode); 2038 //if (content.phoneMode === 'SoftPhone' || content.phoneMode === 'DeskPhone') { 2039 // registration.newMode = content.phoneMode; 2040 // _triggerProviderEvent($this, 'ePhoneModeChanged'); 2041 //} 2042 break; 2043 case 'callstatechange': 2044 _triggerConversationEvent($this, content, 'state'); 2045 break; 2046 case 'externalwindowevent': 2047 _triggerExternalWindowEvent($this, content); 2048 //for docking to work we need to know when video is being received 2049 dockGlobals.isVideoBeingReceived = content.showing; 2050 break; 2051 case 'videoresolutionchange': 2052 _log(true, 'video resolution change detected for call ' + content.callId + 2053 '. Height: ' + content.height + 2054 ', width: ' + content.width, content); 2055 // trigger a conversation event with a 'videoResolution' property 2056 _triggerConversationEvent($this, { 2057 callId: content.callId, 2058 videoResolution: { 2059 width: content.width, 2060 height: content.height 2061 } 2062 }, 'render'); 2063 break; 2064 case 'ssonavigateto': 2065 _triggerSSONavigateTo($this, content); 2066 break; 2067 case 'userprofilecredentialsrequired': 2068 // content.errors - [string], content.error - string, content.authenticatorId - number 2069 _triggerCredentialsRequired($this, content); 2070 break; 2071 case 'ssosigninrequired': 2072 // content.errors - [string], content.error - string 2073 _triggerSSOSignInFailed($this, content); 2074 break; 2075 case 'userprofileemailaddressrequired': 2076 // content.errors - [string], content.error - string 2077 _triggerEmailRequired($this, content); 2078 break; 2079 case 'loggedin': 2080 _triggerSignedIn($this); 2081 break; 2082 case 'invokeresetdata': 2083 _triggerInvokeResetData(content); 2084 break; 2085 case 'ssoshowwindow': 2086 _triggerSSOShowWindow(content); 2087 break; 2088 case 'ssosessionexpirypromptrequired': 2089 _triggerSSOSessionExpiryPromptRequired(); 2090 break; 2091 case 'lifecyclestatechanged': 2092 _triggerLifecycleStateChanged($this, content); 2093 break; 2094 case 'lifecyclessosessionchanged': 2095 _triggerLifecycleSSOSessionChanged($this, content); 2096 break; 2097 case 'cancancelsinglesignonchanged': 2098 _triggerCanCancelSSOChanged($this, content); 2099 break; 2100 case 'invalidcertificate': 2101 _triggerInvalidCertificate($this, content); 2102 break; 2103 default: 2104 // _log(true, 'ciscoSDKServerMessage unknown name '+name, msg); 2105 } 2106 } else { 2107 _log(true, 'Unknown msg from plugin: ', msg); 2108 } 2109 }; 2110 2111 2112 // Good-enough unique ID generation: 2113 // used for clienId (generated only once per cwic instance) and message ids (generated for each message) 2114 // Just needs to be unique for this channel across restarts of this client, 2115 // just in case there's an old server reply in the pipeline. 2116 function generateUniqueId() { 2117 return (new Date()).valueOf().toString() + Math.floor((Math.random() * 10000) + 1).toString(); 2118 } 2119 2120 /** 2121 * sends clientRequest message to browser plugin/extension 2122 * @param {String} name 2123 * @param {Object|String} content Object or string to be passed as arguments for the named request 2124 * @param {function} [successCb(result)] Function to be called upon recieving success reply to the request. 2125 * replyCb should take object as argument the return result from native function. 2126 * @param {function} [errorCb(errorMapAlias)] Function to be called upon recieving error reply to the request. 2127 * errorCb should take object as argument an errorMapAlias string. If no errorCb is provided 2128 * then a generic error.cwic event will be triggered instead. 2129 * @private 2130 */ 2131 function _sendClientRequest() { 2132 if (!_plugin || !_plugin.api) { 2133 _log('_sendClientRequest no plugin available'); 2134 return false; 2135 } 2136 2137 var name = arguments[0]; 2138 var content = null; 2139 var successCb = null; 2140 var $this = _plugin.scope || this; 2141 2142 if ($.isFunction(arguments[1])) { 2143 successCb = arguments[1]; 2144 } else { 2145 content = arguments[1]; 2146 } 2147 2148 // create a default error callback 2149 var errorCb = function (errorMapAlias) { 2150 _triggerError($this, getError(errorMapAlias), { 'nativeError': errorMapAlias, 'nativeRequest': name, 'nativeArgs': content }, 'unexpected error reply from native plugin'); 2151 }; 2152 2153 if ($.isFunction(arguments[2])) { 2154 if (successCb === null) { 2155 successCb = arguments[2]; 2156 } else { 2157 errorCb = arguments[2]; 2158 } 2159 } 2160 2161 if ($.isFunction(arguments[3])) { 2162 errorCb = arguments[3]; 2163 } 2164 2165 // if nothing else, default to no-op 2166 successCb = successCb || $.noop; 2167 2168 var maskContent = (name === 'encryptCucmPassword') ? '*****' : content; 2169 _log(true, '_sendClientRequest: ' + name, { 'successCb': successCb, 'errorCb': errorCb, 'content': maskContent }); 2170 2171 2172 // Good-enough unique ID generation: 2173 // Just needs to be unique for this channel across restarts of this client, 2174 // just in case there's an old server reply in the pipeline. 2175 var uid = (name === 'init') ? 0 : generateUniqueId(); 2176 2177 var clientMsg = { 2178 ciscoSDKClientMessage: { 2179 'messageId': uid, 2180 'name': name, 2181 // if null, set content undefined so that it is omited from the JSON msg 2182 'content': (content === null) ? undefined : content 2183 } 2184 }; 2185 2186 // clone the msg object to allow masking of password for the log 2187 var logMsg = $.extend({}, clientMsg.ciscoSDKClientMessage); 2188 logMsg.content = maskContent; 2189 _log(true, 'send ciscoSDKClientMessage: ' + name, logMsg); 2190 2191 //send message to Chrome 2192 if (_plugin.api.sendRequest) { 2193 _plugin.api.sendRequest(clientMsg); 2194 } else if (_plugin.api.postMessage) { 2195 clientId = clientId || generateUniqueId(); 2196 var clientInfoJSON = { 2197 'id': clientId, 2198 'url': window.location.href, 2199 'hostname': window.location.hostname, 2200 'name': window.document.title 2201 }; 2202 //*****JSON message changed, so it fits NPAPI expectations 2203 var NPAPI_Msg = {'ciscoChannelMessage': $.extend(clientMsg, {'client': clientInfoJSON})}; 2204 2205 //send message to browsers that support NPAPI 2206 _plugin.api.postMessage(JSON.stringify(NPAPI_Msg)); 2207 } 2208 2209 // asynchronous back end so we store the callback 2210 clientRequestCallbacks.callbacks[uid] = { 'successCb': successCb, 'errorCb': errorCb }; 2211 } 2212 2213 var clientRequestCallbacks = { 2214 // callbacks[messageId] = { 'successCb' : successCb, 'errorCb' : errorCb } 2215 callbacks: [], 2216 2217 callCallback: function (messageId, error, nativeResult) { 2218 if (!this.callbacks[messageId]) { 2219 return; 2220 } 2221 var callback, arg; 2222 if (error && error !== 'eNoError') { 2223 callback = this.callbacks[messageId].errorCb; 2224 arg = error; 2225 } else { 2226 callback = this.callbacks[messageId].successCb; 2227 arg = nativeResult; 2228 } 2229 2230 if ($.isFunction(callback)) { 2231 _log(true, 'clientRequestCallbacks calling result callback for msgId: ' + messageId); 2232 try { 2233 callback(arg); 2234 } catch (e) { 2235 _log('Exception occurred in clientRequestCallbacks callback', e); 2236 } 2237 } 2238 delete this.callbacks[messageId]; 2239 }, 2240 2241 purge: function () { 2242 this.callbacks = []; 2243 } 2244 }; 2245 2246 function rebootIfBroken(rebootCb) { 2247 2248 var pluginExists = _doesPluginExist(); 2249 if (!pluginExists) { 2250 _log('Plugin does not exist. Restarting....'); 2251 rebootCb(); 2252 } 2253 2254 return pluginExists; 2255 } 2256 2257 // Returns null if plugin is not installed, 0 if the plugin is installed, but 2258 // we can't tell what version it is, or the version of the plugin. 2259 function _getNpapiPluginVersion() { 2260 // regexp to get the version of the plugin. 2261 var r2 = /(\d+\.\d+\.\d+\.\d+)/; 2262 var match, 2263 version = null; 2264 var pluginMimeType = navigator.mimeTypes['application/x-ciscowebcommunicator']; 2265 var cwcPlugin = pluginMimeType ? pluginMimeType.enabledPlugin : undefined; 2266 2267 if (cwcPlugin) { 2268 // plugin is enabled 2269 version = 0; 2270 if (cwcPlugin.version) { 2271 // use explicit version if provided by browser 2272 version = cwcPlugin.version; 2273 } else { 2274 // extract version from description 2275 match = r2.exec(cwcPlugin.description); 2276 if (match && match[0]) { 2277 version = match[0]; 2278 } 2279 } 2280 } 2281 return version; 2282 } 2283 2284 /** 2285 * Versions, states and capabilities. 2286 * @returns {aboutObject} 2287 */ 2288 function about() { 2289 _log(true, 'about', arguments); 2290 2291 /* 2292 * Versioning scheme: Release.Major.Minor.Revision 2293 * Release should be for major feature releases (such as video) 2294 * Major for an API-breaking ship within a release (or additional APIs that won't work without error checking on previous plug-ins). 2295 * Minor for non API-breaking builds, such as bug fix releases that strongly recommend updating the plug-in 2296 * Revision for unique build tracking. 2297 */ 2298 2299 var ab = { 2300 javascript: { 2301 version: 'REL.MAJ.MIN.BUILD', 2302 system_release: 'Cisco Unified Communications System Release SYSMAJ.SYSMIN' 2303 }, 2304 jquery: { 2305 version: $.fn.jquery 2306 }, 2307 channel: null, // chrome extension, if any 2308 plugin: null, // either NPAPI plugin or native host app 2309 states: { 2310 system: 'unknown', 2311 device: { 2312 exists: false, 2313 inService: false, 2314 lineDNs: [], 2315 modelDescription: '', 2316 name: '' 2317 } 2318 }, 2319 capabilities: {}, 2320 upgrade: {} 2321 }; 2322 2323 // get local cwic javascript version 2324 var m = ab.javascript.version.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/); 2325 if (m) { 2326 ab.javascript.release = m[1]; 2327 ab.javascript.major = m[2]; 2328 ab.javascript.minor = m[3]; 2329 ab.javascript.revision = m[4]; 2330 } 2331 2332 // get channel extension version, if any 2333 if (typeof cwic_plugin !== 'undefined') { 2334 ab.channel = cwic_plugin.about(); 2335 // at some point, validation of compatibility of Chrome Extension might be needed, but not for now. 2336 } 2337 2338 // get plugin (either NPAPI or native host) version and validate compatibility 2339 if (_plugin && _plugin.version) { 2340 ab.plugin = { 2341 version: _plugin.version 2342 }; 2343 } else { 2344 var version = _getNpapiPluginVersion(); 2345 if (version === 0) { 2346 // something is installed but we can't identify it 2347 ab.upgrade.plugin = 'mandatory'; 2348 } else if (version) { 2349 // we extracted a version 2350 ab.plugin = { version: { plugin : version } }; 2351 } 2352 } 2353 2354 if (ab.plugin) { 2355 m = ab.plugin.version.plugin.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/); 2356 if (m) { 2357 ab.plugin.release = m[1]; 2358 ab.plugin.major = m[2]; 2359 ab.plugin.minor = m[3]; 2360 ab.plugin.revision = m[4]; 2361 } 2362 2363 // compare javascript and plugin versions to advise about upgrade 2364 if (ab.javascript.release > ab.plugin.release) { 2365 // release mismatch, upgrade plugin 2366 ab.upgrade.plugin = 'mandatory'; 2367 } else if (ab.javascript.release < ab.plugin.release) { 2368 // release mismatch, upgrade javascript 2369 ab.upgrade.javascript = 'mandatory'; 2370 } else if (ab.javascript.release === ab.plugin.release) { 2371 // same release, compare major 2372 if (ab.javascript.major > ab.plugin.major) { 2373 // newer javascript should always require new plugin 2374 ab.upgrade.plugin = 'mandatory'; 2375 } else if (ab.javascript.major < ab.plugin.major) { 2376 // newer plugin should generally be backward compatible 2377 ab.upgrade.javascript = 'recommended'; 2378 } else if (ab.javascript.major === ab.plugin.major) { 2379 // same release.major, compare minor 2380 if (ab.javascript.minor > ab.plugin.minor) { 2381 ab.upgrade.plugin = 'recommended'; 2382 } else if (ab.javascript.minor < ab.plugin.minor) { 2383 ab.upgrade.javascript = 'recommended'; 2384 } 2385 } 2386 } 2387 } else { 2388 ab.upgrade.plugin = 'unknown'; 2389 } 2390 2391 if (_plugin) { 2392 // _plugin.connectionStatus gets set/updated along the way. If not, keep default from above. 2393 ab.states.system = _plugin.connectionStatus || ab.states.system; 2394 // registration.device gets set in _updateRegistration and registerPhone. If not, use default device from above. 2395 ab.states.device = registration.device || ab.states.device; 2396 // _plugin.capabilities gets set by _cwic_onFBPluginLoaded. If not, keep default from above. 2397 ab.capabilities = _plugin.capabilities || ab.capabilities; 2398 } 2399 2400 return ab; 2401 } 2402 2403 /** 2404 * predict a device based on username 2405 * @param {Object} options 2406 * @type {String} 2407 */ 2408 function _predictDevice(options) { 2409 if ($.isFunction(settings.predictDevice)) { 2410 try { 2411 return settings.predictDevice(options); 2412 } catch (predictDeviceException) { 2413 _log('Exception occurred in application predictDevice callback', predictDeviceException); 2414 if (typeof console !== 'undefined' && console.trace) { 2415 console.trace(); 2416 } 2417 } 2418 2419 } else { 2420 return (options.username) ? settings.devicePrefix + options.username : ''; 2421 } 2422 } 2423 2424 var videowindowloadedcallbacks = { 2425 windowobjects: [], 2426 getWindowId: function (args) { 2427 var win = args.window; 2428 var windowid = this.windowobjects.indexOf(win); 2429 if (windowid === -1 && !args.readOnly) { 2430 this.windowobjects.push(win); 2431 windowid = this.windowobjects.indexOf(win); 2432 this.callbacks[windowid] = {}; 2433 } 2434 return windowid; 2435 }, 2436 // callbacks[windowId] = {pluginId: {callback: <function>, wascalled: <bool> }} 2437 callbacks: [], 2438 2439 callCallback: function (win, pluginIdIn) { 2440 function callbackInner($this, windowId, pluginId) { 2441 if ($this.callbacks[windowId].hasOwnProperty(pluginId)) { 2442 var onloaded = $this.callbacks[windowId][pluginId]; 2443 if (!onloaded.wascalled && onloaded.callback) { 2444 try { 2445 onloaded.callback(pluginId); 2446 } catch (videoLoadedException) { 2447 _log('Exception occurred in application videoLoaded callback', videoLoadedException); 2448 if (typeof console !== 'undefined' && console.trace) { 2449 console.trace(); 2450 } 2451 } 2452 onloaded.wascalled = true; 2453 } 2454 } 2455 } 2456 2457 2458 var windowId = this.getWindowId({ window: win, readOnly: true }), 2459 pluginId; 2460 2461 if (pluginIdIn) { 2462 // Correct plugin id provided by v3 MR2 or later plugin, just call the correct one. 2463 callbackInner(this, windowId, pluginIdIn); 2464 } else { 2465 // Fallback to the old buggy way where thh id was not available in the onload callback. 2466 for (pluginId in this.callbacks[windowId]) { 2467 if (this.callbacks[windowId].hasOwnProperty(pluginId)) { 2468 callbackInner(this, windowId, pluginId); 2469 } 2470 } 2471 } 2472 } 2473 }; 2474 2475 // we should call configure method only once, when first video object is created... 2476 var configureCalled = false; 2477 2478 function generateMessageParamsForIBVideo(callId) { 2479 var cid = clientId || generateUniqueId(), 2480 mid = generateUniqueId(), 2481 url = window.location.href, 2482 hostname = window.location.hostname, 2483 name = window.document.title, 2484 ret, 2485 callIdInt; 2486 2487 ret = [mid, cid, url, hostname, name]; 2488 2489 if (callId) { 2490 try { 2491 callIdInt = parseInt(callId, 10); 2492 } catch (e) { 2493 _log(true, 'generateMessageParamsForIBVideo: invalid callId received...'); 2494 callIdInt = -1; 2495 } 2496 ret.unshift(callIdInt); 2497 //ret = [callIdInt]; 2498 } 2499 2500 return ret; 2501 } 2502 2503 /** 2504 * 2505 * @param {Object} args 2506 * @param {Object|String} args.obj Video plugin object 2507 * @param {String} args.methodName Method name on video object 2508 * @param {Function} [args.error] 2509 * @param {Function} [args.success] 2510 * @param {String} [args.callId] 2511 * @private 2512 */ 2513 function callMethodOnVideoObj(args) { 2514 var obj = args.obj, 2515 methodName = args.methodName, 2516 successCb = args.success, 2517 errorCb = args.error, 2518 msgParams; 2519 2520 msgParams = generateMessageParamsForIBVideo(args.callId); 2521 2522 if (obj && (typeof obj[methodName] === 'function' || typeof obj[methodName] === 'object')) { // on IE, NPAPI function is type of 'object'! 2523 _log(true, 'preparing to call "' + methodName + '" method on video object, with parameters', msgParams); 2524 obj[methodName].apply(this, msgParams); 2525 if (successCb) { 2526 successCb(obj); 2527 } 2528 } else { 2529 _log(true, 'calling "' + methodName + '" method on video object failed!'); 2530 if (errorCb) { 2531 errorCb(getError('InvalidArguments')); 2532 } 2533 } 2534 } 2535 2536 /** 2537 * global(window) level function object to handle video plug-in object onLoad 2538 * @type function 2539 * @param {Object} videopluginobject video plug-in object (DOM Element) 2540 * @param {Object} win Optional window object. Used only if it's called from _cwic_onPopupVideoPluginLoaded 2541 * @returns undefined 2542 */ 2543 window._cwic_onVideoPluginLoaded = function (videopluginobject, win) { 2544 _log('_cwic_onVideoPluginLoaded called'); 2545 var winObj = win || window; 2546 2547 if (!configureCalled) { 2548 callMethodOnVideoObj({ 2549 obj: videopluginobject, 2550 methodName: 'configure', 2551 success: function successCb() { 2552 configureCalled = true; 2553 } 2554 }); 2555 } 2556 2557 // For backward compatibility with existing apps, call the createVideoWindow success callback 2558 videowindowloadedcallbacks.callCallback(winObj, videopluginobject.loadid); 2559 2560 // if the callback happens after the app has added the window to the call, 2561 // we need to make the pending call into the plugin now 2562 // if addWindowToCall or addPreviewWindow is called before video object is created 2563 // with createVideoWindow API, that videoObjectId is cached in pending list. 2564 // When new video object is created, onVideoPluginLoaded or onVideoPLuginObjectLoaded is triggered 2565 // and it's checked if there is a pending object with newly created id in the list. 2566 2567 //videowindowsbycall.checkPendingWindowAdds(window, videopluginobject); 2568 //previewwindows.update({ plugin: videopluginobject }); 2569 }; 2570 2571 2572 /** 2573 * global(window) level function to handle video plug-ins loaded in iframe/popup window. 2574 * Should be called from video object onLoad handler {@link $.fn.cwic-createVideoWindow}.<br> 2575 * Example onLoad function in iframe.html 2576 * @example 2577 * function onvideoPluginLoaded(obj) { 2578 * window.parent._cwic_onPopupVideoPluginLoaded(obj, window); 2579 * } 2580 * @returns undefined 2581 * @param {JSAPI} videopluginobject video plug-in object (DOM Element) 2582 * @param {DOMWindow} win iframe or popup window 2583 * @public 2584 */ 2585 window._cwic_onPopupVideoPluginLoaded = function (videopluginobject, win) { 2586 _log('_cwic_onPopupVideoPluginLoaded called'); 2587 2588 _cwic_onVideoPluginLoaded(videopluginobject, win); 2589 2590 // todo: check IE8,9 support 2591 //win.onbeforeunload = function() { 2592 // // popup/iFrame window closed do any clean up work here 2593 // console.log('popup/iFrame window closed'); 2594 //}; 2595 }; 2596 2597 /** 2598 * global(window) level function object to handle SSO token receiving from child window (popup or iFrame). 2599 * Should be called after the token is returned in URL of the target HTML page configured via 'redirect_uri' parameter. 2600 * @type function 2601 * @param {Object} msg message received from popup/iFrame 2602 * @param {string} msg.url URL with parameters: <ul><li>access_token - returned token</li><li>token_type - Always "Bearer"</li><li>expires_in - Number of seconds the returned token is valid</li></ul> 2603 * @example 2604 * //As soon as the page loads, call SSO token handler on the parent window (parent page is a page with cwic.js loaded) 2605 * window.onload = function(e) { 2606 * // The SSO token information is available in a URL fragment. Pass a whole URL string to SSO token handler 2607 * window.opener._cwic_onSSOTokenReceived(location.hash); 2608 * window.close() 2609 * }; 2610 * @public 2611 */ 2612 window._cwic_onSSOTokenReceived = function onSSOTokenReceived(msg) { 2613 var token = '', 2614 url = msg.url, 2615 ssoTokenValidator = validators.get('ssotoken'); 2616 2617 if (ssoTokenValidator.isValid(url)) { 2618 _sendClientRequest('ssoNavigationCompleted', {result: 200, url: url, document: ''}); 2619 2620 } else { 2621 throw new Error(errorMap.InvalidURLFragment.code); 2622 } 2623 }; 2624 2625 /** 2626 * Update the global registration object with information from the native plug-in 2627 */ 2628 function _updateRegistration(state, updateRegCb) { 2629 var props = {}; 2630 2631 function getDevicesCb(res) { 2632 var devices; 2633 2634 if (res.devices) { 2635 props.devices = res.devices; 2636 } 2637 if (res.device) { 2638 props.device = res.device; 2639 registration.device = $.extend({}, res.device); 2640 } 2641 if (res.line) { 2642 props.line = res.line; 2643 registration.line = $.extend(registration.line, res.line); 2644 } 2645 2646 if ((state === 'eIdle' && props.devices) || (props.devices && props.device && props.line)) { 2647 devices = $.makeArray(props.devices); 2648 // merge device information returned by the plug-in 2649 $.each(devices, function (i, device) { 2650 if (device.name) { 2651 var deviceName = $.trim(device.name); 2652 registration.devices[deviceName] = $.extend({}, registration.devices[deviceName], device); 2653 } 2654 }); 2655 2656 if ($.isFunction(updateRegCb)) { 2657 // devicesAvailableCb needs raw devices array, not what was put in registration.devices 2658 /* 2659 * name 2660 * description 2661 * model 2662 * modelDescription 2663 * isSoftPhone 2664 * isDeskPhone 2665 * lineDNs[] 2666 * serviceState 2667 */ 2668 updateRegCb(devices); 2669 } 2670 } 2671 } 2672 // 2673 //-------------------------------------------- 2674 2675 // add device and line info except during logout 2676 if (state !== 'eIdle') { // state = connection status, not a call state! 2677 _sendClientRequest('getProperty', 'device', getDevicesCb); 2678 _sendClientRequest('getProperty', 'line', getDevicesCb); 2679 } 2680 2681 getAvailableDevices(getDevicesCb); 2682 } 2683 2684 function getAvailableDevices(getDevicesCb) { 2685 /* device caching removed for now 2686 if (regGlobals.telephonyDevices && 2687 regGlobals.telephonyDevices.user === registration.user 2688 ) { 2689 _log(true, '[getAvailableDevices] Devices list: ', regGlobals.telephonyDevices.devices); 2690 setTimeout(function() { 2691 getDevicesCb({devices: regGlobals.telephonyDevices.devices}); 2692 }, 1); 2693 2694 } else { 2695 // there is no need to send getAvailableDevices message. 2696 // if cached value is obsolete, getAvailableDevices will return empty list anyway. 2697 //_sendClientRequest('getAvailableDevices', getDevicesCb); 2698 setTimeout(function() { 2699 getDevicesCb({devices: []}); 2700 }, 1); 2701 _log(true, '[getAvailableDevices] Devices list empty'); 2702 } 2703 */ 2704 _sendClientRequest('getAvailableDevices', getDevicesCb); 2705 } 2706 2707 function _triggerProviderEvent($this, state) { 2708 // state = connection status, not call state! 2709 _log(true, 'providerState ' + state); 2710 2711 var event = $.Event('system.cwic'); 2712 event.phone = { status: state, ready: false }; 2713 2714 // _updateRegistration provides a devices list to the callback but we don't use it here 2715 var updateRegCb = function () { 2716 // add global registration to the system event 2717 event.phone.registration = registration; 2718 2719 // ePhoneModeChanged is a special case where 'state' parameter was used for callcontrolmodechange event 2720 // ePhoneModeChanged is constructed in cwic, there is no such value in ecc/jcf. See "callcontrolmodechange" event 2721 if (state === 'ePhoneModeChanged') { // todo: add ePhoneModeChanged in api docs for system event - evemt.phone.status (check sample app) 2722 var newPhoneMode = registration.newMode; 2723 if (newPhoneMode === 'SoftPhone' || newPhoneMode === 'DeskPhone') { 2724 event.phone.ready = true; 2725 } 2726 $this.trigger(event); 2727 2728 return; 2729 } 2730 2731 // otherwise, state is our connectionStatus 2732 _plugin.connectionStatus = state; 2733 if (state === 'eReady') { 2734 // call success callback only if registering phone 2735 if (regGlobals.registeringPhone || regGlobals.switchingMode) { 2736 regGlobals.registeringPhone = false; 2737 regGlobals.switchingMode = false; 2738 2739 // finish registering 2740 2741 if (regGlobals.successCb) { 2742 // extend a local copy of registration to be passed to client's callback 2743 var localRegistration = $.extend({}, registration, { 2744 cucm: $.makeArray(regGlobals.CUCM), 2745 password: regGlobals.passphrase, 2746 mode: null, 2747 successfulCucm: {} 2748 }); 2749 var getPropsCb = function (res) { 2750 _log(true, 'getPropsCb res: ' + JSON.stringify(res)); 2751 if (res.mode !== null) { 2752 $.extend(localRegistration, { mode: res.mode }); 2753 } 2754 if (res.successfulTftpAddress !== null) { 2755 $.extend(localRegistration.successfulCucm, { successfulTftpAddress: res.successfulTftpAddress }); 2756 } 2757 if (res.successfulCtiAddress !== null) { 2758 $.extend(localRegistration.successfulCucm, { successfulCtiAddress: res.successfulCtiAddress }); 2759 } 2760 if (localRegistration.mode && 2761 localRegistration.successfulCucm.successfulTftpAddress !== null && 2762 localRegistration.successfulCucm.successfulCtiAddress !== null) { 2763 // we got all three callbacks, time to move on 2764 _log(true, 'getPropsCb all props returned'); 2765 try { 2766 regGlobals.successCb(localRegistration); 2767 } catch (successException) { 2768 _log('Exception occurred in application success callback', successException); 2769 if (typeof console !== 'undefined' && console.trace) { 2770 console.trace(); 2771 } 2772 } 2773 } 2774 }; 2775 _sendClientRequest('getProperty', 'successfulTftpAddress', getPropsCb); 2776 _sendClientRequest('getProperty', 'successfulCtiAddress', getPropsCb); 2777 _sendClientRequest('getProperty', 'mode', getPropsCb); 2778 } else { 2779 _log('warning: no registerPhone success callback'); 2780 } 2781 } 2782 2783 event.phone.ready = true; 2784 $this.trigger(event); 2785 2786 var callsCb = function (result) { 2787 $.each($.makeArray(result.calls), function (i, call) { 2788 _triggerConversationEvent($this, call, 'state'); 2789 }); 2790 }; 2791 _sendClientRequest('getCalls', callsCb); 2792 } else if (state === 'eIdle') { // state = connection status, not a call state! 2793 if (typeof regGlobals.unregisterCb === 'function') { 2794 regGlobals.unregisterCb(); 2795 } 2796 2797 $this.trigger(event); 2798 } else { 2799 $this.trigger(event); 2800 } 2801 }; 2802 // update global registration 2803 _updateRegistration(state, updateRegCb); 2804 } // end of _triggerProviderEvent 2805 2806 // Called by _cwic_userAuthHandler when user authorization status is UserAuthorized. 2807 // This occurs either directly from _cwic_onInitReceived in the whitelisted case, 2808 // or when userauthorized event is received in showUserAuthorization case. 2809 function _cwic_onPluginReady($this) { 2810 var error; 2811 try { 2812 var defaults = {}, 2813 phoneRegistered = false; 2814 2815 // current connectionStatus was cached in _plugin before calling _cwic_onPluginReady 2816 var currState = _plugin.connectionStatus; 2817 2818 if (currState === 'eReady') { // state = connection status, not a call state! 2819 phoneRegistered = true; 2820 } 2821 2822 // fire and forget requests 2823 2824 // Get initial mm device list. If web calls cwic getMultimediaDevices before this returns, they'll get no devices. 2825 // That's ok because the success callback here is _triggerMMDeviceEvent, which tells the webapp to refresh its list. 2826 // todo: mediadevices service: move after login and revert 2827 //_sendClientRequest('getMultimediaDevices', function mmDevicesCb(content) { 2828 // _triggerMMDeviceEvent($this, content); 2829 //}); 2830 2831 // wait for reply 2832 var modeCb = function (result) { 2833 if ($.isFunction(settings.ready)) { 2834 try { 2835 settings.ready(defaults, phoneRegistered, result.mode); 2836 } catch (readyException) { 2837 _log('Exception occurred in application ready callback', readyException); 2838 if (typeof console !== 'undefined' && console.trace) { 2839 console.trace(); 2840 } 2841 } 2842 } 2843 }; 2844 _sendClientRequest('getProperty', 'mode', modeCb); 2845 2846 if (phoneRegistered) { 2847 var callsCb = function (result) { 2848 $.each($.makeArray(result.calls), function (i, call) { 2849 _triggerConversationEvent($this, call, 'state'); 2850 }); 2851 }; 2852 _sendClientRequest('getCalls', callsCb); 2853 } else { 2854 // CSCue51645 ensure app is in sync with initial plug-in state 2855 _triggerProviderEvent($this, currState); 2856 } 2857 2858 return; 2859 } catch (e) { 2860 if (typeof console !== 'undefined') { 2861 if (console.trace) { 2862 console.trace(); 2863 } 2864 if (console.log && e.message) { 2865 console.log('Exception occured in _cwic_onPluginReady() ' + e.message); 2866 } 2867 } 2868 _plugin = null; 2869 error = $.extend({}, errorMap.PluginNotAvailable, e); 2870 // TODO: Remove hardcoded string 2871 _triggerError($this, settings.error, 'Cannot Initialize Cisco Web Communicator', error); 2872 } 2873 2874 } 2875 2876 /** 2877 * Wait for the document to be ready, and try to load the Cisco Web Communicator add-on.<br> 2878 * If cwic was successfully initialized, call the options.ready handler, <br> 2879 * passing some stored properties (possibly empty), <br> 2880 * otherwise call the options.error handler<br> 2881 * @param {Object} options Is a set of key/value pairs to configure the phone registration. See {@link $.fn.cwic-settings} for options. 2882 * @example 2883 * jQuery('#phone').cwic('init', { 2884 * ready: function(defaults) { 2885 * console.log('sdk is ready'); 2886 * }, 2887 * error: function(error) { 2888 * console.log('sdk cannot be initialized : ' + error.message); 2889 * }, 2890 * log: function(msg, exception) { 2891 * console.log(msg); if (exception) { console.log(exception); } 2892 * }, 2893 * errorMap: { 2894 * // localized message for error code 'AuthenticationFailure' 2895 * AuthenticationFailure : { message: 'Nom d'utilisateur ou mot de passe incorrect' } 2896 * }, 2897 * predictDevice: function(args) { 2898 * return settings.devicePrefix + args.username; 2899 * } 2900 *}); 2901 */ 2902 function init(options) { 2903 var $this = this, 2904 paramsCheck; 2905 2906 _log('init', arguments); 2907 // window.cwic = $this.cwic.bind($this); // Now can call cwic('api', params). Also no more need for global functions...could use window.cwic.global instead 2908 2909 extendDefaultSettingsObject(options); 2910 2911 // check sd params after extending settings object because user defined error callback should be copied to settings object first 2912 paramsCheck = validateDiscoveryParams(options); 2913 if (!paramsCheck.result) { 2914 return _triggerError($this, settings.error, errorMap.InvalidArguments, paramsCheck.reason.join(', ')); 2915 } 2916 2917 registerGlobalInitHandlers($this); 2918 2919 initAddonOnDocumentReady($this); 2920 2921 return $this; 2922 } 2923 2924 function extendDefaultSettingsObject(options) { 2925 // replace or extend the default error map 2926 if (typeof options.errorMap !== 'undefined') { 2927 // extend the default errorMap 2928 $.each(options.errorMap, function (key, info) { 2929 if (typeof info === 'string') { 2930 errorMap[key] = $.extend({}, errorMap[key], { message: info }); 2931 } else if (typeof info === 'object') { 2932 errorMap[key] = $.extend({}, errorMap[key], info); 2933 } else { 2934 _log('ignoring invalid custom error [key=' + key + ']', info); 2935 } 2936 }); 2937 } 2938 2939 // extend the default settings 2940 $.extend(settings, options); 2941 } 2942 2943 function validateDiscoveryParams(options) { 2944 var ret = {result: true, reason: []}; 2945 // check service discovery related properties if necessary 2946 if (settings.serviceDiscovery) { 2947 if (typeof options.credentialsRequired !== 'function') { 2948 ret.reason.push('No credentialRequired callback provided'); 2949 ret.result = false; 2950 } 2951 2952 //todo: clean 2953 // if (typeof options.SSOSignInFailed !== 'function') { 2954 // ret.reason.push('No SSOSignInFailed callback provided'); 2955 // ret.result = false; 2956 // } 2957 2958 if (typeof options.emailRequired !== 'function') { 2959 ret.reason.push('No emailRequired callback provided'); 2960 ret.result = false; 2961 } 2962 2963 if (!options.redirectUri || typeof options.redirectUri !== 'string') { 2964 ret.reason.push('No redirectUri parameter provided'); 2965 ret.result = false; 2966 } 2967 } 2968 2969 return ret; 2970 } 2971 2972 function registerGlobalInitHandlers($this) { 2973 window._cwic_userAuthHandler = function (result) { 2974 _plugin.userAuthStatus = (result) ? 'UserAuthorized' : 'UserDenied'; 2975 _log('_cwic_userAuthHandler result: ' + _plugin.userAuthStatus); 2976 2977 if (result === true) { 2978 //_getAvailableRingtones($this); // todo: mediadevices service: move after login and revert when JCF fix is ready 2979 2980 _sendClientRequest('getProperty', 'connectionStatus', function (result) { 2981 _plugin.connectionStatus = result.connectionStatus; 2982 _cwic_onPluginReady($this); 2983 }); 2984 } else { 2985 _triggerError($this, settings.error, 'Cannot Initialize Cisco Web Communicator', errorMap.NotUserAuthorized); 2986 2987 if (_plugin.deniedCb) { 2988 _plugin.deniedCb(); 2989 _plugin.deniedCb = null; 2990 } 2991 } 2992 }; 2993 2994 /** 2995 * Called by _handlePluginMessage after the init reply is received. 2996 * @param {object} [content] Data payload of the init reply message when called by _handlePluginMessage. 2997 * @param {object} content.version Version details for the loaded plug-in. 2998 * @param {object} content.instanceId 2999 * @param {object} content.userauthstatus 3000 * @param {object} content.capabilities 3001 * @returns undefined 3002 * @private 3003 */ 3004 window._cwic_onInitReceived = function (content) { 3005 var error; 3006 3007 if (typeof cwic_plugin !== 'undefined' && _plugin !== null) { 3008 // what to do if _cwic_onPluginLoaded called twice without first unloading? 3009 _log('plugin is already loaded.'); 3010 return; 3011 } 3012 3013 try { 3014 if (typeof cwic_plugin !== 'undefined') { 3015 //for Chrome 3016 // plug-in is available, update global reference 3017 _plugin = {}; 3018 _plugin.scope = $this; 3019 _plugin.api = cwic_plugin; 3020 } 3021 3022 if (_plugin.api) { 3023 _plugin.instanceId = content.instanceId; 3024 _plugin.version = content.version; 3025 _plugin.userAuthStatus = content.userauthstatus; 3026 _plugin.capabilities = content.capabilities; 3027 } else { 3028 throw getError('PluginNotAvailable'); 3029 } 3030 3031 _log('initialized ' + _plugin.userAuthStatus + ' plugin', _plugin.version); 3032 3033 var ab = about(); 3034 if (ab.upgrade.plugin === 'mandatory') { 3035 _triggerError($this, settings.error, errorMap.PluginNotAvailable, 'Cisco Web Communicator cannot be used when plugin upgrade is "mandatory"'); 3036 return; 3037 } 3038 3039 $(window).unload(function () { 3040 shutdown.call($this); 3041 }); 3042 3043 if (_plugin.userAuthStatus === 'MustShowAuth') { 3044 // MustShowAuth implies we either do delayed Auth or we pop the dialog now 3045 if ($.isFunction(settings.delayedUserAuth)) { 3046 settings.delayedUserAuth(); 3047 } else { 3048 // No additional deniedCb is needed. _cwic_userAuthHandler will trigger NotUserAuthorized error. 3049 showUserAuthorization({ denied : $.noop }); 3050 } 3051 } else if (_plugin.userAuthStatus === 'UserAuthorized') { 3052 // domain whitelisting can give us immediate authorization 3053 _cwic_userAuthHandler(true); 3054 } 3055 3056 return; 3057 } catch (e) { 3058 if (typeof console !== 'undefined') { 3059 if (console.trace) { 3060 console.trace(); 3061 } 3062 if (console.log && e.message) { 3063 console.log('Exception occured in _cwic_onInitReceived() ' + e.message); 3064 } 3065 } 3066 3067 _plugin = null; 3068 error = $.extend({}, errorMap.PluginNotAvailable, e); 3069 } 3070 3071 return _triggerError($this, settings.error, 'Cannot Initialize Cisco Web Communicator', error); 3072 }; // _cwic_onInitReceived 3073 3074 /** 3075 * Phone Object onLoad handler. This function is called as the onload callback for the NPAPI plugin. 3076 * @returns undefined 3077 * @private 3078 */ 3079 window._cwic_onFBPluginLoaded = function () { // TODO: this should not be on window object definitely 3080 var error; 3081 3082 if (_plugin !== null) { 3083 // what to do if _cwic_onFBPluginLoaded called twice without first unloading? 3084 _log('plugin is already loaded.'); 3085 return; 3086 } 3087 3088 try { 3089 // plug-in is available, update global reference 3090 _plugin = {}; 3091 _plugin.scope = $this; 3092 3093 // look for npapi object 3094 var cwcObject = $('#cwc-plugin'); 3095 3096 if (cwcObject) { 3097 _plugin.api = cwcObject[0]; 3098 _registerNpapiCallbacks(); 3099 _sendClientRequest('init'); 3100 } else { 3101 throw getError('PluginNotAvailable'); 3102 } 3103 3104 return; 3105 } catch (e) { 3106 if (typeof console !== 'undefined') { 3107 if (console.trace) { 3108 console.trace(); 3109 } 3110 if (console.log && e.message) { 3111 console.log('Exception occured in _cwic_onFBPluginLoaded() ' + e.message); 3112 } 3113 } 3114 3115 _plugin = null; 3116 error = $.extend({}, errorMap.PluginNotAvailable, e); 3117 } 3118 3119 return _triggerError($this, settings.error, 'Cannot Initialize Cisco Web Communicator', error); 3120 }; // _cwic_onFBPluginLoaded 3121 } // registerGlobalInitHandlers 3122 3123 function initAddonOnDocumentReady($this) { 3124 $(document.body).ready(function () { 3125 initAddon($this); 3126 }); 3127 } 3128 3129 function initAddon($this) { 3130 if (_plugin !== null) { 3131 return; 3132 } 3133 3134 try { 3135 discoverPluginTypeAndInit(); 3136 } catch (e) { 3137 _plugin = null; 3138 _triggerError($this, settings.error, e); 3139 } 3140 3141 3142 // discover addon type: FB plugin(NPAPI/ActiveX) or Chrome Native Host 3143 function discoverPluginTypeAndInit() { 3144 var is_chrome = navigator.userAgent.toLowerCase().indexOf('chrome') > -1; 3145 if (is_chrome === true) { 3146 // try release extId first 3147 var extSettings = { 3148 cwicExtId : 'ppbllmlcmhfnfflbkbinnhacecaankdh', 3149 verbose : settings.verbose 3150 }; 3151 3152 var createScript = function (extSettings) { 3153 var s = document.createElement('script'); 3154 s.id = extSettings.cwicExtId; 3155 s.onload = function () { 3156 cwic_plugin.init(_handlePluginMessage, extSettings); 3157 }; 3158 s.onerror = function () { 3159 _triggerError($this, settings.error, errorMap.ExtensionNotAvailable, 'Chrome requires Cisco Web Communicator extension'); 3160 }; 3161 s.src = 'chrome-extension://' + s.id + '/cwic_plugin.js'; 3162 return s; 3163 }; 3164 3165 if (typeof cwic_plugin === 'undefined') { 3166 var script = createScript(extSettings); 3167 script.onerror = function () { 3168 // remove our first attempt 3169 try { 3170 document.head.removeChild(document.getElementById(extSettings.cwicExtId)); 3171 } catch (e) { 3172 } 3173 // try or dev extId second 3174 _log('Failed loading release version of Chrome extension. Attempting to load dev version next.'); 3175 extSettings.cwicExtId = 'kekllijkldgcokjdjphahkijinjhlapf'; 3176 script = createScript(extSettings); 3177 document.head.appendChild(script); 3178 }; 3179 document.head.appendChild(script); 3180 } else { 3181 _log(true, 'calling init on previously loaded cwic_plugin script'); 3182 extSettings.cwicExtId = cwic_plugin.about().cwicExtId; 3183 cwic_plugin.init(_handlePluginMessage, extSettings); 3184 } 3185 return; 3186 } 3187 3188 var npapiPlugin = false; 3189 var pluginMimeType = navigator.mimeTypes['application/x-ciscowebcommunicator']; 3190 if ('ActiveXObject' in window) { 3191 // IE - try to load the ActiveX/NPAPI plug-in, throw an error if it fails 3192 try { 3193 new ActiveXObject('CiscoSystems.CWCVideoCall'); 3194 // no exception, plug-in is available 3195 // how to check plug-in is enabled in IE ? 3196 npapiPlugin = true; 3197 } catch (e1) { 3198 _log(true, 'ActiveXObject("CiscoSystems.CWCVideoCall") exception: ' + e1.message); 3199 // check if previous release is installed 3200 try { 3201 new ActiveXObject('ActivexPlugin.WebPhonePlugin.1'); 3202 // no exception. previous plug-in is available 3203 throw getError('ReleaseMismatch'); 3204 } catch (e2) { 3205 _log(true, 'ActiveXObject("ActivexPlugin.WebPhonePlugin.1") exception: ' + e2.message); 3206 throw getError('PluginNotAvailable'); 3207 } 3208 } 3209 } else if (typeof pluginMimeType !== 'undefined') { 3210 // Firefox or Safari with our plugin 3211 npapiPlugin = true; 3212 } else { 3213 // plug-in not available, check if any previous release is installed 3214 pluginMimeType = navigator.mimeTypes['application/x-ciscowebphone']; 3215 if (typeof pluginMimeType !== 'undefined') { 3216 // previous plug-in is available 3217 throw getError('ReleaseMismatch'); 3218 } 3219 } 3220 3221 if (npapiPlugin) { 3222 $(document.body).append('<object id="cwc-plugin" width="1" height="1" type="application/x-ciscowebcommunicator"><param name="onload" value="_cwic_onFBPluginLoaded"></param></object>'); 3223 } else { 3224 throw getError('PluginNotAvailable'); 3225 } 3226 } 3227 } 3228 3229 var videopluginid = 1; 3230 3231 /** 3232 * Creates an object that can be passed to startConversation, addPreviewWindow, or updateConversation('addRemoteVideoWindow'). 3233 * The object is inserted into the element defined by the jQuery context - e.g. jQuery('#placeholder').cwic('createVideoWindow') 3234 * inserts the videowindow under jQuery('#placeholder') 3235 * <br> 3236 * <br>NOTE: This function will just 'do nothing' and the success() callback will never be called if either of the following are true: 3237 * <ul> 3238 * <li>video is not supported on the platform, see {@link aboutObject#capabilities:video}</li> 3239 * <li>video plugin objects are not supported in the browser, see {@link aboutObject#capabilities:videoPluginObject}</li> 3240 * </ul> 3241 * NOTE: System resources used when video windows are created cannot be reliably released on all platforms. The application should reuse the 3242 * video objects returned by createVideoWindow, rather than creating new windows for each call to avoid performance problems on some client platforms. 3243 * @example $('#videocontainer').cwic('createVideoWindow', { 3244 * id: 'videoplugin', 3245 * success: function(pluginid) {$('#conversation').cwic('updateConversation',{'addRemoteVideoWindow': pluginid});} 3246 * }); 3247 * @param {Object} [settings] Settings to use when creating the video render object 3248 * @param {String} [settings.id = generated] The DOM ID of the element to be created 3249 * @param {Function} [settings.success] Called when the object is loaded and ready for use plug-in ID is passed as a parameter 3250 * @param {String} [settings.onload] Not recommended for video windows created in the same window as the main phone plug-in. 3251 * <br>Mandatory in popup windows or iframes. The string must be the name of a function in the global scope, and the function 3252 * must call parent or opener {@link window._cwic_onPopupVideoPluginLoaded}. This function will be called in the onload handler 3253 * of the video object. 3254 * <br>Single parameter is the videoplugin object that must be passed to the parent handler. 3255 */ 3256 function createVideoWindow(settings) { 3257 var $this = this; 3258 var ab = about(); 3259 if (ab.capabilities.video && ab.capabilities.videoPluginObject) { 3260 settings = settings || {}; 3261 settings.window = settings.window || window; 3262 3263 var mimetype = 'application/x-cisco-cwc-videocall'; 3264 var onload = settings.onload || '_cwic_onVideoPluginLoaded'; 3265 var callback = settings.success; 3266 var id = settings.id || '_cwic_vw' + videopluginid; 3267 videopluginid++; 3268 3269 var windowid = videowindowloadedcallbacks.getWindowId({ window: settings.window }); 3270 videowindowloadedcallbacks.callbacks[windowid][id] = { callback: callback, wascalled: false }; 3271 3272 var elemtext = '<object type="' + mimetype + '" id="' + id + '"><param name="loadid" value="' + id + '"></param><param name="onload" value="' + onload + '"></param></object>'; 3273 jQuery($this).append(elemtext); 3274 } 3275 3276 return $this; 3277 } 3278 3279 // generalization for executing video object operations. 3280 // Validates window object passed in 3281 function execVideoObjectOperation(args) { 3282 _log(true,'execVideoObjectOperation called with arguments', args); 3283 3284 var winObj = args.window || window, 3285 videoObject = args.videoObject, 3286 methodName = args.methodName; 3287 3288 if (typeof videoObject === 'string') { 3289 try { 3290 videoObject = winObj.document.getElementById(videoObject); 3291 } catch (e) { 3292 _log(true,'execVideoObjectOperation: invalid window object'); 3293 // handle wrong window object 3294 if (args.error) { 3295 args.error(getError('InvalidArguments')); 3296 } 3297 return; 3298 } 3299 } 3300 3301 // validates video object and calls methodName on that object 3302 callMethodOnVideoObj({ 3303 obj: videoObject, 3304 methodName: methodName, 3305 error: args.error || null, 3306 success: args.success || null, 3307 callId: args.callId || null 3308 }); 3309 } 3310 3311 /** 3312 * Assign a video window object to preview (self-view). 3313 * @example 3314 * $('#phone').cwic('addPreviewWindow',{previewWindow: 'previewVideoObjectID'}); 3315 * $('#phone').cwic('addPreviewWindow',{previewWindow: previewVideoObject, window: iFramePinPWindow}); 3316 * $('#phone').cwic('addPreviewWindow',{previewWindow: 'previewVideoObjectID', error: function(err){console.log(err)}}); 3317 * @param {Object} args arguments object 3318 * @param {DOMWindow} [args.window] DOM Window that contains the plug-in Object defaults to current window 3319 * @param {String|Object} args.previewWindow ID or DOM element of preview window 3320 * @param {Function} [args.error] Called when arguments object is malformed, i.e. args.previewWindow ID or DOM element is non-existent or malformed 3321 */ 3322 function addPreviewWindow(args) { 3323 var $this = this, 3324 ab = about(); 3325 3326 if (ab.capabilities.videoPluginObject === false) { 3327 _log(false, 'addPreviewWindow called from unsupported browser'); 3328 return; 3329 } 3330 3331 args.methodName = 'addPreviewWindow'; 3332 args.videoObject = args.previewWindow; 3333 3334 execVideoObjectOperation(args); 3335 3336 return $this; 3337 } 3338 /** 3339 * Remove a video window object from preview (self-view) 3340 * @example 3341 * $('#phone').cwic('removePreviewWindow', { 3342 * previewWindow: 'previewVideoObjectID' 3343 * }); 3344 * 3345 * $('#phone').cwic('removePreviewWindow', { 3346 * previewWindow: previewVideoObject, 3347 * window: iFramePinPWindow 3348 * }); 3349 * 3350 * $('#phone').cwic('removePreviewWindow', { 3351 * previewWindow: 'previewVideoObjectID', 3352 * error: function (err) { 3353 * console.log(err) 3354 * } 3355 * }); 3356 * @param {Object} args arguments object 3357 * @param {DOMWindow} [args.window] DOM Window that contains the plug-in Object defaults to current window 3358 * @param {String|Object} args.previewWindow id or DOM element of preview window 3359 * @param {Function} [args.error] Called when arguments object is malformed, i.e. args.previewWindow ID or DOM element is non-existent or malformed 3360 */ 3361 function removePreviewWindow(args) { 3362 var $this = this, 3363 ab = about(); 3364 3365 if (ab.capabilities.videoPluginObject === false) { 3366 _log(false, 'removePreviewWindow called from unsupported browser'); 3367 return; 3368 } 3369 3370 args.methodName = 'removePreviewWindow'; 3371 args.videoObject = args.previewWindow; 3372 3373 execVideoObjectOperation(args); 3374 3375 return $this; 3376 } 3377 3378 /** 3379 * Called from "startConversation" or "updateConversation" 3380 * @param {Object} args 3381 * @param {String} args.callId 3382 * @param {DOMWindow} args.window 3383 * @param {String|Object} args.remoteVideoWindow 3384 * @private 3385 */ 3386 function addWindowToCall(args) { 3387 var $this = this, 3388 scb = args.success, 3389 // flag to distinguish between cases when "startRemoteVideo" or "addWindowToCall" methods should be called 3390 // We should call startRemoteVideo when: 3391 // - new conversation is started (from startConversation API) 3392 // - conversation is started without video and later video window is added through updateConversation 3393 // - when swithing between 2 conversations (hold/resume) 3394 // We should call addWindowToCall when: 3395 // - new video window is added (updateConversation.addWindowToCall) for the current conversation which already have at least 1 video window 3396 // All those calles are covered with activeConversation.lastId flag. 3397 // This flag is set when new video window is added (not when new conversation is started!) keeping the last callId value when video window was added last time 3398 newCall = false, 3399 ab = about(); 3400 3401 if (ab.capabilities.videoPluginObject === false) { 3402 _log(false, 'addWindowToCall called from unsupported browser'); 3403 return; 3404 } 3405 3406 if (activeConversation.lastId !== args.callId) { 3407 newCall = true; 3408 } 3409 3410 _log(true, 'addWindowToCall() called with arguments: ', args); 3411 _log(true, 'addWindowToCall(): activeConversation object: ', activeConversation); 3412 3413 args.methodName = newCall ? 'startRemoteVideo' : 'addWindowToCall'; 3414 _log(true, 'addWindowToCall() calling ' + args.methodName); 3415 3416 args.videoObject = args.remoteVideoWindow; 3417 3418 args.success = function (videoObj) { 3419 if (newCall) { 3420 _log(true, 'addWindowToCall(): setting activeConversation object'); 3421 activeConversation.videoObject = videoObj; 3422 activeConversation.window = args.window; 3423 activeConversation.lastId = args.callId; 3424 } 3425 3426 if ($.isFunction(scb)) { // if successCb was set, call it 3427 scb(); 3428 } 3429 }; 3430 3431 execVideoObjectOperation(args); 3432 3433 return $this; 3434 } 3435 3436 /** 3437 * Called from "updateConversation" 3438 * @param {Object} args 3439 * @param {String} args.callId 3440 * @param {DOMWindow} args.window 3441 * @param {String|Object} args.remoteVideoWindow 3442 * @param {Boolean} args.endCall 3443 * @private 3444 */ 3445 function removeWindowFromCall(args) { 3446 var $this = this, 3447 endCall = args.endCall, // flag to distinguish between conversation end and updateConv.removeWindowFromCall 3448 ab = about(); 3449 3450 if (ab.capabilities.videoPluginObject === false) { 3451 _log(false, 'removeWindowFromCall called from unsupported browser'); 3452 return; 3453 } 3454 3455 if (activeConversation.lastId !== args.callId) { 3456 _log('cannot call removeWindowFromCall for callId ' + args.callId + '. Last call id was: ' + activeConversation.lastId); 3457 return; 3458 } 3459 3460 _log(true, 'removeWindowFromCall() called with arguments: ', args); 3461 _log(true, 'removeWindowFromCall(): activeConversation object: ', activeConversation); 3462 3463 args.methodName = endCall ? 'stopRemoteVideo' : 'removeWindowFromCall'; 3464 _log(true, 'removeWindowFromCall() calling method ' + args.methodName); 3465 args.videoObject = args.remoteVideoWindow; 3466 3467 execVideoObjectOperation(args); 3468 3469 return $this; 3470 } 3471 3472 3473 /** 3474 * Shuts down the API<br> 3475 * <ul><li>Unregisters the phone</li> 3476 * <li>Unbinds all cwic events handlers</li> 3477 * <li>Clears all cwic data</li> 3478 * <li>Releases the Cisco Web Communicator add-on instance</li></ul> 3479 * @example 3480 * jQuery(window).unload(function() { <br> 3481 * // not necessary, it is already done in cwic.js by default 3482 * // jQuery('#phone').cwic('shutdown'); <br> 3483 * }); <br> 3484 * @example 3485 * jQuery('#shutdown').click(function() { <br> 3486 * jQuery('#phone').cwic('shutdown'); <br> 3487 * }); <br> 3488 */ 3489 function shutdown() { 3490 var $this = this; 3491 _log(true, 'shutdown', arguments); 3492 3493 resetInitSettings(); 3494 3495 signOut(); 3496 3497 _resetSSO(); 3498 3499 // unbind all cwic events handlers 3500 $this.unbind('.cwic'); 3501 3502 // unbind NPAPI events handlers 3503 _unregisterNpapiCallbacks(); 3504 3505 _sendClientRequest('releaseInstance'); 3506 clientRequestCallbacks.purge(); 3507 3508 // check how to remove NPAPI object from dom? 3509 // when object tag's css proerty "display" is set to None, object element has all regular properties. 3510 // Otherwise, it has only NPAPI related properties, so it cannot be removed 3511 // try with document.getElementById('cwc-plugin').style('display: None;').parent().removeChild() 3512 3513 // $('#cwc-plugin').remove(); 3514 _plugin = null; 3515 } 3516 3517 /** 3518 * Authentication result handler. Only logs received event. Sign in logic is moved to lifecycle event handlers. 3519 * @private 3520 */ 3521 function _triggerAuthResultAndStatus($this, resultAndStatus) { 3522 var result = resultAndStatus.result, 3523 status = resultAndStatus.status; 3524 3525 _log(true, 'authentication result: ' + result); 3526 _log(true, 'authentication status: ' + status); 3527 3528 regGlobals.lastAuthStatus = status; 3529 } 3530 3531 /** 3532 * @private 3533 */ 3534 function _triggerMMDeviceEvent($this, result) { 3535 _log(true, 'mmDeviceChange', result); 3536 if (result) { 3537 // store the updated device list and notify the web app 3538 _plugin.multimediadevices = result.multimediadevices; 3539 } 3540 var event = $.Event('mmDeviceChange.cwic'); 3541 $this.trigger(event); 3542 } 3543 3544 /** 3545 * @private 3546 */ 3547 function _triggerExternalWindowEvent($this, state) { 3548 _log(true, 'externalWindowEvent', state); 3549 3550 var event = $.Event('externalWindowEvent.cwic'); 3551 event.externalWindowState = state; 3552 $this.trigger(event); 3553 } 3554 3555 // translate NPAPI events into ciscoSDKServerMessages 3556 function _registerNpapiCallbacks() { 3557 var fbMessageCbName = 'addonmessage'; 3558 3559 function msgHandler(result){ 3560 var msg; 3561 3562 try { 3563 msg = JSON.parse(result); 3564 } catch (e) { 3565 _log(false, 'Invalid JSON message from plugin', e); 3566 throw new Error(errorMap.NativePluginError.code); 3567 } 3568 3569 if (msg.ciscoChannelMessage && msg.ciscoChannelMessage.ciscoSDKServerMessage) { 3570 if (msg.ciscoChannelMessage.client && msg.ciscoChannelMessage.client.id && msg.ciscoChannelMessage.client.id !== clientId) { 3571 return; 3572 } 3573 msg = msg.ciscoChannelMessage; 3574 3575 } 3576 _handlePluginMessage(msg); 3577 } 3578 3579 // add property to '_registerNpapiCallbacks' function which holds the list of registered handlers. Used as a helper to unregister those handlers later. 3580 var handlers = _registerNpapiCallbacks.handlersList = []; 3581 3582 handlers.push({name: fbMessageCbName, handler: msgHandler}); 3583 3584 _log(true, 'adding npapi listener for ' + fbMessageCbName); 3585 _addListener(_plugin.api, fbMessageCbName, msgHandler); 3586 } 3587 3588 // remove registered NPAPI event handlers. Called in 'shutdown' to prevent multiplication of registered events in case when 'init' API function is called multiple times during one session. 3589 function _unregisterNpapiCallbacks() { 3590 var handlers = _registerNpapiCallbacks.handlersList, 3591 name, handler, i, n; 3592 3593 if (handlers) { 3594 for (i = 0, n = handlers.length; i < n; i+=1) { 3595 name = handlers[i].name; 3596 handler = handlers[i].handler; 3597 3598 if (typeof name === 'string' && typeof handler === 'function') { 3599 _log(true, 'removing npapi listener for ' + name + ' event.'); 3600 _removeListener(_plugin.api, name, handler); 3601 } 3602 } 3603 } 3604 else { 3605 _log(true, 'trying to remove npapi listeners, but no "_registerNpapiCallbacks.handlersList" found.'); 3606 } 3607 } 3608 3609 /** 3610 * Switch mode on a session that is already authorized. Can also be used for switching to different device in the same mode<br> 3611 * @example 3612 * $('#phone').cwic('switchPhoneMode',{ 3613 * success: function(registration) { console.log('Phone is in '+registration.mode+' mode'); }, 3614 * error: function(err) { console.log('Error: '+error.message+' while switching mode'); }, 3615 * mode: 'DeskPhone', 3616 * device: 'SEP01234567' 3617 * }); 3618 * @param options 3619 * @param {Function} [options.progress] A handler called when the mode switch has passed pre-conditions.<br>If specified, the handler is called when the switchMode operation starts. 3620 * @param {Function} [options.success(registration)] A handler called when mode switch complete with registration as a parameter 3621 * @param {Function} [options.error(err)] A handler called when the mode switch fails on pre-conditions. {@link $.fn.cwic-errorMapEntry} is passed as parameter. 3622 * @param {string} [options.mode] The new mode 'SoftPhone'/'DeskPhone'. Defaults to SoftPhone. If you want to change a property on a desk phone, such as the line, you must explicitly set this parameter to 'DeskPhone'. 3623 * @param {string} [options.device] Name of the device (e.g. SEP012345678, ECPUSER) to control. If not specified and switching from SoftPhone to DeskPhone mode, it defaults to picking first available. If not specified and switching from DeskPhone to SoftPhone mode, it defaults to output of predictDevice function (see {@link $.fn.cwic-settings.predictDevice}). If 'first available' is desired result in this case, output of custom predictDevice function should be empty string (''). 3624 * @param {string} [options.line] Phone number of a line valid for the specified device (e.g. '0000'). defaults to picking first available 3625 * @param {Boolean} [options.forceRegistration] Specifies whether to forcibly unregister other softphone instances with CUCM. Default is false. See GracefulRegistration doc for more info. 3626 */ 3627 function switchPhoneMode(options) { 3628 var $this = this; 3629 3630 _log(true, 'switchPhoneMode started with arguments: ', options); 3631 3632 if (typeof options !== 'object') { 3633 _log(true, 'switchPhoneMode: no arguments provided, stoping execution!'); 3634 return $this; 3635 } 3636 3637 regGlobals.successCb = $.isFunction(options.success) ? options.success : null; 3638 regGlobals.errorCb = $.isFunction(options.error) ? options.error : null; 3639 regGlobals.switchingMode = true; 3640 3641 var switchModeArgs = { 3642 phoneMode: options.mode || 'SoftPhone', 3643 deviceName: options.device || (options.mode === 'SoftPhone' ? _predictDevice({ username: registration.user }) : ''), 3644 lineDN: options.line || '', 3645 forceRegistration: options.forceRegistration || false 3646 }; 3647 3648 function getDevicesCb(res) { 3649 var devices = res.devices || [], 3650 chooseDefault = true, 3651 filteredDevices, 3652 filteredDevicesNames, 3653 selectedDeviceName, 3654 isDeviceNameValid, 3655 msg, 3656 mode = switchModeArgs.phoneMode, 3657 name = switchModeArgs.deviceName, 3658 lineDN = switchModeArgs.lineDN, 3659 force = switchModeArgs.forceRegistration; 3660 3661 filteredDevices = $.grep(devices, function (elem) { 3662 return (elem.isDeskPhone && mode === 'DeskPhone' || 3663 elem.isSoftPhone && mode === 'SoftPhone'); 3664 }); 3665 3666 if (filteredDevices.length === 0) { 3667 _log(true, 'switchPhoneMode: filtered device list is empty!'); 3668 3669 if ($.isFunction(options.error)) { 3670 _triggerError($this, options.error, 'no device found for mode: ' + '"' + mode + '"'); 3671 } 3672 3673 return $this; 3674 } 3675 3676 filteredDevicesNames = $.map(filteredDevices, function (device) { 3677 return device.name; 3678 }); 3679 3680 isDeviceNameValid = (name && $.inArray(name, filteredDevicesNames) > -1); 3681 3682 if (name && isDeviceNameValid) { 3683 chooseDefault = false; 3684 } 3685 3686 if(!isDeviceNameValid) { 3687 _log(true, 'switchPhoneMode: Device name not set or invalid, proceeding with default device selection'); 3688 } 3689 3690 if (chooseDefault) { 3691 lineDN = ''; // ignore line if device name not given or not valid 3692 selectedDeviceName = filteredDevices[0] && filteredDevices[0].name; 3693 } else { 3694 selectedDeviceName = name; 3695 } 3696 3697 _log(true, 'switchPhoneMode: selected device: ', selectedDeviceName); 3698 _log(true, 'switchPhoneMode: switching to mode: ', mode); 3699 _log(true, 'switchPhoneMode: switching to line: ', lineDN); 3700 3701 _sendClientRequest('connect', { 3702 phoneMode: mode, 3703 deviceName: selectedDeviceName, 3704 lineDN: lineDN, 3705 forceRegistration: force 3706 }, 3707 function successCb() { 3708 onProgress({message: 'Switch mode operation started'}); 3709 }, 3710 function errorCb(error) { 3711 if ($.isFunction(options.error)) { 3712 _triggerError($this, options.error, getError(error), { message: error }); 3713 } 3714 } 3715 ); 3716 } 3717 3718 getAvailableDevices(getDevicesCb); 3719 3720 function onProgress(msg) { 3721 if ($.isFunction(options.progress)) { 3722 try { 3723 options.progress(msg); 3724 } catch (progressException) { 3725 _log('Exception occurred in application switchPhoneMode progress callback', progressException); 3726 if (typeof console !== 'undefined' && console.trace) { 3727 console.trace(); 3728 } 3729 } 3730 } 3731 } 3732 3733 return this; 3734 } 3735 3736 /** 3737 * Register phone to CUCM (SIP register). Used for manual type of registration, in which connection parameters are manually configured (tftp, ccmcip, cti). 3738 * @param args A map with: 3739 * @param {String} args.user The CUCM end user name (required) 3740 * @param {String|Object} args.password String - clear password. Object - {encrypted: encoded password, cipher:'cucm'} 3741 * @param {String|Object|Array} args.cucm The list of CUCM(s) to attempt to register with (required).<br> 3742 * If String, it will be used as a TFTP, CCMCIP and CTI address.<br> 3743 * If Array, a list of String or Object as described above. 3744 * Three is the maximum number of addresses per server (TFTP, CCMCIP, CTI). 3745 * @param {String[]} [args.cucm.tftp] TFTP addresses. Maximum three values. 3746 * @param {String[]} [args.cucm.ccmcip] CCMCIP addresses (will use tftp values if not present). Maximum three values. 3747 * @param {String[]} [args.cucm.cti] Since: 2.1.1 <br> 3748 * CTI addresses (will use tftp values if not present). Maximum three values. 3749 * @param {String} [args.mode] Register the phone in this mode. Available modes are "SoftPhone" or "DeskPhone". Default of intelligent guess is applied after a device is selected.<br> 3750 * @param {Function} [args.devicesAvailable(devices, phoneMode, callback)] Callback called after successful authentication. 3751 * If this callback is not specified, cwic applies the default device selection algorithm. An array of {@link device} objects is passed so the application can select the device.<br> 3752 * To complete the device registration, call the callback function that is passed in to devicesAvailable as the third parameter. 3753 * The callback function is defined in the API, but it must be called by the function that is specified as the devicesAvailable parameter. 3754 * @param {Function} [args.error(err)] Callback called if the registration fails. {@link $.fn.cwic-errorMapEntry} is passed as parameter. 3755 * @param {Boolean} [args.forceRegistration] Specifies whether to forcibly unregister other softphone instances with CUCM. Default is false. See GracefulRegistration doc for more info. 3756 * @param {Function} [args.success(registration)] Callback called when registration succeeds. A {@link registration} object is passed to the callback: 3757 * registerPhone examples <br> 3758 * @example 3759 * // ************************************* 3760 * // register with lab CUCM in default mode (SoftPhone) 3761 * jQuery('#phone').cwic('registerPhone', { 3762 * user: 'fbar', 3763 * password: 'secret', // clear password 3764 * cucm: '1.2.3.4', 3765 * success: function (registration) { 3766 * console.log('registered in mode ' + registration.mode); 3767 * console.log('registered with device ' + registration.device.name); 3768 * } 3769 * }); 3770 * @example 3771 * // ************************************* 3772 * // register with Alpha CUCM in DeskPhone mode with encrypted password 3773 * jQuery('#phone').cwic('registerPhone', { 3774 * user: 'fbar', 3775 * password: { 3776 * encoded: 'GJH$&*"@$%$^BLKJ==', 3777 * cipher: 'cucm' 3778 * }, 3779 * mode: 'DeskPhone', 3780 * cucm: '1.2.3.4', 3781 * success: function (registration) { 3782 * console.log('registered in mode ' + registration.mode); 3783 * console.log('registered with device ' + registration.device.name); 3784 * } 3785 * ); 3786 * @example 3787 * // ************************************* 3788 * // register with Alpha CUCM in SoftPhone mode, select ECP{user} device 3789 * jQuery('#phone').cwic('registerPhone', { 3790 * user: 'fbar', 3791 * password: { 3792 * encoded: 'GJH$&*"@$%$^BLKJ==', 3793 * cipher: 'cucm' 3794 * }, 3795 * mode: 'SoftPhone', 3796 * cucm: { 3797 * ccmcip: ['1.2.3.4'], 3798 * tftp: ['1.2.3.5'] 3799 * }, 3800 * devicesAvailable: function (devices, phoneMode, callback) { 3801 * for (var i = 0; i < devices.length; i++) { 3802 * var device = devices[i]; 3803 * if (device.name.match(/^ECP/i)) { 3804 * callback(phoneMode, device); 3805 * } // starts with 'ECP' 3806 * } 3807 * return; // stop registration if no ECP{user} device found 3808 * }, 3809 * success: function (registration) { 3810 * console.log('registered in mode ' + registration.mode); 3811 * console.log('registered with device ' + registration.device.name); 3812 * }, 3813 * error: function (err) { 3814 * console.log('cannot register phone: ' + err.message); 3815 * } 3816 * ); 3817 */ 3818 function registerPhone(args) { 3819 var $this = this, 3820 devicesAvailableCb, 3821 tftp = [], 3822 ccmcip = [], 3823 cti = [], 3824 result, 3825 props = {}, 3826 createCb, 3827 passphrase, 3828 clearPassphrase, 3829 passphraseValidator; 3830 3831 logArgsWithMaskedPassphrase(args); 3832 3833 // M for manual 3834 setRegGlobalsM(args); 3835 3836 try { 3837 result = parseAndCheckArgs(args, $this); 3838 devicesAvailableCb = result.devicesAvailableCb; 3839 tftp = result.tftp; 3840 ccmcip = result.ccmcip; 3841 cti = result.cti; 3842 passphrase = result.passphrase; 3843 } catch (e) { 3844 // parseAndCheckArgs triggers error callback 3845 return $this; 3846 } 3847 3848 if (typeof passphrase === 'string') { 3849 // clear passphrase, encrypt it 3850 _sendClientRequest('encryptCucmPassword', passphrase, encryptCb); 3851 } else { 3852 // passphrase valid and already encrypted 3853 encryptCb(); 3854 } 3855 // 3856 // after encrypt, a set of properties is collected and authenticateAndConnect is called 3857 // 3858 3859 return $this; 3860 3861 // function definitions (callbacks) 3862 // 3863 function encryptCb(res) { 3864 if (res) { 3865 // update passphrase with encrypted result 3866 passphrase = { 3867 cipher: 'cucm', 3868 encrypted: res 3869 }; 3870 } 3871 // continue with get and set for various props... 3872 _sendClientRequest('setProperty', { 3873 'TftpAddressList': tftp 3874 }, createCb('TftpAddressList')); 3875 3876 _sendClientRequest('setProperty', { 3877 'CtiAddressList': cti 3878 }, createCb('CtiAddressList')); 3879 3880 _sendClientRequest('setProperty', { 3881 'CcmcipAddressList': ccmcip 3882 }, createCb('CcmcipAddressList')); 3883 3884 _sendClientRequest('getProperty', 'connectionStatus', createCb('connectionStatus')); 3885 } 3886 3887 // first need to set/get a bunch of props before moving on to authenticateAndConnect 3888 function createCb(name) { 3889 return function (res) { 3890 _log(true, name + ' callback received'); 3891 props[name] = res[name]; 3892 if (props.hasOwnProperty('TftpAddressList') && 3893 props.hasOwnProperty('CtiAddressList') && 3894 props.hasOwnProperty('CcmcipAddressList') && 3895 props.hasOwnProperty('connectionStatus') 3896 ) { 3897 // all callbacks returned 3898 _log(true, 'All prop callbacks received. Continuing toward authenticateAndConnect', props); 3899 3900 _plugin.connectionStatus = props.connectionStatus; 3901 3902 // now move on to authenticateAndConnect 3903 authenticateAndConnect(); 3904 } 3905 } 3906 } 3907 3908 // authenticateAndConnect gets called after all the property set and get callbacks return 3909 function authenticateAndConnect() { 3910 var currState = _plugin.connectionStatus; 3911 // is the plugin already ready ? 3912 if (currState === 'eReady') { // state = connection status, not a call state! 3913 _triggerProviderEvent($this, currState); 3914 } 3915 regGlobals.passphrase = passphrase; 3916 3917 if (currState !== 'eReady') { // state = connection status, not a call state! 3918 regGlobals.currState = currState; 3919 // authenticatedCallback is called affter receiving authenticationresult event with positive outcome 3920 regGlobals.authenticatedCallback = getAuthenticatedCb(devicesAvailableCb, $this); 3921 // in this step, passphrase must be in encrypted format 3922 // todo: maybe remove - passphrase already validated a step before 3923 if (!passphrase.encrypted || (passphrase.cipher !== 'cucm')) { 3924 return _triggerError($this, regGlobals.errorCb, errorMap.InvalidArguments, 'authenticateAndConnect: invalid passphrase (type ' + typeof passphrase + ')', { 3925 registration: registration 3926 }); 3927 } 3928 // plugin is saving credentials, so, after first login attempt with wrong credentials, credentialsRequired is triggered with "wrongCredentials" error. 3929 // When next time lifecycle is started, the same message will be again attached to credentialsRequired event, which will cause _triggerError call... 3930 // So, instead of starting lifecycle every time registerPhone is called, only encrypt new credentials and submit. 3931 if (regGlobals.errorState === 'credentialsRequired') { 3932 _triggerCredentialsRequired($this, { 3933 errors: [], 3934 error: '', 3935 authenticatorId: regGlobals.lastAuthenticatorId}); 3936 } else { 3937 _sendClientRequest('startSignIn', { 3938 manualSettings: true 3939 }, $.noop, 3940 function errorCb(error) { 3941 _triggerError($this, regGlobals.errorCb, getError(error), error, { 3942 registration: registration 3943 }); 3944 } 3945 ); 3946 } 3947 } 3948 } 3949 } 3950 3951 // for registerPhone 3952 function logArgsWithMaskedPassphrase(args) { 3953 var argsForLog = $.extend({}, args); 3954 3955 if (argsForLog.passphrase) { 3956 argsForLog.passphrase = '*****'; 3957 } 3958 3959 if (argsForLog.password) { 3960 argsForLog.password = '*****'; 3961 } 3962 3963 _log(true, 'manualSignIn', argsForLog); 3964 } 3965 3966 // for registerPhone 3967 // M for Manual 3968 function setRegGlobalsM(args) { 3969 // flag to indicate cwic is in the process of registering a phone in manual mode 3970 regGlobals.registeringPhone = true; 3971 regGlobals.manual = true; 3972 3973 // reset global registration object 3974 registration = { 3975 user: args.user, 3976 mode: args.mode || 'SoftPhone', 3977 devices: {}, 3978 forceRegistration: args.forceRegistration || false 3979 }; 3980 3981 regGlobals.successCb = $.isFunction(args.success) ? args.success : null; 3982 regGlobals.errorCb = $.isFunction(args.error) ? args.error : null; 3983 regGlobals.CUCM = args.cucm; 3984 regGlobals.user = args.user; 3985 3986 _log(true, 'setRegGlobalsM: regGlobals set: ', regGlobals); 3987 _log(true, 'setRegGlobalsM: registration set: ', registration); 3988 } 3989 3990 // for registerPhone 3991 function parseAndCheckArgs(args, $this) { 3992 var passphraseValidator = validators.get('passphrase'), 3993 result = { 3994 devicesAvailableCb: null, 3995 tftp: [], 3996 ccmcip: [], 3997 cti: [], 3998 passphrase: '' 3999 }; 4000 4001 result.devicesAvailableCb = $.isFunction(args.devicesAvailable) ? args.devicesAvailable : null; 4002 4003 // check plugin state also! 4004 if (!_plugin) { 4005 _triggerError($this, regGlobals.errorCb, errorMap.PluginNotAvailable, 'Plug-in is not available or has not been initialized', { 4006 registration: registration 4007 }); 4008 throw new Error('Break manual login'); 4009 } 4010 4011 // parse CUCM argument into tftp, ccmcip and cti arrays (list of String addresses) 4012 // from the 11.0 release and on, tftp, ccmcip and cti are limited to 3 addresses only 4013 4014 // args.cucm can be a String, an Object or an Array of both 4015 $.each($.makeArray(args.cucm), function (i, elem) { 4016 if (typeof elem === 'string') { 4017 // cucm string can be 'lab call manager 1.2.3.4' 4018 var a = elem.split(' ').pop(); 4019 result.tftp.push(a); 4020 result.ccmcip.push(a); 4021 result.cti.push(a); 4022 } else if (typeof elem === 'object') { 4023 var tftpElem = []; // the tftp array of the current elem 4024 var hasOneProperty = false; // just to log a warning 4025 4026 if ($.isArray(elem.tftp)) { 4027 result.tftp = result.tftp.concat(elem.tftp); 4028 tftpElem = elem.tftp; 4029 hasOneProperty = true; 4030 } 4031 4032 if ($.isArray(elem.ccmcip)) { 4033 result.ccmcip = result.ccmcip.concat(elem.ccmcip); 4034 hasOneProperty = true; 4035 } else { 4036 // ccmcip defaults to tftp (backward compatibility) 4037 result.ccmcip = result.ccmcip.concat(tftpElem); 4038 } 4039 4040 if ($.isArray(elem.cti)) { 4041 result.cti = result.cti.concat(elem.cti); 4042 hasOneProperty = true; 4043 } else { 4044 // cti defaults to tftp (backward compatibility) 4045 result.cti = result.cti.concat(tftpElem); 4046 } 4047 4048 if (!hasOneProperty) { 4049 _log('registerPhone: no ccmcip/tftp/cti properties for cucm element'); 4050 _log(true, elem); 4051 } 4052 } else { 4053 _log('registerPhone: ignoring cucm argument of type ' + typeof elem); 4054 } 4055 }); 4056 4057 _log('registerPhone: ' + result.tftp.length + ' cucm TFTP address(es)'); 4058 _log(true, result.tftp); 4059 _log('registerPhone: ' + result.ccmcip.length + ' cucm CCMCIP address(es)'); 4060 _log(true, result.ccmcip); 4061 _log('registerPhone: ' + result.cti.length + ' cucm CTI address(es)'); 4062 _log(true, result.cti); 4063 4064 if (result.tftp.length > 3 || result.ccmcip.length > 3 || result.cti.length > 3) { 4065 _log('registerPhone: Server address(es) are limited to 3 values. Only first 3 values will be kept.'); 4066 result.tftp = result.tftp.splice(0, 3); 4067 result.ccmcip = result.ccmcip.splice(0, 3); 4068 result.cti = result.cti.splice(0, 3); 4069 } 4070 4071 _log('registerPhone of user=' + registration.user + ' in mode="' + registration.mode + '"'); 4072 4073 if (!registration.user) { 4074 _triggerError($this, regGlobals.errorCb, errorMap.InvalidArguments, 'Missing user name', { 4075 registration: registration 4076 }); 4077 throw new Error('Break manual login'); 4078 } 4079 4080 if (!$.isArray(result.tftp) || result.tftp.length < 1) { 4081 _triggerError($this, regGlobals.errorCb, errorMap.NoCallManagerConfigured, 'Missing CUCM address', { 4082 registration: registration 4083 }); 4084 throw new Error('Break manual login'); 4085 } 4086 4087 if (!registration.mode.match(/^(SoftPhone|DeskPhone)$/)) { 4088 _triggerError($this, regGlobals.errorCb, errorMap.InvalidArguments, 'Invalid phone mode "' + registration.mode + '"', { 4089 registration: registration 4090 }); 4091 throw new Error('Break manual login'); 4092 } 4093 4094 // validate password 4095 result.passphrase = args.passphrase || args.password; 4096 4097 if (typeof result.passphrase === 'string') { 4098 if (passphraseValidator.isNotValid(result.passphrase)) { 4099 _triggerError($this, regGlobals.errorCb, errorMap.InvalidArguments, 'invalid passphrase', { 4100 registration: registration 4101 }); 4102 throw new Error('Break manual login'); 4103 } 4104 } else if (typeof result.passphrase !== 'object' || (result.passphrase.cipher !== 'cucm')) { 4105 _triggerError($this, regGlobals.errorCb, errorMap.InvalidArguments, 'invalid passphrase (type ' + typeof result.passphrase + ')', { 4106 registration: registration 4107 }); 4108 throw new Error('Break manual login'); 4109 } else { 4110 if (passphraseValidator.isNotValid(result.passphrase.encrypted)) { 4111 _triggerError($this, regGlobals.errorCb, errorMap.InvalidArguments, 'invalid passphrase', { 4112 registration: registration 4113 }); 4114 throw new Error('Break manual login'); 4115 } 4116 } 4117 4118 return result; 4119 } 4120 4121 /** 4122 * Alias for {@link $.fn.cwic-registerPhone} 4123 */ 4124 function manualSignIn() { 4125 return; 4126 } 4127 4128 /** <br> 4129 * Unregisters a phone from CUCM:<ul> 4130 * <li>Ends any active call if this is the last instance or forceLogout is set to true.</li> 4131 * <li>In softphone mode, SIP unregisters, in deskphone mode, closes the CTI connection.</li> 4132 * <li>Calls the optional complete handler (always called).</li></ul> 4133 * @param args Is a set of key/value pairs to configure the phone unregistration. 4134 * @param {function} [args.complete] Callback called when unregistration is successfully completed.<br> 4135 * If specified, the handler is called only in the case where the phone was already registered. 4136 * @param {boolean} args.forceLogout: If true, end the phone session even if registered in other instances. 4137 * unregisterPhone examples 4138 * @example 4139 * // ************************************* 4140 * // unregister phone 4141 * jQuery('#phone') 4142 * .unbind('.cwic') // optionally unbind cwic events (it's done in shutdown API automatically) 4143 * .cwic('unregisterPhone', { 4144 * forceLogout: true, 4145 * complete: function() { 4146 * console.log('phone is unregistered'); 4147 * } 4148 * }); 4149 */ 4150 function unregisterPhone() { 4151 _log(true, 'unregisterPhone', arguments); 4152 4153 var $this = this; 4154 4155 // should we remove forceLogout parameter at all? What is the purpose of it? If it is false, nothing happens 4156 // leave it only for backward compatibility 4157 if (isObject(arguments[0]) && arguments[0].forceLogout === true) { 4158 _sendClientRequest('logout'); 4159 4160 // reset global registration object 4161 registration = { 4162 devices: {} 4163 }; 4164 } 4165 4166 if (isObject(arguments[0]) && typeof arguments[0].complete === 'function') { 4167 // call complete callback 4168 var complete = arguments[0].complete; 4169 regGlobals.unregisterCb = function () { 4170 _log(true, 'Calling unregisterCb...'); 4171 try { 4172 complete(); 4173 } catch (completeException) { 4174 _log('Exception occurred in application unregister complete callback', completeException); 4175 if (typeof console !== 'undefined' && console.trace) { 4176 console.trace(); 4177 } 4178 } 4179 4180 regGlobals.unregisterCb = null; 4181 }; 4182 } 4183 4184 _reset(); 4185 4186 return $this; 4187 } 4188 4189 /** <br> 4190 * Signs out a registered device from CUCM:<ul> 4191 * <li>Ends any active call.</li> 4192 * <li>In softphone mode, SIP unregisters, in deskphone mode, closes the CTI connection.</li> 4193 * <li>Calls the optional complete callback</li></ul> 4194 * If specified, the function is called only in the case where the phone was already registered. <br> 4195 * <b>Note:</b> This API is a preferred alternative to unregisterPhone API.<br> 4196 * <br> 4197 * signOut examples: <br> 4198 * @example 4199 * // ************************************* 4200 * // signOut device 4201 * jQuery('#phone') 4202 * .cwic('signOut', { 4203 * complete: function() { 4204 * console.log('device is signed out'); 4205 * } 4206 * }); 4207 * 4208 * @param [args] Is a set of key/value pairs to configure the phone signOut. 4209 * @param {function} [args.complete] Callback called when sign out is successfully completed.<br> 4210 */ 4211 function signOut(args) { 4212 var completeCb; 4213 _log(true, 'signOut', arguments); 4214 4215 if (isObject(arguments[0]) && typeof arguments[0].complete === 'function') { 4216 // call complete callback 4217 completeCb = arguments[0].complete; 4218 } 4219 4220 unregisterPhone({forceLogout:true, complete: completeCb}); 4221 } 4222 4223 /** 4224 * Should clear all resources occupied during the signIn/registerPhone process and during regular usage of registered device. 4225 * @private 4226 */ 4227 function _reset() { 4228 _log(true, '_reset: reseting regGlobals...'); 4229 // clear all cwic data - data attached with $('.cwic-data').data('cwic', somedata); 4230 $('.cwic-data').removeData('cwic'); 4231 resetGlobals(); 4232 } 4233 4234 function _triggerConversationEvent($this, conversation, topic) { 4235 var conversationId = conversation.callId; 4236 var conversationState = conversation.callState; 4237 4238 // prevent call transfer while another one is in progress 4239 if (transferCallGlobals.inProgress) { 4240 conversation.capabilities.canDirectTransfer = false; 4241 } 4242 4243 // determine first participant name and number (remote participant) 4244 // CSCug19119: we no longer use conversation.calledPartyName and conversation.calledPartyNumber since those are depreciated from ECC 4245 // instead, just grab the first entry from participants list, if available 4246 var participant = (conversation.participants && conversation.participants.length > 0) ? conversation.participants[0] : {}; 4247 var number = (participant.directoryNumber && participant.directoryNumber !== '') ? participant.directoryNumber : participant.number; 4248 participant = $.extend({}, participant, { recipient: number }); 4249 4250 // select the conversation container with class cwic-conversation-{conversationId} 4251 var container = $('.cwic-conversation-' + conversationId); 4252 4253 // if no container, select the outgoing conversation (see startConversation) 4254 if (container.length === 0) { 4255 container = $('.cwic-conversation-outgoing'); 4256 4257 // in deskphone mode, container may not exist yet if conversation was initiated from deskphone 4258 //if (container.length == 0 && conversation.callType == 'Outgoing') { 4259 //container = $('<div>').addClass('cwic-conversation-outgoing'); 4260 //} 4261 } 4262 4263 // at this point container may be empty, which means the conversation is incoming 4264 4265 var data = container.data('cwic') || {}; 4266 4267 _log(true, 'conversation id=' + conversationId + ' state=' + conversation.callState || data.state, conversation); 4268 4269 // extend conversation 4270 conversation = $.extend({}, data, conversation, { 4271 id: conversationId, 4272 state: conversationState || data.state, 4273 participant: $.extend(data.participant, participant) 4274 }); 4275 4276 /* ECC call states and old skittles/webphone states 4277 OnHook : Disconnected 4278 OffHook : Created 4279 Ringout : RemotePartyAlerting 4280 Ringin : Alerting 4281 Proceed : Ringin on Deskphone while on a call amongst others 4282 Connected : Connected 4283 Hold : Held 4284 RemHold : "Passive Held" 4285 Resume : ? 4286 Busy : n/a (connected) 4287 Reorder : Failed 4288 Conference : n/a 4289 Dialing : Dialing 4290 RemInUse : "Passive not held" ("RemInUse" should indicate 'Remote In-Use' state, i.e. the line is a shared-line, and another device is actively using the shared line.) 4291 HoldRevert : n/a 4292 Whisper : n/a 4293 Parked : n/a 4294 ParkRevert : n/a 4295 ParkRetrieved : n/a 4296 Preservation : n/a 4297 WaitingForDigits : na/ ? Overlapdial capability ? 4298 Spoof_Ringout : n/a 4299 */ 4300 // check for an incoming call - based on condition: 4301 // * Empty container and conversation state 'Ringin' 4302 if (conversation.state === 'Ringin' && container.length === 0) { 4303 // new container for incoming call, application is supposed to attach it to the DOM 4304 container = $('<div>').addClass('cwic-data cwic-conversation cwic-conversation-' + conversationId).data('cwic', conversation); 4305 $this.trigger('conversationIncoming.cwic', [conversation, container]); 4306 return; 4307 } else if ((conversation.state === 'OnHook' && !conversation.capabilities.canOriginateCall) || !conversation.exists) { 4308 // If we can originate a call, onHook does not mean the call has ended - it means it's just about to start 4309 4310 removeWindowFromCall({ 4311 callId: conversationId, 4312 remoteVideoWindow: activeConversation.videoObject, 4313 window: activeConversation.window, 4314 endCall: true 4315 }); 4316 4317 if (container.length === 0) { 4318 _log('warning: no container for ended conversation ' + conversationId); 4319 $this.trigger('conversationEnd.cwic', [conversation]); 4320 return; 4321 } 4322 4323 container.removeData('cwic') 4324 .removeClass('cwic-data cwic-conversation cwic-conversation-' + conversation.id) 4325 .trigger('conversationEnd.cwic', [conversation]); 4326 4327 return; 4328 } else { 4329 if (conversation.state === 'OffHook' || conversation.state === 'Connected') { 4330 4331 // store media connection time 4332 if (typeof conversation.connect === 'undefined' && conversation.state === 'Connected') { 4333 if (container.length === 0) { 4334 container = $('<div>').addClass('cwic-conversation cwic-conversation-' + conversationId); 4335 } 4336 $.extend(conversation, { connect: new Date() }); 4337 container.data('cwic', conversation); 4338 } 4339 4340 // store start time and trigger start event only once 4341 if (typeof conversation.start === 'undefined') { 4342 if (container.length === 0) { 4343 container = $('<div>'); 4344 } 4345 $.extend(conversation, { start: new Date() }); 4346 container.data('cwic', conversation); 4347 4348 container 4349 .removeClass('cwic-conversation-outgoing') 4350 .addClass('cwic-conversation cwic-conversation-' + conversationId) 4351 .data('cwic', conversation); 4352 4353 $this.trigger('conversationStart.cwic', [conversation, container]); 4354 return; 4355 } 4356 } 4357 4358 if (container.length === 0) { 4359 // if we've just switched to deskphone mode and there's already a call, create a container div 4360 // or if we've just opened a new tab, we also need to trigger a conversation start for an ongoing call 4361 container = $('<div>').data('cwic', conversation).addClass('cwic-conversation cwic-conversation-' + conversationId); 4362 _log('warning: no container for updated conversation ' + conversationId); 4363 if (conversation.exists) { 4364 $this.trigger('conversationStart.cwic', [conversation, container]); 4365 return; 4366 } else { 4367 $this.trigger('conversationUpdate.cwic', [conversation, container]); // trigger update event 4368 return; 4369 } 4370 4371 } else { 4372 container.data('cwic', conversation); 4373 } 4374 4375 container.trigger('conversationUpdate.cwic', [conversation, container]); // trigger update event 4376 } 4377 4378 } // function _triggerConversationEvent 4379 4380 /** 4381 * _triggerError(target, [callback], [code], [data]) <br> 4382 * <br> 4383 * - target (Object): a jQuery selection where to trigger the event error from <br> 4384 * - callback (Function): an optional callback to be called call with the error. if specifed, prevents the generic error event to be triggered <br> 4385 * - code (Number): an optional cwic error code (defaults to 0 - Unknown) <br> 4386 * - data (String, Object): some optional error data, if String, used as error message. if Object, used to extend the error. <br> 4387 * <br> 4388 * cwic builds an error object with the following properties: <br> 4389 * code: a pre-defined error code <br> 4390 * message: the error message (optional) <br> 4391 * any other data passed to _triggerError or set to errorMap (see the init function) <br> 4392 * <br> 4393 * When an error event is triggered, the event object is extended with the error properties. <br> 4394 * <br> 4395 */ 4396 function _triggerError() { 4397 var $this = arguments[0]; // target (first mandatory argument) 4398 var errorCb = null; 4399 4400 // the default error 4401 var error = $.extend({ details: [] }, errorMap.Unknown); 4402 4403 // extend error from arguments 4404 for (var i = 1; i < arguments.length; i++) { 4405 var arg = arguments[i]; 4406 4407 // is the argument a specific error callback ? 4408 if ($.isFunction(arg)) { errorCb = arg; } 4409 4410 else if (typeof arg === 'string') { error.details.push(arg); } 4411 4412 else if (typeof arg === 'object') { $.extend(error, arg); } 4413 4414 } // for 4415 4416 _log(error.message, error); 4417 4418 // if specific error callback, call it 4419 if (errorCb) { 4420 try { 4421 errorCb(error); 4422 } catch (errorException) { 4423 _log('Exception occurred in application error callback', errorException); 4424 if (typeof console !== 'undefined' && console.trace) { 4425 console.trace(); 4426 } 4427 } 4428 4429 } else { 4430 // if no specific error callback, raise generic error event 4431 var event = $.Event('error.cwic'); 4432 $.extend(event, error); 4433 $this.trigger(event); 4434 } 4435 4436 return $this; 4437 } 4438 4439 /** 4440 * @param {Object|call} conversation Can be a new object to start a new conversation or an existing {@link call} which you wish to answer. 4441 * @param {Number} conversation.id Unique identifier of the conversation. Required when referencing an exising call. 4442 * @param {participant} conversation.participant First remote participant of the call. 4443 * @param {String} conversation.participant.recipient The phone number of the participant. Required when placing a new outbound call. This will be the dialed number for the call. 4444 * @param {String} [conversation.participant.name] The participant name. 4445 * @param {String} [conversation.participant.photosrc] A suitable value for the src attribute of an <img> element. 4446 * @param {String} [conversation.state] Current state of the conversation. Can be OffHook, Ringing, Connected, OnHook, Reorder. 4447 * @param {Date} [conversation.start] Start time. Defined on resolution update only. 4448 * @param {Date} [conversation.connect] Media connection time. Defined on resolution update only. 4449 * @param {Object} [conversation.videoResolution] Resolution of the video conversation, contains width and height properties. Defined on resolution update only. 4450 * @param {String|Object} [conversation.container] The HTML element which contains the conversation. Conversation events are triggered on this element. 4451 * If String, specifies a jQuery selector If Object, specifies a jQuery wrapper of matched elements(s). 4452 * By default container is $(this), that is the first element of the matched set startConversation is called on. 4453 * @param {String} [conversation.subject] The subject of the conversation to start. 4454 * @param {Function} [conversation.error(err)] A function to be called if the conversation cannot be started. {@link $.fn.cwic-errorMapEntry} is passed as parameter. 4455 * @param {String} [conversation.videoDirection] The video media direction: 'Inactive' or undefined (audio only by default), 'SendOnly', 'RecvOnly' or 'SendRecv'. 4456 * @param {Object} [conversation.remoteVideoWindow] The video object (must be of mime type application/x-cisco-cwc-videocall). 4457 * @param {DOMWindow} [conversation.window] DOM window that contains the remoteVideoWindow (default to this DOM window) required if specifying a video object on another window (popup/iframe). 4458 * @description Start a conversation with a participant. 4459 * <br>If conversation contain both an ID and a state property, cwic assumes you want to answer that incoming conversation, in this case starting the passed conversation means accepting(answering) it. 4460 * @example 4461 * // start an audio conversation with element #foo as container 4462 * jQuery('#phone').cwic('startConversation', { 4463 * participant: { 4464 * recipient: '1234' 4465 * }, 4466 * container: '#foo' 4467 * }); 4468 * // start an audio conversation with a contact (call work phone number) 4469 * jQuery('#conversation').cwic('startConversation', { 4470 * participant: { 4471 * recipient: '1234', 4472 * displayName: 'Foo Bar', 4473 * screenName: ' fbar', 4474 * phoneNumbers: { 4475 * work: '1234', 4476 * mobile: '5678' 4477 * } 4478 * } 4479 * }); 4480 * // answer an incoming conversation (input has an id property) 4481 * // see another example about the conversationIncoming event 4482 * jQuery('#conversation').cwic('startConversation', { 4483 * participant: { 4484 * recipient: '1234' 4485 * }, 4486 * id: '612', 4487 * state: 'Ringin' 4488 * }); 4489 * // answer an incoming conversation with video 4490 * jQuery('#conversation').cwic('startConversation', 4491 * jQuery.extend(conversation,{ 4492 * videoDirection: (sendingVideo ? 'SendRecv':''), 4493 * remoteVideoWindow: 'remoteVideoWindow', // pass id 4494 * id: callId 4495 * })); 4496 * // answer an incoming conversation with video object hosted in popoutwindow 4497 * jQuery('#conversation').cwic('startConversation', 4498 * jQuery.extend(conversation,{ 4499 * videoDirection: (sendingVideo ? 'SendRecv':''), 4500 * remoteVideoWindow: $('#remoteVideoWindow', popoutwindow.document)[0] // pass object setting jQuery context to popoutwindow document 4501 * window: popoutwindow, 4502 * id: callId 4503 * })); 4504 * // answer an incoming conversation without video 4505 * jQuery('#callcontainer').cwic('startConversation', conversation); 4506 */ 4507 function startConversation() { 4508 _log(true, 'startConversation', arguments); 4509 4510 var $this = this; 4511 4512 var callsettings = arguments[0] || $this.data('cwic') || {}; 4513 var windowhandle, videoDirection; 4514 4515 if ($this.length === 0) { 4516 return _triggerError($this, callsettings.error, errorMap.InvalidArguments, 'cannot start conversation with empty selection'); 4517 } 4518 4519 // container is the jQuery wrapper of the video container 4520 var container = $this; 4521 4522 if (typeof callsettings.container === 'string') { 4523 container = $(callsettings.container); 4524 } else if (typeof callsettings.container === 'object') { 4525 container = callsettings.container; 4526 } 4527 4528 container = container.first(); 4529 4530 if (typeof callsettings.id !== 'undefined') { 4531 // start an incoming conversation 4532 container.addClass('cwic-data cwic-conversation cwic-conversation-' + callsettings.id).data('cwic', callsettings); 4533 4534 if (arguments.length >= 1) { 4535 videoDirection = callsettings.videoDirection; 4536 if (callsettings.remoteVideoWindow) { 4537 addWindowToCall({ 4538 callId: callsettings.id, 4539 remoteVideoWindow: callsettings.remoteVideoWindow 4540 }); 4541 4542 if (callsettings.remoteVideoWindow.windowhandle) { 4543 windowhandle = callsettings.remoteVideoWindow.windowhandle; 4544 } 4545 } 4546 } else { 4547 videoDirection = ''; 4548 } 4549 4550 var answerObject = { 4551 callId: callsettings.id, 4552 videoDirection: videoDirection 4553 }; 4554 4555 if (windowhandle) { 4556 answerObject.windowhandle = windowhandle; 4557 } 4558 4559 _sendClientRequest('answer', answerObject); 4560 4561 } else { 4562 // start an outgoing conversation 4563 var participant = callsettings.participant || {}; 4564 4565 if (typeof participant === 'string') { 4566 participant = { recipient: participant }; 4567 } 4568 4569 if (typeof participant.recipient === 'undefined') { 4570 return _triggerError($this, callsettings.error, errorMap.InvalidArguments, 'cannot start conversation: undefined or empty recipient'); 4571 } 4572 4573 container.addClass('cwic-data cwic-conversation cwic-conversation-outgoing').data('cwic', { participant: participant }); 4574 4575 if (container.is(':hidden')) { 4576 _log(true, 'startConversation - warning: container is hidden'); 4577 } 4578 4579 var originateObject = { 4580 recipient: participant.recipient, 4581 videoDirection: callsettings.videoDirection 4582 }; 4583 4584 if (callsettings.remoteVideoWindow && callsettings.remoteVideoWindow.windowhandle) { 4585 originateObject.windowhandle = callsettings.remoteVideoWindow.windowhandle; 4586 } 4587 4588 _sendClientRequest('originate', originateObject, 4589 function originateCb(res) { 4590 if (res.callId && res.callId >= 1) { 4591 if (callsettings.remoteVideoWindow) { 4592 callsettings.window = callsettings.window || window; 4593 4594 addWindowToCall({ 4595 callId: res.callId, 4596 remoteVideoWindow: callsettings.remoteVideoWindow, 4597 window: callsettings.window 4598 }); 4599 4600 } 4601 } 4602 }, 4603 function errorCb(error) { 4604 if (error) { 4605 _log(true, 'originate error', error); 4606 _triggerError($this, getError(error), error, 'cannot start conversation'); 4607 } 4608 } 4609 ); 4610 } 4611 4612 return $this; 4613 } 4614 4615 /** 4616 * @description Ends a conversation. Triggers a conversationEnd event. 4617 * @param {boolean} iDivert If true, redirects the call to voice mail. See UCM documentation on the Immediate Divert (iDivert) feature for details. The call can be iDiverted only if {@link call#capabilities} contains 'canImmediateDivert', 4618 * @param {String|Object} id A conversation identifier (String) or an Object containing an id property. 4619 * @example 4620 * // typeof input is string 4621 * jQuery('#phone').cwic('endConversation', '1234'); 4622 * // or 4623 * jQuery('#phone').cwic('endConversation', conversation.id); 4624 * // typeof input is object 4625 * jQuery('#phone').cwic('endConversation', conversation); 4626 * // let cwic find the conversation data attached to #conversation 4627 * jQuery('#conversation').cwic('endConversation'); 4628 * // iDivert the conversation 4629 * jQuery('#myconversation').cwic('endConversation', true); 4630 * // iDivert and specify conversation id as a string 4631 * jQuery('#phone').cwic('endConversation', true, '1234'); 4632 * 4633 * 4634 */ 4635 function endConversation() { 4636 _log(true, 'endConversation', arguments); 4637 4638 var $this = this; 4639 4640 if ($this.length === 0) { 4641 return $this; 4642 } 4643 4644 var iDivert = null; 4645 var conversation = null; 4646 var conversationId = null; 4647 4648 if (arguments.length === 0) { 4649 conversation = $this.data('cwic'); 4650 4651 if (!conversation) { 4652 return _triggerError($this, 'cannot end conversation: no conversation exists for this element'); 4653 } 4654 4655 conversationId = conversation.id; 4656 } else if (arguments.length === 1) { 4657 iDivert = typeof arguments[0] === 'boolean' ? arguments[0] : null; 4658 conversation = typeof arguments[0] === 'object' ? arguments[0] : $this.data('cwic'); 4659 conversationId = typeof arguments[0] === 'string' ? arguments[0] : conversation.id; 4660 } else if (arguments.length === 2) { 4661 iDivert = typeof arguments[0] === 'boolean' ? arguments[0] : null; 4662 conversation = typeof arguments[1] === 'object' ? arguments[1] : $this.data('cwic'); 4663 conversationId = typeof arguments[1] === 'string' ? arguments[1] : conversation.id; 4664 } 4665 4666 if (!conversationId) { 4667 return _triggerError($this, errorMap.InvalidArguments, 'cannot end conversation: undefined or empty conversation id'); 4668 } 4669 4670 if (transferCallGlobals.callId === conversationId) { 4671 transferCallGlobals.endTransfer(); 4672 } 4673 4674 if (iDivert) { 4675 // need to check capabilities first 4676 conversation = conversation || $('.cwic-conversation-' + conversationId).data('cwic'); 4677 4678 if (!conversation) { 4679 return _triggerError($this, 'cannot iDivert - undefined conversation'); 4680 } 4681 4682 if (!conversation.capabilities || !conversation.capabilities.canImmediateDivert) { 4683 return _triggerError($this, errorMap.MissingCapability, 'cannot iDivert - missing capability', { conversation: conversation }); 4684 } 4685 4686 _log(true, 'iDivert conversation', conversation); 4687 4688 _sendClientRequest('iDivert', { 4689 callId: conversationId 4690 }); 4691 } else { 4692 _log(true, 'end conversation', conversation); 4693 _sendClientRequest('endCall', { 4694 callId: conversationId 4695 }); 4696 } 4697 4698 return $this; 4699 } 4700 /** 4701 * @description Updates an existing conversation.<br> 4702 * This function controls the call allowing the following operations<ul> 4703 * <li>hold call</li> 4704 * <li>resume call</li> 4705 * <li>mute call</li> 4706 * <li>unmute call</li> 4707 * <li>mute audio only</li> 4708 * <li>mute video only</li> 4709 * <li>unmute audio only</li> 4710 * <li>unmute video only</li> 4711 * <li>add video window for remote sender</li> 4712 * <li>remove video window for remote sender</li> 4713 * <li>update video preference on a call video escalate/de-escalate</li> 4714 * <li>conference two calls together</li> 4715 * <li>transfer a call</li> 4716 * </ul> 4717 * Transfer call flow: 4718 * <ol> 4719 * <li>establish a conversation between clients A and B.</li> 4720 * <li>call "transferCall" API to transfer a conversation to the number of client C. A conversation between clients A and C is established. 4721 * <li>Now, client A have an option to complete an ongoing call transfer. If the call transfer is completed, 4722 * conversation between clients B and C is established and client A is put out from both conversations. To cancel call transfer, endConversation should be called.</li> 4723 * </ol> 4724 * There are two ways to implement the final step of call transfer flow. The first one is to pass a "complete" button, 4725 * either its id or jQuery object, to the "transferCall" API and library will automatically attach/detach handler and enable/disable the button when it's appropriate. 4726 * If more specific behavior is desired comparing to this higher level API, then 'callTransferInProgress.cwic' event could be implemented, see {@link $.fn.cwic#event:callTransferInProgress} 4727 * @param {String|Object} update Update a started conversation. update can be: <br> 4728 * A String: hold, resume, mute, unmute, muteAudio, muteVideo, unmuteAudio, unmuteVideo.<br> 4729 * An Object: contains one or more writable conversation properties to update e.g. videoDirection.<br> 4730 * Triggers a conversationUpdate event. 4731 * @param {String|Object} id A conversation identifier (String) or Object containing an id property <br> 4732 * @example 4733 * // typeof input is string HOLD/RESUME 4734 * jQuery('#phone').cwic('updateConversation', 'hold', '1234') 4735 * jQuery('body').cwic('updateConversation', 'hold', conversation.id); 4736 * jQuery('#myid').cwic('updateConversation', 'hold', conversation); 4737 * // typeof input is object 4738 * jQuery('#conversation').cwic('updateConversation', 'hold'); 4739 * // resume the same conversation, 4740 * // let cwic find the conversation data attached to #conversation 4741 * jQuery('#conversation').cwic('updateConversation', 'resume'); 4742 * // MUTE/UNMUTE 4743 * // typeof input is string 4744 * jQuery('#phone').cwic('updateConversation', 'mute', '1234'); 4745 * jQuery('body').cwic('updateConversation', 'mute', conversation.id); 4746 * jQuery('#myid').cwic('updateConversation', 'mute', conversation); 4747 * // typeof input is object <br> 4748 * jQuery('#conversation').cwic('updateConversation', 'mute'); 4749 * // unmute the same conversation, 4750 * // let cwic find the conversation data attached to #conversation 4751 * jQuery('#conversation').cwic('updateConversation', 'unmute'); 4752 * 4753 * // add/remove video object in this (default) DOMWindow 4754 * jQuery('#conversation').cwic('updateConversation', 4755 * { 'addRemoteVideoWindow':videoObject }); 4756 * jQuery('#conversation').cwic('updateConversation', 4757 * { 'removeRemoteVideoWindow':videoObject }); 4758 * // add/remove video object from another DOMWindow 4759 * jQuery('#conversation').cwic('updateConversation', 4760 * { 'addRemoteVideoWindow':videoObject, window:popupWindow }); 4761 * jQuery('#conversation').cwic('updateConversation', 4762 * { 'removeRemoteVideoWindow':videoObject, window:popupWindow }); 4763 * 4764 * // Escalate to video 4765 * jQuery('#conversation').cwic('updateConversation', {'videoDirection': 'SendRecv'}); // implied source call is call associated with conversation div 4766 * jQuery('#phone').cwic('updateConversation', {'videoDirection': 'SendRecv'}, conversation.id}); // source call id passed 4767 * jQuery('#phone').cwic('updateConversation', {'videoDirection': 'SendRecv'}, conversation}); // source call passed 4768 * // De-escalate from video 4769 * jQuery('#conversation').cwic('updateConversation', {'videoDirection': 'Inactive'}); // implied source call is call associated with conversation div 4770 * jQuery('#phone').cwic('updateConversation', {'videoDirection': 'Inactive'}, conversation.id}); // source call id passed 4771 * jQuery('#phone').cwic('updateConversation', {'videoDirection': 'Inactive'}, conversation}); // source call passed 4772 * 4773 * // Transfer call to target number 4774 * jQuery('#conversation').cwic('updateConversation', {'transferCall': number, completeButton: 'completebtn'}); // implied source call is call associated with conversation div 4775 * jQuery('#phone').cwic('updateConversation', {'transferCall': number}, conversation.id}); // source call id passed. Bind to {@link $.fn.cwic#event:callTransferInProgress} to handle transfer complete 4776 * jQuery('#phone').cwic('updateConversation', {'transferCall': number}, conversation}); // source call passed 4777 * 4778 * // Join target callId to source call 4779 * jQuery('#conversation').cwic('updateConversation', {'joinCall':callId}); // implied source call is call associated with conversation div 4780 * jQuery('#phone').cwic('updateConversation', {'joinCall':callId}, conversation.id}); // source call id passed 4781 * jQuery('#phone').cwic('updateConversation', {'joinCall':callId}, conversation}); // source call passed 4782 */ 4783 function updateConversation() { 4784 _log(true, 'updateConversation', arguments); 4785 4786 var $this = this; 4787 4788 if ($this.length === 0) { 4789 return $this; 4790 } 4791 4792 // mandatory first argument 4793 var update = arguments[0]; 4794 4795 // find conversation information 4796 var conversation = null; 4797 var conversationId = null; 4798 4799 if (typeof arguments[1] === 'object') { 4800 conversation = arguments[1]; 4801 conversationId = conversation.id; 4802 } else if (typeof arguments[1] === 'undefined') { 4803 conversation = $this.data('cwic'); // attached conversation object 4804 if (typeof conversation === 'object') { conversationId = conversation.id; } 4805 } else { 4806 conversationId = arguments[1]; 4807 conversation = $('.cwic-conversation-' + conversationId).data('cwic') || $this.data('cwic'); 4808 } 4809 4810 if (!conversationId || !conversation) { 4811 return _triggerError($this, errorMap.InvalidArguments, 'cannot update conversation: undefined or empty conversation id'); 4812 } 4813 4814 if (typeof update === 'string') { 4815 var request = null, content = null; 4816 4817 if (update.match(/^hold$/i)) { 4818 request = 'hold'; 4819 content = { callId: conversationId }; 4820 } else if (update.match(/^resume$/i)) { 4821 request = 'resume'; 4822 content = { callId: conversationId }; 4823 } else if (update.match(/^mute$/i)) { 4824 request = 'mute'; 4825 content = { callId: conversationId }; 4826 } else if (update.match(/^unmute$/i)) { 4827 request = 'unmute'; 4828 content = { callId: conversationId }; 4829 } else if (update.match(/^muteAudio$/i)) { 4830 request = 'mute'; 4831 content = { callId: conversationId, muteAudio: true }; 4832 } else if (update.match(/^muteVideo$/i)) { 4833 request = 'mute'; 4834 content = { callId: conversationId, muteVideo: true }; 4835 } else if (update.match(/^unmuteAudio$/i)) { 4836 request = 'unmute'; 4837 content = { callId: conversationId, unmuteAudio: true }; 4838 } else if (update.match(/^unmuteVideo$/i)) { 4839 request = 'unmute'; 4840 content = { callId: conversationId, unmuteVideo: true }; 4841 } else { 4842 return _triggerError($this, errorMap.InvalidArguments, 'wrong arguments (update conversation) - ' + update, arguments); 4843 } 4844 4845 _sendClientRequest(request, content, 4846 $.noop, 4847 function errorCb(error) { 4848 _triggerError($this, getError(error), error); 4849 } 4850 ); 4851 4852 } else if (typeof update === 'object') { 4853 var foundWritable = false; 4854 4855 if (update.transferCall) { 4856 if (transferCallGlobals.inProgress) { 4857 _log(true, 'Call transfer already in progress, canceling...'); 4858 return $this; 4859 } 4860 4861 foundWritable = true; 4862 4863 transferCallGlobals.completeBtn = update.completeButton; 4864 transferCallGlobals.inProgress = true; 4865 4866 _sendClientRequest('transferCall', 4867 { 4868 callId: conversationId, 4869 transferToNumber: update.transferCall 4870 }, 4871 $.noop, 4872 function errorCb(error) { 4873 _triggerError($this, getError(error, 'NativePluginError'), 'transferCall', error); 4874 } 4875 ); 4876 } 4877 4878 if (update.joinCall) { 4879 foundWritable = true; 4880 4881 _sendClientRequest('joinCalls', 4882 { 4883 joinCallId: conversationId, 4884 callId: update.joinCall 4885 }, 4886 $.noop, 4887 function errorCb(error) { 4888 _triggerError($this, getError(error, 'NativePluginError'), 'joinCall', error); 4889 } 4890 ); 4891 } 4892 4893 if (update.videoDirection) { 4894 foundWritable = true; 4895 _sendClientRequest('setVideoDirection', 4896 { 4897 callId: conversationId, 4898 videoDirection: update.videoDirection 4899 }, 4900 $.noop, 4901 function errorCb(error) { 4902 _triggerError($this, getError(error, 'NativePluginError'), 'videoDirection', error); 4903 } 4904 ); 4905 } 4906 4907 if (update.addRemoteVideoWindow) { 4908 foundWritable = true; 4909 4910 _log('updateConversation() calling addWindowToCall() for conversationId: ' + conversationId); 4911 4912 addWindowToCall({ 4913 callId: conversationId, 4914 remoteVideoWindow: update.addRemoteVideoWindow, 4915 window: update.window 4916 }); 4917 } 4918 4919 if (update.removeRemoteVideoWindow) { 4920 foundWritable = true; 4921 _log('updateConversation() calling removeWindowFromCall() for conversationId: ' + conversationId); 4922 4923 removeWindowFromCall({ 4924 callId: conversationId, 4925 remoteVideoWindow: update.removeRemoteVideoWindow, 4926 window: update.window, 4927 endCall: false 4928 }); 4929 } 4930 4931 if (!foundWritable) { 4932 _triggerError($this, errorMap.InvalidArguments, 'wrong arguments (update conversation)', arguments); 4933 } 4934 4935 } else { 4936 _triggerError($this, errorMap.InvalidArguments, 'wrong arguments (update conversation)', arguments); 4937 } 4938 4939 return $this; 4940 } 4941 4942 /** 4943 * Sends digit (String) as Dual-Tone Multi-Frequency (DTMF) 4944 * @example 4945 * // SEND DTMF EXAMPLE 4946 * jQuery('#phone').cwic('sendDTMF', '5', '1234'); 4947 * jQuery('#mydiv').cwic('sendDTMF', '3', conversation.id); 4948 * jQuery('body').cwic('sendDTMF', '7', conversation); 4949 * jQuery('#conversation').cwic('sendDTMF', '1'); 4950 * @param {String} digit Dual-Tone Multi-Frequency (DTMF) digit to send. Does not trigger any event. 4951 * @param {String|Object} [id] a {String} conversation identifier or an {Object} containing an id property 4952 */ 4953 function sendDTMF() { 4954 _log(true, 'sendDTMF'); // don't send dtmf digits to logger 4955 4956 var $this = this; 4957 var digit = null; 4958 var conversation = $this.data('cwic'); 4959 var conversationId = conversation ? conversation.id : null; 4960 var allowedDigits = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '#', '*', 'A', 'B', 'C', 'D', 'a', 'b', 'c', 'd']; 4961 4962 // inspect arguments 4963 if (arguments.length > 0) { 4964 digit = typeof arguments[0] === 'string' ? arguments[0] : null; 4965 4966 if (arguments.length > 1) { 4967 if (typeof arguments[1] === 'object') { 4968 conversation = arguments[1]; 4969 conversationId = conversation.id; 4970 } 4971 else if (typeof arguments[1] === 'string') { 4972 conversationId = arguments[1]; 4973 } 4974 } 4975 } 4976 4977 if (typeof digit !== 'string' || !conversationId) { 4978 return _triggerError($this, errorMap.InvalidArguments, 'wrong arguments (sendDTMF)', arguments); 4979 } 4980 4981 if (allowedDigits.indexOf(digit) === -1) { 4982 return _triggerError($this, errorMap.InvalidArguments, 'invalid DTMF digit (sendDTMF)', arguments); 4983 } 4984 4985 _sendClientRequest('sendDTMF', { 4986 callId: conversationId, 4987 digit: digit 4988 }); 4989 4990 return $this; 4991 } 4992 4993 function getInstanceId() { 4994 _log(true, 'getInstanceId'); 4995 return _plugin.instanceId; 4996 } 4997 4998 /** 4999 * Gets a list of objects describing the multimedia devices installed on a system. 5000 * @since 3.0.0 5001 * @returns 5002 * a list of objects describing the multimedia devices with the following properties:<ul> 5003 * <li>deviceID: unique device ID</li> 5004 * <li>deviceName: {string} human readable device name</li> 5005 * <li>vendorID: {string} unique vendor ID</li> 5006 * <li>productID: {string} vendor product ID</li> 5007 * <li>hardwareID: {string} hardware dependent ID</li> 5008 * <li>canRecord: {boolean} indicates whether this object can be used as an audio recording device</li> 5009 * <li>canPlayout: {boolean} indicates whether this object can be used as an audio playout device</li> 5010 * <li>canCapture: {boolean} indicates whether this object can be used as a video capture device</li> 5011 * <li>canRing: {boolean} indicates whether this object can be used as a ringer device</li> 5012 * <li>isDefault: {boolean} indicates whether this object represents the default device of the type indicated by the canRecord, canPlayout, and canCapture flags</li> 5013 * <li>recordingName: {string} human readable name for the audio recording function of this device</li> 5014 * <li>playoutName: {string} human readable name for the audio playout function of this device</li> 5015 * <li>captureName: {string} human readable name for the video capture function of this device</li> 5016 * <li>recordingID: {string} ID for the audio recording function of this device</li> 5017 * <li>playoutID: {string} ID for the audio playout function of this device</li> 5018 * <li>captureID: {string} ID for the video capture function of this device</li> 5019 * <li>clientRecordingID: {string} the ID to pass to setRecordingDevice to select this device as the audio recording device</li> 5020 * <li>clientPlayoutID: {string} the ID to pass to setPlayoutDevice to select this device as the audio playout device</li> 5021 * <li>clientCaptureID: {string} the ID to pass to setCaptureDevice to select this device as the video capture device</li> 5022 * <li>isSelectedRecordingDevice: {boolean} indicates whether this is the currently selected audio recording device</li> 5023 * <li>isSelectedPlayoutDevice: {boolean} indicates whether this is the currently selected audio playout device</li> 5024 * <li>isSelectedCaptureDevice: {boolean} indicates whether this is the currently selected video capture device</li> 5025 * </ul> 5026 * In order to use the list, the client should check the canXXXX fields to determine if a device can be passed as a particular function, then pass the clientXXXID 5027 * to the correct setXXXXDevice function. 5028 * 5029 * Depending on the platform, devices with multiple functions may show up as a single entry with multiple IDs, or multiple times with similar or different IDs. 5030 * 5031 * @example 5032 * see sample.html 5033 */ 5034 function getMultimediaDevices() { 5035 // new messaging interface passes mmDevices list in the change event 5036 // so we just return the data from the most recent event 5037 var devices = { 'multimediadevices': _plugin.multimediadevices }; 5038 5039 _log(true, 'getMultimediaDevices returning:', devices); 5040 return devices; 5041 5042 } 5043 5044 function getAvailableRingtones($this) { 5045 if (isMac()) { 5046 return; 5047 } 5048 5049 _sendClientRequest('getAvailableRingtones', function (ringtones) { 5050 handleRingtonesAvailable($this, ringtones); 5051 }); 5052 } 5053 5054 function handleRingtonesAvailable($this, ringtonesList) { 5055 var event = $.Event('ringtonesListAvailable.cwic'); 5056 event.ringtones = ringtonesList.ringtones; 5057 $this.trigger(event); 5058 } 5059 5060 /** 5061 * Sets the audio recording device used by the Cisco Web Communicator. To set a device, pass the clientRecordingID from a device with the canRecord flag set to true. 5062 * @since 3.0.0 5063 * @param {String} clientRecordingID: clientRecordingID retrieved from getMultimediaDevices() 5064 */ 5065 function setRecordingDevice() { 5066 _log(true, 'setRecordingDevice', arguments); 5067 5068 var clientRecordingIDIn = arguments[0]; 5069 5070 if (typeof clientRecordingIDIn !== 'string' || clientRecordingIDIn.length === 0) { 5071 return _triggerError(this, errorMap.InvalidArguments, 'wrong arguments (setRecordingDevice)', arguments); 5072 } 5073 5074 _sendClientRequest('setRecordingDevice', { 5075 'clientRecordingID': clientRecordingIDIn 5076 }); 5077 5078 // after setting device, we need to refresh our cache 5079 _sendClientRequest('getMultimediaDevices', function mmDevicesCb(content) { 5080 _triggerMMDeviceEvent($this, content); 5081 }); 5082 } 5083 /** 5084 * Sets the ringer device used by the Cisco Web Communicator. To set a device, pass the clientRingerID from a device with the canRing flag set to true. 5085 * @since 4.0.0 5086 * @param {String} clientRingerID: clientRingerID retrieved from getMultimediaDevices() 5087 */ 5088 function setRingerDevice() { 5089 _log(true, 'setRingerDevice', arguments); 5090 5091 var clientRingerIDIn = arguments[0]; 5092 5093 if (typeof clientRingerIDIn !== 'string' || clientRingerIDIn.length === 0) { 5094 return _triggerError(this, errorMap.InvalidArguments, 'wrong arguments (setRingerDevice)', arguments); 5095 } 5096 5097 _sendClientRequest('setRingerDevice', { 5098 'clientRingerID': clientRingerIDIn 5099 }); 5100 5101 // after setting device, we need to refresh our cache 5102 _sendClientRequest('getMultimediaDevices', function mmDevicesCb(content) { 5103 _triggerMMDeviceEvent($this, content); 5104 }); 5105 } 5106 5107 /** 5108 * Sets speaker volume. Works on Windows platform only. 5109 * @since 4.0.0 5110 * @param {String|Number} args.volume Volume to set (0 to 100) 5111 * @param {Function} args.success Success callback 5112 * @param {Function} args.error Error callback 5113 */ 5114 function setSpeakerVolume(content) { 5115 if (isMac()) { 5116 _log(false, 'setPlayRingerOnAllDevices works only on Windows platform for now...'); 5117 return; 5118 } 5119 5120 var volume = parseInt(content.speakerVolume, 10), 5121 success = $.isFunction(content.success) ? content.success : $.noop, 5122 error = $.isFunction(content.error) ? content.error : $.noop; 5123 5124 _sendClientRequest('setCurrentSpeakerVolume', { 5125 volume: volume 5126 }, success, error); 5127 } 5128 5129 /** 5130 * Sets ringer volume. Works on Windows platform only. 5131 * @since 4.0.0 5132 * @param {String|Number} args.volume Volume to set (0 to 100) 5133 * @param {Function} args.success Success callback 5134 * @param {Function} args.error Error callback 5135 */ 5136 function setRingerVolume(content) { 5137 if (isMac()) { 5138 _log(false, 'setRingerVolume works only on Windows platform for now...'); 5139 return; 5140 } 5141 5142 var volume = parseInt(content.ringerVolume, 10), 5143 success = $.isFunction(content.success) ? content.success : $.noop, 5144 error = $.isFunction(content.error) ? content.error : $.noop; 5145 _sendClientRequest('setCurrentRingerVolume', { 5146 volume: volume 5147 }, success, error); 5148 } 5149 5150 /** 5151 * Sets microphone volume. Works on Windows platform only. 5152 * @since 4.0.0 5153 * @param {String|Number} args.volume Volume to set (0 to 100) 5154 * @param {Function} args.success Success callback 5155 * @param {Function} args.error Error callback 5156 */ 5157 function setMicrophoneVolume(content) { 5158 if (isMac()) { 5159 _log(false, 'setMicrophoneVolume works only on Windows platform for now...'); 5160 return; 5161 } 5162 var volume = parseInt(content.microphoneVolume, 10), 5163 success = $.isFunction(content.success) ? content.success : $.noop, 5164 error = $.isFunction(content.error) ? content.error : $.noop; 5165 5166 _sendClientRequest('setCurrentMicrophoneVolume', { 5167 volume: volume 5168 }, success, error); 5169 } 5170 5171 /** 5172 * Sets ringtone. Will trigger ringtoneChange.cwic event. Works on Windows platform only. 5173 * @since 4.0.0 5174 * @param {String} ringtone ringtone name 5175 */ 5176 function setRingtone(ringtone) { 5177 if (isMac()) { 5178 _log(false, 'setRingtone works only on Windows platform for now...'); 5179 return; 5180 } 5181 _sendClientRequest('setCurrentRingtone', { 5182 ringtone: ringtone 5183 }); 5184 } 5185 5186 /** 5187 * Sets all capable devices as ringers. Works on Windows platform only. 5188 * @since 4.0.0 5189 */ 5190 function setPlayRingerOnAllDevices() { 5191 if (isMac()) { 5192 _log(false, 'setPlayRingerOnAllDevices works only on Windows platform for now...'); 5193 return; 5194 } 5195 _sendClientRequest('setPlayRingerOnAllDevices'); 5196 } 5197 5198 5199 /** 5200 * Gets the current volume value for particular device. Async function. 5201 * Works on Windows platform only. 5202 * @since 4.0.0 5203 * @param {String} device Type of device. One of: "Speaker", "Microphone", "Ringer" 5204 * @param {Function} callback(volume) Callback to be called with volume value 5205 */ 5206 function getMultimediaDeviceVolume(device, handleVolumeChangeCallback) { 5207 if (isMac()) { 5208 _log(false, 'getMultimediaDeviceVolume works only on Windows platform for now...'); 5209 return; 5210 } 5211 // todo: implement validation of device parameter 5212 if (isMultimediaStarted) { 5213 _sendClientRequest('getMultimediaDeviceVolume', { 5214 device: device 5215 }, handleVolumeChangeCallback); 5216 } else { 5217 _log(true, 'getMultimediaDeviceVolume: Mutimedia services not started, returning...'); 5218 } 5219 } 5220 5221 5222 /** 5223 * Sets the audio playout device used by the Cisco Web Communicator. To set a device, pass the clientPlayoutID from a device with the canPlayout flag set to true. 5224 * @since 3.0.0 5225 * @param {String} clientPlayoutID: clientPlayoutID retrieved from getMultimediaDevices() 5226 */ 5227 function setPlayoutDevice() { 5228 _log(true, 'setPlayoutDevice', arguments); 5229 5230 var clientPlayoutIDIn = arguments[0]; 5231 5232 if (typeof clientPlayoutIDIn !== 'string' || clientPlayoutIDIn.length === 0) { 5233 return _triggerError(this, errorMap.InvalidArguments, 'wrong arguments (setPlayoutDevice)', arguments); 5234 } 5235 5236 _sendClientRequest('setPlayoutDevice', { 5237 'clientPlayoutID': clientPlayoutIDIn 5238 }); 5239 5240 // after setting device, we need to refresh our cache 5241 _sendClientRequest('getMultimediaDevices', function mmDevicesCb(content) { 5242 _triggerMMDeviceEvent($this, content); 5243 }); 5244 } 5245 5246 /** 5247 * Sets the video capture device used by the Cisco Web Communicator. To set a device, pass the clientCaptureID from a device with the canCapture flag set to true. 5248 * @since 3.0.0 5249 * @param {String} clientCaptureID: clientCaptureID retrieved from getMultimediaDevices() 5250 */ 5251 function setCaptureDevice() { 5252 _log(true, 'setCaptureDevice', arguments); 5253 5254 var clientCaptureIDIn = arguments[0]; 5255 5256 if (typeof clientCaptureIDIn !== 'string' || clientCaptureIDIn.length === 0) { 5257 return _triggerError(this, errorMap.InvalidArguments, 'wrong arguments (setCaptureDevice)', arguments); 5258 } 5259 5260 _sendClientRequest('setCaptureDevice', { 5261 'clientCaptureID': clientCaptureIDIn 5262 }); 5263 5264 // after setting device, we need to refresh our cache 5265 _sendClientRequest('getMultimediaDevices', function mmDevicesCb(content) { 5266 _triggerMMDeviceEvent($this, content); 5267 }); 5268 } 5269 5270 /** 5271 * Shows the call in an external video window. If an external video window already exists, 5272 * the current contents will be replaced by the video stream for the selected call. Otherwise, a new external window will be created. 5273 * To detect changes in the window state, for example the user closes the window, use {@link $.fn.cwic#event:externalWindowEvent}. 5274 * <br> 5275 * By default the external video window will have always on top property and will include a picture-in-picture preview (self-view). 5276 * This can be changed using {@link $.fn.cwic-setExternalWindowAlwaysOnTop} and {@link $.fn.cwic-setExternalWindowShowSelfViewPip}, respectively. 5277 * <br> 5278 * If the user closes an external video window that contains a video call, the call will be ended. 5279 * Use {@link $.fn.cwic-hideExternalWindow} to remove the window without interupting the call. 5280 * @since 3.1.0 5281 * @param {String|Object} [id] A {String} conversation identifier or an {Object} containing an id property. 5282 */ 5283 function showCallInExternalWindow() { 5284 _log(true, 'showCallInExternalWindow'); 5285 5286 var $this = this; 5287 var conversation = $this.data('cwic'); 5288 var conversationId = conversation ? conversation.id : null; 5289 5290 // inspect arguments 5291 if (arguments.length > 0) { 5292 if (typeof arguments[0] === 'object') { 5293 conversation = arguments[0]; 5294 conversationId = conversation.id; 5295 } 5296 else if (typeof arguments[0] === 'string') { 5297 conversationId = arguments[0]; 5298 } 5299 } 5300 5301 if (!conversationId) { 5302 return _triggerError($this, errorMap.InvalidArguments, 'wrong arguments (showCallInExternalWindow)', arguments); 5303 } 5304 5305 dockGlobals.isVideoBeingReceived = true; 5306 _sendClientRequest('showCallInExternalWindow', { 5307 callId: conversationId 5308 }); 5309 } 5310 5311 /** 5312 * Shows preview (self-view) in an external video window. If an external video window already exists, 5313 * the current contents will be replaced by the preview. Otherwise, a new external window will be created. 5314 * To detect changes in the window state, for example the user closes the window, use {@link $.fn.cwic#event:externalWindowEvent}. 5315 * <br> 5316 * By default the external video window will have always on top property. This can be changed using {@link $.fn.cwic-setExternalWindowAlwaysOnTop}. 5317 * <br> 5318 * If preview in picture-in-picture is enabled (see {@link $.fn.cwic-setExternalWindowShowSelfViewPip}) 5319 * it will not be visible while the preview is in the full window. 5320 * <br> 5321 * Use {@link $.fn.cwic-hideExternalWindow} to remove the window. 5322 * @since 3.1.0 5323 */ 5324 function showPreviewInExternalWindow() { 5325 if (isMultimediaStarted) { 5326 _log(true, 'showPreviewInExternalWindow'); 5327 dockGlobals.isVideoBeingReceived = true; 5328 _sendClientRequest('showPreviewInExternalWindow'); 5329 } else { 5330 _log(false, 'returning from showPreviewInExternalWindow ... not supported in the current state'); 5331 } 5332 } 5333 5334 /** 5335 * Triggers an {@link $.fn.cwic#event:externalWindowEvent} to be sent to the application with the current state of the external window. 5336 * @since 3.1.0 5337 */ 5338 function getExternalWindowState() { 5339 if (isMultimediaStarted) { 5340 _log(true, 'getExternalWindowState'); 5341 _sendClientRequest('getExternalWindowState'); 5342 } else { 5343 _log(false, 'returning from getExternalWindowState ... not supported in the current state'); 5344 } 5345 } 5346 5347 /** 5348 * Hides an external video window created by {@link $.fn.cwic-showPreviewInExternalWindow} or {@link $.fn.cwic-showCallInExternalWindow}. 5349 * @since 3.1.0 5350 */ 5351 function hideExternalWindow() { 5352 if (isMultimediaStarted) { 5353 _log(true, 'hideExternalWindow'); 5354 dockGlobals.isVideoBeingReceived = false; 5355 _sendClientRequest('hideExternalWindow'); 5356 } else { 5357 _log(false, 'returning from hideExternalWindow ... not supported in the current state'); 5358 } 5359 } 5360 5361 /** 5362 * Controls whether external video windows created by {@link $.fn.cwic-showPreviewInExternalWindow} or 5363 * {@link $.fn.cwic-showCallInExternalWindow} are shown always on top (default) or not. 5364 * @since 3.1.0 5365 * @param {Boolean} isAlwaysOnTop Set to false to remove the always on top property. Set to true to restore default behavior. 5366 */ 5367 function setExternalWindowAlwaysOnTop() { 5368 if (isMultimediaStarted) { 5369 _log(true, 'setExternalWindowAlwaysOnTop'); 5370 if (typeof arguments[0] === 'boolean') { 5371 _sendClientRequest('setExternalWindowAlwaysOnTop', { alwaysOnTop: arguments[0] }); 5372 } 5373 } else { 5374 _log(false, 'returning from setExternalWindowAlwaysOnTop ... not supported in the current state'); 5375 } 5376 } 5377 5378 /** 5379 * Controls whether a picture-in-picture preview (self-view) is shown when {@link $.fn.cwic-showCallInExternalWindow} is used to put a call in external video window. 5380 * @since 3.1.0 5381 * @param {Boolean} showPipSelfView Set to false to turn off the picture-in-picture. Set to true to restore default behavior. 5382 */ 5383 function setExternalWindowShowSelfViewPip() { 5384 if (isMultimediaStarted) { 5385 _log(true, 'setExternalWindowShowSelfViewPip'); 5386 if (typeof arguments[0] === 'boolean') { 5387 _sendClientRequest('setExternalWindowShowSelfViewPip', { showSelfViewPip: arguments[0] }); 5388 } 5389 } else { 5390 _log(false, 'returning from setExternalWindowShowSelfViewPip ... not supported in the current state'); 5391 } 5392 } 5393 5394 /** 5395 * Controls whether a overlaid controls are shown in external video window created by {@link $.fn.cwic-showCallInExternalWindow} or {@link $.fn.cwic-showPreviewInExternalWindow}. 5396 * @since 4.0.0 5397 * @param {Boolean} showControls Set to false to turn off the overlaid call controls. Set to true to restore default behavior. 5398 */ 5399 function setExternalWindowShowControls(showControls) { 5400 if (isMultimediaStarted) { 5401 _log(true, 'setExternalWindowShowControls'); 5402 if (typeof showControls === 'boolean') { 5403 _sendClientRequest('setExternalWindowShowControls', {showControls: showControls}); 5404 } 5405 } else { 5406 _log(false, 'returning from setExternalWindowShowControls ... not supported in the current state'); 5407 } 5408 } 5409 5410 /** 5411 * Sets the window title used in external video windows created by {@link $.fn.cwic-showPreviewInExternalWindow} or {@link $.fn.cwic-showCallInExternalWindow}. 5412 * @since 3.1.0 5413 * @param {String} title A string value to be used as the window title for the exernal video window. 5414 */ 5415 function setExternalWindowTitle() { 5416 if (isMultimediaStarted) { 5417 _log(true, 'setExternalWindowTitle'); 5418 if (typeof arguments[0] === 'string') { 5419 _sendClientRequest('setExternalWindowTitle', { title: arguments[0] }); 5420 } 5421 } else { 5422 _log(false, 'returning from setExternalWindowTitle ... not supported in the current state'); 5423 } 5424 } 5425 5426 var dockGlobals = { 5427 _about: null, 5428 hasDockingCapabilities: function () { 5429 return dockGlobals._about.capabilities.externalWindowDocking; 5430 }, 5431 isVideoBeingReceived: false, 5432 isDocked: false, 5433 timeOfPreviousDocking: 0, 5434 minimalTimeBeforeChangingPosition: /* TODO: find optimal value to avoid lag, but also to avoid too many dockUpdate messges */ 10, 5435 targetDiv: null, 5436 targetDivStyle: { 5437 'background-color': 'magenta', 5438 'width': '8px', 5439 'height': '8px', 5440 'position': 'fixed', 5441 'border-right': '8px solid black', 5442 'z-index': '2147483647' 5443 }, 5444 frame: window, 5445 element: null, 5446 position: {}, 5447 move: function () { 5448 if (dockGlobals.isDocked) { 5449 // (new Date()).getTime() returns the current time in milliseconds 5450 var millisecondsSinceLastMove = (new Date()).getTime() - dockGlobals.timeOfPreviousDocking; 5451 var rect = dockGlobals.updateOffsets(dockGlobals.element.getBoundingClientRect()); 5452 var dockedWindowMoved = (rect.top === dockGlobals.position.top && 5453 rect.left === dockGlobals.position.left && 5454 rect.height === dockGlobals.position.height && 5455 rect.width === dockGlobals.position.width && 5456 rect.cropTop === dockGlobals.position.cropTop && 5457 rect.cropLeft === dockGlobals.position.cropLeft && 5458 rect.cropHeight === dockGlobals.position.cropHeight && 5459 rect.cropWidth === dockGlobals.position.cropWidth) ? 5460 false : true; 5461 // 8 and 16 because of minimal width of magenta target 5462 if (dockGlobals.isVideoBeingReceived && rect.cropHeight > 8 && rect.cropWidth > 16 && dockGlobals._about.capabilities.showingDockingTarget) { // TODO: change to showDocking 5463 dockGlobals.setTarget(rect); 5464 } else { 5465 $(dockGlobals.targetDiv).css({"display": "none"}); 5466 } 5467 if (dockedWindowMoved && millisecondsSinceLastMove > dockGlobals.minimalTimeBeforeChangingPosition) { 5468 dockGlobals.timeOfPreviousDocking = (new Date()).getTime(); 5469 $.extend(dockGlobals.position, rect); 5470 dockGlobals.sendMessageToAddOn("dockUpdate", dockGlobals.position); 5471 } 5472 dockGlobals.frame.requestAnimationFrame(dockGlobals.move); 5473 } 5474 }, 5475 updateOffsets: function (positionWithinInnermostFrame) { 5476 var currentFrame = dockGlobals.frame, 5477 currentFrameHeight = $(currentFrame).height(), 5478 currentFrameWidth = $(currentFrame).width(), 5479 parrentFrameHeight, 5480 parrentFrameWidth, 5481 position = $.extend({ 5482 cropTop: 0, 5483 cropLeft: 0, 5484 cropBottom: 0, 5485 cropRight: 0, 5486 cropWidth: 0, 5487 cropHeight: 0 5488 }, positionWithinInnermostFrame), 5489 frameBorderOffset = 0, 5490 borderTopOffset = 0, 5491 borderLeftOffset = 0, 5492 paddingTopOffset = 0, 5493 paddingLeftOffset = 0; 5494 5495 // calculating crop values for innermost iframe 5496 position.cropTop = (positionWithinInnermostFrame.top < 0) ? 5497 Math.abs(positionWithinInnermostFrame.top) : 0; 5498 position.cropLeft = (positionWithinInnermostFrame.left < 0) ? 5499 Math.abs(positionWithinInnermostFrame.left) : 0; 5500 position.cropBottom = Math.max(positionWithinInnermostFrame.bottom - currentFrameHeight, 0); 5501 position.cropRight = Math.max(positionWithinInnermostFrame.right - currentFrameWidth, 0); 5502 5503 while (currentFrame != currentFrame.top) { 5504 currentFrameRect = currentFrame.frameElement.getBoundingClientRect(); 5505 parentFrameWidth = $(currentFrame.parent).width(); 5506 parentFrameHeight = $(currentFrame.parent).height(); 5507 5508 // !! converts to boolean: 0 and NaN map to false, the rest of the numbers map to true 5509 if (currentFrame.frameElement.frameBorder === "" || 5510 !!parseInt(currentFrame.frameElement.frameBorder, 10)) { 5511 // after testing on Chrome, whenever a frameBorder is present, it's size is 2px 5512 frameBorderOffset = 2; 5513 } else { 5514 frameBorderOffset = 0; 5515 } 5516 5517 if (currentFrame.frameElement.style.borderTopWidth === "") { 5518 borderTopOffset = frameBorderOffset; 5519 } else { 5520 borderTopOffset = parseInt(currentFrame.frameElement.style.borderTopWidth || 0, 10); 5521 } 5522 paddingTopOffset = parseInt(currentFrame.frameElement.style.paddingTop || 0, 10); 5523 5524 if (currentFrameRect.top < 0) { 5525 if (position.top + position.cropTop < 0) { 5526 position.cropTop += Math.abs(currentFrameRect.top); 5527 } else if (Math.abs(currentFrameRect.top) - (position.top + position.cropTop + borderTopOffset + paddingTopOffset) > 0) { 5528 position.cropTop += Math.abs(Math.abs(currentFrameRect.top) - (position.top + position.cropTop + borderTopOffset + paddingTopOffset)); 5529 } 5530 } 5531 5532 if (currentFrameRect.top + borderTopOffset + paddingTopOffset + position.top + position.height - position.cropBottom > parentFrameHeight) { 5533 position.cropBottom = currentFrameRect.top + borderTopOffset + paddingTopOffset + position.top + position.height - parentFrameHeight; 5534 } 5535 position.top += currentFrameRect.top + borderTopOffset + paddingTopOffset; 5536 5537 if (currentFrame.frameElement.style.borderLeftWidth === "") { 5538 borderLeftOffset = frameBorderOffset; 5539 } else { 5540 borderLeftOffset = parseInt(currentFrame.frameElement.style.borderLeftWidth || 0, 10); 5541 } 5542 paddingLeftOffset = parseInt(currentFrame.frameElement.style.paddingLeft || 0, 10); 5543 5544 if (currentFrameRect.left < 0) { 5545 if (position.left + position.cropLeft < 0) { 5546 position.cropLeft += Math.abs(currentFrameRect.left); 5547 } else if (Math.abs(currentFrameRect.left) - (position.left + position.cropLeft + borderLeftOffset + paddingLeftOffset) > 0) { 5548 position.cropLeft += Math.abs(Math.abs(currentFrameRect.left) - (position.left + position.cropLeft + borderLeftOffset + paddingLeftOffset)); 5549 } 5550 } 5551 5552 if (currentFrameRect.left + borderLeftOffset + paddingLeftOffset + position.left + position.width - position.cropRight > parentFrameWidth) { 5553 position.cropRight = currentFrameRect.left + borderLeftOffset + paddingLeftOffset + position.left + position.width - parentFrameWidth; 5554 } 5555 position.left += currentFrameRect.left + borderLeftOffset + paddingLeftOffset; 5556 currentFrame = currentFrame.parent; 5557 } 5558 5559 position.cropHeight = Math.max(position.height - (position.cropTop + position.cropBottom), 0); 5560 position.cropTop = (position.height > position.cropTop) ? position.cropTop : 0; 5561 position.cropWidth = Math.max(position.width - (position.cropLeft + position.cropRight), 0); 5562 position.cropLeft = (position.width > position.cropLeft) ? position.cropLeft : 0; 5563 5564 return position; 5565 }, 5566 sendMessageToAddOn: function (msgName, position) { 5567 //we need to take into account the devicePixelRatio and the CSS zoom property 5568 // it won't work if css zoom is set on some of parent elements 5569 var scaleCoefficient = dockGlobals.frame.devicePixelRatio * $(dockGlobals.element).css('zoom'); 5570 5571 if (('ontouchstart' in window) && 5572 (navigator.maxTouchPoints > 0) // TODO: perhaps check >1 and check what devices we support 5573 ) { 5574 //browser with either Touch Events of Pointer Events 5575 //running on touch-capable device 5576 5577 // TODO: scroll bars are not calculated here. Find a way to calculate it. 5578 // when we update coefficient this way, regular zoom case will also have scroll bar defect 5579 // var scrollBarWidth = document.body.scrollWidth - document.body.clientWidth, 5580 // inner = dockGlobals.frame.innerWidth; 5581 // if (scrollBarWidth !== 0) { 5582 // inner -= scrollBarWidth; 5583 // } 5584 // scaleCoefficient *= dockGlobals.frame.document.documentElement.clientWidth / inner; 5585 } 5586 5587 var addOnMessageContent = { 5588 offsetX: Math.round(scaleCoefficient * position.left), 5589 offsetY: Math.round(scaleCoefficient * position.top), 5590 width: Math.ceil(scaleCoefficient * position.width), 5591 height: Math.ceil(scaleCoefficient * position.height), 5592 cropOffsetX: Math.round(scaleCoefficient * position.cropLeft) || 0, 5593 cropOffsetY: Math.round(scaleCoefficient * position.cropTop) || 0, 5594 cropWidth: Math.floor(scaleCoefficient * position.cropWidth) || 0, 5595 cropHeight: Math.floor(scaleCoefficient * position.cropHeight) || 0 5596 }; 5597 if (msgName === "dockExternalWindow") { 5598 addOnMessageContent.title = dockGlobals.frame.top.document.title || 5599 (dockGlobals.frame.top.location.host + dockGlobals.frame.top.location.pathname + dockGlobals.frame.top.location.search); 5600 } else { 5601 addOnMessageContent.pageWidth = Math.floor($(dockGlobals.frame.top).width() * scaleCoefficient); 5602 addOnMessageContent.pageHeight = Math.floor($(dockGlobals.frame.top).height() * scaleCoefficient); 5603 } 5604 _sendClientRequest(msgName, addOnMessageContent); // TODO: couple of ms could be saved if we call _plugin.api.sendRequest directly. 5605 }, 5606 resetPosition: function () { 5607 if (dockGlobals.targetDiv) { 5608 $(dockGlobals.targetDiv).css('display', 'none'); 5609 } 5610 dockGlobals.position = {}; 5611 dockGlobals.timeOfPreviousDocking = 0; 5612 }, 5613 setTarget: function (position) { 5614 // if the overlay DOM Element is only partially visible, the target should be at the top left of the visible part of the DOM Element 5615 var targetTop = (position.top + position.cropTop > 0 || position.bottom <= /* magenta target height */ 8) ? 5616 Math.ceil(position.top + position.cropTop) : 0; 5617 var targetLeft = (position.left + position.cropLeft > 0 || position.right <= /* magenta target (+ border) width */ 16) ? 5618 Math.ceil(position.left + position.cropLeft) : 0; 5619 dockGlobals.createTargetDivIfNeeded(); 5620 $(dockGlobals.targetDiv).css({"display": "block", "top": targetTop, "left": targetLeft}); 5621 }, 5622 createTargetDivIfNeeded: function () { 5623 if (!dockGlobals.targetDiv) { 5624 dockGlobals.targetDiv = dockGlobals.frame.top.document.createElement("div"); 5625 dockGlobals.frame.top.document.body.appendChild(dockGlobals.targetDiv); 5626 $(dockGlobals.targetDiv).css(dockGlobals.targetDivStyle); 5627 } 5628 } 5629 }; 5630 5631 /** 5632 * Docks an external video window created by {@link $.fn.cwic-showPreviewInExternalWindow} or {@link $.fn.cwic-showCallInExternalWindow}.<br> 5633 * Example use: 5634 * @example 5635 * // simple case where the target video container is on the same HTML page as cwic.js file 5636 * $('#videocontainer').cwic('dock'); 5637 * // or an extended form where the window object along with target element could be specified. 5638 * // Window object could be from an iFrame or a popup with the same origin as "parent" page 5639 * $().cwic('dock', {window: windowElementOfSomeHtmlDocument, element: targetElementForVideoOverlay}); 5640 * @param {Object} [args] Information about target element for video overlay 5641 * @param {DOMWindow} args.window Window object in which the target element is located 5642 * @param {DOMElement} args.element Target element for video overlay 5643 * @since 3.1.2 5644 */ 5645 function dock (args) { 5646 var $this = this, 5647 frame = args ? args.window : window, 5648 element = args ? args.element : $this[0]; 5649 5650 dockGlobals._about = about(); 5651 5652 if (dockGlobals.hasDockingCapabilities() && (element instanceof frame.HTMLElement)) { 5653 dockGlobals.isDocked = true; 5654 _log(true, 'dock', arguments); 5655 dockGlobals.resetPosition(); 5656 dockGlobals.frame = frame; 5657 dockGlobals.element = element; 5658 dockGlobals.sendMessageToAddOn("dockExternalWindow", 5659 dockGlobals.updateOffsets(dockGlobals.element.getBoundingClientRect())); 5660 dockGlobals.frame.requestAnimationFrame(dockGlobals.move); 5661 } else if (!dockGlobals.hasDockingCapabilities()) { 5662 _triggerError($this, errorMap.DockingCapabilitiesNotAvailable); 5663 } else if (!(element instanceof frame.HTMLElement)) { 5664 _triggerError($this, errorMap.DockArgumentNotHTMLElement); 5665 } 5666 return $this; 5667 } 5668 5669 /** 5670 * Undocks an external video window previously docked by {@link $.fn.cwic-dock}<br> 5671 * Example use: 5672 * @example 5673 * $('#videocontainer').cwic('undock'); 5674 * // or: 5675 * $(document).cwic('undock'); 5676 * // or: 5677 * $().cwic('undock'); 5678 * @since 3.1.2 5679 */ 5680 function undock () { 5681 var $this = this; 5682 if (dockGlobals.hasDockingCapabilities() && dockGlobals.isDocked) { 5683 _log(true, 'undock', arguments); 5684 dockGlobals.resetPosition(); 5685 dockGlobals.isDocked = false; 5686 _sendClientRequest('undockExternalWindow'); 5687 } else if (!dockGlobals.hasDockingCapabilities()) { 5688 _triggerError($this, errorMap.DockingCapabilitiesNotAvailable); 5689 } 5690 return $this; 5691 } 5692 5693 /** @description Gets the current user authorization status. 5694 * @since 3.0.1 5695 * @returns {String} a value indicating the current user authorization status. 5696 * <ul> 5697 * <li>"UserAuthorized" indicates the user has authorized the Cisco Web Communicator add-on and it is ready to use.</li> 5698 * <li>"MustShowAuth" indicates the application must call {@link $.fn.cwic-showUserAuthorization} to show the user authorization dialog.</li> 5699 * <li>"UserDenied" indicates the user has denied the application access to the Cisco Web Communicator add-on.</li> 5700 * <li>"UserAuthPending" indicates the dialog box is currently displayed and the user has not yet selected "allow", "deny", or "always allow".</li> 5701 * <li>"Unknown" indicates status cannot be determined because delay authorization feature is not supported by the current Cisco Web Communicator add-on. 5702 * This case will trigger {@link $.fn.cwic-errorMap.OperationNotSupported} as well.</li> 5703 * </ul> 5704 */ 5705 function getUserAuthStatus() { 5706 var ab = about(); 5707 5708 if (!ab.capabilities.delayedUserAuth) { 5709 _triggerError(this, errorMap.OperationNotSupported, 'Check cwic("about").capabilities.delayedUserAuth'); 5710 return 'Unknown'; 5711 } 5712 5713 return _plugin.userAuthStatus; 5714 } 5715 /** @description Shows the user authorization dialog. This API must only be called if the application has provided a delayedUserAuth callback 5716 * in the settings object provided to the init function, and the status returned by {@link $.fn.cwic-getUserAuthStatus} is "MustShowAuth" If the application 5717 * receives the {@link $.fn.cwic-settings.delayedUserAuth} callback, the user authorization state will always be "MustShowAuth" so the application can safely call 5718 * showUserAuthorization from within the delayedUserAuth callback without checking getUserAuthStatus. 5719 * @since 3.0.1 5720 * @param {Function} denied A callback that will be called if the user selects "deny" from the user authorization dialog. If the user 5721 * selects allow or always allow, the settings.ready callback will be called. 5722 * @param {Boolean} [force=false] Since 3.1.0 <br> 5723 * Set <tt>true</tt> to force the dialog to display even if the page is currently hidden. 5724 * Setting this may cause the dialog to appear when the page is not yet accessible to the user. 5725 */ 5726 function showUserAuthorization(args) { 5727 if (!args || !args.denied || !$.isFunction(args.denied)) { 5728 return _triggerError(this, errorMap.InvalidArguments, 'showUserAuthorization: wrong arguments'); 5729 } 5730 5731 // if page is not visible, then wait for visibilitychange event and retry showUserAuthorization 5732 if (document.hidden && !args.force) { 5733 _log('showUserAuthorization deferred with visibilityState: ' + document.visibilityState); 5734 _addListener(document,'visibilitychange', function handleVisibilityChange() { 5735 if(!document.hidden) { 5736 // show deferred dialog and remove listener 5737 _log('showUserAuthorization detected visibilitychange from hidden to ' + document.visibilityState); 5738 showUserAuthorization(args); 5739 _removeListener(document,'visibilitychange',handleVisibilityChange); 5740 } 5741 // else continue listening for 'visibilitychange' until not hidden 5742 }); 5743 return; 5744 } 5745 5746 _sendClientRequest('showUserAuthorization', function() { 5747 _plugin.deniedCb = args.denied; 5748 _plugin.userAuthStatus = 'UserAuthPending'; 5749 }); 5750 } 5751 5752 // a map with all exposed methods 5753 var methods = { 5754 about: about, 5755 init: init, 5756 shutdown: shutdown, 5757 rebootIfBroken: rebootIfBroken, 5758 registerPhone: registerPhone, 5759 manualSignIn: registerPhone, 5760 switchPhoneMode: switchPhoneMode, 5761 unregisterPhone: unregisterPhone, 5762 startConversation: startConversation, 5763 updateConversation: updateConversation, 5764 endConversation: endConversation, 5765 createVideoWindow: createVideoWindow, 5766 addPreviewWindow: addPreviewWindow, 5767 removePreviewWindow: removePreviewWindow, 5768 sendDTMF: sendDTMF, 5769 getInstanceId: getInstanceId, 5770 getMultimediaDevices: getMultimediaDevices, 5771 setRecordingDevice: setRecordingDevice, 5772 setPlayoutDevice: setPlayoutDevice, 5773 setCaptureDevice: setCaptureDevice, 5774 setRingerDevice: setRingerDevice, 5775 getUserAuthStatus: getUserAuthStatus, 5776 showUserAuthorization: showUserAuthorization, 5777 showCallInExternalWindow: showCallInExternalWindow, 5778 hideExternalWindow: hideExternalWindow, 5779 showPreviewInExternalWindow: showPreviewInExternalWindow, 5780 setExternalWindowAlwaysOnTop: setExternalWindowAlwaysOnTop, 5781 setExternalWindowShowSelfViewPip: setExternalWindowShowSelfViewPip, 5782 setExternalWindowShowControls: setExternalWindowShowControls, 5783 setExternalWindowTitle: setExternalWindowTitle, 5784 getExternalWindowState: getExternalWindowState, 5785 dock: dock, 5786 undock: undock, 5787 startDiscovery: startDiscovery, 5788 cancelSSO: cancelSSO, 5789 resetData: resetData, 5790 signOut: signOut, 5791 setSpeakerVolume: setSpeakerVolume, 5792 setRingerVolume: setRingerVolume, 5793 setMicrophoneVolume: setMicrophoneVolume, 5794 setRingtone: setRingtone, 5795 setPlayRingerOnAllDevices: setPlayRingerOnAllDevices, 5796 getMultimediaDeviceVolume : getMultimediaDeviceVolume 5797 }; 5798 5799 // the jQuery plugin 5800 /** 5801 * @description 5802 * CWIC is a jQuery plug-in to access the Cisco Web Communicator<br> 5803 * Audio and Video media require the Cisco Web Communicator add-on to be installed <br> 5804 * <h3>Fields overview</h3> 5805 * <h3>Methods overview</h3> 5806 * All cwic methods are called in the following manner<br> 5807 * <pre class="code">$('#selector').cwic('method',parameters)</pre><br> 5808 * <h3>Events overview</h3> 5809 * All events are part of the cwic namespace. For example: 5810 * <ul> 5811 * <li>conversationStart.cwic</li> 5812 * <li>system.cwic</li> 5813 * <li>error.cwic</li> 5814 * </ul> 5815 * <h4>Example conversation events:</h4> 5816 * These are conversation-related events that can be triggered by the SDK.<br> 5817 * The event handlers are passed the conversation properties as a single object. For example:<br> 5818 * @example 5819 * // start an audio conversation with phone a number and bind to conversation events 5820 * TODO: check this example. Is it better to put jQuery('#phone') instead of jQuery('#conversation') 5821 * jQuery('#conversation') 5822 * .cwic('startConversation', '+1 234 567') // container defaults to $(this) 5823 * .bind('conversationStart.cwic', function(event, conversation, container) { 5824 * console.log('conversation has just started'); 5825 * // container is jQuery('#conversation') TODO: is it!? It should be a new container created for conversation. Check startConversation API. 5826 * }) 5827 * .bind('conversationUpdate.cwic', function(event, conversation) { 5828 * console.log('conversation has just been updated'); 5829 * }) 5830 * .bind('conversationEnd.cwic', function(event, conversation) { 5831 * console.log('conversation has just ended'); 5832 * }); 5833 * @example 5834 * // listen for incoming conversation 5835 * jQuery('#phone') 5836 * .bind('conversationIncoming.cwic', function(event, conversation, container) { 5837 * console.log('incoming conversation with id ' + conversation.id); 5838 * // attach the 'toast' container to the DOM and bind to events 5839 * container 5840 * .appendTo('#phone') 5841 * .bind('conversationUpdate.cwic', function(event, conversation) { 5842 * // update on incoming conversation 5843 * }) 5844 * .bind('conversationEnd.cwic', function(event, conversation) { 5845 * // incoming conversation has ended 5846 * container.remove(); 5847 * }); 5848 * // suppose UI has a button with id 'answer' 5849 * jQuery('#answer').click(function() { 5850 * // answer the incoming conversation 5851 * // conversation has an id property, so startConversation accepts it 5852 * // use element #conversation as container 5853 * jQuery('#conversation').cwic('startConversation', conversation); 5854 * // remove incoming container 5855 * container.remove(); 5856 * }); 5857 * }); 5858 * @class 5859 * @static 5860 * @param {String} method The name of the method to call 5861 * @param {Variable} arguments trailing arguments are passed to the specific call see methods below 5862 */ 5863 $.fn.cwic = function(method) { 5864 5865 try { 5866 // Method calling logic 5867 if (methods[method]) { 5868 return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 5869 } else if (typeof method === 'object' || !method) { 5870 return methods.init.apply(this, arguments); 5871 } else { 5872 throw method + ': no such method on jQuery.cwic'; 5873 } 5874 } 5875 catch (e) { 5876 if (typeof console !== 'undefined') { 5877 if (console.trace) { 5878 console.trace(); 5879 } 5880 if (console.log && e.message) { 5881 console.log('Exception occured in $.fn.cwic() ' + e.message); 5882 } 5883 } 5884 _triggerError(this, e); 5885 } 5886 }; 5887 } (jQuery)); 5888