'use strict'; /* global angular: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('&')); } // 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('/branding', { controller: 'BrandingController', templateUrl: 'views/branding.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('/services', { controller: 'ServicesController', templateUrl: 'views/services.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 ''; } }; }); // 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; }; }); // see apps.js $scope.states app.filter('selectedStateFilter', function () { return function selectedStateFilter(apps, selectedState) { return apps.filter(function (app) { if (!selectedState || !selectedState.state) return true; if (selectedState.state === 'running') return app.runState === 'running' && app.health === 'healthy' && app.installationState === 'installed'; if (selectedState.state === 'stopped') return app.runState === 'stopped'; return app.runState === 'running' && (app.health !== 'healthy' || app.installationState !== 'installed'); // not responding }); }; }); 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; appSearch = appSearch.toLowerCase(); return app.fqdn.indexOf(appSearch) !== -1 || (app.label && app.label.toLowerCase().indexOf(appSearch) !== -1) || (app.manifest.title && app.manifest.title.toLowerCase().indexOf(appSearch) !== -1); }); }; }); app.filter('prettyDomains', function () { return function prettyDomains(domains) { return domains.map(function (d) { return d.domain; }).join(', '); }; }); 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) { 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 || addresses === '<>') return '<>'; if (Array.isArray(addresses)) return addresses.map(function (a) { return a.slice(1, -1); }).join(', '); return addresses.slice(1, -1); }; }); // 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' }, require: '^form', link: function ($scope, element, attrs, formCtrl) { $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 = $scope.tagArray(); // prevent adding empty or existing items if ($scope.tagText.length === 0 || tagArray.indexOf($scope.tagText) !== -1) { return $scope.tagText = ''; } 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); } } formCtrl.$setDirty(); 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('click', function () { element[0].firstChild.lastChild.focus(); }); 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) { e.preventDefault(); return $scope.$apply('addTag()'); } }); }, template: '