diff --git a/app/accounts/views/accountsLoginTwoFactor.html b/app/accounts/views/accountsLoginTwoFactor.html index aa5e8db5..744c9ea7 100644 --- a/app/accounts/views/accountsLoginTwoFactor.html +++ b/app/accounts/views/accountsLoginTwoFactor.html @@ -83,7 +83,8 @@ -
+
diff --git a/app/organization/views/organizationSettings.html b/app/organization/views/organizationSettings.html index 45d66ef2..6e806a09 100644 --- a/app/organization/views/organizationSettings.html +++ b/app/organization/views/organizationSettings.html @@ -63,6 +63,36 @@
+
+
+

Two-step Login Providers

+
+
+
+ + + + + + + + +
+ + {{::provider.name}} + + + {{::provider.name}} +
{{::provider.description}}
+
+ + {{provider.enabled ? 'Enabled' : 'Disabled'}} + +
+
+
+

Import/Export

diff --git a/index.html b/index.html index ad127cbd..51cb1900 100644 --- a/index.html +++ b/index.html @@ -15,9 +15,9 @@ - + - + @@ -35,11 +35,11 @@ integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"> - - + + - - - + + + diff --git a/js/app.min.js b/js/app.min.js index d2a0683e..214bcb58 100644 --- a/js/app.min.js +++ b/js/app.min.js @@ -25,7 +25,7 @@ angular ]); angular.module("bit") -.constant("appSettings", {"apiUri":"/api","identityUri":"/identity","iconsUri":"https://icons.bitwarden.com","stripeKey":"pk_live_bpN0P37nMxrMQkcaHXtAybJk","braintreeKey":"production_qfbsv8kc_njj2zjtyngtjmbjd","selfHosted":false,"version":"1.25.0","environment":"Production"}); +.constant("appSettings", {"apiUri":"/api","identityUri":"/identity","iconsUri":"https://icons.bitwarden.com","stripeKey":"pk_live_bpN0P37nMxrMQkcaHXtAybJk","braintreeKey":"production_qfbsv8kc_njj2zjtyngtjmbjd","selfHosted":false,"version":"1.26.0","environment":"Production"}); angular .module('bit.accounts', ['ui.bootstrap', 'ngCookies']); @@ -40,19 +40,19 @@ angular .module('bit.global', []); angular - .module('bit.reports', ['toastr', 'ngSanitize']); + .module('bit.organization', ['ui.bootstrap']); angular - .module('bit.organization', ['ui.bootstrap']); + .module('bit.reports', ['toastr', 'ngSanitize']); angular .module('bit.services', ['ngResource', 'ngStorage', 'angular-jwt']); angular - .module('bit.tools', ['ui.bootstrap', 'toastr']); + .module('bit.settings', ['ui.bootstrap', 'toastr']); angular - .module('bit.settings', ['ui.bootstrap', 'toastr']); + .module('bit.tools', ['ui.bootstrap', 'toastr']); angular .module('bit.vault', ['ui.bootstrap', 'ngclipboard']); @@ -481,7 +481,8 @@ angular.module('bit') duo: 2, authenticator: 0, email: 1, - remember: 5 + remember: 5, + organizationDuo: 6 }, cipherType: { login: 1, @@ -553,14 +554,15 @@ angular.module('bit') type: 0, name: 'Authenticator App', description: 'Use an authenticator app (such as Authy or Google Authenticator) to generate time-based ' + - 'verification codes.', + 'verification codes.', enabled: false, active: true, free: true, image: 'authapp.png', displayOrder: 0, priority: 1, - requiresUsb: false + requiresUsb: false, + organization: false }, { type: 3, @@ -571,7 +573,8 @@ angular.module('bit') image: 'yubico.png', displayOrder: 1, priority: 3, - requiresUsb: true + requiresUsb: true, + organization: false }, { type: 2, @@ -582,7 +585,8 @@ angular.module('bit') image: 'duo.png', displayOrder: 2, priority: 2, - requiresUsb: false + requiresUsb: false, + organization: false }, { type: 4, @@ -593,7 +597,8 @@ angular.module('bit') image: 'fido.png', displayOrder: 3, priority: 4, - requiresUsb: true + requiresUsb: true, + organization: false }, { type: 1, @@ -605,7 +610,21 @@ angular.module('bit') image: 'gmail.png', displayOrder: 4, priority: 0, - requiresUsb: false + requiresUsb: false, + organization: false + }, + { + type: 6, + name: 'Duo (Organization)', + description: 'Verify with Duo Security for your organization using the Duo Mobile app, SMS, ' + + 'phone call, or U2F security key.', + enabled: false, + active: true, + image: 'duo.png', + displayOrder: 1, + priority: 10, + requiresUsb: false, + organization: true } ], plans: { @@ -873,8 +892,9 @@ angular function init() { stopU2fCheck = true; var params; - if ($scope.twoFactorProvider === constants.twoFactorProvider.duo) { - params = $scope.twoFactorProviders[constants.twoFactorProvider.duo]; + if ($scope.twoFactorProvider === constants.twoFactorProvider.duo || + $scope.twoFactorProvider === constants.twoFactorProvider.organizationDuo) { + params = $scope.twoFactorProviders[$scope.twoFactorProvider]; $window.Duo.init({ host: params.Host, @@ -1140,6 +1160,9 @@ angular $scope.providers = []; + if (providers.hasOwnProperty(constants.twoFactorProvider.organizationDuo)) { + add(constants.twoFactorProvider.organizationDuo); + } if (providers.hasOwnProperty(constants.twoFactorProvider.authenticator)) { add(constants.twoFactorProvider.authenticator); } @@ -2243,45 +2266,6 @@ angular }; }]); -angular - .module('bit.tools') - - .controller('reportsBreachController', ["$scope", "apiService", "toastr", "authService", function ($scope, apiService, toastr, authService) { - $scope.loading = true; - $scope.error = false; - $scope.breachAccounts = []; - $scope.email = null; - - $scope.$on('$viewContentLoaded', function () { - authService.getUserProfile().then(function (userProfile) { - $scope.email = userProfile.email; - return apiService.hibp.get({ email: $scope.email }).$promise; - }).then(function (response) { - var breachAccounts = []; - for (var i = 0; i < response.length; i++) { - var breach = { - id: response[i].Name, - title: response[i].Title, - domain: response[i].Domain, - date: new Date(response[i].BreachDate), - reportedDate: new Date(response[i].AddedDate), - modifiedDate: new Date(response[i].ModifiedDate), - count: response[i].PwnCount, - description: response[i].Description, - classes: response[i].DataClasses, - image: 'https://haveibeenpwned.com/Content/Images/PwnedLogos/' + response[i].Name + '.' + response[i].LogoType - }; - breachAccounts.push(breach); - } - $scope.breachAccounts = breachAccounts; - $scope.loading = false; - }, function (response) { - $scope.error = response.status !== 404; - $scope.loading = false; - }); - }); - }]); - angular .module('bit.organization') @@ -4261,12 +4245,15 @@ angular angular .module('bit.organization') - .controller('organizationSettingsController', ["$scope", "$state", "apiService", "toastr", "authService", "$uibModal", "$analytics", "appSettings", function ($scope, $state, apiService, toastr, authService, $uibModal, - $analytics, appSettings) { + .controller('organizationSettingsController', ["$scope", "$state", "apiService", "toastr", "authService", "$uibModal", "$analytics", "appSettings", "constants", "$filter", function ($scope, $state, apiService, toastr, authService, $uibModal, + $analytics, appSettings, constants, $filter) { $scope.selfHosted = appSettings.selfHosted; $scope.model = {}; + $scope.twoStepProviders = $filter('filter')(constants.twoFactorProviderInfo, { organization: true }); + $scope.use2fa = false; + $scope.$on('$viewContentLoaded', function () { - apiService.organizations.get({ id: $state.params.orgId }, function (org) { + apiService.organizations.get({ id: $state.params.orgId }).$promise.then(function (org) { $scope.model = { name: org.Name, billingEmail: org.BillingEmail, @@ -4277,6 +4264,29 @@ angular businessCountry: org.BusinessCountry, businessTaxNumber: org.BusinessTaxNumber }; + + $scope.use2fa = org.Use2fa; + if (org.Use2fa) { + return apiService.twoFactor.listOrganization({ orgId: $state.params.orgId }).$promise; + } + else { + return null; + } + }).then(function (response) { + if (!response || !response.Data) { + return; + } + + for (var i = 0; i < response.Data.length; i++) { + if (!response.Data[i].Enabled) { + continue; + } + + var provider = $filter('filter')($scope.twoStepProviders, { type: response.Data[i].Type }); + if (provider.length) { + provider[0].enabled = true; + } + } }); }); @@ -4316,6 +4326,32 @@ angular controller: 'organizationDeleteController' }); }; + + $scope.edit = function (provider) { + if (provider.type === constants.twoFactorProvider.organizationDuo) { + typeName = 'Duo'; + } + else { + return; + } + + var modal = $uibModal.open({ + animation: true, + templateUrl: 'app/settings/views/settingsTwoStep' + typeName + '.html', + controller: 'settingsTwoStep' + typeName + 'Controller', + resolve: { + enabled: function () { return provider.enabled; }, + orgId: function () { return $state.params.orgId; } + } + }); + + modal.result.then(function (enabled) { + if (enabled || enabled === false) { + // do not adjust when undefined or null + provider.enabled = enabled; + } + }); + }; }]); angular @@ -5235,9 +5271,16 @@ angular resetSelected(); $scope.selectedCollection = col; $scope.selectedIcon = 'fa-cube'; - $scope.filter = function (c) { - return c.collectionIds && c.collectionIds.indexOf(col.id) > -1; - }; + if (col.id) { + $scope.filter = function (c) { + return c.collectionIds && c.collectionIds.indexOf(col.id) > -1; + }; + } + else { + $scope.filter = function (c) { + return !c.collectionIds || c.collectionIds.length === 0; + }; + } fixLayout(); }; @@ -5449,6 +5492,45 @@ angular } }]); +angular + .module('bit.tools') + + .controller('reportsBreachController', ["$scope", "apiService", "toastr", "authService", function ($scope, apiService, toastr, authService) { + $scope.loading = true; + $scope.error = false; + $scope.breachAccounts = []; + $scope.email = null; + + $scope.$on('$viewContentLoaded', function () { + authService.getUserProfile().then(function (userProfile) { + $scope.email = userProfile.email; + return apiService.hibp.get({ email: $scope.email }).$promise; + }).then(function (response) { + var breachAccounts = []; + for (var i = 0; i < response.length; i++) { + var breach = { + id: response[i].Name, + title: response[i].Title, + domain: response[i].Domain, + date: new Date(response[i].BreachDate), + reportedDate: new Date(response[i].AddedDate), + modifiedDate: new Date(response[i].ModifiedDate), + count: response[i].PwnCount, + description: response[i].Description, + classes: response[i].DataClasses, + image: 'https://haveibeenpwned.com/Content/Images/PwnedLogos/' + response[i].Name + '.' + response[i].LogoType + }; + breachAccounts.push(breach); + } + $scope.breachAccounts = breachAccounts; + $scope.loading = false; + }, function (response) { + $scope.error = response.status !== 404; + $scope.loading = false; + }); + }); + }]); + angular .module('bit.services') @@ -5611,9 +5693,11 @@ angular _service.twoFactor = $resource(_apiUri + '/two-factor', {}, { list: { method: 'GET', params: {} }, + listOrganization: { url: _apiUri + '/organizations/:orgId/two-factor', method: 'GET', params: { orgId: '@orgId' } }, getEmail: { url: _apiUri + '/two-factor/get-email', method: 'POST', params: {} }, getU2f: { url: _apiUri + '/two-factor/get-u2f', method: 'POST', params: {} }, getDuo: { url: _apiUri + '/two-factor/get-duo', method: 'POST', params: {} }, + getOrganizationDuo: { url: _apiUri + '/organizations/:orgId/two-factor/get-duo', method: 'POST', params: { orgId: '@orgId' } }, getAuthenticator: { url: _apiUri + '/two-factor/get-authenticator', method: 'POST', params: {} }, getYubi: { url: _apiUri + '/two-factor/get-yubikey', method: 'POST', params: {} }, sendEmail: { url: _apiUri + '/two-factor/send-email', method: 'POST', params: {} }, @@ -5622,8 +5706,10 @@ angular putU2f: { url: _apiUri + '/two-factor/u2f', method: 'POST', params: {} }, putAuthenticator: { url: _apiUri + '/two-factor/authenticator', method: 'POST', params: {} }, putDuo: { url: _apiUri + '/two-factor/duo', method: 'POST', params: {} }, + putOrganizationDuo: { url: _apiUri + '/organizations/:orgId/two-factor/duo', method: 'POST', params: { orgId: '@orgId' } }, putYubi: { url: _apiUri + '/two-factor/yubikey', method: 'POST', params: {} }, disable: { url: _apiUri + '/two-factor/disable', method: 'POST', params: {} }, + disableOrganization: { url: _apiUri + '/organizations/:orgId/two-factor/disable', method: 'POST', params: { orgId: '@orgId' } }, recover: { url: _apiUri + '/two-factor/recover', method: 'POST', params: {} }, getRecover: { url: _apiUri + '/two-factor/get-recover', method: 'POST', params: {} } }); @@ -5823,6 +5909,7 @@ angular useGroups: profile.Organizations[i].UseGroups, useDirectory: profile.Organizations[i].UseDirectory, useEvents: profile.Organizations[i].UseEvents, + use2fa: profile.Organizations[i].Use2fa, useTotp: profile.Organizations[i].UseTotp }; } @@ -5858,6 +5945,7 @@ angular useGroups: org.UseGroups, useDirectory: org.UseDirectory, useEvents: org.UseEvents, + use2fa: org.Use2fa, useTotp: org.UseTotp }; profile.organizations[o.id] = o; @@ -11317,521 +11405,6 @@ angular return _service; }); -angular - .module('bit.tools') - - .controller('toolsController', ["$scope", "$uibModal", "apiService", "toastr", "authService", function ($scope, $uibModal, apiService, toastr, authService) { - $scope.import = function () { - $uibModal.open({ - animation: true, - templateUrl: 'app/tools/views/toolsImport.html', - controller: 'toolsImportController' - }); - }; - - $scope.export = function () { - $uibModal.open({ - animation: true, - templateUrl: 'app/tools/views/toolsExport.html', - controller: 'toolsExportController' - }); - }; - }]); - -angular - .module('bit.tools') - - .controller('toolsExportController', ["$scope", "apiService", "$uibModalInstance", "cipherService", "$q", "toastr", "$analytics", "constants", function ($scope, apiService, $uibModalInstance, cipherService, $q, - toastr, $analytics, constants) { - $analytics.eventTrack('toolsExportController', { category: 'Modal' }); - $scope.export = function (model) { - $scope.startedExport = true; - var decCiphers = [], - decFolders = []; - - var folderPromise = apiService.folders.list({}, function (folders) { - decFolders = cipherService.decryptFolders(folders.Data); - }).$promise; - - var ciphersPromise = apiService.ciphers.list({}, function (ciphers) { - decCiphers = cipherService.decryptCiphers(ciphers.Data); - }).$promise; - - $q.all([folderPromise, ciphersPromise]).then(function () { - if (!decCiphers.length) { - toastr.error('Nothing to export.', 'Error!'); - $scope.close(); - return; - } - - var foldersDict = {}; - for (var i = 0; i < decFolders.length; i++) { - foldersDict[decFolders[i].id] = decFolders[i]; - } - - try { - var exportCiphers = []; - for (i = 0; i < decCiphers.length; i++) { - // only export logins and secure notes - if (decCiphers[i].type !== constants.cipherType.login && - decCiphers[i].type !== constants.cipherType.secureNote) { - continue; - } - - var cipher = { - folder: decCiphers[i].folderId && (decCiphers[i].folderId in foldersDict) ? - foldersDict[decCiphers[i].folderId].name : null, - favorite: decCiphers[i].favorite ? 1 : null, - type: null, - name: decCiphers[i].name, - notes: decCiphers[i].notes, - fields: null, - // Login props - login_uri: null, - login_username: null, - login_password: null, - login_totp: null - }; - - var j; - if (decCiphers[i].fields) { - for (j = 0; j < decCiphers[i].fields.length; j++) { - if (!cipher.fields) { - cipher.fields = ''; - } - else { - cipher.fields += '\n'; - } - - cipher.fields += ((decCiphers[i].fields[j].name || '') + ': ' + decCiphers[i].fields[j].value); - } - } - - switch (decCiphers[i].type) { - case constants.cipherType.login: - cipher.type = 'login'; - cipher.login_username = decCiphers[i].login.username; - cipher.login_password = decCiphers[i].login.password; - cipher.login_totp = decCiphers[i].login.totp; - - if (decCiphers[i].login.uris && decCiphers[i].login.uris.length) { - cipher.login_uri = []; - for (j = 0; j < decCiphers[i].login.uris.length; j++) { - cipher.login_uri.push(decCiphers[i].login.uris[j].uri); - } - } - break; - case constants.cipherType.secureNote: - cipher.type = 'note'; - break; - default: - continue; - } - - exportCiphers.push(cipher); - } - - var csvString = Papa.unparse(exportCiphers); - var csvBlob = new Blob([csvString]); - - // IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx - if (window.navigator.msSaveOrOpenBlob) { - window.navigator.msSaveBlob(csvBlob, makeFileName()); - } - else { - var a = window.document.createElement('a'); - a.href = window.URL.createObjectURL(csvBlob, { type: 'text/plain' }); - a.download = makeFileName(); - document.body.appendChild(a); - // IE: "Access is denied". - // ref: https://connect.microsoft.com/IE/feedback/details/797361/ie-10-treats-blob-url-as-cross-origin-and-denies-access - a.click(); - document.body.removeChild(a); - } - - $analytics.eventTrack('Exported Data'); - toastr.success('Your data has been exported. Check your browser\'s downloads folder.', 'Success!'); - $scope.close(); - } - catch (err) { - toastr.error('Something went wrong. Please try again.', 'Error!'); - $scope.close(); - } - }, function () { - toastr.error('Something went wrong. Please try again.', 'Error!'); - $scope.close(); - }); - }; - - $scope.close = function () { - $uibModalInstance.dismiss('cancel'); - }; - - function makeFileName() { - var now = new Date(); - var dateString = - now.getFullYear() + '' + padNumber(now.getMonth() + 1, 2) + '' + padNumber(now.getDate(), 2) + - padNumber(now.getHours(), 2) + '' + padNumber(now.getMinutes(), 2) + - padNumber(now.getSeconds(), 2); - - return 'bitwarden_export_' + dateString + '.csv'; - } - - function padNumber(number, width, paddingCharacter) { - paddingCharacter = paddingCharacter || '0'; - number = number + ''; - return number.length >= width ? number : new Array(width - number.length + 1).join(paddingCharacter) + number; - } - }]); - -angular - .module('bit.tools') - - .controller('toolsImportController', ["$scope", "$state", "apiService", "$uibModalInstance", "cryptoService", "cipherService", "toastr", "importService", "$analytics", "$sce", "validationService", function ($scope, $state, apiService, $uibModalInstance, cryptoService, cipherService, - toastr, importService, $analytics, $sce, validationService) { - $analytics.eventTrack('toolsImportController', { category: 'Modal' }); - $scope.model = { source: '' }; - $scope.source = {}; - $scope.splitFeatured = true; - - $scope.options = [ - { - id: 'bitwardencsv', - name: 'Bitwarden (csv)', - featured: true, - sort: 1, - instructions: $sce.trustAsHtml('Export using the web vault (vault.bitwarden.com). ' + - 'Log into the web vault and navigate to "Tools" > "Export".') - }, - { - id: 'lastpass', - name: 'LastPass (csv)', - featured: true, - sort: 2, - instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' + - '' + - 'https://help.bitwarden.com/article/import-from-lastpass/') - }, - { - id: 'chromecsv', - name: 'Chrome (csv)', - featured: true, - sort: 3, - instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' + - '' + - 'https://help.bitwarden.com/article/import-from-chrome/') - }, - { - id: 'firefoxpasswordexportercsvxml', - name: 'Firefox Password Exporter (xml)', - featured: true, - sort: 4, - instructions: $sce.trustAsHtml('Use the ' + - '' + - 'Password Exporter addon for FireFox to export your passwords to a XML file. After installing ' + - 'the addon, type about:addons in your FireFox navigation bar. Locate the Password Exporter ' + - 'addon and click the "Options" button. In the dialog that pops up, click the "Export Passwords" button ' + - 'to save the XML file.') - }, - { - id: 'keepass2xml', - name: 'KeePass 2 (xml)', - featured: true, - sort: 5, - instructions: $sce.trustAsHtml('Using the KeePass 2 desktop application, navigate to "File" > "Export" and ' + - 'select the KeePass XML (2.x) option.') - }, - { - id: 'keepassxcsv', - name: 'KeePassX (csv)', - instructions: $sce.trustAsHtml('Using the KeePassX desktop application, navigate to "Database" > ' + - '"Export to CSV file" and save the CSV file.') - }, - { - id: 'dashlanecsv', - name: 'Dashlane (csv)', - featured: true, - sort: 7, - instructions: $sce.trustAsHtml('Using the Dashlane desktop application, navigate to "File" > "Export" > ' + - '"Unsecured archive (readable) in CSV format" and save the CSV file.') - }, - { - id: '1password1pif', - name: '1Password (1pif)', - featured: true, - sort: 6, - instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' + - '' + - 'https://help.bitwarden.com/article/import-from-1password/') - }, - { - id: '1password6wincsv', - name: '1Password 6 Windows (csv)', - instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' + - '' + - 'https://help.bitwarden.com/article/import-from-1password/') - }, - { - id: 'roboformhtml', - name: 'RoboForm (html)', - instructions: $sce.trustAsHtml('Using the RoboForm Editor desktop application, navigate to "RoboForm" ' + - '(top left) > "Print List" > "Logins". When the following print dialog pops up click on the "Save" button ' + - 'and save the HTML file.') - }, - { - id: 'keepercsv', - name: 'Keeper (csv)', - instructions: $sce.trustAsHtml('Log into the Keeper web vault (keepersecurity.com/vault). Navigate to "Backup" ' + - '(top right) and find the "Export to Text File" option. Click "Export Now" to save the TXT/CSV file.') - }, - { - id: 'enpasscsv', - name: 'Enpass (csv)', - instructions: $sce.trustAsHtml('Using the Enpass desktop application, navigate to "File" > "Export" > ' + - '"As CSV". Select "Yes" to the warning alert and save the CSV file. Note that the importer only fully ' + - 'supports files exported while Enpass is set to the English language, so adjust your settings accordingly.') - }, - { - id: 'safeincloudxml', - name: 'SafeInCloud (xml)', - instructions: $sce.trustAsHtml('Using the SaveInCloud desktop application, navigate to "File" > "Export" > ' + - '"As XML" and save the XML file.') - }, - { - id: 'pwsafexml', - name: 'Password Safe (xml)', - instructions: $sce.trustAsHtml('Using the Password Safe desktop application, navigate to "File" > ' + - '"Export To" > "XML format..." and save the XML file.') - }, - { - id: 'stickypasswordxml', - name: 'Sticky Password (xml)', - instructions: $sce.trustAsHtml('Using the Sticky Password desktop application, navigate to "Menu" ' + - '(top right) > "Export" > "Export all". Select the unencrypted format XML option and then the ' + - '"Save to file" button. Save the XML file.') - }, - { - id: 'msecurecsv', - name: 'mSecure (csv)', - instructions: $sce.trustAsHtml('Using the mSecure desktop application, navigate to "File" > ' + - '"Export" > "CSV File..." and save the CSV file.') - }, - { - id: 'truekeycsv', - name: 'True Key (csv)', - instructions: $sce.trustAsHtml('Using the True Key desktop application, click the gear icon (top right) and ' + - 'then navigate to "App Settings". Click the "Export" button, enter your password and save the CSV file.') - }, - { - id: 'passwordbossjson', - name: 'Password Boss (json)', - instructions: $sce.trustAsHtml('Using the Password Boss desktop application, navigate to "File" > ' + - '"Export data" > "Password Boss JSON - not encrypted" and save the JSON file.') - }, - { - id: 'zohovaultcsv', - name: 'Zoho Vault (csv)', - instructions: $sce.trustAsHtml('Log into the Zoho web vault (vault.zoho.com). Navigate to "Tools" > ' + - '"Export Secrets". Select "All Secrets" and click the "Zoho Vault Format CSV" button. Highlight ' + - 'and copy the data from the textarea. Open a text editor like Notepad and paste the data. Save the ' + - 'data from the text editor as zoho_export.csv.') - }, - { - id: 'splashidcsv', - name: 'SplashID (csv)', - instructions: $sce.trustAsHtml('Using the SplashID Safe desktop application, click on the SplashID ' + - 'blue lock logo in the top right corner. Navigate to "Export" > "Export as CSV" and save the CSV file.') - }, - { - id: 'passworddragonxml', - name: 'Password Dragon (xml)', - instructions: $sce.trustAsHtml('Using the Password Dragon desktop application, navigate to "File" > ' + - '"Export" > "To XML". In the dialog that pops up select "All Rows" and check all fields. Click ' + - 'the "Export" button and save the XML file.') - }, - { - id: 'padlockcsv', - name: 'Padlock (csv)', - instructions: $sce.trustAsHtml('Using the Padlock desktop application, click the hamburger icon ' + - 'in the top left corner and navigate to "Settings". Click the "Export Data" option. Ensure that ' + - 'the "CSV" option is selected from the dropdown. Highlight and copy the data from the textarea. ' + - 'Open a text editor like Notepad and paste the data. Save the data from the text editor as ' + - 'padlock_export.csv.') - }, - { - id: 'clipperzhtml', - name: 'Clipperz (html)', - instructions: $sce.trustAsHtml('Log into the Clipperz web application (clipperz.is/app). Click the ' + - 'hamburger menu icon in the top right to expand the navigation bar. Navigate to "Data" > ' + - '"Export". Click the "download HTML+JSON" button to save the HTML file.') - }, - { - id: 'avirajson', - name: 'Avira (json)', - instructions: $sce.trustAsHtml('Using the Avira browser extension, click your username in the top ' + - 'right corner and navigate to "Settings". Locate the "Export Data" section and click "Export". ' + - 'In the dialog that pops up, click the "Export Password Manager Data" button to save the ' + - 'TXT/JSON file.') - }, - { - id: 'saferpasscsv', - name: 'SaferPass (csv)', - instructions: $sce.trustAsHtml('Using the SaferPass browser extension, click the hamburger icon ' + - 'in the top left corner and navigate to "Settings". Click the "Export accounts" button to ' + - 'save the CSV file.') - }, - { - id: 'upmcsv', - name: 'Universal Password Manager (csv)', - instructions: $sce.trustAsHtml('Using the Universal Password Manager desktop application, navigate ' + - 'to "Database" > "Export" and save the CSV file.') - }, - { - id: 'ascendocsv', - name: 'Ascendo DataVault (csv)', - instructions: $sce.trustAsHtml('Using the Ascendo DataVault desktop application, navigate ' + - 'to "Tools" > "Export". In the dialog that pops up, select the "All Items (DVX, CSV)" ' + - 'option. Click the "Ok" button to save the CSV file.') - }, - { - id: 'meldiumcsv', - name: 'Meldium (csv)', - instructions: $sce.trustAsHtml('Using the Meldium web vault, navigate to "Settings". ' + - 'Locate the "Export data" function and click "Show me my data" to save the CSV file.') - }, - { - id: 'passkeepcsv', - name: 'PassKeep (csv)', - instructions: $sce.trustAsHtml('Using the PassKeep mobile app, navigate to "Backup/Restore". ' + - 'Locate the "CSV Backup/Restore" section and click "Backup to CSV" to save the CSV file.') - }, - { - id: 'operacsv', - name: 'Opera (csv)', - instructions: $sce.trustAsHtml('The process for importing from Opera is exactly the same as ' + - 'importing from Google Chrome. See detailed instructions on our help site at ' + - '' + - 'https://help.bitwarden.com/article/import-from-chrome/') - }, - { - id: 'vivaldicsv', - name: 'Vivaldi (csv)', - instructions: $sce.trustAsHtml('The process for importing from Vivaldi is exactly the same as ' + - 'importing from Google Chrome. See detailed instructions on our help site at ' + - '' + - 'https://help.bitwarden.com/article/import-from-chrome/') - }, - { - id: 'gnomejson', - name: 'GNOME Passwords and Keys/Seahorse (json)', - instructions: $sce.trustAsHtml('Make sure you have python-keyring and python-gnomekeyring installed. ' + - 'Save the GNOME Keyring Import/Export ' + - 'python script by Luke Plant to your desktop as pw_helper.py. Open terminal and run ' + - 'chmod +rx Desktop/pw_helper.py and then ' + - 'python Desktop/pw_helper.py export Desktop/my_passwords.json. Then upload ' + - 'the resulting my_passwords.json file here to Bitwarden.') - } - ]; - - $scope.setSource = function () { - for (var i = 0; i < $scope.options.length; i++) { - if ($scope.options[i].id === $scope.model.source) { - $scope.source = $scope.options[i]; - break; - } - } - }; - $scope.setSource(); - - $scope.import = function (model, form) { - if (!model.source || model.source === '') { - validationService.addError(form, 'source', 'Select the format of the import file.', true); - return; - } - - var file = document.getElementById('file').files[0]; - if (!file && (!model.fileContents || model.fileContents === '')) { - validationService.addError(form, 'file', 'Select the import file or copy/paste the import file contents.', true); - return; - } - - $scope.processing = true; - importService.import(model.source, file || model.fileContents, importSuccess, importError); - }; - - function importSuccess(folders, ciphers, folderRelationships) { - if (!folders.length && !ciphers.length) { - importError('Nothing was imported.'); - return; - } - else if (ciphers.length) { - var halfway = Math.floor(ciphers.length / 2); - var last = ciphers.length - 1; - if (cipherIsBadData(ciphers[0]) && cipherIsBadData(ciphers[halfway]) && cipherIsBadData(ciphers[last])) { - importError('Data is not formatted correctly. Please check your import file and try again.'); - return; - } - } - - apiService.ciphers.import({ - folders: cipherService.encryptFolders(folders), - ciphers: cipherService.encryptCiphers(ciphers), - folderRelationships: folderRelationships - }, function () { - $uibModalInstance.dismiss('cancel'); - $state.go('backend.user.vault', { refreshFromServer: true }).then(function () { - $analytics.eventTrack('Imported Data', { label: $scope.model.source }); - toastr.success('Data has been successfully imported into your vault.', 'Import Success'); - }); - }, importError); - } - - function cipherIsBadData(cipher) { - return (cipher.name === null || cipher.name === '--') && - (cipher.login && (cipher.login.password === null || cipher.login.password === '')); - } - - function importError(error) { - $analytics.eventTrack('Import Data Failed', { label: $scope.model.source }); - $uibModalInstance.dismiss('cancel'); - - if (error) { - var data = error.data; - if (data && data.ValidationErrors) { - var message = ''; - for (var key in data.ValidationErrors) { - if (!data.ValidationErrors.hasOwnProperty(key)) { - continue; - } - - for (var i = 0; i < data.ValidationErrors[key].length; i++) { - message += (key + ': ' + data.ValidationErrors[key][i] + ' '); - } - } - - if (message !== '') { - toastr.error(message); - return; - } - } - else if (data && data.Message) { - toastr.error(data.Message); - return; - } - else { - toastr.error(error); - return; - } - } - - toastr.error('Something went wrong. Try again.', 'Oh No!'); - } - - $scope.close = function () { - $uibModalInstance.dismiss('cancel'); - }; - }]); - angular .module('bit.vault') @@ -13106,7 +12679,7 @@ angular .controller('settingsTwoStepController', ["$scope", "apiService", "toastr", "$analytics", "constants", "$filter", "$uibModal", "authService", function ($scope, apiService, toastr, $analytics, constants, $filter, $uibModal, authService) { - $scope.providers = constants.twoFactorProviderInfo; + $scope.providers = $filter('filter')(constants.twoFactorProviderInfo, { organization: false }); $scope.premium = true; authService.getUserProfile().then(function (profile) { @@ -13163,7 +12736,8 @@ angular templateUrl: 'app/settings/views/settingsTwoStep' + typeName + '.html', controller: 'settingsTwoStep' + typeName + 'Controller', resolve: { - enabled: function () { return provider.enabled; } + enabled: function () { return provider.enabled; }, + orgId: function () { return null; } } }); @@ -13187,8 +12761,8 @@ angular angular .module('bit.settings') - .controller('settingsTwoStepDuoController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "toastr", "$analytics", "constants", "$timeout", function ($scope, apiService, $uibModalInstance, cryptoService, - toastr, $analytics, constants, $timeout) { + .controller('settingsTwoStepDuoController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "toastr", "$analytics", "constants", "$timeout", "orgId", function ($scope, apiService, $uibModalInstance, cryptoService, + toastr, $analytics, constants, $timeout, orgId) { $analytics.eventTrack('settingsTwoStepDuoController', { category: 'Modal' }); var _masterPasswordHash; @@ -13206,9 +12780,16 @@ angular $scope.auth = function (model) { $scope.authPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) { _masterPasswordHash = hash; - return apiService.twoFactor.getDuo({}, { + var requestModel = { masterPasswordHash: _masterPasswordHash - }).$promise; + }; + + if (orgId) { + return apiService.twoFactor.getOrganizationDuo({ orgId: orgId }, requestModel).$promise; + } + else { + return apiService.twoFactor.getDuo({}, requestModel).$promise; + } }).then(function (apiResponse) { processResult(apiResponse); $scope.authed = true; @@ -13229,27 +12810,52 @@ angular return; } - $scope.submitPromise = apiService.twoFactor.disable({}, { - masterPasswordHash: _masterPasswordHash, - type: constants.twoFactorProvider.duo - }, function (response) { - $analytics.eventTrack('Disabled Two-step Duo'); - toastr.success('Duo has been disabled.'); - $scope.enabled = response.Enabled; - $scope.close(); - }).$promise; + if (orgId) { + $scope.submitPromise = apiService.twoFactor.disableOrganization({ orgId: orgId }, { + masterPasswordHash: _masterPasswordHash, + type: constants.twoFactorProvider.organizationDuo + }, function (response) { + $analytics.eventTrack('Disabled Two-step Organization Duo'); + toastr.success('Duo has been disabled.'); + $scope.enabled = response.Enabled; + $scope.close(); + }).$promise; + } + else { + $scope.submitPromise = apiService.twoFactor.disable({}, { + masterPasswordHash: _masterPasswordHash, + type: constants.twoFactorProvider.duo + }, function (response) { + $analytics.eventTrack('Disabled Two-step Duo'); + toastr.success('Duo has been disabled.'); + $scope.enabled = response.Enabled; + $scope.close(); + }).$promise; + } } function update(model) { - $scope.submitPromise = apiService.twoFactor.putDuo({}, { + var requestModel = { integrationKey: model.ikey, secretKey: model.skey, host: model.host, masterPasswordHash: _masterPasswordHash - }, function (response) { - $analytics.eventTrack('Enabled Two-step Duo'); - processResult(response); - }).$promise; + }; + + if (orgId) { + $scope.submitPromise = apiService.twoFactor.putOrganizationDuo({ orgId: orgId }, requestModel, + function (response) { + $analytics.eventTrack('Enabled Two-step Organization Duo'); + processResult(response); + }).$promise; + } + else { + $scope.submitPromise = apiService.twoFactor.putDuo({}, requestModel, + function (response) { + $analytics.eventTrack('Enabled Two-step Duo'); + processResult(response); + }).$promise; + } } function processResult(response) { @@ -13266,7 +12872,7 @@ angular closing = true; $uibModalInstance.close($scope.enabled); }; - + $scope.$on('modal.closing', function (e, reason, closed) { if (closing) { return; @@ -13759,6 +13365,521 @@ angular }; }]); +angular + .module('bit.tools') + + .controller('toolsController', ["$scope", "$uibModal", "apiService", "toastr", "authService", function ($scope, $uibModal, apiService, toastr, authService) { + $scope.import = function () { + $uibModal.open({ + animation: true, + templateUrl: 'app/tools/views/toolsImport.html', + controller: 'toolsImportController' + }); + }; + + $scope.export = function () { + $uibModal.open({ + animation: true, + templateUrl: 'app/tools/views/toolsExport.html', + controller: 'toolsExportController' + }); + }; + }]); + +angular + .module('bit.tools') + + .controller('toolsExportController', ["$scope", "apiService", "$uibModalInstance", "cipherService", "$q", "toastr", "$analytics", "constants", function ($scope, apiService, $uibModalInstance, cipherService, $q, + toastr, $analytics, constants) { + $analytics.eventTrack('toolsExportController', { category: 'Modal' }); + $scope.export = function (model) { + $scope.startedExport = true; + var decCiphers = [], + decFolders = []; + + var folderPromise = apiService.folders.list({}, function (folders) { + decFolders = cipherService.decryptFolders(folders.Data); + }).$promise; + + var ciphersPromise = apiService.ciphers.list({}, function (ciphers) { + decCiphers = cipherService.decryptCiphers(ciphers.Data); + }).$promise; + + $q.all([folderPromise, ciphersPromise]).then(function () { + if (!decCiphers.length) { + toastr.error('Nothing to export.', 'Error!'); + $scope.close(); + return; + } + + var foldersDict = {}; + for (var i = 0; i < decFolders.length; i++) { + foldersDict[decFolders[i].id] = decFolders[i]; + } + + try { + var exportCiphers = []; + for (i = 0; i < decCiphers.length; i++) { + // only export logins and secure notes + if (decCiphers[i].type !== constants.cipherType.login && + decCiphers[i].type !== constants.cipherType.secureNote) { + continue; + } + + var cipher = { + folder: decCiphers[i].folderId && (decCiphers[i].folderId in foldersDict) ? + foldersDict[decCiphers[i].folderId].name : null, + favorite: decCiphers[i].favorite ? 1 : null, + type: null, + name: decCiphers[i].name, + notes: decCiphers[i].notes, + fields: null, + // Login props + login_uri: null, + login_username: null, + login_password: null, + login_totp: null + }; + + var j; + if (decCiphers[i].fields) { + for (j = 0; j < decCiphers[i].fields.length; j++) { + if (!cipher.fields) { + cipher.fields = ''; + } + else { + cipher.fields += '\n'; + } + + cipher.fields += ((decCiphers[i].fields[j].name || '') + ': ' + decCiphers[i].fields[j].value); + } + } + + switch (decCiphers[i].type) { + case constants.cipherType.login: + cipher.type = 'login'; + cipher.login_username = decCiphers[i].login.username; + cipher.login_password = decCiphers[i].login.password; + cipher.login_totp = decCiphers[i].login.totp; + + if (decCiphers[i].login.uris && decCiphers[i].login.uris.length) { + cipher.login_uri = []; + for (j = 0; j < decCiphers[i].login.uris.length; j++) { + cipher.login_uri.push(decCiphers[i].login.uris[j].uri); + } + } + break; + case constants.cipherType.secureNote: + cipher.type = 'note'; + break; + default: + continue; + } + + exportCiphers.push(cipher); + } + + var csvString = Papa.unparse(exportCiphers); + var csvBlob = new Blob([csvString]); + + // IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx + if (window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(csvBlob, makeFileName()); + } + else { + var a = window.document.createElement('a'); + a.href = window.URL.createObjectURL(csvBlob, { type: 'text/plain' }); + a.download = makeFileName(); + document.body.appendChild(a); + // IE: "Access is denied". + // ref: https://connect.microsoft.com/IE/feedback/details/797361/ie-10-treats-blob-url-as-cross-origin-and-denies-access + a.click(); + document.body.removeChild(a); + } + + $analytics.eventTrack('Exported Data'); + toastr.success('Your data has been exported. Check your browser\'s downloads folder.', 'Success!'); + $scope.close(); + } + catch (err) { + toastr.error('Something went wrong. Please try again.', 'Error!'); + $scope.close(); + } + }, function () { + toastr.error('Something went wrong. Please try again.', 'Error!'); + $scope.close(); + }); + }; + + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + + function makeFileName() { + var now = new Date(); + var dateString = + now.getFullYear() + '' + padNumber(now.getMonth() + 1, 2) + '' + padNumber(now.getDate(), 2) + + padNumber(now.getHours(), 2) + '' + padNumber(now.getMinutes(), 2) + + padNumber(now.getSeconds(), 2); + + return 'bitwarden_export_' + dateString + '.csv'; + } + + function padNumber(number, width, paddingCharacter) { + paddingCharacter = paddingCharacter || '0'; + number = number + ''; + return number.length >= width ? number : new Array(width - number.length + 1).join(paddingCharacter) + number; + } + }]); + +angular + .module('bit.tools') + + .controller('toolsImportController', ["$scope", "$state", "apiService", "$uibModalInstance", "cryptoService", "cipherService", "toastr", "importService", "$analytics", "$sce", "validationService", function ($scope, $state, apiService, $uibModalInstance, cryptoService, cipherService, + toastr, importService, $analytics, $sce, validationService) { + $analytics.eventTrack('toolsImportController', { category: 'Modal' }); + $scope.model = { source: '' }; + $scope.source = {}; + $scope.splitFeatured = true; + + $scope.options = [ + { + id: 'bitwardencsv', + name: 'Bitwarden (csv)', + featured: true, + sort: 1, + instructions: $sce.trustAsHtml('Export using the web vault (vault.bitwarden.com). ' + + 'Log into the web vault and navigate to "Tools" > "Export".') + }, + { + id: 'lastpass', + name: 'LastPass (csv)', + featured: true, + sort: 2, + instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' + + '' + + 'https://help.bitwarden.com/article/import-from-lastpass/') + }, + { + id: 'chromecsv', + name: 'Chrome (csv)', + featured: true, + sort: 3, + instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' + + '' + + 'https://help.bitwarden.com/article/import-from-chrome/') + }, + { + id: 'firefoxpasswordexportercsvxml', + name: 'Firefox Password Exporter (xml)', + featured: true, + sort: 4, + instructions: $sce.trustAsHtml('Use the ' + + '' + + 'Password Exporter addon for FireFox to export your passwords to a XML file. After installing ' + + 'the addon, type about:addons in your FireFox navigation bar. Locate the Password Exporter ' + + 'addon and click the "Options" button. In the dialog that pops up, click the "Export Passwords" button ' + + 'to save the XML file.') + }, + { + id: 'keepass2xml', + name: 'KeePass 2 (xml)', + featured: true, + sort: 5, + instructions: $sce.trustAsHtml('Using the KeePass 2 desktop application, navigate to "File" > "Export" and ' + + 'select the KeePass XML (2.x) option.') + }, + { + id: 'keepassxcsv', + name: 'KeePassX (csv)', + instructions: $sce.trustAsHtml('Using the KeePassX desktop application, navigate to "Database" > ' + + '"Export to CSV file" and save the CSV file.') + }, + { + id: 'dashlanecsv', + name: 'Dashlane (csv)', + featured: true, + sort: 7, + instructions: $sce.trustAsHtml('Using the Dashlane desktop application, navigate to "File" > "Export" > ' + + '"Unsecured archive (readable) in CSV format" and save the CSV file.') + }, + { + id: '1password1pif', + name: '1Password (1pif)', + featured: true, + sort: 6, + instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' + + '' + + 'https://help.bitwarden.com/article/import-from-1password/') + }, + { + id: '1password6wincsv', + name: '1Password 6 Windows (csv)', + instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' + + '' + + 'https://help.bitwarden.com/article/import-from-1password/') + }, + { + id: 'roboformhtml', + name: 'RoboForm (html)', + instructions: $sce.trustAsHtml('Using the RoboForm Editor desktop application, navigate to "RoboForm" ' + + '(top left) > "Print List" > "Logins". When the following print dialog pops up click on the "Save" button ' + + 'and save the HTML file.') + }, + { + id: 'keepercsv', + name: 'Keeper (csv)', + instructions: $sce.trustAsHtml('Log into the Keeper web vault (keepersecurity.com/vault). Navigate to "Backup" ' + + '(top right) and find the "Export to Text File" option. Click "Export Now" to save the TXT/CSV file.') + }, + { + id: 'enpasscsv', + name: 'Enpass (csv)', + instructions: $sce.trustAsHtml('Using the Enpass desktop application, navigate to "File" > "Export" > ' + + '"As CSV". Select "Yes" to the warning alert and save the CSV file. Note that the importer only fully ' + + 'supports files exported while Enpass is set to the English language, so adjust your settings accordingly.') + }, + { + id: 'safeincloudxml', + name: 'SafeInCloud (xml)', + instructions: $sce.trustAsHtml('Using the SaveInCloud desktop application, navigate to "File" > "Export" > ' + + '"As XML" and save the XML file.') + }, + { + id: 'pwsafexml', + name: 'Password Safe (xml)', + instructions: $sce.trustAsHtml('Using the Password Safe desktop application, navigate to "File" > ' + + '"Export To" > "XML format..." and save the XML file.') + }, + { + id: 'stickypasswordxml', + name: 'Sticky Password (xml)', + instructions: $sce.trustAsHtml('Using the Sticky Password desktop application, navigate to "Menu" ' + + '(top right) > "Export" > "Export all". Select the unencrypted format XML option and then the ' + + '"Save to file" button. Save the XML file.') + }, + { + id: 'msecurecsv', + name: 'mSecure (csv)', + instructions: $sce.trustAsHtml('Using the mSecure desktop application, navigate to "File" > ' + + '"Export" > "CSV File..." and save the CSV file.') + }, + { + id: 'truekeycsv', + name: 'True Key (csv)', + instructions: $sce.trustAsHtml('Using the True Key desktop application, click the gear icon (top right) and ' + + 'then navigate to "App Settings". Click the "Export" button, enter your password and save the CSV file.') + }, + { + id: 'passwordbossjson', + name: 'Password Boss (json)', + instructions: $sce.trustAsHtml('Using the Password Boss desktop application, navigate to "File" > ' + + '"Export data" > "Password Boss JSON - not encrypted" and save the JSON file.') + }, + { + id: 'zohovaultcsv', + name: 'Zoho Vault (csv)', + instructions: $sce.trustAsHtml('Log into the Zoho web vault (vault.zoho.com). Navigate to "Tools" > ' + + '"Export Secrets". Select "All Secrets" and click the "Zoho Vault Format CSV" button. Highlight ' + + 'and copy the data from the textarea. Open a text editor like Notepad and paste the data. Save the ' + + 'data from the text editor as zoho_export.csv.') + }, + { + id: 'splashidcsv', + name: 'SplashID (csv)', + instructions: $sce.trustAsHtml('Using the SplashID Safe desktop application, click on the SplashID ' + + 'blue lock logo in the top right corner. Navigate to "Export" > "Export as CSV" and save the CSV file.') + }, + { + id: 'passworddragonxml', + name: 'Password Dragon (xml)', + instructions: $sce.trustAsHtml('Using the Password Dragon desktop application, navigate to "File" > ' + + '"Export" > "To XML". In the dialog that pops up select "All Rows" and check all fields. Click ' + + 'the "Export" button and save the XML file.') + }, + { + id: 'padlockcsv', + name: 'Padlock (csv)', + instructions: $sce.trustAsHtml('Using the Padlock desktop application, click the hamburger icon ' + + 'in the top left corner and navigate to "Settings". Click the "Export Data" option. Ensure that ' + + 'the "CSV" option is selected from the dropdown. Highlight and copy the data from the textarea. ' + + 'Open a text editor like Notepad and paste the data. Save the data from the text editor as ' + + 'padlock_export.csv.') + }, + { + id: 'clipperzhtml', + name: 'Clipperz (html)', + instructions: $sce.trustAsHtml('Log into the Clipperz web application (clipperz.is/app). Click the ' + + 'hamburger menu icon in the top right to expand the navigation bar. Navigate to "Data" > ' + + '"Export". Click the "download HTML+JSON" button to save the HTML file.') + }, + { + id: 'avirajson', + name: 'Avira (json)', + instructions: $sce.trustAsHtml('Using the Avira browser extension, click your username in the top ' + + 'right corner and navigate to "Settings". Locate the "Export Data" section and click "Export". ' + + 'In the dialog that pops up, click the "Export Password Manager Data" button to save the ' + + 'TXT/JSON file.') + }, + { + id: 'saferpasscsv', + name: 'SaferPass (csv)', + instructions: $sce.trustAsHtml('Using the SaferPass browser extension, click the hamburger icon ' + + 'in the top left corner and navigate to "Settings". Click the "Export accounts" button to ' + + 'save the CSV file.') + }, + { + id: 'upmcsv', + name: 'Universal Password Manager (csv)', + instructions: $sce.trustAsHtml('Using the Universal Password Manager desktop application, navigate ' + + 'to "Database" > "Export" and save the CSV file.') + }, + { + id: 'ascendocsv', + name: 'Ascendo DataVault (csv)', + instructions: $sce.trustAsHtml('Using the Ascendo DataVault desktop application, navigate ' + + 'to "Tools" > "Export". In the dialog that pops up, select the "All Items (DVX, CSV)" ' + + 'option. Click the "Ok" button to save the CSV file.') + }, + { + id: 'meldiumcsv', + name: 'Meldium (csv)', + instructions: $sce.trustAsHtml('Using the Meldium web vault, navigate to "Settings". ' + + 'Locate the "Export data" function and click "Show me my data" to save the CSV file.') + }, + { + id: 'passkeepcsv', + name: 'PassKeep (csv)', + instructions: $sce.trustAsHtml('Using the PassKeep mobile app, navigate to "Backup/Restore". ' + + 'Locate the "CSV Backup/Restore" section and click "Backup to CSV" to save the CSV file.') + }, + { + id: 'operacsv', + name: 'Opera (csv)', + instructions: $sce.trustAsHtml('The process for importing from Opera is exactly the same as ' + + 'importing from Google Chrome. See detailed instructions on our help site at ' + + '' + + 'https://help.bitwarden.com/article/import-from-chrome/') + }, + { + id: 'vivaldicsv', + name: 'Vivaldi (csv)', + instructions: $sce.trustAsHtml('The process for importing from Vivaldi is exactly the same as ' + + 'importing from Google Chrome. See detailed instructions on our help site at ' + + '' + + 'https://help.bitwarden.com/article/import-from-chrome/') + }, + { + id: 'gnomejson', + name: 'GNOME Passwords and Keys/Seahorse (json)', + instructions: $sce.trustAsHtml('Make sure you have python-keyring and python-gnomekeyring installed. ' + + 'Save the GNOME Keyring Import/Export ' + + 'python script by Luke Plant to your desktop as pw_helper.py. Open terminal and run ' + + 'chmod +rx Desktop/pw_helper.py and then ' + + 'python Desktop/pw_helper.py export Desktop/my_passwords.json. Then upload ' + + 'the resulting my_passwords.json file here to Bitwarden.') + } + ]; + + $scope.setSource = function () { + for (var i = 0; i < $scope.options.length; i++) { + if ($scope.options[i].id === $scope.model.source) { + $scope.source = $scope.options[i]; + break; + } + } + }; + $scope.setSource(); + + $scope.import = function (model, form) { + if (!model.source || model.source === '') { + validationService.addError(form, 'source', 'Select the format of the import file.', true); + return; + } + + var file = document.getElementById('file').files[0]; + if (!file && (!model.fileContents || model.fileContents === '')) { + validationService.addError(form, 'file', 'Select the import file or copy/paste the import file contents.', true); + return; + } + + $scope.processing = true; + importService.import(model.source, file || model.fileContents, importSuccess, importError); + }; + + function importSuccess(folders, ciphers, folderRelationships) { + if (!folders.length && !ciphers.length) { + importError('Nothing was imported.'); + return; + } + else if (ciphers.length) { + var halfway = Math.floor(ciphers.length / 2); + var last = ciphers.length - 1; + if (cipherIsBadData(ciphers[0]) && cipherIsBadData(ciphers[halfway]) && cipherIsBadData(ciphers[last])) { + importError('Data is not formatted correctly. Please check your import file and try again.'); + return; + } + } + + apiService.ciphers.import({ + folders: cipherService.encryptFolders(folders), + ciphers: cipherService.encryptCiphers(ciphers), + folderRelationships: folderRelationships + }, function () { + $uibModalInstance.dismiss('cancel'); + $state.go('backend.user.vault', { refreshFromServer: true }).then(function () { + $analytics.eventTrack('Imported Data', { label: $scope.model.source }); + toastr.success('Data has been successfully imported into your vault.', 'Import Success'); + }); + }, importError); + } + + function cipherIsBadData(cipher) { + return (cipher.name === null || cipher.name === '--') && + (cipher.login && (cipher.login.password === null || cipher.login.password === '')); + } + + function importError(error) { + $analytics.eventTrack('Import Data Failed', { label: $scope.model.source }); + $uibModalInstance.dismiss('cancel'); + + if (error) { + var data = error.data; + if (data && data.ValidationErrors) { + var message = ''; + for (var key in data.ValidationErrors) { + if (!data.ValidationErrors.hasOwnProperty(key)) { + continue; + } + + for (var i = 0; i < data.ValidationErrors[key].length; i++) { + message += (key + ': ' + data.ValidationErrors[key][i] + ' '); + } + } + + if (message !== '') { + toastr.error(message); + return; + } + } + else if (data && data.Message) { + toastr.error(data.Message); + return; + } + else { + toastr.error(error); + return; + } + } + + toastr.error('Something went wrong. Try again.', 'Oh No!'); + } + + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + }]); + angular .module('bit.vault') @@ -13797,6 +13918,11 @@ angular $scope.savePromise = null; $scope.save = function () { + if ($scope.cipher.type === constants.cipherType.login && $scope.cipher.login.uris.length === 1 && + ($scope.cipher.login.uris[0].uri == null || $scope.cipher.login.uris[0].uri === '')) { + $scope.cipher.login.uris = null; + } + var cipher = cipherService.encryptCipher($scope.cipher); $scope.savePromise = apiService.ciphers.post(cipher, function (cipherResponse) { $analytics.eventTrack('Created Cipher'); diff --git a/js/fallback-styles.min.js b/js/fallback-styles.min.js index 6c5ff74a..7da74cae 100644 --- a/js/fallback-styles.min.js +++ b/js/fallback-styles.min.js @@ -1,4 +1,4 @@ -var cacheTag = 'ucqrsv' || ''; +var cacheTag = 'u6pc0i' || ''; function loadStylesheetIfMissing(property, value, paths) { var scripts = document.getElementsByTagName('SCRIPT'), diff --git a/js/lib.min.js b/js/lib.min.js index 521113d0..8cf1fd60 100644 --- a/js/lib.min.js +++ b/js/lib.min.js @@ -462,6 +462,106 @@ function propertyName(name) { } })(angular); +(function() { + var showErrorsModule; + + showErrorsModule = angular.module('ui.bootstrap.showErrors', []); + + showErrorsModule.directive('showErrors', [ + '$timeout', 'showErrorsConfig', '$interpolate', function($timeout, showErrorsConfig, $interpolate) { + var getShowSuccess, getTrigger, linkFn; + getTrigger = function(options) { + var trigger; + trigger = showErrorsConfig.trigger; + if (options && (options.trigger != null)) { + trigger = options.trigger; + } + return trigger; + }; + getShowSuccess = function(options) { + var showSuccess; + showSuccess = showErrorsConfig.showSuccess; + if (options && (options.showSuccess != null)) { + showSuccess = options.showSuccess; + } + return showSuccess; + }; + linkFn = function(scope, el, attrs, formCtrl) { + var blurred, inputEl, inputName, inputNgEl, options, showSuccess, toggleClasses, trigger; + blurred = false; + options = scope.$eval(attrs.showErrors); + showSuccess = getShowSuccess(options); + trigger = getTrigger(options); + inputEl = el[0].querySelector('.form-control[name]'); + inputNgEl = angular.element(inputEl); + inputName = $interpolate(inputNgEl.attr('name') || '')(scope); + if (!inputName) { + throw "show-errors element has no child input elements with a 'name' attribute and a 'form-control' class"; + } + inputNgEl.bind(trigger, function() { + blurred = true; + return toggleClasses(formCtrl[inputName].$invalid); + }); + scope.$watch(function() { + return formCtrl[inputName] && formCtrl[inputName].$invalid; + }, function(invalid) { + if (!blurred) { + return; + } + return toggleClasses(invalid); + }); + scope.$on('show-errors-check-validity', function() { + return toggleClasses(formCtrl[inputName].$invalid); + }); + scope.$on('show-errors-reset', function() { + return $timeout(function() { + el.removeClass('has-error'); + el.removeClass('has-success'); + return blurred = false; + }, 0, false); + }); + return toggleClasses = function(invalid) { + el.toggleClass('has-error', invalid); + if (showSuccess) { + return el.toggleClass('has-success', !invalid); + } + }; + }; + return { + restrict: 'A', + require: '^form', + compile: function(elem, attrs) { + if (attrs['showErrors'].indexOf('skipFormGroupCheck') === -1) { + if (!(elem.hasClass('form-group') || elem.hasClass('input-group'))) { + throw "show-errors element does not have the 'form-group' or 'input-group' class"; + } + } + return linkFn; + } + }; + } + ]); + + showErrorsModule.provider('showErrorsConfig', function() { + var _showSuccess, _trigger; + _showSuccess = false; + _trigger = 'blur'; + this.showSuccess = function(showSuccess) { + return _showSuccess = showSuccess; + }; + this.trigger = function(trigger) { + return _trigger = trigger; + }; + this.$get = function() { + return { + showSuccess: _showSuccess, + trigger: _trigger + }; + }; + }); + +}).call(this); + /** * @license AngularJS v1.6.7 * (c) 2010-2017 Google, Inc. http://angularjs.org @@ -789,106 +889,6 @@ angular.module('ngCookies').provider('$$cookieWriter', /** @this */ function $$C })(window, window.angular); -(function() { - var showErrorsModule; - - showErrorsModule = angular.module('ui.bootstrap.showErrors', []); - - showErrorsModule.directive('showErrors', [ - '$timeout', 'showErrorsConfig', '$interpolate', function($timeout, showErrorsConfig, $interpolate) { - var getShowSuccess, getTrigger, linkFn; - getTrigger = function(options) { - var trigger; - trigger = showErrorsConfig.trigger; - if (options && (options.trigger != null)) { - trigger = options.trigger; - } - return trigger; - }; - getShowSuccess = function(options) { - var showSuccess; - showSuccess = showErrorsConfig.showSuccess; - if (options && (options.showSuccess != null)) { - showSuccess = options.showSuccess; - } - return showSuccess; - }; - linkFn = function(scope, el, attrs, formCtrl) { - var blurred, inputEl, inputName, inputNgEl, options, showSuccess, toggleClasses, trigger; - blurred = false; - options = scope.$eval(attrs.showErrors); - showSuccess = getShowSuccess(options); - trigger = getTrigger(options); - inputEl = el[0].querySelector('.form-control[name]'); - inputNgEl = angular.element(inputEl); - inputName = $interpolate(inputNgEl.attr('name') || '')(scope); - if (!inputName) { - throw "show-errors element has no child input elements with a 'name' attribute and a 'form-control' class"; - } - inputNgEl.bind(trigger, function() { - blurred = true; - return toggleClasses(formCtrl[inputName].$invalid); - }); - scope.$watch(function() { - return formCtrl[inputName] && formCtrl[inputName].$invalid; - }, function(invalid) { - if (!blurred) { - return; - } - return toggleClasses(invalid); - }); - scope.$on('show-errors-check-validity', function() { - return toggleClasses(formCtrl[inputName].$invalid); - }); - scope.$on('show-errors-reset', function() { - return $timeout(function() { - el.removeClass('has-error'); - el.removeClass('has-success'); - return blurred = false; - }, 0, false); - }); - return toggleClasses = function(invalid) { - el.toggleClass('has-error', invalid); - if (showSuccess) { - return el.toggleClass('has-success', !invalid); - } - }; - }; - return { - restrict: 'A', - require: '^form', - compile: function(elem, attrs) { - if (attrs['showErrors'].indexOf('skipFormGroupCheck') === -1) { - if (!(elem.hasClass('form-group') || elem.hasClass('input-group'))) { - throw "show-errors element does not have the 'form-group' or 'input-group' class"; - } - } - return linkFn; - } - }; - } - ]); - - showErrorsModule.provider('showErrorsConfig', function() { - var _showSuccess, _trigger; - _showSuccess = false; - _trigger = 'blur'; - this.showSuccess = function(showSuccess) { - return _showSuccess = showSuccess; - }; - this.trigger = function(trigger) { - return _trigger = trigger; - }; - this.$get = function() { - return { - showSuccess: _showSuccess, - trigger: _trigger - }; - }; - }); - -}).call(this); - (function() { @@ -1283,788 +1283,6 @@ angular.module('angular-jwt.options', []) }); }()); -/** - * @license AngularJS v1.6.7 - * (c) 2010-2017 Google, Inc. http://angularjs.org - * License: MIT - */ -(function(window, angular) {'use strict'; - -var forEach; -var isArray; -var isString; -var jqLite; - -/** - * @ngdoc module - * @name ngMessages - * @description - * - * The `ngMessages` module provides enhanced support for displaying messages within templates - * (typically within forms or when rendering message objects that return key/value data). - * Instead of relying on JavaScript code and/or complex ng-if statements within your form template to - * show and hide error messages specific to the state of an input field, the `ngMessages` and - * `ngMessage` directives are designed to handle the complexity, inheritance and priority - * sequencing based on the order of how the messages are defined in the template. - * - * Currently, the ngMessages module only contains the code for the `ngMessages`, `ngMessagesInclude` - * `ngMessage` and `ngMessageExp` directives. - * - * ## Usage - * The `ngMessages` directive allows keys in a key/value collection to be associated with a child element - * (or 'message') that will show or hide based on the truthiness of that key's value in the collection. A common use - * case for `ngMessages` is to display error messages for inputs using the `$error` object exposed by the - * {@link ngModel ngModel} directive. - * - * The child elements of the `ngMessages` directive are matched to the collection keys by a `ngMessage` or - * `ngMessageExp` directive. The value of these attributes must match a key in the collection that is provided by - * the `ngMessages` directive. - * - * Consider the following example, which illustrates a typical use case of `ngMessages`. Within the form `myForm` we - * have a text input named `myField` which is bound to the scope variable `field` using the {@link ngModel ngModel} - * directive. - * - * The `myField` field is a required input of type `email` with a maximum length of 15 characters. - * - * ```html - *
- * - *
- *
Please enter a value for this field.
- *
This field must be a valid email address.
- *
This field can be at most 15 characters long.
- *
- *
- * ``` - * - * In order to show error messages corresponding to `myField` we first create an element with an `ngMessages` attribute - * set to the `$error` object owned by the `myField` input in our `myForm` form. - * - * Within this element we then create separate elements for each of the possible errors that `myField` could have. - * The `ngMessage` attribute is used to declare which element(s) will appear for which error - for example, - * setting `ng-message="required"` specifies that this particular element should be displayed when there - * is no value present for the required field `myField` (because the key `required` will be `true` in the object - * `myForm.myField.$error`). - * - * ### Message order - * - * By default, `ngMessages` will only display one message for a particular key/value collection at any time. If more - * than one message (or error) key is currently true, then which message is shown is determined by the order of messages - * in the HTML template code (messages declared first are prioritised). This mechanism means the developer does not have - * to prioritize messages using custom JavaScript code. - * - * Given the following error object for our example (which informs us that the field `myField` currently has both the - * `required` and `email` errors): - * - * ```javascript - * - * myField.$error = { required : true, email: true, maxlength: false }; - * ``` - * The `required` message will be displayed to the user since it appears before the `email` message in the DOM. - * Once the user types a single character, the `required` message will disappear (since the field now has a value) - * but the `email` message will be visible because it is still applicable. - * - * ### Displaying multiple messages at the same time - * - * While `ngMessages` will by default only display one error element at a time, the `ng-messages-multiple` attribute can - * be applied to the `ngMessages` container element to cause it to display all applicable error messages at once: - * - * ```html - * - *
...
- * - * - * ... - * ``` - * - * ## Reusing and Overriding Messages - * In addition to prioritization, ngMessages also allows for including messages from a remote or an inline - * template. This allows for generic collection of messages to be reused across multiple parts of an - * application. - * - * ```html - * - * - *
- *
- *
- * ``` - * - * However, including generic messages may not be useful enough to match all input fields, therefore, - * `ngMessages` provides the ability to override messages defined in the remote template by redefining - * them within the directive container. - * - * ```html - * - * - * - *
- * - * - *
- * - *
You did not enter your email address
- * - * - *
Your email address is invalid
- * - * - *
- *
- *
- * ``` - * - * In the example HTML code above the message that is set on required will override the corresponding - * required message defined within the remote template. Therefore, with particular input fields (such - * email addresses, date fields, autocomplete inputs, etc...), specialized error messages can be applied - * while more generic messages can be used to handle other, more general input errors. - * - * ## Dynamic Messaging - * ngMessages also supports using expressions to dynamically change key values. Using arrays and - * repeaters to list messages is also supported. This means that the code below will be able to - * fully adapt itself and display the appropriate message when any of the expression data changes: - * - * ```html - *
- * - *
- *
You did not enter your email address
- *
- * - *
{{ errorMessage.text }}
- *
- *
- *
- * ``` - * - * The `errorMessage.type` expression can be a string value or it can be an array so - * that multiple errors can be associated with a single error message: - * - * ```html - * - *
- *
You did not enter your email address
- *
- * Your email must be between 5 and 100 characters long - *
- *
- * ``` - * - * Feel free to use other structural directives such as ng-if and ng-switch to further control - * what messages are active and when. Be careful, if you place ng-message on the same element - * as these structural directives, Angular may not be able to determine if a message is active - * or not. Therefore it is best to place the ng-message on a child element of the structural - * directive. - * - * ```html - *
- *
- *
Please enter something
- *
- *
- * ``` - * - * ## Animations - * If the `ngAnimate` module is active within the application then the `ngMessages`, `ngMessage` and - * `ngMessageExp` directives will trigger animations whenever any messages are added and removed from - * the DOM by the `ngMessages` directive. - * - * Whenever the `ngMessages` directive contains one or more visible messages then the `.ng-active` CSS - * class will be added to the element. The `.ng-inactive` CSS class will be applied when there are no - * messages present. Therefore, CSS transitions and keyframes as well as JavaScript animations can - * hook into the animations whenever these classes are added/removed. - * - * Let's say that our HTML code for our messages container looks like so: - * - * ```html - * - * ``` - * - * Then the CSS animation code for the message container looks like so: - * - * ```css - * .my-messages { - * transition:1s linear all; - * } - * .my-messages.ng-active { - * // messages are visible - * } - * .my-messages.ng-inactive { - * // messages are hidden - * } - * ``` - * - * Whenever an inner message is attached (becomes visible) or removed (becomes hidden) then the enter - * and leave animation is triggered for each particular element bound to the `ngMessage` directive. - * - * Therefore, the CSS code for the inner messages looks like so: - * - * ```css - * .some-message { - * transition:1s linear all; - * } - * - * .some-message.ng-enter {} - * .some-message.ng-enter.ng-enter-active {} - * - * .some-message.ng-leave {} - * .some-message.ng-leave.ng-leave-active {} - * ``` - * - * {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate. - */ -angular.module('ngMessages', [], function initAngularHelpers() { - // Access helpers from angular core. - // Do it inside a `config` block to ensure `window.angular` is available. - forEach = angular.forEach; - isArray = angular.isArray; - isString = angular.isString; - jqLite = angular.element; -}) - .info({ angularVersion: '1.6.7' }) - - /** - * @ngdoc directive - * @module ngMessages - * @name ngMessages - * @restrict AE - * - * @description - * `ngMessages` is a directive that is designed to show and hide messages based on the state - * of a key/value object that it listens on. The directive itself complements error message - * reporting with the `ngModel` $error object (which stores a key/value state of validation errors). - * - * `ngMessages` manages the state of internal messages within its container element. The internal - * messages use the `ngMessage` directive and will be inserted/removed from the page depending - * on if they're present within the key/value object. By default, only one message will be displayed - * at a time and this depends on the prioritization of the messages within the template. (This can - * be changed by using the `ng-messages-multiple` or `multiple` attribute on the directive container.) - * - * A remote template can also be used to promote message reusability and messages can also be - * overridden. - * - * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. - * - * @usage - * ```html - * - * - * ... - * ... - * ... - * - * - * - * - * ... - * ... - * ... - * - * ``` - * - * @param {string} ngMessages an angular expression evaluating to a key/value object - * (this is typically the $error object on an ngModel instance). - * @param {string=} ngMessagesMultiple|multiple when set, all messages will be displayed with true - * - * @example - * - * - *
- * - *
myForm.myName.$error = {{ myForm.myName.$error | json }}
- * - *
- *
You did not enter a field
- *
Your field is too short
- *
Your field is too long
- *
- *
- *
- * - * angular.module('ngMessagesExample', ['ngMessages']); - * - *
- */ - .directive('ngMessages', ['$animate', function($animate) { - var ACTIVE_CLASS = 'ng-active'; - var INACTIVE_CLASS = 'ng-inactive'; - - return { - require: 'ngMessages', - restrict: 'AE', - controller: ['$element', '$scope', '$attrs', function NgMessagesCtrl($element, $scope, $attrs) { - var ctrl = this; - var latestKey = 0; - var nextAttachId = 0; - - this.getAttachId = function getAttachId() { return nextAttachId++; }; - - var messages = this.messages = {}; - var renderLater, cachedCollection; - - this.render = function(collection) { - collection = collection || {}; - - renderLater = false; - cachedCollection = collection; - - // this is true if the attribute is empty or if the attribute value is truthy - var multiple = isAttrTruthy($scope, $attrs.ngMessagesMultiple) || - isAttrTruthy($scope, $attrs.multiple); - - var unmatchedMessages = []; - var matchedKeys = {}; - var messageItem = ctrl.head; - var messageFound = false; - var totalMessages = 0; - - // we use != instead of !== to allow for both undefined and null values - while (messageItem != null) { - totalMessages++; - var messageCtrl = messageItem.message; - - var messageUsed = false; - if (!messageFound) { - forEach(collection, function(value, key) { - if (!messageUsed && truthy(value) && messageCtrl.test(key)) { - // this is to prevent the same error name from showing up twice - if (matchedKeys[key]) return; - matchedKeys[key] = true; - - messageUsed = true; - messageCtrl.attach(); - } - }); - } - - if (messageUsed) { - // unless we want to display multiple messages then we should - // set a flag here to avoid displaying the next message in the list - messageFound = !multiple; - } else { - unmatchedMessages.push(messageCtrl); - } - - messageItem = messageItem.next; - } - - forEach(unmatchedMessages, function(messageCtrl) { - messageCtrl.detach(); - }); - - if (unmatchedMessages.length !== totalMessages) { - $animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS); - } else { - $animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS); - } - }; - - $scope.$watchCollection($attrs.ngMessages || $attrs['for'], ctrl.render); - - // If the element is destroyed, proactively destroy all the currently visible messages - $element.on('$destroy', function() { - forEach(messages, function(item) { - item.message.detach(); - }); - }); - - this.reRender = function() { - if (!renderLater) { - renderLater = true; - $scope.$evalAsync(function() { - if (renderLater && cachedCollection) { - ctrl.render(cachedCollection); - } - }); - } - }; - - this.register = function(comment, messageCtrl) { - var nextKey = latestKey.toString(); - messages[nextKey] = { - message: messageCtrl - }; - insertMessageNode($element[0], comment, nextKey); - comment.$$ngMessageNode = nextKey; - latestKey++; - - ctrl.reRender(); - }; - - this.deregister = function(comment) { - var key = comment.$$ngMessageNode; - delete comment.$$ngMessageNode; - removeMessageNode($element[0], comment, key); - delete messages[key]; - ctrl.reRender(); - }; - - function findPreviousMessage(parent, comment) { - var prevNode = comment; - var parentLookup = []; - - while (prevNode && prevNode !== parent) { - var prevKey = prevNode.$$ngMessageNode; - if (prevKey && prevKey.length) { - return messages[prevKey]; - } - - // dive deeper into the DOM and examine its children for any ngMessage - // comments that may be in an element that appears deeper in the list - if (prevNode.childNodes.length && parentLookup.indexOf(prevNode) === -1) { - parentLookup.push(prevNode); - prevNode = prevNode.childNodes[prevNode.childNodes.length - 1]; - } else if (prevNode.previousSibling) { - prevNode = prevNode.previousSibling; - } else { - prevNode = prevNode.parentNode; - parentLookup.push(prevNode); - } - } - } - - function insertMessageNode(parent, comment, key) { - var messageNode = messages[key]; - if (!ctrl.head) { - ctrl.head = messageNode; - } else { - var match = findPreviousMessage(parent, comment); - if (match) { - messageNode.next = match.next; - match.next = messageNode; - } else { - messageNode.next = ctrl.head; - ctrl.head = messageNode; - } - } - } - - function removeMessageNode(parent, comment, key) { - var messageNode = messages[key]; - - var match = findPreviousMessage(parent, comment); - if (match) { - match.next = messageNode.next; - } else { - ctrl.head = messageNode.next; - } - } - }] - }; - - function isAttrTruthy(scope, attr) { - return (isString(attr) && attr.length === 0) || //empty attribute - truthy(scope.$eval(attr)); - } - - function truthy(val) { - return isString(val) ? val.length : !!val; - } - }]) - - /** - * @ngdoc directive - * @name ngMessagesInclude - * @restrict AE - * @scope - * - * @description - * `ngMessagesInclude` is a directive with the purpose to import existing ngMessage template - * code from a remote template and place the downloaded template code into the exact spot - * that the ngMessagesInclude directive is placed within the ngMessages container. This allows - * for a series of pre-defined messages to be reused and also allows for the developer to - * determine what messages are overridden due to the placement of the ngMessagesInclude directive. - * - * @usage - * ```html - * - * - * ... - * - * - * - * - * ... - * - * ``` - * - * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. - * - * @param {string} ngMessagesInclude|src a string value corresponding to the remote template. - */ - .directive('ngMessagesInclude', - ['$templateRequest', '$document', '$compile', function($templateRequest, $document, $compile) { - - return { - restrict: 'AE', - require: '^^ngMessages', // we only require this for validation sake - link: function($scope, element, attrs) { - var src = attrs.ngMessagesInclude || attrs.src; - $templateRequest(src).then(function(html) { - if ($scope.$$destroyed) return; - - if (isString(html) && !html.trim()) { - // Empty template - nothing to compile - replaceElementWithMarker(element, src); - } else { - // Non-empty template - compile and link - $compile(html)($scope, function(contents) { - element.after(contents); - replaceElementWithMarker(element, src); - }); - } - }); - } - }; - - // Helpers - function replaceElementWithMarker(element, src) { - // A comment marker is placed for debugging purposes - var comment = $compile.$$createComment ? - $compile.$$createComment('ngMessagesInclude', src) : - $document[0].createComment(' ngMessagesInclude: ' + src + ' '); - var marker = jqLite(comment); - element.after(marker); - - // Don't pollute the DOM anymore by keeping an empty directive element - element.remove(); - } - }]) - - /** - * @ngdoc directive - * @name ngMessage - * @restrict AE - * @scope - * @priority 1 - * - * @description - * `ngMessage` is a directive with the purpose to show and hide a particular message. - * For `ngMessage` to operate, a parent `ngMessages` directive on a parent DOM element - * must be situated since it determines which messages are visible based on the state - * of the provided key/value map that `ngMessages` listens on. - * - * More information about using `ngMessage` can be found in the - * {@link module:ngMessages `ngMessages` module documentation}. - * - * @usage - * ```html - * - * - * ... - * ... - * - * - * - * - * ... - * ... - * - * ``` - * - * @param {expression} ngMessage|when a string value corresponding to the message key. - */ - .directive('ngMessage', ngMessageDirectiveFactory()) - - - /** - * @ngdoc directive - * @name ngMessageExp - * @restrict AE - * @priority 1 - * @scope - * - * @description - * `ngMessageExp` is the same as {@link directive:ngMessage `ngMessage`}, but instead of a static - * value, it accepts an expression to be evaluated for the message key. - * - * @usage - * ```html - * - * - * ... - * - * - * - * - * ... - * - * ``` - * - * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. - * - * @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key. - */ - .directive('ngMessageExp', ngMessageDirectiveFactory()); - -function ngMessageDirectiveFactory() { - return ['$animate', function($animate) { - return { - restrict: 'AE', - transclude: 'element', - priority: 1, // must run before ngBind, otherwise the text is set on the comment - terminal: true, - require: '^^ngMessages', - link: function(scope, element, attrs, ngMessagesCtrl, $transclude) { - var commentNode = element[0]; - - var records; - var staticExp = attrs.ngMessage || attrs.when; - var dynamicExp = attrs.ngMessageExp || attrs.whenExp; - var assignRecords = function(items) { - records = items - ? (isArray(items) - ? items - : items.split(/[\s,]+/)) - : null; - ngMessagesCtrl.reRender(); - }; - - if (dynamicExp) { - assignRecords(scope.$eval(dynamicExp)); - scope.$watchCollection(dynamicExp, assignRecords); - } else { - assignRecords(staticExp); - } - - var currentElement, messageCtrl; - ngMessagesCtrl.register(commentNode, messageCtrl = { - test: function(name) { - return contains(records, name); - }, - attach: function() { - if (!currentElement) { - $transclude(function(elm, newScope) { - $animate.enter(elm, null, element); - currentElement = elm; - - // Each time we attach this node to a message we get a new id that we can match - // when we are destroying the node later. - var $$attachId = currentElement.$$attachId = ngMessagesCtrl.getAttachId(); - - // in the event that the element or a parent element is destroyed - // by another structural directive then it's time - // to deregister the message from the controller - currentElement.on('$destroy', function() { - if (currentElement && currentElement.$$attachId === $$attachId) { - ngMessagesCtrl.deregister(commentNode); - messageCtrl.detach(); - } - newScope.$destroy(); - }); - }); - } - }, - detach: function() { - if (currentElement) { - var elm = currentElement; - currentElement = null; - $animate.leave(elm); - } - } - }); - } - }; - }]; - - function contains(collection, key) { - if (collection) { - return isArray(collection) - ? collection.indexOf(key) >= 0 - : collection.hasOwnProperty(key); - } - } -} - - -})(window, window.angular); - -'use strict'; - -var app = angular - .module('angular-promise-polyfill', []) - .run(['$q', '$window', function($q, $window) { - $window.Promise = function(executor) { - return $q(executor); - }; - - $window.Promise.all = $q.all.bind($q); - $window.Promise.reject = $q.reject.bind($q); - $window.Promise.resolve = $q.when.bind($q); - - $window.Promise.race = function(promises) { - var promiseMgr = $q.defer(); - - for(var i = 0; i < promises.length; i++) { - promises[i].then(function(result) { - if(promiseMgr) { - promiseMgr.resolve(result); - promiseMgr = null; - } - }); - - promises[i].catch(function(result) { - if(promiseMgr) { - promiseMgr.reject(result); - promiseMgr = null; - } - }); - } - - return promiseMgr.promise; - }; - }]); - -if (typeof module === 'object') { - module.exports = app.name; -} - (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.angularCreditCards = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o + * + *
+ *
Please enter a value for this field.
+ *
This field must be a valid email address.
+ *
This field can be at most 15 characters long.
+ *
+ * + * ``` + * + * In order to show error messages corresponding to `myField` we first create an element with an `ngMessages` attribute + * set to the `$error` object owned by the `myField` input in our `myForm` form. + * + * Within this element we then create separate elements for each of the possible errors that `myField` could have. + * The `ngMessage` attribute is used to declare which element(s) will appear for which error - for example, + * setting `ng-message="required"` specifies that this particular element should be displayed when there + * is no value present for the required field `myField` (because the key `required` will be `true` in the object + * `myForm.myField.$error`). + * + * ### Message order + * + * By default, `ngMessages` will only display one message for a particular key/value collection at any time. If more + * than one message (or error) key is currently true, then which message is shown is determined by the order of messages + * in the HTML template code (messages declared first are prioritised). This mechanism means the developer does not have + * to prioritize messages using custom JavaScript code. + * + * Given the following error object for our example (which informs us that the field `myField` currently has both the + * `required` and `email` errors): + * + * ```javascript + * + * myField.$error = { required : true, email: true, maxlength: false }; + * ``` + * The `required` message will be displayed to the user since it appears before the `email` message in the DOM. + * Once the user types a single character, the `required` message will disappear (since the field now has a value) + * but the `email` message will be visible because it is still applicable. + * + * ### Displaying multiple messages at the same time + * + * While `ngMessages` will by default only display one error element at a time, the `ng-messages-multiple` attribute can + * be applied to the `ngMessages` container element to cause it to display all applicable error messages at once: + * + * ```html + * + *
...
+ * + * + * ... + * ``` + * + * ## Reusing and Overriding Messages + * In addition to prioritization, ngMessages also allows for including messages from a remote or an inline + * template. This allows for generic collection of messages to be reused across multiple parts of an + * application. + * + * ```html + * + * + *
+ *
+ *
+ * ``` + * + * However, including generic messages may not be useful enough to match all input fields, therefore, + * `ngMessages` provides the ability to override messages defined in the remote template by redefining + * them within the directive container. + * + * ```html + * + * + * + *
+ * + * + *
+ * + *
You did not enter your email address
+ * + * + *
Your email address is invalid
+ * + * + *
+ *
+ *
+ * ``` + * + * In the example HTML code above the message that is set on required will override the corresponding + * required message defined within the remote template. Therefore, with particular input fields (such + * email addresses, date fields, autocomplete inputs, etc...), specialized error messages can be applied + * while more generic messages can be used to handle other, more general input errors. + * + * ## Dynamic Messaging + * ngMessages also supports using expressions to dynamically change key values. Using arrays and + * repeaters to list messages is also supported. This means that the code below will be able to + * fully adapt itself and display the appropriate message when any of the expression data changes: + * + * ```html + *
+ * + *
+ *
You did not enter your email address
+ *
+ * + *
{{ errorMessage.text }}
+ *
+ *
+ *
+ * ``` + * + * The `errorMessage.type` expression can be a string value or it can be an array so + * that multiple errors can be associated with a single error message: + * + * ```html + * + *
+ *
You did not enter your email address
+ *
+ * Your email must be between 5 and 100 characters long + *
+ *
+ * ``` + * + * Feel free to use other structural directives such as ng-if and ng-switch to further control + * what messages are active and when. Be careful, if you place ng-message on the same element + * as these structural directives, Angular may not be able to determine if a message is active + * or not. Therefore it is best to place the ng-message on a child element of the structural + * directive. + * + * ```html + *
+ *
+ *
Please enter something
+ *
+ *
+ * ``` + * + * ## Animations + * If the `ngAnimate` module is active within the application then the `ngMessages`, `ngMessage` and + * `ngMessageExp` directives will trigger animations whenever any messages are added and removed from + * the DOM by the `ngMessages` directive. + * + * Whenever the `ngMessages` directive contains one or more visible messages then the `.ng-active` CSS + * class will be added to the element. The `.ng-inactive` CSS class will be applied when there are no + * messages present. Therefore, CSS transitions and keyframes as well as JavaScript animations can + * hook into the animations whenever these classes are added/removed. + * + * Let's say that our HTML code for our messages container looks like so: + * + * ```html + * + * ``` + * + * Then the CSS animation code for the message container looks like so: + * + * ```css + * .my-messages { + * transition:1s linear all; + * } + * .my-messages.ng-active { + * // messages are visible + * } + * .my-messages.ng-inactive { + * // messages are hidden + * } + * ``` + * + * Whenever an inner message is attached (becomes visible) or removed (becomes hidden) then the enter + * and leave animation is triggered for each particular element bound to the `ngMessage` directive. + * + * Therefore, the CSS code for the inner messages looks like so: + * + * ```css + * .some-message { + * transition:1s linear all; + * } + * + * .some-message.ng-enter {} + * .some-message.ng-enter.ng-enter-active {} + * + * .some-message.ng-leave {} + * .some-message.ng-leave.ng-leave-active {} + * ``` + * + * {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate. + */ +angular.module('ngMessages', [], function initAngularHelpers() { + // Access helpers from angular core. + // Do it inside a `config` block to ensure `window.angular` is available. + forEach = angular.forEach; + isArray = angular.isArray; + isString = angular.isString; + jqLite = angular.element; +}) + .info({ angularVersion: '1.6.7' }) + + /** + * @ngdoc directive + * @module ngMessages + * @name ngMessages + * @restrict AE + * + * @description + * `ngMessages` is a directive that is designed to show and hide messages based on the state + * of a key/value object that it listens on. The directive itself complements error message + * reporting with the `ngModel` $error object (which stores a key/value state of validation errors). + * + * `ngMessages` manages the state of internal messages within its container element. The internal + * messages use the `ngMessage` directive and will be inserted/removed from the page depending + * on if they're present within the key/value object. By default, only one message will be displayed + * at a time and this depends on the prioritization of the messages within the template. (This can + * be changed by using the `ng-messages-multiple` or `multiple` attribute on the directive container.) + * + * A remote template can also be used to promote message reusability and messages can also be + * overridden. + * + * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. + * + * @usage + * ```html + * + * + * ... + * ... + * ... + * + * + * + * + * ... + * ... + * ... + * + * ``` + * + * @param {string} ngMessages an angular expression evaluating to a key/value object + * (this is typically the $error object on an ngModel instance). + * @param {string=} ngMessagesMultiple|multiple when set, all messages will be displayed with true + * + * @example + * + * + *
+ * + *
myForm.myName.$error = {{ myForm.myName.$error | json }}
+ * + *
+ *
You did not enter a field
+ *
Your field is too short
+ *
Your field is too long
+ *
+ *
+ *
+ * + * angular.module('ngMessagesExample', ['ngMessages']); + * + *
+ */ + .directive('ngMessages', ['$animate', function($animate) { + var ACTIVE_CLASS = 'ng-active'; + var INACTIVE_CLASS = 'ng-inactive'; + + return { + require: 'ngMessages', + restrict: 'AE', + controller: ['$element', '$scope', '$attrs', function NgMessagesCtrl($element, $scope, $attrs) { + var ctrl = this; + var latestKey = 0; + var nextAttachId = 0; + + this.getAttachId = function getAttachId() { return nextAttachId++; }; + + var messages = this.messages = {}; + var renderLater, cachedCollection; + + this.render = function(collection) { + collection = collection || {}; + + renderLater = false; + cachedCollection = collection; + + // this is true if the attribute is empty or if the attribute value is truthy + var multiple = isAttrTruthy($scope, $attrs.ngMessagesMultiple) || + isAttrTruthy($scope, $attrs.multiple); + + var unmatchedMessages = []; + var matchedKeys = {}; + var messageItem = ctrl.head; + var messageFound = false; + var totalMessages = 0; + + // we use != instead of !== to allow for both undefined and null values + while (messageItem != null) { + totalMessages++; + var messageCtrl = messageItem.message; + + var messageUsed = false; + if (!messageFound) { + forEach(collection, function(value, key) { + if (!messageUsed && truthy(value) && messageCtrl.test(key)) { + // this is to prevent the same error name from showing up twice + if (matchedKeys[key]) return; + matchedKeys[key] = true; + + messageUsed = true; + messageCtrl.attach(); + } + }); + } + + if (messageUsed) { + // unless we want to display multiple messages then we should + // set a flag here to avoid displaying the next message in the list + messageFound = !multiple; + } else { + unmatchedMessages.push(messageCtrl); + } + + messageItem = messageItem.next; + } + + forEach(unmatchedMessages, function(messageCtrl) { + messageCtrl.detach(); + }); + + if (unmatchedMessages.length !== totalMessages) { + $animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS); + } else { + $animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS); + } + }; + + $scope.$watchCollection($attrs.ngMessages || $attrs['for'], ctrl.render); + + // If the element is destroyed, proactively destroy all the currently visible messages + $element.on('$destroy', function() { + forEach(messages, function(item) { + item.message.detach(); + }); + }); + + this.reRender = function() { + if (!renderLater) { + renderLater = true; + $scope.$evalAsync(function() { + if (renderLater && cachedCollection) { + ctrl.render(cachedCollection); + } + }); + } + }; + + this.register = function(comment, messageCtrl) { + var nextKey = latestKey.toString(); + messages[nextKey] = { + message: messageCtrl + }; + insertMessageNode($element[0], comment, nextKey); + comment.$$ngMessageNode = nextKey; + latestKey++; + + ctrl.reRender(); + }; + + this.deregister = function(comment) { + var key = comment.$$ngMessageNode; + delete comment.$$ngMessageNode; + removeMessageNode($element[0], comment, key); + delete messages[key]; + ctrl.reRender(); + }; + + function findPreviousMessage(parent, comment) { + var prevNode = comment; + var parentLookup = []; + + while (prevNode && prevNode !== parent) { + var prevKey = prevNode.$$ngMessageNode; + if (prevKey && prevKey.length) { + return messages[prevKey]; + } + + // dive deeper into the DOM and examine its children for any ngMessage + // comments that may be in an element that appears deeper in the list + if (prevNode.childNodes.length && parentLookup.indexOf(prevNode) === -1) { + parentLookup.push(prevNode); + prevNode = prevNode.childNodes[prevNode.childNodes.length - 1]; + } else if (prevNode.previousSibling) { + prevNode = prevNode.previousSibling; + } else { + prevNode = prevNode.parentNode; + parentLookup.push(prevNode); + } + } + } + + function insertMessageNode(parent, comment, key) { + var messageNode = messages[key]; + if (!ctrl.head) { + ctrl.head = messageNode; + } else { + var match = findPreviousMessage(parent, comment); + if (match) { + messageNode.next = match.next; + match.next = messageNode; + } else { + messageNode.next = ctrl.head; + ctrl.head = messageNode; + } + } + } + + function removeMessageNode(parent, comment, key) { + var messageNode = messages[key]; + + var match = findPreviousMessage(parent, comment); + if (match) { + match.next = messageNode.next; + } else { + ctrl.head = messageNode.next; + } + } + }] + }; + + function isAttrTruthy(scope, attr) { + return (isString(attr) && attr.length === 0) || //empty attribute + truthy(scope.$eval(attr)); + } + + function truthy(val) { + return isString(val) ? val.length : !!val; + } + }]) + + /** + * @ngdoc directive + * @name ngMessagesInclude + * @restrict AE + * @scope + * + * @description + * `ngMessagesInclude` is a directive with the purpose to import existing ngMessage template + * code from a remote template and place the downloaded template code into the exact spot + * that the ngMessagesInclude directive is placed within the ngMessages container. This allows + * for a series of pre-defined messages to be reused and also allows for the developer to + * determine what messages are overridden due to the placement of the ngMessagesInclude directive. + * + * @usage + * ```html + * + * + * ... + * + * + * + * + * ... + * + * ``` + * + * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. + * + * @param {string} ngMessagesInclude|src a string value corresponding to the remote template. + */ + .directive('ngMessagesInclude', + ['$templateRequest', '$document', '$compile', function($templateRequest, $document, $compile) { + + return { + restrict: 'AE', + require: '^^ngMessages', // we only require this for validation sake + link: function($scope, element, attrs) { + var src = attrs.ngMessagesInclude || attrs.src; + $templateRequest(src).then(function(html) { + if ($scope.$$destroyed) return; + + if (isString(html) && !html.trim()) { + // Empty template - nothing to compile + replaceElementWithMarker(element, src); + } else { + // Non-empty template - compile and link + $compile(html)($scope, function(contents) { + element.after(contents); + replaceElementWithMarker(element, src); + }); + } + }); + } + }; + + // Helpers + function replaceElementWithMarker(element, src) { + // A comment marker is placed for debugging purposes + var comment = $compile.$$createComment ? + $compile.$$createComment('ngMessagesInclude', src) : + $document[0].createComment(' ngMessagesInclude: ' + src + ' '); + var marker = jqLite(comment); + element.after(marker); + + // Don't pollute the DOM anymore by keeping an empty directive element + element.remove(); + } + }]) + + /** + * @ngdoc directive + * @name ngMessage + * @restrict AE + * @scope + * @priority 1 + * + * @description + * `ngMessage` is a directive with the purpose to show and hide a particular message. + * For `ngMessage` to operate, a parent `ngMessages` directive on a parent DOM element + * must be situated since it determines which messages are visible based on the state + * of the provided key/value map that `ngMessages` listens on. + * + * More information about using `ngMessage` can be found in the + * {@link module:ngMessages `ngMessages` module documentation}. + * + * @usage + * ```html + * + * + * ... + * ... + * + * + * + * + * ... + * ... + * + * ``` + * + * @param {expression} ngMessage|when a string value corresponding to the message key. + */ + .directive('ngMessage', ngMessageDirectiveFactory()) + + + /** + * @ngdoc directive + * @name ngMessageExp + * @restrict AE + * @priority 1 + * @scope + * + * @description + * `ngMessageExp` is the same as {@link directive:ngMessage `ngMessage`}, but instead of a static + * value, it accepts an expression to be evaluated for the message key. + * + * @usage + * ```html + * + * + * ... + * + * + * + * + * ... + * + * ``` + * + * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. + * + * @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key. + */ + .directive('ngMessageExp', ngMessageDirectiveFactory()); + +function ngMessageDirectiveFactory() { + return ['$animate', function($animate) { + return { + restrict: 'AE', + transclude: 'element', + priority: 1, // must run before ngBind, otherwise the text is set on the comment + terminal: true, + require: '^^ngMessages', + link: function(scope, element, attrs, ngMessagesCtrl, $transclude) { + var commentNode = element[0]; + + var records; + var staticExp = attrs.ngMessage || attrs.when; + var dynamicExp = attrs.ngMessageExp || attrs.whenExp; + var assignRecords = function(items) { + records = items + ? (isArray(items) + ? items + : items.split(/[\s,]+/)) + : null; + ngMessagesCtrl.reRender(); + }; + + if (dynamicExp) { + assignRecords(scope.$eval(dynamicExp)); + scope.$watchCollection(dynamicExp, assignRecords); + } else { + assignRecords(staticExp); + } + + var currentElement, messageCtrl; + ngMessagesCtrl.register(commentNode, messageCtrl = { + test: function(name) { + return contains(records, name); + }, + attach: function() { + if (!currentElement) { + $transclude(function(elm, newScope) { + $animate.enter(elm, null, element); + currentElement = elm; + + // Each time we attach this node to a message we get a new id that we can match + // when we are destroying the node later. + var $$attachId = currentElement.$$attachId = ngMessagesCtrl.getAttachId(); + + // in the event that the element or a parent element is destroyed + // by another structural directive then it's time + // to deregister the message from the controller + currentElement.on('$destroy', function() { + if (currentElement && currentElement.$$attachId === $$attachId) { + ngMessagesCtrl.deregister(commentNode); + messageCtrl.detach(); + } + newScope.$destroy(); + }); + }); + } + }, + detach: function() { + if (currentElement) { + var elm = currentElement; + currentElement = null; + $animate.leave(elm); + } + } + }); + } + }; + }]; + + function contains(collection, key) { + if (collection) { + return isArray(collection) + ? collection.indexOf(key) >= 0 + : collection.hasOwnProperty(key); + } + } +} + + +})(window, window.angular); + +/** + * @license AngularJS v1.6.7 + * (c) 2010-2017 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular) {'use strict'; + +var $resourceMinErr = angular.$$minErr('$resource'); + +// Helper functions and regex to lookup a dotted path on an object +// stopping at undefined/null. The path must be composed of ASCII +// identifiers (just like $parse) +var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/; + +function isValidDottedPath(path) { + return (path != null && path !== '' && path !== 'hasOwnProperty' && + MEMBER_NAME_REGEX.test('.' + path)); +} + +function lookupDottedPath(obj, path) { + if (!isValidDottedPath(path)) { + throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); + } + var keys = path.split('.'); + for (var i = 0, ii = keys.length; i < ii && angular.isDefined(obj); i++) { + var key = keys[i]; + obj = (obj !== null) ? obj[key] : undefined; + } + return obj; +} + +/** + * Create a shallow copy of an object and clear other fields from the destination + */ +function shallowClearAndCopy(src, dst) { + dst = dst || {}; + + angular.forEach(dst, function(value, key) { + delete dst[key]; + }); + + for (var key in src) { + if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } + } + + return dst; +} + +/** + * @ngdoc module + * @name ngResource + * @description + * + * The `ngResource` module provides interaction support with RESTful services + * via the $resource service. + * + * See {@link ngResource.$resourceProvider} and {@link ngResource.$resource} for usage. + */ + +/** + * @ngdoc provider + * @name $resourceProvider + * + * @description + * + * Use `$resourceProvider` to change the default behavior of the {@link ngResource.$resource} + * service. + * + * ## Dependencies + * Requires the {@link ngResource } module to be installed. + * + */ + +/** + * @ngdoc service + * @name $resource + * @requires $http + * @requires ng.$log + * @requires $q + * @requires ng.$timeout + * + * @description + * A factory which creates a resource object that lets you interact with + * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. + * + * The returned resource object has action methods which provide high-level behaviors without + * the need to interact with the low level {@link ng.$http $http} service. + * + * Requires the {@link ngResource `ngResource`} module to be installed. + * + * By default, trailing slashes will be stripped from the calculated URLs, + * which can pose problems with server backends that do not expect that + * behavior. This can be disabled by configuring the `$resourceProvider` like + * this: + * + * ```js + app.config(['$resourceProvider', function($resourceProvider) { + // Don't strip trailing slashes from calculated URLs + $resourceProvider.defaults.stripTrailingSlashes = false; + }]); + * ``` + * + * @param {string} url A parameterized URL template with parameters prefixed by `:` as in + * `/user/:username`. If you are using a URL with a port number (e.g. + * `http://example.com:8080/api`), it will be respected. + * + * If you are using a url with a suffix, just add the suffix, like this: + * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` + * or even `$resource('http://example.com/resource/:resource_id.:format')` + * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be + * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you + * can escape it with `/\.`. + * + * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in + * `actions` methods. If a parameter value is a function, it will be called every time + * a param value needs to be obtained for a request (unless the param was overridden). The function + * will be passed the current data value as an argument. + * + * Each key value in the parameter object is first bound to url template if present and then any + * excess keys are appended to the url search query after the `?`. + * + * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in + * URL `/path/greet?salutation=Hello`. + * + * If the parameter value is prefixed with `@`, then the value for that parameter will be + * extracted from the corresponding property on the `data` object (provided when calling actions + * with a request body). + * For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of + * `someParam` will be `data.someProp`. + * Note that the parameter will be ignored, when calling a "GET" action method (i.e. an action + * method that does not accept a request body) + * + * @param {Object.=} actions Hash with declaration of custom actions that will be available + * in addition to the default set of resource actions (see below). If a custom action has the same + * key as a default action (e.g. `save`), then the default action will be *overwritten*, and not + * extended. + * + * The declaration should be created in the format of {@link ng.$http#usage $http.config}: + * + * {action1: {method:?, params:?, isArray:?, headers:?, ...}, + * action2: {method:?, params:?, isArray:?, headers:?, ...}, + * ...} + * + * Where: + * + * - **`action`** – {string} – The name of action. This name becomes the name of the method on + * your resource object. + * - **`method`** – {string} – Case insensitive HTTP method (e.g. `GET`, `POST`, `PUT`, + * `DELETE`, `JSONP`, etc). + * - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of + * the parameter value is a function, it will be called every time when a param value needs to + * be obtained for a request (unless the param was overridden). The function will be passed the + * current data value as an argument. + * - **`url`** – {string} – action specific `url` override. The url templating is supported just + * like for the resource-level urls. + * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, + * see `returns` section. + * - **`transformRequest`** – + * `{function(data, headersGetter)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * request body and headers and returns its transformed (typically serialized) version. + * By default, transformRequest will contain one function that checks if the request data is + * an object and serializes it using `angular.toJson`. To prevent this behavior, set + * `transformRequest` to an empty array: `transformRequest: []` + * - **`transformResponse`** – + * `{function(data, headersGetter, status)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * response body, headers and status and returns its transformed (typically deserialized) + * version. + * By default, transformResponse will contain one function that checks if the response looks + * like a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior, + * set `transformResponse` to an empty array: `transformResponse: []` + * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the + * GET request, otherwise if a cache instance built with + * {@link ng.$cacheFactory $cacheFactory} is supplied, this cache will be used for + * caching. + * - **`timeout`** – `{number}` – timeout in milliseconds.
+ * **Note:** In contrast to {@link ng.$http#usage $http.config}, {@link ng.$q promises} are + * **not** supported in $resource, because the same value would be used for multiple requests. + * If you are looking for a way to cancel requests, you should use the `cancellable` option. + * - **`cancellable`** – `{boolean}` – if set to true, the request made by a "non-instance" call + * will be cancelled (if not already completed) by calling `$cancelRequest()` on the call's + * return value. Calling `$cancelRequest()` for a non-cancellable or an already + * completed/cancelled request will have no effect.
+ * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the + * XHR object. See + * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5) + * for more information. + * - **`responseType`** - `{string}` - see + * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). + * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - + * `response` and `responseError`. Both `response` and `responseError` interceptors get called + * with `http response` object. See {@link ng.$http $http interceptors}. In addition, the + * resource instance or array object is accessible by the `resource` property of the + * `http response` object. + * Keep in mind that the associated promise will be resolved with the value returned by the + * response interceptor, if one is specified. The default response interceptor returns + * `response.resource` (i.e. the resource instance or array). + * - **`hasBody`** - `{boolean}` - allows to specify if a request body should be included or not. + * If not specified only POST, PUT and PATCH requests will have a body. + * + * @param {Object} options Hash with custom settings that should extend the + * default `$resourceProvider` behavior. The supported options are: + * + * - **`stripTrailingSlashes`** – {boolean} – If true then the trailing + * slashes from any calculated URL will be stripped. (Defaults to true.) + * - **`cancellable`** – {boolean} – If true, the request made by a "non-instance" call will be + * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return value. + * This can be overwritten per action. (Defaults to false.) + * + * @returns {Object} A resource "class" object with methods for the default set of resource actions + * optionally extended with custom `actions`. The default set contains these actions: + * ```js + * { 'get': {method:'GET'}, + * 'save': {method:'POST'}, + * 'query': {method:'GET', isArray:true}, + * 'remove': {method:'DELETE'}, + * 'delete': {method:'DELETE'} }; + * ``` + * + * Calling these methods invoke an {@link ng.$http} with the specified http method, + * destination and parameters. When the data is returned from the server then the object is an + * instance of the resource class. The actions `save`, `remove` and `delete` are available on it + * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, + * read, update, delete) on server-side data like this: + * ```js + * var User = $resource('/user/:userId', {userId:'@id'}); + * var user = User.get({userId:123}, function() { + * user.abc = true; + * user.$save(); + * }); + * ``` + * + * It is important to realize that invoking a $resource object method immediately returns an + * empty reference (object or array depending on `isArray`). Once the data is returned from the + * server the existing reference is populated with the actual data. This is a useful trick since + * usually the resource is assigned to a model which is then rendered by the view. Having an empty + * object results in no rendering, once the data arrives from the server then the object is + * populated with the data and the view automatically re-renders itself showing the new data. This + * means that in most cases one never has to write a callback function for the action methods. + * + * The action methods on the class object or instance object can be invoked with the following + * parameters: + * + * - "class" actions without a body: `Resource.action([parameters], [success], [error])` + * - "class" actions with a body: `Resource.action([parameters], postData, [success], [error])` + * - instance actions: `instance.$action([parameters], [success], [error])` + * + * + * When calling instance methods, the instance itself is used as the request body (if the action + * should have a body). By default, only actions using `POST`, `PUT` or `PATCH` have request + * bodies, but you can use the `hasBody` configuration option to specify whether an action + * should have a body or not (regardless of its HTTP method). + * + * + * Success callback is called with (value (Object|Array), responseHeaders (Function), + * status (number), statusText (string)) arguments, where the value is the populated resource + * instance or collection object. The error callback is called with (httpResponse) argument. + * + * Class actions return empty instance (with additional properties below). + * Instance actions return promise of the action. + * + * The Resource instances and collections have these additional properties: + * + * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this + * instance or collection. + * + * On success, the promise is resolved with the same resource instance or collection object, + * updated with data from server. This makes it easy to use in + * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view + * rendering until the resource(s) are loaded. + * + * On failure, the promise is rejected with the {@link ng.$http http response} object. + * + * If an interceptor object was provided, the promise will instead be resolved with the value + * returned by the interceptor. + * + * - `$resolved`: `true` after first server interaction is completed (either with success or + * rejection), `false` before that. Knowing if the Resource has been resolved is useful in + * data-binding. + * + * The Resource instances and collections have these additional methods: + * + * - `$cancelRequest`: If there is a cancellable, pending request related to the instance or + * collection, calling this method will abort the request. + * + * The Resource instances have these additional methods: + * + * - `toJSON`: It returns a simple object without any of the extra properties added as part of + * the Resource API. This object can be serialized through {@link angular.toJson} safely + * without attaching Angular-specific fields. Notice that `JSON.stringify` (and + * `angular.toJson`) automatically use this method when serializing a Resource instance + * (see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON%28%29_behavior)). + * + * @example + * + * ### Credit card resource + * + * ```js + // Define CreditCard class + var CreditCard = $resource('/user/:userId/card/:cardId', + {userId:123, cardId:'@id'}, { + charge: {method:'POST', params:{charge:true}} + }); + + // We can retrieve a collection from the server + var cards = CreditCard.query(function() { + // GET: /user/123/card + // server returns: [ {id:456, number:'1234', name:'Smith'} ]; + + var card = cards[0]; + // each item is an instance of CreditCard + expect(card instanceof CreditCard).toEqual(true); + card.name = "J. Smith"; + // non GET methods are mapped onto the instances + card.$save(); + // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} + // server returns: {id:456, number:'1234', name: 'J. Smith'}; + + // our custom method is mapped as well. + card.$charge({amount:9.99}); + // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} + }); + + // we can create an instance as well + var newCard = new CreditCard({number:'0123'}); + newCard.name = "Mike Smith"; + newCard.$save(); + // POST: /user/123/card {number:'0123', name:'Mike Smith'} + // server returns: {id:789, number:'0123', name: 'Mike Smith'}; + expect(newCard.id).toEqual(789); + * ``` + * + * The object returned from this function execution is a resource "class" which has "static" method + * for each action in the definition. + * + * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and + * `headers`. + * + * @example + * + * ### User resource + * + * When the data is returned from the server then the object is an instance of the resource type and + * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD + * operations (create, read, update, delete) on server-side data. + + ```js + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}, function(user) { + user.abc = true; + user.$save(); + }); + ``` + * + * It's worth noting that the success callback for `get`, `query` and other methods gets passed + * in the response that came from the server as well as $http header getter function, so one + * could rewrite the above example and get access to http headers as: + * + ```js + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}, function(user, getResponseHeaders){ + user.abc = true; + user.$save(function(user, putResponseHeaders) { + //user => saved user object + //putResponseHeaders => $http header getter + }); + }); + ``` + * + * You can also access the raw `$http` promise via the `$promise` property on the object returned + * + ``` + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}) + .$promise.then(function(user) { + $scope.user = user; + }); + ``` + * + * @example + * + * ### Creating a custom 'PUT' request + * + * In this example we create a custom method on our resource to make a PUT request + * ```js + * var app = angular.module('app', ['ngResource', 'ngRoute']); + * + * // Some APIs expect a PUT request in the format URL/object/ID + * // Here we are creating an 'update' method + * app.factory('Notes', ['$resource', function($resource) { + * return $resource('/notes/:id', null, + * { + * 'update': { method:'PUT' } + * }); + * }]); + * + * // In our controller we get the ID from the URL using ngRoute and $routeParams + * // We pass in $routeParams and our Notes factory along with $scope + * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', + function($scope, $routeParams, Notes) { + * // First get a note object from the factory + * var note = Notes.get({ id:$routeParams.id }); + * $id = note.id; + * + * // Now call update passing in the ID first then the object you are updating + * Notes.update({ id:$id }, note); + * + * // This will PUT /notes/ID with the note object in the request payload + * }]); + * ``` + * + * @example + * + * ### Cancelling requests + * + * If an action's configuration specifies that it is cancellable, you can cancel the request related + * to an instance or collection (as long as it is a result of a "non-instance" call): + * + ```js + // ...defining the `Hotel` resource... + var Hotel = $resource('/api/hotel/:id', {id: '@id'}, { + // Let's make the `query()` method cancellable + query: {method: 'get', isArray: true, cancellable: true} + }); + + // ...somewhere in the PlanVacationController... + ... + this.onDestinationChanged = function onDestinationChanged(destination) { + // We don't care about any pending request for hotels + // in a different destination any more + this.availableHotels.$cancelRequest(); + + // Let's query for hotels in '' + // (calls: /api/hotel?location=) + this.availableHotels = Hotel.query({location: destination}); + }; + ``` + * + */ +angular.module('ngResource', ['ng']). + info({ angularVersion: '1.6.7' }). + provider('$resource', function ResourceProvider() { + var PROTOCOL_AND_IPV6_REGEX = /^https?:\/\/\[[^\]]*][^/]*/; + + var provider = this; + + /** + * @ngdoc property + * @name $resourceProvider#defaults + * @description + * Object containing default options used when creating `$resource` instances. + * + * The default values satisfy a wide range of usecases, but you may choose to overwrite any of + * them to further customize your instances. The available properties are: + * + * - **stripTrailingSlashes** – `{boolean}` – If true, then the trailing slashes from any + * calculated URL will be stripped.
+ * (Defaults to true.) + * - **cancellable** – `{boolean}` – If true, the request made by a "non-instance" call will be + * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return + * value. For more details, see {@link ngResource.$resource}. This can be overwritten per + * resource class or action.
+ * (Defaults to false.) + * - **actions** - `{Object.}` - A hash with default actions declarations. Actions are + * high-level methods corresponding to RESTful actions/methods on resources. An action may + * specify what HTTP method to use, what URL to hit, if the return value will be a single + * object or a collection (array) of objects etc. For more details, see + * {@link ngResource.$resource}. The actions can also be enhanced or overwritten per resource + * class.
+ * The default actions are: + * ```js + * { + * get: {method: 'GET'}, + * save: {method: 'POST'}, + * query: {method: 'GET', isArray: true}, + * remove: {method: 'DELETE'}, + * delete: {method: 'DELETE'} + * } + * ``` + * + * #### Example + * + * For example, you can specify a new `update` action that uses the `PUT` HTTP verb: + * + * ```js + * angular. + * module('myApp'). + * config(['$resourceProvider', function ($resourceProvider) { + * $resourceProvider.defaults.actions.update = { + * method: 'PUT' + * }; + * }]); + * ``` + * + * Or you can even overwrite the whole `actions` list and specify your own: + * + * ```js + * angular. + * module('myApp'). + * config(['$resourceProvider', function ($resourceProvider) { + * $resourceProvider.defaults.actions = { + * create: {method: 'POST'}, + * get: {method: 'GET'}, + * getAll: {method: 'GET', isArray:true}, + * update: {method: 'PUT'}, + * delete: {method: 'DELETE'} + * }; + * }); + * ``` + * + */ + this.defaults = { + // Strip slashes by default + stripTrailingSlashes: true, + + // Make non-instance requests cancellable (via `$cancelRequest()`) + cancellable: false, + + // Default actions configuration + actions: { + 'get': {method: 'GET'}, + 'save': {method: 'POST'}, + 'query': {method: 'GET', isArray: true}, + 'remove': {method: 'DELETE'}, + 'delete': {method: 'DELETE'} + } + }; + + this.$get = ['$http', '$log', '$q', '$timeout', function($http, $log, $q, $timeout) { + + var noop = angular.noop, + forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy, + isArray = angular.isArray, + isDefined = angular.isDefined, + isFunction = angular.isFunction, + isNumber = angular.isNumber, + encodeUriQuery = angular.$$encodeUriQuery, + encodeUriSegment = angular.$$encodeUriSegment; + + function Route(template, defaults) { + this.template = template; + this.defaults = extend({}, provider.defaults, defaults); + this.urlParams = {}; + } + + Route.prototype = { + setUrlParams: function(config, params, actionUrl) { + var self = this, + url = actionUrl || self.template, + val, + encodedVal, + protocolAndIpv6 = ''; + + var urlParams = self.urlParams = Object.create(null); + forEach(url.split(/\W/), function(param) { + if (param === 'hasOwnProperty') { + throw $resourceMinErr('badname', 'hasOwnProperty is not a valid parameter name.'); + } + if (!(new RegExp('^\\d+$').test(param)) && param && + (new RegExp('(^|[^\\\\]):' + param + '(\\W|$)').test(url))) { + urlParams[param] = { + isQueryParamValue: (new RegExp('\\?.*=:' + param + '(?:\\W|$)')).test(url) + }; + } + }); + url = url.replace(/\\:/g, ':'); + url = url.replace(PROTOCOL_AND_IPV6_REGEX, function(match) { + protocolAndIpv6 = match; + return ''; + }); + + params = params || {}; + forEach(self.urlParams, function(paramInfo, urlParam) { + val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; + if (isDefined(val) && val !== null) { + if (paramInfo.isQueryParamValue) { + encodedVal = encodeUriQuery(val, true); + } else { + encodedVal = encodeUriSegment(val); + } + url = url.replace(new RegExp(':' + urlParam + '(\\W|$)', 'g'), function(match, p1) { + return encodedVal + p1; + }); + } else { + url = url.replace(new RegExp('(/?):' + urlParam + '(\\W|$)', 'g'), function(match, + leadingSlashes, tail) { + if (tail.charAt(0) === '/') { + return tail; + } else { + return leadingSlashes + tail; + } + }); + } + }); + + // strip trailing slashes and set the url (unless this behavior is specifically disabled) + if (self.defaults.stripTrailingSlashes) { + url = url.replace(/\/+$/, '') || '/'; + } + + // Collapse `/.` if found in the last URL path segment before the query. + // E.g. `http://url.com/id/.format?q=x` becomes `http://url.com/id.format?q=x`. + url = url.replace(/\/\.(?=\w+($|\?))/, '.'); + // Replace escaped `/\.` with `/.`. + // (If `\.` comes from a param value, it will be encoded as `%5C.`.) + config.url = protocolAndIpv6 + url.replace(/\/(\\|%5C)\./, '/.'); + + + // set params - delegate param encoding to $http + forEach(params, function(value, key) { + if (!self.urlParams[key]) { + config.params = config.params || {}; + config.params[key] = value; + } + }); + } + }; + + + function resourceFactory(url, paramDefaults, actions, options) { + var route = new Route(url, options); + + actions = extend({}, provider.defaults.actions, actions); + + function extractParams(data, actionParams) { + var ids = {}; + actionParams = extend({}, paramDefaults, actionParams); + forEach(actionParams, function(value, key) { + if (isFunction(value)) { value = value(data); } + ids[key] = value && value.charAt && value.charAt(0) === '@' ? + lookupDottedPath(data, value.substr(1)) : value; + }); + return ids; + } + + function defaultResponseInterceptor(response) { + return response.resource; + } + + function Resource(value) { + shallowClearAndCopy(value || {}, this); + } + + Resource.prototype.toJSON = function() { + var data = extend({}, this); + delete data.$promise; + delete data.$resolved; + delete data.$cancelRequest; + return data; + }; + + forEach(actions, function(action, name) { + var hasBody = action.hasBody === true || (action.hasBody !== false && /^(POST|PUT|PATCH)$/i.test(action.method)); + var numericTimeout = action.timeout; + var cancellable = isDefined(action.cancellable) ? + action.cancellable : route.defaults.cancellable; + + if (numericTimeout && !isNumber(numericTimeout)) { + $log.debug('ngResource:\n' + + ' Only numeric values are allowed as `timeout`.\n' + + ' Promises are not supported in $resource, because the same value would ' + + 'be used for multiple requests. If you are looking for a way to cancel ' + + 'requests, you should use the `cancellable` option.'); + delete action.timeout; + numericTimeout = null; + } + + Resource[name] = function(a1, a2, a3, a4) { + var params = {}, data, success, error; + + switch (arguments.length) { + case 4: + error = a4; + success = a3; + // falls through + case 3: + case 2: + if (isFunction(a2)) { + if (isFunction(a1)) { + success = a1; + error = a2; + break; + } + + success = a2; + error = a3; + // falls through + } else { + params = a1; + data = a2; + success = a3; + break; + } + // falls through + case 1: + if (isFunction(a1)) success = a1; + else if (hasBody) data = a1; + else params = a1; + break; + case 0: break; + default: + throw $resourceMinErr('badargs', + 'Expected up to 4 arguments [params, data, success, error], got {0} arguments', + arguments.length); + } + + var isInstanceCall = this instanceof Resource; + var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); + var httpConfig = {}; + var responseInterceptor = action.interceptor && action.interceptor.response || + defaultResponseInterceptor; + var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || + undefined; + var hasError = !!error; + var hasResponseErrorInterceptor = !!responseErrorInterceptor; + var timeoutDeferred; + var numericTimeoutPromise; + + forEach(action, function(value, key) { + switch (key) { + default: + httpConfig[key] = copy(value); + break; + case 'params': + case 'isArray': + case 'interceptor': + case 'cancellable': + break; + } + }); + + if (!isInstanceCall && cancellable) { + timeoutDeferred = $q.defer(); + httpConfig.timeout = timeoutDeferred.promise; + + if (numericTimeout) { + numericTimeoutPromise = $timeout(timeoutDeferred.resolve, numericTimeout); + } + } + + if (hasBody) httpConfig.data = data; + route.setUrlParams(httpConfig, + extend({}, extractParams(data, action.params || {}), params), + action.url); + + var promise = $http(httpConfig).then(function(response) { + var data = response.data; + + if (data) { + // Need to convert action.isArray to boolean in case it is undefined + if (isArray(data) !== (!!action.isArray)) { + throw $resourceMinErr('badcfg', + 'Error in resource configuration for action `{0}`. Expected response to ' + + 'contain an {1} but got an {2} (Request: {3} {4})', name, action.isArray ? 'array' : 'object', + isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url); + } + if (action.isArray) { + value.length = 0; + forEach(data, function(item) { + if (typeof item === 'object') { + value.push(new Resource(item)); + } else { + // Valid JSON values may be string literals, and these should not be converted + // into objects. These items will not have access to the Resource prototype + // methods, but unfortunately there + value.push(item); + } + }); + } else { + var promise = value.$promise; // Save the promise + shallowClearAndCopy(data, value); + value.$promise = promise; // Restore the promise + } + } + response.resource = value; + + return response; + }, function(response) { + response.resource = value; + return $q.reject(response); + }); + + promise = promise['finally'](function() { + value.$resolved = true; + if (!isInstanceCall && cancellable) { + value.$cancelRequest = noop; + $timeout.cancel(numericTimeoutPromise); + timeoutDeferred = numericTimeoutPromise = httpConfig.timeout = null; + } + }); + + promise = promise.then( + function(response) { + var value = responseInterceptor(response); + (success || noop)(value, response.headers, response.status, response.statusText); + return value; + }, + (hasError || hasResponseErrorInterceptor) ? + function(response) { + if (hasError && !hasResponseErrorInterceptor) { + // Avoid `Possibly Unhandled Rejection` error, + // but still fulfill the returned promise with a rejection + promise.catch(noop); + } + if (hasError) error(response); + return hasResponseErrorInterceptor ? + responseErrorInterceptor(response) : + $q.reject(response); + } : + undefined); + + if (!isInstanceCall) { + // we are creating instance / collection + // - set the initial promise + // - return the instance / collection + value.$promise = promise; + value.$resolved = false; + if (cancellable) value.$cancelRequest = cancelRequest; + + return value; + } + + // instance call + return promise; + + function cancelRequest(value) { + promise.catch(noop); + if (timeoutDeferred !== null) { + timeoutDeferred.resolve(value); + } + } + }; + + + Resource.prototype['$' + name] = function(params, success, error) { + if (isFunction(params)) { + error = success; success = params; params = {}; + } + var result = Resource[name].call(this, params, this, success, error); + return result.$promise || result; + }; + }); + + return Resource; + } + + return resourceFactory; + }]; + }); + + +})(window, window.angular); + /** * @license AngularJS v1.6.7 * (c) 2010-2017 Google, Inc. http://angularjs.org @@ -3767,1022 +4625,1637 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { })(window, window.angular); -(function() { - 'use strict'; +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.angularStripe = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 0) { - toast.scope.refreshTimer(newTime); - } - } - - function remove(toastId, wasClicked) { - var toast = findToast(toastId); - - if (toast && ! toast.deleting) { // Avoid clicking when fading out - toast.deleting = true; - toast.isOpened = false; - $animate.leave(toast.el).then(function() { - if (toast.scope.options.onHidden) { - toast.scope.options.onHidden(!!wasClicked, toast); - } - toast.scope.$destroy(); - var index = toasts.indexOf(toast); - delete openToasts[toast.scope.message]; - toasts.splice(index, 1); - var maxOpened = toastrConfig.maxOpened; - if (maxOpened && toasts.length >= maxOpened) { - toasts[maxOpened - 1].open.resolve(); - } - if (lastToast()) { - container.remove(); - container = null; - containerDefer = $q.defer(); - } - }); +function Nodeback (apply, resolve, reject) { + return function nodeback (err, value) { + var args = arguments + apply(function () { + if (err) { + return reject(err) } - function findToast(toastId) { - for (var i = 0; i < toasts.length; i++) { - if (toasts[i].toastId === toastId) { - return toasts[i]; - } - } + if (args.length <= 2) { + return resolve(value) } - function lastToast() { - return !toasts.length; - } + resolve(toArray(args, 1)) + }) + } +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"assert-function":12,"to-array":34}],4:[function(_dereq_,module,exports){ +(function (global){ +'use strict' + +var angular = (typeof window !== "undefined" ? window['angular'] : typeof global !== "undefined" ? global['angular'] : null) +var provider = _dereq_('./provider') + +module.exports = angular.module('angular-stripe', [ + _dereq_('angular-q-promisify'), + _dereq_('angular-assert-q-constructor') +]) +.provider('stripe', provider) +.run(verifyQ) +.name + +verifyQ.$inject = ['assertQConstructor'] +function verifyQ (assertQConstructor) { + assertQConstructor('angular-stripe: For Angular <= 1.2 support, first load https://github.com/bendrucker/angular-q-constructor') +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./provider":6,"angular-assert-q-constructor":1,"angular-q-promisify":2}],5:[function(_dereq_,module,exports){ +'use strict' + +var Lazy = _dereq_('lazy-async') +var dot = _dereq_('dot-prop') +var loadScript = _dereq_('load-script-global') +var stripeErrback = _dereq_('stripe-errback') + +module.exports = LazyStripe + +function LazyStripe (url, promisify) { + var methods = stripeErrback.methods.async.concat(stripeErrback.methods.sync) + var lazy = Lazy(methods, load) + + return methods.reduce(function (acc, method) { + var fn = dot.get(lazy, method) + dot.set(acc, method, promisify(fn)) + return acc + }, {}) + + function load (callback) { + loadScript({ + url: url, + global: 'Stripe' + }, onLoad) + + function onLoad (err, Stripe) { + if (err) return callback(err) + var stripe = stripeErrback(Stripe) + stripe.setPublishableKey = Success(stripe.setPublishableKey, stripe) + callback(null, stripe) } + } +} - /* Internal functions */ - function _buildNotification(type, message, title, optionsOverride) { - if (angular.isObject(title)) { - optionsOverride = title; - title = null; - } +function Success (fn, context) { + return function success () { + var callback = Array.prototype.pop.call(arguments) + fn.apply(context, arguments) + callback() + } +} - return _notify({ - iconClass: type, - message: message, - optionsOverride: optionsOverride, - title: title - }); +},{"dot-prop":17,"lazy-async":25,"load-script-global":27,"stripe-errback":33}],6:[function(_dereq_,module,exports){ +'use strict' + +var LazyStripe = _dereq_('./lazy') + +module.exports = stripeProvider + +function stripeProvider () { + var key = null + var stripe = null + + this.url = 'https://js.stripe.com/v2/' + this.setPublishableKey = function setPublishableKey (_key) { + key = _key + } + + this.$get = service + this.$get.$inject = ['promisify', '$exceptionHandler'] + + function service (promisify, $exceptionHandler) { + if (stripe) return stripe + stripe = LazyStripe(this.url, promisify) + stripe.setPublishableKey(key) + return stripe + } +} + +},{"./lazy":5}],7:[function(_dereq_,module,exports){ +/*! + * array-last + * + * Copyright (c) 2014 Jon Schlinkert, contributors. + * Licensed under the MIT license. + */ + +var isNumber = _dereq_('is-number'); +var slice = _dereq_('array-slice'); + +module.exports = function last(arr, num) { + if (!Array.isArray(arr)) { + throw new Error('array-last expects an array as the first argument.'); + } + + if (arr.length === 0) { + return null; + } + + var res = slice(arr, arr.length - (isNumber(num) ? +num : 1)); + if (+num === 1 || num == null) { + return res[0]; + } + return res; +}; + +},{"array-slice":8,"is-number":20}],8:[function(_dereq_,module,exports){ +/*! + * array-slice + * + * Copyright (c) 2014-2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + +'use strict'; + +module.exports = function slice(arr, start, end) { + var len = arr.length >>> 0; + var range = []; + + start = idx(arr, start); + end = idx(arr, end, len); + + while (start < end) { + range.push(arr[start++]); + } + return range; +}; + + +function idx(arr, pos, end) { + var len = arr.length >>> 0; + + if (pos == null) { + pos = end || 0; + } else if (pos < 0) { + pos = Math.max(len + pos, 0); + } else { + pos = Math.min(pos, len); + } + + return pos; +} +},{}],9:[function(_dereq_,module,exports){ +"use strict"; + +// rawAsap provides everything we need except exception management. +var rawAsap = _dereq_("./raw"); +// RawTasks are recycled to reduce GC churn. +var freeTasks = []; +// We queue errors to ensure they are thrown in right order (FIFO). +// Array-as-queue is good enough here, since we are just dealing with exceptions. +var pendingErrors = []; +var requestErrorThrow = rawAsap.makeRequestCallFromTimer(throwFirstError); + +function throwFirstError() { + if (pendingErrors.length) { + throw pendingErrors.shift(); } +} - function _getOptions() { - return angular.extend({}, toastrConfig); +/** + * Calls a task as soon as possible after returning, in its own event, with priority + * over other events like animation, reflow, and repaint. An error thrown from an + * event will not interrupt, nor even substantially slow down the processing of + * other events, but will be rather postponed to a lower priority event. + * @param {{call}} task A callable object, typically a function that takes no + * arguments. + */ +module.exports = asap; +function asap(task) { + var rawTask; + if (freeTasks.length) { + rawTask = freeTasks.pop(); + } else { + rawTask = new RawTask(); } + rawTask.task = task; + rawAsap(rawTask); +} - function _createOrGetContainer(options) { - if(container) { return containerDefer.promise; } +// We wrap tasks with recyclable task objects. A task object implements +// `call`, just like a function. +function RawTask() { + this.task = null; +} - container = angular.element('
'); - container.attr('id', options.containerId); - container.addClass(options.positionClass); - container.css({'pointer-events': 'auto'}); - - var target = angular.element(document.querySelector(options.target)); - - if ( ! target || ! target.length) { - throw 'Target for toasts doesn\'t exist'; - } - - $animate.enter(container, target).then(function() { - containerDefer.resolve(); - }); - - return containerDefer.promise; - } - - function _notify(map) { - var options = _getOptions(); - - if (shouldExit()) { return; } - - var newToast = createToast(); - - toasts.push(newToast); - - if (ifMaxOpenedAndAutoDismiss()) { - var oldToasts = toasts.slice(0, (toasts.length - options.maxOpened)); - for (var i = 0, len = oldToasts.length; i < len; i++) { - remove(oldToasts[i].toastId); - } - } - - if (maxOpenedNotReached()) { - newToast.open.resolve(); - } - - newToast.open.promise.then(function() { - _createOrGetContainer(options).then(function() { - newToast.isOpened = true; - if (options.newestOnTop) { - $animate.enter(newToast.el, container).then(function() { - newToast.scope.init(); - }); - } else { - var sibling = container[0].lastChild ? angular.element(container[0].lastChild) : null; - $animate.enter(newToast.el, container, sibling).then(function() { - newToast.scope.init(); - }); - } - }); - }); - - return newToast; - - function ifMaxOpenedAndAutoDismiss() { - return options.autoDismiss && options.maxOpened && toasts.length > options.maxOpened; - } - - function createScope(toast, map, options) { - if (options.allowHtml) { - toast.scope.allowHtml = true; - toast.scope.title = $sce.trustAsHtml(map.title); - toast.scope.message = $sce.trustAsHtml(map.message); +// The sole purpose of wrapping the task is to catch the exception and recycle +// the task object after its single use. +RawTask.prototype.call = function () { + try { + this.task.call(); + } catch (error) { + if (asap.onerror) { + // This hook exists purely for testing purposes. + // Its name will be periodically randomized to break any code that + // depends on its existence. + asap.onerror(error); } else { - toast.scope.title = map.title; - toast.scope.message = map.message; + // In a web browser, exceptions are not fatal. However, to avoid + // slowing down the queue of pending tasks, we rethrow the error in a + // lower priority turn. + pendingErrors.push(error); + requestErrorThrow(); + } + } finally { + this.task = null; + freeTasks[freeTasks.length] = this; + } +}; + +},{"./raw":10}],10:[function(_dereq_,module,exports){ +(function (global){ +"use strict"; + +// Use the fastest means possible to execute a task in its own turn, with +// priority over other events including IO, animation, reflow, and redraw +// events in browsers. +// +// An exception thrown by a task will permanently interrupt the processing of +// subsequent tasks. The higher level `asap` function ensures that if an +// exception is thrown by a task, that the task queue will continue flushing as +// soon as possible, but if you use `rawAsap` directly, you are responsible to +// either ensure that no exceptions are thrown from your task, or to manually +// call `rawAsap.requestFlush` if an exception is thrown. +module.exports = rawAsap; +function rawAsap(task) { + if (!queue.length) { + requestFlush(); + flushing = true; + } + // Equivalent to push, but avoids a function call. + queue[queue.length] = task; +} + +var queue = []; +// Once a flush has been requested, no further calls to `requestFlush` are +// necessary until the next `flush` completes. +var flushing = false; +// `requestFlush` is an implementation-specific method that attempts to kick +// off a `flush` event as quickly as possible. `flush` will attempt to exhaust +// the event queue before yielding to the browser's own event loop. +var requestFlush; +// The position of the next task to execute in the task queue. This is +// preserved between calls to `flush` so that it can be resumed if +// a task throws an exception. +var index = 0; +// If a task schedules additional tasks recursively, the task queue can grow +// unbounded. To prevent memory exhaustion, the task queue will periodically +// truncate already-completed tasks. +var capacity = 1024; + +// The flush function processes all tasks that have been scheduled with +// `rawAsap` unless and until one of those tasks throws an exception. +// If a task throws an exception, `flush` ensures that its state will remain +// consistent and will resume where it left off when called again. +// However, `flush` does not make any arrangements to be called again if an +// exception is thrown. +function flush() { + while (index < queue.length) { + var currentIndex = index; + // Advance the index before calling the task. This ensures that we will + // begin flushing on the next task the task throws an error. + index = index + 1; + queue[currentIndex].call(); + // Prevent leaking memory for long chains of recursive calls to `asap`. + // If we call `asap` within tasks scheduled by `asap`, the queue will + // grow, but to avoid an O(n) walk for every task we execute, we don't + // shift tasks off the queue after they have been executed. + // Instead, we periodically shift 1024 tasks off the queue. + if (index > capacity) { + // Manually shift all values starting at the index back to the + // beginning of the queue. + for (var scan = 0, newLength = queue.length - index; scan < newLength; scan++) { + queue[scan] = queue[scan + index]; + } + queue.length -= index; + index = 0; + } + } + queue.length = 0; + index = 0; + flushing = false; +} + +// `requestFlush` is implemented using a strategy based on data collected from +// every available SauceLabs Selenium web driver worker at time of writing. +// https://docs.google.com/spreadsheets/d/1mG-5UYGup5qxGdEMWkhP6BWCz053NUb2E1QoUTU16uA/edit#gid=783724593 + +// Safari 6 and 6.1 for desktop, iPad, and iPhone are the only browsers that +// have WebKitMutationObserver but not un-prefixed MutationObserver. +// Must use `global` or `self` instead of `window` to work in both frames and web +// workers. `global` is a provision of Browserify, Mr, Mrs, or Mop. + +/* globals self */ +var scope = typeof global !== "undefined" ? global : self; +var BrowserMutationObserver = scope.MutationObserver || scope.WebKitMutationObserver; + +// MutationObservers are desirable because they have high priority and work +// reliably everywhere they are implemented. +// They are implemented in all modern browsers. +// +// - Android 4-4.3 +// - Chrome 26-34 +// - Firefox 14-29 +// - Internet Explorer 11 +// - iPad Safari 6-7.1 +// - iPhone Safari 7-7.1 +// - Safari 6-7 +if (typeof BrowserMutationObserver === "function") { + requestFlush = makeRequestCallFromMutationObserver(flush); + +// MessageChannels are desirable because they give direct access to the HTML +// task queue, are implemented in Internet Explorer 10, Safari 5.0-1, and Opera +// 11-12, and in web workers in many engines. +// Although message channels yield to any queued rendering and IO tasks, they +// would be better than imposing the 4ms delay of timers. +// However, they do not work reliably in Internet Explorer or Safari. + +// Internet Explorer 10 is the only browser that has setImmediate but does +// not have MutationObservers. +// Although setImmediate yields to the browser's renderer, it would be +// preferrable to falling back to setTimeout since it does not have +// the minimum 4ms penalty. +// Unfortunately there appears to be a bug in Internet Explorer 10 Mobile (and +// Desktop to a lesser extent) that renders both setImmediate and +// MessageChannel useless for the purposes of ASAP. +// https://github.com/kriskowal/q/issues/396 + +// Timers are implemented universally. +// We fall back to timers in workers in most engines, and in foreground +// contexts in the following browsers. +// However, note that even this simple case requires nuances to operate in a +// broad spectrum of browsers. +// +// - Firefox 3-13 +// - Internet Explorer 6-9 +// - iPad Safari 4.3 +// - Lynx 2.8.7 +} else { + requestFlush = makeRequestCallFromTimer(flush); +} + +// `requestFlush` requests that the high priority event queue be flushed as +// soon as possible. +// This is useful to prevent an error thrown in a task from stalling the event +// queue if the exception handled by Node.js’s +// `process.on("uncaughtException")` or by a domain. +rawAsap.requestFlush = requestFlush; + +// To request a high priority event, we induce a mutation observer by toggling +// the text of a text node between "1" and "-1". +function makeRequestCallFromMutationObserver(callback) { + var toggle = 1; + var observer = new BrowserMutationObserver(callback); + var node = document.createTextNode(""); + observer.observe(node, {characterData: true}); + return function requestCall() { + toggle = -toggle; + node.data = toggle; + }; +} + +// The message channel technique was discovered by Malte Ubl and was the +// original foundation for this library. +// http://www.nonblocking.io/2011/06/windownexttick.html + +// Safari 6.0.5 (at least) intermittently fails to create message ports on a +// page's first load. Thankfully, this version of Safari supports +// MutationObservers, so we don't need to fall back in that case. + +// function makeRequestCallFromMessageChannel(callback) { +// var channel = new MessageChannel(); +// channel.port1.onmessage = callback; +// return function requestCall() { +// channel.port2.postMessage(0); +// }; +// } + +// For reasons explained above, we are also unable to use `setImmediate` +// under any circumstances. +// Even if we were, there is another bug in Internet Explorer 10. +// It is not sufficient to assign `setImmediate` to `requestFlush` because +// `setImmediate` must be called *by name* and therefore must be wrapped in a +// closure. +// Never forget. + +// function makeRequestCallFromSetImmediate(callback) { +// return function requestCall() { +// setImmediate(callback); +// }; +// } + +// Safari 6.0 has a problem where timers will get lost while the user is +// scrolling. This problem does not impact ASAP because Safari 6.0 supports +// mutation observers, so that implementation is used instead. +// However, if we ever elect to use timers in Safari, the prevalent work-around +// is to add a scroll event listener that calls for a flush. + +// `setTimeout` does not call the passed callback if the delay is less than +// approximately 7 in web workers in Firefox 8 through 18, and sometimes not +// even then. + +function makeRequestCallFromTimer(callback) { + return function requestCall() { + // We dispatch a timeout with a specified delay of 0 for engines that + // can reliably accommodate that request. This will usually be snapped + // to a 4 milisecond delay, but once we're flushing, there's no delay + // between events. + var timeoutHandle = setTimeout(handleTimer, 0); + // However, since this timer gets frequently dropped in Firefox + // workers, we enlist an interval handle that will try to fire + // an event 20 times per second until it succeeds. + var intervalHandle = setInterval(handleTimer, 50); + + function handleTimer() { + // Whichever timer succeeds will cancel both timers and + // execute the callback. + clearTimeout(timeoutHandle); + clearInterval(intervalHandle); + callback(); + } + }; +} + +// This is for `asap.js` only. +// Its name will be periodically randomized to break any code that depends on +// its existence. +rawAsap.makeRequestCallFromTimer = makeRequestCallFromTimer; + +// ASAP was originally a nextTick shim included in Q. This was factored out +// into this ASAP package. It was later adapted to RSVP which made further +// amendments. These decisions, particularly to marginalize MessageChannel and +// to capture the MutationObserver implementation in a closure, were integrated +// back into ASAP proper. +// https://github.com/tildeio/rsvp.js/blob/cddf7232546a9cf858524b75cde6f9edf72620a7/lib/rsvp/asap.js + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],11:[function(_dereq_,module,exports){ +'use strict' + +var assert = _dereq_('assert-ok') +var format = _dereq_('simple-format') +var print = _dereq_('print-value') + +module.exports = function assertEqual (a, b) { + assert(a === b, format('expected `%s` to equal `%s`', print(a), print(b))) +} + +},{"assert-ok":13,"print-value":30,"simple-format":32}],12:[function(_dereq_,module,exports){ +'use strict' + +module.exports = function assertFunction (value) { + if (typeof value !== 'function') { + throw new TypeError('Expected function, got: ' + value) + } +} + +},{}],13:[function(_dereq_,module,exports){ +'use strict' + +module.exports = function assertOk (value, message) { + if (!value) { + throw new Error(message || 'Expected true, got ' + value) + } +} + +},{}],14:[function(_dereq_,module,exports){ +'use strict' + +module.exports = CallAll + +function CallAll (fns) { + fns = Array.isArray(fns) ? fns : arguments + return function callAll () { + var args = arguments + var ret = new Array(fns.length) + for (var i = 0, ii = fns.length; i < ii; i++) { + ret[i] = fns[i].apply(null, args) + } + return ret + } +} + +},{}],15:[function(_dereq_,module,exports){ +/** + * cuid.js + * Collision-resistant UID generator for browsers and node. + * Sequential for fast db lookups and recency sorting. + * Safe for element IDs and server-side lookups. + * + * Extracted from CLCTR + * + * Copyright (c) Eric Elliott 2012 + * MIT License + */ + +/*global window, navigator, document, require, process, module */ +(function (app) { + 'use strict'; + var namespace = 'cuid', + c = 0, + blockSize = 4, + base = 36, + discreteValues = Math.pow(base, blockSize), + + pad = function pad(num, size) { + var s = "000000000" + num; + return s.substr(s.length-size); + }, + + randomBlock = function randomBlock() { + return pad((Math.random() * + discreteValues << 0) + .toString(base), blockSize); + }, + + safeCounter = function () { + c = (c < discreteValues) ? c : 0; + c++; // this is not subliminal + return c - 1; + }, + + api = function cuid() { + // Starting with a lowercase letter makes + // it HTML element ID friendly. + var letter = 'c', // hard-coded allows for sequential access + + // timestamp + // warning: this exposes the exact date and time + // that the uid was created. + timestamp = (new Date().getTime()).toString(base), + + // Prevent same-machine collisions. + counter, + + // A few chars to generate distinct ids for different + // clients (so different computers are far less + // likely to generate the same id) + fingerprint = api.fingerprint(), + + // Grab some more chars from Math.random() + random = randomBlock() + randomBlock(); + + counter = pad(safeCounter().toString(base), blockSize); + + return (letter + timestamp + counter + fingerprint + random); + }; + + api.slug = function slug() { + var date = new Date().getTime().toString(36), + counter, + print = api.fingerprint().slice(0,1) + + api.fingerprint().slice(-1), + random = randomBlock().slice(-2); + + counter = safeCounter().toString(36).slice(-4); + + return date.slice(-2) + + counter + print + random; + }; + + api.globalCount = function globalCount() { + // We want to cache the results of this + var cache = (function calc() { + var i, + count = 0; + + for (i in window) { + count++; } - toast.scope.toastType = toast.iconClass; - toast.scope.toastId = toast.toastId; - toast.scope.extraData = options.extraData; + return count; + }()); - toast.scope.options = { - extendedTimeOut: options.extendedTimeOut, - messageClass: options.messageClass, - onHidden: options.onHidden, - onShown: generateEvent('onShown'), - onTap: generateEvent('onTap'), - progressBar: options.progressBar, - tapToDismiss: options.tapToDismiss, - timeOut: options.timeOut, - titleClass: options.titleClass, - toastClass: options.toastClass - }; + api.globalCount = function () { return cache; }; + return cache; + }; - if (options.closeButton) { - toast.scope.options.closeHtml = options.closeHtml; + api.fingerprint = function browserPrint() { + return pad((navigator.mimeTypes.length + + navigator.userAgent.length).toString(36) + + api.globalCount().toString(36), 4); + }; + + // don't change anything from here down. + if (app.register) { + app.register(namespace, api); + } else if (typeof module !== 'undefined') { + module.exports = api; + } else { + app[namespace] = api; + } + +}(this.applitude || this)); + +},{}],16:[function(_dereq_,module,exports){ +var wrappy = _dereq_('wrappy') +module.exports = wrappy(dezalgo) + +var asap = _dereq_('asap') + +function dezalgo (cb) { + var sync = true + asap(function () { + sync = false + }) + + return function zalgoSafe() { + var args = arguments + var me = this + if (sync) + asap(function() { + cb.apply(me, args) + }) + else + cb.apply(me, args) + } +} + +},{"asap":9,"wrappy":35}],17:[function(_dereq_,module,exports){ +'use strict'; +var isObj = _dereq_('is-obj'); + +module.exports.get = function (obj, path) { + if (!isObj(obj) || typeof path !== 'string') { + return obj; + } + + var pathArr = getPathSegments(path); + + for (var i = 0; i < pathArr.length; i++) { + var descriptor = Object.getOwnPropertyDescriptor(obj, pathArr[i]) || Object.getOwnPropertyDescriptor(Object.prototype, pathArr[i]); + if (descriptor && !descriptor.enumerable) { + return; + } + + obj = obj[pathArr[i]]; + + if (obj === undefined || obj === null) { + // `obj` is either `undefined` or `null` so we want to stop the loop, and + // if this is not the last bit of the path, and + // if it did't return `undefined` + // it would return `null` if `obj` is `null` + // but we want `get({foo: null}, 'foo.bar')` to equal `undefined` not `null` + if (i !== pathArr.length - 1) { + return undefined; + } + + break; + } + } + + return obj; +}; + +module.exports.set = function (obj, path, value) { + if (!isObj(obj) || typeof path !== 'string') { + return; + } + + var pathArr = getPathSegments(path); + + for (var i = 0; i < pathArr.length; i++) { + var p = pathArr[i]; + + if (!isObj(obj[p])) { + obj[p] = {}; + } + + if (i === pathArr.length - 1) { + obj[p] = value; + } + + obj = obj[p]; + } +}; + +module.exports.delete = function (obj, path) { + if (!isObj(obj) || typeof path !== 'string') { + return; + } + + var pathArr = getPathSegments(path); + + for (var i = 0; i < pathArr.length; i++) { + var p = pathArr[i]; + + if (i === pathArr.length - 1) { + delete obj[p]; + return; + } + + obj = obj[p]; + } +}; + +module.exports.has = function (obj, path) { + if (!isObj(obj) || typeof path !== 'string') { + return false; + } + + var pathArr = getPathSegments(path); + + for (var i = 0; i < pathArr.length; i++) { + obj = obj[pathArr[i]]; + + if (obj === undefined) { + return false; + } + } + + return true; +}; + +function getPathSegments(path) { + var pathArr = path.split('.'); + var parts = []; + + for (var i = 0; i < pathArr.length; i++) { + var p = pathArr[i]; + + while (p[p.length - 1] === '\\' && pathArr[i + 1] !== undefined) { + p = p.slice(0, -1) + '.'; + p += pathArr[++i]; + } + + parts.push(p); + } + + return parts; +} + +},{"is-obj":21}],18:[function(_dereq_,module,exports){ +'use strict' + +var assertFn = _dereq_('assert-function') + +module.exports = Ear + +function Ear () { + var callbacks = [] + + function listeners () { + var args = arguments + var i = 0 + var length = callbacks.length + for (; i < length; i++) { + var callback = callbacks[i] + callback.apply(null, args) + } + } + + listeners.add = function (listener) { + assertFn(listener) + callbacks.push(listener) + return function remove () { + var i = 0 + var length = callbacks.length + for (; i < length; i++) { + if (callbacks[i] === listener) { + callbacks.splice(i, 1) + return } - - function generateEvent(event) { - if (options[event]) { - return function() { - options[event](toast); - }; - } - } - } - - function createToast() { - var newToast = { - toastId: index++, - isOpened: false, - scope: $rootScope.$new(), - open: $q.defer() - }; - newToast.iconClass = map.iconClass; - if (map.optionsOverride) { - angular.extend(options, cleanOptionsOverride(map.optionsOverride)); - newToast.iconClass = map.optionsOverride.iconClass || newToast.iconClass; - } - - createScope(newToast, map, options); - - newToast.el = createToastEl(newToast.scope); - - return newToast; - - function cleanOptionsOverride(options) { - var badOptions = ['containerId', 'iconClasses', 'maxOpened', 'newestOnTop', - 'positionClass', 'preventDuplicates', 'preventOpenDuplicates', 'templates']; - for (var i = 0, l = badOptions.length; i < l; i++) { - delete options[badOptions[i]]; - } - - return options; - } - } - - function createToastEl(scope) { - var angularDomEl = angular.element('
'), - $compile = $injector.get('$compile'); - return $compile(angularDomEl)(scope); - } - - function maxOpenedNotReached() { - return options.maxOpened && toasts.length <= options.maxOpened || !options.maxOpened; - } - - function shouldExit() { - var isDuplicateOfLast = options.preventDuplicates && map.message === previousToastMessage; - var isDuplicateOpen = options.preventOpenDuplicates && openToasts[map.message]; - - if (isDuplicateOfLast || isDuplicateOpen) { - return true; - } - - previousToastMessage = map.message; - openToasts[map.message] = true; - - return false; } } } -}()); -(function() { - 'use strict'; + return listeners +} - angular.module('toastr') - .constant('toastrConfig', { - allowHtml: false, - autoDismiss: false, - closeButton: false, - closeHtml: '', - containerId: 'toast-container', - extendedTimeOut: 1000, - iconClasses: { - error: 'toast-error', - info: 'toast-info', - success: 'toast-success', - warning: 'toast-warning' - }, - maxOpened: 0, - messageClass: 'toast-message', - newestOnTop: true, - onHidden: null, - onShown: null, - onTap: null, - positionClass: 'toast-top-right', - preventDuplicates: false, - preventOpenDuplicates: false, - progressBar: false, - tapToDismiss: true, - target: 'body', - templates: { - toast: 'directives/toast/toast.html', - progressbar: 'directives/progressbar/progressbar.html' - }, - timeOut: 5000, - titleClass: 'toast-title', - toastClass: 'toast' - }); -}()); +},{"assert-function":12}],19:[function(_dereq_,module,exports){ +(function (global){ +var win; -(function() { - 'use strict'; +if (typeof window !== "undefined") { + win = window; +} else if (typeof global !== "undefined") { + win = global; +} else if (typeof self !== "undefined"){ + win = self; +} else { + win = {}; +} - angular.module('toastr') - .directive('progressBar', progressBar); +module.exports = win; - progressBar.$inject = ['toastrConfig']; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],20:[function(_dereq_,module,exports){ +/*! + * is-number + * + * Copyright (c) 2014 Jon Schlinkert, contributors. + * Licensed under the MIT License + */ - function progressBar(toastrConfig) { - return { - require: '^toast', - templateUrl: function() { - return toastrConfig.templates.progressbar; - }, - link: linkFunction - }; +'use strict'; - function linkFunction(scope, element, attrs, toastCtrl) { - var intervalId, currentTimeOut, hideTime; +module.exports = function isNumber(n) { + return !!(+n) || n === 0 || n === '0'; +}; - toastCtrl.progressBar = scope; +},{}],21:[function(_dereq_,module,exports){ +'use strict'; +module.exports = function (x) { + var type = typeof x; + return x !== null && (type === 'object' || type === 'function'); +}; - scope.start = function(duration) { - if (intervalId) { - clearInterval(intervalId); +},{}],22:[function(_dereq_,module,exports){ +module.exports = Array.isArray || function (arr) { + return Object.prototype.toString.call(arr) == '[object Array]'; +}; + +},{}],23:[function(_dereq_,module,exports){ +/*! + * isobject + * + * Copyright (c) 2014-2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + +'use strict'; + +var isArray = _dereq_('isarray'); + +module.exports = function isObject(o) { + return o != null && typeof o === 'object' && !isArray(o); +}; + +},{"isarray":22}],24:[function(_dereq_,module,exports){ +exports = module.exports = stringify +exports.getSerialize = serializer + +function stringify(obj, replacer, spaces, cycleReplacer) { + return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces) +} + +function serializer(replacer, cycleReplacer) { + var stack = [], keys = [] + + if (cycleReplacer == null) cycleReplacer = function(key, value) { + if (stack[0] === value) return "[Circular ~]" + return "[Circular ~." + keys.slice(0, stack.indexOf(value)).join(".") + "]" + } + + return function(key, value) { + if (stack.length > 0) { + var thisPos = stack.indexOf(this) + ~thisPos ? stack.splice(thisPos + 1) : stack.push(this) + ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key) + if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value) + } + else stack.push(value) + + return replacer == null ? value : replacer.call(this, key, value) + } +} + +},{}],25:[function(_dereq_,module,exports){ +'use strict' + +var assert = _dereq_('assert-ok') +var assertEqual = _dereq_('assert-equal') +var dot = _dereq_('dot-prop') +var toArray = _dereq_('to-array') +var last = _dereq_('array-last') +var dezalgo = _dereq_('dezalgo') +var all = _dereq_('call-all-fns') + +module.exports = Lazy + +function Lazy (methods, load) { + assert(Array.isArray(methods), 'methods are required') + assertEqual(typeof load, 'function', 'load fn is required') + + var api = null + var error = null + var queue = [] + + load(function (err, lib) { + error = err + api = lib + all(queue)(err, lib) + queue = null + }) + + return methods.reduce(function (lazy, method) { + dot.set(lazy, method, Deferred(method)) + return lazy + }, {}) + + function Deferred (method) { + return function deferred () { + var args = arguments + onReady(function (err, api) { + if (!err) return dot.get(api, method).apply(null, args) + var callback = last(toArray(args)) + if (typeof callback === 'function') { + return callback(err) } - - currentTimeOut = parseFloat(duration); - hideTime = new Date().getTime() + currentTimeOut; - intervalId = setInterval(updateProgress, 10); - }; - - scope.stop = function() { - if (intervalId) { - clearInterval(intervalId); - } - }; - - function updateProgress() { - var percentage = ((hideTime - (new Date().getTime())) / currentTimeOut) * 100; - element.css('width', percentage + '%'); - } - - scope.$on('$destroy', function() { - // Failsafe stop - clearInterval(intervalId); - }); + }) } } -}()); -(function() { - 'use strict'; + function onReady (callback) { + callback = dezalgo(callback) - angular.module('toastr') - .controller('ToastController', ToastController); - - function ToastController() { - this.progressBar = null; - - this.startProgressBar = function(duration) { - if (this.progressBar) { - this.progressBar.start(duration); - } - }; - - this.stopProgressBar = function() { - if (this.progressBar) { - this.progressBar.stop(); - } - }; + if (api || error) return callback(error, api) + queue.push(callback) } -}()); +} -(function() { - 'use strict'; +},{"array-last":7,"assert-equal":11,"assert-ok":13,"call-all-fns":14,"dezalgo":16,"dot-prop":26,"to-array":34}],26:[function(_dereq_,module,exports){ +'use strict'; +var isObj = _dereq_('is-obj'); - angular.module('toastr') - .directive('toast', toast); +module.exports.get = function (obj, path) { + if (!isObj(obj) || typeof path !== 'string') { + return obj; + } - toast.$inject = ['$injector', '$interval', 'toastrConfig', 'toastr']; + var pathArr = getPathSegments(path); - function toast($injector, $interval, toastrConfig, toastr) { - return { - templateUrl: function() { - return toastrConfig.templates.toast; - }, - controller: 'ToastController', - link: toastLinkFunction - }; + for (var i = 0; i < pathArr.length; i++) { + obj = obj[pathArr[i]]; - function toastLinkFunction(scope, element, attrs, toastCtrl) { - var timeout; + if (obj === undefined) { + break; + } + } - scope.toastClass = scope.options.toastClass; - scope.titleClass = scope.options.titleClass; - scope.messageClass = scope.options.messageClass; - scope.progressBar = scope.options.progressBar; + return obj; +}; - if (wantsCloseButton()) { - var button = angular.element(scope.options.closeHtml), - $compile = $injector.get('$compile'); - button.addClass('toast-close-button'); - button.attr('ng-click', 'close(true, $event)'); - $compile(button)(scope); - element.children().prepend(button); - } +module.exports.set = function (obj, path, value) { + if (!isObj(obj) || typeof path !== 'string') { + return; + } - scope.init = function() { - if (scope.options.timeOut) { - timeout = createTimeout(scope.options.timeOut); - } - if (scope.options.onShown) { - scope.options.onShown(); - } - }; + var pathArr = getPathSegments(path); - element.on('mouseenter', function() { - hideAndStopProgressBar(); - if (timeout) { - $interval.cancel(timeout); - } - }); + for (var i = 0; i < pathArr.length; i++) { + var p = pathArr[i]; - scope.tapToast = function () { - if (angular.isFunction(scope.options.onTap)) { - scope.options.onTap(); - } - if (scope.options.tapToDismiss) { - scope.close(true); - } - }; + if (!isObj(obj[p])) { + obj[p] = {}; + } - scope.close = function (wasClicked, $event) { - if ($event && angular.isFunction($event.stopPropagation)) { - $event.stopPropagation(); - } - toastr.remove(scope.toastId, wasClicked); - }; - - scope.refreshTimer = function(newTime) { - if (timeout) { - $interval.cancel(timeout); - timeout = createTimeout(newTime || scope.options.timeOut); - } - }; + if (i === pathArr.length - 1) { + obj[p] = value; + } - element.on('mouseleave', function() { - if (scope.options.timeOut === 0 && scope.options.extendedTimeOut === 0) { return; } - scope.$apply(function() { - scope.progressBar = scope.options.progressBar; - }); - timeout = createTimeout(scope.options.extendedTimeOut); - }); + obj = obj[p]; + } +}; - function createTimeout(time) { - toastCtrl.startProgressBar(time); - return $interval(function() { - toastCtrl.stopProgressBar(); - toastr.remove(scope.toastId); - }, time, 1); - } +module.exports.delete = function (obj, path) { + if (!isObj(obj) || typeof path !== 'string') { + return; + } - function hideAndStopProgressBar() { - scope.progressBar = false; - toastCtrl.stopProgressBar(); - } + var pathArr = getPathSegments(path); - function wantsCloseButton() { - return scope.options.closeHtml; - } + for (var i = 0; i < pathArr.length; i++) { + var p = pathArr[i]; + + if (i === pathArr.length - 1) { + delete obj[p]; + return; + } + + obj = obj[p]; + } +}; + +module.exports.has = function (obj, path) { + if (!isObj(obj) || typeof path !== 'string') { + return false; + } + + var pathArr = getPathSegments(path); + + for (var i = 0; i < pathArr.length; i++) { + obj = obj[pathArr[i]]; + + if (obj === undefined) { + return false; + } + } + + return true; +}; + +function getPathSegments(path) { + var pathArr = path.split('.'); + var parts = []; + + for (var i = 0; i < pathArr.length; i++) { + var p = pathArr[i]; + + while (p[p.length - 1] === '\\') { + p = p.slice(0, -1) + '.'; + p += pathArr[++i]; + } + + parts.push(p); + } + + return parts; +} + +},{"is-obj":21}],27:[function(_dereq_,module,exports){ +'use strict' + +var load = _dereq_('load-script') +var window = _dereq_('global/window') +var extend = _dereq_('xtend') +var assert = _dereq_('assert-ok') +var dezalgo = _dereq_('dezalgo') +var Listeners = _dereq_('ear') +var extendQuery = _dereq_('query-extend') +var cuid = _dereq_('cuid') + +module.exports = loadGlobal + +var listeners = {} + +function loadGlobal (options, callback) { + assert(options, 'options required') + assert(options.url, 'url required') + assert(options.global, 'global required') + assert(callback, 'callback required') + + options = extend(options) + callback = dezalgo(callback) + + if (getGlobal(options)) { + return callback(null, getGlobal(options)) + } + + callback = cache(options, callback) + if (!callback) return + + if (options.jsonp) { + var id = jsonpCallback(options, callback) + options.url = extendQuery(options.url, {callback: id}) + } + + load(options.url, options, function (err) { + if (err) return callback(err) + if (!options.jsonp) { + var library = getGlobal(options) + if (!library) return callback(new Error('expected: `window.' + options.global + '`, actual: `' + library + '`')) + callback(null, library) + } + }) +} + +function cache (options, callback) { + if (!get()) { + set(Listeners()) + get().add(callback) + return function onComplete (err, lib) { + get()(err, lib) + set(Listeners()) } } -}()); -(function() { - 'use strict'; + get().add(callback) + return undefined - angular.module('toastr', []) - .factory('toastr', toastr); + function get () { + return listeners[options.global] + } - toastr.$inject = ['$animate', '$injector', '$document', '$rootScope', '$sce', 'toastrConfig', '$q']; + function set (value) { + listeners[options.global] = value + } +} - function toastr($animate, $injector, $document, $rootScope, $sce, toastrConfig, $q) { - var container; - var index = 0; - var toasts = []; +function getGlobal (options) { + return window[options.global] +} - var previousToastMessage = ''; - var openToasts = {}; +function jsonpCallback (options, callback) { + var id = cuid() + window[id] = function jsonpCallback () { + callback(null, getGlobal(options)) + delete window[id] + } + return id +} - var containerDefer = $q.defer(); +},{"assert-ok":13,"cuid":15,"dezalgo":16,"ear":18,"global/window":19,"load-script":28,"query-extend":31,"xtend":36}],28:[function(_dereq_,module,exports){ - var toast = { - active: active, - clear: clear, - error: error, - info: info, - remove: remove, - success: success, - warning: warning, - refreshTimer: refreshTimer - }; +module.exports = function load (src, opts, cb) { + var head = document.head || document.getElementsByTagName('head')[0] + var script = document.createElement('script') - return toast; + if (typeof opts === 'function') { + cb = opts + opts = {} + } - /* Public API */ - function active() { - return toasts.length; - } + opts = opts || {} + cb = cb || function() {} - function clear(toast) { - // Bit of a hack, I will remove this soon with a BC - if (arguments.length === 1 && !toast) { return; } + script.type = opts.type || 'text/javascript' + script.charset = opts.charset || 'utf8'; + script.async = 'async' in opts ? !!opts.async : true + script.src = src - if (toast) { - remove(toast.toastId); - } else { - for (var i = 0; i < toasts.length; i++) { - remove(toasts[i].toastId); - } - } - } + if (opts.attrs) { + setAttributes(script, opts.attrs) + } - function error(message, title, optionsOverride) { - var type = _getOptions().iconClasses.error; - return _buildNotification(type, message, title, optionsOverride); - } + if (opts.text) { + script.text = '' + opts.text + } - function info(message, title, optionsOverride) { - var type = _getOptions().iconClasses.info; - return _buildNotification(type, message, title, optionsOverride); - } + var onend = 'onload' in script ? stdOnEnd : ieOnEnd + onend(script, cb) - function success(message, title, optionsOverride) { - var type = _getOptions().iconClasses.success; - return _buildNotification(type, message, title, optionsOverride); - } + // some good legacy browsers (firefox) fail the 'in' detection above + // so as a fallback we always set onload + // old IE will ignore this and new IE will set onload + if (!script.onload) { + stdOnEnd(script, cb); + } - function warning(message, title, optionsOverride) { - var type = _getOptions().iconClasses.warning; - return _buildNotification(type, message, title, optionsOverride); - } + head.appendChild(script) +} - function refreshTimer(toast, newTime) { - if (toast && toast.isOpened && toasts.indexOf(toast) >= 0) { - toast.scope.refreshTimer(newTime); - } - } +function setAttributes(script, attrs) { + for (var attr in attrs) { + script.setAttribute(attr, attrs[attr]); + } +} - function remove(toastId, wasClicked) { - var toast = findToast(toastId); +function stdOnEnd (script, cb) { + script.onload = function () { + this.onerror = this.onload = null + cb(null, script) + } + script.onerror = function () { + // this.onload = null here is necessary + // because even IE9 works not like others + this.onerror = this.onload = null + cb(new Error('Failed to load ' + this.src), script) + } +} - if (toast && ! toast.deleting) { // Avoid clicking when fading out - toast.deleting = true; - toast.isOpened = false; - $animate.leave(toast.el).then(function() { - if (toast.scope.options.onHidden) { - toast.scope.options.onHidden(!!wasClicked, toast); - } - toast.scope.$destroy(); - var index = toasts.indexOf(toast); - delete openToasts[toast.scope.message]; - toasts.splice(index, 1); - var maxOpened = toastrConfig.maxOpened; - if (maxOpened && toasts.length >= maxOpened) { - toasts[maxOpened - 1].open.resolve(); - } - if (lastToast()) { - container.remove(); - container = null; - containerDefer = $q.defer(); - } - }); - } +function ieOnEnd (script, cb) { + script.onreadystatechange = function () { + if (this.readyState != 'complete' && this.readyState != 'loaded') return + this.onreadystatechange = null + cb(null, script) // there is no way to catch loading errors in IE8 + } +} - function findToast(toastId) { - for (var i = 0; i < toasts.length; i++) { - if (toasts[i].toastId === toastId) { - return toasts[i]; - } - } - } +},{}],29:[function(_dereq_,module,exports){ +'use strict'; - function lastToast() { - return !toasts.length; - } - } +module.exports = function split(str) { + var a = 1, + res = ''; - /* Internal functions */ - function _buildNotification(type, message, title, optionsOverride) { - if (angular.isObject(title)) { - optionsOverride = title; - title = null; - } + var parts = str.split('%'), + len = parts.length; - return _notify({ - iconClass: type, - message: message, - optionsOverride: optionsOverride, - title: title - }); - } + if (len > 0) { res += parts[0]; } - function _getOptions() { - return angular.extend({}, toastrConfig); - } - - function _createOrGetContainer(options) { - if(container) { return containerDefer.promise; } - - container = angular.element('
'); - container.attr('id', options.containerId); - container.addClass(options.positionClass); - container.css({'pointer-events': 'auto'}); - - var target = angular.element(document.querySelector(options.target)); - - if ( ! target || ! target.length) { - throw 'Target for toasts doesn\'t exist'; - } - - $animate.enter(container, target).then(function() { - containerDefer.resolve(); - }); - - return containerDefer.promise; - } - - function _notify(map) { - var options = _getOptions(); - - if (shouldExit()) { return; } - - var newToast = createToast(); - - toasts.push(newToast); - - if (ifMaxOpenedAndAutoDismiss()) { - var oldToasts = toasts.slice(0, (toasts.length - options.maxOpened)); - for (var i = 0, len = oldToasts.length; i < len; i++) { - remove(oldToasts[i].toastId); - } - } - - if (maxOpenedNotReached()) { - newToast.open.resolve(); - } - - newToast.open.promise.then(function() { - _createOrGetContainer(options).then(function() { - newToast.isOpened = true; - if (options.newestOnTop) { - $animate.enter(newToast.el, container).then(function() { - newToast.scope.init(); - }); - } else { - var sibling = container[0].lastChild ? angular.element(container[0].lastChild) : null; - $animate.enter(newToast.el, container, sibling).then(function() { - newToast.scope.init(); - }); - } - }); - }); - - return newToast; - - function ifMaxOpenedAndAutoDismiss() { - return options.autoDismiss && options.maxOpened && toasts.length > options.maxOpened; - } - - function createScope(toast, map, options) { - if (options.allowHtml) { - toast.scope.allowHtml = true; - toast.scope.title = $sce.trustAsHtml(map.title); - toast.scope.message = $sce.trustAsHtml(map.message); + for (var i = 1; i < len; i++) { + if (parts[i][0] === 's' || parts[i][0] === 'd') { + var value = arguments[a++]; + res += parts[i][0] === 'd' ? Math.floor(value) : value; + } else if (parts[i][0]) { + res += '%' + parts[i][0]; } else { - toast.scope.title = map.title; - toast.scope.message = map.message; + i++; + res += '%' + parts[i][0]; } - toast.scope.toastType = toast.iconClass; - toast.scope.toastId = toast.toastId; - toast.scope.extraData = options.extraData; - - toast.scope.options = { - extendedTimeOut: options.extendedTimeOut, - messageClass: options.messageClass, - onHidden: options.onHidden, - onShown: generateEvent('onShown'), - onTap: generateEvent('onTap'), - progressBar: options.progressBar, - tapToDismiss: options.tapToDismiss, - timeOut: options.timeOut, - titleClass: options.titleClass, - toastClass: options.toastClass - }; - - if (options.closeButton) { - toast.scope.options.closeHtml = options.closeHtml; - } - - function generateEvent(event) { - if (options[event]) { - return function() { - options[event](toast); - }; - } - } - } - - function createToast() { - var newToast = { - toastId: index++, - isOpened: false, - scope: $rootScope.$new(), - open: $q.defer() - }; - newToast.iconClass = map.iconClass; - if (map.optionsOverride) { - angular.extend(options, cleanOptionsOverride(map.optionsOverride)); - newToast.iconClass = map.optionsOverride.iconClass || newToast.iconClass; - } - - createScope(newToast, map, options); - - newToast.el = createToastEl(newToast.scope); - - return newToast; - - function cleanOptionsOverride(options) { - var badOptions = ['containerId', 'iconClasses', 'maxOpened', 'newestOnTop', - 'positionClass', 'preventDuplicates', 'preventOpenDuplicates', 'templates']; - for (var i = 0, l = badOptions.length; i < l; i++) { - delete options[badOptions[i]]; - } - - return options; - } - } - - function createToastEl(scope) { - var angularDomEl = angular.element('
'), - $compile = $injector.get('$compile'); - return $compile(angularDomEl)(scope); - } - - function maxOpenedNotReached() { - return options.maxOpened && toasts.length <= options.maxOpened || !options.maxOpened; - } - - function shouldExit() { - var isDuplicateOfLast = options.preventDuplicates && map.message === previousToastMessage; - var isDuplicateOpen = options.preventOpenDuplicates && openToasts[map.message]; - - if (isDuplicateOfLast || isDuplicateOpen) { - return true; - } - - previousToastMessage = map.message; - openToasts[map.message] = true; - - return false; - } + res += parts[i].substring(1); } - } -}()); -(function() { - 'use strict'; + return res; +}; - angular.module('toastr') - .constant('toastrConfig', { - allowHtml: false, - autoDismiss: false, - closeButton: false, - closeHtml: '', - containerId: 'toast-container', - extendedTimeOut: 1000, - iconClasses: { - error: 'toast-error', - info: 'toast-info', - success: 'toast-success', - warning: 'toast-warning' - }, - maxOpened: 0, - messageClass: 'toast-message', - newestOnTop: true, - onHidden: null, - onShown: null, - onTap: null, - positionClass: 'toast-top-right', - preventDuplicates: false, - preventOpenDuplicates: false, - progressBar: false, - tapToDismiss: true, - target: 'body', - templates: { - toast: 'directives/toast/toast.html', - progressbar: 'directives/progressbar/progressbar.html' - }, - timeOut: 5000, - titleClass: 'toast-title', - toastClass: 'toast' +},{}],30:[function(_dereq_,module,exports){ +'use strict' + +var isObject = _dereq_('isobject') +var safeStringify = _dereq_('json-stringify-safe') + +module.exports = function print (value) { + var toString = isJson(value) ? stringify : String + return toString(value) +} + +function isJson (value) { + return isObject(value) || Array.isArray(value) +} + +function stringify (value) { + return safeStringify(value, null, '') +} + +},{"isobject":23,"json-stringify-safe":24}],31:[function(_dereq_,module,exports){ +!function(glob) { + + var queryToObject = function(query) { + var obj = {}; + if (!query) return obj; + each(query.split('&'), function(val) { + var pieces = val.split('='); + var key = parseKey(pieces[0]); + var keyDecoded = decodeURIComponent(key.val); + var valDecoded = pieces[1] && decodeURIComponent(pieces[1]); + + if (key.type === 'array') { + if (!obj[keyDecoded]) obj[keyDecoded] = []; + obj[keyDecoded].push(valDecoded); + } else if (key.type === 'string') { + obj[keyDecoded] = valDecoded; + } }); -}()); + return obj; + }; -(function() { - 'use strict'; - - angular.module('toastr') - .directive('progressBar', progressBar); - - progressBar.$inject = ['toastrConfig']; - - function progressBar(toastrConfig) { - return { - require: '^toast', - templateUrl: function() { - return toastrConfig.templates.progressbar; - }, - link: linkFunction - }; - - function linkFunction(scope, element, attrs, toastCtrl) { - var intervalId, currentTimeOut, hideTime; - - toastCtrl.progressBar = scope; - - scope.start = function(duration) { - if (intervalId) { - clearInterval(intervalId); - } - - currentTimeOut = parseFloat(duration); - hideTime = new Date().getTime() + currentTimeOut; - intervalId = setInterval(updateProgress, 10); - }; - - scope.stop = function() { - if (intervalId) { - clearInterval(intervalId); - } - }; - - function updateProgress() { - var percentage = ((hideTime - (new Date().getTime())) / currentTimeOut) * 100; - element.css('width', percentage + '%'); + var objectToQuery = function(obj) { + var pieces = [], encodedKey; + for (var k in obj) { + if (!obj.hasOwnProperty(k)) continue; + if (typeof obj[k] === 'undefined') { + pieces.push(encodeURIComponent(k)); + continue; } - - scope.$on('$destroy', function() { - // Failsafe stop - clearInterval(intervalId); - }); - } - } -}()); - -(function() { - 'use strict'; - - angular.module('toastr') - .controller('ToastController', ToastController); - - function ToastController() { - this.progressBar = null; - - this.startProgressBar = function(duration) { - if (this.progressBar) { - this.progressBar.start(duration); - } - }; - - this.stopProgressBar = function() { - if (this.progressBar) { - this.progressBar.stop(); - } - }; - } -}()); - -(function() { - 'use strict'; - - angular.module('toastr') - .directive('toast', toast); - - toast.$inject = ['$injector', '$interval', 'toastrConfig', 'toastr']; - - function toast($injector, $interval, toastrConfig, toastr) { - return { - templateUrl: function() { - return toastrConfig.templates.toast; - }, - controller: 'ToastController', - link: toastLinkFunction - }; - - function toastLinkFunction(scope, element, attrs, toastCtrl) { - var timeout; - - scope.toastClass = scope.options.toastClass; - scope.titleClass = scope.options.titleClass; - scope.messageClass = scope.options.messageClass; - scope.progressBar = scope.options.progressBar; - - if (wantsCloseButton()) { - var button = angular.element(scope.options.closeHtml), - $compile = $injector.get('$compile'); - button.addClass('toast-close-button'); - button.attr('ng-click', 'close(true, $event)'); - $compile(button)(scope); - element.children().prepend(button); - } - - scope.init = function() { - if (scope.options.timeOut) { - timeout = createTimeout(scope.options.timeOut); - } - if (scope.options.onShown) { - scope.options.onShown(); - } - }; - - element.on('mouseenter', function() { - hideAndStopProgressBar(); - if (timeout) { - $interval.cancel(timeout); - } - }); - - scope.tapToast = function () { - if (angular.isFunction(scope.options.onTap)) { - scope.options.onTap(); - } - if (scope.options.tapToDismiss) { - scope.close(true); - } - }; - - scope.close = function (wasClicked, $event) { - if ($event && angular.isFunction($event.stopPropagation)) { - $event.stopPropagation(); - } - toastr.remove(scope.toastId, wasClicked); - }; - - scope.refreshTimer = function(newTime) { - if (timeout) { - $interval.cancel(timeout); - timeout = createTimeout(newTime || scope.options.timeOut); - } - }; - - element.on('mouseleave', function() { - if (scope.options.timeOut === 0 && scope.options.extendedTimeOut === 0) { return; } - scope.$apply(function() { - scope.progressBar = scope.options.progressBar; + encodedKey = encodeURIComponent(k); + if (isArray(obj[k])) { + each(obj[k], function(val) { + pieces.push(encodedKey + '[]=' + encodeURIComponent(val)); }); - timeout = createTimeout(scope.options.extendedTimeOut); - }); - - function createTimeout(time) { - toastCtrl.startProgressBar(time); - return $interval(function() { - toastCtrl.stopProgressBar(); - toastr.remove(scope.toastId); - }, time, 1); + continue; } + pieces.push(encodedKey + '=' + encodeURIComponent(obj[k])); + } + return pieces.length ? ('?' + pieces.join('&')) : ''; + }; - function hideAndStopProgressBar() { - scope.progressBar = false; - toastCtrl.stopProgressBar(); - } + // for now we will only support string and arrays + var parseKey = function(key) { + var pos = key.indexOf('['); + if (pos === -1) return { type: 'string', val: key }; + return { type: 'array', val: key.substr(0, pos) }; + }; - function wantsCloseButton() { - return scope.options.closeHtml; + var isArray = function(val) { + return Object.prototype.toString.call(val) === '[object Array]'; + }; + + var extract = function(url) { + var pos = url.lastIndexOf('?'); + var hasQuery = pos !== -1; + var base = void 0; + + if (hasQuery && pos > 0) { + base = url.substring(0, pos); + } else if (!hasQuery && (url && url.length > 0)) { + base = url; + } + + return { + base: base, + query: hasQuery ? url.substring(pos+1) : void 0 + }; + }; + + // thanks raynos! + // https://github.com/Raynos/xtend + var extend = function() { + var target = {}; + for (var i = 0; i < arguments.length; i++) { + var source = arguments[i]; + for (var key in source) { + if (source.hasOwnProperty(key)) { + target[key] = source[key]; + } } } - } -}()); + return target; + }; -angular.module("toastr").run(["$templateCache", function($templateCache) {$templateCache.put("directives/progressbar/progressbar.html","
\n"); -$templateCache.put("directives/toast/toast.html","
\n
\n
{{title}}
\n
{{message}}
\n
\n
\n
\n \n
\n");}]); + var queryExtend = function() { + var args = Array.prototype.slice.call(arguments, 0); + var asObject = args[args.length-1] === true; + var base = ''; + + if (!args.length) { + return base; + } + + if (asObject) { + args.pop(); + } + + var normalized = map(args, function(param) { + if (typeof param === 'string') { + var extracted = extract(param); + if (extracted.base) base = extracted.base; + return queryToObject(extracted.query); + } + return param; + }); + + if (asObject) { + return extend.apply({}, normalized); + } else { + return base + objectToQuery(extend.apply({}, normalized)); + } + + }; + + var each = function(arr, fn) { + for (var i = 0, l = arr.length; i < l; i++) { + fn(arr[i], i); + } + }; + + var map = function(arr, fn) { + var res = []; + for (var i = 0, l = arr.length; i < l; i++) { + res.push( fn(arr[i], i) ); + } + return res; + }; + + if (typeof module !== 'undefined' && module.exports) { + // Node.js / browserify + module.exports = queryExtend; + } else if (typeof define === 'function' && define.amd) { + // require.js / AMD + define(function() { + return queryExtend; + }); + } else { + // + diff --git a/version.json b/version.json index cf9b178a..a5ec78e6 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "1.25.0" + "version": "1.26.0" } \ No newline at end of file