'use strict'; /* global angular:false */ /* global showdown:false */ /* global moment:false */ /* global $:false */ /* global ERROR,ISTATES,HSTATES,RSTATES */ // deal with accessToken in the query, this is passed for example on password reset and account setup upon invite var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {}); if (search.accessToken) { localStorage.token = search.accessToken; // strip the accessToken and expiresAt, then preserve the rest delete search.accessToken; delete search.expiresAt; // this will reload the page as this is not a hash change window.location.search = encodeURIComponent(Object.keys(search).map(function (key) { return key + '=' + search[key]; }).join('&')); } // poor man's async in the global namespace function asyncForEachParallel(items, handler, callback) { var alreadyDone = 0; var errored = false; if (items.length === 0) return callback(); function done(error) { // do nothing if already called back due to error if (errored) return; if (error) { errored = true; return callback(error); } ++alreadyDone; // we are done if (alreadyDone === items.length) callback(); } for (var i = 0; i < items.length; ++i) { handler(items[i], done); } } function asyncForEach(items, handler, callback) { var cur = 0; if (items.length === 0) return callback(); (function iterator() { handler(items[cur], function (error) { if (error) return callback(error); if (cur >= items.length-1) return callback(); ++cur; iterator(); }); })(); } function asyncSeries(funcs, callback) { var cur = 0; if (funcs.length === 0) return callback(); (function iterator() { funcs[cur](function (error) { if (error) return callback(error); if (cur >= funcs.length-1) return callback(); ++cur; iterator(); }); })(); } // create main application module var app = angular.module('Application', ['ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld', 'ui.multiselect']); app.config(['NotificationProvider', function (NotificationProvider) { NotificationProvider.setOptions({ delay: 5000, startTop: 60, positionX: 'left', templateUrl: 'notification.html' }); }]); // configure resourceUrlWhitelist https://code.angularjs.org/1.5.8/docs/api/ng/provider/$sceDelegateProvider#resourceUrlWhitelist app.config(function ($sceDelegateProvider) { $sceDelegateProvider.resourceUrlWhitelist([ // Allow same origin resource loads. 'self', // Allow loading from our assets domain. 'https://cloudron.io/**', 'https://staging.cloudron.io/**', 'https://dev.cloudron.io/**', // Allow local development against the appstore pages 'http://localhost:5000/**' ]); }); // setup all major application routes app.config(['$routeProvider', function ($routeProvider) { $routeProvider.when('/', { redirectTo: '/apps' }).when('/users', { controller: 'UsersController', templateUrl: 'views/users.html?<%= revision %>' }).when('/app/:appId/:view?', { controller: 'AppController', templateUrl: 'views/app.html?<%= revision %>' }).when('/appstore', { controller: 'AppStoreController', templateUrl: 'views/appstore.html?<%= revision %>' }).when('/appstore/:appId', { controller: 'AppStoreController', templateUrl: 'views/appstore.html?<%= revision %>' }).when('/apps', { controller: 'AppsController', templateUrl: 'views/apps.html?<%= revision %>' }).when('/profile', { controller: 'ProfileController', templateUrl: 'views/profile.html?<%= revision %>' }).when('/backups', { controller: 'BackupsController', templateUrl: 'views/backups.html?<%= revision %>' }).when('/graphs', { controller: 'GraphsController', templateUrl: 'views/graphs.html?<%= revision %>' }).when('/network', { controller: 'NetworkController', templateUrl: 'views/network.html?<%= revision %>' }).when('/domains', { controller: 'DomainsController', templateUrl: 'views/domains.html?<%= revision %>' }).when('/email', { controller: 'EmailsController', templateUrl: 'views/emails.html?<%= revision %>' }).when('/email/:domain', { controller: 'EmailController', templateUrl: 'views/email.html?<%= revision %>' }).when('/notifications', { controller: 'NotificationsController', templateUrl: 'views/notifications.html?<%= revision %>' }).when('/settings', { controller: 'SettingsController', templateUrl: 'views/settings.html?<%= revision %>' }).when('/activity', { controller: 'ActivityController', templateUrl: 'views/activity.html?<%= revision %>' }).when('/support', { controller: 'SupportController', templateUrl: 'views/support.html?<%= revision %>' }).when('/system', { controller: 'SystemController', templateUrl: 'views/system.html?<%= revision %>' }).when('/tokens', { controller: 'TokensController', templateUrl: 'views/tokens.html?<%= revision %>' }).otherwise({ redirectTo: '/'}); }]); app.filter('installError', function () { return function (app) { if (!app) return false; if (app.installationState === ISTATES.ERROR) return true; if (app.installationState === ISTATES.INSTALLED) { // app.health can also be null to indicate insufficient data if (app.health === HSTATES.UNHEALTHY || app.health === HSTATES.ERROR || app.health === HSTATES.DEAD) return true; } return false; }; }); app.filter('activeTask', function () { return function (app) { if (!app) return false; return app.taskId !== null; }; }); app.filter('installSuccess', function () { return function (app) { if (!app) return false; return app.installationState === ISTATES.INSTALLED; }; }); app.filter('appIsInstalledAndHealthy', function () { return function (app) { if (!app) return false; return (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && app.runState === RSTATES.RUNNING); }; }); app.filter('applicationLink', function() { return function(app) { if (!app) return ''; if (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && app.runState === RSTATES.RUNNING && !app.pendingPostInstallConfirmation) { return 'https://' + app.fqdn; } else { return ''; } }; }); app.filter('activeOAuthClients', function () { return function (clients, user) { return clients.filter(function (c) { return user.admin || (c.activeTokens && c.activeTokens.length > 0); }); }; }); // this appears when an item in app grid is clicked app.filter('prettyAppErrorMessage', function () { return function (app) { if (!app) return ''; if (app.installationState === ISTATES.INSTALLED) { // app.health can also be null to indicate insufficient data if (app.health === HSTATES.UNHEALTHY) return 'The app is not responding to health checks. Check the logs for any error messages.'; } if (app.error.reason === 'Access Denied') { if (app.error.domain) return 'The DNS record for this location is not setup correctly. Please verify your DNS settings and repair this app.'; } else if (app.error.reason === 'Already Exists') { if (app.error.domain) return 'The DNS record for this location already exists. Cloudron does not remove existing DNS records. Manually remove the DNS record and then click on repair.'; } return app.error.message; }; }); // this appears as tool tip in app grid app.filter('appProgressMessage', function () { return function (app) { var message = app.message || (app.error ? app.error.message : ''); return message; }; }); app.filter('selectedTagFilter', function () { return function selectedTagFilter(apps, selectedTags) { return apps.filter(function (app) { if (selectedTags.length === 0) return true; if (!app.tags) return false; for (var i = 0; i < selectedTags.length; i++) { if (app.tags.indexOf(selectedTags[i]) === -1) return false; } return true; }); }; }); app.filter('selectedDomainFilter', function () { return function selectedDomainFilter(apps, selectedDomain) { return apps.filter(function (app) { if (!selectedDomain) return true; if (selectedDomain._alldomains) return true; // magic domain for single select, see apps.js ALL_DOMAINS_DOMAIN if (selectedDomain.domain === app.domain) return true; return !!app.alternateDomains.find(function (ad) { return ad.domain === selectedDomain.domain; }); }); }; }); app.filter('appSearchFilter', function () { return function appSearchFilter(apps, appSearch) { return apps.filter(function (app) { if (!appSearch) return true; return app.fqdn.indexOf(appSearch) !== -1 || (app.label && app.label.indexOf(appSearch) !== -1); }); }; }); app.filter('prettyDomains', function () { return function prettyDomains(domains) { return domains.map(function (d) { return d.domain; }).join(', '); }; }); app.filter('prettyMemory', function () { return function (memory) { // Adjust the default memory limit if it changes return memory ? Math.floor(memory / 1024 / 1024) : 256; }; }); app.filter('prettyDiskSize', function () { return function (size) { if (!size) return 'not available yet'; var i = Math.floor(Math.log(size) / Math.log(1024)); return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; }; }); app.filter('installationActive', function () { return function (app) { if (app.installationState === ISTATES.ERROR) return false; if (app.installationState === ISTATES.INSTALLED) return false; return true; }; }); // this appears in the app grid app.filter('installationStateLabel', function () { return function(app, user) { if (!app) return ''; var waiting = app.progress === 0 ? ' (Queued)' : ''; switch (app.installationState) { case ISTATES.PENDING_INSTALL: return 'Installing' + waiting; case ISTATES.PENDING_CLONE: return 'Cloning' + waiting; case ISTATES.PENDING_LOCATION_CHANGE: case ISTATES.PENDING_CONFIGURE: case ISTATES.PENDING_RECREATE_CONTAINER: case ISTATES.PENDING_DEBUG: return 'Configuring' + waiting; case ISTATES.PENDING_RESIZE: return 'Resizing' + waiting; case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'Migrating data' + waiting; case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting; case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting; case ISTATES.PENDING_UPDATE: return 'Updating' + waiting; case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting; case ISTATES.PENDING_START: return 'Starting' + waiting; case ISTATES.PENDING_STOP: return 'Stopping' + waiting; case ISTATES.PENDING_RESTART: return 'Restarting' + waiting; case ISTATES.ERROR: { if (app.error && app.error.message === 'ETRYAGAIN') return 'DNS Error'; return 'Error'; } case ISTATES.INSTALLED: { if (app.debugMode) { return 'Recovery Mode'; } else if (app.runState === 'running') { if (!app.health) return 'Starting...'; // no data yet if (app.health === HSTATES.HEALTHY) return 'Running'; return 'Not responding'; // dead/exit/unhealthy } else if (app.runState === 'stopped') return 'Stopped'; else return app.runState; } default: return app.installationState; } }; }); app.filter('taskName', function () { return function(installationState) { switch (installationState) { case ISTATES.PENDING_INSTALL: return 'install'; case ISTATES.PENDING_CLONE: return 'clone'; case ISTATES.PENDING_LOCATION_CHANGE: return 'location change'; case ISTATES.PENDING_CONFIGURE: return 'configure'; case ISTATES.PENDING_RECREATE_CONTAINER: return 'create container'; case ISTATES.PENDING_DEBUG: return 'debug'; case ISTATES.PENDING_RESIZE: return 'resize'; case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'data migration'; case ISTATES.PENDING_UNINSTALL: return 'uninstall'; case ISTATES.PENDING_RESTORE: return 'restore'; case ISTATES.PENDING_UPDATE: return 'update'; case ISTATES.PENDING_BACKUP: return 'backup'; case ISTATES.PENDING_START: return 'start app'; case ISTATES.PENDING_STOP: return 'stop app'; case ISTATES.PENDING_RESTART: return 'restart app'; default: return installationState || ''; } }; }); app.filter('errorSuggestion', function () { return function (error) { if (!error) return ''; switch (error.reason) { case ERROR.ACCESS_DENIED: if (error.domain) return 'Check the DNS credentials of ' + error.domain.domain + ' in the Domains & Certs view'; return ''; case ERROR.COLLECTD_ERROR: return 'Check if collectd is running on the server'; case ERROR.DATABASE_ERROR: return 'Check if MySQL database is running on the server'; case ERROR.DOCKER_ERROR: return 'Check if docker is running on the server'; case ERROR.DNS_ERROR: return 'Check if the DNS service of the domain is running'; case ERROR.LOGROTATE_ERROR: return 'Check if logrotate is running on the server'; case ERROR.NETWORK_ERROR: return 'Check if there are any network issues on the server'; case ERROR.REVERSEPROXY_ERROR: return 'Check if nginx is running on the server'; default: return ''; } }; }); app.filter('readyToUpdate', function () { return function (apps) { return apps.every(function (app) { return (app.installationState === ISTATES.ERROR) || (app.installationState === ISTATES.INSTALLED); }); }; }); app.filter('inProgressApps', function () { return function (apps) { return apps.filter(function (app) { return app.installationState !== ISTATES.ERROR && app.installationState !== ISTATES.INSTALLED; }); }; }); app.filter('prettyHref', function () { return function (input) { if (!input) return input; if (input.indexOf('http://') === 0) return input.slice('http://'.length); if (input.indexOf('https://') === 0) return input.slice('https://'.length); return input; }; }); app.filter('prettyDate', function () { // http://ejohn.org/files/pretty.js return function prettyDate(utc) { var date = new Date(utc), // this converts utc into browser timezone and not cloudron timezone! diff = (((new Date()).getTime() - date.getTime()) / 1000) + 30, // add 30seconds for clock skew day_diff = Math.floor(diff / 86400); if (isNaN(day_diff) || day_diff < 0) return 'just now'; return day_diff === 0 && ( diff < 60 && 'just now' || diff < 120 && '1 minute ago' || diff < 3600 && Math.floor( diff / 60 ) + ' minutes ago' || diff < 7200 && '1 hour ago' || diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') || day_diff === 1 && 'Yesterday' || day_diff < 7 && day_diff + ' days ago' || day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' || day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' || Math.round( day_diff / 365 ) + ' years ago'; }; }); app.filter('prettyLongDate', function () { return function prettyLongDate(utc) { return moment(utc).format('MMMM Do YYYY, h:mm:ss a'); // this converts utc into browser timezone and not cloudron timezone! }; }); app.filter('prettyShortDate', function () { return function prettyShortDate(utc) { return moment(utc).format('MMMM Do YYYY'); // this converts utc into browser timezone and not cloudron timezone! }; }); app.filter('prettyEmailAddresses', function () { return function prettyEmailAddresses(addresses) { if (!addresses) return ''; if (Array.isArray(addresses)) return addresses.map(function (a) { return a.slice(1, -1); }).join(', '); return addresses.slice(1, -1); }; }); app.filter('markdown2html', function () { var converter = new showdown.Converter({ extensions: [], simplifiedAutoLink: true, strikethrough: true, tables: true }); return function (text) { return converter.makeHtml(text); }; }); app.filter('postInstallMessage', function () { var SSO_MARKER = '=== sso ==='; return function (text, app) { if (!text) return ''; if (!app) return text; var parts = text.split(SSO_MARKER); if (parts.length === 1) { // [^] matches even newlines. '?' makes it non-greedy if (app.sso) return text.replace(/\[^]*?\<\/nosso\>/g, ''); else return text.replace(/\[^]*?\<\/sso\>/g, ''); } if (app.sso) return parts[1]; else return parts[0]; }; }); // custom directive for dynamic names in forms // See http://stackoverflow.com/questions/23616578/issue-registering-form-control-with-interpolated-name#answer-23617401 app.directive('laterName', function () { // (2) return { restrict: 'A', require: ['?ngModel', '^?form'], // (3) link: function postLink(scope, elem, attrs, ctrls) { attrs.$set('name', attrs.laterName); var modelCtrl = ctrls[0]; // (3) var formCtrl = ctrls[1]; // (3) if (modelCtrl && formCtrl) { modelCtrl.$name = attrs.name; // (4) formCtrl.$addControl(modelCtrl); // (2) scope.$on('$destroy', function () { formCtrl.$removeControl(modelCtrl); // (5) }); } } }; }); app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) { var original = $location.path; $location.path = function (path, reload) { if (reload === false) { var lastRoute = $route.current; var un = $rootScope.$on('$locationChangeSuccess', function () { $route.current = lastRoute; un(); }); } return original.apply($location, [path]); }; }]); app.directive('ngClickSelect', function () { return { restrict: 'AC', link: function (scope, element/*, attrs */) { element.bind('click', function () { var selection = window.getSelection(); var range = document.createRange(); range.selectNodeContents(this); selection.removeAllRanges(); selection.addRange(range); }); } }; }); app.directive('ngClickReveal', function () { return { restrict: 'A', link: function (scope, element, attrs) { element.addClass('hand'); var value = ''; scope.$watch(attrs.ngClickReveal, function (newValue, oldValue) { if (newValue !== oldValue) { element.html('hidden'); value = newValue; } }); element.bind('click', function () { element.text(value); }); } }; }); // https://codepen.io/webmatze/pen/isuHh app.directive('tagInput', function () { return { restrict: 'E', scope: { inputTags: '=taglist' }, link: function ($scope, element, attrs) { $scope.defaultWidth = 200; $scope.tagText = ''; // current tag being edited $scope.placeholder = attrs.placeholder; $scope.tagArray = function () { if ($scope.inputTags === undefined) { return []; } return $scope.inputTags.split(',').filter(function (tag) { return tag !== ''; }); }; $scope.addTag = function () { var tagArray; if ($scope.tagText.length === 0) { return; } tagArray = $scope.tagArray(); tagArray.push($scope.tagText); $scope.inputTags = tagArray.join(','); return $scope.tagText = ''; }; $scope.deleteTag = function (key) { var tagArray; tagArray = $scope.tagArray(); if (tagArray.length > 0 && $scope.tagText.length === 0 && key === undefined) { tagArray.pop(); } else { if (key !== undefined) { tagArray.splice(key, 1); } } return $scope.inputTags = tagArray.join(','); }; $scope.$watch('tagText', function (newVal, oldVal) { var tempEl; if (!(newVal === oldVal && newVal === undefined)) { tempEl = $('' + newVal + '').appendTo('body'); $scope.inputWidth = tempEl.width() + 5; if ($scope.inputWidth < $scope.defaultWidth) { $scope.inputWidth = $scope.defaultWidth; } return tempEl.remove(); } }); element.bind('keydown', function (e) { var key = e.which; if (key === 9 || key === 13) { e.preventDefault(); } if (key === 8) { return $scope.$apply('deleteTag()'); } }); element.bind('keyup', function (e) { var key = e.which; if (key === 9 || key === 13 || key === 32 || key === 188) { e.preventDefault(); return $scope.$apply('addTag()'); } }); }, template: '
' + '
' + '{{tag}}' + '
×
' + '
' + '' + '
' }; }); app.config(['fitTextConfigProvider', function (fitTextConfigProvider) { fitTextConfigProvider.config = { loadDelay: 250, compressor: 0.9, min: 8, max: 24 }; }]);