'use strict'; /* global $, async, angular, redirectIfNeeded */ /* global ERROR,ISTATES,HSTATES,RSTATES,APP_TYPES,NOTIFICATION_TYPES */ // 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', ['pascalprecht.translate', 'ngCookies', 'ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', '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?' + window.VITE_CACHE_ID }).when('/user-directory', { // controller: 'UserSettingsController', // templateUrl: 'views/user-directory.html?' + window.VITE_CACHE_ID }).when('/app/:appId/:view?', { controller: 'AppController', templateUrl: 'views/app.html?' + window.VITE_CACHE_ID }).when('/appstore', { // controller: 'AppStoreController', // templateUrl: 'views/appstore.html?' + window.VITE_CACHE_ID }).when('/appstore/:appId', { // controller: 'AppStoreController', // templateUrl: 'views/appstore.html?' + window.VITE_CACHE_ID }).when('/apps', { // controller: 'AppsController', // templateUrl: 'views/apps.html?' + window.VITE_CACHE_ID }).when('/profile', { // controller: 'ProfileController', // templateUrl: 'views/profile.html?' + window.VITE_CACHE_ID }).when('/backups', { // controller: 'BackupsController', // templateUrl: 'views/backups.html?' + window.VITE_CACHE_ID }).when('/branding', { // controller: 'BrandingController', // templateUrl: 'views/branding.html?' + window.VITE_CACHE_ID }).when('/network', { // controller: 'NetworkController', // templateUrl: 'views/network.html?' + window.VITE_CACHE_ID }).when('/domains', { // controller: 'DomainsController', // templateUrl: 'views/domains.html?' + window.VITE_CACHE_ID }).when('/email', { controller: 'EmailsController', templateUrl: 'views/emails.html?' + window.VITE_CACHE_ID }).when('/emails-eventlog', { controller: 'EmailsEventlogController', templateUrl: 'views/emails-eventlog.html?' + window.VITE_CACHE_ID }).when('/emails-queue', { controller: 'EmailsQueueController', templateUrl: 'views/emails-queue.html?' + window.VITE_CACHE_ID }).when('/email/:domain/:view?', { controller: 'EmailController', templateUrl: 'views/email.html?' + window.VITE_CACHE_ID }).when('/notifications', { controller: 'NotificationsController', templateUrl: 'views/notifications.html?' + window.VITE_CACHE_ID }).when('/oidc', { redirectTo: '/user-directory' }).when('/settings', { // controller: 'SettingsController', // templateUrl: 'views/settings.html?' + window.VITE_CACHE_ID }).when('/eventlog', { // controller: 'EventLogController', // templateUrl: 'views/eventlog.html?' + window.VITE_CACHE_ID }).when('/support', { // controller: 'SupportController', // templateUrl: 'views/support.html?' + window.VITE_CACHE_ID }).when('/system', { controller: 'SystemController', templateUrl: 'views/system.html?' + window.VITE_CACHE_ID }).when('/services', { // controller: 'ServicesController', // templateUrl: 'views/services.html?' + window.VITE_CACHE_ID }).when('/volumes', { // controller: 'VolumesController', // templateUrl: 'views/volumes.html?' + window.VITE_CACHE_ID }).otherwise({ redirectTo: '/'}); }]); app.filter('notificationTypeToColor', function () { return function (n) { switch (n.type) { case NOTIFICATION_TYPES.REBOOT: case NOTIFICATION_TYPES.APP_OOM: case NOTIFICATION_TYPES.MAIL_STATUS: case NOTIFICATION_TYPES.CERTIFICATE_RENEWAL_FAILED: case NOTIFICATION_TYPES.DISK_SPACE: case NOTIFICATION_TYPES.BACKUP_CONFIG: case NOTIFICATION_TYPES.BACKUP_FAILED: return '#ff4c4c'; case NOTIFICATION_TYPES.BOX_UPDATE: case NOTIFICATION_TYPES.MANUAL_APP_UPDATE: return '#f0ad4e'; default: return '#2196f3'; } }; }); app.filter('capitalize', function () { return function (s) { return s.charAt(0).toUpperCase() + s.slice(1); }; }); 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 ''; // app links have http already in the fqdn if (app.fqdn.indexOf('http') !== 0) return 'https://' + app.fqdn; return app.fqdn; }; }); app.filter('userManagementFilter', function () { return function(apps, option) { return apps.filter(function (app) { if (option.id === '') return true; if (option.id === 'sso') return !!(app.manifest.optionalSso || app.manifest.addons.ldap || app.manifest.addons.proxyAuth); if (option.id === 'nosso') return app.manifest.optionalSso || (!app.manifest.addons.ldap && !app.manifest.addons.proxyAuth); if (option.id === 'email') return !!app.manifest.addons.email; return false; }); }; }); // 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', ['Client', function (Client) { return function selectedStateFilter(apps, selectedState) { return apps.filter(function (app) { if (!selectedState || !selectedState.state) return true; if (selectedState.state === 'running') return app.runState === RSTATES.RUNNING && app.health === HSTATES.HEALTHY && app.installationState === ISTATES.INSTALLED; if (selectedState.state === 'stopped') return app.runState === RSTATES.STOPPED; if (selectedState.state === 'update_available') return !!(Client.getConfig().update[app.id] && Client.getConfig().update[app.id].manifest.version && Client.getConfig().update[app.id].manifest.version !== app.manifest.version); return app.runState === RSTATES.RUNNING && (app.health !== HSTATES.HEALTHY || app.installationState !== ISTATES.INSTALLED); // not responding }); }; }]); app.filter('selectedGroupAccessFilter', function () { return function selectedGroupAccessFilter(apps, group) { return apps.filter(function (app) { if (!group.id) return true; // case for no filter entry if (!app.accessRestriction) return true; if (!app.accessRestriction.groups) return false; if (app.accessRestriction.groups.indexOf(group.id) !== -1) return true; return false; }); }; }); 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._alldomains) return true; // magic domain for single select, see apps.js ALL_DOMAINS_DOMAIN if (app.type === APP_TYPES.LINK) return false; if (selectedDomain.domain === app.domain) return true; if (app.aliasDomains.find(function (ad) { return ad.domain === selectedDomain.domain; })) return true; if (app.redirectDomains.find(function (ad) { return ad.domain === selectedDomain.domain; })) return true; return false; }); }; }); 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) || (appSearch.length >=6 && app.id.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; }; }); // color indicator in app list app.filter('installationStateClass', function () { const ERROR_CLASS = 'status-error'; const BUSY_CLASS = 'status-starting fa-beat-fade'; const INACTIVE_CLASS = 'status-inactive'; const ACTIVE_CLASS = 'status-active'; return function(app) { if (!app) return ''; switch (app.installationState) { case ISTATES.ERROR: return ERROR_CLASS; case ISTATES.INSTALLED: { if (app.debugMode) { return INACTIVE_CLASS; } else { if (app.runState === RSTATES.RUNNING) { if (!app.health) return BUSY_CLASS; // no data yet if (app.type === APP_TYPES.LINK || app.health === HSTATES.HEALTHY) return ACTIVE_CLASS; return ERROR_CLASS; // dead/exit/unhealthy } else { return INACTIVE_CLASS; } } } default: return BUSY_CLASS; } }; }); // 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_SERVICES_CHANGE: 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_IMPORT: return 'Importing' + 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 === RSTATES.RUNNING) { if (!app.health) return 'Starting...'; // no data yet if (app.type === APP_TYPES.LINK) return ''; if (app.health === HSTATES.HEALTHY) return 'Running'; return 'Not responding'; // dead/exit/unhealthy } else if (app.runState === RSTATES.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_IMPORT: return 'import'; 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('canUpdate', 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('prettyEmailAddresses', function () { return function prettyEmailAddresses(addresses) { if (!addresses) return ''; if (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); }); } }; }); // handles various states and triggers a href or configure/repair view // used by attaching to controller $scope // if $scope.appPostInstallConfirm.show(app); exists it will be called if not yet confirmed function onAppClick(app, $event, isOperator, $scope) { function stopEvent() { $event.originalEvent.stopPropagation(); $event.originalEvent.preventDefault(); } if (app.installationState !== ISTATES.INSTALLED) { if (app.installationState === ISTATES.ERROR && isOperator) $scope.showAppConfigure(app, 'repair'); return stopEvent(); } // app.health can also be null to indicate insufficient data if (!app.health) return stopEvent(); if (app.runState === RSTATES.STOPPED) return stopEvent(); if (app.runState === RSTATES.STOPPED) return stopEvent(); if (app.health === HSTATES.UNHEALTHY || app.health === HSTATES.ERROR || app.health === HSTATES.DEAD) { if (isOperator) $scope.showAppConfigure(app, 'repair'); return stopEvent(); } if (app.pendingPostInstallConfirmation && $scope.appPostInstallConfirm) { $scope.appPostInstallConfirm.show(app); return stopEvent(); } } 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: '