<commit_before>TC.control = TC.control || {};

if (!TC.control.MapInfo) {
    TC.syncLoadJS(TC.apiLocation + 'TC/control/MapInfo');
}

TC.control.Share = function (options) {
    var self = this;
    self._classSelector = '.' + self.CLASS;

    TC.Control.apply(self, arguments);

    self.exportsState = true;

    var opts = options || {};
    self._dialogDiv = TC.Util.getDiv(opts.dialogDiv);
    if (window.$) {
        self._$dialogDiv = $(self._dialogDiv);
    }
    if (!opts.dialogDiv) {
        document.body.appendChild(self._dialogDiv);
    }

    self.render();
};

TC.inherit(TC.control.Share, TC.control.MapInfo);

(function () {
    var ctlProto = TC.control.Share.prototype;

    ctlProto.CLASS = 'tc-ctl-share';
    ctlProto.MAILTO_MAX_LENGTH = 256;
    ctlProto.IFRAME_WIDTH = '600px';
    ctlProto.IFRAME_HEIGHT = '450px';
    ctlProto.FEATURE_PARAM = 'showfeature';

    ctlProto.MOBILEFAV = 'Siga las instrucciones del navegador del dispositivo móvil para añadir como favorito. Se guardará el estado actual del mapa.';
    ctlProto.NAVALERT = ' +D para guardar en marcadores.';

    ctlProto.render = function (callback) {
        const self = this;
        return self.getRenderedHtml(self.CLASS + '-dialog', null, function (html) {
            self._dialogDiv.innerHTML = html;
        }).then(function () {
            return TC.Control.prototype.renderData.call(self, { controlId: self.id }, function () {
                //Si el navegador no soporta copiar al portapapeles, ocultamos el botón de copiar
                if (document.queryCommandSupported && document.queryCommandSupported('copy')) {
                    self.div.querySelectorAll('button').forEach(function (btn) {
                        btn.classList.remove('hide');
                    });
                    self.div.querySelectorAll('input[type=text]').forEach(function (input) {
                        delete input.dataset.dataOriginalTitle;
                    });
                }

                // Si el SO no es móvil, ocultamos el botón de compartir a WhatsApp
                if (!TC.Util.detectMobile()) {
                    self.div.querySelector(".share-whatsapp").classList.add(TC.Consts.classes.HIDDEN);
                }

                const options = self.div.querySelectorAll('.' + self.CLASS + '-url-box');
                self.div.querySelectorAll('span:not(.tc-beta)').forEach(function (span) {
                    span.addEventListener(TC.Consts.event.CLICK, function (e) {
                        var label = this;
                        while (label && label.tagName !== 'LABEL') {
                            label = label.parentElement;
                        }
                        const newFormat = label.querySelector(`input[type=radio][name="${self.id}-format"]`).value;

                        options.forEach(function (option) {
                            option.classList.toggle(TC.Consts.classes.HIDDEN, !option.matches('.tc-' + newFormat));
                        });
                    }, { passive: true });
                });
                if (TC.Util.isFunction(callback)) {
                    callback();
                }
            });
        });
    };

    /**
     * Obtiene una URL "limpia" para compartir el mapa.
     */
    ctlProto.getLocation = function () {
        var result = window.location.href;
        if (window.location.hash) {
            result = result.substr(0, result.indexOf(window.location.hash));
        }
        return result;
    };

    ctlProto.manageMaxLengthExceed = function (maxLengthExceed) {
        const self = this;

        const browserAlert = self.div.querySelector('.' + self.CLASS + '-alert');
        //Si la URL sobrepasa el tamaño máximo avisamos que puede fallar en Edge
        browserAlert.classList.toggle(TC.Consts.classes.HIDDEN, !maxLengthExceed.browser);

        const qrAlert = self._dialogDiv.querySelector('.' + self.CLASS + '-qr-alert');
        qrAlert.classList.toggle(TC.Consts.classes.HIDDEN, !maxLengthExceed.qr);
    };

    ctlProto.generateIframe = async function (url) {
        const self = this;
        const urlString = url || await self.generateLink();
        if (urlString) {
            return '<iframe style="width:' + self.IFRAME_WIDTH + ';height:' + self.IFRAME_HEIGHT + ';" src="' + urlString + '"></iframe>';
        }
        return '';
    }

    ctlProto.loadParamFeature = function () {
        const self = this;
        var result = null;
        var featureToShow = TC.Util.getParameterByName(self.FEATURE_PARAM);
        if (featureToShow) {
            var featureObj;
            try {
                featureObj = JSON.parse(decodeURIComponent(escape(window.atob(featureToShow))));
            }
            catch (error) {
                TC.error(self.getLocaleString('sharedFeatureNotValid'), TC.Consts.msgErrorMode.TOAST);
            }
            if (featureObj && self.map) {
                if (featureObj.geom) {
                    self.map.addLayer({
                        id: self.getUID(),
                        type: TC.Consts.layerType.VECTOR,
                        title: self.getLocaleString('foi'),
                        owner: self,
                        stealth: true
                    }).then(function (layer) {
                        self.paramFeatureLayer = layer;
                        layer.importState({ features: [featureObj] }).then(function () {
                            self.map.zoomToFeatures(layer.features);
                        });
                    });
                }
                else {
                    result = featureObj;
                }
            }
        }
        return result;
    };

    ctlProto.register = function (map) {
        const self = this;

        const result = TC.control.MapInfo.prototype.register.call(self, map);

        self.exportsState = true;

        self.MOBILEFAV = self.getLocaleString('mobileBookmarks.instructions');
        self.NAVALERT = self.getLocaleString('bookmarks.instructions');

        self.div.addEventListener('click', TC.EventTarget.listenerBySelector('h2', async function (evt) {
            const input = self.div.querySelector('.tc-url input[type=text]');
            if (input.value.trim().length === 0) {
                const link = await self.generateLink();
                self.registerListeners();
                input.value = link;
                self.div.querySelector('.tc-iframe input[type=text]').value = await self.generateIframe(link);
            }
        }));

        self.map.on(TC.Consts.event.MAPCHANGE, function (e) {
            const self = this;
            const input = self.div.querySelector('.tc-url input[type=text]');
            if (input) {
                input.dataset["update"] = true;
            }
        });

        var selectInputField = async function (elm, shorten) {
            const input = elm.parentElement.querySelector("input[type=text]");

            if (shorten) {
                if (input.dataset["update"] || !input.dataset["shortened"]) {
                    self.shortenedLink()
                        .then(async function (value) {
                            if (value && value.trim().length > 0) {

                                elm.textContent = self.getLocaleString('shortened');
                                setTimeout(function () {
                                    elm.textContent = self.getLocaleString('shorten');
                                    unselectInputField();
                                }, 1000);

                                delete input.dataset["update"];
                                input.dataset["shortened"] = true;
                                input.value = value;

                                input.select();

                            } else {
                                delete input.dataset["update"];
                                delete input.dataset["shortened"];
                                input.value = await self.generateLink();
                            }
                        });
                }
            } else {
                if (!input.classList.contains('tc-url')) {
                    input.value = await self.generateIframe();
                } else {
                    if (input.dataset["update"]) {
                        delete input.dataset["update"];
                        delete input.dataset["shortened"];
                        input.value = await self.generateLink();
                    }
                }

                input.select();
            }
        };

        var unselectInputField = function () {
            document.getSelection().removeAllRanges();
            document.getSelection().addRange(document.createRange());
        };

        self.div.addEventListener('click', TC.EventTarget.listenerBySelector('.tc-ctl-share-url-box button', function (evt) {
            const btn = evt.target;
            const isCopyBtn = !(btn.classList.contains('shorten'));

            const copy = async function (value) {
                await selectInputField(btn);

                document.execCommand('copy');

                btn.textContent = self.getLocaleString('copied');

                setTimeout(function () {
                    btn.textContent = self.getLocaleString((isCopyBtn ? 'copy' : 'shorten'));
                    unselectInputField();
                }, 1000);
            };

            if (!isCopyBtn) {
                selectInputField(btn, true);
            } else {
                copy();
            }
        }));

        self.div.addEventListener('click', TC.EventTarget.listenerBySelector('input[type=text]', function (evt) {
            selectInputField(evt.target);
        }));

        //Deshabilitar el click de ratón en los enlaces de compartir cuando están deshabilitados
        self.div.addEventListener('click', TC.EventTarget.listenerBySelector('.ga-share-icon.tc-disabled', function (evt) {
            evt.stopImmediatePropagation();
            evt.preventDefault();
            return false;
        }));

        //Enviar por e-mail
        self.div.addEventListener('click', TC.EventTarget.listenerBySelector('a.share-email', async function (evt) {
            evt.preventDefault();
            var url = await self.generateLink();

            if (url) {
                const body = encodeURIComponent(url + "\n");
                if (body.length > self.MAILTO_MAX_LENGTH) {
                    map.toast(self.getLocaleString('urlTooLongForMailto'), { type: TC.Consts.msgType.WARNING });
                }
                window.location.href = 'mailto:?body=' + body;
            }
        }));

        //Generar código QR
        self.div.addEventListener("click", TC.EventTarget.listenerBySelector("a.qr-generator", function (evt) {
            evt.preventDefault();
            const qrContainer = self._dialogDiv.querySelector(".qrcode");
            qrContainer.innerHTML = '';

            if (self._dialogDiv.querySelector('.' + self.CLASS + '-qr-alert').classList.contains(TC.Consts.classes.HIDDEN)) {
                self.makeQRCode(qrContainer, 256, 256).then(function (qrCodeBase64) {
                    if (qrCodeBase64) {
                        TC.Util.showModal(self._dialogDiv.querySelector(self._classSelector + '-qr-dialog'));
                    }
                });
            } else {
                TC.Util.showModal(self._dialogDiv.querySelector(self._classSelector + '-qr-dialog'));
            }
        }));

        const openSocialMedia = function (win, url, process) {
            if (url && url.trim().length > 0) {
                win.location.href = process(url);
            } else {
                TC.error(self.getLocaleString('urlTooLongForShortener'));
                win.close();
            }
        };

        //Compartir en Facebook
        self.div.addEventListener("click", TC.EventTarget.listenerBySelector("a.share-fb", function (evt) {
            evt.preventDefault();

            const w = window.open();
            self.shortenedLink().then(function (url) {
                openSocialMedia(w, url, function (url) {
                    return "https://www.facebook.com/sharer/sharer.php?u=" + encodeURIComponent(url)
                });
            });

            return false;
        }));

        //Compartir en Twitter
        self.div.addEventListener("click", TC.EventTarget.listenerBySelector("a.share-twitter", function (evt) {
            evt.preventDefault();

            const w = window.open();
            self.shortenedLink().then(function (url) {
                openSocialMedia(w, url, function (url) {
                    var titulo = encodeURIComponent(window.document.title ? window.document.title : "Visor API SITNA");
                    return "https://twitter.com/intent/tweet?text=" + titulo + "&url=" + encodeURIComponent(url);
                });
            });

            return false;
        }));

        //Compartir en Whatsapp
        if (TC.Util.detectMobile()) {
            self.div.addEventListener("click", TC.EventTarget.listenerBySelector("a.share-whatsapp", function (evt) {
                evt.preventDefault();

                self.shortenedLink().then(async function (url) {
                    var waText = 'whatsapp://send?text=';
                    if (url !== undefined) {
                        location.href = waText + encodeURIComponent(url);
                    } else {
                        location.href = waText + encodeURIComponent(await self.generateLink());
                    }
                });

                return false;
            }));
        }

        //Guardar en marcadores
        self.div.addEventListener("click", TC.EventTarget.listenerBySelector("a.share-star", async function (evt) {
            evt.preventDefault();

            var bookmarkURL = await self.generateLink();
            var bookmarkTitle = document.title;

            if (TC.Util.detectMobile()) {
                // Mobile browsers
                alert(ctlProto.MOBILEFAV);
            } else if (window.sidebar && window.sidebar.addPanel) {
                // Firefox version < 23
                window.sidebar.addPanel(bookmarkTitle, bookmarkURL, '');
            } else if ((window.sidebar && /Firefox/i.test(navigator.userAgent)) || (window.opera && window.print)) {
                // Firefox version >= 23 and Opera Hotlist

                window.location.href = bookmarkURL;
                alert((/Mac/i.test(navigator.userAgent) ? 'Cmd' : 'Ctrl') + ctlProto.NAVALERT);

            } else if (window.external && ('AddFavorite' in window.external)) {
                // IE Favorite
                window.external.AddFavorite(bookmarkURL, bookmarkTitle);
            } else {
                // Other browsers (mainly WebKit - Chrome/Safari)
                window.location.href = bookmarkURL;
                alert((/Mac/i.test(navigator.userAgent) ? 'Cmd' : 'Ctrl') + ctlProto.NAVALERT);
            }

            return false;
        }));

        //Cuando se añada o borre una capa, comprobamos de nuevo si la URL cumple los requisitos de longitud para habilitar el control
        //map.on(TC.Consts.event.MAPLOAD, function () {
        //    map.on(TC.Consts.event.LAYERREMOVE + ' ' + TC.Consts.event.LAYERADD, function (e) {
        //        self.generateLink();
        //    });
        //});

        return result;
    };

    ctlProto.template = {};
    ctlProto.template[ctlProto.CLASS] = TC.apiLocation + "TC/templates/tc-ctl-share.hbs";
    ctlProto.template[ctlProto.CLASS + '-dialog'] = TC.apiLocation + "TC/templates/tc-ctl-share-dialog.hbs";

    ctlProto.generateLink = async function () {
        const self = this;
        const shareIconContainer = self.div.querySelector('.ga-share-icons');
        const textboxes = self.div.querySelectorAll(`.${self.CLASS}-url-box tc-textbox`);
        const buttons = self.div.querySelectorAll(`.${self.CLASS}-url-box button`);
        const alert = self.div.querySelector('.' + self.CLASS + '-alert');
        shareIconContainer.classList.add(TC.Consts.classes.LOADING);
        textboxes.forEach(tb => tb.disabled = true);
        buttons.forEach(btn => {
            btn.classList.add(TC.Consts.classes.LOADING);
            btn.disabled = true;
        });
        alert.classList.add(TC.Consts.classes.LOADING);
        const result = await TC.control.MapInfo.prototype.generateLink.call(self);
        textboxes.forEach(tb => tb.disabled = false);
        buttons.forEach(btn => {
            btn.disabled = false;
            btn.classList.remove(TC.Consts.classes.LOADING);
        });
        shareIconContainer.classList.remove(TC.Consts.classes.LOADING);
        alert.classList.remove(TC.Consts.classes.LOADING);
        return result;
    };

})();<commit_msg>Add update method<commit_after>TC.control = TC.control || {};

