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