mirror of
https://github.com/bluxmit/alnoda-workspaces.git
synced 2024-09-20 03:37:11 +12:00
1146 lines
34 KiB
JavaScript
Executable file
1146 lines
34 KiB
JavaScript
Executable file
// Cronicle Web App
|
|
// Author: Joseph Huckaby
|
|
// Copyright (c) 2015 Joseph Huckaby and PixlCore.com
|
|
|
|
if (!window.app) throw new Error("App Framework is not present.");
|
|
|
|
app.extend({
|
|
|
|
name: '',
|
|
preload_images: ['loading.gif'],
|
|
activeJobs: {},
|
|
eventQueue: {},
|
|
state: null,
|
|
plain_text_post: true,
|
|
clock_visible: false,
|
|
scroll_time_visible: false,
|
|
default_prefs: {
|
|
schedule_group_by: 'category'
|
|
},
|
|
|
|
receiveConfig: function(resp) {
|
|
// receive config from server
|
|
if (resp.code) {
|
|
app.showProgress( 1.0, "Waiting for master server..." );
|
|
setTimeout( function() { load_script( '/api/app/config?callback=app.receiveConfig' ); }, 1000 );
|
|
return;
|
|
}
|
|
delete resp.code;
|
|
window.config = resp.config;
|
|
|
|
for (var key in resp) {
|
|
this[key] = resp[key];
|
|
}
|
|
|
|
// allow visible app name to be changed in config
|
|
this.name = config.name;
|
|
$('#d_header_title').html( '<b>' + this.name + '</b>' );
|
|
|
|
// hit the master server directly from now on
|
|
this.setMasterHostname( resp.master_hostname );
|
|
|
|
this.config.Page = [
|
|
{ ID: 'Home' },
|
|
{ ID: 'Login' },
|
|
{ ID: 'Schedule' },
|
|
{ ID: 'History' },
|
|
{ ID: 'JobDetails' },
|
|
{ ID: 'MyAccount' },
|
|
{ ID: 'Admin' }
|
|
];
|
|
this.config.DefaultPage = 'Home';
|
|
|
|
// did we try to init and fail? if so, try again now
|
|
if (this.initReady) {
|
|
this.hideProgress();
|
|
delete this.initReady;
|
|
this.init();
|
|
}
|
|
},
|
|
|
|
init: function() {
|
|
// initialize application
|
|
if (this.abort) return; // fatal error, do not initialize app
|
|
|
|
if (!this.config) {
|
|
// must be in master server wait loop
|
|
this.initReady = true;
|
|
return;
|
|
}
|
|
|
|
if (!this.servers) this.servers = {};
|
|
this.server_groups = [];
|
|
|
|
// timezone support
|
|
this.tz = jstz.determine().name();
|
|
this.zones = moment.tz.names();
|
|
|
|
// preload a few essential images
|
|
for (var idx = 0, len = this.preload_images.length; idx < len; idx++) {
|
|
var filename = '' + this.preload_images[idx];
|
|
var img = new Image();
|
|
img.src = '/images/'+filename;
|
|
}
|
|
|
|
// populate prefs for first time user
|
|
for (var key in this.default_prefs) {
|
|
if (!(key in window.localStorage)) {
|
|
window.localStorage[key] = this.default_prefs[key];
|
|
}
|
|
}
|
|
|
|
// pop version into footer
|
|
$('#d_footer_version').html( "Version " + this.version || 0 );
|
|
|
|
// some css classing for browser-specific adjustments
|
|
var ua = navigator.userAgent;
|
|
if (ua.match(/Safari/) && !ua.match(/(Chrome|Opera)/)) {
|
|
$('body').addClass('safari');
|
|
}
|
|
else if (ua.match(/Chrome/)) {
|
|
$('body').addClass('chrome');
|
|
}
|
|
else if (ua.match(/Firefox/)) {
|
|
$('body').addClass('firefox');
|
|
}
|
|
|
|
// follow scroll so we can fade in/out the scroll time widget
|
|
window.addEventListener( "scroll", function() {
|
|
app.checkScrollTime();
|
|
}, false );
|
|
app.checkScrollTime();
|
|
|
|
this.page_manager = new PageManager( always_array(config.Page) );
|
|
|
|
// this.setHeaderClock();
|
|
this.socketConnect();
|
|
|
|
// Nav.init();
|
|
},
|
|
|
|
updateHeaderInfo: function() {
|
|
// update top-right display
|
|
var html = '';
|
|
html += '<div id="d_header_divider" class="right" style="margin-right:0;"></div>';
|
|
html += '<div class="header_option logout right" onMouseUp="app.doUserLogout()"><i class="fa fa-power-off fa-lg"> </i>Logout</div>';
|
|
html += '<div id="d_header_divider" class="right"></div>';
|
|
html += '<div id="d_header_user_bar" class="right" style="background-image:url(' + this.getUserAvatarURL( this.retina ? 64 : 32 ) + ')" onMouseUp="app.doMyAccount()">' + (this.user.full_name || app.username).replace(/\s+.+$/, '') + '</div>';
|
|
$('#d_header_user_container').html( html );
|
|
},
|
|
|
|
doUserLogin: function(resp) {
|
|
// user login, called from login page, or session recover
|
|
// overriding this from base.js, so we can pass the session ID to the websocket
|
|
delete resp.code;
|
|
|
|
for (var key in resp) {
|
|
this[key] = resp[key];
|
|
}
|
|
|
|
if (this.isCategoryLimited() || this.isGroupLimited()) {
|
|
this.pruneSchedule();
|
|
this.pruneCategories();
|
|
this.pruneActiveJobs();
|
|
}
|
|
|
|
this.setPref('username', resp.username);
|
|
this.setPref('session_id', resp.session_id);
|
|
|
|
this.updateHeaderInfo();
|
|
|
|
// update clock
|
|
this.setHeaderClock( this.epoch );
|
|
|
|
// show scheduler master switch
|
|
this.updateMasterSwitch();
|
|
if (this.hasPrivilege('state_update')) $('#d_tab_master').addClass('active');
|
|
|
|
// show admin tab if user is worthy
|
|
if (this.isAdmin()) $('#tab_Admin').show();
|
|
else $('#tab_Admin').hide();
|
|
|
|
// authenticate websocket
|
|
this.socket.emit( 'authenticate', { token: resp.session_id } );
|
|
},
|
|
|
|
doUserLogout: function(bad_cookie) {
|
|
// log user out and redirect to login screen
|
|
var self = this;
|
|
|
|
if (!bad_cookie) {
|
|
// user explicitly logging out
|
|
this.showProgress(1.0, "Logging out...");
|
|
this.setPref('username', '');
|
|
}
|
|
|
|
this.api.post( 'user/logout', {
|
|
session_id: this.getPref('session_id')
|
|
},
|
|
function(resp, tx) {
|
|
delete self.user;
|
|
delete self.username;
|
|
delete self.user_info;
|
|
|
|
if (self.socket) self.socket.emit( 'logout', {} );
|
|
|
|
self.setPref('session_id', '');
|
|
|
|
$('#d_header_user_container').html( '' );
|
|
$('#d_tab_master').html( '' );
|
|
|
|
$('div.header_clock_layer').fadeTo( 1000, 0 );
|
|
$('#d_tab_time > span').html( '' );
|
|
self.clock_visible = false;
|
|
self.checkScrollTime();
|
|
|
|
if (app.config.external_users) {
|
|
// external user api
|
|
Debug.trace("User session cookie was deleted, querying external user API");
|
|
setTimeout( function() {
|
|
if (bad_cookie) app.doExternalLogin();
|
|
else app.doExternalLogout();
|
|
}, 250 );
|
|
}
|
|
else {
|
|
Debug.trace("User session cookie was deleted, redirecting to login page");
|
|
self.hideProgress();
|
|
Nav.go('Login');
|
|
}
|
|
|
|
setTimeout( function() {
|
|
if (!app.config.external_users) {
|
|
if (bad_cookie) self.showMessage('error', "Your session has expired. Please log in again.");
|
|
else self.showMessage('success', "You were logged out successfully.");
|
|
}
|
|
|
|
self.activeJobs = {};
|
|
delete self.servers;
|
|
delete self.schedule;
|
|
delete self.categories;
|
|
delete self.plugins;
|
|
delete self.server_groups;
|
|
delete self.epoch;
|
|
|
|
}, 150 );
|
|
|
|
$('#tab_Admin').hide();
|
|
} );
|
|
},
|
|
|
|
doExternalLogin: function() {
|
|
// login using external user management system
|
|
// Force API to hit current page hostname vs. master server, so login redirect URL reflects it
|
|
app.api.post( '/api/user/external_login', { cookie: document.cookie }, function(resp) {
|
|
if (resp.user) {
|
|
Debug.trace("User Session Resume: " + resp.username + ": " + resp.session_id);
|
|
app.hideProgress();
|
|
app.doUserLogin( resp );
|
|
Nav.refresh();
|
|
}
|
|
else if (resp.location) {
|
|
Debug.trace("External User API requires redirect");
|
|
app.showProgress(1.0, "Logging in...");
|
|
setTimeout( function() { window.location = resp.location; }, 250 );
|
|
}
|
|
else app.doError(resp.description || "Unknown login error.");
|
|
} );
|
|
},
|
|
|
|
doExternalLogout: function() {
|
|
// redirect to external user management system for logout
|
|
var url = app.config.external_user_api;
|
|
url += (url.match(/\?/) ? '&' : '?') + 'logout=1';
|
|
|
|
Debug.trace("External User API requires redirect");
|
|
app.showProgress(1.0, "Logging out...");
|
|
setTimeout( function() { window.location = url; }, 250 );
|
|
},
|
|
|
|
socketConnect: function() {
|
|
// init socket.io client
|
|
var self = this;
|
|
|
|
var url = this.proto + this.masterHostname + ':' + this.port;
|
|
if (!config.web_socket_use_hostnames && this.servers && this.servers[this.masterHostname] && this.servers[this.masterHostname].ip) {
|
|
// use ip instead of hostname if available
|
|
url = this.proto + this.servers[this.masterHostname].ip + ':' + this.port;
|
|
}
|
|
if (!config.web_direct_connect) {
|
|
url = this.proto + location.host;
|
|
}
|
|
Debug.trace("Websocket Connect: " + url);
|
|
|
|
if (this.socket) {
|
|
Debug.trace("Destroying previous socket");
|
|
this.socket.removeAllListeners();
|
|
if (this.socket.connected) this.socket.disconnect();
|
|
this.socket = null;
|
|
}
|
|
|
|
var socket = this.socket = io( url, {
|
|
// forceNew: true,
|
|
transports: config.socket_io_transports || ['websocket'],
|
|
reconnection: false,
|
|
reconnectionDelay: 1000,
|
|
reconnectionDelayMax: 2000,
|
|
reconnectionAttempts: 9999,
|
|
timeout: 3000
|
|
} );
|
|
|
|
socket.on('connect', function() {
|
|
if (!Nav.inited) Nav.init();
|
|
|
|
Debug.trace("socket.io connected successfully");
|
|
// if (self.progress) self.hideProgress();
|
|
|
|
// if we are already logged in, authenticate websocket now
|
|
var session_id = app.getPref('session_id');
|
|
if (session_id) socket.emit( 'authenticate', { token: session_id } );
|
|
} );
|
|
|
|
socket.on('connect_error', function(err) {
|
|
Debug.trace("socket.io connect error: " + err);
|
|
} );
|
|
|
|
socket.on('connect_timeout', function(err) {
|
|
Debug.trace("socket.io connect timeout");
|
|
} );
|
|
|
|
socket.on('reconnecting', function() {
|
|
Debug.trace("socket.io reconnecting...");
|
|
// self.showProgress( 0.5, "Reconnecting to server..." );
|
|
} );
|
|
|
|
socket.on('reconnect', function() {
|
|
Debug.trace("socket.io reconnected successfully");
|
|
// if (self.progress) self.hideProgress();
|
|
} );
|
|
|
|
socket.on('reconnect_failed', function() {
|
|
Debug.trace("socket.io has given up -- we must refresh");
|
|
location.reload();
|
|
} );
|
|
|
|
socket.on('disconnect', function() {
|
|
// unexpected disconnection
|
|
Debug.trace("socket.io disconnected unexpectedly");
|
|
} );
|
|
|
|
socket.on('status', function(data) {
|
|
if (!data.master) {
|
|
// OMG we're not talking to master anymore?
|
|
self.recalculateMaster(data);
|
|
}
|
|
else {
|
|
// connected to master
|
|
self.epoch = data.epoch;
|
|
self.servers = data.servers;
|
|
self.setHeaderClock( data.epoch );
|
|
|
|
// update active jobs
|
|
self.updateActiveJobs( data );
|
|
|
|
// notify current page
|
|
var id = self.page_manager.current_page_id;
|
|
var page = self.page_manager.find(id);
|
|
if (page && page.onStatusUpdate) page.onStatusUpdate(data);
|
|
|
|
// remove dialog if present
|
|
if (self.waitingForMaster && self.progress) {
|
|
self.hideProgress();
|
|
delete self.waitingForMaster;
|
|
}
|
|
} // master
|
|
} );
|
|
|
|
socket.on('update', function(data) {
|
|
// receive data update (global list contents)
|
|
var limited_user = self.isCategoryLimited() || self.isGroupLimited();
|
|
|
|
for (var key in data) {
|
|
self[key] = data[key];
|
|
|
|
if (limited_user) {
|
|
if (key == 'schedule') self.pruneSchedule();
|
|
else if (key == 'categories') self.pruneCategories();
|
|
}
|
|
|
|
var id = self.page_manager.current_page_id;
|
|
var page = self.page_manager.find(id);
|
|
if (page && page.onDataUpdate) page.onDataUpdate(key, data[key]);
|
|
}
|
|
|
|
// update master switch (once per minute)
|
|
if (data.state) self.updateMasterSwitch();
|
|
|
|
// clear event autosave data if schedule was updated
|
|
if (data.schedule) delete self.autosave_event;
|
|
} );
|
|
|
|
// --- Keep socket.io connected forever ---
|
|
// This is the worst hack in history, but socket.io-client
|
|
// is simply not behaving, and I have tried EVERYTHING ELSE.
|
|
setInterval( function() {
|
|
if (socket && !socket.connected) {
|
|
Debug.trace("Forcing socket to reconnect");
|
|
socket.connect();
|
|
}
|
|
}, 5000 );
|
|
},
|
|
|
|
updateActiveJobs: function(data) {
|
|
// update active jobs
|
|
var jobs = data.active_jobs;
|
|
var changed = false;
|
|
|
|
// determine if jobs have been added or deleted
|
|
for (var id in jobs) {
|
|
// check for new jobs added
|
|
if (!this.activeJobs[id]) changed = true;
|
|
}
|
|
for (var id in this.activeJobs) {
|
|
// check for jobs completed
|
|
if (!jobs[id]) changed = true;
|
|
}
|
|
|
|
this.activeJobs = jobs;
|
|
if (this.isCategoryLimited() || this.isGroupLimited()) this.pruneActiveJobs();
|
|
data.jobs_changed = changed;
|
|
},
|
|
|
|
pruneActiveJobs: function() {
|
|
// remove active jobs that the user should not see, due to category/group privs
|
|
if (!this.activeJobs) return;
|
|
|
|
for (var id in this.activeJobs) {
|
|
var job = this.activeJobs[id];
|
|
if (!this.hasCategoryAccess(job.category) || !this.hasGroupAccess(job.target)) {
|
|
delete this.activeJobs[id];
|
|
}
|
|
}
|
|
},
|
|
|
|
pruneSchedule: function() {
|
|
// remove schedule items that the user should not see, due to category/group privs
|
|
if (!this.schedule || !this.schedule.length) return;
|
|
var new_items = [];
|
|
|
|
for (var idx = 0, len = this.schedule.length; idx < len; idx++) {
|
|
var item = this.schedule[idx];
|
|
if (this.hasCategoryAccess(item.category) && this.hasGroupAccess(item.target)) {
|
|
new_items.push(item);
|
|
}
|
|
}
|
|
|
|
this.schedule = new_items;
|
|
},
|
|
|
|
pruneCategories: function() {
|
|
// remove categories that the user should not see, due to category privs
|
|
if (!this.categories || !this.categories.length) return;
|
|
var new_items = [];
|
|
|
|
for (var idx = 0, len = this.categories.length; idx < len; idx++) {
|
|
var item = this.categories[idx];
|
|
if (this.hasCategoryAccess(item.id)) new_items.push(item);
|
|
}
|
|
|
|
this.categories = new_items;
|
|
},
|
|
|
|
isCategoryLimited: function() {
|
|
// return true if user is limited to specific categories, false otherwise
|
|
if (this.isAdmin()) return false;
|
|
return( app.user && app.user.privileges && app.user.privileges.cat_limit );
|
|
},
|
|
|
|
isGroupLimited: function() {
|
|
// return true if user is limited to specific server groups, false otherwise
|
|
if (this.isAdmin()) return false;
|
|
return( app.user && app.user.privileges && app.user.privileges.grp_limit );
|
|
},
|
|
|
|
hasCategoryAccess: function(cat_id) {
|
|
// check if user has access to specific category
|
|
if (!app.user || !app.user.privileges) return false;
|
|
if (app.user.privileges.admin) return true;
|
|
if (!app.user.privileges.cat_limit) return true;
|
|
|
|
var priv_id = 'cat_' + cat_id;
|
|
return( !!app.user.privileges[priv_id] );
|
|
},
|
|
|
|
hasGroupAccess: function(grp_id) {
|
|
// check if user has access to specific server group
|
|
if (!app.user || !app.user.privileges) return false;
|
|
if (app.user.privileges.admin) return true;
|
|
if (!app.user.privileges.grp_limit) return true;
|
|
|
|
var priv_id = 'grp_' + grp_id;
|
|
var result = !!app.user.privileges[priv_id];
|
|
if (result) return true;
|
|
|
|
// make sure grp_id is a hostname from this point on
|
|
if (find_object(app.server_groups, { id: grp_id })) return false;
|
|
|
|
var groups = app.server_groups.filter( function(group) {
|
|
return grp_id.match( group.regexp );
|
|
} );
|
|
|
|
// we just need one group to match, then the user has permission to target the server
|
|
for (var idx = 0, len = groups.length; idx < len; idx++) {
|
|
priv_id = 'grp_' + groups[idx].id;
|
|
result = !!app.user.privileges[priv_id];
|
|
if (result) return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
hasPrivilege: function(priv_id) {
|
|
// check if user has privilege
|
|
if (!app.user || !app.user.privileges) return false;
|
|
if (app.user.privileges.admin) return true;
|
|
return( !!app.user.privileges[priv_id] );
|
|
},
|
|
|
|
recalculateMaster: function(data) {
|
|
// Oops, we're connected to a slave! Master must have been restarted.
|
|
// If slave knows who is master, switch now, otherwise go into wait loop
|
|
var self = this;
|
|
this.showProgress( 1.0, "Waiting for master server..." );
|
|
this.waitingForMaster = true;
|
|
|
|
if (data.master_hostname) {
|
|
// reload browser which should connect to master
|
|
location.reload();
|
|
}
|
|
},
|
|
|
|
setMasterHostname: function(hostname) {
|
|
// set new master hostname, update stuff
|
|
Debug.trace("New Master Hostname: " + hostname);
|
|
this.masterHostname = hostname;
|
|
|
|
if (config.web_direct_connect) {
|
|
this.base_api_url = this.proto + this.masterHostname + ':' + this.port + config.base_api_uri;
|
|
if (!config.web_socket_use_hostnames && this.servers && this.servers[this.masterHostname] && this.servers[this.masterHostname].ip) {
|
|
// use ip instead of hostname if available
|
|
this.base_api_url = this.proto + this.servers[this.masterHostname].ip + ':' + this.port + config.base_api_uri;
|
|
}
|
|
}
|
|
else {
|
|
this.base_api_url = this.proto + location.host + config.base_api_uri;
|
|
}
|
|
|
|
Debug.trace("API calls now going to: " + this.base_api_url);
|
|
},
|
|
|
|
setHeaderClock: function(when) {
|
|
// move the header clock hands to the selected time
|
|
|
|
if (!when) when = time_now();
|
|
var dargs = get_date_args( when );
|
|
|
|
// hour hand
|
|
var hour = (((dargs.hour + (dargs.min / 60)) % 12) / 12) * 360;
|
|
$('#d_header_clock_hour').css({
|
|
transform: 'rotateZ('+hour+'deg)',
|
|
'-webkit-transform': 'rotateZ('+hour+'deg)'
|
|
});
|
|
|
|
// minute hand
|
|
var min = ((dargs.min + (dargs.sec / 60)) / 60) * 360;
|
|
$('#d_header_clock_minute').css({
|
|
transform: 'rotateZ('+min+'deg)',
|
|
'-webkit-transform': 'rotateZ('+min+'deg)'
|
|
});
|
|
|
|
// second hand
|
|
var sec = (dargs.sec / 60) * 360;
|
|
$('#d_header_clock_second').css({
|
|
transform: 'rotateZ('+sec+'deg)',
|
|
'-webkit-transform': 'rotateZ('+sec+'deg)'
|
|
});
|
|
|
|
// show clock if needed
|
|
if (!this.clock_visible) {
|
|
this.clock_visible = true;
|
|
$('div.header_clock_layer, #d_tab_time').fadeTo( 1000, 1.0 );
|
|
this.checkScrollTime();
|
|
}
|
|
|
|
// date/time in tab bar
|
|
// $('#d_tab_time, #d_scroll_time > span').html( get_nice_date_time( when, true, true ) );
|
|
var num_active = num_keys( app.activeJobs || {} );
|
|
var nice_active = commify(num_active) + ' ' + pluralize('Job', num_active);
|
|
if (!num_active) nice_active = "Idle";
|
|
|
|
$('#d_tab_time > span, #d_scroll_time > span').html(
|
|
// get_nice_date_time( when, true, true ) + ' ' +
|
|
get_nice_time(when, true) + ' ' +
|
|
moment.tz( when * 1000, app.tz).format("z") + ' - ' +
|
|
nice_active
|
|
);
|
|
},
|
|
|
|
updateMasterSwitch: function() {
|
|
// update master switch display
|
|
var html = '';
|
|
if (this.hasPrivilege('state_update')) {
|
|
html = '<i '+(this.state.enabled ? 'class="fa fa-check-square-o">' : 'class="fa fa-square-o">')+'</i> <b>Scheduler Enabled</b>';
|
|
}
|
|
else {
|
|
if (this.state.enabled) html = '<i class="fa fa-check"> </i><b>Scheduler Enabled</b>';
|
|
else html = '<i class="fa fa-times"> </i><b>Scheduler Disabled</b>';
|
|
}
|
|
|
|
$('#d_tab_master')
|
|
.css( 'color', this.state.enabled ? '#3f7ed5' : '#777' )
|
|
.html( html );
|
|
},
|
|
|
|
toggleMasterSwitch: function() {
|
|
// toggle master scheduler switch on/off
|
|
var self = this;
|
|
var enabled = this.state.enabled ? 0 : 1;
|
|
|
|
if (!this.hasPrivilege('state_update')) return;
|
|
|
|
// $('#d_tab_master > i').removeClass().addClass('fa fa-spin fa-spinner');
|
|
|
|
app.api.post( 'app/update_master_state', { enabled: enabled }, function(resp) {
|
|
app.showMessage('success', "Scheduler has been " + (enabled ? 'enabled' : 'disabled') + ".");
|
|
self.state.enabled = enabled;
|
|
self.updateMasterSwitch();
|
|
} );
|
|
},
|
|
|
|
checkScrollTime: function() {
|
|
// check page scroll, see if we need to fade in/out the scroll time widget
|
|
var pos = get_scroll_xy();
|
|
var y = pos.y;
|
|
var min_y = 70;
|
|
|
|
if ((y >= min_y) && this.clock_visible) {
|
|
if (!this.scroll_time_visible) {
|
|
// time to fade it in
|
|
$('#d_scroll_time').stop().css('top', '0px').fadeTo( 1000, 1.0 );
|
|
this.scroll_time_visible = true;
|
|
}
|
|
}
|
|
else {
|
|
if (this.scroll_time_visible) {
|
|
// time to fade it out
|
|
$('#d_scroll_time').stop().fadeTo( 500, 0, function() {
|
|
$(this).css('top', '-30px');
|
|
} );
|
|
this.scroll_time_visible = false;
|
|
}
|
|
}
|
|
},
|
|
|
|
get_password_type: function() {
|
|
// get user's pref for password field type, defaulting to config
|
|
return this.getPref('password_type') || config.default_password_type || 'password';
|
|
},
|
|
|
|
get_password_toggle_html: function() {
|
|
// get html for a password toggle control
|
|
var text = (this.get_password_type() == 'password') ? 'Show' : 'Hide';
|
|
return '<span class="link password_toggle" onMouseUp="app.toggle_password_field(this)">' + text + '</span>';
|
|
},
|
|
|
|
toggle_password_field: function(span) {
|
|
// toggle password field visible / masked
|
|
var $span = $(span);
|
|
var $field = $span.prev();
|
|
if ($field.attr('type') == 'password') {
|
|
$field.attr('type', 'text');
|
|
$span.html( 'Hide' );
|
|
this.setPref('password_type', 'text');
|
|
}
|
|
else {
|
|
$field.attr('type', 'password');
|
|
$span.html( 'Show' );
|
|
this.setPref('password_type', 'password');
|
|
}
|
|
},
|
|
|
|
password_strengthify: function(sel) {
|
|
// add password strength meter (text field should be wrapped by div)
|
|
var $field = $(sel);
|
|
var $div = $field.parent();
|
|
|
|
var $cont = $('<div class="psi_container" title="Password strength indicator" onClick="window.open(\'https://tech.dropbox.com/2012/04/zxcvbn-realistic-password-strength-estimation/\')"></div>');
|
|
$cont.css('width', $field[0].offsetWidth );
|
|
$cont.html( '<div class="psi_bar"></div>' );
|
|
$div.append( $cont );
|
|
|
|
$field.keyup( function() {
|
|
setTimeout( function() {
|
|
app.update_password_strength($field, $cont);
|
|
}, 1 );
|
|
} );
|
|
|
|
if (!window.zxcvbn) load_script('js/external/zxcvbn.js');
|
|
},
|
|
|
|
update_password_strength: function($field, $cont) {
|
|
// update password strength indicator after keypress
|
|
if (window.zxcvbn) {
|
|
var password = $field.val();
|
|
var result = zxcvbn( password );
|
|
// Debug.trace("Password score: " + password + ": " + result.score);
|
|
var $bar = $cont.find('div.psi_bar');
|
|
$bar.removeClass('str0 str1 str2 str3 str4');
|
|
if (password.length) $bar.addClass('str' + result.score);
|
|
app.last_password_strength = result;
|
|
}
|
|
},
|
|
|
|
get_password_warning: function() {
|
|
// return string of text used for bad password dialog
|
|
var est_length = app.last_password_strength.crack_time_display;
|
|
if (est_length == 'instant') est_length = 'instantly';
|
|
else est_length = 'in about ' + est_length;
|
|
|
|
return "The password you entered is <b>insecure</b>, and could be easily compromised by hackers. Our anaysis indicates that it could be cracked via brute force " + est_length + ". For more details see <a href=\"http://en.wikipedia.org/wiki/Password_strength\" target=\"_blank\">this article</a>.<br/><br/>Do you really want to use this password?";
|
|
},
|
|
|
|
get_color_checkbox_html: function(id, label, checked) {
|
|
// get html for color label checkbox, with built-in handlers to toggle state
|
|
if (checked === true) checked = "checked";
|
|
else if (checked === false) checked = "";
|
|
|
|
return '<span id="'+id+'" class="color_label checkbox ' + checked + '" onMouseUp="app.toggle_color_checkbox(this)"><i class="fa '+(checked.match(/\bchecked\b/) ? 'fa-check-square-o' : 'fa-square-o')+'"> </i>'+label+'</span>';
|
|
},
|
|
|
|
toggle_color_checkbox: function(elem) {
|
|
// toggle color checkbox state
|
|
var $elem = $(elem);
|
|
if ($elem.hasClass('checked')) {
|
|
// uncheck
|
|
$elem.removeClass('checked').find('i').removeClass('fa-check-square-o').addClass('fa-square-o');
|
|
}
|
|
else {
|
|
// check
|
|
$elem.addClass('checked').find('i').addClass('fa-check-square-o').removeClass('fa-square-o');
|
|
}
|
|
}
|
|
|
|
}); // app
|
|
|
|
function get_pretty_int_list(arr, ranges) {
|
|
// compose int array to string using commas + spaces, and
|
|
// the english "and" to group the final two elements.
|
|
// also detect sequences and collapse those into dashed ranges
|
|
if (!arr || !arr.length) return '';
|
|
if (arr.length == 1) return arr[0].toString();
|
|
arr = deep_copy_object(arr).sort( function(a, b) { return a - b; } );
|
|
|
|
// check for ranges and collapse them
|
|
if (ranges) {
|
|
var groups = [];
|
|
var group = [];
|
|
for (var idx = 0, len = arr.length; idx < len; idx++) {
|
|
var elem = arr[idx];
|
|
if (!group.length || (elem == group[group.length - 1] + 1)) group.push(elem);
|
|
else { groups.push(group); group = [elem]; }
|
|
}
|
|
if (group.length) groups.push(group);
|
|
arr = [];
|
|
for (var idx = 0, len = groups.length; idx < len; idx++) {
|
|
var group = groups[idx];
|
|
if (group.length == 1) arr.push( group[0] );
|
|
else if (group.length == 2) {
|
|
arr.push( group[0] );
|
|
arr.push( group[1] );
|
|
}
|
|
else {
|
|
arr.push( group[0] + ' - ' + group[group.length - 1] );
|
|
}
|
|
}
|
|
} // ranges
|
|
|
|
if (arr.length == 1) return arr[0].toString();
|
|
return arr.slice(0, arr.length - 1).join(', ') + ' and ' + arr[ arr.length - 1 ];
|
|
}
|
|
|
|
function summarize_event_timing(timing, timezone) {
|
|
// summarize event timing into human-readable string
|
|
if (!timing) return "On demand";
|
|
|
|
// years
|
|
var year_str = '';
|
|
if (timing.years && timing.years.length) {
|
|
year_str = get_pretty_int_list(timing.years, true);
|
|
}
|
|
|
|
// months
|
|
var mon_str = '';
|
|
if (timing.months && timing.months.length) {
|
|
mon_str = get_pretty_int_list(timing.months, true).replace(/(\d+)/g, function(m_all, m_g1) {
|
|
return _months[ parseInt(m_g1) - 1 ][1];
|
|
});
|
|
}
|
|
|
|
// days
|
|
var mday_str = '';
|
|
if (timing.days && timing.days.length) {
|
|
mday_str = get_pretty_int_list(timing.days, true).replace(/(\d+)/g, function(m_all, m_g1) {
|
|
return m_g1 + _number_suffixes[ parseInt( m_g1.substring(m_g1.length - 1) ) ];
|
|
});
|
|
}
|
|
|
|
// weekdays
|
|
var wday_str = '';
|
|
if (timing.weekdays && timing.weekdays.length) {
|
|
wday_str = get_pretty_int_list(timing.weekdays, true).replace(/(\d+)/g, function(m_all, m_g1) {
|
|
return _day_names[ parseInt(m_g1) ] + 's';
|
|
});
|
|
wday_str = wday_str.replace(/Mondays\s+\-\s+Fridays/, 'weekdays');
|
|
}
|
|
|
|
// hours
|
|
var hour_str = '';
|
|
if (timing.hours && timing.hours.length) {
|
|
hour_str = get_pretty_int_list(timing.hours, true).replace(/(\d+)/g, function(m_all, m_g1) {
|
|
return _hour_names[ parseInt(m_g1) ];
|
|
});
|
|
}
|
|
|
|
// minutes
|
|
var min_str = '';
|
|
if (timing.minutes && timing.minutes.length) {
|
|
min_str = get_pretty_int_list(timing.minutes, false).replace(/(\d+)/g, function(m_all, m_g1) {
|
|
return ':' + ((m_g1.length == 1) ? ('0'+m_g1) : m_g1);
|
|
});
|
|
}
|
|
|
|
// construct final string
|
|
var groups = [];
|
|
var mday_compressed = false;
|
|
|
|
if (year_str) {
|
|
groups.push( 'in ' + year_str );
|
|
if (mon_str) groups.push( mon_str );
|
|
}
|
|
else if (mon_str) {
|
|
// compress single month + single day
|
|
if (timing.months && timing.months.length == 1 && timing.days && timing.days.length == 1) {
|
|
groups.push( 'on ' + mon_str + ' ' + mday_str );
|
|
mday_compressed = true;
|
|
}
|
|
else {
|
|
groups.push( 'in ' + mon_str );
|
|
}
|
|
}
|
|
|
|
if (mday_str && !mday_compressed) {
|
|
if (mon_str || wday_str) groups.push( 'on the ' + mday_str );
|
|
else groups.push( 'monthly on the ' + mday_str );
|
|
}
|
|
if (wday_str) groups.push( 'on ' + wday_str );
|
|
|
|
// compress single hour + single minute
|
|
if (timing.hours && timing.hours.length == 1 && timing.minutes && timing.minutes.length == 1) {
|
|
hour_str.match(/^(\d+)(\w+)$/);
|
|
var hr = RegExp.$1;
|
|
var ampm = RegExp.$2;
|
|
var new_str = hr + min_str + ampm;
|
|
|
|
if (mday_str || wday_str) groups.push( 'at ' + new_str );
|
|
else groups.push( 'daily at ' + new_str );
|
|
}
|
|
else {
|
|
var min_added = false;
|
|
if (hour_str) {
|
|
if (mday_str || wday_str) groups.push( 'at ' + hour_str );
|
|
else groups.push( 'daily at ' + hour_str );
|
|
}
|
|
else {
|
|
// check for repeating minute pattern
|
|
if (timing.minutes && timing.minutes.length) {
|
|
var interval = detect_num_interval( timing.minutes, 60 );
|
|
if (interval) {
|
|
var new_str = 'every ' + interval + ' minutes';
|
|
if (timing.minutes[0] > 0) {
|
|
var m_g1 = timing.minutes[0].toString();
|
|
new_str += ' starting on the :' + ((m_g1.length == 1) ? ('0'+m_g1) : m_g1);
|
|
}
|
|
groups.push( new_str );
|
|
min_added = true;
|
|
}
|
|
}
|
|
|
|
if (!min_added) {
|
|
if (min_str) groups.push( 'hourly' );
|
|
}
|
|
}
|
|
|
|
if (!min_added) {
|
|
if (min_str) groups.push( 'on the ' + min_str.replace(/\:00/, 'hour').replace(/\:30/, 'half-hour') );
|
|
else groups.push( 'every minute' );
|
|
}
|
|
}
|
|
|
|
var text = groups.join(', ');
|
|
var output = text.substring(0, 1).toUpperCase() + text.substring(1, text.length);
|
|
|
|
if (timezone && (timezone != app.tz)) {
|
|
// get tz abbreviation
|
|
output += ' (' + moment.tz.zone(timezone).abbr( (new Date()).getTime() ) + ')';
|
|
}
|
|
|
|
return output;
|
|
};
|
|
|
|
function detect_num_interval(arr, max) {
|
|
// detect interval between array elements, return if found
|
|
// all elements must have same interval between them
|
|
if (arr.length < 2) return false;
|
|
// if (arr[0] > 0) return false;
|
|
|
|
var interval = arr[1] - arr[0];
|
|
for (var idx = 1, len = arr.length; idx < len; idx++) {
|
|
var temp = arr[idx] - arr[idx - 1];
|
|
if (temp != interval) return false;
|
|
}
|
|
|
|
// if max is provided, final element + interval must equal max
|
|
// if (max && (arr[arr.length - 1] + interval != max)) return false;
|
|
if (max && ((arr[arr.length - 1] + interval) % max != arr[0])) return false;
|
|
|
|
return interval;
|
|
};
|
|
|
|
// Crontab Parsing Tools
|
|
// by Joseph Huckaby, (c) 2015, MIT License
|
|
|
|
var cron_aliases = {
|
|
jan: 1,
|
|
feb: 2,
|
|
mar: 3,
|
|
apr: 4,
|
|
may: 5,
|
|
jun: 6,
|
|
jul: 7,
|
|
aug: 8,
|
|
sep: 9,
|
|
oct: 10,
|
|
nov: 11,
|
|
dec: 12,
|
|
|
|
sun: 0,
|
|
mon: 1,
|
|
tue: 2,
|
|
wed: 3,
|
|
thu: 4,
|
|
fri: 5,
|
|
sat: 6
|
|
};
|
|
var cron_alias_re = new RegExp("\\b(" + hash_keys_to_array(cron_aliases).join('|') + ")\\b", "g");
|
|
|
|
function parse_crontab_part(timing, raw, key, min, max, rand_seed) {
|
|
// parse one crontab part, e.g. 1,2,3,5,20-25,30-35,59
|
|
// can contain single number, and/or list and/or ranges and/or these things: */5 or 10-50/5
|
|
if (raw == '*') { return; } // wildcard
|
|
if (raw == 'h') {
|
|
// unique value over accepted range, but locked to random seed
|
|
// https://github.com/jhuckaby/Cronicle/issues/6
|
|
raw = min + (parseInt( hex_md5(rand_seed), 16 ) % ((max - min) + 1));
|
|
raw = '' + raw;
|
|
}
|
|
if (!raw.match(/^[\w\-\,\/\*]+$/)) { throw new Error("Invalid crontab format: " + raw); }
|
|
var values = {};
|
|
var bits = raw.split(/\,/);
|
|
|
|
for (var idx = 0, len = bits.length; idx < len; idx++) {
|
|
var bit = bits[idx];
|
|
if (bit.match(/^\d+$/)) {
|
|
// simple number, easy
|
|
values[bit] = 1;
|
|
}
|
|
else if (bit.match(/^(\d+)\-(\d+)$/)) {
|
|
// simple range, e.g. 25-30
|
|
var start = parseInt( RegExp.$1 );
|
|
var end = parseInt( RegExp.$2 );
|
|
for (var idy = start; idy <= end; idy++) { values[idy] = 1; }
|
|
}
|
|
else if (bit.match(/^\*\/(\d+)$/)) {
|
|
// simple step interval, e.g. */5
|
|
var step = parseInt( RegExp.$1 );
|
|
var start = min;
|
|
var end = max;
|
|
for (var idy = start; idy <= end; idy += step) { values[idy] = 1; }
|
|
}
|
|
else if (bit.match(/^(\d+)\-(\d+)\/(\d+)$/)) {
|
|
// range step inverval, e.g. 1-31/5
|
|
var start = parseInt( RegExp.$1 );
|
|
var end = parseInt( RegExp.$2 );
|
|
var step = parseInt( RegExp.$3 );
|
|
for (var idy = start; idy <= end; idy += step) { values[idy] = 1; }
|
|
}
|
|
else {
|
|
throw new Error("Invalid crontab format: " + bit + " (" + raw + ")");
|
|
}
|
|
}
|
|
|
|
// min max
|
|
var to_add = {};
|
|
var to_del = {};
|
|
for (var value in values) {
|
|
value = parseInt( value );
|
|
if (value < min) {
|
|
to_del[value] = 1;
|
|
to_add[min] = 1;
|
|
}
|
|
else if (value > max) {
|
|
to_del[value] = 1;
|
|
value -= min;
|
|
value = value % ((max - min) + 1); // max is inclusive
|
|
value += min;
|
|
to_add[value] = 1;
|
|
}
|
|
}
|
|
for (var value in to_del) delete values[value];
|
|
for (var value in to_add) values[value] = 1;
|
|
|
|
// convert to sorted array
|
|
var list = hash_keys_to_array(values);
|
|
for (var idx = 0, len = list.length; idx < len; idx++) {
|
|
list[idx] = parseInt( list[idx] );
|
|
}
|
|
list = list.sort( function(a, b) { return a - b; } );
|
|
if (list.length) timing[key] = list;
|
|
};
|
|
|
|
function parse_crontab(raw, rand_seed) {
|
|
// parse standard crontab syntax, return timing object
|
|
// e.g. 1,2,3,5,20-25,30-35,59 23 31 12 * *
|
|
// optional 6th element == years
|
|
if (!rand_seed) rand_seed = get_unique_id();
|
|
var timing = {};
|
|
|
|
// resolve all @shortcuts
|
|
raw = trim(raw).toLowerCase();
|
|
if (raw.match(/\@(yearly|annually)/)) raw = '0 0 1 1 *';
|
|
else if (raw == '@monthly') raw = '0 0 1 * *';
|
|
else if (raw == '@weekly') raw = '0 0 * * 0';
|
|
else if (raw == '@daily') raw = '0 0 * * *';
|
|
else if (raw == '@hourly') raw = '0 * * * *';
|
|
|
|
// expand all month/wday aliases
|
|
raw = raw.replace(cron_alias_re, function(m_all, m_g1) {
|
|
return cron_aliases[m_g1];
|
|
} );
|
|
|
|
// at this point string should not contain any alpha characters or '@', except for 'h'
|
|
if (raw.match(/([a-gi-z\@]+)/i)) throw new Error("Invalid crontab keyword: " + RegExp.$1);
|
|
|
|
// split into parts
|
|
var parts = raw.split(/\s+/);
|
|
if (parts.length > 6) throw new Error("Invalid crontab format: " + parts.slice(6).join(' '));
|
|
if (!parts[0].length) throw new Error("Invalid crontab format");
|
|
|
|
// parse each part
|
|
if ((parts.length > 0) && parts[0].length) parse_crontab_part( timing, parts[0], 'minutes', 0, 59, rand_seed );
|
|
if ((parts.length > 1) && parts[1].length) parse_crontab_part( timing, parts[1], 'hours', 0, 23, rand_seed );
|
|
if ((parts.length > 2) && parts[2].length) parse_crontab_part( timing, parts[2], 'days', 1, 31, rand_seed );
|
|
if ((parts.length > 3) && parts[3].length) parse_crontab_part( timing, parts[3], 'months', 1, 12, rand_seed );
|
|
if ((parts.length > 4) && parts[4].length) parse_crontab_part( timing, parts[4], 'weekdays', 0, 6, rand_seed );
|
|
if ((parts.length > 5) && parts[5].length) parse_crontab_part( timing, parts[5], 'years', 1970, 3000, rand_seed );
|
|
|
|
return timing;
|
|
};
|
|
|
|
// TAB handling code from http://www.webdeveloper.com/forum/showthread.php?t=32317
|
|
// Hacked to do my bidding - JH 2008-09-15
|
|
function setSelectionRange(input, selectionStart, selectionEnd) {
|
|
if (input.setSelectionRange) {
|
|
input.focus();
|
|
input.setSelectionRange(selectionStart, selectionEnd);
|
|
}
|
|
else if (input.createTextRange) {
|
|
var range = input.createTextRange();
|
|
range.collapse(true);
|
|
range.moveEnd('character', selectionEnd);
|
|
range.moveStart('character', selectionStart);
|
|
range.select();
|
|
}
|
|
};
|
|
|
|
function replaceSelection (input, replaceString) {
|
|
var oldScroll = input.scrollTop;
|
|
if (input.setSelectionRange) {
|
|
var selectionStart = input.selectionStart;
|
|
var selectionEnd = input.selectionEnd;
|
|
input.value = input.value.substring(0, selectionStart)+ replaceString + input.value.substring(selectionEnd);
|
|
|
|
if (selectionStart != selectionEnd){
|
|
setSelectionRange(input, selectionStart, selectionStart + replaceString.length);
|
|
}else{
|
|
setSelectionRange(input, selectionStart + replaceString.length, selectionStart + replaceString.length);
|
|
}
|
|
|
|
}else if (document.selection) {
|
|
var range = document.selection.createRange();
|
|
|
|
if (range.parentElement() == input) {
|
|
var isCollapsed = range.text == '';
|
|
range.text = replaceString;
|
|
|
|
if (!isCollapsed) {
|
|
range.moveStart('character', -replaceString.length);
|
|
range.select();
|
|
}
|
|
}
|
|
}
|
|
input.scrollTop = oldScroll;
|
|
};
|
|
|
|
function catchTab(item,e){
|
|
var c = e.which ? e.which : e.keyCode;
|
|
|
|
if (c == 9){
|
|
replaceSelection(item,String.fromCharCode(9));
|
|
setTimeout("document.getElementById('"+item.id+"').focus();",0);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
function get_text_from_seconds_round_custom(sec, abbrev) {
|
|
// convert raw seconds to human-readable relative time
|
|
// round to nearest instead of floor, but allow one decimal point if under 10 units
|
|
var neg = '';
|
|
if (sec < 0) { sec =- sec; neg = '-'; }
|
|
|
|
var text = abbrev ? "sec" : "second";
|
|
var amt = sec;
|
|
|
|
if (sec > 59) {
|
|
var min = sec / 60;
|
|
text = abbrev ? "min" : "minute";
|
|
amt = min;
|
|
|
|
if (min > 59) {
|
|
var hour = min / 60;
|
|
text = abbrev ? "hr" : "hour";
|
|
amt = hour;
|
|
|
|
if (hour > 23) {
|
|
var day = hour / 24;
|
|
text = "day";
|
|
amt = day;
|
|
} // hour>23
|
|
} // min>59
|
|
} // sec>59
|
|
|
|
if (amt < 10) amt = Math.round(amt * 10) / 10;
|
|
else amt = Math.round(amt);
|
|
|
|
var text = "" + amt + " " + text;
|
|
if ((amt != 1) && !abbrev) text += "s";
|
|
|
|
return(neg + text);
|
|
};
|