if (!TC.control.MapInfo) {
    TC.syncLoadJS(TC.apiLocation + 'TC/control/MapInfo');
}

TC.control.Share = function (options) {
    var self = this;
    self._classSelector = '.' + self.CLASS;

    TC.Control.apply(self, arguments);

    self.exportsState = true;

    var opts = options || {};
    self._dialogDiv = TC.Util.getDiv(opts.dialogDiv);
    if (window.$) {
        self._$dialogDiv = $(self._dialogDiv);
    }
    if (!opts.dialogDiv) {
        document.body.appendChild(self._dialogDiv);
    }

    self.render();
};

TC.inherit(TC.control.Share, TC.control.MapInfo);

(function () {
    var ctlProto = TC.control.Share.prototype;

    ctlProto.CLASS = 'tc-ctl-share';
    ctlProto.MAILTO_MAX_LENGTH = 256;
    ctlProto.IFRAME_WIDTH = '600px';
    ctlProto.IFRAME_HEIGHT = '450px';
    ctlProto.FEATURE_PARAM = 'showfeature';

    ctlProto.MOBILEFAV = 'Siga las instrucciones del navegador del dispositivo móvil para añadir como favorito. Se guardará el estado actual del mapa.';
    ctlProto.NAVALERT = ' +D para guardar en marcadores.';

    ctlProto.render = function (callback) {
        const self = this;
        return self.getRenderedHtml(self.CLASS + '-dialog', null, function (html) {
            self._dialogDiv.innerHTML = html;
        }).then(function () {
            return TC.Control.prototype.renderData.call(self, { controlId: self.id }, function () {
                //Si el navegador no soporta copiar al portapapeles, ocultamos el botón de copiar
                if (document.queryCommandSupported && document.queryCommandSupported('copy')) {
                    self.div.querySelectorAll('button').forEach(function (btn) {
                        btn.classList.remove('hide');
                    });
                    self.div.querySelectorAll('input[type=text]').forEach(function (input) {
                        delete input.dataset.dataOriginalTitle;
                    });
                }

                // Si el SO no es móvil, ocultamos el botón de compartir a WhatsApp
                if (!TC.Util.detectMobile()) {
                    self.div.querySelector(".share-whatsapp").classList.add(TC.Consts.classes.HIDDEN);
                }

                const options = self.div.querySelectorAll('.' + self.CLASS + '-url-box');
                self.div.querySelectorAll('span:not(.tc-beta)').forEach(function (span) {
                    span.addEventListener(TC.Consts.event.CLICK, function (e) {
                        var label = this;
                        while (label && label.tagName !== 'LABEL') {
                            label = label.parentElement;
                        }
                        const newFormat = label.querySelector(`input[type=radio][name="${self.id}-format"]`).value;

                        options.forEach(function (option) {
                            option.classList.toggle(TC.Consts.classes.HIDDEN, !option.matches('.tc-' + newFormat));
                        });
                    }, { passive: true });
                });
                if (TC.Util.isFunction(callback)) {
                    callback();
                }
            });
        });
    };

    /*
     * Obtiene una URL "limpia" para compartir el mapa.
     */
    ctlProto.getLocation = function () {
        var result = window.location.href;
        if (window.location.hash) {
            result = result.substr(0, result.indexOf(window.location.hash));
        }
        return result;
    };

    ctlProto.manageMaxLengthExceed = function (maxLengthExceed) {
        const self = this;

        const browserAlert = self.div.querySelector('.' + self.CLASS + '-alert');
        //Si la URL sobrepasa el tamaño máximo avisamos que puede fallar en Edge
        browserAlert.classList.toggle(TC.Consts.classes.HIDDEN, !maxLengthExceed.browser);

        const qrAlert = self._dialogDiv.querySelector('.' + self.CLASS + '-qr-alert');
        qrAlert.classList.toggle(TC.Consts.classes.HIDDEN, !maxLengthExceed.qr);
    };

    ctlProto.generateIframe = async function (url) {
        const self = this;
        const urlString = url || await self.generateLink();
        if (urlString) {
            return '<iframe style="width:' + self.IFRAME_WIDTH + ';height:' + self.IFRAME_HEIGHT + ';" src="' + urlString + '"></iframe>';
        }
        return '';
    }

    ctlProto.loadParamFeature = function () {
        const self = this;
        var result = null;
        var featureToShow = TC.Util.getParameterByName(self.FEATURE_PARAM);
        if (featureToShow) {
            var featureObj;
            try {
                featureObj = JSON.parse(decodeURIComponent(escape(window.atob(featureToShow))));
            }
            catch (error) {
                TC.error(self.getLocaleString('sharedFeatureNotValid'), TC.Consts.msgErrorMode.TOAST);
            }
            if (featureObj && self.map) {
                if (featureObj.geom) {
                    self.map.addLayer({
                        id: self.getUID(),
                        type: TC.Consts.layerType.VECTOR,
                        title: self.getLocaleString('foi'),
                        owner: self,
                        stealth: true
                    }).then(function (layer) {
                        self.paramFeatureLayer = layer;
                        layer.importState({ features: [featureObj] }).then(function () {
                            self.map.zoomToFeatures(layer.features);
                        });
                    });
                }
                else {
                    result = featureObj;
                }
            }
        }
        return result;
    };

    ctlProto.update = async function () {
        const self = this;
        const input = self.div.querySelector('.tc-url input[type=text]');
        if (input.value.trim().length === 0) {
            const link = await self.generateLink();
            self.registerListeners();
            input.value = link;
            self.div.querySelector('.tc-iframe input[type=text]').value = await self.generateIframe(link);
        }
    };

    ctlProto.register = function (map) {
        const self = this;

        const result = TC.control.MapInfo.prototype.register.call(self, map);

        result.then(function () {

            self.exportsState = true;

            self.MOBILEFAV = self.getLocaleString('mobileBookmarks.instructions');
            self.NAVALERT = self.getLocaleString('bookmarks.instructions');

            self.div.addEventListener('click', TC.EventTarget.listenerBySelector('h2', function (evt) {
                self.update();
            }));

            self.map.on(TC.Consts.event.MAPCHANGE, function (e) {
                const self = this;
                const input = self.div.querySelector('.tc-url input[type=text]');
                if (input) {
                    input.dataset["update"] = true;
                }

                delete self.map._controlStatesCache;
            }.bind(self));

            var selectInputField = async function (elm, shorten) {
                const input = elm.parentElement.querySelector("input[type=text]");

                if (shorten) {
                    if (input.dataset["update"] || !input.dataset["shortened"]) {
                        self.shortenedLink()
                            .then(async function (value) {
                                if (value && value.trim().length > 0) {

                                    elm.textContent = self.getLocaleString('shortened');
                                    setTimeout(function () {
                                        elm.textContent = self.getLocaleString('shorten');
                                        unselectInputField();
                                    }, 1000);

                                    delete input.dataset["update"];
                                    input.dataset["shortened"] = true;
                                    input.value = value;

                                    input.select();

                                } else {
                                    delete input.dataset["update"];
                                    delete input.dataset["shortened"];
                                    input.value = await self.generateLink();
                                }
                            });
                    }
                } else {
                    if (!input.classList.contains('tc-url')) {
                        input.value = await self.generateIframe();
                    } else {
                        if (input.dataset["update"]) {
                            delete input.dataset["update"];
                            delete input.dataset["shortened"];
                            input.value = await self.generateLink();
                        }
                    }

                    input.select();
                }
            };

            var unselectInputField = function () {
                document.getSelection().removeAllRanges();
                document.getSelection().addRange(document.createRange());
            };

            self.div.addEventListener('click', TC.EventTarget.listenerBySelector('.tc-ctl-share-url-box button', function (evt) {
                const btn = evt.target;
                const isCopyBtn = !(btn.classList.contains('shorten'));

                const copy = async function (value) {
                    await selectInputField(btn);

                    document.execCommand('copy');

                    btn.textContent = self.getLocaleString('copied');

                    setTimeout(function () {
                        btn.textContent = self.getLocaleString((isCopyBtn ? 'copy' : 'shorten'));
                        unselectInputField();
                    }, 1000);
                };

                if (!isCopyBtn) {
                    selectInputField(btn, true);
                } else {
                    copy();
                }
            }));

            self.div.addEventListener('click', TC.EventTarget.listenerBySelector('input[type=text]', function (evt) {
                selectInputField(evt.target);
            }));

            //Deshabilitar el click de ratón en los enlaces de compartir cuando están deshabilitados
            self.div.addEventListener('click', TC.EventTarget.listenerBySelector('.ga-share-icon.tc-disabled', function (evt) {
                evt.stopImmediatePropagation();
                evt.preventDefault();
                return false;
            }));

            //Enviar por e-mail
            self.div.addEventListener('click', TC.EventTarget.listenerBySelector('a.share-email', async function (evt) {
                evt.preventDefault();
                self.shortenedLink().then(function (url) {
                    if (url) {
                        const body = encodeURIComponent(url + "\n");
                        if (body.length > self.MAILTO_MAX_LENGTH) {
                            map.toast(self.getLocaleString('urlTooLongForMailto'), { type: TC.Consts.msgType.WARNING });
                        }
                        window.location.href = 'mailto:?body=' + body;
                    }
                });
            }));

            //Generar código QR
            self.div.addEventListener("click", TC.EventTarget.listenerBySelector("a.qr-generator", function (evt) {
                evt.preventDefault();
                const qrContainer = self._dialogDiv.querySelector(".qrcode");
                qrContainer.innerHTML = '';

                if (self._dialogDiv.querySelector('.' + self.CLASS + '-qr-alert').classList.contains(TC.Consts.classes.HIDDEN)) {
                    self.makeQRCode(qrContainer, 256, 256).then(function (qrCodeBase64) {
                        if (qrCodeBase64) {
                            TC.Util.showModal(self._dialogDiv.querySelector(self._classSelector + '-qr-dialog'));
                        }
                    });
                } else {
                    TC.Util.showModal(self._dialogDiv.querySelector(self._classSelector + '-qr-dialog'));
                }
            }));

            const openSocialMedia = function (win, url, process) {
                if (url && url.trim().length > 0) {
                    win.location.href = process(url);
                } else {
                    TC.error(self.getLocaleString('urlTooLongForShortener'));
                    win.close();
                }
            };

            //Compartir en Facebook
            self.div.addEventListener("click", TC.EventTarget.listenerBySelector("a.share-fb", function (evt) {
                evt.preventDefault();

                const w = window.open();
                self.shortenedLink().then(function (url) {
                    openSocialMedia(w, url, function (url) {
                        return "https://www.facebook.com/sharer/sharer.php?u=" + encodeURIComponent(url)
                    });
                });

                return false;
            }));

            //Compartir en Twitter
            self.div.addEventListener("click", TC.EventTarget.listenerBySelector("a.share-twitter", function (evt) {
                evt.preventDefault();

                const w = window.open();
                self.shortenedLink().then(function (url) {
                    openSocialMedia(w, url, function (url) {
                        var titulo = encodeURIComponent(window.document.title ? window.document.title : "Visor API SITNA");
                        return "https://twitter.com/intent/tweet?text=" + titulo + "&url=" + encodeURIComponent(url);
                    });
                });

                return false;
            }));

            //Compartir en Whatsapp
            if (TC.Util.detectMobile()) {
                self.div.addEventListener("click", TC.EventTarget.listenerBySelector("a.share-whatsapp", function (evt) {
                    evt.preventDefault();

                    self.shortenedLink().then(async function (url) {
                        var waText = 'whatsapp://send?text=';
                        if (url !== undefined) {
                            location.href = waText + encodeURIComponent(url);
                        } else {
                            location.href = waText + encodeURIComponent(await self.generateLink());
                        }
                    });

                    return false;
                }));
            }

            //Guardar en marcadores
            self.div.addEventListener("click", TC.EventTarget.listenerBySelector("a.share-star", async function (evt) {
                evt.preventDefault();

                var bookmarkURL = await self.generateLink();
                var bookmarkTitle = document.title;

                if (TC.Util.detectMobile()) {
                    // Mobile browsers
                    alert(ctlProto.MOBILEFAV);
                } else if (window.sidebar && window.sidebar.addPanel) {
                    // Firefox version < 23
                    window.sidebar.addPanel(bookmarkTitle, bookmarkURL, '');
                } else if ((window.sidebar && /Firefox/i.test(navigator.userAgent)) || (window.opera && window.print)) {
                    // Firefox version >= 23 and Opera Hotlist

                    window.location.href = bookmarkURL;
                    alert((/Mac/i.test(navigator.userAgent) ? 'Cmd' : 'Ctrl') + ctlProto.NAVALERT);

                } else if (window.external && ('AddFavorite' in window.external)) {
                    // IE Favorite
                    window.external.AddFavorite(bookmarkURL, bookmarkTitle);
                } else {
                    // Other browsers (mainly WebKit - Chrome/Safari)
                    window.location.href = bookmarkURL;
                    alert((/Mac/i.test(navigator.userAgent) ? 'Cmd' : 'Ctrl') + ctlProto.NAVALERT);
                }

                return false;
            }));

            //Cuando se añada o borre una capa, comprobamos de nuevo si la URL cumple los requisitos de longitud para habilitar el control
            //map.on(TC.Consts.event.MAPLOAD, function () {
            //    map.on(TC.Consts.event.LAYERREMOVE + ' ' + TC.Consts.event.LAYERADD, function (e) {
            //        self.generateLink();
            //    });
            //});

            map.loaded(() => {
                if (!self.div.classList.contains(TC.Consts.classes.COLLAPSED)) {
                    self.update();
                }
            });
        });

        return result;
    };

    ctlProto.template = {};
    ctlProto.template[ctlProto.CLASS] = TC.apiLocation + "TC/templates/tc-ctl-share.hbs";
    ctlProto.template[ctlProto.CLASS + '-dialog'] = TC.apiLocation + "TC/templates/tc-ctl-share-dialog.hbs";

    ctlProto.generateLink = async function () {
        const self = this;
        const shareIconContainer = self.div.querySelector('.ga-share-icons');
        const textboxes = self.div.querySelectorAll(`.${self.CLASS}-url-box tc-textbox`);
        const buttons = self.div.querySelectorAll(`.${self.CLASS}-url-box button`);
        const alert = self.div.querySelector('.' + self.CLASS + '-alert');
        shareIconContainer.classList.add(TC.Consts.classes.LOADING);
        textboxes.forEach(tb => tb.disabled = true);
        buttons.forEach(btn => {
            btn.classList.add(TC.Consts.classes.LOADING);
            btn.disabled = true;
        });
        alert.classList.add(TC.Consts.classes.LOADING);
        const result = await TC.control.MapInfo.prototype.generateLink.call(self);
        textboxes.forEach(tb => tb.disabled = false);
        buttons.forEach(btn => {
            btn.disabled = false;
            btn.classList.remove(TC.Consts.classes.LOADING);
        });
        shareIconContainer.classList.remove(TC.Consts.classes.LOADING);
        alert.classList.remove(TC.Consts.classes.LOADING);
        return result;
    };

})();<|endoftext|>


