text: /*
   * Copyright (c) 2019 - present Adobe. All rights reserved.
   *
   * Permission is hereby granted, free of charge, to any person obtaining a
   * copy of this software and associated documentation files (the "Software"),
   * to deal in the Software without restriction, including without limitation
   * the rights to use, copy, modify, merge, publish, distribute, sublicense,
   * and/or sell copies of the Software, and to permit persons to whom the
   * Software is furnished to do so, subject to the following conditions:
   *
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
   * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
   * DEALINGS IN THE SOFTWARE.
   *
   */
  
  /**
   *  module for displaying in-app notifications
   *
   */
  define(function (require, exports, module) {
      "use strict";
  
      var AppInit              = brackets.getModule("utils/AppInit"),
          PreferencesManager   = brackets.getModule("preferences/PreferencesManager"),
          ExtensionUtils       = brackets.getModule("utils/ExtensionUtils"),
          ExtensionManager     = brackets.getModule("extensibility/ExtensionManager"),
          HealthLogger         = brackets.getModule("utils/HealthLogger"),
          NotificationBarHtml  = require("text!htmlContent/notificationContainer.html");
  
      ExtensionUtils.loadStyleSheet(module, "styles/styles.css");
  
      // duration of one day in milliseconds
      var ONE_DAY = 1000 * 60 * 60 * 24;
  
      // Init default last notification number
      PreferencesManager.stateManager.definePreference("lastHandledNotificationNumber", "number", 0);
  
      // Init default last info URL fetch time
      PreferencesManager.stateManager.definePreference("lastNotificationURLFetchTime", "number", 0);
  
      /**
       * Constructs notification info URL for XHR
       *
       * @param {string=} localeParam - optional locale, defaults to 'brackets.getLocale()' when omitted.
       * @returns {string} the new notification info url
       */
      function _getVersionInfoUrl(localeParam) {
  
          var locale = localeParam || brackets.getLocale();
  
          if (locale.length > 2) {
              locale = locale.substring(0, 2);
          }
  
          return brackets.config.notification_info_url.replace("<locale>", locale);
      }
  
      /**
       * Get a data structure that has information for all Brackets targeted notifications.
       *
       * _notificationInfoUrl is used for unit testing.
       */
      function _getNotificationInformation(_notificationInfoUrl) {
          // Last time the versionInfoURL was fetched
          var lastInfoURLFetchTime = PreferencesManager.getViewState("lastNotificationURLFetchTime");
  
          var result = new $.Deferred();
          var fetchData = false;
          var data;
  
          // If we don't have data saved in prefs, fetch
          data = PreferencesManager.getViewState("notificationInfo");
          if (!data) {
              fetchData = true;
          }
  
          // If more than 24 hours have passed since our last fetch, fetch again
          if (Date.now() > lastInfoURLFetchTime + ONE_DAY) {
              fetchData = true;
          }
  
          if (fetchData) {
              var lookupPromise = new $.Deferred(),
                  localNotificationInfoUrl;
  
              // If the current locale isn't "en" or "en-US", check whether we actually have a
              // locale-specific notification target, and fall back to "en" if not.
              var locale = brackets.getLocale().toLowerCase();
              if (locale !== "en" && locale !== "en-us") {
                  localNotificationInfoUrl = _notificationInfoUrl || _getVersionInfoUrl();
                  // Check if we can reach a locale specific notifications source
                  $.ajax({
                      url: localNotificationInfoUrl,
                      cache: false,
                      type: "HEAD"
                  }).fail(function (jqXHR, status, error) {
                      // Fallback to "en" locale
                      localNotificationInfoUrl = _getVersionInfoUrl("en");
                  }).always(function (jqXHR, status, error) {
                      lookupPromise.resolve();
                  });
              } else {
                  localNotificationInfoUrl = _notificationInfoUrl || _getVersionInfoUrl("en");
                  lookupPromise.resolve();
              }
  
              lookupPromise.done(function () {
                  $.ajax({
                      url: localNotificationInfoUrl,
                      dataType: "json",
                      cache: false
                  }).done(function (notificationInfo, textStatus, jqXHR) {
                      lastInfoURLFetchTime = (new Date()).getTime();
                      PreferencesManager.setViewState("lastNotificationURLFetchTime", lastInfoURLFetchTime);
                      PreferencesManager.setViewState("notificationInfo", notificationInfo);
                      result.resolve(notificationInfo);
                  }).fail(function (jqXHR, status, error) {
                      // When loading data for unit tests, the error handler is
                      // called but the responseText is valid. Try to use it here,
                      // but *don't* save the results in prefs.
  
                      if (!jqXHR.responseText) {
                          // Text is NULL or empty string, reject().
                          result.reject();
                          return;
                      }
  
                      try {
                          data = JSON.parse(jqXHR.responseText);
                          result.resolve(data);
                      } catch (e) {
                          result.reject();
                      }
                  });
              });
          } else {
              result.resolve(data);
          }
  
          return result.promise();
      }
  
  
      /**
       * Check for notifications, notification overlays are always displayed
       *
       * @return {$.Promise} jQuery Promise object that is resolved or rejected after the notification check is complete.
       */
      function checkForNotification(versionInfoUrl) {
          var result = new $.Deferred();
  
          _getNotificationInformation(versionInfoUrl)
              .done(function (notificationInfo) {
                  // Get all available notifications
                  var notifications = notificationInfo.notifications;
                  if (notifications && notifications.length > 0) {
                      // Iterate through notifications and act only on the most recent
                      // applicable notification
                      notifications.every(function(notificationObj) {
                          // Only show the notification overlay if the user hasn't been
                          // alerted of this notification
                          if (_checkNotificationValidity(notificationObj)) {
                              if (notificationObj.silent) {
                                  // silent notifications, to gather user validity based on filters
                                  HealthLogger.sendAnalyticsData("notification", notificationObj.sequence, "handled");
                              } else {
                                  showNotification(notificationObj);
                              }
                              // Break, we have acted on one notification already
                              return false;
                          }
                          // Continue, we haven't yet got a notification to act on
                          return true;
                      });
                  }
                  result.resolve();
              })
              .fail(function () {
                  // Error fetching the update data. If this is a forced check, alert the user
                  result.reject();
              });
  
          return result.promise();
      }
  
      function _checkPlatform(filters, _platform) {
          return !filters.platforms || filters.platforms.length === 0 || filters.platforms.indexOf(_platform) >=0;
      }
  
      function _checkBuild(filters, _build) {
          return !filters.builds || filters.builds.length === 0 || filters.builds.indexOf(_build) >=0;
      }
  
      function _checkVersion(filters, _version) {
          var re = new RegExp(filters.version);
          return re.exec(_version);
      }
  
      function _checkLocale(filters, _locale) {
          return !filters.locales || filters.locales.length === 0 || filters.locales.indexOf(_locale) >=0;
      }
  
      function _checkExpiry(expiry) {
          return Date.now() <= expiry;
      }
  
      function _checkExtensions(filters) {
          var allExtensions = ExtensionManager.extensions,
              allExtnsMatched = true,
              userExtensionKeys = Object.keys(allExtensions).filter(function(k) {
                  return allExtensions[k].installInfo.locationType === 'user';
              });
  
          if (!filters.extensions) {
              allExtnsMatched = userExtensionKeys.size === 0;
          } else if (filters.extensions.length === 0) {
              allExtnsMatched = userExtensionKeys.length > 0;
          } else {
              var filteredExtns = filters.extensions,
                  extnIterator = null;
              for (var i=0; i < filteredExtns.length; i++) {
                  extnIterator = filteredExtns[i];
                  if (userExtensionKeys.indexOf(extnIterator) === -1) {
                      allExtnsMatched = false;
                      break;
                  }
              }
          }
          return allExtnsMatched;
      }
  
      function _checkNotificationValidity(notificationObj) {
  
          var filters = notificationObj.filters,
              _platform = brackets.getPlatformInfo(),
              _locale = brackets.getLocale(),
              _lastHandledNotificationNumber = PreferencesManager.getViewState("lastHandledNotificationNumber"),
              // Extract current build number from package.json version field 0.0.0-0
              _buildNumber = Number(/-([0-9]+)/.exec(brackets.metadata.version)[1]),
              _version = brackets.metadata.apiVersion;
  
          if(_locale.length > 2) {
              _locale = _locale.substring(0, 2);
          }
  
          return  notificationObj.sequence > _lastHandledNotificationNumber
                  && _checkExpiry(notificationObj.expiry)
                  && _checkPlatform(filters, _platform)
                  && _checkLocale(filters, _locale)
                  && _checkVersion(filters, _version)
                  && _checkBuild(filters, _buildNumber)
                  && _checkExtensions(filters);
      }
  
  
      /**
       * Removes and cleans up the notification bar from DOM
       */
      function cleanNotificationBar() {
          var $notificationBar = $('#notification-bar');
          if ($notificationBar.length > 0) {
              $notificationBar.remove();
          }
      }
  
      /**
       * Displays the Notification Bar UI
       * @param   {object} msgObj - json object containing message info to be displayed
       *
       */
      function showNotification(msgObj) {
          var $htmlContent = $(msgObj.html),
              $notificationBarElement = $(NotificationBarHtml);
  
          // Remove any SCRIPT tag to avoid secuirity issues
          $htmlContent.find('script').remove();
  
          // Remove any STYLE tag to avoid styling impact on Brackets DOM
          $htmlContent.find('style').remove();
  
          cleanNotificationBar(); //Remove an already existing notification bar, if any
          $notificationBarElement.prependTo(".content");
  
          var $notificationBar = $('#notification-bar'),
              $notificationContent = $notificationBar.find('.content-container'),
              $closeIcon = $notificationBar.find('.close-icon');
  
          $notificationContent.append($htmlContent);
          HealthLogger.sendAnalyticsData("notification", msgObj.sequence, "shown");
  
          // Click handlers on actionable elements
          if ($closeIcon.length > 0) {
              $closeIcon.click(function () {
                  cleanNotificationBar();
                  PreferencesManager.setViewState("lastHandledNotificationNumber", msgObj.sequence);
                  HealthLogger.sendAnalyticsData("notification", msgObj.sequence, "dismissedByClose");
              });
          }
  
          if (msgObj.actionables) {
              $(msgObj.actionables).click(function () {
                  cleanNotificationBar();
                  PreferencesManager.setViewState("lastHandledNotificationNumber", msgObj.sequence);
                  HealthLogger.sendAnalyticsData("notification", msgObj.sequence, "dismissedBy" + this.id);
              });
          }
      }
  
  
      AppInit.appReady(function () {
          checkForNotification();
      });
  
      // For unit tests only
      exports.checkForNotification = checkForNotification;
  });
  