PageSourceSearch

https://online.roboform.com/js/application.js?vvv=202609141201

js roboform.com collected 2026-09-24 08:26:14 UTC 189,382 bytes, 4,374 lines download raw bytes

1/* global $, RfapiJS, RFO */
2
3var RFO = {};
4try {
5    RFO = JSON.parse(document.querySelector('meta[name="rfo-config"]').content);
6    RFO.PAGE_TYPE = document.querySelector('meta[name="page-type"]').content
7} catch (e) {}
8
9RFO.DIR_REQUEST = '/requests/';
10RFO.POLICIES = '';
11RFO.ACCOUNT_INFO = {};
12RFO.COMPANY_INFO = {};
13RFO.IDPStateData = null;
14RFO.bFrameRelocated = false;
15
16RFO.ACCOUNT_LOGIN_MIN = 6;   // chars
17RFO.ACCOUNT_LOGIN_MAX = 80;  // bytes
18RFO.ACCOUNT_NAME_MIN  = 2;   // chars
19RFO.ACCOUNT_NAME_MAX  = 80;  // bytes
20RFO.GROUP_NAME_MIN    = 1;   // chars
21RFO.GROUP_NAME_MAX    = 60;  // bytes
22RFO.PWD_MIN           = 12;  // chars
23RFO.PWD_MAX           = 500; // bytes
24RFO.PWD_NONNUM_MIN    = 4;   // chars
25RFO.COMPANY_NAME_MIN  = 2;   // chars
26RFO.COMPANY_NAME_MAX  = 60;  // bytes
27RFO.COMPANY_URL_MIN   = 7;   // chars
28RFO.COMPANY_URL_MAX   = 60;  // bytes
29
30var RFMgr = new RfapiJS.RfDataManager();
31RFMgr.SetServerUrl(location.protocol + '//' + location.host);
32RFMgr.setBrowserParam(RFO.CLIENT_TYPE);
33
34$.fn.selectText = function(){
35    var el = this[0];
36    var doc = document;
37    if (doc.body.createTextRange) {
38        var range = doc.body.createTextRange();
39        range.moveToElementText(el);
40        range.select();
41    } else if (window.getSelection) {
42        var selection = window.getSelection();        
43        var range = doc.createRange();
44        range.selectNodeContents(el);
45        selection.removeAllRanges();
46        selection.addRange(range);
47    }
48    if (el.setSelectionRange) { // textarea in Chrome
49        el.setSelectionRange(0,el.value.length);
50    }
51};
52
53// reposition tooltip after inserting content dynamically
54// usage: $element.popover('reposition')
55$.fn.popover.Constructor.prototype.reposition = function(){
56    var $tip = this.tip();
57    var placement = typeof this.options.placement === 'function' ? this.options.placement.call(this, $tip[0], this.$element[0]) : this.options.placement;
58    var pos = this.getPosition();
59    var actualWidth = $tip[0].offsetWidth;
60    var actualHeight = $tip[0].offsetHeight;
61    var orgPlacement = placement;
62    var viewportDim = this.getPosition(this.$viewport);
63    placement = placement === 'bottom' &&
64        pos.bottom + actualHeight > viewportDim.bottom ? 'top' : placement === 'top' &&
65        pos.top - actualHeight < viewportDim.top ? 'bottom' : placement === 'right' &&
66        pos.right + actualWidth > viewportDim.width ? 'left' : placement === 'left' &&
67        pos.left - actualWidth < viewportDim.left ? 'right' : placement;
68    $tip.removeClass(orgPlacement).addClass(placement);
69    var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight);
70    this.applyPlacement(calculatedOffset, placement);
71};
72
73// This code is needed for IE11 only as it is not support Array.from
74// Production steps of ECMA-262, Edition 6, 22.1.2.1
75// see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from
76if (!Array.from) {
77    Array.from = (function () {
78        var symbolIterator;
79        try {
80            symbolIterator = Symbol.iterator
81                ? Symbol.iterator
82                : 'Symbol(Symbol.iterator)';
83        } catch (e) {
84            symbolIterator = 'Symbol(Symbol.iterator)';
85        }
86
87        var toStr = Object.prototype.toString;
88        var isCallable = function (fn) {
89            return (
90                typeof fn === 'function' ||
91                    toStr.call(fn) === '[object Function]'
92                );
93        };
94        var toInteger = function (value) {
95            var number = Number(value);
96            if (isNaN(number)) return 0;
97            if (number === 0 || !isFinite(number)) return number;
98            return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
99        };
100        var maxSafeInteger = Math.pow(2, 53) - 1;
101        var toLength = function (value) {
102            var len = toInteger(value);
103            return Math.min(Math.max(len, 0), maxSafeInteger);
104        };
105
106        var setGetItemHandler = function setGetItemHandler(isIterator, items) {
107            var iterator = isIterator && items[symbolIterator]();
108            return function getItem(k) {
109                return isIterator ? iterator.next() : items[k];
110            };
111        };
112
113        var getArray = function getArray(
114            T,
115            A,
116            len,
117            getItem,
118            isIterator,
119            mapFn
120            ) {
121            // 16. Let k be 0.
122            var k = 0;
123
124            // 17. Repeat, while k < len… or while iterator is done (also steps a - h)
125            while (k < len || isIterator) {
126                var item = getItem(k);
127                var kValue = isIterator ? item.value : item;
128
129                if (isIterator && item.done) {
130                    return A;
131                } else {
132                    if (mapFn) {
133                        A[k] =
134                            typeof T === 'undefined'
135                                ? mapFn(kValue, k)
136                                : mapFn.call(T, kValue, k);
137                    } else {
138                        A[k] = kValue;
139                    }
140                }
141                k += 1;
142            }
143
144            if (isIterator) {
145                throw new TypeError(
146                    'Array.from: provided arrayLike or iterator has length more then 2 ** 52 - 1'
147                );
148            } else {
149                A.length = len;
150            }
151
152            return A;
153        };
154
155        // The length property of the from method is 1.
156        return function from(arrayLikeOrIterator /*, mapFn, thisArg */) {
157            // 1. Let C be the this value.
158            var C = this;
159
160            // 2. Let items be ToObject(arrayLikeOrIterator).
161            var items = Object(arrayLikeOrIterator);
162            var isIterator = isCallable(items[symbolIterator]);
163
164            // 3. ReturnIfAbrupt(items).
165            if (arrayLikeOrIterator == null && !isIterator) {
166                throw new TypeError(
167                    'Array.from requires an array-like object or iterator - not null or undefined'
168                );
169            }
170
171            // 4. If mapfn is undefined, then let mapping be false.
172            var mapFn = arguments.length > 1 ? arguments[1] : void undefined;
173            var T;
174            if (typeof mapFn !== 'undefined') {
175                // 5. else
176                // 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
177                if (!isCallable(mapFn)) {
178                    throw new TypeError(
179                        'Array.from: when provided, the second argument must be a function'
180                    );
181                }
182
183                // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
184                if (arguments.length > 2) {
185                    T = arguments[2];
186                }
187            }
188
189            // 10. Let lenValue be Get(items, "length").
190            // 11. Let len be ToLength(lenValue).
191            var len = toLength(items.length);
192
193            // 13. If IsConstructor(C) is true, then
194            // 13. a. Let A be the result of calling the [[Construct]] internal method
195            // of C with an argument list containing the single item len.
196            // 14. a. Else, Let A be ArrayCreate(len).
197            var A = isCallable(C) ? Object(new C(len)) : new Array(len);
198
199            return getArray(
200                T,
201                A,
202                len,
203                setGetItemHandler(isIterator, items),
204                isIterator,
205                mapFn
206            );
207        };
208    })();
209}
210// ==== End of code for Array.from ===============
211
212
213$(document).ready(function(){
214
215    // location.hash looks like "#opParams={opParams}" - operator followed the link from main to additional server
216    if (location.hash && location.hash.substr(0, 9) === '#opParams') {
217        if ($('#login_form').length) {
218            // not logged in - redirect to correct login page
219            location.href = RFO.ROOT_PATH + 'login?authReturnUrl=op_login&opParams=' + location.hash.substr(10);
220        } else {
221            // logged in - clear hash
222            location.href = location.pathname + location.search;
223        }
224    }
225
226    if ($('input[data-vk-id]').length) {
227        new VKI({
228            // attach keyboard without image button,
229            // show keyboard by clicking to the element with #id = 'data-vk-id' attribute
230            VKI_imageURI: "#"
231        });
232    } else {
233        var vkKeyboardImg = "/js/keyboard/keyboard.png";
234        (new Image()).src = vkKeyboardImg; // preload
235        new VKI({
236            offset_down:20,
237            VKI_imageURI: vkKeyboardImg
238        });
239    }
240
241    // check for old browser
242    if ($('#old-browser-msg').css('opacity') === '1') {
243        $('#old-browser-msg').removeClass('hidden');
244        $('body.loading').removeClass('loading');
245    } else {
246        var sNotSupported = '';
247        // we do not support browsers without atob and btoa functions (IE9 for example)
248        if (!window.atob) {
249            sNotSupported = _STR.CL_OLD_BROWSER_MSG;
250        } else {
251            // we do not support browsers with disabled cookies
252            $.cookie('_testCookie_', '1');
253            if ($.cookie('_testCookie_') === '1') {
254                $.cookie('_testCookie_', null);
255                // we do not support browsers without localStorage
256                if (!Utils.Storage.test()) {
257                    sNotSupported = _STR.CL_LOCAL_STORAGE_NOT_SUPPORTED;
258                }
259            } else {
260                sNotSupported = _STR.CL_DISABLED_COOKIES_NOT_SUPPORTED;
261            }
262        }
263        if (sNotSupported) {
264            $('.b-main').addClass('hidden');
265            $('#old-browser-msg .b3').html(sNotSupported);
266            $('#old-browser-msg').removeClass('hidden').css('opacity', '1');
267            $('body.loading').removeClass('loading');
268        }
269    }
270    RFO.bFrameRelocated = RelocateFrame();
271    if (!$('#old-browser-msg').hasClass('hidden')) {
272        return;
273    }
274
275    // scrollable tables
276    $('.tableScrollable').each(function(){
277        var $scrollableTable = $(this);
278        $scrollableTable.on('adjustHeader', function(){
279            if (!$scrollableTable.hasClass('_adjusted')) {
280                var headerHeight = $scrollableTable.find('thead th, thead td')[0].offsetHeight+1; // +1 needed for Chrome
281                $scrollableTable.addClass('_adjusted').css('margin-top', '-'+headerHeight+'px').find('thead div').css('height', headerHeight+'px');
282                $scrollableTable.parent().css('margin-top', headerHeight+'px');
283            }
284        }).on('resetScrollState', function(){
285            $scrollableTable.parent().scrollLeft(0);
286            $scrollableTable.trigger('scroll');
287        });
288        // add divs to the table headers to adjust its width
289        $scrollableTable.find('thead th, thead td').each(function(){
290            var $this = $(this);
291            $this.append($('<div>'+$this.html()+'</div>'));
292        });
293        $scrollableTable.filter(':visible').trigger('adjustHeader');
294        // needed for horizontal table scrolling
295        var currentOffset = 0;
296        var $theadDiv = $scrollableTable.find('thead div');
297        $scrollableTable.parent().on('scroll', function(){
298            var offset = $scrollableTable.position().left;
299            if (offset !== currentOffset) {
300                var value = 'translateX('+offset+'px)';
301                $theadDiv.css({
302                    'transform': value,
303                    '-moz-transform': value,
304                    '-webkit-transform': value,
305                    '-o-transform': value,
306                    '-ms-transform': value
307                });
308                currentOffset = offset;
309            }
310        });
311    });
312
313    // sortable tables
314    $('.tableSortable').each(function(){
315        var $table = $(this);
316        var $tbody = $table.children('tbody');
317        var FILTERED_LIST = $tbody.children('tr');
318        var bRegularTable = true;
319        $table.off('sort.sortable').on('sort.sortable', function(e, newRowsArray){
320            if (newRowsArray) {
321                FILTERED_LIST = newRowsArray;
322                bRegularTable = (FILTERED_LIST[0] && FILTERED_LIST[0].nodeName);
323            }
324            $table.children('thead').find('.headerSortUp, .headerSortDown').trigger('sortTH.sortable');
325        }).off('reset.sortable').on('reset.sortable', function(e, newRowsArray){
326            if (newRowsArray) {
327                FILTERED_LIST = newRowsArray;
328                bRegularTable = (FILTERED_LIST[0] && FILTERED_LIST[0].nodeName);
329            }
330            $table.children('thead').find('.headerSortUp, .headerSortDown').removeClass('headerSortUp headerSortDown');
331        }).children('thead').find('td, th').each(function(col, th){
332            var $th = $(th);
333            var dataSort = $th.attr('data-sort');
334            if (dataSort) {
335                $th.off('sortTH.sortable').on('sortTH.sortable', function(){
336                    var dir = $th.hasClass('headerSortDown') ? 1 : 0;
337                    if (bRegularTable) {
338                        var bSortInGroups = $table.attr('data-sort-in-groups');
339                        FILTERED_LIST.sort(function (a, b) {
340                            var a_sortGroup = bSortInGroups ? parseInt($(a).attr('data-sort-group'), 10) : 0;
341                            var b_sortGroup = bSortInGroups ? parseInt($(b).attr('data-sort-group'), 10) : 0;
342                            if (a_sortGroup === b_sortGroup) {
343                                var compare = 0;
344                                var $a = $($(a)[0].cells[col]);
345                                var $b = $($(b)[0].cells[col]);
346                                var a_val = $a.attr('data-sort-value') || $a.text().trim().toLowerCase();
347                                var b_val = $b.attr('data-sort-value') || $b.text().trim().toLowerCase();
348                                switch (dataSort) {
349                                    case 'alpha':
350                                        compare = a_val > b_val ? 1 : (a_val < b_val ? -1 : 0);
351                                        break;
352                                    case 'number':
353                                        compare = parseFloat(a_val ? a_val : 0) - parseFloat(b_val ? b_val : 0);
354                                        break;
355                                }
356                                return compare * -(dir || -1); // * -1 if want opposite order
357                            }
358                            return (a_sortGroup - b_sortGroup);
359                        });
360                    } else {
361                        FILTERED_LIST.sort(function (a, b) {
362                            var compare = 0;
363                            var a_val = a['_data_'].sortValue[col];
364                            var b_val = b['_data_'].sortValue[col];
365                            switch (dataSort) {
366                                case 'alpha':
367                                    compare = a_val > b_val ? 1 : (a_val < b_val ? -1 : 0);
368                                    break;
369                                case 'number':
370                                    compare = parseFloat(a_val ? a_val : 0) - parseFloat(b_val ? b_val : 0);
371                                    break;
372                            }
373                            return compare * -(dir || -1); // * -1 if want opposite order
374                        });
375                    }
376                    $table.trigger('reset.sortable');
377                    $th.addClass(dir ? 'headerSortUp' : 'headerSortDown');
378                }).off('click').on('click', function(){
379                    $th.trigger('sortTH.sortable');
380                    if ($table.data('paginator')) {
381                        $table.trigger('init.paginator', [FILTERED_LIST]);
382                    } else {
383                        FILTERED_LIST.appendTo($tbody);
384                    }
385                }).addClass('headerSort').append($('<i></i>')).find('div').append($('<i></i>'));
386            }
387        });
388    });
389
390    // init RFMgr
391    if (RFO.LOGGED_ACCOUNT_ID) {
392        RFMgr.SetRfoOptions ({showAllGroups: true});
393        RFMgr.GetAccountInfo(RFO.LOGGED_ACCOUNT_ID,
394            // ************  OK (AnswerCallBack) ************
395            function (data) {
396                RFO.ACCOUNT_INFO = data;
397                if (!Utils.restoreMasterPassword()) {
398                    return;
399                }
400                RFO.POLICIES = data.policies ? Utils.parseINI(Utils.base64Decode(data.policies)) : '';
401                if (!RFO.ACCOUNT_INFO.passwordExpired) {
402                    RFMgr.UpdateSecurityScore({
403                        accountId:        RFO.LOGGED_ACCOUNT_ID,
404                        accountInfo:      RFO.ACCOUNT_INFO,
405                        itemsCounters:    false
406                    });
407                }
408                if (RFO.ACCOUNT_INFO.companies) {
409                    RFO.ACCOUNT_INFO.companiesPolicies = {};
410                    $.each(RFO.ACCOUNT_INFO.companies, function(i, companyData){
411                        RFO.ACCOUNT_INFO.companiesPolicies[companyData.companyId] = companyData.policies ? Utils.parseINI(Utils.base64Decode(companyData.policies)) : '';
412                        if (RFO.ACCOUNT_INFO.companies.length === 1 || companyData.companyId === $.cookie('selectedCompany')) {
413                            RFO.COMPANY_INFO = companyData;
414                        }
415                    });
416                    if (RFO.COMPANY_INFO.companyId && RFO.COMPANY_INFO.isAdmin) {
417                        Utils.createJSONRequest('/idp/get-settings', {companyId: RFO.COMPANY_INFO.companyId}).then(function (data) {
418                            RFO.IDPStateData = data;
419                            $(RFMgr).trigger('init');
420                        }).catch(function (e) {
421                            $(RFMgr).trigger('init');
422                        });
423                    } else {
424                        $(RFMgr).trigger('init');
425                    }
426                } else {
427                    $(RFMgr).trigger('init');
428                }
429            },
430            //  ***********  ERROR (ErrorCallBack) ***********
431            function (errObject) {
432                if (errObject.Code && (errObject.Code === 401 || errObject.Code === 403 && errObject.HttpRequest.getResponseHeader('x-sib-reason') === 'need-idp-auth')) {
433                    //access denied error (session expired)
434                    doLogout(RFO.ROOT_PATH + 'login?relogin');
435                } else {
436                    showResultMsg(Utils.stripTags(errObject.Description), true);
437                }
438            }
439        );
440    } else {
441        $(RFMgr).trigger('init');
442    }
443
444    // IE10 viewport hack for Surface/desktop Windows 8 bug
445    // See the Getting Started docs for more information:
446    // http://getbootstrap.com/getting-started/#support-ie10-width
447    if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
448        var msViewportStyle = document.createElement('style');
449        msViewportStyle.appendChild(
450            document.createTextNode(
451                '@-ms-viewport{width:auto!important}'
452            )
453        );
454        document.querySelector('head').appendChild(msViewportStyle);
455    }
456
457    // enables all tooltips on the page
458    $('[data-toggle="popover"]').popover({
459        html: true,
460        trigger: 'hover',
461        container: 'body',
462        delay: { "show": 200, "hide": 0 }
463    });
464
465    // set sticky navbar
466    var affixOffsetHeight = $('.b-navbar-1').outerHeight(true);
467    var affixOffsetMargin = $('.b-navbar-2').outerHeight(true);
468    $('.b-navbar-2').on('affixed.bs.affix', function(){
469        $('.b-navbar-1').addClass('invisible').css('margin-top', affixOffsetMargin+'px');
470        $('.modal-dialog').addClass('modal-dialog-affix');
471    }).on('affixed-top.bs.affix', function(){
472        $('.b-navbar-1').css('margin-top', '').removeClass('invisible');
473        $('.modal-dialog').removeClass('modal-dialog-affix');
474    }).affix({offset: {top: affixOffsetHeight}});
475
476    // set language selector
477    var f = location.search.match(/&?\??lang=(\w\w)/);
478    var lang = f ? f[1] : $.cookie('lang');
479    $('#selectLangDialog ul').html($('.b-lang-alt .dropdown-menu').html());
480    $('.b-lang, .b-lang-alt, #selectLangDialog').find('li a').each(function(){
481        var $this = $(this);
482        if ($this.attr('data-lang') === lang) {
483            $this.closest('.dropdown').find('._current').text($this.text());
484        }
485        $this.on('click', function(e){
486            e.preventDefault();
487            var urlParts = location.toString().split('#');
488            urlParts[0] = (urlParts[0].replace(/&?(\??)lang=\w\w/, '$1') + (location.search ? '&' : '?') + 'lang=' + $this.attr('data-lang')).replace('?&', '?');
489            location.assign(urlParts.join('#'));
490        });
491    });
492    $('._selectLang').on('click', function(e){
493        e.preventDefault();
494        $('#selectLangDialog').modal('show');
495    });
496
497    // show/hide dropdown menu on hover
498    $('#mainMenuNavbar .dropdown').each(function(){
499        var timerId;
500        var $this = $(this);
501        $this.on('mouseover', function(){
502            clearTimeout(timerId);
503            timerId = setTimeout(function(){
504                var $toggle = $this.addClass('open').find('.dropdown-toggle').on('click.myevent', function(){
505                    // prevent hiding the menu on the click event when it just appeared
506                    return false;
507                });
508                setTimeout(function(){
509                    // and then grant hiding the menu by the click event
510                    $toggle.off('click.myevent');
511                }, 700);
512            }, 0);
513        }).on('mouseout', function(){
514            clearTimeout(timerId);
515            timerId = setTimeout(function(){
516                $this.removeClass('open');
517            }, 200);
518        });
519    });
520
521    $('.b-nav-dashboard').each(function () {
522        var $thisEl = $(this).removeClass('fixedHeight');
523        if ($thisEl.prop('scrollHeight') > 55) {
524            $thisEl.find('.info-block-bordered.visible-lg').removeClass('visible-lg').hide();
525            $thisEl.find('.info-block-bordered.hidden-lg').removeClass('hidden-lg').show();
526        }
527    });
528
529    // 'Storage Type' and 'Usage Permission' selectors in dialogs
530    $("div._sync_selector, div._perm_selector").each(function(){
531        var $this = $(this);
532        $this.find('.dropdown-menu tr').on('click', function(e){
533            var $tr = $(this);
534            var val = $tr.attr('data');
535            if (val!=0) {
536                var selText = $tr.find('td.value').html();
537            } else {
538                var selText = "<span>"+$this.find('tr[data="'+$this.attr('defdata')+'"] td.value').text()+"</span> "+_STR.CL_VALUE_DEFAULT;
539            }
540            $this.find('.dropdown-toggle span._selector_value').html(selText);
541            $tr.siblings('tr').removeClass('selected');
542            $tr.addClass('selected');
543        });
544    });
545
546    // show/hide 'more settings' section in dialogs
547    $('._moreSettings').each(function(){
548        var $moreSettings = $(this);
549        var $moreSettingsDiv = $moreSettings.find('.moreSettingsDiv');
550        var $settingsLink = $moreSettings.find('.moreSettings');
551        $moreSettings.on('collapse', function(){
552            $moreSettingsDiv.hide();
553            $settingsLink.removeClass('dropup');
554        });
555        $settingsLink.on('click', function(){
556            if ($settingsLink.hasClass('dropup')) {
557                $settingsLink.removeClass('dropup');
558                $moreSettingsDiv.hide();
559            } else {
560                $settingsLink.addClass('dropup');
561                $moreSettingsDiv.show();
562            }
563        });
564    });
565
566    // restore current filemanager url
567    $('._files').on('click', function(e){
568        e.preventDefault();
569        document.location.href = $(this).attr('href') + (sessionStorage.filebrowserUrl || '');
570    });
571
572    $('._logout').on('click', function(e){
573        e.preventDefault();
574        doLogout(RFO.ROOT_PATH + 'logout');
575    });
576
577    $('._email-verified').each(function(){
578        var urlParts = Utils.parseUrlQuery();
579        var errText = '';
580        if (urlParts.error === 'bad-code') {
581            errText = _STR.TEXT_EMAIL_VERIFY_BAD_CODE+"<br><br>"+Utils.xReplace(_STR.TEXT_EMAIL_VERIFY_INSTRUCTIONS, location.origin+'/login');
582        } else if (urlParts.error === 'expired-code') {
583            errText = _STR.TEXT_EMAIL_VERIFY_EXPIRED_CODE+"<br><br>"+Utils.xReplace(_STR.TEXT_EMAIL_VERIFY_INSTRUCTIONS, location.origin+'/login');
584        } else if (urlParts.error === 'try-later' && urlParts.url) {
585            errText = _STR.TEXT_EMAIL_VERIFY_FAILED + '<br>' + Utils.xReplace(_STR.TEXT_ALT_AUTH_EMAIL_VERIFY_LINK, 'href="'+encodeURIComponent(urlParts.url)+'"');
586        } else if (urlParts.error) {
587            errText = _STR.ERR_UNEXPECTED;
588        }
589        $(this).find('.alert').addClass(errText ? 'alert-danger' : 'alert-success').html(errText || _STR.TEXT_SUCCESS_VERIFY_EMAIL);
590    });
591
592    // disable spellchecking for all text inputs and textareas
593    $('input[type="text"], textarea').attr('spellcheck', 'false');
594
595    if (RFO.PAGE_TYPE === 'account_deleted') {
596        $('#account').text(sessionStorage.UserID);
597        $.cookie(null);
598    }
599});
600
601$(window).on('load', function(){
602    if (!RFO.bFrameRelocated) {
603        $('._autofocus').trigger('focus');
604        var $inp = $("#scorebar").parent().find('input');
605        if ($inp.length) {
606            $(window).on('resize', function(){
607                $("#scorebar").css({'width': $inp.outerWidth()+'px', 'left': $inp.position().left + parseInt($inp.css('marginLeft'), 10) + 'px'});
608            }).trigger('resize');
609        }
610    }
611});
612
613$(RFMgr).on('init', function() {
614    'use strict';
615
616    if (RFO.LOGGED_ACCOUNT_ID) {
617        (new AuthChecker()).start();
618    }
619
620    // analyze password strength
621    $('#signup_ent #signup_password').on('input focusin', function(){
622        Utils.updateStrength(this.value);
623    });
624
625    // breach password monitoring and check password compliance
626    $('#resetPass #password, #newPChange, #auth #signup_password, #login #signup_password').attr('maxlength', RFO.PWD_MAX).on('blur', function(){
627        var $password = $(this);
628        var password = $password.val();
629        if ($password.attr('VKI_shown') || PasswordChecker.lastCheckedPassword() === password) {
630            return;
631        }
632        var checks = PasswordChecker.checkPasswordForCompliance(password);
633        if (checks.problems.length) {
634            // password does not comply restriction, do not need to check for breachness
635            return;
636        }
637        var $qualityIndicator = $('.pwd-quality-ind').addClass('in-progress');
638        PasswordChecker.isPasswordCompromised(password, function(isPasswordBreached){
639            if (isPasswordBreached) {
640                $('.breach-warning').removeClass('hidden');
641                $password.addClass('weak-value');
642            } else {
643                $('.breach-warning').addClass('hidden');
644                $password.removeClass('weak-value');
645            }
646            $qualityIndicator.removeClass('in-progress');
647        });
648    }).on('input', function(){
649        var $password = $(this);
650        var $qualityIndicator = $('.pwd-quality-ind').removeClass('hidden');
651        $password.removeClass('bad-value weak-value');
652        $('.breach-warning').addClass('hidden');
653        PasswordChecker.stopCheckingPasswordForBreachness();
654        PasswordChecker.updatePasswordQualityIndicator($password, $qualityIndicator, $qualityIndicator.find('.check-list'));
655    }).trigger('input');
656
657    // check company invitations
658    if (
659        RFO.ACCOUNT_INFO && RFO.ACCOUNT_INFO.receivedData && RFO.ACCOUNT_INFO.receivedData.companyInvitations.length &&
660        (!RFO.ACCOUNT_INFO.companies || RFO.ACCOUNT_INFO.companies.length === 0) // not a member of other companies
661    ) {
662        $(document).on('click', '._companyInviteAccept, ._companyInviteReject', function(){
663            var $this = $(this);
664            var companyID = $this.val().trim();
665            var confirmText = $this.hasClass('_companyInviteAccept') ? _STR.CL_CONFIRM_ACCEPT_COMPANY : _STR.CL_CONFIRM_REJECT_COMPANY;
666            var successText = $this.hasClass('_companyInviteAccept') ? _STR.INVITATION_TEXT_SUCCESS_ACCEPT : _STR.INVITATION_TEXT_SUCCESS_REJECT;
667            var processFunc = $this.hasClass('_companyInviteAccept') ? RFMgr.JoinCompany : RFMgr.LeaveCompany;
668            if (!RFO.ACCOUNT_INFO.emailVerified && $this.hasClass('_companyInviteAccept')) {
669                return Utils.emailVerifyBeforeAction(_STR.CL_CONFIRM_ACCEPT_COMPANY_VERIFY_EMAIL, function(){
670                    $this.trigger('click');
671                });
672            }
673            Utils.confirm(Utils.xReplace(confirmText, $this.attr('data-company-name')), function(){
674                processFunc.call(RFMgr, companyID,
675                    //  ************  OK (AnswerCallBack) ************
676                    function () {
677                        showResultMsg(successText, false, undefined, function(){
678                            location.reload();
679                        });
680                    },
681                    //  ***********  ERROR (ErrorCallBack) ***********
682                    RFMgrCommonErrorHandler.bind(this, null)
683                );
684            });
685        });
686
687        $.each(RFO.ACCOUNT_INFO.receivedData.companyInvitations, function(){
688            var $alert = $('._companyInvitation').clone().removeClass('_companyInvitation hidden').appendTo($('.alertsContainer'));
689            var $alertText = $alert.find('._companyInvitationText');
690            $alertText.text(Utils.xReplace($alertText.text(), this.name));
691            $alert.find('._companyInviteAccept, ._companyInviteReject').val(this.companyId).attr('data-company-name', this.name);
692            if (RFMgr.getAccountMode() === RfapiJS.accountMode.OFS) {
693                $alert.find('._companyInvitationTypeOld').hide();
694            } else {
695                $alert.find('._companyInvitationTypeNew').hide();
696            }
697        });
698    }
699
700    // check emergency access invitations
701    if (RFO.ACCOUNT_INFO && RFO.ACCOUNT_INFO.receivedData && RFO.ACCOUNT_INFO.receivedData.emergencyAccess.length) {
702        $(document).on('click', '._emergencyInviteAccept, ._emergencyInviteReject', function(){
703            var $this = $(this);
704            var accountID = $this.val().trim();
705            var confirmText = $this.hasClass('_emergencyInviteAccept') ? _STR.CL_EA_ACCEPT_CONFIRM : _STR.CL_EA_REJECT_CONFIRM;
706            var successText = $this.hasClass('_emergencyInviteAccept') ? _STR.CL_EA_ACCEPT_SUCCESS : _STR.CL_EA_REJECT_SUCCESS;
707            var processFunc = $this.hasClass('_emergencyInviteAccept') ? RFMgr.AcceptEmergencyContactInvitation : RFMgr.RejectEmergencyContactInvitation;
708            if (!RFO.ACCOUNT_INFO.emailVerified && $this.hasClass('_emergencyInviteAccept')) {
709                return Utils.emailVerifyBeforeAction(_STR.CL_EA_ACCEPT_CONFIRM_VERIFY_EMAIL, function(){
710                    $this.trigger('click');
711                });
712            }
713            Utils.confirm(Utils.xReplace(confirmText, $this.attr('data-account-name')), function(){
714                processFunc.call(RFMgr, accountID,
715                    //  ************  OK (AnswerCallBack) ************
716                    function () {
717                        showResultMsg(successText, false, undefined, function(){
718                            location.reload();
719                        });
720                    },
721                    //  ***********  ERROR (ErrorCallBack) ***********
722                    RFMgrCommonErrorHandler.bind(this, null)
723                );
724            });
725        });
726
727        $.each(RFO.ACCOUNT_INFO.receivedData.emergencyAccess, function(){
728            if (this.accessStatus) {
729                return;
730            }
731            var $alert = $('._emergencyInvitation').clone().removeClass('_emergencyInvitation hidden').appendTo($('.alertsContainer'));
732            var $alertText = $alert.find('._emergencyInvitationText');
733            var nameText = this.name ? this.name+' ('+this.email+')' : this.email;
734            $alertText.text(Utils.xReplace($alertText.text(), nameText));
735            $alert.find('._emergencyInviteAccept, ._emergencyInviteReject').val(this.accountId).attr('data-account-name', nameText);
736            if (RFMgr.getAccountMode() === RfapiJS.accountMode.OFS) {
737                $alert.find('._emergencyInvitationTypeOld').hide();
738            } else {
739                $alert.find('._emergencyInvitationTypeNew').hide();
740            }
741        });
742    }
743
744    // show start screen after account creation
745    var $startScreenDialog = $('#startScreenWidget');
746    if ($startScreenDialog.length && sessionStorage.showStartScreen) {
747        $startScreenDialog.one('show.bs.modal', function(){
748            if (RFO.ACCOUNT_INFO.enterprise) {
749                $startScreenDialog.find('._enterprise').removeClass('hidden');
750            } else {
751                var browserName = Utils.browserName();
752                var $body = $startScreenDialog.find('._consumer').removeClass('hidden');
753                $body.find('.text._text_extension').html(Utils.xReplace($body.find('.text').html(), _STR.CL_STARTSCREEN_CONSUMER_TEXT_EXTENSION, browserName));
754                if (browserName === 'Android') {
755                    $body.find('._howto_extension').addClass('hidden');
756                    $body.find('.text._text_extension').addClass('hidden');
757                    $body.find('.text._text_install').removeClass('hidden');
758                    $body.find('._android').removeClass('hidden');
759                } else if (browserName === 'iOS') {
760                    $body.find('._howto_extension').addClass('hidden');
761                    $body.find('.text._text_extension').addClass('hidden');
762                    $body.find('.text._text_install').removeClass('hidden');
763                    $body.find('._ios').removeClass('hidden');
764                } else if (browserName === 'Internet Explorer') {
765                    $body.find('._howto_extension').addClass('hidden');
766                    $body.find('.text._text_extension').addClass('hidden');
767                    $body.find('.text._text_install').removeClass('hidden');
768                    $body.find('._windows').removeClass('hidden');
769                } else if (browserName === 'Firefox') {
770                    $body.find('.text').html(Utils.xReplace($body.find('.text').html(), _STR.CL_STARTSCREEN_CONSUMER_TEXT_ADDON, browserName));
771                    $body.find('._firefox').removeClass('hidden');
772                } else if (browserName === 'Safari') {
773                    $body.find('._safari').removeClass('hidden');
774                } else if (browserName === 'Edge') {
775                    $body.find('._edge').removeClass('hidden');
776                } else if (browserName === 'Chrome' || browserName === 'Opera') {
777                    $body.find('._chrome').removeClass('hidden');
778                }
779            }
780        }).modal('show');
781    }
782    sessionStorage.removeItem('showStartScreen');
783
784    $('.navbar-message a').on('click', function(e) {
785        e.preventDefault();
786        let passwd = RFMgr.getMasterPwd();
787        sessionStorage['rfo-cred'] = JSON.stringify({value: JSON.stringify({
788            login: RFO.ACCOUNT_INFO.login,
789            passw
789ord: Utils.encrypt(passwd, RFO.ACCOUNT_INFO.clientKey),
790        })});
791        window.location.href = $(this).attr('href');
792    });
793});
794
795// auto close collapsed bootstrap navbar on click
796$(document).on('click','.navbar-collapse.in',function(e) {
797    if ($(e.target).is('a') && $(e.target).attr('class') !== 'dropdown-toggle') {
798        $(this).collapse('hide');
799    }
800});
801
802// Backdrop z-index fix for multiple bootstrap modals
803$(document).on('show.bs.modal', '.modal', function () {
804    var zIndex = 1040 + (10 * $('.modal:visible').length);
805    $(this).css('z-index', zIndex+'');
806    // use a setTimeout because the .modal-backdrop isn't created when the event show.bs.modal is triggered '
807    setTimeout(function() {
808        $('.modal-backdrop').not('.modal-stack').css('z-index', (zIndex - 1)+'').addClass('modal-stack');
809    }, 0);
810});
811
812// Scrollbar fix for multiple bootstrap modals
813$(document).on('hidden.bs.modal', '.modal', function () {
814    $('.modal:visible').length && $(document.body).addClass('modal-open');
815});
816
817// --- utilities ---
818
819String.prototype.startsWith = function(searchString) {
820    return this.indexOf(searchString) === 0;
821};
822
823String.prototype.endsWith = function(searchString) {
824    return this.indexOf(searchString, this.length - searchString.length) !== -1;
825};
826
827var Utils = {
828
829    Storage: {
830        set: function(key, value) {
831            localStorage[key] = value;
832        },
833        get: function(key) {
834            return localStorage[key];
835        },
836        remove: function(key) {
837            localStorage.removeItem(key);
838        },
839        test: function(){
840            var result = true;
841            try {
842                localStorage.setItem('localStorage', 'test');
843                if (localStorage.getItem('localStorage') !== 'test') {
844                    result = false;
845                }
846                localStorage.removeItem('localStorage');
847            } catch (e) {
848                result = false;
849            }
850            return result;
851        }
852    },
853
854    RFObjectProp: {
855        GOTOURL  : "g",
856        FIELDS   : "f",
857        NAME     : "n",
858        DISPNAME : "t",
859        VALUE    : "v",
860        TYPE     : "t",
861        GROUPS   : "g",
862        INSTANCES: "i",
863        MATCHES  : "o",
864        NOTE     : "n"
865    },
866
867    passwordStarChar: '&#x25cf;',
868
869    // User/Groups permissions
870    // =====================================================
871    // Convert from sync_selector to DB flags
872    serveronly_convert: {0: null, 1:true, 2:false},
873    // =====================================================
874    // Convert from perm_selector to DB flags
875    ismanager_convert    : {0: false, 1:false, 2:false, 3:true},
876    readonly_convert     : {0: null,  1:true,  2:false, 3:false},
877    showpasswords_convert: {0: null,  1:false, 2:true,  3:true},
878    // =====================================================
879
880    in_array: function(value, array) {
881        return !!~array.indexOf(value);
882    },
883
884    xReplace: function(str) {
885        for (var i = 1; i < arguments.length; i++) {
886            str = str.split('%'+i).join(arguments[i]);
887        }
888        return str;
889    },
890
891    escapeHtml: function(text) {
892        var map = {
893            '&': '&amp;',
894            '<': '&lt;',
895            '>': '&gt;',
896            '"': '&quot;',
897            "'": '&#039;'
898        };
899        return (text+'').replace(/[&<>"']/g, function(m) { return map[m]; });
900    },
901
902    stripTags: function(str) {
903        return $('<div>'+str+'</div>').text();
904    },
905
906    escapeRegExp : function(str) {
907        return (str+'').replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
908    },
909
910    isValidEmail: function(email) {
911        var checkEmailRegexp = /^([a-z0-9_\-+']+\.)*[a-z0-9_\-+']+@([a-z0-9\-]*[a-z0-9]\.)+[a-z]{2,}$/i;
912        if (RFO.RFO_CUSTOM_MODE) {
913            // RFO_CUSTOM_MODE: email with domain without dot is considered as valid
914            checkEmailRegexp = /^([a-z0-9_\-+']+\.)*[a-z0-9_\-+']+@[a-z0-9]*([a-z0-9]+[\-.])*[a-z0-9]+$/i;
915        }
916        email = (email || '').trim();
917        return email.length <= 75 &&
918            (checkEmailRegexp).test(email) &&
919            !email.match(/^[.@+]/) &&
920            !email.match(/\+\+/);
921    },
922
923    isValidPhone: function(phone) {
924        var phonedigits = phone.replace(/[^0-9]/g, '');
925        var phonefixed = phone.replace(/[^\+0-9]/g, '');
926        return (/^[\+0-9\ \-\(\)\.]+$/i).test(phone) && (/^\+[0-9]+$/i).test(phonefixed) && phonedigits.length>=9;
927    },
928
929    isAllowedPhone: function(phone) {
930        var phonefixed = phone.replace(/[^\+0-9]/g, '');
931        if ((/^\+254/i).test(phonefixed)) {
932            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Kenya");
933        }
934        if ((/^\+420/i).test(phonefixed)) {
935            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Czech Republic");
936        }
937        if ((/^\+375/i).test(phonefixed)) {
938            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Belarus");
939        }
940        if ((/^\+63/i).test(phonefixed)) {
941            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Philippines");
942        }
943        if ((/^\+65/i).test(phonefixed)) {
944            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Singapore");
945        }
946        if ((/^\+7/i).test(phonefixed)) {
947            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Russia/Kazakhstan");
948        }
949        if ((/^\+84/i).test(phonefixed)) {
950            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Vietnam");
951        }
952        if ((/^\+86/i).test(phonefixed)) {
953            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "China");
954        }
955        if ((/^\+90/i).test(phonefixed)) {
956            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Turkey");
957        }
958        if ((/^\+94/i).test(phonefixed)) {
959            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Sri Lanka");
960        }
961        if ((/^\+961/i).test(phonefixed)) {
962            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Lebanon");
963        }
964        if ((/^\+965/i).test(phonefixed)) {
965            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Kuwait");
966        }
967        if ((/^\+994/i).test(phonefixed)) {
968            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Azerbaijan");
969        }
970        if ((/^\+996/i).test(phonefixed)) {
971            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Kyrgyzstan");
972        }
973        if ((/^\+998/i).test(phonefixed)) {
974            return Utils.xReplace(_STR.ERR_COUNTRY_SMS_NOT_SUPPORTED, "Uzbekistan");
975        }
976        return true;
977    },
978
979    isValidCompanyPhone: function(phone) {
980        var phonedigits = phone.replace(/[^0-9]/g, '');
981        return (/^[\+0-9\ \-\(\)\.\,ext]+$/i).test(phone) && phonedigits.length>=6;
982    },
983
984    // simple formatting, just add " " between the country code and the rest of the number. For +1 and +7 also format number as +1 (XXX) XXX-XXXX
985    // County codes list is taken from Wikipedia: https://en.wikipedia.org/wiki/List_of_country_calling_codes
986    formatPhoneNumber: function(phone) {
987        phone = phone.replace(/[^\+0-9]/g, '');
988        if (!phone || phone[0] !== '+') {
989            return phone;
990        }
991        var match;
992        if (phone.substring(0, 2) === '+1' && (match = phone.match(/^\+1(\d\d\d)(\d\d\d)(.+)$/)) !== null) {
993            return '+1 ('+match[1]+') '+match[2]+'-'+match[3];
994        }
995        if (phone.substring(0, 2) === '+7' && (match = phone.match(/^\+7(\d\d\d)(\d\d\d)(.+)$/)) !== null) {
996            return '+7 ('+match[1]+') '+match[2]+'-'+match[3];
997        }
998        if (
999            (match = phone.match(/^(\+\d\d)(.+)$/)) !== null &&
1000            Utils.in_array(match[1], ['+20','+27','+28','+30','+31','+32','+33','+34','+36','+39','+40','+41','+43','+44','+45','+46','+47','+48','+49','+51','+52','+53','+54','+55','+56','+57','+58','+60','+61','+62','+63','+64','+65','+66','+81','+82','+83','+84','+86','+89','+90','+91','+92','+93','+94','+95','+98'])
1001        ) {
1002            return match[1]+' '+match[2];
1003        }
1004        if ((match = phone.match(/^(\+\d\d\d)(.+)$/)) !== null) {
1005            return match[1]+' '+match[2];
1006        }
1007        return phone;
1008    },
1009
1010    parseUrlQuery: function() {
1011        var data = {};
1012        var urlParts= [];
1013        [location.hash, location.search].forEach(function (urlPart) {
1014            if (urlPart) {
1015                urlParts.push(urlPart.substring(1));
1016            }
1017        });
1018        urlParts.join('&').replace(/\+/g, ' ').split('&').forEach(function (pair) {
1019            if (pair) {
1020                var param = pair.split('=');
1021                data[param[0]] = param[1] !== undefined ? decodeURIComponent(param[1]) : true;
1022            }
1023        });
1024        return data;
1025    },
1026
1027    // returns the byte length of an utf8 string
1028    byteLength: function(str) {
1029        // see description of the algorithm: http://stackoverflow.com/questions/5515869/string-length-in-bytes-in-javascript
1030        var s = str.length;
1031        for (var i = str.length-1; i >= 0; i--) {
1032            var code = str.charCodeAt(i);
1033            if (code > 0x7f && code <= 0x7ff) {
1034                s++;
1035            } else if (code > 0x7ff && code <= 0xffff) {
1036                s+=2;
1037            }
1038            if (code >= 0xDC00 && code <= 0xDFFF) {
1039                i--; //trail surrogate
1040            }
1041        }
1042        return s;
1043    },
1044
1045    // counting UTF-8 characters
1046    countChars: function(str) {
1047        return Array.from(str).length;
1048    },
1049
1050    // fixing charAt() to support non-Basic-Multilingual-Plane (BMP) characters
1051    fixedCharAt: function(str, idx) {
1052        // see description of the algorithm: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charAt#fixing_charat_to_support_non-basic-multilingual-plane_bmp_characters
1053        var ret = '';
1054        str += '';
1055        var end = str.length;
1056        var surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
1057        while ((surrogatePairs.exec(str)) != null) {
1058            var li = surrogatePairs.lastIndex;
1059            if (li - 2 < idx) {
1060                idx++;
1061            } else {
1062                break;
1063            }
1064        }
1065        if (idx >= end || idx < 0) {
1066            return '';
1067        }
1068        ret += str.charAt(idx);
1069        if (/[\uD800-\uDBFF]/.test(ret) && /[\uDC00-\uDFFF]/.test(str.charAt(idx + 1))) {
1070            // go one further, since one of the "characters" is part of a surrogate pair
1071            ret += str.charAt(idx + 1);
1072        }
1073        return ret;
1074    },
1075
1076    // returns the max chars length from utf8 string limited by bytes length
1077    maxCharsLength: function(str, bytesLimit) {
1078        var chars = 0;
1079        var s = 0;
1080        var cnt = Utils.countChars(str);
1081        for (var i = 0; i < cnt; i++) {
1082            s += Utils.byteLength(Utils.fixedCharAt(str, i));
1083            if (s > bytesLimit) {
1084                break;
1085            }
1086            chars++;
1087        }
1088        return chars;
1089    },
1090
1091    filterBadChars: function(str) {
1092        return (str+'').replace(/[\:\*\?\"\<\>\|\\\/\x00-\x1F]/g, '');
1093    },
1094
1095    checkIfRfDataItemNameIsInvalid: function (name) {
1096        if (!name) {
1097            return _STR.ERR_NAME_CANNOT_BE_EMPTY;
1098        }
1099        if (name[0] === ' ') {
1100            return _STR.ERR_NAME_CANNOT_START_WITH_SPACES;
1101        }
1102        if (name[0] === '.') {
1103            return _STR.ERR_NAME_CANNOT_START_WITH_PERIODS;
1104        }
1105        if (name[name.length - 1] === '.') {
1106            return _STR.ERR_NAME_CANNOT_FINISH_WITH_PERIODS;
1107        }
1108        if (Utils.byteLength(name) > 255) {
1109            return _STR.ERR_TOO_LONG_NAME;
1110        }
1111        if (/[\\/:*?"<>|]/.test(name)) {
1112            return _STR.ERR_NAME_CANNOT_CONTAIN_CHARS;
1113        }
1114        return false;
1115    },
1116
1117    normalizeAndCheckName: function (name, type) {
1118        var normalizedName = '';
1119        var sErrMsg = Utils.checkIfRfDataItemNameIsInvalid(name);
1120        if (!sErrMsg) {
1121            normalizedName = RfapiJS.utils.NormalizeAndCheckFileName(name);
1122            if (normalizedName === undefined) {
1123                sErrMsg = _STR.ERR_NAME_CANNOT_CONTAIN_CHARS;
1124            } else if (normalizedName === RFO.ENT_ADSYNC_GROUP) {
1125                switch (type) {
1126                    case 'group':  sErrMsg = _STR.ERR_WRONG_GROUPNAME_SPECIAL;         break;
1127                    case 'share':  sErrMsg = _STR.ERR_WRONG_SHAREDFOLDER_NAME_SPECIAL; break;
1128                    case 'folder': sErrMsg = _STR.ERR_WRONG_FOLDER_NAME_SPECIAL;       break;
1129                }
1130            } else if (Utils.in_array(type, ['group', 'share'])) {
1131                if (normalizedName.length < RFO.GROUP_NAME_MIN) {
1132                    sErrMsg = Utils.xReplace(_STR.ERR_NAME_WRONG_LENGTH, RFO.GROUP_NAME_MIN, RFO.GROUP_NAME_MAX);
1133                } else {
1134                    var maxCharsLength = Utils.maxCharsLength(normalizedName, RFO.GROUP_NAME_MAX);
1135                    if (Utils.countChars(normalizedName) > maxCharsLength) {
1136                        sErrMsg = Utils.xReplace(_STR.ERR_NAME_WRONG_LENGTH, RFO.GROUP_NAME_MIN, maxCharsLength);
1137                    }
1138                }
1139            }
1140        }
1141        return { name: normalizedName, error: sErrMsg };
1142    },
1143
1144    makeValidFileName: function (name) {
1145        return RfapiJS.utils.MakeValidFileName(name, '_', 200);
1146    },
1147
1148    basename: function(path) {
1149        var parts = path.split('/');
1150        return parts[parts.length-1];
1151    },
1152
1153    nameWithoutExt: function(name) {
1154        var pos = name.lastIndexOf('.');
1155        if (pos != -1 && pos != 0) {
1156            name = name.substr(0, pos);
1157        }
1158        return name;
1159    },
1160
1161    encrypt: function(text, key) {
1162        return RfapiJS.utils.EncryptAESPBKDF2ToBase64String(text, key);
1163    },
1164
1165    decrypt: function(text, key) {
1166        return RfapiJS.utils.DecryptAESPBKDF2FromBase64String(text, key);
1167    },
1168
1169    unescape: function(hex) {
1170        return unescape(hex);
1171    },
1172
1173    escape: function(bin) {
1174        return escape(bin);
1175    },
1176
1177    base64Encode: function(str) {
1178        try {
1179            var data = window.btoa(unescape(encodeURIComponent(str)));
1180        } catch (e) {
1181            return false;
1182        }
1183        return data;
1184    },
1185
1186    base64Decode: function(str) {
1187        try {
1188            var data = decodeURIComponent(escape(window.atob(str)));
1189        } catch (e) {
1190            return false;
1191        }
1192        return data;
1193    },
1194
1195    parseJSON: function(str) {
1196        try {
1197            var jsonData = JSON.parse(str);
1198        } catch (e) {
1199            return false;
1200        }
1201        return jsonData;
1202    },
1203
1204    beautifyJSON: function(str) {
1205        if (str === '') {
1206            return '';
1207        }
1208        var jsonObj = Utils.parseJSON(str);
1209        if (jsonObj !== false) {
1210            return JSON.stringify(jsonObj, null, 4);
1211        }
1212        return false;
1213    },
1214
1215    parseCSV: function(str) {
1216        str = (str || '').trim() + "\r\n";
1217
1218        var arr = [];
1219        var quote = false;  // true means we're inside a quoted field
1220        var groupData = false; // true means we're inside a groupData field (marked with <>) (example: GroupName<Server,Limited>)
1221
1222        // iterate over each character, keep track of current row and column (of the returned array)
1223        var row, col, c;
1224        for (row = col = c = 0; c < str.length; c++) {
1225            var cc = str[c], nc = str[c+1];        // current character, next character
1226            arr[row] = arr[row] || [];             // create a new row if necessary
1227            arr[row][col] = arr[row][col] || '';   // create a new column (start with empty string) if necessary
1228
1229            // If the current character is a quotation mark, and we're inside a
1230            // quoted field, and the next character is also a quotation mark,
1231            // add a quotation mark to the current column and skip the next character
1232            if (cc === '"' && quote && nc === '"') { arr[row][col] += cc; ++c; continue; }
1233
1234            // If it's just one quotation mark, begin/end quoted field
1235            if (cc === '"') { quote = !quote; continue; }
1236
1237            if (cc === '<' && !groupData) { groupData = true; }
1238            if (cc === '>' && groupData) { groupData = false; }
1239
1240            // If it's a comma and we're not in a quoted field or a groupData field, move on to the next column
1241            if ((cc === ',' || cc === ';' || cc === '\t') && !quote && !groupData) { ++col; continue; }
1242
1243            // If it's a newline and we're not in a quoted field, move on to the next
1244            // row and move to column 0 of that new row
1245            if (cc === '\r' && nc === '\n' && !quote) { ++row; col = 0; ++c; continue; }
1246            if ((cc === '\r' || cc === '\n') && !quote) { ++row; col = 0; continue; }
1247
1248            // Otherwise, append the current character to the current column
1249            arr[row][col] += cc;
1250        }
1251        return arr;
1252    },
1253
1254    parseINI: function(str) {
1255        var regex = {
1256            section: /^\s*\[\s*([^\]]*)\s*\]\s*$/,
1257            param: /^\s*([\w\.\-\_]+)\s*=\s*(.*?)\s*$/,
1258            comment: /^\s*;.*$/
1259        };
1260        var value = {};
1261        var lines = str.split(/\r\n|\r|\n/);
1262        var section = null;
1263        var match;
1264        lines.forEach(function(line) {
1265            if (!regex.comment.test(line)) {
1266                if ((match = line.match(regex.param)) !== null) {
1267                    if (section) {
1268                        value[section][match[1]] = match[2];
1269                    } else {
1270                        value[match[1]] = match[2];
1271                    }
1272                } else if ((match = line.match(regex.section)) !== null) {
1273                    value[match[1]] = {};
1274                    section = match[1];
1275                } else if (line.length === 0 && section) {
1276                    section = null;
1277                }
1278            }
1279        });
1280        return value;
1281    },
1282
1283    stringifyINI: function(value, delimiter) {
1284        var delim = delimiter || '\r\n';
1285        // Recursively walks through each nodes and all subnodes within it
1286        // and creates appropriate sections combining the names from names of all subnodes.
1287        var walk = function(result, value, upperLevel) {
1288            var section = [];
1289            for (var p in value) {
1290                if (value.hasOwnProperty(p)) {
1291                    var v = value[p];
1292                    if (typeof v === 'object') {
1293                        section.push(delim + '[' + upperLevel + p + ']');
1294                        walk(section, v, upperLevel + p);
1295                        continue;
1296                    }
1297                    v = [].concat(v);
1298                    for (var i = 0; i < v.length; i++) {
1299                        result.push(p.trim() + '=' + v[i]);
1300                    }
1301                }
1302            }
1303            result.push.apply(result, section);
1304        };
1305        var result = [];
1306        walk(result, value, '');
1307        return result.join(delim).trim();
1308    },
1309
1310    humanReadableSize: function(iSize) {
1311        var sSize;
1312        iSize = parseInt(iSize, 10);
1313        if (iSize < 1000) {
1314            sSize = iSize + 'b';
1315        } else if (iSize < 1024*100) {
1316            sSize = (iSize/1024).toFixed(0) + 'Kb';
1317        } else {
1318            sSize = (iSize/1024/1024).toFixed(1) + 'Mb';
1319        }
1320        return sSize;
1321    },
1322
1323    getErrorDescription: function(errObject) {
1324        var errorMessage = '';
1325        // retry-after error
1326        if (errObject.Code && Utils.in_array(errObject.Code, [403, 503])) {
1327            if (errObject.HttpRequest && errObject.HttpRequest.getResponseHeader('retry-after')) {
1328                errorMessage = _STR.ERR_RETRY_LATER;
1329            } else {
1330                errorMessage = Utils.stripTags(errObject.Description);
1331            }
1332        // wrong params
1333        } else if (errObject.Code === RfapiJS.RfErrType.RfErr_WrongParams) {
1334            errorMessage = Utils.stripTags(errObject.Description);
1335        // connection error
1336        } else if (errObject.Code === 0) {
1337            errorMessage = _STR.ERR_NO_CONNECTION;
1338        // other errors
1339        } else {
1340            if (errObject.Description) {
1341                errorMessage += Utils.stripTags(errObject.Description)+" ";
1342            }
1343            if (errObject.oException && errObject.oException.sibError) {
1344                errorMessage += "("+Utils.stripTags(errObject.oException.sibError)+"): ";
1345            }
1346            if (errObject.oException && errObject.oException.description) {
1347                errorMessage += Utils.stripTags(errObject.oException.description)+" ";
1348            }
1349            if (errorMessage === "Server Error " && errObject.HttpRequest.responseText) {
1350                // Display the message returned by server, not generic "Server Error"
1351                errorMessage = Utils.stripTags(errObject.HttpRequest.responseText)+" ";
1352            }
1353            if (errorMessage === '') {
1354                errorMessage = _STR.ERR_UNEXPECTED;
1355            }
1356        }
1357        return errorMessage;
1358    },
1359
1360    getLocationHash: function() {
1361        // Firefox bug: location.hash returns url-decoded hash.
1362        // So we have to take the whole location string, then get the hash part of it.
1363        var locationParts = location.toString().split('#');
1364        return (typeof locationParts[1] !== 'undefined' ? '#'+locationParts[1] : '');
1365    },
1366
1367    // simple check for IP range format like 192.168.1.1-192.168.1.100
1368    checkRegularIPRangeFormat: function(ip) {
1369        return ip.match(/^(\d+\.){3}\d+\-(\d+\.){3}\d+$/);
1370    },
1371
1372    // check IP matching the format: x.y.z.w OR x.y.z.* OR x.y.z1-z2.*
1373    checkIPMask: function(ip) {
1374        if (ip === '') {
1375            return true;
1376        }
1377        // returns:
1378        // - false if not valid
1379        // - 1 if valid regular number
1380        // - 2 if valid interval or *
1381        var checkInterval = function(part) {
1382            if (part !== '*') {
1383                var ipPartsArr = part.split('-');
1384                var re = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)$/;
1385                if (ipPartsArr.length > 2 || !ipPartsArr[0].match(re)) {
1386                    return false;
1387                }
1388                if (ipPartsArr.length === 1) {
1389                    return 1;
1390                }
1391                if (!ipPartsArr[1].match(re) || parseInt(ipPartsArr[0], 10) > parseInt(ipPartsArr[1], 10)) {
1392                    return false;
1393                }
1394            }
1395            return 2;
1396        };
1397        var checkIpAddress = function(ipAddr) {
1398            var ipParts = ipAddr.split('.');
1399            if (ipParts.length !== 4) {
1400                return false;
1401            }
1402            while (ipParts.length > 0) {
1403                var check = checkInterval(ipParts.shift());
1404                if (check === false) {
1405                    return false;
1406                }
1407                if (check === 2) { // this is an interval - all other parts must be '*'
1408                    for (var i = 0; i < ipParts.length; i++) {
1409                        if (ipParts[i] !== '*') {
1410                            return false;
1411                        }
1412                    }
1413                }
1414            }
1415            return true;
1416        };
1417        if (Utils.checkRegularIPRangeFormat(ip)) {
1418            var ipAddress = ip.split('-');
1419            if (!checkIpAddress(ipAddress[0]) || !checkIpAddress(ipAddress[1]) || Utils.ipToInt(ipAddress[0]) > Utils.ipToInt(ipAddress[1])) {
1420                return false;
1421            }
1422        } else {
1423            return checkIpAddress(ip);
1424        }
1425        return true;
1426    },
1427
1428    // Merge overlapping intervals
1429    // Note: [1,2] and [3,4] consider as overlapping, so produce [1,4]
1430    mergeIntervals: function(intervals) {
1431        if (!intervals.length) {
1432            return intervals;
1433        }
1434        // sort intervals based on start value
1435        intervals = intervals.sort(function(a, b){
1436            return a[0] - b[0];
1437        });
1438        // push the first interval onto the stack
1439        var stack = [intervals[0]];
1440        // start from next interval, merge if necessary
1441        for (var i = 1; i < intervals.length; i++) {
1442            // get the top of the stack
1443            var top = stack[stack.length-1];
1444            // if current interval is not overlapping with the stack top, push it to the stack
1445            if (top[1]+1 < intervals[i][0]) {
1446                stack.push(intervals[i]);
1447            // update end value of previous interval range if the current interval end value is larger
1448            } else if (top[1] < intervals[i][1]) {
1449                stack[stack.length-1][1] = intervals[i][1];
1450            }
1451        }
1452        return stack;
1453    },
1454
1455    // convert dotted IP address to integer
1456    ipToInt: function(ip) {
1457        var d = ip.split('.');
1458        return ((((((+d[0])*256)+(+d[1]))*256)+(+d[2]))*256)+(+d[3]);
1459    },
1460
1461    // convert integer to dotted IP address
1462    intToIP: function(integer) {
1463        var part1 = integer & 255;
1464        var part2 = ((integer >> 8) & 255);
1465        var part3 = ((integer >> 16) & 255);
1466        var part4 = ((integer >> 24) & 255);
1467        return part4 + "." + part3 + "." + part2 + "." + part1;
1468    },
1469
1470    // unmask IP range: 3.7.10-20.* => [3.7.10.0, 3.7.20.255]
1471    unmaskIPRange: function(ip) {
1472        var intervalStart, intervalEnd;
1473        var ipParts = ip.split('-');
1474        if (ipParts.length > 1) {
1475            if (Utils.checkRegularIPRangeFormat(ip)) {
1476                intervalStart = ipParts[0];
1477                intervalEnd = ipParts[1];
1478            } else {
1479                var dotPos0 = ipParts[0].lastIndexOf('.');
1480                var dotPos1 = ipParts[1].indexOf('.');
1481                intervalStart = ipParts[0] + (dotPos1 !== -1 ? ipParts[1].substring(dotPos1) : '');
1482                intervalEnd = (dotPos0 !== -1 ? ipParts[0].substring(0, dotPos0+1) : '') + ipParts[1];
1483            }
1484        } else {
1485            intervalStart = intervalEnd = ip;
1486        }
1487        intervalStart = intervalStart.split('*').join('0');
1488        intervalEnd = intervalEnd.split('*').join('255');
1489        return [intervalStart, intervalEnd];
1490    },
1491
1492    // mask IP by interval
1493    // 1.2.3.6 - 10.10.200.0 => 1.2.3.6 - 10.10.200.0
1494    // 3.7.10.0 - 3.7.20.255 => 3.7.10-20.*
1495    maskIP: function(interval) {
1496        if (interval[0] === interval[1]) {
1497            return interval[0];
1498        }
1499        var getParts = function(ip){
1500            return ip.split('.').map(function(val){
1501                return parseInt(val, 10);
1502            });
1503        };
1504        var parts1 = getParts(interval[0]);
1505        var parts2 = getParts(interval[1]);
1506        for (var i = 0; i < 4; i++) {
1507            if (parts1[i] !== parts2[i]) {
1508                var bValid = true;
1509                for (var k = i+1; k < 4; k++) {
1510                    if (parts1[k] !== 0 || parts2[k] !== 255) {
1511                        bValid = false;
1512                        break;
1513                    } else {
1514                        parts1[k] = parts2[k] = '*';
1515                    }
1516                }
1517                if (bValid) {
1518                    parts1[i] += '-' + parts2[i];
1519                    return parts1.join('.').replace('.0-255', '.*');
1520                }
1521                break;
1522            }
1523        }
1524        return interval.join('-');
1525    },
1526
1527    getFormattedIPRangesArray: function(text) {
1528        var formattedIP = [];
1529        $.each(text.split(';'), function(){
1530            if (Utils.checkRegularIPRangeFormat(this)) {
1531                formattedIP.push(this.replace('-', ' - '));
1532            } else {
1533                formattedIP.push(this);
1534            }
1535        });
1536        return formattedIP;
1537    },
1538
1539    compileIPRanges: function(IPRangesText) {
1540        var intervalsMasked = [];
1541        var intervalsUnmasked = [];
1542        var allowedIP = (IPRangesText || '').trim()
1543            .replace(/\s*\-\s*/g, '-')
1544            .replace(/[;,\s]/g, "\n")
1545            .replace(/\n+/g, "\n")
1546            .split("\n");
1547        if (allowedIP.length && allowedIP[0] === '') {
1548            return '';
1549        }
1550        for (var i = 0; i < allowedIP.length; i++) {
1551            if (!Utils.checkIPMask(allowedIP[i])) {
1552                return false;
1553            }
1554            var ranges = Utils.unmaskIPRange(allowedIP[i]);
1555            intervalsUnmasked.push([Utils.ipToInt(ranges[0]), Utils.ipToInt(ranges[1])]);
1556        }
1557        // union and sort IP intervals
1558        intervalsUnmasked = Utils.mergeIntervals(intervalsUnmasked);
1559        // mask IP intervals
1560        $.each(intervalsUnmasked, function(){
1561            var interval = this;
1562            interval[0] = Utils.intToIP(interval[0]);
1563            interval[1] = Utils.intToIP(interval[1]);
1564            intervalsMasked = intervalsMasked.concat(Utils.maskIP(interval));
1565        });
1566        return intervalsMasked.join(';');
1567    },
1568
1569    isValidURI: function (uri) {
1570        var url;
1571        try {
1572            url = new URL(uri);
1573            return url.protocol === "http:" || url.protocol === "https:";
1574        } catch (_) {
1575            // fallback for IE11 and URL without prepended http|https://
1576            var pattern = new RegExp('^(https?:\\/\\/)?'+                                       // protocol
1577            '((([a-zа-яёА-ЯЁ\\d]([a-zа-яёА-ЯЁ\\d-]*[a-zа-яёА-ЯЁ\\d])*)\\.)+[a-zа-яёА-ЯЁ]{2,}|'+ // domain name
1578            '((\\d{1,3}\\.){3}\\d{1,3}))'+                                                      // OR ip (v4) address
1579            '(\\:\\d+)?(\\/[-a-zа-яёА-ЯЁ\\d%_.~+]*)*'+                                          // port and path
1580            '(\\?[;&a-zа-яёА-ЯЁ\\d%_.~+=-]*)?'+                                                 // query string
1581            '(\\#[-a-zа-яёА-ЯЁ\\d_]*)?$','i');                                                  // fragment locator
1582            return !!pattern.test(uri);
1583        }
1584    },
1585
1586    updateStrength: function(passwd) {
1587        var colorImgHeight = 500;
1588        var scorebar_height = $('#scorebar')[0].offsetHeight;
1589
1590        var strength = RfapiJS.SecurityScore.CalcPasswordStrength(passwd);
1591        if (passwd.length != 0 && strength==0) {
1592            strength = 1; // we display 0 with gray color, so need to change to something else.
1593        }
1594        var pos = (colorImgHeight/100)*(strength > 100 ? 100 : strength);
1595        if (pos > colorImgHeight) {
1596            pos = colorImgHeight;
1597        }
1598        $('#scorebar').css('background-position', '0 '+(scorebar_height-pos)+'px');
1599
1600        var $text = $('#scorebar_text');
1601        if (passwd.length === 0) {
1602            $text.css('visibility', 'hidden');
1603        } else {
1604            $text.css('visibility', 'visible');
1605        }
1606
1607        if (passwd.length < 8) {
1608            $text.html(_STR.RFJS_TEXT_PASSWORD_TOO_SHORT);
1609            $text.css('color', '#AD0C0C');
1610        } else if (strength <= 25) {
1611            $text.html(_STR.RFJS_TEXT_PASSWORD_WEAK);
1612            $text.css('color', '#AD0C0C');
1613        } else if (strength <= 50) {
1614            $text.html(_STR.RFJS_TEXT_PASSWORD_MEDIUM);
1615            $text.css('color', '#DD9900');
1616        } else if (strength <= 75) {
1617            $text.html(_STR.RFJS_TEXT_PASSWORD_GOOD);
1618            $text.css('color', '#339933');
1619        } else {
1620            $text.html(_STR.RFJS_TEXT_PASSWORD_STRONG);
1621            $text.css('color', '#339933');
1622        }
1623    },
1624
1625    indicateError: function($el){
1626        $el.one($el.attr('type') === 'text' ? 'input' : 'change', function(){
1627            $(this).parent().removeClass('has-error');
1628        }).trigger('focus').parent().addClass('has-error');
1629    },
1630
1631    showPopupError: function($el, content, placement, container){
1632        $el.popover({
1633            trigger: 'focus',
1634            html: true,
1635            container: typeof container !== 'undefined' ? container : 'body',
1636            placement: placement,
1637            content: content
1638        }).one('hide.bs.popover', function(){
1639            $el.parent().removeClass('has-error');
1640        }).one('hidden.bs.popover', function(){
1641            $el.popover('destroy');
1642        }).one('input', function(){
1643            $el.popover('hide');
1644        }).trigger('focus').popover('show').parent().addClass('has-error');
1645    },
1646
1647    calcReadOnly: function(node){
1648        return node.Shared && (node.Shared.readOnly || !node.Shared.showPasswords) && !node.Shared.isManager && !node.Shared.isAdmin;
1649    },
1650
1651    calcForbidIP: function(node){
1652        return node.Shared && !node.Shared.accountInfo.isAllowedIP && !node.Shared.isAdmin && (node.Shared.company || !node.Shared.isManager);
1653    },
1654
1655    calcSharedIconOverlayClass: function(node){
1656        var shared = node.s || node.Shared || node.isShared || node.granted || node.received;
1657        var bEnterprise = node.company || (shared && shared.company);
1658        var bManager = node.isManager || (shared && shared.isManager) || node.isAdmin || (shared && shared.isAdmin) || node.granted;
1659        var bReadOnly = node.readOnly || (shared && shared.readOnly);
1660        if (shared) {
1661            if (bEnterprise) {
1662                if (bManager) {
1663                    return 'entManager';
1664                } else if (bReadOnly) {
1665                    return 'entLimited';
1666                } else {
1667                    return 'entRegular';
1668                }
1669            } else {
1670                if (bManager) {
1671                    return 'consGranted';
1672                } else if (bReadOnly) {
1673                    return 'consLimited';
1674                } else {
1675                    return 'consReceived';
1676                }
1677            }
1678        }
1679        return '';
1680    },
1681
1682    confirm: function(html, okCallback){
1683        var $dialog = $('#confirmDialog');
1684        $dialog.find('.modal-body').html(html);
1685        $dialog.one('shown.bs.modal', function(){
1686            $dialog.find('._ok').off('click').one('click', function(){
1687                $dialog.one('hidden.bs.modal', function(){
1688                    if (typeof okCallback === 'function') {
1689                        okCallback($dialog);
1690                    }
1691                }).modal('hide');
1692            });
1693        }).modal('show');
1694    },
1695
1696    confirmExt: function(params){
1697        var $dialog = $('#confirmDialog').clone();
1698        var $buttonTpl = $('<button type="button" class="btn"></button>');
1699        var $footer = $dialog.find('.modal-footer').html('');
1700        if (params.wide) {
1701            $dialog.find('.modal-dialog').removeClass('modal-sm');
1702        }
1703        if (params.title) {
1704            $dialog.find('.modal-title').html(params.title);
1705        }
1706        if (params.html) {
1707            $dialog.find('.modal-body').html(params.html);
1708        }
1709        if (params.footnote) {
1710            $dialog.find('.modal-content').append('<div class="modal-footnote">' + params.footnote + '</div>');
1711        }
1712        (params.buttons || []).forEach(function (button) {
1713            $footer.append($buttonTpl.clone()
1714                .addClass(button.class || '')
1715                .text(button.text || '')
1716                .one('click', function () {
1717                    $dialog.one('hidden.bs.modal', function(){
1718                        $dialog.remove();
1719                        if (typeof button.click === 'function') {
1720                            button.click();
1721                        }
1722                    }).modal('hide');
1723                })
1724            );
1725        });
1726        $dialog.modal('show');
1727    },
1728
1729    resolveNamesConflict: function($dialog, $folderName, bGroup) {
1730        var folderName = RfapiJS.utils.NormalizeUnicodeStr($folderName.val().trim());
1731        $('#resolveNamesConflictDialog').one('show.bs.modal', function(){
1732            var $rDialog = $(this);
1733            $rDialog.find('.result-error').html('').hide();
1734            $rDialog.find('.has-error').removeClass('has-error');
1735            $rDialog.find('._name').text(folderName);
1736            $('#resolveName').val(folderName);
1737            if (bGroup) {
1738                $rDialog.find('._groupText').show();
1739                $rDialog.find('._sharedfolderText').hide();
1740                $rDialog.find('._groupName').text(_STR.LABEL_GROUP_NAME);
1741                $rDialog.find('._optionText').text(_STR.TEXT_RESOLVE_NAMES_CONFLICT_OPT1);
1742            } else {
1743                $rDialog.find('._groupText').hide();
1744                $rDialog.find('._sharedfolderText').show();
1745                $rDialog.find('._groupName').text(_STR.LABEL_SHAREDFOLDER_NAME);
1746                $rDialog.find('._optionText').text(_STR.TEXT_RESOLVE_NAMES_CONFLICT_OPT1_1);
1747            }
1748            var $rType = $rDialog.find('input[name="resolveType"]').on('click', function(){
1749                if ($(this).val() === '0') {
1750                    $rDialog.find('._groupName').show();
1751                    $rDialog.find('._folderName').hide();
1752                } else {
1753                    $rDialog.find('._groupName').hide();
1754                    $rDialog.find('._folderName').show();
1755                }
1756            });
1757            $($rType[0]).trigger('click'); // select first option
1758            $rDialog.find('._ok').off('click').on('click', function(){
1759                $rDialog.find('.result-error').html('').hide();
1760                var $newName = $('#resolveName');
1761                var rType = $rDialog.find('input[name="resolveType"]:checked').val();
1762                var nameData = Utils.normalizeAndCheckName($newName.val(), rType === '0' ? (bGroup ? 'group' : 'shared') : 'folder');
1763                if (nameData.error) {
1764                    $rDialog.find('.result-error').html(nameData.error).show();
1765                    Utils.indicateError($newName);
1766                    return false;
1767                }
1768                var newName = nameData.name;
1769                if (rType === '0') {
1770                    // create shared account
1771                    $rDialog.off('hidden.bs.modal').one('hidden.bs.modal', function(){
1772                        $dialog.modal('show');
1773                        $folderName.val(newName);
1774                        $dialog.find('._ok').trigger('click');
1775                    }).modal('hide');
1776                } else {
1777                    // rename folder
1778                    var dataType = RfapiJS.EnRfDataItemType.RfItemType_Folder;
1779                    var pathFrom = '/' + folderName;
1780                    var pathTo = '/' + newName;
1781                    $rDialog.off('hidden.bs.modal');
1782                    startProcessing($rDialog);
1783                    RFMgr.MoveObject(pathFrom, dataType, pathTo, dataType,
1784                        //  ************  OK (AnswerCallBack) ************
1785                        function(){
1786                            stopProcessing($rDialog, function(){
1787                                // create shared account
1788                                $dialog.modal('show');
1789                                $dialog.find('._ok').trigger('click');
1790                            });
1791                        },
1792                        //  ***********  ERROR (ErrorCallBack) ***********
1793                        function(errObject){
1794                            if (errObject.Code && errObject.Code === 401) {
1795                                //access denied error (session expired)
1796                                doLogout(RFO.ROOT_PATH + 'login?relogin');
1797                            } else {
1798                                stopProcessing($rDialog, function(){
1799                                    var errorMsg = Utils.stripTags(errObject.Description);
1800                                    showResultMsg(errorMsg, true, false, function(){
1801                                        $rDialog.one('hidden.bs.modal', function(){
1802                                            $dialog.modal('show');
1803                                        }).modal('show');
1804                                    });
1805                                });
1806                            }
1807                        }
1808                    );
1809                }
1810            });
1811        }).one('shown.bs.modal', function(){
1812            $(this).find('._autofocus').trigger('focus');
1813        }).one('hidden.bs.modal', function(){
1814            $dialog.modal('show');
1815        }).modal('show');
1816    },
1817
1818    extractDomain: function(url) {
1819        return (url.indexOf("://") > -1 ? url.split('/')[2] : url.split('/')[0]).split(':')[0];
1820    },
1821
1822    checkBlackWhiteListPolicies: function(blacklist, whitelist, checkedURL) {
1823        var bValidUrl = true;
1824        var matchToList = function(list, checkedURL){
1825            var isMatch = false;
1826            $.each(list, function(){
1827                var curUrl = this.toLowerCase();
1828                if (curUrl.startsWith('http://') || curUrl.startsWith('https://')) {
1829                    // exact match
1830                    if (checkedURL.startsWith(this)) {
1831                        isMatch = true;
1832                    }
1833                } else {
1834                    // domain match
1835                    if (Utils.extractDomain(checkedURL).endsWith(curUrl)) {
1836                        isMatch = true;
1837                    }
1838                }
1839            });
1840            return isMatch;
1841        };
1842        if (blacklist.length > 0) {
1843            bValidUrl = !matchToList(blacklist, checkedURL);
1844        }
1845        if (whitelist.length > 0 && bValidUrl) {
1846            bValidUrl = matchToList(whitelist, checkedURL);
1847        }
1848        return bValidUrl;
1849    },
1850
1851    getRFType: function(path) {
1852        var parts = Utils.basename(path).split('.');
1853        var fileext = parts[parts.length-1].toLowerCase();
1854        var rfTypes = {
1855            'rfp': RfapiJS.EnRfDataItemType.RfItemType_Login,
1856            'rfb': RfapiJS.EnRfDataItemType.RfItemType_Bookmark,
1857            'rfq': RfapiJS.EnRfDataItemType.RfItemType_SearchCard,
1858            'rfx': RfapiJS.EnRfDataItemType.RfItemType_BlockingPasscard,
1859            'rft': RfapiJS.EnRfDataItemType.RfItemType_Identity,
1860            'rfc': RfapiJS.EnRfDataItemType.RfItemType_Contact,
1861            'rfn': RfapiJS.EnRfDataItemType.RfItemType_Safenote
1862        };
1863        return typeof rfTypes[fileext] === 'undefined' ? RfapiJS.EnRfDataItemType.RfItemType_Undefined : rfTypes[fileext];
1864    },
1865
1866    getRFName: function(path) {
1867        var basename = Utils.basename(path);
1868        var parts = basename.split('.');
1869        parts.pop();
1870        if (parts.length) {
1871            return parts.join('.');
1872        }
1873        return basename;
1874    },
1875
1876    getRFDirPath: function(path) {
1877        return path.replace(/\/[^\/]*$/, '');
1878    },
1879
1880    createCompany: function(sCompanyAdmin, sCompanyName, nCompanyUsers, okCallback, errorCallback) {
1881        // define policies to save on create company
1882        var policiesINIBase64 = undefined;
1883        if (RFO.POLICIES_WIN_TYPES) {
1884            var policyData = {};
1885            $.each(RFO.POLICIES_WIN_TYPES, function(section, sectionWinData){
1886                $.each(sectionWinData, function(name, policyWinData){
1887                    if (typeof policyWinData['onCreateCompany'] !== 'undefined') {
1888                        policyData[section] = policyData[section] || {};
1889                        policyData[section][name] = policyWinData['onCreateCompany'];
1890                    }
1891                });
1892            });
1893            if (!$.isEmptyObject(policyData)) {
1894                policiesINIBase64 = Utils.base64Encode(Utils.stringifyINI(policyData));
1895            }
1896        }
1897        RFMgr.CreateCompany(sCompanyName, okCallback,
1898            //  ***********  ERROR (ErrorCallBack) ***********
1899            function (errObject) {
1900                var errorMessage = '';
1901                var bCompanyExists = false;
1902                // wrong params
1903                if (errObject.oException && errObject.oException.fields) {
1904                    var errMsgList = [];
1905                    var fields = errObject.oException.fields;
1906                    for (var i = 0; i < fields.length; i++) {
1907                        if (fields[i].name === 'name' && fields[i].exists) {
1908                            errMsgList.push(Utils.xReplace(_STR.ERR_COMPANY_EXIST, sCompanyName));
1909                            bCompanyExists = true;
1910                        } else {
1911                            errMsgList.push(Utils.getErrorDescription(errObject));
1912                            break;
1913                        }
1914                    }
1915                    errorMessage = errMsgList.join('<br>');
1916                } else {
1917                    errorMessage = Utils.getErrorDescription(errObject);
1918                }
1919                if (errorMessage !== '') {
1920                    errorCallback(errorMessage, bCompanyExists);
1921                }
1922            },
1923            undefined,
1924            sCompanyAdmin,
1925            undefined,
1926            undefined,
1927            policiesINIBase64,
1928            nCompanyUsers
1929        );
1930    },
1931
1932    getBrowserTimezone: function() { // return a string like "GMT+06:00"
1933        var nOffset = (new Date()).getTimezoneOffset();
1934        var nHours = parseInt(Math.abs(nOffset / 60));
1935        var sHours = ('00' + nHours).slice(-2); // left padding with zeros
1936        var nMinutes = Math.abs(nOffset % 60);
1937        var sMinutes = ('00' + nMinutes).slice(-2);
1938        return 'GMT' + (nOffset < 0 ? '+' : '-') + sHours + ':' + sMinutes;
1939    },
1940
1941    isDstObserved: function() {
1942        var now = new Date();
1943        var jan = new Date(now.getFullYear(), 0, 1).getTimezoneOffset();
1944        var jul = new Date(now.getFullYear(), 6, 1).getTimezoneOffset();
1945        return Math.max(jan, jul) != now.getTimezoneOffset();
1946    },
1947
1948    getSecurityScoreLevel: function(score){
1949        if (score > 75) {
1950            return 'excellent';
1951        } else if (score > 50) {
1952            return 'good';
1953        } else if (score > 25) {
1954            return 'average';
1955        } else if (score > 0) {
1956            return 'low';
1957        } else {
1958            return 'undefined';
1959        }
1960    },
1961
1962    getPasswordStrengthLevel: function(pwdStrength){
1963        if (pwdStrength > 3) {
1964            return 'strong';
1965        } else if (pwdStrength > 2) {
1966            return 'good';
1967        } else if (pwdStrength > 1) {
1968            return 'medium';
1969        } else {
1970            return 'weak';
1971        }
1972    },
1973
1974    setDelayedHoverHint: function($this, content, extraParams) {
1975        var mouseEnterId;
1976        var params = $.extend({
1977            html: true,
1978            trigger: 'manual',
1979            container: 'body',
1980            sanitize: false
1981        }, extraParams);
1982        if (content) {
1983            params.content = content;
1984        }
1985        $this.data('bHideTooltip', true).popover('destroy').popover(params).on('mouseenter', function(){
1986            mouseEnterId = setTimeout(function(){
1987                $this.popover('show');
1988            }, 200);
1989        }).on('mouseleave', function(){
1990            if (mouseEnterId) {
1991                clearTimeout(mouseEnterId);
1992            }
1993            setTimeout(function(){
1994                if ($this.data('bHideTooltip')) {
1995                    $this.popover('hide');
1996                }
1997            }, 200);
1998        });
1999        $this.data("bs.popover").tip().on('mouseenter', function(){
2000            $this.data('bHideTooltip', false);
2001        }).on('mouseleave', function(){
2002            $this.data('bHideTooltip', true).popover('hide');
2003        });
2004    },
2005
2006    setSecurityStatsHint: function($el, aSecurityStats, aDialogInfoData) {
2007        var totalLogins = 0;
2008        var $securityStatsPreview = $('#securityStatsPreview').clone().removeAttr('id').removeClass('hidden');
2009        if (aSecurityStats) {
2010            totalLogins = aSecurityStats['weak'] + aSecurityStats['medium'] + aSecurityStats['good'] + aSecurityStats['excellent'];
2011        }
2012        if (!totalLogins) {
2013            // show 'no data available'
2014            $securityStatsPreview.find('._data').remove();
2015            Utils.setDelayedHoverHint($el, $securityStatsPreview[0].outerHTML);
2016            return false;
2017        }
2018        var setValues = function($content){
2019            $content.find('._total').text(totalLogins);
2020            $content.find('._weak').text(aSecurityStats['weak']);
2021            $content.find('._medium').text(aSecurityStats['medium']);
2022            $content.find('._good').text(aSecurityStats['good']);
2023            $content.find('._strong').text(aSecurityStats['excellent']);
2024            $content.find('._reused').text(aSecurityStats['reused']);
2025            $content.find('._duplicates').text(aSecurityStats['duplicates']);
2026        };
2027        setValues($securityStatsPreview);
2028        $securityStatsPreview.find('._nodata').remove();
2029        Utils.setDelayedHoverHint($el, $securityStatsPreview[0].outerHTML);
2030        $el.on('click', function(){
2031            $el.data('bHideTooltip', true).popover('hide');
2032            if (aDialogInfoData) {
2033                aDialogInfoData.$dialog.one('show.bs.modal', function(){
2034                    aDialogInfoData.$dialog.find('.title').html(Utils.xReplace(_STR.CL_SECURITY_STATS_INFO_TITLE, '<span>' + Utils.escapeHtml(aDialogInfoData.name) + '</span>'));
2035                    aDialogInfoData.$dialog.find('.secScoreMask').text(aDialogInfoData.value + '%');
2036                    aDialogInfoData.$dialog.find('.modal-header').removeClass().addClass('modal-header ' + Utils.getSecurityScoreLevel(aDialogInfoData.value));
2037                    setValues(aDialogInfoData.$dialog);
2038                }).modal('show');
2039            }
2040        });
2041        return true;
2042    },
2043
2044    showSecurityStatsDetailedInfo: function(aDialogInfoData, errorCallback) {
2045        var $dialog = $('#securityStatsInfoDetail');
2046        $dialog.find('.modal-dialog').removeClass('modal-lg').addClass('modal-sm');
2047        $dialog.find('.modal-header').hide();
2048        $dialog.one('show.bs.modal', function(){
2049            startProcessing($dialog);
2050        }).one('shown.bs.modal', function(){
2051            RFMgr.GetAccountSecurityInfo(
2052                {
2053                    accountId:                 aDialogInfoData.accountId,
2054                    countReceivedFiles:        true,
2055                    countGroups:               true,
2056                    countRegularSharedFolders: true
2057                },
2058                function (data) {
2059                    if (aDialogInfoData.name === '') {
2060                        $dialog.find('._name').hide();
2061                    } else {
2062                        $dialog.find('._name').text(aDialogInfoData.name).show();
2063                    }
2064                    $dialog.find('.modal-header').removeClass().addClass('modal-header ' + Utils.getSecurityScoreLevel(data.securityStats.score));
2065                    $dialog.find('.secScoreMask').text(data.securityStats.score + '%');
2066                    $dialog.find('._all').text(data.items.all.length);
2067                    $dialog.find('._reused').text(data.items.reused.length);
2068                    $dialog.find('._duplicates').text(data.items.duplicates.length);
2069
2070                    var createRows = function(items, $row, groupId){
2071                        var rowsContent = '';
2072                        $.each(items, function(i, itemData){
2073                            var m, $curRow = $row.clone().removeClass('hidden');
2074                            var rootName = itemData.accountInfo.accountId !== RFO.LOGGED_ACCOUNT_ID ? ('/' + itemData.accountInfo.name) : '';
2075                            var url = RFO.ROOT_PATH+'account#all_files::1.' + encodeURIComponent(rootName + itemData.path);
2076                            $curRow.addClass('_group_row_'+groupId).attr('data-sort-group', groupId).find('.c_name').text(Utils.getRFName(itemData.path)).attr('href', url);
2077                            if (itemData.gotoUrl && (m = itemData.gotoUrl.match(/^http(s)?:\/\/([^\/:]+)/))) {
2078                                $curRow.find('.c_icon').attr('src', '/icons/?def=/css/images/new/icon_rfp.png&size=16&domain=' + m[2]);
2079                            }
2080                            $curRow.find('._c_url').text(itemData.gotoUrl);
2081                            $curRow.find('._c_password').attr('data-value', itemData.password).html(itemData.password.replace(/./g, Utils.passwordStarChar));
2082                            $curRow.find('._c_strength').addClass(Utils.getPasswordStrengthLevel(itemData.pwdStrength)).attr('data-sort-value', itemData.pwdStrength);
2083                            $curRow.find('._c_location').text(rootName + Utils.getRFDirPath(itemData.path));
2084                            $curRow.find('.c_age').text(itemData.modTime ? Math.floor((Date.now() / 1000 - itemData.modTime) / 86400) : '');
2085                            rowsContent += $curRow[0].outerHTML;
2086                        });
2087                        return rowsContent;
2088                    };
2089
2090                    $.each(['all', 'reused', 'duplicates'], function(i, type){
2091                        var $table = $('#tab-'+type).find('.modalTable');
2092                        var $row = $table.find('._row.hidden');
2093                        var rows = $row[0].outerHTML;
2094                        if (type === 'all') {
2095                            rows += createRows(data.items[type], $row, 0);
2096                        } else {
2097                            var $rowGroup = $table.attr('data-sort-in-groups', 1).find('._row_group.hidden');
2098                            rows += $rowGroup[0].outerHTML;
2099                            $.each(data.items[type], function(i, itemGroupData){
2100                                var $curRowGroup = $rowGroup.clone().removeClass('hidden').attr('data-group-id', i*2).attr('data-sort-group', i*2-1);
2101                                $curRowGroup.find('._group_url').text(itemGroupData.gotoUrl);
2102                                rows += $curRowGroup[0].outerHTML + createRows(itemGroupData.items, $row, i*2);
2103                            });
2104                        }
2105                        $table.find('tbody').html(rows);
2106                    });
2107
2108                    $dialog.find('.togglePass').off('click').on('click', function(){
2109                        var $this = $(this);
2110                        $this.toggleClass($this.attr('data-toggle'));
2111                        $this.closest('table').find('._c_password:visible').each(function(){
2112                            var $thisPass = $(this);
2113                            if ($this.hasClass('rfoicon-hide') && $thisPass.attr('data-value')) {
2114                                $thisPass.text($thisPass.attr('data-value'));
2115                            } else {
2116                                $thisPass.html($thisPass.attr('data-value').replace(/./g, Utils.passwordStarChar));
2117                            }
2118                        });
2119                    });
2120
2121                    $dialog.find('._row_group').off('click').on('click', function(){
2122                        var $thisRow = $(this);
2123                        var $thisIcon = $thisRow.find('.rfoicon');
2124                        $thisIcon.toggleClass($thisIcon.attr('data-toggle'));
2125                        $thisRow.closest('table').find('._group_row_'+$thisRow.attr('data-group-id')).each(function(){
2126                            if ($thisIcon.hasClass('rfoicon-expand-more')) {
2127                                $(this).show();
2128                            } else {
2129                                $(this).hide();
2130                            }
2131                        });
2132                    });
2133
2134                    $dialog.find('.modal-dialog').removeClass('modal-sm').addClass('modal-lg');
2135                    $dialog.find('.modal-header').show();
2136                    stopProcessing($dialog, undefined, true);
2137
2138                    var $tabContent = $dialog.find('.tab-content');
2139                    $dialog.find('.nav-tabs li').removeClass('active').find('a').one('shown.bs.tab', function(e){
2140                        var $table = $tabContent.find('.tableScrollable:visible');
2141                        $table.trigger('adjustHeader').trigger('reset.sortable', [$table.find('tbody tr')])
2142                              .find('.togglePass').removeClass('rfoicon-hide').addClass('rfoicon-show');
2143                    }).eq(0).tab('show');
2144                },
2145                function (errObject) {
2146                    if (errObject.Code && errObject.Code === 401) {
2147                        //access denied error (session expired)
2148                        doLogout(RFO.ROOT_PATH + 'login?relogin');
2149                    } else {
2150                        errorCallback(errObject);
2151                    }
2152                }
2153            );
2154        }).modal('show');
2155    },
2156
2157    enableSearch: function($search, $rows, $paginator){
2158        var $doFilterBtn = $search.parent().find('span');
2159        var classOn = $doFilterBtn.attr('data-on');
2160        var classOff = $doFilterBtn.attr('data-off');
2161        var filterTimerId;
2162        $search.val('').off('input').on('input', function(){
2163            clearTimeout(filterTimerId);
2164            filterTimerId = setTimeout(function(){
2165                var searchText = $search.val().trim().toLowerCase();
2166                if (searchText !== '') {
2167                    $rows.each(function(){
2168                        var bShow = false;
2169                        var $thisRow = $(this);
2170                        $thisRow.find('._filter').each(function(){
2171                            if (~$(this).text().trim().toLowerCase().indexOf(searchText)) {
2172                                bShow = true;
2173                                return false; // break each loop
2174                            }
2175                        });
2176                        if (bShow) {
2177                            $paginator ? $thisRow.addClass('paginable') : $thisRow.show();
2178                        } else {
2179                            $paginator ? $thisRow.removeClass('paginable') : $thisRow.hide();
2180                        }
2181                    });
2182                    $doFilterBtn.removeClass(classOn).addClass(classOff);
2183                } else {
2184                    $paginator ? $rows.addClass('paginable') : $rows.show();
2185                    $doFilterBtn.removeClass(classOff).addClass(classOn);
2186                }
2187                if ($paginator) {
2188                    $paginator.trigger('init.paginator');
2189                }
2190            }, 100);
2191        });
2192        $doFilterBtn.off('click').on('click', function(){
2193            if ($doFilterBtn.hasClass($doFilterBtn.attr('data-off'))) {
2194                $search.val('');
2195            }
2196            $search.trigger('input');
2197        }).removeClass(classOff).addClass(classOn);
2198    },
2199
2200    enablePaginator: function($paginator, afterDisplayItems) {
2201        var $sourceTable = $('.' + $paginator.attr('data-source')).data('paginator', $paginator);
2202        var $visibleRows = $();
2203        var nShownItems = parseInt($paginator.attr('data-items-count'));
2204        var $paginableRows, nFrom;
2205
2206        var $btnBegin  = $paginator.find('.paginator-begin');
2207        var $btnPrev   = $paginator.find('.paginator-prev');
2208        var $btnNext   = $paginator.find('.paginator-next');
2209        var $btnEnd    = $paginator.find('.paginator-end');
2210        var $shownText = $paginator.find('.paginator-text');
2211
2212        var displayItems = function() {
2213            var nTo = Math.min(nFrom + nShownItems, $paginableRows.length);
2214            $visibleRows.hide();
2215            $visibleRows = $();
2216            for (var index = 0; index < $paginableRows.length; ++index) {
2217                if (index >= nFrom && index < nTo) {
2218                    $paginableRows.eq(index).show();
2219                    $visibleRows.push($paginableRows[index]);
2220                }
2221            }
2222            $([$btnBegin[0], $btnPrev[0]]).prop('disabled', nFrom === 0);
2223            $([$btnNext[0], $btnEnd[0]]).prop('disabled', nTo === $paginableRows.length);
2224            $shownText.html(Utils.xReplace(_STR.PAGINATOR_TEXT, Math.min(nTo, nFrom + 1), nTo, $paginableRows.length));
2225            if (typeof afterDisplayItems === 'function') {
2226                afterDisplayItems();
2227            }
2228        };
2229
2230        // by default - paginate through all rows (when using filters this count will be changed)
2231        $sourceTable.find('._row').addClass('paginable').hide();
2232
2233        $btnBegin.off('click').on('click', function() {
2234            nFrom = 0;
2235            displayItems();
2236        });
2237        $btnPrev.off('click').on('click', function() {
2238            nFrom = Math.max(0, nFrom - nShownItems);
2239            displayItems();
2240        });
2241        $btnNext.off('click').on('click', function() {
2242            nFrom = Math.min(nFrom + nShownItems, $paginableRows.length - 1);
2243            displayItems();
2244        });
2245        $btnEnd.off('click').on('click', function() {
2246            nFrom = Math.floor(($paginableRows.length-1)/nShownItems) * nShownItems;
2247            displayItems();
2248        });
2249
2250        $paginator.off('init.paginator').on('init.paginator', function() {
2251            $paginableRows = $sourceTable.find('.paginable');
2252            nFrom = 0;
2253            displayItems();
2254        }).trigger('init.paginator');
2255    },
2256
2257    enableScrollPaginator: function($sourceTable, initialRowsArray, afterDisplayItems) {
2258        $sourceTable.data('paginator', 1);
2259
2260        var rowsArray = initialRowsArray;
2261        var $sourceTableBody = $sourceTable.find('tbody');
2262        var $heightRow = $('<tr><td colspan="'+$sourceTable.find('thead th, thead td').length+'" style="padding:0;margin:0;border:none;"></td></tr>');
2263        var $heightRowTD = $heightRow.find('td');
2264
2265        var $scrollWrapper = $sourceTable.parent();
2266        var $scroller = $scrollWrapper.find('._scroller');
2267        if (!$scroller.length) {
2268            $scroller = $('<div class="_scroller"></div>').css('float', 'left').prependTo($scrollWrapper);
2269        }
2270
2271        var nShownItems = 1;
2272        var rowHeight = 0;
2273        var bUpdateAll = true;
2274
2275        var fillTable = function(nFrom, posOffset) {
2276            // add 1 row after the visible interval of rows in container - used for smooth scrolling
2277            var nTo = Math.min(nFrom + nShownItems + 1, rowsArray.length);
2278            var aChangedRowsIndexes = [];
2279            if (bUpdateAll || !posOffset || Math.abs(posOffset) >= nShownItems) {
2280                // update all rows
2281                var resultHTML = '';
2282                for (var i = nFrom; i < nTo; i++) {
2283                    resultHTML += "<tr class='_row' data-i='"+i+"'></tr>";
2284                    aChangedRowsIndexes.push(i);
2285                }
2286                // bug in IE11: if set an empty html body, then the inner html of the $heightRow object is removed
2287                $sourceTableBody.html(resultHTML || '<tr></tr>').prepend($heightRow);
2288                bUpdateAll = false;
2289            } else {
2290                // update only needed rows
2291                if (posOffset < 0) {
2292                    $sourceTableBody.find('._row').slice(posOffset).each(function(idx){
2293                        var changeIndex = nFrom - posOffset - idx - 1;
2294                        $(this).attr('data-i', changeIndex).insertAfter($heightRow);
2295                        aChangedRowsIndexes.push(changeIndex);
2296                    });
2297                } else {
2298                    $sourceTableBody.find('._row').slice(0, posOffset).each(function(idx){
2299                        var changeIndex = nTo - posOffset + idx;
2300                        $(this).attr('data-i', changeIndex).appendTo($sourceTableBody);
2301                        aChangedRowsIndexes.push(changeIndex);
2302                    });
2303                }
2304            }
2305            $('.popover').remove(); // remove all popover that could be shown
2306            if (typeof afterDisplayItems === 'function') {
2307                afterDisplayItems(rowsArray, aChangedRowsIndexes);
2308            }
2309            if (rowHeight === 0) {
2310                rowHeight = $sourceTable.find('._row').eq(0).height() || 0;
2311            }
2312        };
2313
2314        if ($sourceTable.attr('data-paginator-items-count')) {
2315            nShownItems = parseInt($sourceTable.attr('data-paginator-items-count'), 10);
2316        } else {
2317            var containerHeight = $(window).height() - $sourceTable.position().top - $('.b-navbar-2').height()*2;
2318            fillTable(0); // it is needed to calc rowHeight
2319            if (rowHeight > 0) {
2320                nShownItems = Math.ceil(containerHeight / rowHeight);
2321            }
2322        }
2323
2324        var nOldPos = 0;
2325        $scrollWrapper.off('scroll.paginator').on('scroll.paginator', function(e){
2326            var scrollTop = $scrollWrapper[0].scrollTop;
2327            var pos = Math.floor(scrollTop / rowHeight);
2328            if (pos > rowsArray.length - nShownItems - 1) {
2329                pos = rowsArray.length - nShownItems - 1;
2330                pos = pos > 0 ? pos : 0;
2331            }
2332            if (pos === nOldPos) {
2333                // scroll event occurs too many times, so we repaint table only if the interval of visible rows changes
2334                // all other steps uses internal browser scrolling process and we can see smooth scrolling effect
2335                return;
2336            }
2337            fillTable(pos, pos - nOldPos);
2338            nOldPos = pos;
2339            $heightRowTD.css('height', (pos * rowHeight)+'px');
2340        });
2341
2342        $sourceTable.off('init.paginator').on('init.paginator', function(e, newRowsArray) {
2343            bUpdateAll = true;
2344            if (newRowsArray) {
2345                rowsArray = newRowsArray;
2346            }
2347            fillTable(0);
2348            nOldPos = 0;
2349            $heightRowTD.css('height', '0px');
2350            $scroller.css('height', (rowsArray.length * rowHeight)+'px');
2351            $scrollWrapper.css('max-height', (nShownItems * rowHeight)+'px').scrollTop(0);
2352        }).trigger('init.paginator');
2353
2354        $sourceTable.trigger('reset.sortable', [initialRowsArray]);
2355    },
2356
2357    enableScrollPaginatorSearch: function($table, $search, rowsArray, okCallback) {
2358        var FILTERED_LIST = rowsArray;
2359        var $doFilterBtn = $search.parent().find('span');
2360        var classOn = $doFilterBtn.attr('data-on');
2361        var classOff = $doFilterBtn.attr('data-off');
2362        var filterTimerId;
2363        $search.val('').off('input').on('input', function(){
2364            clearTimeout(filterTimerId);
2365            filterTimerId = setTimeout(function(){
2366                var searchText = $search.val().trim().toLowerCase();
2367                if (searchText !== '') {
2368                    FILTERED_LIST = rowsArray.filter(function(thisRow){
2369                        return ~thisRow['_data_'].textFilter.indexOf(searchText);
2370                    });
2371                    $doFilterBtn.removeClass(classOn).addClass(classOff);
2372                } else {
2373                    FILTERED_LIST = rowsArray;
2374                    $doFilterBtn.removeClass(classOff).addClass(classOn);
2375                }
2376                if (typeof okCallback === 'function') {
2377                    okCallback(FILTERED_LIST);
2378                }
2379                $table.trigger('sort.sortable', [FILTERED_LIST]).trigger('init.paginator', [FILTERED_LIST]);
2380            }, 100);
2381        });
2382        $doFilterBtn.off('click').on('click', function(){
2383            if ($doFilterBtn.hasClass($doFilterBtn.attr('data-off'))) {
2384                $search.val('');
2385            }
2386            $search.trigger('input');
2387        }).removeClass(classOff).addClass(classOn);
2388    },
2389
2390    virtualList: function(params) {
2391        var $container  = params.container.html('');
2392        var itemHeight = params.itemHeight;
2393        var countInRow  = params.countInRow || 1;
2394        var createItem = params.createItem;
2395
2396        var $scroller = $('<div class="_scroller"></div>').css('float', 'left').prependTo($container);
2397        var $scrollContent = $('<div class="_scrollContent"></div>').appendTo($container);
2398
2399        var mItems = [];
2400        var lastPos = 0;
2401
2402        // Define maximum items that could be visible in scrollable area.
2403        // It can be assumed that number of items filled all window height will be enough.
2404        var maxShownItems = Math.ceil($(window).height() / (itemHeight / countInRow));
2405
2406        var updateViewport = function(from) {
2407            from = typeof from !== 'undefined' ? from : lastPos;
2408            var fragment = document.createDocumentFragment();
2409            var to = Math.min(from + maxShownItems, mItems.length);
2410            for (var i = from; i < to; i++) {
2411                fragment.appendChild(createItem(mItems[i], i));
2412            }
2413            $scrollContent.html('').append(fragment).css('padding-top', (from * itemHeight / countInRow) + 'px');
2414            lastPos = from;
2415        };
2416
2417        return {
2418            UpdateItems: function(items) {
2419                if (items) {
2420                    mItems = items;
2421                    $container.scrollTop(0);
2422                    $scroller.css('height', mItems.length * itemHeight / countInRow);
2423                    var oldPos = -1;
2424                    $container.off('scroll.virtuallist').on('scroll.virtuallist', function() {
2425                        var scroll_top = $container[0].scrollTop;
2426                        var pos = Math.floor(scroll_top / itemHeight) * countInRow;
2427                        if (pos === oldPos) {
2428                            // scroll event occures too much times, so we repaint table only if the interval of visible rows changes
2429                            // all other steps uses internal browser scrolling process and we can see smooth scrolling effect
2430                            return;
2431                        }
2432                        oldPos = pos;
2433                        updateViewport(pos);
2434                    });
2435                }
2436                updateViewport();
2437            }
2438        };
2439    },
2440
2441    processGroupCreate: function(companyId, okCallback) {
2442        var companyPolicies = RFO.ACCOUNT_INFO.companiesPolicies[companyId] ? RFO.ACCOUNT_INFO.companiesPolicies[companyId] : '';
2443        $('#createEditGroupWidget').one('show.bs.modal', function(){
2444            var $this = $(this);
2445            $this.find('form')[0].reset();
2446            $this.find('._caption').html($this.find('._caption').attr('data-caption-create'));
2447            $this.find('.result-error').html('').hide();
2448            $this.find('.has-error').removeClass('has-error');
2449            $this.find('._moreSettings').trigger('collapse');
2450            $this.find('._groupActions').hide();
2451
2452            var $disableAccessFrom = $('#disableAccessFrom');
2453            var $disableAccessFromFieldset = $('.disableAccessFrom');
2454            var $disableAccessFromTypes = $disableAccessFromFieldset.find('input[type="checkbox"]');
2455            var $disableAccessFromHint = $this.find('.disableAccessFromHint');
2456            $disableAccessFrom.prop('checked', false).prop('disabled', false).off('click').on('click', function(){
2457                if ($disableAccessFrom.prop('checked')) {
2458                    $disableAccessFromFieldset.prop('disabled', false);
2459                    $disableAccessFromTypes.prop('disabled', false);
2460                } else {
2461                    $disableAccessFromFieldset.prop('disabled', true);
2462                    $disableAccessFromTypes.prop('checked', false).prop('disabled', true);
2463                }
2464            });
2465            $disableAccessFromHint.hide();
2466            var companyPolicyDisableDevices = [];
2467            if (companyPolicies && companyPolicies.CONFIG && companyPolicies.CONFIG['DisableDevices']) {
2468                companyPolicyDisableDevices = companyPolicies.CONFIG['DisableDevices'].split(' ');
2469            }
2470            $disableAccessFromTypes.each(function(){
2471                var $curDevType = $(this);
2472                if (Utils.in_array($curDevType.val(), companyPolicyDisableDevices)) {
2473                    $curDevType.prop('checked', true).prop('disabled', true);
2474                    $disableAccessFrom.prop('checked', true).prop('disabled', true);
2475                    $disableAccessFromHint.show();
2476                } else {
2477                    $curDevType.prop('checked', false).prop('disabled', false);
2478                }
2479            });
2480            if (!$disableAccessFrom.prop('checked')) {
2481                $disableAccessFrom.prop('checked', true).trigger('click');
2482            } else {
2483                $disableAccessFromFieldset.prop('disabled', false);
2484            }
2485
2486            var $groupAllowedIP = $('#groupAllowedIP');
2487            var bAllowEditIPRanges = !(companyPolicies && companyPolicies.CONFIG && companyPolicies.CONFIG['AllowedIPRanges']);
2488            if (bAllowEditIPRanges) {
2489                $groupAllowedIP.val('').prop('disabled', false).parent().find('label').removeClass('disabled');
2490                $this.find('.allowedIPGroupPolicyHint').hide();
2491            } else {
2492                $groupAllowedIP.val('').prop('disabled', true).parent().find('label').addClass('disabled');
2493                $this.find('.allowedIPGroupPolicyHint').show();
2494            }
2495
2496            $this.find('._ok').off('click').on('click', function () {
2497                var policyArr = {};
2498                var allowedIP = Utils.compileIPRanges($groupAllowedIP.val());
2499                // set 'Allowed IP Addresses' as policy
2500                if (bAllowEditIPRanges && allowedIP) {
2501                    if (!policyArr.CONFIG) {
2502                        policyArr.CONFIG = {};
2503                    }
2504                    policyArr.CONFIG['EnableGroupForIPRanges'] = allowedIP;
2505                }
2506                // set 'Disable Access from device' as policy
2507                var disableDevTypePolicyValue = '';
2508                $disableAccessFromTypes.each(function(){
2509                    var $curDevType = $(this);
2510                    var sDevType = $curDevType.val();
2511                    if (!Utils.in_array(sDevType, companyPolicyDisableDevices) && $curDevType.prop('checked')) {
2512                        disableDevTypePolicyValue += ' ' + sDevType;
2513                    }
2514                });
2515                if (disableDevTypePolicyValue) {
2516                    if (!policyArr.CONFIG) {
2517                        policyArr.CONFIG = {};
2518                    }
2519                    policyArr.CONFIG['DisableGroupForDevices'] = disableDevTypePolicyValue;
2520                }
2521                var policyBase64 = undefined;
2522                if (!$.isEmptyObject(policyArr)) {
2523                    policyBase64 = Utils.base64Encode(Utils.stringifyINI(policyArr));
2524                }
2525                Utils.groupCreate(
2526                    $this,
2527                    companyId,
2528                    policyBase64,
2529                    $groupAllowedIP,
2530                    okCallback,
2531                    true
2532                );
2533            });
2534            $this.find('form').off('submit').on('submit', function(){
2535                $this.find('._ok').trigger('click');
2536                return false;
2537            });
2538            $groupAllowedIP.on('keydown', function(){
2539                $(this).popover('hide');
2540            });
2541        }).one('shown.bs.modal', function(){
2542            $(this).find('._autofocus').trigger('focus');
2543        }).modal('show');
2544    },
2545
2546    groupCreate: function($dialog, companyId, policyBase64, $groupAllowedIP, okCallback, bStopProcessingOnOK) {
2547        var $groupNameEl = $dialog.find('input[name="groupName"]');
2548
2549        var nameData = Utils.normalizeAndCheckName($groupNameEl.val(), 'group');
2550        if (nameData.error) {
2551            $dialog.find('.result-error').html(nameData.error).show();
2552            Utils.indicateError($groupNameEl);
2553            return false;
2554        }
2555        var groupNameElVal = nameData.name;
2556
2557        if ($groupAllowedIP && Utils.compileIPRanges($groupAllowedIP.val()) === false) {
2558            $dialog.find('.result-error').html(_STR.CL_WRONG_IP_FORMAT).show();
2559            Utils.indicateError($groupAllowedIP);
2560            return false;
2561        }
2562
2563        var sync_value = $dialog.find('._sync_selector tr.selected').attr('data');
2564        var perm_value = $dialog.find('._perm_selector tr.selected').attr('data');
2565
2566        var processFunc = function() {
2567            RFMgr.CreateSharedAccount(
2568                //  ************  OK (AnswerCallBack) ************
2569                function (status, groupID) {
2570                    if (bStopProcessingOnOK) {
2571                        stopProcessing($dialog, function(){
2572                            showResultMsg(_STR.CL_USERGROUP_GROUPS_SUCCESS_CREATE, false, undefined, function(){
2573                                if (typeof okCallback === 'function') {
2574                                    okCallback(groupNameElVal, groupID);
2575                                }
2576                            });
2577                        });
2578                    } else {
2579                        if (typeof okCallback === 'function') {
2580                            okCallback(groupNameElVal, groupID);
2581                        }
2582                    }
2583                },
2584                //  ***********  ERROR (ErrorCallBack) ***********
2585                function (errObject) {
2586                    if (errObject.Code && errObject.Code === 401) {
2587                        //access denied error (session expired)
2588                        doLogout(RFO.ROOT_PATH + 'login?relogin');
2589                    } else if (errObject.Code && errObject.Code === RfapiJS.RfErrType.RfErr_AccountExists) {
2590                        stopProcessing($dialog, function(){
2591                            var errorMsg = Utils.xReplace(_STR.ERR_CANNOT_CREATE_GROUP_ALREADY_EXISTS, Utils.escapeHtml(groupNameElVal));
2592                            showResultMsg(errorMsg, true, false, function(){
2593                                $dialog.modal('show');
2594                            });
2595                        });
2596                    } else if (errObject.Code && errObject.Code === RfapiJS.RfErrType.RfErr_FolderExists && !$('#quickstartWidget').hasClass('in')) {
2597                        stopProcessing($dialog, function(){
2598                            Utils.resolveNamesConflict($dialog, $groupNameEl, true);
2599                        });
2600                    } else {
2601                        stopProcessing($dialog, function(){
2602                            var errorMsg = _STR.CL_USERGROUP_GROUPS_ERROR_CREATE + ':<br>' + Utils.stripTags(errObject.Description);
2603                            showResultMsg(errorMsg, true, false, function(){
2604                                $dialog.modal('show');
2605                            });
2606                        });
2607                    }
2608                },
2609                undefined, groupNameElVal, undefined, undefined, companyId,
2610                Utils.readonly_convert[perm_value],          // new flag value
2611                Utils.showpasswords_convert[perm_value],     // new flag value
2612                Utils.serveronly_convert[sync_value],        // new flag value
2613                undefined,
2614                policyBase64,
2615                true
2616            );
2617        };
2618        startProcessing($dialog, function() {
2619            processFunc();
2620        });
2621    },
2622
2623    processAdConnector: function($connectorPage) {
2624        $connectorPage.find('.connector-block').addClass('hidden');
2625        $connectorPage.find('.connector-block.offline').removeClass('hidden').addClass('loader');
2626        RFMgr.HandleADConnectorData(undefined,
2627            function(data){
2628                $connectorPage.find('.loader').removeClass('loader');
2629                data = Utils.parseJSON(data);
2630                if (!data || data.reset || $.isEmptyObject(data.adSyncSettings)) {
2631                    return;
2632                }
2633                $connectorPage.find('.connector-block').removeClass('hidden');
2634                $connectorPage.find('.connector-block.offline').addClass('hidden');
2635            },
2636            function (errObject) {
2637                $connectorPage.find('.loader').removeClass('loader');
2638                if (errObject.Code && errObject.Code === 401) {
2639                    // access denied error (session expired)
2640                    doLogout(RFO.ROOT_PATH + 'login?relogin');
2641                    // code=6: no groups or no AdSyncGroup or no ad_state.json file
2642                } else if (!errObject.Code || errObject.Code !== 6) {
2643                    showResultMsg(Utils.getErrorDescription(errObject), true);
2644                }
2645            }
2646        );
2647    },
2648
2649    processUserCreate: function($dialog, companyId, isAdmin, okCallback) {
2650        var $name = $dialog.find('input[name="userName"]');
2651        var $email = $dialog.find('input[name="userEmail"]');
2652        var name = RfapiJS.utils.NormalizeUnicodeStr($name.val().trim());
2653        var email = $email.val().trim();
2654        if (name.length < RFO.ACCOUNT_NAME_MIN) {
2655            Utils.indicateError($name);
2656            $dialog.find('.result-error').html(Utils.xReplace(_STR.ERR_NAME_WRONG_LENGTH, RFO.ACCOUNT_NAME_MIN, RFO.ACCOUNT_NAME_MAX)).show();
2657            return;
2658        }
2659        var maxCharsLength = Utils.maxCharsLength(name, RFO.ACCOUNT_NAME_MAX);
2660        if (Utils.countChars(name) > maxCharsLength) {
2661            Utils.indicateError($name);
2662            $dialog.find('.result-error').html(Utils.xReplace(_STR.ERR_NAME_WRONG_LENGTH, RFO.ACCOUNT_NAME_MIN, maxCharsLength)).show();
2663            return;
2664        }
2665        if (name !== Utils.filterBadChars(name)) {
2666            Utils.indicateError($name);
2667            $dialog.find('.result-error').html(_STR.ERR_INVALID_USERNAME).show();
2668            return;
2669        }
2670        if (name === RFO.ENT_ADSYNC_USER) {
2671            Utils.indicateError($name);
2672            $dialog.find('.result-error').html(_STR.ERR_WRONG_USERNAME_SPECIAL).show();
2673            return;
2674        }
2675        if (!Utils.isValidEmail(email)) {
2676            Utils.indicateError($email);
2677            $dialog.find('.result-error').html(_STR.ERR_INVALID_EMAIL).show();
2678            return;
2679        }
2680        var processFunc = function() {
2681            var pwd = RfapiJS.utils.generatePassword(12, true, true, true, true, undefined, undefined, false, true, 4);
2682            RFMgr.CreateCompanyMember({
2683                    'memberLogin': email,
2684                    'memberPwd': pwd,
2685                    'memberName': name,
2686                    'memberMail': email,
2687                    'company': companyId,
2688                    'isAdmin': isAdmin,
2689                    'otpOn': undefined
2690                },
2691                //  ************  OK (AnswerCallBack) ************
2692                function() {
2693                    stopProcessing($dialog, function(){
2694                        if (typeof okCallback === 'function') {
2695                            okCallback();
2696                        }
2697                    });
2698                },
2699                //  ***********  ERROR (ErrorCallBack) ***********
2700                function (errObject) {
2701                    if (errObject.oException && errObject.oException.fields) {
2702                        var errMsgList = [];
2703                        var fields = errObject.oException.fields;
2704                        for (var i = 0; i < fields.length; i++) {
2705                            if ((fields[i].name === 'login' || fields[i].name === 'email') && fields[i].exists) {
2706                                errMsgList.push(Utils.xReplace(_STR.ERR_EMAIL_EXISTS1, email));
2707                            }
2708                            if (fields[i].name === 'email' && !fields[i].exists) {
2709                                errMsgList.push(_STR.ERR_INVALID_EMAIL);
2710                            }
2711                            if (fields[i].name === 'name') {
2712                                errMsgList.push(Utils.xReplace(_STR.ERR_NAME_WRONG_LENGTH, RFO.ACCOUNT_NAME_MIN, RFO.ACCOUNT_NAME_MAX));
2713                            }
2714                        }
2715                        stopProcessing($dialog, function(){
2716                            showResultMsg(errMsgList.join('<br>'), true, false, function(){
2717                                $dialog.modal('show');
2718                            });
2719                        });
2720                    } else if (errObject.Code && errObject.Code === 401) {
2721                        //access denied error (session expired)
2722                        doLogout(RFO.ROOT_PATH + 'login?relogin');
2723                    } else {
2724                        stopProcessing($dialog, function(){
2725                            showResultMsg(Utils.getErrorDescription(errObject), true, false, function(){
2726                                $dialog.modal('show');
2727                            });
2728                        });
2729                    }
2730                }
2731            );
2732        };
2733        startProcessing($dialog, function() {
2734            processFunc();
2735        });
2736    },
2737
2738    processUserJoin: function($dialog, COMPANY_INFO, okCallback) {
2739        var errorMsg = '';
2740        $dialog.find('.result-error').html('').hide();
2741        var $joinUserIDEl = $dialog.find('input[name="userJoinID"]');
2742        var joinUserID = $joinUserIDEl.val().trim();
2743        if (joinUserID === '') {
2744            errorMsg = _STR.CL_USERGROUP_USERS_EMPTY_USERID;
2745        } else if (!/^[a-zA-Z0-9_\-@\.\+]+$/.test(joinUserID)) {
2746            errorMsg = _STR.CL_USERGROUP_USERS_INVALID_USERID;
2747        }
2748        if (errorMsg) {
2749            $dialog.find('.result-error').html(errorMsg).show();
2750            Utils.indicateError($joinUserIDEl);
2751            return false;
2752        }
2753        var processFunc = function() {
2754            RFMgr.CompanyAddUser(joinUserID, COMPANY_INFO.companyId,
2755                //  ************  OK (AnswerCallBack) ************
2756                function () {
2757                    stopProcessing($dialog, function(){
2758                        if (typeof okCallback === 'function') {
2759                            okCallback();
2760                        }
2761                    });
2762                },
2763                //  ***********  ERROR (ErrorCallBack) ***********
2764                function (errObject) {
2765                    if (errObject.Code && errObject.Code === 401) {
2766                        //access denied error (session expired)
2767                        doLogout(RFO.ROOT_PATH + 'login?relogin');
2768                    } else {
2769                        var errorMsg = Utils.getErrorDescription(errObject);
2770                        if (errObject.Code && errObject.Code === 403) {
2771                            var errReason = errObject.HttpRequest.getResponseHeader("x-sib-reason");
2772                            if (errReason && errReason === 'all-company-licenses-used') {
2773                                errorMsg = _STR.CL_USERGROUP_USERS_ERROR_REACH_LICENSE_LIMIT;
2774                            } else if (errReason && errReason === 'company-license-expired') {
2775                                errorMsg = Utils.xReplace(_STR.CL_USERGROUP_USERS_ERROR_LICENSE_EXPIRED, COMPANY_INFO.name);
2776                            } else if (errReason && errReason === 'user-not-found') {
2777                                errorMsg = joinUserID.match(/@/) ? _STR.CL_USERGROUP_USERS_EMAIL_NOTFOUND : _STR.CL_USERGROUP_USERS_ID_NOTFOUND;
2778                            } else if (errReason && errReason === 'already-joined') {
2779                                errorMsg = Utils.xReplace(_STR.ERR_ACC_ALREADY_JOINED, joinUserID);
2780                            } else if (errReason && errReason === 'one-company-limit') {
2781                                errorMsg = _STR.CL_USERGROUP_USERS_MULTIPLE_COMPANIES;
2782                            } else if (errReason && errReason === 'redirect') {
2783                                var serverID = Utils.detectServerID(errObject.HttpRequest.getResponseHeader("Location"));
2784                                var helpCenterURL = $('._help_center').attr('href');
2785                                if (helpCenterURL) {
2786                                    errorMsg = serverID
2787                                        ? Utils.xReplace(_STR.CL_USERGROUP_USERS_ERROR_ANOTHER_SERVER, serverID, 'target="_blank" href="' + helpCenterURL + '"')
2788                                        : Utils.xReplace(_STR.CL_USERGROUP_USERS_ERROR_ANOTHER_SERVER1, 'target="_blank" href="' + helpCenterURL + '"');
2789                                } else {
2790                                    errorMsg = serverID
2791                                        ? Utils.xReplace(Utils.stripTags(_STR.CL_USERGROUP_USERS_ERROR_ANOTHER_SERVER), serverID)
2792                                        : Utils.stripTags(_STR.CL_USERGROUP_USERS_ERROR_ANOTHER_SERVER1);
2793                                }
2794                            }
2795                        }
2796                        stopProcessing($dialog, function(){
2797                            showResultMsg(errorMsg, true, false, function(){
2798                                $dialog.modal('show');
2799                            });
2800                        });
2801                    }
2802                }
2803            );
2804        };
2805        startProcessing($dialog, function() {
2806            processFunc();
2807        });
2808    },
2809
2810    includeIntoGroups: function(items, okCallback, errorCallback, progressCallback) {
2811        var promises = [];
2812        var aTotalProgress = {};
2813        var oLastError;
2814        var aErrors = [];
2815        var itemsLength = Object.keys(items).length;
2816        var batch = Utils.createBatchOperations();
2817        $.each(items, function(accountId, recipients){
2818            var DEF = $.Deferred();
2819            promises.push(DEF.promise());
2820            batch.run(function(){
2821                RFMgr.GrantAccount(undefined, accountId, undefined, undefined, undefined, undefined,
2822                    function (reports) {
2823                        reports.forEach(function(report){
2824                            if (report['error']) {
2825                                oLastError = report['error'];
2826                                report['group'] = accountId;
2827                                aErrors.push(report);
2828                            }
2829                        });
2830                        DEF.resolve();
2831                        batch.done();
2832                    },
2833                    function (errObject) {
2834                        if (errObject.Code && errObject.Code === 401) {
2835                            //access denied error (session expired)
2836                            doLogout(RFO.ROOT_PATH + 'login?relogin');
2837                        } else {
2838                            oLastError = errObject;
2839                            DEF.reject();
2840                        }
2841                        batch.done();
2842                    },
2843                    undefined, undefined, undefined, {recipients: recipients},
2844                    function(n) {
2845                        if (typeof progressCallback !== 'undefined') {
2846                            var nTotalProgress = 0;
2847                            aTotalProgress[accountId] = n;
2848                            $.each(aTotalProgress, function(i, progress){
2849                                nTotalProgress += progress;
2850                            });
2851                            progressCallback(nTotalProgress / itemsLength);
2852                        }
2853                    }
2854                );
2855            });
2856        });
2857        $.when.apply($, promises).done(function() {
2858            if (oLastError && typeof errorCallback !== 'undefined') {
2859                errorCallback(oLastError, aErrors);
2860            } else if (typeof okCallback !== 'undefined') {
2861                okCallback();
2862            }
2863        });
2864        $.when.apply($, promises).fail(function() {
2865            if (typeof errorCallback !== 'undefined') {
2866                errorCallback(oLastError, aErrors);
2867            }
2868        });
2869    },
2870
2871    processCompanyLogoChange: function($fileInput, okCallback, errorCallback) {
2872        // Check for the various File API support.
2873        if (!window.File && window.FileReader && window.FileList) {
2874            showResultMsg('The File APIs are not fully supported in this browser.', true);
2875            return false;
2876        }
2877        var $logoPreview = $fileInput.closest('form').find('.companyLogoPreview');
2878        var $clearLogo = $fileInput.closest('form').find('input[name="clearLogo"]');
2879        var logoLimitMb = $fileInput.attr('data-limit');
2880        var file = $fileInput[0].files[0];
2881        var errorHandler = function(errMsg) {
2882            $logoPreview.addClass('hidden');
2883            $clearLogo.prop('checked', true);
2884            showResultMsg(errMsg, true);
2885            if (typeof errorCallback === 'function') {
2886                errorCallback();
2887            }
2888            return false;
2889        };
2890        if (file) {
2891            if (!Utils.in_array(file.type, ['image/jpg', 'image/jpeg', 'image/png', 'image/bmp', 'image/gif'])) {
2892                return errorHandler(_STR.CL_USERGROUP_EDITCOMPANY_BAD_IMAGE);
2893            }
2894            if (file.size > logoLimitMb * 1024 * 1024) {
2895                return errorHandler(Utils.xReplace(_STR.CL_USERGROUP_EDITCOMPANY_BAD_IMAGE_SIZE, logoLimitMb));
2896            }
2897            var reader = new FileReader();
2898            reader.onloadend = function(){
2899                var sLogoBase64 = reader.result;
2900                $('<img/>').on('load', function(){
2901                    $logoPreview.removeClass('hidden').addClass('_changed').find('img').attr({src: sLogoBase64, width: this.naturalWidth, height: this.naturalHeight});
2902                    $clearLogo.prop('checked', false);
2903                    if (typeof okCallback === 'function') {
2904                        okCallback();
2905                    }
2906                }).on('error', function(){
2907                    errorHandler(_STR.CL_USERGROUP_EDITCOMPANY_BAD_IMAGE);
2908                }).attr('src', sLogoBase64);
2909            };
2910            reader.onerror = function(){
2911                errorHandler(_STR.CL_USERGROUP_EDITCOMPANY_BAD_IMAGE);
2912            };
2913            reader.readAsDataURL(file);
2914        }
2915    },
2916
2917    processUpdateCompanyProperties: function(companyId, sCompanyName, sCompanyURL, sLogo, okCallback) {
2918        RFMgr.UpdateCompanyProperties(
2919            {
2920                companyId: companyId,
2921                name: sCompanyName,
2922                url: sCompanyURL,
2923                logo: sLogo
2924            },
2925            function(){
2926                showResultMsg(_STR.CL_USERGROUP_EDITCOMPANY_SUCCESS, false, document.location.href);
2927                if (typeof okCallback === 'function') {
2928                    okCallback();
2929                }
2930            },
2931            function (errObject) {
2932                if (errObject.Code && errObject.Code === 401) {
2933                    // access denied error (session expired)
2934                    doLogout(RFO.ROOT_PATH + 'login?relogin');
2935                } else {
2936                    showResultMsg(_STR.CL_USERGROUP_EDITCOMPANY_ERROR + ':<br>' + Utils.getErrorDescription(errObject), true);
2937                }
2938            }
2939        );
2940    },
2941
2942    typeaheadMemberList: function($el, memberType, bAddCurrentAdmin, onInput, filterFunction) {
2943        var lastQuery, lastUserList;
2944        // create autocomplete combobox
2945        $el.typeahead('destroy').typeahead({
2946            source: function(query, process) {
2947                var processList = function(items) {
2948                    var userList = [];
2949                    items.forEach(function(userData) {
2950                        if (
2951                            userData.name === RFO.ENT_ADSYNC_USER ||
2952                            memberType === 'member' && !userData.company ||
2953                            memberType === 'manager' && !(userData.isAdmin || userData.isManager) ||
2954                            memberType === 'admin' && !userData.isAdmin ||
2955                            typeof filterFunction === 'function' && !filterFunction(userData)
2956                        ) {
2957                            return;
2958                        }
2959                        userList.push({name: userData.name, email: userData.email, isAdmin: userData.isAdmin});
2960                    });
2961                    if (bAddCurrentAdmin && ~((RFO.ACCOUNT_INFO.name || '') + ' ' + RFO.ACCOUNT_INFO.email).toLowerCase().indexOf(query)) {
2962                        userList.push({name: RFO.ACCOUNT_INFO.name || '', email: RFO.ACCOUNT_INFO.email, isAdmin: true});
2963                    }
2964                    process(userList);
2965                };
2966                if (lastQuery === query) {
2967                    processList(lastUserList);
2968                    return;
2969                }
2970                var sortParams = '-isAdmin';
2971                if (memberType === 'manager') {
2972                    sortParams += ',-isManager';
2973                } else if (memberType === 'member') {
2974                    sortParams += ',-company';
2975                }
2976                RFMgr.GetUserKnownRecipients({filter: '%'+query+'%', sort: sortParams},
2977                    function(data) {
2978                        lastQuery = query;
2979                        lastUserList = data.recipients;
2980                        processList(lastUserList);
2981                    },
2982                    function(errObject) {
2983                        if (errObject.Code && errObject.Code === 401) {
2984                            // access denied error (session expired)
2985                            doLogout(RFO.ROOT_PATH + 'login?relogin');
2986                        }
2987                    }
2988                );
2989            },
2990            menuClass: 'typeahead-users',
2991            fitToElement: true,
2992            displayText: function(item) {
2993                if (item.selectedText) {
2994                    return item.selectedText;
2995                }
2996                if (item.name) {
2997                    // space between tags is needed to properly convert html -> text
2998                    return '<div>'+Utils.escapeHtml(item.name)+'</div> <em>'+Utils.escapeHtml(item.email)+'</em>';
2999                } else {
3000                    return '<div>'+Utils.escapeHtml(item.email)+'</div>';
3001                }
3002            },
3003            updater: function(item) {
3004                return {selectedText: item.email};
3005            },
3006            sorter: function(items) {
3007                // response from server already sorted by name+email
3008                return items;
3009            },
3010            onInput: onInput,
3011            showHintOnFocus: true,
3012            selectOnBlur: false,
3013            items: 'all'
3014        }).addClass('_typeahead');
3015    },
3016
3017    detectServerID: function(url) {
3018        var serverID = '';
3019        url += '/';
3020        if (RFO.RFO_SERVER_URLS && ~url.indexOf(RFO.RFO_SERVER_URLS.main || '')) {
3021            serverID = 'US';
3022        } else if (RFO.RFO_SERVER_URLS && ~url.indexOf(RFO.RFO_SERVER_URLS.add_eu || '')) {
3023            serverID = 'EU';
3024        }
3025        return serverID;
3026    },
3027
3028    userFormatDate: function(timestamp, bWithTime) {
3029        if (!timestamp) {
3030            return '';
3031        }
3032        var formattedDT = '';
3033        var d = new Date(timestamp * 1000);
3034        var curOffset = d.getTimezoneOffset();
3035        if (RFO.DATE_FORMATTER_SETTINGS && RFO.DATE_FORMATTER_SETTINGS.offsetMinutes !== -1*curOffset) {
3036            // convert to msec, add local time zone offset, get UTC time in msec
3037            var utc = d.getTime() + (curOffset * 60000);
3038            // create new Date object using supplied offset
3039            d = new Date(utc + (60000 * RFO.DATE_FORMATTER_SETTINGS.offsetMinutes));
3040        }
3041        if (RFO.DATE_FORMATTER_SETTINGS && RFO.DATE_FORMATTER_SETTINGS.format && Utils.in_array(RFO.DATE_FORMATTER_SETTINGS.format, RFO.DATE_FORMATTER_SETTINGS.customFormats)) {
3042            var addZero = function(n) {
3043                return (n < 10 ? '0' : '') + n;
3044            };
3045            formattedDT = RFO.DATE_FORMATTER_SETTINGS.format.replace('yyyy', d.getFullYear()).replace('MM', addZero(d.getMonth() + 1)).replace('dd', addZero(d.getDate()));
3046        } else {
3047            formattedDT = d.toLocaleDateString().replace(/\u200E/g, '');
3048        }
3049        // 2019-Jul-05 Igor: use .replace(/\u200E/g, '') to fix bug in IE11
3050        // see: https://stackoverflow.com/questions/21413757/tolocaledatestring-changes-in-ie11#comment52437188_21413757
3051        return formattedDT + (bWithTime ? ' ' + d.toLocaleTimeString().replace(/\u200E/g, '') 
3051: '');
3052    },
3053
3054    getCompanyInfoByID: function(companies, id){
3055        var companyInfo = {};
3056        var companies = companies || [];
3057        $.each(companies, function(i, company){
3058            if (company.companyId === id) {
3059                companyInfo = company;
3060                return false;
3061            }
3062        });
3063        return companyInfo;
3064    },
3065
3066    // convert number of seconds to the format hh:mm:ss
3067    secondsToHms: function(d){
3068        d = Number(d);
3069        var h = Math.floor(d / 3600);
3070        var m = Math.floor(d % 3600 / 60);
3071        var s = Math.floor(d % 3600 % 60);
3072        return ('0' + h).slice(-2) + ':' + ('0' + m).slice(-2) + ':' + ('0' + s).slice(-2);
3073    },
3074
3075    // converts YYYY-MM-DD 00:00:00 to YYYY-MM-DD;
3076    // else drops seconds YYYY-MM-DD HH:MM, and marks them to display in smaller size.
3077    dateZeroTime: function(nTimestamp) {
3078        if (!nTimestamp) {
3079            return '';
3080        }
3081        var d = new Date(nTimestamp * 1000);
3082        var date = 'Y-m-d'.replace('Y', d.getFullYear()).replace('m', ('0'+(d.getMonth()+1)).slice(-2)).replace('d', ('0'+d.getDate()).slice(-2));
3083        if (d.getHours() + d.getMinutes() + d.getSeconds() !== 0) {
3084            date += '<span class="ztime">' + ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2) + '</span>';
3085        }
3086        return date;
3087    },
3088
3089    getFileIconName: function(nFileType) {
3090        var icon;
3091        switch (nFileType) {
3092            case RfapiJS.EnRfDataItemType.RfItemType_Login            : icon = 'icon_rfp.png'; break;
3093            case RfapiJS.EnRfDataItemType.RfItemType_Bookmark         : icon = 'icon_rfb.png'; break;
3094            case RfapiJS.EnRfDataItemType.RfItemType_SearchCard       : icon = 'icon_rfq.png'; break;
3095            case RfapiJS.EnRfDataItemType.RfItemType_BlockingPasscard : icon = 'icon_rfx.png'; break;
3096            case RfapiJS.EnRfDataItemType.RfItemType_Identity         : icon = 'icon_rft.png'; break;
3097            case RfapiJS.EnRfDataItemType.RfItemType_Contact          : icon = 'icon_rfc.png'; break;
3098            case RfapiJS.EnRfDataItemType.RfItemType_Safenote         : icon = 'icon_rfn.png'; break;
3099            case RfapiJS.EnRfDataItemType.RfItemType_Folder           : icon = 'icon_folder.png'; break;
3100            default: icon = 'icon_unknown.png';
3101        }
3102        return icon;
3103    },
3104
3105    createRFObjectPreview: function(data, $tpl, fieldsLimit) {
3106        var rf_object = Utils.parseJSON(data.rf_object);
3107
3108        // filter wrong json objects
3109        if (rf_object === false) {
3110            return '';
3111        }
3112
3113        var objType = Utils.getRFType(data.path);
3114
3115        // filter blocking passcards
3116        if (objType === RfapiJS.EnRfDataItemType.RfItemType_BlockingPasscard) {
3117            return '';
3118        }
3119
3120        var prop = Utils.RFObjectProp;
3121
3122        var $item = $tpl.find('._itemTpl').clone();
3123        var $itemContent = $item.find('.itemContent');
3124        $item.find('.itemIcon').addClass('icon-' + objType);
3125        $item.find('.itemName').text(Utils.getRFName(data.path));
3126        $item.find('._download').attr('data-path', data.path);
3127
3128        switch (objType) {
3129            case RfapiJS.EnRfDataItemType.RfItemType_Login:
3130            case RfapiJS.EnRfDataItemType.RfItemType_SearchCard:
3131            case RfapiJS.EnRfDataItemType.RfItemType_Bookmark:
3132                // process GoToURL
3133                if (rf_object[prop.GOTOURL] !== '') {
3134                    $('<div>').addClass('gotoURL').text(rf_object[prop.GOTOURL]).appendTo($itemContent);
3135                }
3136                // process other fields
3137                if (rf_object[prop.FIELDS]) {
3138                    for (var i = 0; i < rf_object[prop.FIELDS].length && (!fieldsLimit || (i < fieldsLimit)); i++) {
3139                        var field = rf_object[prop.FIELDS][i],
3140                            type = parseInt(field[prop.TYPE] || 0, 10),
3141                            name = field[prop.NAME],
3142                            value = field[prop.VALUE] || '';
3143                        if (value === '' && fieldsLimit) {
3144                            fieldsLimit++;
3145                            continue;
3146                        }
3147                        if (!Utils.in_array(name, ['Note$', '$Note$'])) {
3148                            if (type === 2 || (type === 0 && name === 'Password$')) {
3149                                var stars = value.replace(/./g, Utils.passwordStarChar);
3150                                var $field = $tpl.find('._passwordTpl').clone().removeClass('_passwordTpl');
3151                                $field.attr('title', name).find('.pwd').attr('data-value', value).html(stars);
3152                                $field.appendTo($itemContent);
3153                            } else {
3154                                $('<div>').attr('title', name).text(value).appendTo($itemContent);
3155                            }
3156                        }
3157                    }
3158                }
3159                break;
3160            case RfapiJS.EnRfDataItemType.RfItemType_Identity:
3161            case RfapiJS.EnRfDataItemType.RfItemType_Contact:
3162                if (rf_object[prop.GROUPS] && rf_object[prop.GROUPS].length && rf_object[prop.GROUPS][0][prop.INSTANCES] && rf_object[prop.GROUPS][0][prop.INSTANCES].length) {
3163                    var group = rf_object[prop.GROUPS][0];
3164                    var instance = group[prop.INSTANCES][0];
3165                    if (group[prop.NAME] !== 'Custom') {
3166                        for (var k = 0; k < instance[prop.FIELDS].length && k < 4; k++) {
3167                            $('<div>').attr('title', instance[prop.FIELDS][k][prop.NAME]).text(instance[prop.FIELDS][k][prop.VALUE]).appendTo($itemContent);
3168                        }
3169                    }
3170                }
3171                break;
3172            case RfapiJS.EnRfDataItemType.RfItemType_Safenote:
3173                var text = rf_object[prop.NOTE] || '';
3174                $('<div class="safenote">').text(text).appendTo($itemContent);
3175                break;
3176        }
3177
3178        return $item.html();
3179    },
3180
3181    suspendRestoreUsers: function (action, items, okCallback, errorCallback, progressCallback, company) {
3182        var promises = [];
3183        var sError = '';
3184        var nCount = 0;
3185        var batch = Utils.createBatchOperations();
3186        $.each(items, function(i, item){
3187            var DEF = $.Deferred();
3188            promises.push(DEF.promise());
3189            batch.run(function(){
3190                RFMgr.SetAccountInfo(
3191                    function () {
3192                        if (typeof progressCallback !== 'undefined') {
3193                            nCount++;
3194                            progressCallback(Math.round(nCount / items.length * 100));
3195                        }
3196                        DEF.resolve();
3197                        batch.done();
3198                    },
3199                    function (errObject) {
3200                        if (errObject.Code && errObject.Code === 401) {
3201                            //access denied error (session expired)
3202                            doLogout(RFO.ROOT_PATH + 'login?relogin');
3203                        } else {
3204                            sError = Utils.getErrorDescription(errObject);
3205                            DEF.reject();
3206                        }
3207                        batch.done();
3208                    },
3209                    item, undefined, undefined, undefined, (action === 'suspend'),
3210                    undefined, undefined, undefined, undefined, c
3210ompany
3211                );
3212            });
3213        });
3214        $.when.apply($, promises).done(function() {
3215            if (typeof okCallback !== 'undefined') {
3216                okCallback();
3217            }
3218        });
3219        $.when.apply($, promises).fail(function() {
3220            if (typeof errorCallback !== 'undefined') {
3221                errorCallback(sError);
3222            }
3223        });
3224    },
3225
3226    arrayToCSV: function(data) {
3227        var result = '';
3228        var columnDelimiter = RFO.CSV_LIST_SEPARATOR || ',';
3229        var lineDelimiter = '\n';
3230        (data || []).forEach(function(item) {
3231            result += item.map(function(val) {
3232                // Avoid CSV Injection in Logs: fields that begin with any of the above symbols [=, -, +, @]
3233                // should be prepended by a single quote or apostrophe (') character.
3234                // Microsoft Excel will preserve data integrity by hiding this character when rendering the spreadsheet.
3235                // So e.g. name =A2+B2 should be exported as '=A2+B2
3236                if (~"=-+@'".indexOf(val[0])) {
3237                    val = "'" + val;
3238                }
3239                return '"' + (val+'').replace(/"/g, '""') + '"';
3240            }).join(columnDelimiter) + lineDelimiter;
3241        });
3242        // need to add UTF-8 BOM for correct display csv in excel
3243        // see: https://technet.microsoft.com/en-us/2yfce773(v=vs.118)
3244        var universalBOM = "\uFEFF";
3245        return universalBOM + result;
3246    },
3247
3248    downloadFile: function(content, fileName, mimeType) {
3249        var a = document.createElement('a');
3250        mimeType = mimeType || 'application/octet-stream';
3251        if (navigator.msSaveBlob) { // IE10+
3252            navigator.msSaveBlob(new Blob([content], {
3253                type: mimeType
3254            }), fileName);
3255        } else if (URL && 'download' in a) { //html5 A[download]
3256            a.href = URL.createObjectURL(new Blob([content], {
3257                type: mimeType
3258            }));
3259            a.setAttribute('download', fileName);
3260            document.body.appendChild(a);
3261            a.click();
3262            document.body.removeChild(a);
3263        } else {
3264            location.href = 'data:application/octet-stream,' + encodeURIComponent(content); // only this mime type is supported
3265        }
3266    },
3267
3268    downloadFileCSV: function(content, fileName) {
3269        Utils.downloadFile(content, fileName, 'text/csv;encoding:utf-8');
3270    },
3271
3272    createBatchOperations: function() {
3273        var nProcessing = 0;
3274        return {
3275            run: function(processingFunction) {
3276                var process = function(){
3277                    if (nProcessing > 100) { // allow no more than 100 simultaneous operations.
3278                        // do not run the next operation, instead delay for N ms.
3279                        setTimeout(process, 10);
3280                        return;
3281                    }
3282                    nProcessing++;
3283                    processingFunction();
3284                };
3285                process();
3286            },
3287            done: function() {
3288                nProcessing--;
3289            }
3290        };
3291    },
3292
3293    createBatchOperationsSequential: function() {
3294        var batches = [];
3295        var bStarted = false;
3296        return {
3297            run: function(processingFunction) {
3298                if (bStarted) {
3299                    batches.push(processingFunction);
3300                } else {
3301                    bStarted = true;
3302                    processingFunction();
3303                }
3304            },
3305            done: function() {
3306                if (batches.length) {
3307                    var processingFunction = batches.shift();
3308                    processingFunction();
3309                }
3310            }
3311        };
3312    },
3313
3314    processChangeEmailPhone: function(params, okCallback, errorCallback) {
3315        var email = params.email;
3316        var phone = params.phone;
3317        if (!email && !phone) {
3318            return;
3319        }
3320        var $verifyEmailAndRedirect = $('#verifyEmailAndRedirect');
3321        $('#confirm_otp_widget').one('show.bs.modal', function(){
3322            var $dialog = $(this);
3323            var errFuncCallback = function (errObject) {
3324                if (errObject.Code && errObject.Code === 401) {
3325                    // access denied error (session expired)
3326                    doLogout(RFO.ROOT_PATH + 'login?relogin');
3327                } else {
3328                    stopProcessing($dialog, function(){
3329                        errorCallback(errObject);
3330                    });
3331                }
3332            };
3333            var sendCodeOkCallback = function(){
3334                $dialog.find('.modal-title').text(email ? _STR.TEXT_CONFIRM_TITLE_EMAIL : _STR.TEXT_CONFIRM_TITLE_PHONE);
3335                $dialog.find('._code_sent').html(
3336                    email
3337                        ? Utils.xReplace(_STR.TEXT_CONFIRM_CODE_SENT_EMAIL, email)
3338                        : Utils.xReplace(_STR.TEXT_CONFIRM_CODE_SENT_PHONE, Utils.formatPhoneNumber(phone))
3339                );
3340                $dialog.find('._confirm_text').html(email ? _STR.TEXT_ENTER_CONFIRM_CODE_EMAIL : _STR.TEXT_ENTER_CONFIRM_CODE_PHONE);
3341                var $verifyCode = $dialog.find('input[name="otp_confirm"]').val('');
3342                var $error = $dialog.find('._error').text('').hide();
3343                $dialog.find('.has-error').removeClass('has-error');
3344                $dialog.find('.yes').off('click').on('click', function(){
3345                    $error.text('').hide();
3346                    $dialog.find('.has-error').removeClass('has-error');
3347                    var verifyCode = $verifyCode.val().trim();
3348                    if (verifyCode === '') {
3349                        Utils.indicateError($verifyCode);
3350                        return false;
3351                    }
3352                    setAccountInfo(verifyCode);
3353                });
3354                $dialog.find('._autofocus').trigger('focus');
3355            };
3356            var sendCodeErrorCallback = function (errObject) {
3357                if (email && errObject.Code === 500 && Utils.stripTags(errObject.Description).match(/421|450|451/)) {
3358                    stopProcessing($dialog, function(){
3359                        sendCodeOkCallback();
3360                        $dialog.find('._error').text(_STR.TEXT_CONFIRM_CODE_SEND_LATER).show();
3361                    }, true);
3362                } else if (errObject.Code === 503 &&
3363                    errObject.HttpRequest &&
3364                    errObject.HttpRequest.getResponseHeader('retry-after') &&
3365                    errObject.HttpRequest.getResponseHeader('reason')
3366                ) {
3367                    stopProcessing($dialog, function(){
3368                        showResultMsg(errObject.HttpRequest.getResponseHeader('reason'), true);
3369                    });
3370                } else {
3371                    errFuncCallback(errObject);
3372                }
3373            };
3374            var setAccountInfo = function(verifyCode){
3375                startProcessing($dialog);
3376                RFMgr.SetAccountInfo(
3377                    function(){
3378                        var successFunc = function(){
3379                            stopProcessing($dialog, function(){
3380                                if ($verifyEmailAndRedirect.length) {
3381                                    document.location.href += '&verifyEmailAndRedirectDone=1';
3382                                } else {
3383                                    okCallback();
3384                                }
3385                            });
3386                        };
3387                        if (params.oldEmail && params.oldEmail !== email) {
3388                            var requestParams = {
3389                                afterChangeEmail: 1,
3390                                oldEmail: params.oldEmail,
3391                                aid: RFO.AID
3392                            };
3393                            $.post(RFO.DIR_REQUEST+'accountChange.php', requestParams, function(){
3394                                successFunc();
3395                            }).fail(function() {
3396                                successFunc();
3397                            });
3398                        } else {
3399                            successFunc();
3400                        }
3401                    },
3402                    function(errObject){
3403                        if (errObject.Code === 403 && errObject.HttpRequest.getResponseHeader("x-sib-reason") === 'expired-code') {
3404                            RFMgr.SendVerificationCode(email ? {email: email} : {phone: phone},
3405                                function () {
3406                                    stopProcessing($dialog, function(){
3407                                        $dialog.find('._error').text(_STR.TEXT_CONFIRM_CODE_EXPIRED).show();
3408                                    }, true);
3409                                },
3410                                sendCodeErrorCallback
3411                            );
3412                        } else {
3413                            stopProcessing($dialog, function(){
3414                                $dialog.find('._error').text(Utils.stripTags(errObject.Description)).show();
3415                            }, true);
3416                        }
3417                    },
3418                    RFO.LOGGED_ACCOUNT_ID,
3419                    undefined/*block*/, undefined/*unblock*/,
3420                    phone === '-' && RFO.ACCOUNT_INFO['altAuth'] === 'sms' ? 'email' : undefined/*otpOn*/,
3421                    undefined/*suspend*/, undefined/*security_score*/, undefined/*logins_count*/, undefined/*totpKey*/, undefined/*totp*/, undefined/*company*/, undefined/*name*/,
3422                    email ? email : undefined/*email*/,
3423                    undefined/*security_stats*/, undefined/*enroll_device*/, undefined/*unenroll_devices*/,
3424                    phone ? (phone === '-' ? '' : phone) : undefined/*phone*/,
3425                    verifyCode/*verificationCode*/
3426                );
3427            };
3428            // the case when the user removes the phone
3429            if (phone === '-') {
3430                setAccountInfo();
3431                return;
3432            }
3433            $dialog.find('.modal-title').text(email ? _STR.TEXT_VERIFY_SENDING_TITLE_EMAIL : _STR.TEXT_VERIFY_SENDING_TITLE_PHONE);
3434            startProcessing($dialog);
3435            RFMgr.SendVerificationCode(email ? {email: email} : {phone: phone},
3436                function () {
3437                    stopProcessing($dialog, function(){
3438                        sendCodeOkCallback();
3439                    }, true);
3440                },
3441                sendCodeErrorCallback
3442            );
3443        }).one('hide.bs.modal', function(){
3444            $verifyEmailAndRedirect.remove();
3445        }).modal('show');
3446    },
3447
3448    browserName: function() {
3449        if (navigator.userAgent.match(/\bAndroid/)) {
3450            return 'Android';
3451        }
3452        if (navigator.userAgent.match(/\b(iPhone|iPad)/) && navigator.userAgent.match(/\bMobile\//)) {
3453            return 'iOS';
3454        }
3455
3456        var match = navigator.userAgent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*\d+/i) || [];
3457        var temp;
3458        if (/trident/i.test(match[1])) {
3459            return 'Internet Explorer';
3460        }
3461        if (match[1] === 'Chrome') {
3462            temp = navigator.userAgent.match(/\b(OPR|Edge)\/\d+/);
3463            if (temp !== null) {
3464                return 'Opera';
3465            }
3466            temp = navigator.userAgent.match(/\b(Edg)\/\d+/);
3467            if (temp !== null) {
3468                return 'Edge';
3469            }
3470            return 'Chrome';
3471        }
3472        return match[1] || '';
3473    },
3474
3475    isStatusInactive: function(timestamp) {
3476        // older than 90 days
3477        return RFO.CUSTOM_VARS && RFO.CUSTOM_VARS.useInactiveStatus && timestamp && (timestamp + 90*24*60*60)*1000 < Date.now();
3478    },
3479
3480    emailVerifyBeforeAction: function(confirmMessage, afterAction) {
3481        Utils.confirm(confirmMessage, function(){
3482            Utils.processChangeEmailPhone({email: RFO.ACCOUNT_INFO.email},
3483                function(){
3484                    RFO.ACCOUNT_INFO.emailVerified = true;
3485                    showResultMsg(_STR.TEXT_SUCCESS_VERIFY_EMAIL, false, undefined, afterAction, undefined, undefined, _STR.BUTTON_CONTINUE);
3486                },
3487                RFMgrCommonErrorHandler.bind(this, null)
3488            );
3489        });
3490    },
3491
3492    createJSONRequest: function(url, post, headers) {
3493        var request = $.Deferred();
3494        $.ajax({
3495            url: url,
3496            method: 'post',
3497            data: JSON.stringify(post || ''),
3498            headers: headers || {},
3499            success: request.resolve,
3500            error: function (e) {
3501                var error = e.responseJSON && e.responseJSON.error || {};
3502                var errorMessage = 'Error: '
3503                    + (e.status ? e.status + ' - ' : '')
3504                    + (error.code ? '[' + error.code + '] - ' : '')
3505                    + (error.message || e.statusText || _STR.ERR_UNEXPECTED);
3506                request.reject(errorMessage);
3507            }
3508        });
3509        return request.promise();
3510    },
3511
3512    switchLoginMethodConfirmDialog: function(dialogHtml, type, canIgnoreSwitch, confirmClick, ignoreClick) {
3513        let dialogButtons = [];
3514        let confirmButton = {
3515            class: 'btn-success',
3516            click: confirmClick
3517        };
3518        let ignoreButton = {
3519            class: 'btn-danger',
3520            click: ignoreClick
3521        };
3522        if (canIgnoreSwitch) {
3523            confirmButton.text = Utils.xReplace(
3524                _STR.CL_SWITCH_LOGIN_METHOD_BTN_CONFIRM,
3525                type === 'sso' ? _STR.CL_SWITCH_LOGIN_METHOD_BTN_TEXT_SSO : _STR.CL_SWITCH_LOGIN_METHOD_BTN_TEXT_MP
3526            );
3527            ignoreButton.text = Utils.xReplace(
3528                _STR.CL_SWITCH_LOGIN_METHOD_BTN_IGNORE,
3529                type === 'sso' ? _STR.CL_SWITCH_LOGIN_METHOD_BTN_TEXT_MP : _STR.CL_SWITCH_LOGIN_METHOD_BTN_TEXT_SSO
3530            );
3531            dialogButtons = [confirmButton, ignoreButton];
3532        } else {
3533            confirmButton.text = _STR.BUTTON_CONTINUE;
3534            dialogButtons = [confirmButton];
3535        }
3536        Utils.confirmExt({
3537            wide: canIgnoreSwitch,
3538            html: dialogHtml,
3539            buttons: dialogButtons
3540        });
3541    },
3542
3543    encryptAndSaveMasterPassword: function(passwd, key) {
3544        Utils.Storage.set('_rfo-passwd', Utils.encrypt(passwd, key || RFO.ACCOUNT_INFO.clientKey));
3545    },
3546
3547    restoreMasterPassword: function() {
3548        try {
3549            if (!RFO.ACCOUNT_INFO || !RFO.ACCOUNT_INFO.clientKey) {
3550                throw 'Password error #0';
3551            }
3552            let passwd, passwdCase;
3553            let setPasswdData = function(case_id, pwd_encrypted, pwd_key, need_reencrypt) {
3554                if (!pwd_encrypted) {
3555                    passwdCase = passwdCase || (case_id + 'e');
3556                    return;
3557                }
3558                passwdCase = passwdCase || case_id;
3559                passwd = Utils.decrypt(pwd_encrypted, pwd_key);
3560                if (passwd) {
3561                    if (need_reencrypt) {
3562                        Utils.encryptAndSaveMasterPassword(passwd);
3563                    }
3564                    RFMgr.setMasterPwd(passwd);
3565                    RFMgr.setAccountPwd(passwd);
3566                }
3567            };
3568            // transition from RF extension - use mpkey from session
3569            if (RFO.MPKEY && Utils.Storage.get('passwd')) {
3570                let passwdEncrypted = Utils.Storage.get('passwd');
3571                Utils.Storage.remove('passwd');
3572                setPasswdData(1, passwdEncrypted, RFO.MPKEY, true);
3573            }
3574            // transition from new RFO
3575            if (!passwd && sessionStorage['rfo-cred']) {
3576                let rfoCred = Utils.parseJSON(sessionStorage['rfo-cred']);
3577                let cred = rfoCred && rfoCred.value && Utils.parseJSON(rfoCred.value) || {};
3578                let passwdEncrypted = cred && cred.password || '';
3579                sessionStorage.removeItem('rfo-cred');
3580                setPasswdData(2, passwdEncrypted, RFO.ACCOUNT_INFO.clientKey, true);
3581            }
3582            // regular login
3583            if (!passwd && Utils.Storage.get('_rfo-passwd')) {
3584                setPasswdData(3, Utils.Storage.get('_rfo-passwd'), RFO.ACCOUNT_INFO.clientKey, false);
3585            }
3586            if (!passwd) {
3587                if (passwdCase) {
3588                    // encrypted password cannot be restored - show error message with the first checked passwd case
3589                    throw 'Password error #' + passwdCase + ' for ' + RFO.ACCOUNT_INFO.email;
3590                } else {
3591                    // no encrypted password found - just logout with session expired error
3592                    doLogout(RFO.ROOT_PATH + 'login?relogin');
3593                }
3594                return false;
3595            }
3596        } catch (e) {
3597            // show error and logout with session expired error
3598            showResultMsg(e, true, undefined, function(){
3599                doLogout(RFO.ROOT_PATH + 'login?relogin');
3600            });
3601            return false;
3602        }
3603        return true;
3604    }
3605};
3606
3607// This item used to set its value later in async function
3608Utils.AsyncItem = function(asyncFunc) {
3609    this.value = '';
3610    setTimeout(asyncFunc, 0);
3611};
3612Utils.AsyncItem.prototype.toString = function(){
3613    return this.value;
3614};
3615
3616////////////////////
3617
3618function startProcessing($dialog, readyCallback) {
3619    if ($('#quickstartWidget').hasClass('in')) { // quickstartWidget is shown
3620        $('#waitDialog').find('.modal-body').addClass('processing').html('<div class="wait-loader">' + _STR.TEXT_PLEASE_WAIT + '</div>').show();
3621        if (typeof readyCallback === 'function') {
3622            $('#waitDialog').one('shown.bs.modal', function() {
3623                setTimeout(function() {
3624                    readyCallback();
3625                }, 20);
3626            });
3627        }
3628        $('#waitDialog').modal('show');
3629    } else {
3630        $dialog.off('keydown.dismiss.bs.modal'); // disable escape key
3631        $dialog.data('bs.modal').options.backdrop = 'static';
3632        $dialog.find('.close').hide();
3633        $dialog.find('.modal-body, .modal-footer').hide();
3634        $dialog.find('.modal-body.processing').show();
3635        if (typeof readyCallback === 'function') {
3636            readyCallback();
3637        }
3638    }
3639}
3640
3641function stopProcessing($dialog, endProcessCallback, bNotHide) {
3642    var processStop = function(){
3643        $dialog.data('bs.modal').escape(); // reset keyboard
3644        $dialog.data('bs.modal').options.backdrop = true;
3645        $dialog.find('.close').show();
3646        $dialog.find('.modal-body, .modal-footer').show();
3647        $dialog.find('.modal-body.processing, .modal-body.progress-wrapper').hide();
3648        if (typeof endProcessCallback === 'function') {
3649            endProcessCallback();
3650        }
3651    };
3652    if ($('#quickstartWidget').hasClass('in')) { // quickstartWidget is shown
3653        if (typeof endProcessCallback === 'function') {
3654            $('#waitDialog').one('hidden.bs.modal', function(){
3655                setTimeout(function() {
3656                    endProcessCallback();
3657                }, 20);
3658            });
3659        }
3660        $('#waitDialog').modal('hide');
3661    } else if (bNotHide) {
3662        processStop();
3663    } else {
3664        $dialog.one('hidden.bs.modal', function(){
3665            processStop();
3666        }).modal('hide');
3667    }
3668}
3669
3670function DialogProgress($dialog) {
3671    this.$progressWrapper = $dialog.find('.modal-body.progress-wrapper');
3672    this.fProgress = 0.0; // float, 0 .. 100
3673    this.fStep = 0.0;
3674    this.fDisplayProgressPrev = -1;
3675    this.nPrecision = 0;
3676    this.parts = {};
3677
3678    this.start = function() {
3679        $dialog.off('keydown.dismiss.bs.modal'); // disable escape key
3680        $dialog.data('bs.modal').options.backdrop = 'static';
3681        $dialog.find('.close').hide();
3682        $dialog.find('.modal-body, .modal-footer').hide();
3683        this.$progressWrapper.show();
3684        this.setProgress(0); // display progress bar with 0%
3685    };
3686    this.stop = function(endProcessCallback) {
3687        $dialog.one('hidden.bs.modal', function(){
3688            $dialog.data('bs.modal').escape(); // reset keyboard
3689            $dialog.data('bs.modal').options.backdrop = true;
3690            $dialog.find('.close').show();
3691            $dialog.find('.modal-body, .modal-footer').show();
3692            $dialog.find('.modal-body.processing, .modal-body.progress-wrapper').hide();
3693            if (typeof endProcessCallback === 'function') {
3694                endProcessCallback();
3695            }
3696        }).modal('hide');
3697    };
3698    this.setParts = function(nParts) {
3699        this.fStep = 100 / nParts;
3700        if (this.fStep < 0.01) {
3701            this.nPrecision = 2;
3702        } else if (this.fStep < 0.1) {
3703            this.nPrecision = 1;
3704        }
3705    };
3706    this.setPartProgress = function(sPart, n) {
3707        if (this.fProgress < 100) {
3708            n = Math.min(100, n);
3709            var fDelta = this.parts[sPart] ? (n - this.parts[sPart])/100 : n/100;
3710            this.parts[sPart] = n;
3711            this.setProgress(this.fProgress + this.fStep * fDelta);
3712        }
3713    };
3714    this.setProgress = function(progress) {
3715        this.fProgress = Math.min(100, progress);
3716        var sDisplayProgress = this.fProgress.toFixed(this.nPrecision);
3717        var fDisplayProgress = parseFloat(sDisplayProgress);
3718        if (this.fDisplayProgressPrev < fDisplayProgress) {
3719            this.fDisplayProgressPrev = fDisplayProgress;
3720            this.$progressWrapper.find('.progressbar-bg').css('width', sDisplayProgress + '%');
3721            this.$progressWrapper.find('.progressbar-value').html(_STR.CL_PROCESSING + ' ' + sDisplayProgress + '%');
3722        }
3723    };
3724
3725    this.start();
3726}
3727
3728var YoutubePlayer = function() {
3729    this.playerID = undefined;
3730    this.loadingIntervalID = undefined;
3731    this.loadingIntervalDelay = 300;
3732    this.loadAPI = function() {
3733        if (window.YTPlayers) {
3734            return;
3735        }
3736        $('body').append($('<script />', {
3737            src: 'https://www.youtube.com/iframe_api'
3738        }));
3739        window.YTPlayers = {
3740            'users': {
3741                player: undefined,
3742                videoID: 'ioTcdPYcRBo',
3743                isIframeLoaded: false
3744            },
3745            'group': {
3746                player: undefined,
3747                videoID: 'Hq3q2FmT3pM',
3748                isIframeLoaded: false
3749            },
3750            'files': {
3751                player: undefined,
3752                videoID: 'irLiimFHdNQ',
3753                isIframeLoaded: false
3754            },
3755            'files_nofiles': {
3756                player: undefined,
3757                videoID: 'irLiimFHdNQ',
3758                isIframeLoaded: false
3759            },
3760            'policies': {
3761                player: undefined,
3762                videoID: 'BiCpwenMSRE',
3763                isIframeLoaded: false
3764            },
3765            'reports': {
3766                player: undefined,
3767                videoID: '4Dw7bxX1BRw',
3768                isIframeLoaded: false
3769            }
3770        };
3771    };
3772    this.loadPlayer = function(playerID) {
3773        if (!window.YTPlayers) {
3774            this.loadAPI();
3775        }
3776        var $iframe = $('#youtubeiframe_' + playerID);
3777        if (!$iframe.is('[src]')) { // not have 'src' attribute
3778            $iframe.attr('src', 'https://www.youtube.com/embed/' + window.YTPlayers[playerID].videoID + '?enablejsapi=1&origin=' + window.location.origin);
3779            $iframe.on('load', function() {
3780                window.YTPlayers[playerID].isIframeLoaded = true;
3781            });
3782        }
3783        if (
3784            window.YT && window.YT.Player &&
3785            window.YTPlayers[playerID].isIframeLoaded &&
3786            !window.YTPlayers[playerID].player
3787        ) {
3788            window.YTPlayers[playerID].player = new YT.Player('youtubeiframe_' + playerID);
3789        }
3790    };
3791    this.play = function(playerID, readyCallback) {
3792        this.pause();
3793        this.playerID = playerID;
3794        var obj = this;
3795        var startPlaying = function() {
3796            var isReady = obj.isReady(playerID);
3797            if (isReady) {
3798                obj.clearInterval();
3799                if (obj.playerID != playerID) {
3800                    // YT.Player has not been loaded/created yet but user already cancelled video playback
3801                    return isReady;
3802                }
3803                window.YTPlayers[playerID].player.playVideo();
3804                if (typeof readyCallback === 'function') {
3805                    readyCallback(window.YTPlayers[playerID].player);
3806                }
3807            } else {
3808                obj.loadPlayer(playerID);
3809            }
3810            return isReady;
3811        };
3812        if (!startPlaying()) {
3813            this.loadingIntervalID = setInterval(function() {
3814                startPlaying();
3815            }, this.loadingIntervalDelay);
3816        }
3817    };
3818    this.pause = function() {
3819        if (this.playerID && this.isReady(this.playerID)) {
3820            window.YTPlayers[this.playerID].player.pauseVideo();
3821        }
3822        this.playerID = undefined;
3823        this.clearInterval();
3824    };
3825    this.setToBegining = function(playerID) {
3826        if (this.isReady(playerID)) {
3827            window.YTPlayers[playerID].player.seekTo(0);
3828            window.YTPlayers[playerID].player.pauseVideo();
3829        }
3830    };
3831    this.isReady = function(playerID) {
3832        return (
3833            window.YTPlayers &&
3834            window.YTPlayers[playerID] &&
3835            window.YTPlayers[playerID].player &&
3836            window.YTPlayers[playerID].player.playVideo
3837        );
3838    };
3839    this.clearInterval = function() {
3840        if (this.loadingIntervalID !== undefined) {
3841            clearInterval(this.loadingIntervalID);
3842            this.loadingIntervalID = undefined;
3843        }
3844    };
3845};
3846
3847var AuthChecker = function() {
3848    var checkShortPeriod = 1*60*1000; // 1 minute
3849    var checkLongPeriod  = 5*60*1000; // 5 minutes
3850
3851    var isVisible, shortInterval, longInterval;
3852
3853    // Set the name of the hidden property and the change event for visibility
3854    var hidden, visibilityChange;
3855    if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support
3856        hidden = 'hidden';
3857        visibilityChange = "visibilitychange";
3858    } else if (typeof document.msHidden !== 'undefined') {
3859        hidden = 'msHidden';
3860        visibilityChange = 'msvisibilitychange';
3861    } else if (typeof document.webkitHidden !== 'undefined') {
3862        hidden = 'webkitHidden';
3863        visibilityChange = 'webkitvisibilitychange';
3864    }
3865
3866    var checkStatus = function() {
3867        $.get('/rf-api?test', function() {
3868            // session is valid, do nothing
3869        }).fail(function(err) {
3870            if (err.status === 401) {
3871                // access denied error (session expired)
3872                doLogout(RFO.ROOT_PATH + 'login?relogin');
3873            }
3874        });
3875    };
3876
3877    var restartCheckers = function() {
3878        if (typeof isVisible !== 'undefined') {
3879            // check session status immediately, except first time
3880            checkStatus();
3881        }
3882        // this checker used for check if user clicked something on the page after {checkShortPeriod} or more seconds of active state
3883        var setShortChecker = function(){
3884            clearInterval(shortInterval);
3885            shortInterval = setInterval(function(){
3886                $(document).off('click.AuthChecker').one('click.AuthChecker', restartCheckers);
3887                clearInterval(shortInterval);
3888            }, checkShortPeriod);
3889        };
3890        $(document).off('click.AuthChecker').on('click.AuthChecker', setShortChecker);
3891        setShortChecker();
3892        // this checker used for check session status periodically every {checkLongPeriod} seconds while the page is active
3893        clearInterval(longInterval);
3894        longInterval = setInterval(restartCheckers, checkLongPeriod);
3895    };
3896
3897    var onVisible = function() {
3898        if (!isVisible) { // prevent double execution
3899            restartCheckers();
3900            isVisible = true;
3901        }
3902    };
3903
3904    var onHidden = function() {
3905        if (isVisible) { // prevent double execution
3906            clearInterval(shortInterval);
3907            clearInterval(longInterval);
3908            isVisible = false;
3909        }
3910    };
3911
3912    this.start = function() {
3913        if (typeof visibilityChange !== 'undefined') {
3914            $(document).off(visibilityChange).on(visibilityChange, function() {
3915                document[hidden] ? onHidden() : onVisible();
3916            });
3917            onVisible();
3918        }
3919    };
3920};
3921
3922var PasswordChecker = (function() {
3923
3924    var PasswordProblems = {
3925        TooShort:          0,
3926        TooLong:           1,
3927        BlanksOnly:        2,
3928        FewNonNumChars:    3,
3929        FewUpperCaseChars: 4,
3930        FewLowerCaseChars: 5,
3931        FewDigitChars:     6
3932    };
3933
3934    var m_breach_check_tasks = [];
3935    var m_last_checked_password = '';
3936    var m_cached_compromised_passwords = [];
3937
3938    function _GetMasterPasswordRestrictions() {
3939        var restrictions = {
3940            minLength: RFO.PWD_MIN,
3941            maxLength: RFO.PWD_MAX,
3942            minNonNumChars: RFO.PWD_NONNUM_MIN,
3943            minUpperCaseChars: 0,
3944            minLowerCaseChars: 0,
3945            minDigitChars: 0
3946        };
3947        if (RFO.POLICIES && RFO.POLICIES.CONFIG && RFO.POLICIES.CONFIG.MasterPasswordMinLength) {
3948            restrictions.minLength = RFO.POLICIES.CONFIG.MasterPasswordMinLength;
3949        }
3950        if (RFO.POLICIES && RFO.POLICIES.CONFIG && RFO.POLICIES.CONFIG.MasterPasswordMinNonNumChars) {
3951            restrictions.minNonNumChars = RFO.POLICIES.CONFIG.MasterPasswordMinNonNumChars;
3952        }
3953        if (RFO.POLICIES && RFO.POLICIES.CONFIG && RFO.POLICIES.CONFIG.MasterPasswordMinUpperCaseChars) {
3954            restrictions.minUpperCaseChars = RFO.POLICIES.CONFIG.MasterPasswordMinUpperCaseChars;
3955        }
3956        if (RFO.POLICIES && RFO.POLICIES.CONFIG && RFO.POLICIES.CONFIG.MasterPasswordMinLowerCaseChars) {
3957            restrictions.minLowerCaseChars = RFO.POLICIES.CONFIG.MasterPasswordMinLowerCaseChars;
3958        }
3959        if (RFO.POLICIES && RFO.POLICIES.CONFIG && RFO.POLICIES.CONFIG.MasterPasswordMinDigitChars) {
3960            restrictions.minDigitChars = RFO.POLICIES.CONFIG.MasterPasswordMinDigitChars;
3961        }
3962        return restrictions;
3963    }
3964
3965    function stopCheckingPasswordForBreachness() {
3966        m_breach_check_tasks.forEach(function(DEF){
3967            DEF.reject();
3968        });
3969        m_breach_check_tasks = [];
3970        m_last_checked_password = '';
3971    }
3972
3973    function checkPasswordForCompliance(password) {
3974        password = password || '';
3975        var problems = [];
3976        var restrictions = _GetMasterPasswordRestrictions();
3977        var password_length = Utils.countChars(password);
3978
3979        if (password_length < restrictions.minLength) {
3980            problems.push(PasswordProblems.TooShort);
3981        }
3982        if (password_length > restrictions.maxLength) {
3983            problems.push(PasswordProblems.TooLong);
3984        }
3985        if (!password.trim()) {
3986            problems.push(PasswordProblems.BlanksOnly);
3987        }
3988        // non-numeric characters also need to be counted and checked always
3989        var upper_case_chars = 0;
3990        var lower_case_chars = 0;
3991        var digit_chars = 0;
3992        Array.from(password).forEach(function(character) {
3993            if (/[0-9]/.test(character)) {
3994                digit_chars++;
3995            } else if (character.toLowerCase() !== character.toUpperCase()) {
3996                if (character === character.toUpperCase()) {
3997                    upper_case_chars++;
3998                } else if (character === character.toLowerCase()) {
3999                    lower_case_chars++;
4000                }
4001            }
4002        });
4003        if ((password_length - digit_chars) < restrictions.minNonNumChars) {
4004            problems.push(PasswordProblems.FewNonNumChars);
4005        }
4006        if (upper_case_chars < restrictions.minUpperCaseChars) {
4007            problems.push(PasswordProblems.FewUpperCaseChars);
4008        }
4009        if (lower_case_chars < restrictions.minLowerCaseChars) {
4010            problems.push(PasswordProblems.FewLowerCaseChars);
4011        }
4012        if (digit_chars < restrictions.minDigitChars) {
4013            problems.push(PasswordProblems.FewDigitChars);
4014        }
4015        return {problems: problems, restrictions: restrictions};
4016    }
4017
4018    return {
4019        PasswordProblems: PasswordProblems,
4020
4021        stopCheckingPasswordForBreachness: stopCheckingPasswordForBreachness,
4022        checkPasswordForCompliance: checkPasswordForCompliance,
4023
4024        lastCheckedPassword: function() {
4025            return m_last_checked_password;
4026        },
4027
4028        isPasswordCompromised: function(password, callback) {
4029            stopCheckingPasswordForBreachness();
4030
4031            var DEF = $.Deferred();
4032            m_breach_check_tasks.push(DEF);
4033            DEF.done(function(result){
4034                m_breach_check_tasks = [];
4035                callback(result);
4036            }).fail(function(){
4037                m_breach_check_tasks = [];
4038                callback(null);
4039            });
4040
4041            if (!password) {
4042                DEF.reject();
4043                return;
4044            }
4045
4046            m_last_checked_password = password;
4047
4048            if (Utils.in_array(password, m_cached_compromised_passwords)) {
4049                DEF.resolve(true);
4050            } else {
4051                $.get('/etc', function(data) {
4052                    if (!data.breachMonUrl) {
4053                        DEF.reject();
4054                        return;
4055                    }
4056                    var password_utf8_bytes = RfapiJS.UTF8.stringToBytes(password);
4057                    var pwd_hash_sha1_hex_lo = RfapiJS.utils.SHA1AsLowerHexStr(password_utf8_bytes);
4058                    var hash_head = pwd_hash_sha1_hex_lo.substr(0, 5);
4059                    var oReq = new XMLHttpRequest();
4060                    oReq.open('GET', data.breachMonUrl + '?p=' + hash_head, true);
4061                    oReq.responseType = 'arraybuffer';
4062                    oReq.onload = function() {
4063                        // process server response - blob with hashes of compromised passwords
4064                        if (!oReq.response) {
4065                            DEF.reject();
4066                            return;
4067                        }
4068                        var isPasswordCompromised = false;
4069                        var hashes_blob = new Uint8Array(oReq.response);
4070                        // not 5, because length must be even
4071                        var hash_tail_bin = RfapiJS.utils.hexToBytes(pwd_hash_sha1_hex_lo.substr(4));
4072                        var record_len = hash_tail_bin.length;
4073                        var last_pos = hashes_blob.length - record_len;
4074                        for (var pos = 0; pos <= last_pos; pos += record_len) {
4075                            if (((hash_tail_bin[0] ^ hashes_blob[pos]) & 0x0F) === 0) {
4076                                if (
4077                                    hash_tail_bin.every(function(v, i){
4078                                        return (i === 0 || v === hashes_blob[pos + i]);
4079                                    })
4080                                ) {
4081                                    isPasswordCompromised = true;
4082                                    m_cached_compromised_passwords.push(password);
4083                                    break;
4084                                }
4085                            }
4086                        }
4087                        DEF.resolve(isPasswordCompromised);
4088                    };
4089                    oReq.onerror = oReq.onabort = function() {
4090                        DEF.reject();
4091                    };
4092                    oReq.send();
4093                }).fail(function(){
4094                    DEF.reject();
4095                });
4096            }
4097        },
4098
4099        updatePasswordQualityIndicator: function($input, $qualityIndicator, $problemsList) {
4100            var password = $input.val();
4101            var checkResult = password ? checkPasswordForCompliance(password) : null;
4102            var passwordRestrictions = checkResult ? checkResult.restrictions : _GetMasterPasswordRestrictions();
4103
4104            function _CreateProblemsListItem(text, problem) {
4105                var status = checkResult && checkResult.problems
4106                    ? Utils.in_array(problem, checkResult.problems)
4107                    ? 'failed'
4108                    : 'passed'
4109                    : 'initial';
4110                return '<div class="item '+status+'"><div class="icon"></div><div class="text">'+text+'</div></div>';
4111            }
4112
4113            if (checkResult && checkResult.problems.length) {
4114                $input.addClass('bad-value');
4115                $qualityIndicator.addClass('have-problems');
4116            } else {
4117                $input.removeClass('bad-value');
4118                $qualityIndicator.removeClass('have-problems');
4119            }
4120            $problemsList.children().remove();
4121            if (!checkResult) {
4122                $problemsList.append('<div class="title">'+_STR.PWD_QUALITY_HINT_TITLE+'</div>');
4123            }
4124            $problemsList.append(_CreateProblemsListItem(
4125                Utils.xReplace(_STR.PWD_QUALITY_HINT_MIN_LENGTH, passwordRestrictions.minLength),
4126                PasswordProblems.TooShort
4127            ));
4128            if (checkResult && checkResult.problems && Utils.in_array(PasswordProblems.TooLong, checkResult.problems)) {
4129                $problemsList.append(_CreateProblemsListItem(
4130                    Utils.xReplace(_STR.PWD_QUALITY_HINT_MAX_LENGTH, passwordRestrictions.maxLength),
4131                    PasswordProblems.TooLong
4132                ));
4133            }
4134            if (passwordRestrictions.minNonNumChars !== 0) {
4135                $problemsList.append(_CreateProblemsListItem(
4136                    Utils.xReplace(_STR.PWD_QUALITY_HINT_MIN_NON_NUM_CHARS, passwordRestrictions.minNonNumChars),
4137                    PasswordProblems.FewNonNumChars
4138                ));
4139            }
4140            if (passwordRestrictions.minUpperCaseChars !== 0) {
4141                $problemsList.append(_CreateProblemsListItem(
4142                    Utils.xReplace(_STR.PWD_QUALITY_HINT_MIN_UPPER_CASE_CHARS, passwordRestrictions.minUpperCaseChars),
4143                    PasswordProblems.FewUpperCaseChars
4144                ));
4145            }
4146            if (passwordRestrictions.minLowerCaseChars !== 0) {
4147                $problemsList.append(_CreateProblemsListItem(
4148                    Utils.xReplace(_STR.PWD_QUALITY_HINT_MIN_LOWER_CASE_CHARS, passwordRestrictions.minLowerCaseChars),
4149                    PasswordProblems.FewLowerCaseChars
4150                ));
4151            }
4152            if (passwordRestrictions.minDigitChars !== 0) {
4153                $problemsList.append(_CreateProblemsListItem(
4154                    Utils.xReplace(_STR.PWD_QUALITY_HINT_MIN_DIGIT_CHARS, passwordRestrictions.minDigitChars),
4155                    PasswordProblems.FewDigitChars
4156                ));
4157            }
4158        }
4159    };
4160})();
4161
4162function showResultMsgWide(msg, bError, sGotoLocation, onHiddenResultDialog, onShownResultDialog, title, buttonTitle, bWarning) {
4163    showResultMsg_Generic(true, msg, bError, sGotoLocation, onHiddenResultDialog, onShownResultDialog, title, buttonTitle, bWarning);
4164}
4165
4166function showResultMsg(msg, bError, sGotoLocation, onHiddenResultDialog, onShownResultDialog, title, buttonTitle, bWarning) {
4167    showResultMsg_Generic(false, msg, bError, sGotoLocation, onHiddenResultDialog, onShownResultDialog, title, buttonTitle, bWarning);
4168}
4169
4170function showResultMsg_Generic(bWide, msg, bError, sGotoLocation, onHiddenResultDialog, onShownResultDialog, title, buttonTitle, bWarning) {
4171    var $resultDialog = $('#resultDialog');
4172    if (bWide) {
4173        $resultDialog.addClass('wide').find('.modal-dialog').removeClass('modal-sm');
4174    } else {
4175        $resultDialog.removeClass('wide').find('.modal-dialog').addClass('modal-sm');
4176    }
4177    var $title = $resultDialog.find('.modal-title');
4178    var sTitle = '';
4179    if (bWarning) {
4180        $resultDialog.removeClass('error').addClass('warning');
4181        sTitle = $title.attr('data-warning');
4182    } else if (bError) {
4183        $resultDialog.removeClass('warning').addClass('error');
4184        sTitle = $title.attr('data-error');
4185    } else {
4186        $resultDialog.removeClass('warning error');
4187        sTitle = $title.attr('data-success');
4188    }
4189    if (typeof title !== 'undefined') {
4190        sTitle = title;
4191    }
4192    // replace the default server "legacy account" message with a more friendly.
4193    if (msg=='Forbidden: cannot share to legacy account') {
4194        msg=_STR.CL_USERGROUP_USERS_CANNOT_SHARE_TO_LEGACY_ACCOUNT;
4195        sTitle=_STR.CL_INFORMATION;
4196    }
4197    $title.html(sTitle ? sTitle : '&nbsp;');
4198    $resultDialog.find('.modal-body').html(msg);
4199    $resultDialog.find('.modal-footer .btn-default').html(buttonTitle || _STR.BUTTON_CLOSE);
4200    $resultDialog.one('show.bs.modal', function(){
4201        // close result dialog by enter key
4202        $resultDialog.one('keydown', function(e){
4203            if (e.which === 13) {
4204                $resultDialog.modal('hide');
4205            }
4206        });
4207    });
4208    if ($('#quickstartWidget').hasClass('in')) { // quickstartWidget is shown
4209        sGotoLocation = onHiddenResultDialog = onShownResultDialog = false;
4210    }
4211    if (sGotoLocation) {
4212        $resultDialog.one('hide.bs.modal', function(){
4213            document.location.href = sGotoLocation;
4214        });
4215    }
4216    if (typeof onHiddenResultDialog === 'function') {
4217        $resultDialog.one('hidden.bs.modal', function(){
4218            onHiddenResultDialog($resultDialog);
4219        });
4220    }
4221    if (typeof onShownResultDialog === 'function') {
4222        $resultDialog.one('shown.bs.modal', function(){
4223            onShownResultDialog($resultDialog);
4224        });
4225    }
4226    $resultDialog.modal('show');
4227}
4228
4229function doLogout(logoutURL) {
4230    var dontRemoveCookies = ['lang'],
4231        i, tempArr = {}, len = dontRemoveCookies.length;
4232    for (i = 0; i < len; i++) {
4233        tempArr[dontRemoveCookies[i]] = $.cookie(dontRemoveCookies[i]);
4234    }
4235    $.cookie(null);
4236    for (i = 0; i < len; i++) {
4237        $.cookie(dontRemoveCookies[i], tempArr[dontRemoveCookies[i]]);
4238    }
4239    Utils.Storage.remove('_rfo-passwd');
4240    document.location.href = logoutURL + (sessionStorage.affid ? (~logoutURL.indexOf('?') ? '&' : '?') + 'affid=' + sessionStorage.affid : '');
4241}
4242
4243// Do not allow the site to load in frame, except for plain signup in modern browser
4244function RelocateFrame() {
4245    if (RFO.PAGE_TYPE !== 'signup-plain' && top.location !== location) {
4246        top.location.href = location.href;
4247        return true;
4248    }
4249    return false;
4250}
4251
4252function RFMgrCommonErrorHandler($dialog, errObject) {
4253    if (errObject.Code && errObject.Code === RfapiJS.HTTP_Status_Code_Unauthorized) {
4254        //access denied error (session expired)
4255        doLogout(RFO.ROOT_PATH + 'login?relogin');
4256    } else if ($dialog) {
4257        stopProcessing($dialog, function(){
4258            showResultMsg(Utils.getErrorDescription(errObject), true);
4259        });
4260    } else {
4261        showResultMsg(Utils.getErrorDescription(errObject), true);
4262    }
4263}
4264
4265function detectDeviceIcon(row) {
4266    sIcon = 'devices_unknown.png';
4267    switch (row.id.charAt(0)) {
4268        case 'B': // Browser
4269            if (row.description.match(/^Safari/)) {
4270                sIcon = 'devices_safari.png';
4271            } else if (row.description.match(/^Edge/)) {
4272                sIcon = 'devices_edge.png';
4273            } else if (row.description.match(/^Chrome/)) {
4274                sIcon = 'devices_chrome.png';
4275            } else if (row.description.match(/^Firefox/)) {
4276                sIcon = 'devices_firefox.png';
4277            } else if (row.description.match(/^Opera/)) {
4278                sIcon = 'devices_opera.png';
4279            } else if (row.description.match(/^IE(\d+)?/)) {
4280                sIcon = 'devices_ie.png';
4281            } else if (row.description.match(/^RoboForm/i)) {
4282                sIcon = 'devices_roboform.png';
4283            } else {
4284                sIcon = 'devices_browser.png';
4285            }
4286            break;
4287        case 'W': // RF-Win
4288            sIcon = 'devices_laptop.png';
4289            break;
4290        case 'M': // RF-Mac
4291            if (row.description.match(/iMac/)) {
4292                sIcon = 'devices_computer.png';
4293            } else {
4294                sIcon = 'devices_laptop.png';
4295            }
4296            break;
4297        case 'A': // Android
4298            sIcon = 'devices_phone.png';
4299            break;
4300        case 'I': // iOS
4301            if (row.description.match(/iPad/)) {
4302                sIcon = 'devices_tablet.png';
4303            } else {
4304                sIcon = 'devices_phone.png';
4305            }
4306            break;
4307        case 'E': // RF-Edge
4308            sIcon = 'devices_edge.png';
4309            break;
4310        case 'C': // RF-Chrome
4311            sIcon = 'devices_chrome.png';
4312            break;
4313        case 'F': // RF-Firefox
4314            sIcon = 'devices_firefox.png';
4315            break;
4316        case 'O': // RF-Opera
4317            sIcon = 'devices_opera.png';
4318            break;
4319    }
4320    switch (row.client) {
4321        case 'RF-Win':
4322            sIcon = 'devices_laptop.png';
4323            break;
4324        case 'RF-Mac':
4325            if (row.description.match(/iMac/)) {
4326                sIcon = 'devices_computer.png';
4327            } else {
4328                sIcon = 'devices_laptop.png';
4329            }
4330            break;
4331        case 'RF-iPhone':
4332            if (row.description.match(/iPad/)) {
4333                sIcon = 'devices_tablet.png';
4334            } else {
4335                sIcon = 'devices_phone.png';
4336            }
4337            break;
4338        case 'RF-Android':
4339            sIcon = 'devices_phone.png';
4340            break;
4341        case 'RF-Edge':
4342            sIcon = 'devices_edge.png';
4343            break;
4344        case 'RF-Chrome':
4345            sIcon = 'devices_chrome.png';
4346            break;
4347        case 'RF-Firefox':
4348            sIcon = 'devices_firefox.png';
4349            break;
4350        case 'RF-Opera':
4351            sIcon = 'devices_opera.png';
4352            break;
4353    }
4354    return sIcon;
4355}
4356
4357
4358// AffID Script
4359var affid = '';
4360(function(){
4361    var affid_ = Utils.parseUrlQuery().affid;
4362    if (affid_) {
4363        affid = affid_.slice(0, 5);
4364        var aDomainComponents = location.hostname.split(".");
4365        aDomainComponents.push("");
4366        // pass domain as .roboform.com obtained from www.roboform.com
4367        $.cookie('affid', affid, {expires: 7, path: '/', domain: "."+aDomainComponents.slice(-3, -1).join(".")});
4368        // save affid from URL parameter to session storage too,
4369        // it will be used to get proper OpenID providers on login page after logout or session expiration
4370        sessionStorage.affid = affid_;
4371    } else if ($.cookie('affid')) {
4372        affid = $.cookie('affid').slice(0, 5);
4373    }
4374})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.