<commit_before>/*    Copyright 2017 - 2020 Firewalla Inc
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * Created by Melvin Tu on 05/01/2017.
 */

/*
 * WARNING:
 *   UPNP operations must be isolated to be one process.  NATPMP requires
 *   openning a port, which may cause trouble if two processes are doing
 *   the same
 */

'use strict';

let instance = null;

const log = require("../../net2/logger.js")(__filename);
const util = require('util');

const sysManager = require('../../net2/SysManager.js');

const _ = require('lodash');

const natpmp = require('./nat-pmp');
const natupnp = require('./nat-upnp');
const natpmpTimeout = 86400;

const sclient = require('../../util/redis_manager.js').getSubscriptionClient();
const Message = require('../../net2/Message.js');
const f = require('../../net2/Firewalla.js');
const ip = require('ip');
const mode = require('../../net2/Mode.js');

let registeredUpnpMappings = [];
const upnpCheckInterval = 15 * 60 * 1000 // 15 mins

module.exports = class {
  constructor() {
    if (instance == null) {
      // WAN UPnP/NATPMP client is used to add/remove mappings
      this.wanUpnpClient = null;
      this.wanNatPmpClient = null;

      // monitorUpnpClients are used to query mappings, i.e. read-only
      this.monitoredUpnpClients = [];

      instance = this;
      this.refreshTimers = {};

      this.scheduleReload();

      // TODO: move this to UPNPSensor
      // periodical checks whether all upnp mappings registered are alive
      // if not, adds back
      if (f.isMain()) {
        this.upnpIntervalHandler = setInterval(
          async () => {
            log.info("UPnP periodical check starts")
            if (registeredUpnpMappings.isEmpty) {
              log.info("No mapping registered.")
              return;
            }
            const results = await this.getPortMappingsUPNP().catch((err) => {
              log.error("Failed to get current mappings", err);
              return null;
            });
            if (results) {
              log.info("Current mappings: ", results);
              registeredUpnpMappings.forEach((check) => {
                log.info("Checking registered mapping:", check);
                if (_.isEmpty(
                  results.find((m) => this.mappingCompare(m, check))
                )) {
                  log.info("Mapping no longer exists, adding back to router...")
                  let { protocol, localPort, externalPort, description } = check;
                  this.addPortMappingUPNP(protocol, localPort, externalPort, description)
                } else {
                  log.info("Mapping still exists")
                }
              })
            }
          },
          upnpCheckInterval
        );

        sclient.on("message", (channel, message) => {
          if (channel === Message.MSG_SYS_NETWORK_INFO_RELOADED) {
            log.info("Schedule reload upnp clients since network info is reloaded");
            this.scheduleReload();
          }
        });
        sclient.subscribe(Message.MSG_SYS_NETWORK_INFO_RELOADED);
      }
    }
    return instance;
  }

  scheduleReload() {
    if (this.reloadTask)
      clearTimeout(this.reloadTask);
    this.reloadTask = setTimeout(async () => {
      if (this.wanNatPmpClient)
        this.wanNatPmpClient.close();
      this.wanNatPmpClient = null;
      if (this.wanUpnpClient)
        this.wanUpnpClient.close();
      this.wanUpnpClient = null;
      for (const c of this.monitoredUpnpClients) {
        c.close();
      }
      this.monitoredUpnpClients = [];
      // check availability of UPnP
      const defaultWanIp = sysManager.myDefaultWanIp();
      if (defaultWanIp && ip.isPrivate(defaultWanIp) && !(await mode.isRouterModeOn())) {
        const wanUpnpClient = natupnp.createClient({listenAddr: defaultWanIp});
        wanUpnpClient.externalIp((err, ip) => {
          if (err || ip == null) {
            log.info(`UPnP write client does not work on ${defaultWanIp}`);
            wanUpnpClient.close();
            this.wanUpnpClient = null;
          } else {
            this.wanUpnpClient = wanUpnpClient;
          }
        });
      }
      // check availability of NATPMP
      const defaultGateway = sysManager.myDefaultGateway();
      if (defaultGateway && ip.isPrivate(defaultWanIp) && !(await mode.isRouterModeOn())) {
        const wanNatPmpClient = natpmp.connect(defaultGateway);
        wanNatPmpClient.on('error', (err) => {
          log.error(`NATPMP write clien does not work on gw ${defaultGateway}`, err);
          wanNatPmpClient.close();
          this.wanNatPmpClient = null;
        });
        if (wanNatPmpClient) {
          wanNatPmpClient.externalIp((err, info) => {
            if (err || info == null) {
              log.info(`NATPMP write client does not work on gw ${defaultGateway}`);
              wanNatPmpClient.close();
              this.wanNatPmpClient = null;
            } else {
              this.wanNatPmpClient = wanNatPmpClient;
            }
          });
        }
      }

      // initalize read-only upnp clients
      const monitoringInterfaces = sysManager.getMonitoringInterfaces();
      for (const iface of monitoringInterfaces) {
        if (iface.name.endsWith(":0"))
          continue;
        if (!iface.ip_address)
          continue;
        const upnpClient = natupnp.createClient({listenAddr: iface.ip_address});
        upnpClient.externalIp((err, ip) => {
          if (err || ip == null) {
            log.info(`UPnP monitor client does not work on ${iface.ip_address}`);
            upnpClient.close();
          } else {
            this.monitoredUpnpClients.push(upnpClient);
          }
        });
      }

    }, 5000);
  }

  addPortMapping(protocol, localPort, externalPort, description, callback) {
    protocol = protocol.toLowerCase()
    callback = callback || function() { };

    try {
      if (this.wanUpnpClient) {
        return this.addPortMappingUPNP(this.wanUpnpClient, protocol, localPort, externalPort, description, callback);
      }
      if (this.wanNatPmpClient) {
        return this.addPortMappingNATPMP(this.wanNatPmpClient, protocol, localPort, externalPort, description, callback);
      }
      log.warn("Neither UPnP nor NATPMP write client works");
      callback(null);
    } catch (err) {
      log.error("Failed to add port mapping", err);
      callback(err);
    }
  }

  addPortMappingUPNP(client, protocol, localPort, externalPort, description, callback) {
    protocol = protocol.toLowerCase()
    callback = callback || function () { };

    client.portMapping({
      type: protocol,
      protocol: protocol,
      private: { host: sysManager.myDefaultWanIp(), port: localPort },
      public: externalPort,
      ttl: 0, // set ttl to 0 for better compatibility
      description: description
    }, (err) => {
      if (err) {
        log.error("Failed to add port mapping ", description, " :", err);
        callback(err);
        return;
      }
      log.info(util.format("Port mapping [%s, %s, %s] is added successfully.",
        protocol, localPort, externalPort));

      let mappingObj = { protocol, localPort, externalPort, description };

      // check if mapping registered
      if (_.isEmpty(registeredUpnpMappings.find((m) =>
        m.localPort     == localPort &&
        m.externalPort  == externalPort &&
        m.protocol      === protocol
      ))) {
        registeredUpnpMappings.push(mappingObj);
      } else {
        log.info("Mapping handler already exists");
      }

      callback();
    });
  }

  addPortMappingNATPMP(client, protocol, localPort, externalPort, description, callback) {
    protocol = protocol.toLowerCase()
    callback = callback || function () { };

    client.portMapping({ type: protocol, private: localPort, public: externalPort, ttl: natpmpTimeout }, (err, info) => {
      if (err == null) {
        this.refreshTimers[localPort + ":" + externalPort] = setTimeout(() => {
          this.addPortMappingNATPMP(protocol, localPort, externalPort, description, () => {
          });
        }, natpmpTimeout / 2 * 1000);
      }
      callback(err, info);
    });
  }

  removePortMappingNATPMP(client, protocol, localPort, externalPort, callback) {
    protocol = protocol.toLowerCase()
    callback = callback || function () { };
    let timer = this.refreshTimers[localPort + ":" + externalPort];
    if (timer) {
      clearTimeout(timer);
    }
    client.portUnmapping({ type: protocol, private: localPort, public: externalPort, ttl: 0 }, (err, info) => {
      if (err) {
        log.error("UPNP.removePortMappingNATPMP", err);
      }
      callback(err, info);
    });
  }

  removePortMapping(protocol, localPort, externalPort, callback) {
    protocol = protocol.toLowerCase()
    callback = callback || function () { };

    try {
      if (this.wanUpnpClient) {
        return this.removePortMappingUPNP(this.wanUpnpClient, protocol, localPort, externalPort, callback);
      }
      if (this.wanNatPmpClient) {
        return this.removePortMappingNATPMP(this.wanNatPmpClient, protocol, localPort, externalPort, callback);
      }
      log.warn("Neither UPnP nor NATPMP write client works");
      callback(null);
    } catch (err) {
      log.error("Failed to remove port mapping", err);
      callback(err);
    }
  }

  removePortMappingUPNP(client, protocol, localPort, externalPort, callback) {
    protocol = protocol.toLowerCase()
    callback = callback || function () { };

    client.portUnmapping({
      protocol: protocol,
      private: { host: sysManager.myDefaultWanIp(), port: localPort },
      public: externalPort
    }, (err) => {
      if (err) {
        log.error(`UPNP Failed to remove port mapping [${protocol}, ${localPort}, ${externalPort}]: ` + err);
        if (callback) {
          callback(err);
        }
        return;
      }

      registeredUpnpMappings = _.reject(registeredUpnpMappings, (m) =>
        m.localPort     == localPort &&
        m.externalPort  == externalPort &&
        m.protocol      === protocol
      );

      log.info(util.format("Port mapping [%s, %s, %s] is removed successfully"
        , protocol, localPort, externalPort));

      if (callback) {
        callback();
      }
    });
  }

  getLocalPortMappings(description, callback) {
    callback = callback || function() {};
    this.getPortMappingsUPNP().then((result) => {
      callback(null, result)
    }).catch((err) => {
      callback(err, null);
    });
  }

  async getPortMappingsUPNP() {
    let results = [];
    for (const c of this.monitoredUpnpClients) {
      const getMappingsAsync = util.promisify(c.getMappings).bind(c);
      const mappings = await getMappingsAsync().catch((err) => []);
      results = results.concat(mappings);
    }
    return results;
  }

  getRegisteredUpnpMappings() {
    return registeredUpnpMappings;
  }

  mappingCompare(natUpnpMapping, localMapping) {
    // "==" is used instead of "===" with intention here to enable comparison between number and string
    return  natUpnpMapping.public.port   ==  localMapping.externalPort &&
      natUpnpMapping.private.port  ==  localMapping.localPort &&
      natUpnpMapping.protocol      === localMapping.protocol;
  }

  async getExternalIP() {
    return new Promise((resolve, reject) => {
      if (this.wanUpnpClient) {
        this.wanUpnpClient.externalIp((err, ip) => {
          if(err) {
            reject(err);
          } else {
            resolve(ip);
          }
        })
      } else {
        resolve(null);
      }
    });
  }
}

<commit_msg>do not close upnp client even if external IP is not available during initialization<commit_after>/*    Copyright 2017 - 2020 Firewalla Inc
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * Created by Melvin Tu on 05/01/2017.
 */

/*
 * WARNING:
 *   UPNP operations must be isolated to be one process.  NATPMP requires
 *   openning a port, which may cause trouble if two processes are doing
 *   the same
 */

'use strict';

let instance = null;

const log = require("../../net2/logger.js")(__filename);
const util = require('util');

const sysManager = require('../../net2/SysManager.js');

const _ = require('lodash');

const natpmp = require('./nat-pmp');
const natupnp = require('./nat-upnp');
const natpmpTimeout = 86400;

const sclient = require('../../util/redis_manager.js').getSubscriptionClient();
const Message = require('../../net2/Message.js');
const f = require('../../net2/Firewalla.js');
const ip = require('ip');
const mode = require('../../net2/Mode.js');

let registeredUpnpMappings = [];
const upnpCheckInterval = 15 * 60 * 1000 // 15 mins

module.exports = class {
  constructor() {
    if (instance == null) {
      // WAN UPnP/NATPMP client is used to add/remove mappings
      this.wanUpnpClient = null;
      this.wanNatPmpClient = null;

      // monitorUpnpClients are used to query mappings, i.e. read-only
      this.monitoredUpnpClients = [];

      instance = this;
      this.refreshTimers = {};

      this.scheduleReload();

      // TODO: move this to UPNPSensor
      // periodical checks whether all upnp mappings registered are alive
      // if not, adds back
      if (f.isMain()) {
        this.upnpIntervalHandler = setInterval(
          async () => {
            log.info("UPnP periodical check starts")
            if (registeredUpnpMappings.isEmpty) {
              log.info("No mapping registered.")
              return;
            }
            const results = await this.getPortMappingsUPNP().catch((err) => {
              log.error("Failed to get current mappings", err);
              return null;
            });
            if (results) {
              log.info("Current mappings: ", results);
              registeredUpnpMappings.forEach((check) => {
                log.info("Checking registered mapping:", check);
                if (_.isEmpty(
                  results.find((m) => this.mappingCompare(m, check))
                )) {
                  log.info("Mapping no longer exists, adding back to router...")
                  let { protocol, localPort, externalPort, description } = check;
                  this.addPortMappingUPNP(protocol, localPort, externalPort, description)
                } else {
                  log.info("Mapping still exists")
                }
              })
            }
          },
          upnpCheckInterval
        );

        sclient.on("message", (channel, message) => {
          if (channel === Message.MSG_SYS_NETWORK_INFO_RELOADED) {
            log.info("Schedule reload upnp clients since network info is reloaded");
            this.scheduleReload();
          }
        });
        sclient.subscribe(Message.MSG_SYS_NETWORK_INFO_RELOADED);
      }
    }
    return instance;
  }

  scheduleReload() {
    if (this.reloadTask)
      clearTimeout(this.reloadTask);
    this.reloadTask = setTimeout(async () => {
      if (this.wanNatPmpClient)
        this.wanNatPmpClient.close();
      this.wanNatPmpClient = null;
      if (this.wanUpnpClient)
        this.wanUpnpClient.close();
      this.wanUpnpClient = null;
      for (const c of this.monitoredUpnpClients) {
        c.close();
      }
      this.monitoredUpnpClients = [];
      // check availability of UPnP
      const defaultWanIp = sysManager.myDefaultWanIp();
      if (defaultWanIp && ip.isPrivate(defaultWanIp) && !(await mode.isRouterModeOn())) {
        const wanUpnpClient = natupnp.createClient({listenAddr: defaultWanIp});
        wanUpnpClient.externalIp((err, ip) => {
          if (err || ip == null) {
            log.info(`UPnP write client may not work on WAN ${defaultWanIp}`);
          }
          this.wanUpnpClient = wanUpnpClient;
        });
      }
      // check availability of NATPMP
      const defaultGateway = sysManager.myDefaultGateway();
      if (defaultGateway && ip.isPrivate(defaultWanIp) && !(await mode.isRouterModeOn())) {
        const wanNatPmpClient = natpmp.connect(defaultGateway);
        wanNatPmpClient.on('error', (err) => {
          log.error(`NATPMP write clien does not work on gw ${defaultGateway}`, err);
          wanNatPmpClient.close();
          this.wanNatPmpClient = null;
        });
        if (wanNatPmpClient) {
          wanNatPmpClient.externalIp((err, info) => {
            if (err || info == null) {
              log.info(`NATPMP write client may not work on gw ${defaultGateway}`);
            }
            this.wanNatPmpClient = wanNatPmpClient;
          });
        }
      }

      // initalize read-only upnp clients
      const monitoringInterfaces = sysManager.getMonitoringInterfaces();
      for (const iface of monitoringInterfaces) {
        if (iface.name.endsWith(":0"))
          continue;
        if (!iface.ip_address)
          continue;
        const upnpClient = natupnp.createClient({listenAddr: iface.ip_address});
        upnpClient.externalIp((err, ip) => {
          if (err || ip == null) {
            log.info(`UPnP monitor client may not work on ${iface.ip_address}`);
          }
          this.monitoredUpnpClients.push(upnpClient);
        });
      }

    }, 5000);
  }

  addPortMapping(protocol, localPort, externalPort, description, callback) {
    protocol = protocol.toLowerCase()
    callback = callback || function() { };

    try {
      if (this.wanUpnpClient) {
        return this.addPortMappingUPNP(this.wanUpnpClient, protocol, localPort, externalPort, description, callback);
      }
      if (this.wanNatPmpClient) {
        return this.addPortMappingNATPMP(this.wanNatPmpClient, protocol, localPort, externalPort, description, callback);
      }
      log.warn("Neither UPnP nor NATPMP write client works");
      callback(null);
    } catch (err) {
      log.error("Failed to add port mapping", err);
      callback(err);
    }
  }

  addPortMappingUPNP(client, protocol, localPort, externalPort, description, callback) {
    protocol = protocol.toLowerCase()
    callback = callback || function () { };

    client.portMapping({
      type: protocol,
      protocol: protocol,
      private: { host: sysManager.myDefaultWanIp(), port: localPort },
      public: externalPort,
      ttl: 0, // set ttl to 0 for better compatibility
      description: description
    }, (err) => {
      if (err) {
        log.error("Failed to add port mapping ", description, " :", err);
        callback(err);
        return;
      }
      log.info(util.format("Port mapping [%s, %s, %s] is added successfully.",
        protocol, localPort, externalPort));

      let mappingObj = { protocol, localPort, externalPort, description };

      // check if mapping registered
      if (_.isEmpty(registeredUpnpMappings.find((m) =>
        m.localPort     == localPort &&
        m.externalPort  == externalPort &&
        m.protocol      === protocol
      ))) {
        registeredUpnpMappings.push(mappingObj);
      } else {
        log.info("Mapping handler already exists");
      }

      callback();
    });
  }

  addPortMappingNATPMP(client, protocol, localPort, externalPort, description, callback) {
    protocol = protocol.toLowerCase()
    callback = callback || function () { };

    client.portMapping({ type: protocol, private: localPort, public: externalPort, ttl: natpmpTimeout }, (err, info) => {
      if (err == null) {
        this.refreshTimers[localPort + ":" + externalPort] = setTimeout(() => {
          this.addPortMappingNATPMP(protocol, localPort, externalPort, description, () => {
          });
        }, natpmpTimeout / 2 * 1000);
      }
      callback(err, info);
    });
  }

  removePortMappingNATPMP(client, protocol, localPort, externalPort, callback) {
    protocol = protocol.toLowerCase()
    callback = callback || function () { };
    let timer = this.refreshTimers[localPort + ":" + externalPort];
    if (timer) {
      clearTimeout(timer);
    }
    client.portUnmapping({ type: protocol, private: localPort, public: externalPort, ttl: 0 }, (err, info) => {
      if (err) {
        log.error("UPNP.removePortMappingNATPMP", err);
      }
      callback(err, info);
    });
  }

  removePortMapping(protocol, localPort, externalPort, callback) {
    protocol = protocol.toLowerCase()
    callback = callback || function () { };

    try {
      if (this.wanUpnpClient) {
        return this.removePortMappingUPNP(this.wanUpnpClient, protocol, localPort, externalPort, callback);
      }
      if (this.wanNatPmpClient) {
        return this.removePortMappingNATPMP(this.wanNatPmpClient, protocol, localPort, externalPort, callback);
      }
      log.warn("Neither UPnP nor NATPMP write client works");
      callback(null);
    } catch (err) {
      log.error("Failed to remove port mapping", err);
      callback(err);
    }
  }

  removePortMappingUPNP(client, protocol, localPort, externalPort, callback) {
    protocol = protocol.toLowerCase()
    callback = callback || function () { };

    client.portUnmapping({
      protocol: protocol,
      private: { host: sysManager.myDefaultWanIp(), port: localPort },
      public: externalPort
    }, (err) => {
      if (err) {
        log.error(`UPNP Failed to remove port mapping [${protocol}, ${localPort}, ${externalPort}]: ` + err);
        if (callback) {
          callback(err);
        }
        return;
      }

      registeredUpnpMappings = _.reject(registeredUpnpMappings, (m) =>
        m.localPort     == localPort &&
        m.externalPort  == externalPort &&
        m.protocol      === protocol
      );

      log.info(util.format("Port mapping [%s, %s, %s] is removed successfully"
        , protocol, localPort, externalPort));

      if (callback) {
        callback();
      }
    });
  }

  getLocalPortMappings(description, callback) {
    callback = callback || function() {};
    this.getPortMappingsUPNP().then((result) => {
      callback(null, result)
    }).catch((err) => {
      callback(err, null);
    });
  }

  async getPortMappingsUPNP() {
    let results = [];
    for (const c of this.monitoredUpnpClients) {
      const getMappingsAsync = util.promisify(c.getMappings).bind(c);
      const mappings = await getMappingsAsync().catch((err) => []);
      results = results.concat(mappings);
    }
    return results;
  }

  getRegisteredUpnpMappings() {
    return registeredUpnpMappings;
  }

  mappingCompare(natUpnpMapping, localMapping) {
    // "==" is used instead of "===" with intention here to enable comparison between number and string
    return  natUpnpMapping.public.port   ==  localMapping.externalPort &&
      natUpnpMapping.private.port  ==  localMapping.localPort &&
      natUpnpMapping.protocol      === localMapping.protocol;
  }

  async getExternalIP() {
    return new Promise((resolve, reject) => {
      if (this.wanUpnpClient) {
        this.wanUpnpClient.externalIp((err, ip) => {
          if(err) {
            reject(err);
          } else {
            resolve(ip);
          }
        })
      } else {
        resolve(null);
      }
    });
  }
}

<|endoftext|>


<commit_before>( function( mw, $ ) {"use strict";
	mw.AkamaiMediaAnalytics = function( embedPlayer, callback ) {
		return this.init( embedPlayer, callback );
	}

	mw.AkamaiMediaAnalytics.prototype = {

		// Bind PostFix
		bindPostFix : '.akamaiMediaAnalytics',

		defaultConfigPath : 'http://ma193-r.analytics.edgesuite.net/config/beacon-3431.xml?beaconSentNotify=1',

		defaultConfigPathHTTPS : 'https://ma193-r.analytics.edgekey.net/config/beacon-3898.xml?beaconSentNotify=1',

		defaultJS : 'http://79423.analytics.edgesuite.net/html5/akamaihtml5-min.js',

		defaultJSHTTPS : 'https://79423.analytics.edgekey.net/html5/akamaihtml5-min.js',

		init: function( embedPlayer, callback ) {
			var _this = this;
			this.embedPlayer = embedPlayer;
			// Unbind any existing bindings
			this.embedPlayer.unbindHelper( _this.bindPostFix );

			if ( _this.getConfig( 'trackEventMonitor' ) && window.parent[ _this.getConfig( 'trackEventMonitor' ) ] ) {
				this.trackEventMonitor = window.parent[ _this.getConfig( 'trackEventMonitor' ) ];
			}

			var configPath = this.getConfigPath();
			window.AKAMAI_MEDIA_ANALYTICS_CONFIG_FILE_PATH = configPath;
			window.parent.AKAMAI_MEDIA_ANALYTICS_CONFIG_FILE_PATH = configPath;

			if ( typeof setAkamaiMediaAnalyticsData == 'function' ) {
				// Akamai HTML5 JS is already loaded, don't reload
				_this.setData( embedPlayer );
				callback();
			}
			else {
				var jsSrc = _this.defaultJS;
				if ( this.isHttps() ) {
					jsSrc = _this.defaultJSHTTPS;
				}
				kWidget.appendScriptUrl( jsSrc, function() {
					_this.setData( embedPlayer );
					callback();
				}, window.document );
			}
        },

		setData: function( embedPlayer ) {
			var flavorSrc = embedPlayer.getSource();
			var flavorURL = '';
			if ( flavorSrc ) {
				flavorURL = flavorSrc.src;
			}
			var startIndex = flavorURL.indexOf( '/flavorId/' ) + 10;
			var flavorId = flavorURL.substr( startIndex, flavorURL.indexOf( '/format/' ) - startIndex );

			setAkamaiMediaAnalyticsData( 'publisherId', embedPlayer.kpartnerid );
			setAkamaiMediaAnalyticsData( 'title', embedPlayer.kentryid );
			setAkamaiMediaAnalyticsData( 'playerId', embedPlayer.kuiconfid );
			setAkamaiMediaAnalyticsData( 'flavorId', flavorId );
			setAkamaiMediaAnalyticsData( 'playerVersion', MWEMBED_VERSION );
			setAkamaiMediaAnalyticsData( 'category', this.getMediaTypeName() );
			setAkamaiMediaAnalyticsData( 'contentLength', embedPlayer.evaluate( '{mediaProxy.entry.msDuration}' ) );
			setAkamaiMediaAnalyticsData( 'device', navigator.platform );

			var setPlayerLoadTime = function() {
				setAkamaiMediaAnalyticsData( 'playerLoadtime', embedPlayer.evaluate( '{playerStatusProxy.loadTime}' )  );
			};

            //if we already have load time - set it
			if (embedPlayer.evaluate( '{playerStatusProxy.loadTime}' )) {
				setPlayerLoadTime();
			}
			//else wait for widget load event
			else {
				embedPlayer.bindHelper( 'widgetLoaded',function(){
					setPlayerLoadTime();
				});
			}
		},

		getConfigPath: function() {
			// Check for configuration override
			var configPath = null;
			if ( this.getConfig( 'configPath' ) ) {
				configPath = this.getConfig( 'configPath' );
			}
			// Akamai has a special https url ( does not support protocol relative urls )
			if ( this.isHttps() ) {
				// If configuration override includes https use it
				if ( configPath && ( configPath.indexOf( 'https' ) != -1 ) ) {
					return configPath;
				}
				// If configuration path is not overriden or overriden with insecure URL, use default secure location
				return this.defaultConfigPathHTTPS;
			}
			// The default config path for kaltura akami account
			return this.defaultConfigPath;
		},

		getConfig: function( attr )  {
			return this.embedPlayer.getKalturaConfig( 'akamaiMediaAnalytics', attr );
		},

		/**
		 * Get a media type string
		 */
		getMediaTypeName: function() {
			switch( this.embedPlayer.evaluate( '{mediaProxy.entry.mediaType}' ) ) {
				case 2:
					return 'Image';
					break;
				case 5:
					return 'Audio';
					break;
				case 201:
					return 'Live_Stream_Flash';
					break;
				case 202:
					return 'Live_Stream_Windows_Media';
					break;
				case 203:
					return 'Live_Stream_Real_Media';
					break;
				case 204:
					return 'Live_Stream_Quicktime';
					break;
			}
			// By default return video
			return 'Video';
		},

		isHttps: function() {
			return ( document.location.protocol == 'https:' );
		}
	};
})( window.mw, window.jQuery );
<commit_msg>AkamaiAnalytics: fixed reporting of loadtime<commit_after>( function( mw, $ ) {"use strict";
	mw.AkamaiMediaAnalytics = function( embedPlayer, callback ) {
		return this.init( embedPlayer, callback );
	}

	mw.AkamaiMediaAnalytics.prototype = {

		// Bind PostFix
		bindPostFix : '.akamaiMediaAnalytics',

		defaultConfigPath : 'http://ma193-r.analytics.edgesuite.net/config/beacon-3431.xml?beaconSentNotify=1',

		defaultConfigPathHTTPS : 'https://ma193-r.analytics.edgekey.net/config/beacon-3898.xml?beaconSentNotify=1',

		defaultJS : 'http://79423.analytics.edgesuite.net/html5/akamaihtml5-min.js',

		defaultJSHTTPS : 'https://79423.analytics.edgekey.net/html5/akamaihtml5-min.js',

		init: function( embedPlayer, callback ) {
			var _this = this;
			this.embedPlayer = embedPlayer;
			// Unbind any existing bindings
			this.embedPlayer.unbindHelper( _this.bindPostFix );

			var configPath = this.getConfigPath();
			window.AKAMAI_MEDIA_ANALYTICS_CONFIG_FILE_PATH = configPath;
			window.parent.AKAMAI_MEDIA_ANALYTICS_CONFIG_FILE_PATH = configPath;

			if ( typeof setAkamaiMediaAnalyticsData == 'function' ) {
				// Akamai HTML5 JS is already loaded, don't reload
				_this.setData( embedPlayer );
				callback();
			} else {
				var jsSrc = _this.defaultJS;
				if ( this.isHttps() ) {
					jsSrc = _this.defaultJSHTTPS;
				}
				kWidget.appendScriptUrl( jsSrc, function() {
					_this.setData( embedPlayer );
					callback();
				}, window.document );
			}
        },

		setData: function( embedPlayer ) {
			var _this = this;
			var flavorSrc = embedPlayer.getSource();
			var flavorURL = '';
			if ( flavorSrc ) {
				flavorURL = flavorSrc.src;
			}
			var startIndex = flavorURL.indexOf( '/flavorId/' ) + 10;
			var flavorId = flavorURL.substr( startIndex, flavorURL.indexOf( '/format/' ) - startIndex );

			this.sendAkamaiData( 'publisherId', embedPlayer.kpartnerid );
			this.sendAkamaiData( 'title', embedPlayer.kentryid );
			this.sendAkamaiData( 'playerId', embedPlayer.kuiconfid );
			this.sendAkamaiData( 'flavorId', flavorId );
			this.sendAkamaiData( 'playerVersion', MWEMBED_VERSION );
			this.sendAkamaiData( 'category', this.getMediaTypeName() );
			this.sendAkamaiData( 'contentLength', embedPlayer.evaluate( '{mediaProxy.entry.msDuration}' ) );
			this.sendAkamaiData( 'device', navigator.platform );

			var setPlayerLoadTime = function() {
				_this.sendAkamaiData( 'playerLoadtime', embedPlayer.evaluate( '{playerStatusProxy.loadTime}' )  );
			};

			//if we already have load time - set it
			if (embedPlayer.evaluate( '{playerStatusProxy.loadTime}' )) {
				setPlayerLoadTime();
			}
			//else wait for widget load event
			else {
				embedPlayer.bindHelper( 'playerReady',function(){
					// add a timeout to give the parent frame a chance to update the total load time
					setTimeout(function(){
						setPlayerLoadTime();
					},0);
				});
			}
		},
		sendAkamaiData: function( eventId, data ){
			// send the data with the Akamai method:
			setAkamaiMediaAnalyticsData( eventId, data );
			// log to the trackEventMonitor if not present:
			if ( this.getConfig( 'trackEventMonitor' ) ) {
				try{
					window.parent[ this.getConfig( 'trackEventMonitor' ) ]( eventId, data );
				} catch(e){
					// error could not log event.
				}
			}
		},
		getConfigPath: function() {
			// Check for configuration override
			var configPath = null;
			if ( this.getConfig( 'configPath' ) ) {
				configPath = this.getConfig( 'configPath' );
			}
			// Akamai has a special https url ( does not support protocol relative urls )
			if ( this.isHttps() ) {
				// If configuration override includes https use it
				if ( configPath && ( configPath.indexOf( 'https' ) != -1 ) ) {
					return configPath;
				}
				// If configuration path is not overriden or overriden with insecure URL, use default secure location
				return this.defaultConfigPathHTTPS;
			}
			// The default config path for kaltura akami account
			return this.defaultConfigPath;
		},

		getConfig: function( attr )  {
			return this.embedPlayer.getKalturaConfig( 'akamaiMediaAnalytics', attr );
		},

		/**
		 * Get a media type string
		 */
		getMediaTypeName: function() {
			switch( this.embedPlayer.evaluate( '{mediaProxy.entry.mediaType}' ) ) {
				case 2:
					return 'Image';
					break;
				case 5:
					return 'Audio';
					break;
				case 201:
					return 'Live_Stream_Flash';
					break;
				case 202:
					return 'Live_Stream_Windows_Media';
					break;
				case 203:
					return 'Live_Stream_Real_Media';
					break;
				case 204:
					return 'Live_Stream_Quicktime';
					break;
			}
			// By default return video
			return 'Video';
		},

		isHttps: function() {
			return ( document.location.protocol == 'https:' );
		}
	};
})( window.mw, window.jQuery );
<|endoftext|>


<commit_before>"use strict";

var linter = require("eslint").linter,
    ESLintTester = require("eslint-tester");

var eslintTester = new ESLintTester(linter);

var test = require("../../utils").test;

eslintTester.addRuleTest("lib/rules/default", {
  valid: [
    test({code: "import foo from './empty-folder';"}),
    test({code: "import { foo } from './default-export';"}),
    test({code: "import foo from './default-export';"}),
    test({code: "import foo from './mixed-exports';"}),
    test({
      code: "import bar from './default-export';"}),
    test({
      code: "import CoolClass from './default-class';"}),
    test({
      code: "import bar, { baz } from './default-export';"})
  ],

  invalid: [
    test({
      code: "import crypto from 'crypto';",
      errors: [{message: "No default export found in module.", type: "ImportDefaultSpecifier"}]}),
    test({
      code: "import baz from './named-exports';",
      errors: [{message: "No default export found in module.", type: "ImportDefaultSpecifier"}]}),

    test({
      code: "import bar from './common';",
      errors: [{message: "No default export found in module.", type: "ImportDefaultSpecifier"}]})
  ]
});
<commit_msg>cleanup<commit_after>'use strict'

var linter = require('eslint').linter,
    ESLintTester = require('eslint-tester')

var eslintTester = new ESLintTester(linter)

var test = require('../../utils').test

eslintTester.addRuleTest('lib/rules/default', {
  valid: [
    test({code: 'import foo from "./empty-folder";'}),
    test({code: 'import { foo } from "./default-export";'}),
    test({code: 'import foo from "./default-export";'}),
    test({code: 'import foo from "./mixed-exports";'}),
    test({
      code: 'import bar from "./default-export";'}),
    test({
      code: 'import CoolClass from "./default-class";'}),
    test({
      code: 'import bar, { baz } from "./default-export";'})
  ],

  invalid: [
    test({
      code: 'import crypto from "crypto";',
      errors: [{ message: 'No default export found in module.'
               , type: 'ImportDefaultSpecifier'}]}),
    test({
      code: 'import baz from "./named-exports";',
      errors: [{ message: 'No default export found in module.'
               , type: 'ImportDefaultSpecifier'}]}),

    test({
      code: 'import bar from "./common";',
      errors: [{ message: 'No default export found in module.'
               , type: 'ImportDefaultSpecifier'}]})
  ]
})
<|endoftext|>


<commit_before>Ext.application({
    name: 'subjectadmin',
    appFolder: DevilrySettings.DEVILRY_STATIC_URL + '/subjectadmin/app',

    controllers: [
        'Actions',
        'Shortcuts'
    ],

    launch: function() {
        Ext.create('Ext.container.Viewport', {
            layout: 'fit',
            items: {
                xtype: 'shortcutlist'
            }
        });
    }
});
<commit_msg>apps.subjectadmin...app: Routes.<commit_after>Ext.application({
    name: 'subjectadmin',
    appFolder: DevilrySettings.DEVILRY_STATIC_URL + '/subjectadmin/app',

    controllers: [
        'Actions',
        'Shortcuts'
    ],

    launch: function() {
        this.viewport = Ext.create('Ext.container.Viewport', {
            layout: 'fit'
        });
        this.route = Ext.create('guibase.Router', this);
        this.route.add("", 'shortcutlist');
        this.route.add("/@@dashboard/shortcutlist", 'shortcutlist');
        this.route.add("/:subject/:period", 'period_show');
        this.route.add("/:subject/:period/@@edit", 'period_edit');
        this.route.add("/:subject/:period/:assignment", 'assignment_show');
        this.route.start();
    },

    dashboard: function() {
        this.setView({xtype: 'component', html: 'dashboard'});
    },

    shortcutlist: function() {
        this.setView({xtype: 'shortcutlist'});
    },

    period_show: function(action, subject, period) {
        console.log('PE', action, subject, period);
    },
    period_edit: function(action, subject, period) {
        console.log(action, subject, period);
    },

    assignment_show: function(action, subject, period, assignment) {
        console.log('Assignment', subject, period, assignment);
    },

    routeNotFound: function() {
        this.setView({
            xtype: 'component',
            html: '
Not found

Route not found.
'
        });
    },

    setView: function(component) {
        this.viewport.removeAll();
        this.viewport.add(component);
    }
});
<|endoftext|>