PluginProbe
UpdraftCentral Dashboard / 0.3.5
UpdraftCentral Dashboard v0.3.5
0.8.33 0.7.2 0.7.3 0.7.4 0.8.0 0.8.1 0.8.10 0.8.11 0.8.12 0.8.13 0.8.14 0.8.15 0.8.16 0.8.17 0.8.18 0.8.19 0.8.2 0.8.20 0.8.21 0.8.22 0.8.23 0.8.24 0.8.25 0.8.26 0.8.27 All 51 releases
updraftcentral / js / dashboard.js

dashboard.js in UpdraftCentral Dashboard 0.3.5, at js/dashboard.js

1,779 lines 81.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 jQuery(document).ready(function($) {
2 UpdraftCentral = UpdraftCentral();
3 });
4
5 // Only needed for IE9 support - http://caniuse.com/#feat=console-basic
6 // Console-polyfill. MIT license.
7 // https://github.com/paulmillr/console-polyfill
8 // Make it safe to do console.log() always.
9 (function(global) {
10 'use strict';
11 global.console = global.console || {};
12 var con = global.console;
13 var prop, method;
14 var empty = {};
15 var dummy = function() {};
16 var properties = 'memory'.split(',');
17 var methods = ('assert,clear,count,debug,dir,dirxml,error,exception,group,' +
18 'groupCollapsed,groupEnd,info,log,markTimeline,profile,profiles,profileEnd,' +
19 'show,table,time,timeEnd,timeline,timelineEnd,timeStamp,trace,warn').split(',');
20 while (prop = properties.pop()) if (!con[prop]) con[prop] = empty;
21 while (method = methods.pop()) if (typeof con[method] !== 'function') con[method] = dummy;
22 // Using `this` for web workers & supports Browserify / Webpack.
23 })(typeof window === 'undefined' ? this : window);
24
25 var UpdraftCentral = function() {
26
27 // This is just used internally to log more things. Set to 0 to turn it off (which won't necessarily prevent all console logging).
28 // It's not completely systematic/consistent. Logging has been added in an ad hoc manner during development/testing to help with debugging.
29 // This gets passed through from the PHP constant UPDRAFTCENTRAL_DEBUG_LEVEL
30 var updraftcentral_debug_level = ('undefined' === typeof udclion || !udclion.hasOwnProperty('debug_level')) ? 0 : udclion.debug_level;
31 var listener_poll_interval = (updraftcentral_debug_level > 0) ? 5000 : 10000;
32
33 var mobile_width = 583
34
35 var $ = jQuery;
36
37 var modal_action_callback;
38
39 // Use to hold the current site being operated on (e.g. state for modals) (N.B. - you may need to explicitly set this, depending on whether you're using a convenience function that already sets it, or not)
40 var $row_parent;
41
42 /**
43 * @param {Number} debug_level - debugging level, controlling how much console output there will be. The higher the value, the more output. Generally only 0 (minimal), 1 (some), 2 (very much) and 3 (even more) are useful levels
44 */
45 this.set_debug_level = function(debug_level) {
46 updraftcentral_debug_level = debug_level;
47 }
48
49 // Dialog methods - this is just an abstraction layer (currently onto Bootbox, http://bootboxjs.com), allowing us to easily swap to a different provider if we ever need to
50 this.dialog = {};
51
52 /**
53 * Function to be called whenever a bootbox dialog is opened
54 *
55 * We use it simply to move the bootbox within the DOM if in fullscreen mode (because otherwise it won't be seen).
56 */
57 var bootbox_opened = function() {
58 // It only needs moving if in full-screen mode; so, we're conservative and otherwise leave it alone
59 if ($.fullscreen.isFullScreen()) {
60 $('.bootbox.modal').prependTo('#updraftcentral_dashboard');
61 }
62 // Use a new browser portal for any clicks to updraftplus.com
63 $('.bootbox.modal').on('click', 'a', function(e) {
64 var href = $(this).attr('href');
65 if (href.substring(0, 23) == 'https://updraftplus.com' || href.substring(0, 23) == 'http://updraftplus.com') {
66 e.preventDefault();
67 var win = window.open(href, '_blank');
68 win.focus();
69 }
70 });
71 $('.bootbox.modal .updraftcentral_site_editdescription').click(function(e) {
72 e.preventDefault();
73 $(this).closest('.modal').modal('hide');
74 open_site_configuration(UpdraftCentral.$row_parent);
75 });
76 $('.bootbox.modal .updraftcentral_test_other_connection_methods').click(function(e) {
77 e.preventDefault();
78 $(this).closest('.modal').modal('hide');
79 open_connection_test(UpdraftCentral.$row_parent);
80 });
81 }
82
83 $('#updraftcentral_modal').on('click', '.updraftcentral_test_other_connection_methods', function(e) {
84 e.preventDefault();
85 open_connection_test(UpdraftCentral.$row_parent);
86 });
87 $('#updraftcentral_modal').on('click', 'a.connection-test-switch', function(e) {
88 e.preventDefault();
89 var connection_method = $(this).data('connection_method');
90
91 UpdraftCentral.close_modal();
92
93 var site_id = $(this).data('site_id');
94
95 send_ajax('edit_site_connection_method', { site_id: site_id, connection_method: connection_method }, null, 'via_mothership_encrypting', '#updraftcentral_dashboard_existingsites', function(resp, code, error_code) {
96
97 if ('ok' == code) {
98
99 if (resp.hasOwnProperty('message')) { add_dashboard_notice(resp.message); }
100
101 if (resp.hasOwnProperty('sites_html')) {
102 set_existing_sites_to(resp.sites_html);
103 setup_menunav();
104 } else {
105 console.log(resp);
106 add_dashboard_notice(udclion.unknown_response, 'error');
107 }
108 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
109 }
110 });
111 });
112
113 /**
114 * @callable dialogresultCallback
115 * @param {null|boolean|String} [result=null] - the button clicked (for a confirm call) or the value entered (for a prompt); or, undefined for an alert
116 */
117
118 /**
119 * Open an alert box (as a more aesthetic alternative to the traditional browser-provided alert()).
120 * @param {string} message - the message to display in the alert box
121 * @param {dialogresultCallback} - callback function that is invoked when the alert box is closed
122 * @param {Boolean} [sanitize_message=true] - whether or not to put the message through sanitize_html()
123 *
124 * @uses sanitize_html
125 */
126 this.dialog.alert = function(message, result_callback, sanitize_message) {
127 sanitize_message = ('undefined' == sanitize_message) ? true : sanitize_message;
128 if (sanitize_message) {
129 message = this.sanitize_html(message);
130 }
131 bootbox.alert(message, result_callback);
132 bootbox_opened();
133 }
134
135 /**
136 * Open a confirmation box (as a more aesthetic alternative to the traditional browser-provided confirm()).
137 * @param {string} message - the message to display in the alert box
138 * @param {dialogresultCallback} - callback function that is invoked when the alert box is closed
139 */
140 this.dialog.confirm = function(question, result_callback) {
141 bootbox.confirm(question, result_callback);
142 bootbox_opened();
143 }
144
145 /**
146 * Open a prompt box (as a more aesthetic alternative to the traditional browser-provided prompt()).
147 * @param {string} title - the message to display in the alert box
148 * @param {string} default_value - the default value for the user response field
149 * @param {dialogresultCallback} - callback function that is invoked when the alert box is closed
150 */
151 this.dialog.prompt = function(title, default_value, result_callback) {
152 bootbox.prompt({ title: title, value: default_value, callback: result_callback});
153 bootbox_opened();
154 }
155
156 /**
157 * Encode to base64 encoding
158 * This (or its callers) can be swapped for atob() once IE 10 support is not desired - http://caniuse.com/#feat=atob-btoa
159 *
160 * @param {String} data - the data to base64-encode
161 *
162 * @returns {String} - the encoded data
163 */
164 this.base64_encode = function(data) {
165 return forge.util.encode64(data);
166 }
167
168 /**
169 * Open a modal window with the specified contents. Modals are separate to dialogues - that is, you can have both open at once without them interfering.
170 * @param {String} title - the title to use for the modal window
171 * @param {String} body - the HTML contents to place in the modal window
172 * @param {callback|true} action_button_callback - a callback to call when the main action button is pressed; or just true to close the modal
173 * @param {String|false} [action_button_text="Go"] - text for the action button; or, if false, an indication that there should be no action button
174 * @param {callback|null} [pre_open_callback=null] - an optional callback to call immediately before opening the modal
175 * @param {boolean} [sanitize_body=true] - whether or not to call the sanitize_html() method the passed body, or not, before placing it in the modal
176 * @param {String} [extra_classes=''] - extra CSS classes for the modal dialog (e.g. modal-lg)
177 */
178 this.open_modal = function(title, body, action_button_callback, action_button_text, pre_open_callback, sanitize_body, extra_classes) {
179 action_button_text = typeof action_button_text !== 'undefined' ? action_button_text : udclion.go;
180 // By default, we assume that the input is potentially evil, and sanitize it
181 sanitize_body = typeof sanitize_body !== 'undefined' ? sanitize_body : true;
182 extra_classes = typeof extra_classes !== 'undefined' ? extra_classes : '';
183
184 // Reset the modal's CSS classes
185 $('#updraftcentral_modal .modal-dialog').removeClass().addClass('modal-dialog '+extra_classes);
186
187 $('#updraftcentral_modal .modal-title').html(title);
188 if (sanitize_body) body = this.sanitize_html(body);
189 $('#updraftcentral_modal .modal-body').html(body);
190 if (false === action_button_text) {
191 $('#updraftcentral_modal button.updraft_modal_button_goahead').hide();
192 } else {
193 $('#updraftcentral_modal button.updraft_modal_button_goahead').html(action_button_text).show();
194 }
195 modal_action_callback = action_button_callback;
196 if (typeof pre_open_callback !== 'undefined' && null !== pre_open_callback) pre_open_callback.call(this);
197
198 $('#updraftcentral_modal').modal();
199 }
200
201 /**
202 * Allow the user to download/save a file, with contents supplied from the inner HTML of a specified element
203 *
204 * @param {string} filename - the filename that will be suggested to the user to save as
205 * @param {string} element_id - the DOM id of the element whose inner HTML is to be used as content
206 * @param {string} [mime_type='text/plain'] - the MIME type to indicate in the header sent to the browser
207 */
208 this.download_inner_html = function(filename, element_id, mime_type) {
209 mime_type = mime_type || 'text/plain';
210 var element_html = document.getElementById(element_id).innerHTML;
211 var link = document.body.appendChild(document.createElement('a'));
212 link.setAttribute('download', filename);
213 link.setAttribute('style', "display:none;");
214 link.setAttribute('href', 'data:' + mime_type + ';charset=utf-8,' + encodeURIComponent(element_html));
215 link.click();
216 }
217
218 /**
219 * Close the modal dialog
220 */
221 this.close_modal = function() {
222 $('#updraftcentral_modal').modal('hide');
223 }
224
225 /**
226 * Sanitizes passed HTML, so that it is safe for display. Uses Google's Caja parser.
227 *
228 * @param {string} html - the potentially suspicious HTML
229 * @returns {string} The sanitized HTML
230 */
231 this.sanitize_html = function(html) {
232 var web_only = function(url) { if(/^https?:\/\//.test(url)) { return url; }}
233 var same_id = function(id) { return id; }
234 // The html_sanitize object comes from Google's Caja
235 // This version retains data- attributes. It removes style attributes (but not CSS classes)
236 return html_sanitize.sanitize(html, web_only, same_id);
237 }
238
239 /**
240 * jQuery callback for row click events
241 *
242 * @callable rowclickerCallback
243 * @param {string} $row_parent - the jQuery row object for the site that the click was for
244 * @param {Number} site_id - the site ID for the site that the click was for
245 * @param {Object} event - the event received from jQuery
246 *
247 * @return {*} prevent_default - if anything other than (boolean)true, then event.preventDefault() is called
248 */
249
250 /**
251 * Register click events for specified items in the UpdraftCentral site list (prevents repeating lots of jQuery boilerplate)
252 *
253 * @param {string} selector - the selector to use
254 * @param {rowclickerCallback} - callback function that will be called upon the click event
255 * @param {Boolean} [hide_other_sites=false] - if set, then the click will cause other sites in the tab to be hidden
256 * @param {string} [onevent='click'] - the event type to listen for. In the special case of 'keypress', the default event will not be prevented
257 */
258 this.register_row_clicker = function(selector, callback, hide_other_sites, on_event) {
259 on_event = typeof on_event !== 'undefined' ? on_event : 'click';
260 hide_other_sites = typeof hide_other_sites !== 'undefined' ? hide_other_sites : false;
261 params = {};
262 if ('tripleclick' == on_event) { params.threshold = 500; };
263 $('#updraftcentral_dashboard_existingsites_container').on(on_event, '.updraftcentral_site_row '+selector, params, function(event) {
264 if (on_event != 'keypress') { event.preventDefault(); }
265 UpdraftCentral.$row_parent = $(this).closest('.updraftcentral_site_row');
266 var site_id = UpdraftCentral.$row_parent.data('site_id');
267 if (hide_other_sites) {
268 $('#updraftcentral_dashboard_existingsites > .updraftcentral_site_row:not([data-site_id="'+site_id+'"]), #updraftcentral_dashboard_existingsites > .updraftcentral_row_divider').slideUp();
269 $('.updraftcentral_mode_actions .updraftcentral_action_choose_another_site').show();
270 }
271 callback.call(this, UpdraftCentral.$row_parent, site_id, event);
272 });
273 }
274
275 $('#updraftcentral_modal button.updraft_modal_button_goahead').click(function() {
276 if (true === modal_action_callback) {
277 this.close_modal();
278 } else {
279 modal_action_callback.call(this);
280 }
281 });
282
283
284 /**
285 * jQuery callback for row click events
286 *
287 * @callable listenerCallback
288 *
289 * @param {Object} $listener_row - the jQuery object of the listener itself
290 * @param {Object} $site_row - the jQuery row object for the site that the click was for
291 * @param {Number} site_id - the site ID for the site that this is a listener for
292 * @param {*} [data] - the returned data from the polling operation (if it is that sort of listener)
293 * @returns {*} - if 0 is returned, then the listener will be closed. If 1 is returned, then no more polling will be done, but the listener will not be closed. If an object with a property 'call' is returned, then this will be called. Otherwise, nothing will be done.
294 */
295
296 var listener_processors = {};
297 /**
298 * Register a listener callback - a callback function to be used in association with dashboard notices which poll and update
299 *
300 * @see create_dashboard_listener
301 *
302 * @param {string} listener-type - an identifying string, indicating the listener type
303 * @param {listenerCallback} - a listener callback function
304 */
305 this.register_listener_processor = function(listener_type, callback) {
306 listener_processors[listener_type] = callback;
307 }
308
309 /**
310 * Poll all listener rows on the dashboard for activity
311 */
312 function poll_listeners() {
313
314 // var listener_calls = {};
315
316 $('#updraftcentral_notice_container .updraftcentral_listener').each(function(ind) {
317 var site_id = $(this).data('site_id');
318 var listener_type = $(this).data('type');
319 var $listener_row = this;
320 var $site_row = $('#updraftcentral_dashboard_existingsites .updraftcentral_site_row[data-site_id="'+site_id+'"');
321 var finished = $(this).data('finished');
322
323 if (finished) { return; }
324
325 console.log("site_id="+site_id+", listener_type="+listener_type);
326
327 if ($site_row.length > 0 && listener_processors.hasOwnProperty(listener_type)) {
328 // if (typeof listener_calls[site_id] === 'undefined') listener_calls[site_id] = [];
329 var call_this = listener_processors[listener_type].call(this, $listener_row, $site_row, site_id);
330 // We could multiplex all the calls to the same site. That would involve plenty of work, but would be worth if for the efficiency - if it weren't the case that HTTP/2 takes care of this.
331 if (0 === call_this) {
332 $(this).data('finished', true);
333 $($listener_row).clearQueue().delay(10000).slideUp('slow', function() { $(this).remove(); });
334 } else if (1 === call_this) {
335 $(this).data('finished', true);
336 } else if (null != call_this && call_this.hasOwnProperty('call')) {
337 var call_type = call_this.call;
338 UpdraftCentral.send_site_rpc(call_this.call, call_this.data, $site_row, function(response, code, error_code) {
339 if ('ok' == code && false !== response && response.hasOwnProperty('data')) {
340 if (listener_processors.hasOwnProperty(call_type)) {
341 listener_processors[call_type].call(this, $listener_row, $site_row, site_id, response.data);
342 } else {
343 console.log("UpdraftCentral: listener type "+call_type+" has no registered processor (dump of all registered processors follows)");
344 console.log(listener_processors);
345 }
346 }
347 });
348 }
349 } else if ($site_row.length > 0) {
350 console.log("UpdraftCentral: listener type "+listener_type+" has no registered processor (dump of all registered processors follows)");
351 console.log(listener_processors);
352 } else {
353 console.log("UpdraftCentral: listener for site_id="+site_id+" with type "+listener_type+": site row not found");
354 }
355 });
356
357 }
358
359 setInterval(function() { poll_listeners(); }, listener_poll_interval);
360
361 // A separate ud_rpc object for each site
362 var ud_rpcs = [];
363
364 /**
365 * Given a site (identified by its row), get a UpdraftPlus_Remote_Communications (remote communications) object for remote communications. This function does the heavy lifting of getting all the connection configuration for the site, and then passing it along to get_udrpc()
366 *
367 * @param {Object} $site_row - the jQuery object for the site row
368 * @param {String} [connection_method_config="direct"] - either 'direct_default_auth'|'direct_jquery_auth'|'direct_manual_auth' (which means to send directly to the destination) or 'via_mothership' which also sends via the PHP-back-end, but does the RSA encryption in the browser. This is not necessarily the same value as inferred from $site_row - we provide it as an extra parameter to make it possible to over-ride - e.g. for diagnostics, or where the browser mixed-content model restricts the choices.
369 *
370 * @returns {Object} - the UpdraftPlus_Remote_Communications object
371 *
372 * @uses get_udrpc
373 */
374 function get_site_udrpc($site_row, connection_method_config) {
375
376 var site_remote_public_key = $site_row.data('site_remote_public_key');
377 var site_local_private_key = $site_row.data('site_local_private_key');
378 var site_url = $site_row.data('site_url');
379 var site_id = $site_row.data('site_id');
380 var key_name_indicator = $site_row.data('key_name_indicator');
381 var remote_user_id = $site_row.data('remote_user_id');
382
383 if ('undefined' === typeof connection_method_config || ('direct_manual_auth' != connection_method_config && 'via_mothership' != connection_method_config && 'via_mothership_encrypting' != connection_method_config && 'direct_jquery_auth' != connection_method_config)) { connection_method_config = 'direct_default_auth'; }
384
385 // The connection method ought not to be via_mothership - such sites shouldn't be being routed into here (but via_mothership_encrypting is allowed)
386 if ('via_mothership_encrypting' == connection_method_config) {
387 console.warn("UpdraftCentral: A site ("+site_id+", "+site_url+") routed via_mothership_encrypting was passed into get_site_udrpc");
388 console.log($site_row);
389 }
390
391 var message_wrapper = false;
392
393 if ('direct_default_auth' == connection_method_config) {
394 // Normally, of course, feature detection should be used. But, it really is the case that we need to do browser-detection here, as they're working differently.
395 var is_firefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
396 // N.B. If they're set to use Digest authentication, this should not use manual - should switch back
397 // connection_method = (is_firefox) ? 'direct_manual_auth' : 'direct_jquery_auth';
398 // Actually, 'jQuery method' also works in Firefox
399 connection_method = 'direct_jquery_auth';
400 } else {
401 connection_method = connection_method_config;
402
403 // We're relying here on the fact that UDRPC object store includes the connection method in its unique ID - i.e. that this can be set on the UDRPC object, without any bad consequences.
404
405 if ('via_mothership' == connection_method) {
406
407 message_wrapper = {
408 action: 'updraftcentral_dashboard_ajax',
409 subaction: 'site_rpc',
410 component: 'dashboard',
411 nonce: udclion.updraftcentral_dashboard_nonce,
412 site_id: site_id,
413 site_rpc_preencrypted: 1
414 };
415
416 }
417
418 }
419
420 var send_cors_headers = $site_row.data('send_cors_headers');
421 if ('undefined' === typeof send_cors_headers) { send_cors_headers = 1; }
422
423 var auth_method = ('direct_manual_auth' == connection_method) ? 'manual' : 'jquery';
424
425 var http_credentials = {};
426
427 var comms_url = site_url;
428
429 // When routing via the mothership, don't put in credentials, as the mothership will do that
430 if ('via_mothership_encrypting' != connection_method && 'via_mothership' != connection_method) {
431 var http_username = $site_row.data('http_username');
432 if ('undefined' !== typeof http_username && http_username) {
433 http_credentials.username = http_username;
434 var http_password = $site_row.data('http_password');
435 if ('undefined' !== typeof http_password) {
436 http_credentials.password = http_password;
437 }
438 }
439 } else {
440 comms_url = udclion.ajaxurl;
441 }
442
443 if (updraftcentral_debug_level > 0) {
444 console.log("UDRPC communications method: site_id="+site_id+", name_indicator="+key_name_indicator+", site_url="+site_url+", comms_url="+comms_url+", remote_user_id="+remote_user_id+", connection_method="+connection_method_config+"/"+connection_method+", send_cors_headers="+send_cors_headers);
445 if (updraftcentral_debug_level > 1) {
446 console.log("Remote public key follows");
447 console.log(site_remote_public_key);
448 }
449 }
450
451 var reuse_id = site_id+' '+connection_method;
452
453 return get_udrpc(reuse_id, remote_user_id, key_name_indicator, site_remote_public_key, site_local_private_key, comms_url, send_cors_headers, http_credentials, auth_method, message_wrapper);
454
455 }
456
457 /**
458 * Given the relevant site information, get a UpdraftPlus_Remote_Communications (remote communications) object for remote communications, and also sets the current debugging level upon it.
459 *
460 * @param {Number} reuse_id - A unique ID, that can be used for re-using of the result
461 * @param {Number} remote_user_id - The ID of the user on the remote WP site that the keys are for
462 * @param {String} key_name_indicator - The key name indicator (which indicates to the remote site which key to use to decrypt the message)
463 * @param {String} site_remote_public_key - The RSA public key for contacting the remote site, in PEM format
464 * @param {String} site_local_private_key - The RSA private key for the local site, in PEM format
465 * @param {String} site_url - The URL for the remote site
466 * @param {Boolean} [cors_headers_wanted=true] - Whether to request that the remote application sets CORS headers with its reply
467 * @param {Object} [http_credentials={}] - an object with any HTTP credentials to be set (useful properties: username, password)
468 * @param {String} [auth_method] - the authentication method to use ('jquery' or 'manual')
469 * @param {Object|Boolean} [message_wrapper=false] - a wrapper to enclose the message in; or false for none. This is passed on to the UDRPC library, which is where the actual wrapping is done.
470 *
471 *
472 * @returns {Object} - the UpdraftPlus_Remote_Communications object
473 *
474 */
475 function get_udrpc(reuse_id, remote_user_id, key_name_indicator, site_remote_public_key, site_local_private_key, site_url, cors_headers_wanted, http_credentials, auth_method, message_wrapper) {
476 if (typeof ud_rpcs[reuse_id] != 'undefined') {
477 ud_rpc = ud_rpcs[reuse_id];
478 } else {
479 cors_headers_wanted = (typeof cors_headers_wanted === 'undefined') ? true : cors_headers_wanted;
480 var ud_rpc = new UpdraftPlus_Remote_Communications(key_name_indicator);
481 ud_rpc.set_key_local(site_local_private_key);
482 ud_rpc.set_key_remote(site_remote_public_key);
483 ud_rpc.activate_replay_protection();
484 ud_rpc.set_destination_url(site_url);
485 if ('undefined' != typeof http_credentials) { ud_rpc.set_http_credentials(http_credentials); }
486 if ('undefined' != typeof auth_method) { ud_rpc.set_auth_method(auth_method); }
487 if ('undefined' != typeof message_wrapper && false !== message_wrapper) {
488 ud_rpc.set_message_wrapper(message_wrapper);
489 ud_rpc.set_message_unwrapper(function(response) {
490 var processed = process_direct_ajax_response(response, 2, false);
491 if (true === processed) {
492 if (response.hasOwnProperty('wrapped_response')) {
493 return response.wrapped_response;
494 } else {
495 processed = 'wrapped_response_not_found';
496 }
497 }
498 console.error("UDRPC: Attempt to unwrap the message failed (code: "+processed+")");
499 // This is usually redundant - something further down the line will log it
500 if (updraftcentral_debug_level > 1) {
501 console.log(response);
502 }
503 return false;
504 });
505 }
506 ud_rpc.set_cors_headers_wanted(cors_headers_wanted);
507 ud_rpcs[reuse_id] = ud_rpc;
508 }
509 if (updraftcentral_debug_level > 0) {
510 // UDRPC, at debug level 2, console.log()s lots of cryptographic internals which are only really needed when debugging that
511 var ud_rpc_debug_level = (updraftcentral_debug_level > 2) ? 2 : 1;
512 ud_rpc.set_debug_level(ud_rpc_debug_level);
513 }
514 return ud_rpc;
515 }
516
517 /**
518 * @callable ajaxCallback
519 * @param {*} response - the response data for the result of the call
520 * @param {String} [code] - the response code; can be 'error' in the case of an error
521 * @param {String} [error_code] - in the case of code being 'error', this contains the error code
522 */
523
524 /**
525 * This function is for processing responses received via send_ajax. Since that function has two separate methods for routing the request, the common response code is abstracted out.
526 *
527 * @param {ajaxCallback} response_callback - callback that will be called with the results of the AJAX call
528 * @param {String} [code] - the response code; can be 'error' in the case of an error
529 * @param {String} [error_code] - in the case of code being 'error', this contains the error code
530 * @param {Boolean|Number} is_site_rpc - whether it was command to a remote site or not. If set to '2', then this indicates that it is site_rpc, and that the encryption was definitely done in the browser (so, we can ignore/drop certain unencrypted responses)
531 * @param {ajaxCallback} response_callback - callback that will be called with the results of the AJAX call
532 * @param {Boolean} [allow_visual_responses=true] - whether or not it is permissible to display UI elements in response to the results (set this to false if the caller wants to handle it internally only)
533 */
534 function process_ajax_response(response, code, error_code, is_site_rpc, response_callback, allow_visual_responses) {
535
536 allow_visual_responses = ('undefined' === typeof allow_visual_responses) ? true : allow_visual_responses;
537
538 // Bring errors up from the RPC layer. "ok" as the main code just means that a result came back successfully; but that result might itself be an error. That is someting to handle here, not in the lower-level communications library.
539
540 if ('error' == code) {
541 console.error("process_ajax_response: return code: "+code+", error_code: "+error_code+" - parsed response follows");
542 console.log(response);
543 } else if (updraftcentral_debug_level > 0) {
544 console.log("process_ajax_response: return code: "+code+" - parsed response follows");
545 console.log(response);
546 }
547
548 if (is_site_rpc && 'ok' == code && response.hasOwnProperty('response') && 'rpcerror' == response.response) {
549 code = 'error';
550 error_code = 'rpc_unknown_error';
551 if (response.hasOwnProperty('data') && response.data.hasOwnProperty('code')) {
552 error_code = response.data.code;
553 console.error("UpdraftCentral: RPC: Error occurred ("+error_code+"); data follows");
554 console.log(response.data);
555 response = response.data.data;
556 // A default message for if we don't recognise the code
557 var dash_message = udclion.js_exception_occurred+' ('+error_code+')';
558 // Get the error's own message, if we know about it
559 if (udclion.rpcerrors.hasOwnProperty(error_code)) { dash_message = udclion.rpcerrors[error_code]; }
560
561 if (allow_visual_responses) { UpdraftCentral.dialog.alert('<h2>'+udclion.communications_error+'</h2>'+dash_message); }
562
563 response_callback.call(this, response, code, error_code);
564
565 }
566 }
567
568 if (code == 'error') {
569
570 var msg = udclion.general_js_comms_failure;
571 // This variable doesn't have to be 100% correct - it's use is that a link to a relevant article is shown if it is true
572 var is_comms_failure = true;
573 var title = udclion.error;
574
575 // If the response didn't unwrap, it may be an error response.
576 if (2 == is_site_rpc && 'unwrapper_failure' == error_code && response.hasOwnProperty('code')) { error_code = response.code; }
577
578 if ('json_parse_fail' == error_code) {
579 if (response.indexOf('<html') > -1) {
580 console.error("UpdraftCentral: JSON parse fail: looks like html was returned - remote plugin is probably not installed/inactive/blocked");
581 msg = udclion.general_js_comms_failure;
582 title = udclion.communications_error;
583 }
584 } else if ('response_empty' == error_code || 'http_post_fail' == error_code) {
585 msg = udclion.general_js_comms_failure;
586 title = udclion.communications_error;
587 } else if ('timeout' == error_code) {
588 msg = udclion.comms_failure_timeout;
589 title = udclion.communications_error;
590 } else if ('unauthorized' == error_code) {
591 msg = udclion.comms_failure_unauthorised;
592 title = udclion.communications_error;
593 } else if ('unknown_response' == error_code) {
594 msg = udclion.unknown_response;
595 title = udclion.communications_error;
596 } else if ('cannot_contact_localdev' == error_code) {
597 title = udclion.communications_error;
598 msg = response.message;
599 if (response.hasOwnProperty('request_info') && response.request_info.hasOwnProperty('method') && response.request_info.hasOwnProperty('use_method') && response.request_info.method != response.request_info.use_method) {
600 msg += '<br>'+udclion.localdev_can_work_better_with_https;
601 }
602 } else if ('unexpected_http_code' == error_code && is_site_rpc && response.hasOwnProperty('data') && response.data !== null && response.data.hasOwnProperty('headers') && response.data.headers.hasOwnProperty('www-authenticate') && response.data.headers['www-authenticate'].search(/Digest/i) == 0) {
603 msg = response.message;
604 msg += "<br>"+udclion.digest_auth_not_supported;
605 } else if ('unexpected_http_code' == error_code && is_site_rpc && response.hasOwnProperty('data') && null !== response.data && response.data.hasOwnProperty('response') && response.data.response.hasOwnProperty('code') && 401 == response.data.response.code) {
606 msg = udclion.comms_failure_unauthorised+' <a href="#" class="updraftcentral_site_editdescription">'+udclion.open_site_configuration+'...</a>';
607 } else if (response.hasOwnProperty('message')) {
608 msg = response.message;
609 if (!is_site_rpc) { is_comms_failure = false; }
610 } else {
611 is_comms_failure = false;
612 msg += '<br>'+udclion.error_code+': '+error_code;
613 }
614
615 // ns_error_dom_bad_uri: access to restricted uri denied - Firefox
616 if (response.hasOwnProperty('status') && 401 == response.status) {
617 msg = udclion.comms_failure_unauthorised+' <a href="#" class="updraftcentral_site_editdescription">'+udclion.open_site_configuration+'...</a>';
618 } else if ('ns_error_dom_bad_uri: access to restricted uri denied' == error_code) {
619 msg = udclion.comms_failure_unauthorised_by_browser+' <a href="#" class="updraftcentral_site_editdescription">'+udclion.open_site_configuration+'...</a>';
620 }
621
622 msg = '<p>'+msg+'</p>';
623
624 if (is_comms_failure) {
625 msg += '<p><a href="'+udclion.common_urls.connection_checklist+'">'+udclion.go_here_for_connection_help+'</a></p>';
626 msg += '<p><a href="#" class="updraftcentral_test_other_connection_methods">'+udclion.test_other_connection_methods+'</a></p>';
627 }
628
629 if (response.hasOwnProperty('status') && 200 != response.status && 0 != response.status) {
630 msg += '<p>'+udclion.http_response_status+': '+response.status+'</p>';
631 }
632
633 if (allow_visual_responses) { UpdraftCentral.dialog.alert('<h2>'+title+'</h2>'+msg); }
634 }
635
636 if (is_site_rpc && response.hasOwnProperty('data') && null != response.data) {
637 if (response.data.hasOwnProperty('php_events')) {
638 $.each(response.data.php_events, function(index, logline) {
639 console.log("UpdraftCentral: PHP event on remote side: "+logline);
640 });
641 }
642 if (response.data.hasOwnProperty('caught_output')) {
643 console.log("UpdraftCentral: direct output on remote side: "+response.data.caught_output);
644 }
645 if (response.data.hasOwnProperty('php_events') || response.data.hasOwnProperty('caught_output')) {
646 response.data = response.data.previous_data;
647 }
648 }
649
650 response_callback.call(this, response, code, error_code);
651 }
652
653 /**
654 * Process responses received back from the mothership over AJAX. This will do some processing, and then call process_ajax_response()
655 *
656 * @param {String} response - the response received
657 * @param {Boolean} is_site_rpc - whether it was command to a remote site or not.
658 * @param {ajaxCallback|Boolean} response_callback - callback that will be called with the results of the AJAX call - or, to not call, false
659 * @param {Boolean} [allow_visual_responses=true] - whether or not it is permissible to display UI elements in response to the results (set this to false if the caller wants to handle it internally only). This is just passed on to process_ajax_response
660 *
661 * @returns {Boolean|String} - If the parsing did not turn up any errors, true is return; otherwise, an error code.
662 */
663 function process_direct_ajax_response(response, is_site_rpc, response_callback, allow_visual_responses) {
664
665 allow_visual_responses = ('undefined' === typeof allow_visual_responses) ? true : allow_visual_responses;
666
667 // AJAX via the mothership comes with its results wrapped
668
669 if (response.hasOwnProperty('responsetype') && 'error' == response.responsetype) {
670 if (response.hasOwnProperty('message')) { console.error("UpdraftCentral error via AJAX: "+response.message); }
671 if ('cannot_contact_localdev' == response.code) { response.request_info = { method: method, use_method: use_method} }
672 if (false !== response_callback) {
673 process_ajax_response(response, 'error', response.code, is_site_rpc, response_callback, allow_visual_responses);
674 }
675 return response.code;
676 }
677
678 if (!response.hasOwnProperty('message') && !response.hasOwnProperty('code')) {
679 console.log(response);
680 if (false !== response_callback) {
681 process_ajax_response(response, 'error', 'unknown_response', is_site_rpc, response_callback, allow_visual_responses);
682 }
683 return 'unknown_response';
684 }
685
686 if (updraftcentral_debug_level > 1) {
687 console.log(response.responsetype+': '+response.message);
688 }
689
690 // When doing site RPC, the remote site's reply is in the 'data' attribute
691 if (is_site_rpc) {
692
693 if (response.hasOwnProperty('php_events')) {
694 $.each(response.php_events, function(index, logline) {
695 console.info("UpdraftCentral: PHP event on remote side: "+logline);
696 });
697 }
698
699 if (response.hasOwnProperty('mothership_caught_output')) {
700 console.info("UpdraftCentral: direct output on remote side: "+response.caught_output);
701 }
702
703 // This is set for a successful communication
704 if (response.hasOwnProperty('rpc_response')) {
705 response = response.rpc_response;
706 }
707 }
708
709 if (false !== response_callback) {
710 process_ajax_response(response, 'ok', null, is_site_rpc, response_callback, allow_visual_responses);
711 }
712
713 return true;
714 }
715
716 /**
717 * Sends a remote command via AJAX - either directly, or via the site that this plugin is installed upon.
718 *
719 * @param {String} command - the command to send
720 * @param {*} data - data to send with the remote request
721 * @param {Object|null} $site_row - the jQuery object for the site that the command is for; or, for commands not associated with a site, null
722 * @param {String} [connection_method="direct_default_auth"] - either 'direct_default_auth' (which means to send directly to the destination) or 'via_mothership_encrypting' (which means to send via our PHP-back-end, which then sends the request), or 'via_mothership' which also sends via the PHP-back-end, but does the RSA encryption in the browser. This is not necessarily the same value as inferred from $site_row - we provide it as an extra parameter to make it possible to over-ride - e.g. for diagnostics, or where the browser mixed-content model restricts the choices.
723 * @param {Object|String|null} [spinner_where=null] - jQuery object or CSS identifier indicating where, if anywhere, to add a spinner whilst the call is ongoing
724 * @param {ajaxCallback} response_callback - callback that will be called with the results of the AJAX call
725 * @param {Number} [timeout=30] - the number of seconds to allow before the call times out
726 * @param {Boolean} [allow_visual_responses=true] - whether or not it is permissible to display UI elements in response to the results (set this to false if the caller wants to handle it internally only). This is just passed on to process_ajax_response
727 *
728 * @uses process_ajax_response
729 */
730
731 function send_ajax(command, data, $site_row, connection_method, spinner_where, response_callback, timeout, allow_visual_responses) {
732
733 connection_method = typeof connection_method !== 'undefined' ? connection_method : 'direct_default_auth';
734 timeout = typeof timeout !== 'undefined' ? timeout : 30;
735 spinner_where = typeof spinner_where !== 'undefined' ? spinner_where : null;
736 allow_visual_responses = ('undefined' === typeof allow_visual_responses) ? true : allow_visual_responses;
737
738 // Boil it down to one of 'direct', 'server', 'server_proxies' (i.e. factor out the sub-methods)
739 var ajax_method = ('via_mothership' == connection_method || 'via_mothership_encrypting' == connection_method) ? ('via_mothership' == connection_method ? 'server_proxies' : 'server') : 'direct';
740
741 var is_site_rpc = (null === $site_row) ? false : true;
742
743 if (is_site_rpc) {
744 var unlicensed = $site_row.data('site_unlicensed');
745 if ('undefined' !== typeof unlicensed && unlicensed) {
746 UpdraftCentral.dialog.alert('<h2>'+udclion.error+'</h2>'+udclion.site_unlicensed_message);
747 return;
748 }
749 }
750
751 if (spinner_where) {
752 // $(spinner_where).addClass('updraftcentral_spinner');
753 $(spinner_where).prepend('<div class="updraftcentral_spinner"></div>');
754 }
755
756 if ('direct' == ajax_method && 'https:'== document.location.protocol) {
757 var site_url = $site_row.data('site_url');
758 if (site_url.substring(0, 5).toLowerCase() == 'http:') {
759 // Mixed content policy in all mainstream desktop browsers forbids requests to HTTP from HTTPS domains
760 ajax_method = 'server';
761 }
762 }
763
764 if (updraftcentral_debug_level > 0) {
765 console.log("send_message(ajax_method="+ajax_method+", requested_method="+connection_method+", command="+command+", data(follows))");
766 console.log(data);
767 }
768
769 if ('direct' == ajax_method || 'server_proxies' == ajax_method) {
770
771 if (!is_site_rpc) { throw 'send_ajax() called with direct method ('+connection_method+'), but no site row object passed in'; }
772
773 var ud_rpc = get_site_udrpc($site_row, connection_method);
774
775 ud_rpc.send_message(command, data, timeout, function(response, code, error_code) {
776
777 if (spinner_where) {
778 $(spinner_where).removeClass('updraftcentral_spinner');
779 $(spinner_where).children('.updraftcentral_spinner').remove();
780 }
781
782 if (updraftcentral_debug_level > 2) {
783 console.log("Raw response, pre-processing, follows");
784 console.log(response);
785 }
786
787 var is_site_rpc_flag = ('server_proxies' == ajax_method) ? 2 : 1;
788
789 try {
790 process_ajax_response(response, code, error_code, is_site_rpc_flag, response_callback, allow_visual_responses);
791 } catch (e) {
792 UpdraftCentral.dialog.alert('<h2>'+udclion.error+'</h2>'+udclion.js_exception_occurred+'<br>'+e.toString());
793 console.log(e);
794 }
795 });
796
797
798 } else {
799 // 'server' == ajax_method
800
801 var site_id = 0;
802 if (null !== $site_row) {
803 site_id = $site_row.data('site_id');
804 }
805
806 var ajax_subaction = (is_site_rpc) ? 'site_rpc' : command;
807
808 var ajax_data = (is_site_rpc) ? { command: command, data: data } : data;
809
810 var ajax_options = {
811 type: 'POST',
812 url: udclion.ajaxurl,
813 timeout: timeout * 1000, // In ms
814 headers: {
815 'X-Secondary-User-Agent': 'UpdraftCentral-dashboard.js/'+udclion.udc_version,
816 },
817 data: {
818 action: 'updraftcentral_dashboard_ajax',
819 subaction: ajax_subaction,
820 component: 'dashboard',
821 nonce: udclion.updraftcentral_dashboard_nonce,
822 site_id: site_id,
823 data: ajax_data
824 },
825 dataType: 'text',
826 success: function(response) {
827
828 if (spinner_where) {
829 $(spinner_where).children('.updraftcentral_spinner').remove();
830 // $(spinner_where).removeClass('updraftcentral_spinner');
831 }
832
833 if ('undefined' === typeof response || '' === response) {
834 console.log("UDRPC: the response from the remote site was empty");
835 process_ajax_response(response, 'error', 'response_empty', is_site_rpc, response_callback, allow_visual_responses);
836 return;
837 }
838
839 try {
840 var parsed_response = JSON.parse(response);
841 } catch(e) {
842 console.log(e);
843 console.log(response);
844 process_ajax_response(response, 'error', 'json_parse_fail', is_site_rpc, response_callback, allow_visual_responses);
845 return;
846 }
847
848 response = parsed_response;
849
850 process_direct_ajax_response(response, is_site_rpc, response_callback, allow_visual_responses);
851
852 },
853 error: function(request, status, error_thrown) {
854
855 if (spinner_where) {
856 $(spinner_where).children('.updraftcentral_spinner').remove();
857 // $(spinner_where).removeClass('updraftcentral_spinner');
858 }
859
860 console.error("UpdraftCentral: Error in AJAX operation");
861 console.log(request);
862 console.log(status);
863 // https://api.jquery.com/jquery.ajax/ says: 'When an HTTP error occurs, (this parameter) receives the textual portion of the HTTP status, such as "Not Found" or "Internal Server Error."'
864 // "Unauthorized" is what you get when HTTP authentication is required. "Timeout" when there's a timeout.
865 console.error(error_thrown);
866
867 if ('' == error_thrown) { error_thrown = 'http_post_fail'; }
868
869 if (error_thrown.hasOwnProperty('statusText')) {
870 error_thrown = error_thrown.statusText.toString();
871 }
872
873 if ('function' === typeof error_thrown.toLowerCase) {
874 error_thrown = error_thrown.toLowerCase();
875 } else {
876 try {
877 var tmp = error_thrown.toString().toLowerCase();
878 if (tmp) { error_thrown = tmp; }
879 } catch (e) {
880 }
881 }
882
883 process_ajax_response(request, 'error', error_thrown, is_site_rpc, response_callback, allow_visual_responses);
884 }
885 }
886
887 if (updraftcentral_debug_level > 1) {
888 console.log("UpdraftCentral: jQuery POST: options follow:");
889 console.log(ajax_options);
890 }
891
892 jQuery.ajax(ajax_options);
893
894 }
895
896 }
897
898 /**
899 * Set up menu navigation for each site row item. This should be called after any actions that replace the HTML of row items
900 */
901 function setup_menunav() {
902 // This is no longer needed.
903 //$('#updraftcentral_dashboard .updraft-dropdown-menu').dropit();
904 var how_many_sites = $('#updraftcentral_dashboard_existingsites > .updraftcentral_site_row:not(.updraft_site_unlicensed)').length;
905 $('#updraftcentral_licences_in_use').html(how_many_sites);
906 }
907
908 // Toggle the mobile menu on/off, if at a relevant width
909 $('#updraftcentral_dashboard .updraft-mobile-menu').on('click', function() {
910 var w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
911 // Currently only using the width.
912 // var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
913 if(w <= mobile_width) {
914 $('#updraftcentral_dashboard .updraft-menu-item-links').slideToggle();
915 }
916 });
917
918 /**
919 * Set the section of the dashboard that displays the existing sites to the specified value. All code that wants to update this section should route through here, so that any other associated operations can be carried out.
920 *
921 * @param {String} html - the HTML to place within the site list container in the dashboard
922 */
923 function set_existing_sites_to(html) {
924 // Reset the connection objects, as the IDs and credentials/options may have changed
925 ud_rpcs = [];
926 $('#updraftcentral_dashboard_existingsites').html(html);
927 // Show/hide the relevant buttons/sections for the current tab
928 UpdraftCentral.set_dashboard_mode(true, true);
929 setup_menunav();
930 }
931
932 /*
933 * Used to show or hide the mobile menu on click. Will also toggle when a menu item has been clicked.
934 * Only want this to occur at when the mobile menu is visible to stop the toggle at higher viewports
935 */
936 // Toggle the mobile menu on/off, if at a relevant width
937 $('#updraftcentral_dashboard .updraft-mobile-menu, .updraft-menu-item-links').on('click', function() {
938 var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
939
940 if(width < mobile_width) {
941 $('#updraftcentral_dashboard .updraft-menu-item-links').slideToggle();
942 }
943 });
944
945 /**
946 * Quote the input, so that it is suitable for placing in HTML attributes values
947 * @see https://stackoverflow.com/questions/7753448/how-do-i-escape-quotes-in-html-attribute-values
948 *
949 * @param {String} s - The string to be quoted
950 * @param {boolean} preserveCR - if true, then \r and \n are replaced with an HTML entity; otherwise with \n
951 *
952 * @returns {String} the quoted string
953 */
954 function quote_attribute(s, preserveCR) {
955 preserveCR = preserveCR ? '&#13;' : '\n';
956 return ('' + s) /* Forces the conversion to string. */
957 .replace(/&/g, '&amp;') /* This MUST be the 1st replacement. */
958 .replace(/'/g, '&apos;') /* The 4 other predefined entities, required. */
959 .replace(/"/g, '&quot;')
960 .replace(/</g, '&lt;')
961 .replace(/>/g, '&gt;')
962 /*
963 * You may add other replacements here for HTML only
964 * (but it's not necessary).
965 * Or for XML, only if the named entities are defined in its DTD.
966 */
967 .replace(/\r\n/g, preserveCR) /* Must be before the next replacement. */
968 .replace(/[\r\n]/g, preserveCR);
969 ;
970 }
971
972 /**
973 * Adds a dashboard notice
974 *
975 * @param {string} message - The message text to display
976 * @param {string} [level="notice"] - The level for the notice. Can also start with 'listener_', which is styled as if it were 'info' (but can be over-ridden, as it gets its own classes too)
977 * @param {Number|bool} [remove_after=30000] - The number of milliseconds to remove the notice after; or, 0|false to not remove
978 * @param {Object} [extra_data={}] - Extra data to store with the dashboard notice (via data attributes)
979 * @returns {Object} the jQuery object for the newly created notice
980 */
981 this.add_dashboard_notice = function(message, level, remove_after, extra_data) {
982 remove_after = typeof remove_after !== 'undefined' ? remove_after : 30000;
983 extra_data = typeof extra_data !== 'undefined' ? extra_data : { };
984 level = typeof level !== 'undefined' ? level : 'notice';
985 var type = 'notice';
986 var extra_classes = '';
987
988 if ('listener_' == level.substr(0, 9)) {
989 type = 'listener';
990 extra_classes = 'updraftcentral_listener updraftcentral_listener_'+level.substr(9);
991 extra_data.type = level.substr(9);
992 level = 'info';
993 }
994
995 $container = $('#updraftcentral_notice_container');
996
997 var newnotice_container_opener = '<div class="updraftcentral_notice updraftcentral_notice_new updraftcentral_notice_level_'+level+' '+extra_classes+'"';
998 $.each(extra_data, function(key, val) {
999 newnotice_container_opener += 'data-'+key+'="'+quote_attribute(val)+'"';
1000 });
1001
1002 var $newnotice = $(newnotice_container_opener+'><button type="button" class="updraftcentral_notice_dismiss"></button><div class="updraftcentral_notice_contents">'+message+'</div></div>');
1003 $container.append($newnotice);
1004 if (remove_after) {
1005 $newnotice.slideDown('medium').delay(30000).slideUp('slow', function() {
1006 $(this).remove();
1007 });
1008 } else {
1009 $newnotice.slideDown('medium');
1010 }
1011
1012 return $newnotice;
1013 }
1014
1015 /**
1016 * Creates a special type of dashboard notice which polls for status updates
1017 * @see register_listener_processor
1018 *
1019 * @param {string} type - Listener type (an identifying string) (not shown; stored and used for CSS classes)
1020 * @param {Object} $site_row - a jQuery object identifying the site row that the listener is associated with
1021 * @param {string} message - HTML to be placed in the dashboard notice
1022 * @param {*} [data={}] - Data associated with the listener (which will be stored in an HTML data attribute)
1023 * @returns {Object} the jQuery object for the newly created notice
1024 *
1025 */
1026 this.create_dashboard_listener = function(type, $site_row, message, data) {
1027 data = ('undefined' === typeof data) ? {} : data;
1028 data.site_url = $site_row.data('site_url');
1029 data.site_id = $site_row.data('site_id');
1030 return this.add_dashboard_notice('<h2>'+$site_row.data('site_description')+'</h2>'+message, 'listener_'+type, false, data);
1031 }
1032
1033 // Only trigger a removal if the close button is directly in the notice. This allows other sub-elements to re-use the style class.
1034 $('#updraftcentral_notice_container').on('click', '.updraftcentral_notice > .updraftcentral_notice_dismiss', function() {
1035 $(this).parents('.updraftcentral_notice').clearQueue().slideUp('slow', function() { $(this).remove(); });
1036 });
1037
1038 /**
1039 * Set up the dashboard, by hiding things that don't belong in the currently active tab
1040 *
1041 * @param {String|Boolean} new_mode=true - the mode to switch to. These correspond to keys for items placed in the main menu via the updraftcentral_main_navigation_items filter. If set to true, then it will choose the current mode (only useful if setting force to true).
1042 * @param {Boolean} [force=false] - run the commands to set up the mode, even if it appears to be the current mode (useful for resetting the state within the mode)
1043 */
1044 this.set_dashboard_mode = function (new_mode, force) {
1045
1046 force = ('undefined' === typeof force) ? false : true;
1047
1048 var current_mode = $('#updraftcentral_dashboard').data('updraftcentral_mode');
1049
1050 if (true === new_mode) { new_mode = current_mode; }
1051
1052 if (!force && new_mode == current_mode) { return; }
1053
1054 if (current_mode) { $('#updraftcentral_dashboard').removeClass('updraftcentral_mode_'+current_mode); }
1055
1056 $('#updraftcentral_dashboard_existingsites_container .updraftcentral_row_extracontents').empty();
1057
1058 // Show all sites again
1059 $('#updraftcentral_dashboard_existingsites > .updraftcentral_site_row, #updraftcentral_dashboard_existingsites > .updraftcentral_row_divider').show();
1060
1061 $('#updraftcentral_dashboard').data('updraftcentral_mode', new_mode);
1062 $('#updraftcentral_dashboard').addClass('updraftcentral_mode_'+new_mode);
1063 $('#updraft-menu-item-'+current_mode).removeClass('updraft-menu-item-links-active');
1064 $('#updraft-menu-item-'+new_mode).addClass('updraft-menu-item-links-active');
1065
1066 // Since there exist classes for both "show everywhere except <here>" and "hide everywhere except here", you could, of course, add CSS classes that result in contradictory instructions. The outcome of doing so is not defined.
1067 $('#updraftcentral_dashboard .updraftcentral-hide-in-other-tabs:not(.updraftcentral-show-in-tab-'+new_mode+'), #updraftcentral_dashboard .updraftcentral-hide-in-tab-'+new_mode).hide();
1068 $('#updraftcentral_dashboard .updraftcentral-show-in-tab-'+new_mode+' .updraftcentral-hide-in-tab-initially').hide();
1069 $('#updraftcentral_dashboard .updraftcentral-show-in-tab-'+new_mode+', #updraftcentral_dashboard .updraftcentral-show-in-other-tabs:not(.updraftcentral-hide-in-tab-'+new_mode+')').slideDown(1);
1070 }
1071
1072 $('.updraftcentral_mode_actions .updraftcentral_action_choose_another_site').click(function() {
1073 UpdraftCentral.set_dashboard_mode(true, true);
1074 });
1075
1076 $('#updraft-central-navigation .updraft-menu-item').click(function() {
1077 var item_dom_id = $(this).attr('id');
1078 if ('undefined' === typeof item_dom_id) { return; }
1079 if ('updraft-menu-item-' != item_dom_id.substring(0, 18)) {
1080 console.log("UDCentral: menu item without the ID in the expected format");
1081 console.log(this);
1082 return;
1083 }
1084
1085 var new_mode = item_dom_id.substring(18);
1086 UpdraftCentral.set_dashboard_mode(new_mode);
1087
1088 });
1089
1090 $('#updraftcentral_dashboard .updraftcentral_action_box .updraftcentral_action_manage_sites').click(function() {
1091 UpdraftCentral.set_dashboard_mode('sites');
1092 });
1093
1094 $('#updraftcentral_modal').on('click', '#updraftcentral_addsite_expertoptions_show', function(e) {
1095 $(this).slideUp();
1096 $('#updraftcentral_modal #updraftcentral_editsite_expertoptions .initially-hidden').show();
1097 e.preventDefault();
1098 });
1099
1100 /**
1101 * Do any processing necessary with the passed information about current status
1102 *
1103 * @params {Object} status_info - any recognised properties will be processed
1104 */
1105 function process_sites_status_info(status_info) {
1106 if (status_info.hasOwnProperty('how_many_licences_in_use')) {
1107 $('.updraftcentral_licences_in_use').html(status_info.how_many_licences_in_use);
1108 }
1109 if (status_info.hasOwnProperty('how_many_licences_available')) {
1110 var display = (status_info.how_many_licences_available < 0) ? '&#8734;' : status_info.how_many_licences_available;
1111 $('.updraftcentral_licences_total').html(display);
1112 }
1113 }
1114
1115 $('#updraftcentral_modal').on('change', '#updraftcentral_site_connection_method', function() {
1116 var site_connection_method = $('#updraftcentral_site_connection_method').val();
1117
1118 if (null == site_connection_method) { return; }
1119
1120 if (site_connection_method.substring(0, 7) == 'direct_' && 'https:' == document.location.protocol) {
1121 $('#updraftcentral_site_connection_method_message').show().html(udclion.http_must_go_via_mothership);
1122 } else {
1123 $('#updraftcentral_site_connection_method_message').hide();
1124 }
1125 });
1126
1127 $('#updraftcentral_dashboard_newsite').click(function() {
1128
1129 var advanced_site_options_html = UpdraftCentral.get_advanced_site_options_html({ http_username: '', http_password: ''});
1130
1131 UpdraftCentral.open_modal(udclion.add_site, UpdraftCentral.template_replace('sites-add-new-modal', { advanced_options: advanced_site_options_html } ), function() {
1132
1133 var key = $('#updraftcentral_addsite_key').val();
1134 UpdraftCentral.close_modal();
1135
1136 if ('undefined' === typeof key || key === null || key === '') { return; }
1137
1138 var extra_site_info = UpdraftCentral.get_serialized_options('#updraftcentral_modal #updraftcentral_editsite_expertoptions .expert_option');
1139 var send_cors_headers = $('#updraftcentral_modal #updraftcentral_site_send_cors_headers').is(':checked') ? 1 : 0;
1140 var connection_method = $('#updraftcentral_modal #updraftcentral_site_connection_method').val();
1141
1142 send_ajax('newsite', { key: key, extra_site_info: extra_site_info, send_cors_headers: send_cors_headers, connection_method: connection_method }, null, 'via_mothership_encrypting', '#updraftcentral_dashboard_existingsites', function(resp, code, error_code) {
1143
1144 if ('ok' == code) {
1145
1146 if (resp.hasOwnProperty('message')) {
1147 add_dashboard_notice(resp.message, 'info');
1148 if (resp.hasOwnProperty('sites_html')) {
1149 set_existing_sites_to(resp.sites_html);
1150 } else {
1151 console.log("Expected sites_html data not found:");
1152 console.log(resp);
1153 }
1154 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
1155 }
1156
1157 if (resp.hasOwnProperty('key_needs_sending')) {
1158
1159 var site_id = resp.key_needs_sending.key_site_id;
1160 var site_ajax_url = resp.key_needs_sending.url;
1161 var $site_row = $('#updraftcentral_dashboard_existingsites .updraftcentral_site_row[data-site_id="'+site_id+'"');
1162 var site_remote_public_key = resp.key_needs_sending.remote_public_key;
1163
1164 $($site_row).prepend('<div class="updraftcentral_spinner"></div>');
1165
1166 var send_key_url = site_ajax_url+'&action=updraftcentral_receivepublickey&updraft_key_index='+encodeURIComponent(resp.key_needs_sending.updraft_key_index)+'&public_key='+encodeURIComponent(UpdraftCentral.base64_encode(site_remote_public_key));
1167 var win = window.open(send_key_url, '_blank', 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=600,height=320');
1168 win.focus();
1169 return;
1170
1171 // This requires CORS code on the receiving side
1172 /*
1173 $.post(site_ajax_url, {
1174 action: 'updraftcentral_receivepublickey',
1175 public_key: site_remote_public_key,
1176 updraft_key_index: resp.key_needs_sending.updraft_key_index
1177 }, function(response) {
1178 if (spinner_where) {
1179 $($site_row).children('.updraftcentral_spinner').remove();
1180 }
1181 // Obsolete - parse_response() no longer exists
1182 var resp = parse_response(response);
1183 if (false !== resp) {
1184 if (resp.code == 'ok') {
1185 console.log("UDCentral: successfully sent public key to remote site");
1186 } else {
1187 console.log("UDCentral: failed to send public key to remote site");
1188 console.log(resp);
1189 if (resp.code == 'already_have' || resp.code == 'already_have') {
1190 add_dashboard_notice(udclion[resp.code], 'error');
1191 } else {
1192 add_dashboard_notice(udclion.unknown_response, 'error');
1193 }
1194 }
1195 }
1196
1197 });
1198 */
1199
1200 }
1201 }
1202 });
1203 }, udclion.add_site, function() {
1204 $('#updraftcentral_modal #updraftcentral_site_send_cors_headers').prop('checked', true);
1205 // $('#updraftcentral_modal #updraftcentral_addsite_key').focus();
1206 } , false);
1207 });
1208
1209 this.register_row_clicker('.updraftcentral_site_adddescription', function($row_parent) {
1210 open_site_configuration($row_parent);
1211 });
1212 $('#updraftcentral_modal').on('click', '.updraftcentral_site_editdescription', function(e) {
1213 e.preventDefault();
1214 open_site_configuration(UpdraftCentral.$row_parent);
1215 });
1216
1217 /**
1218 * Gets the HTML fragment for advanced site editing options
1219 *
1220 * @param {Object} values - the values to pass to the template
1221 *
1222 * @returns {String} - the HTML
1223 */
1224 this.get_advanced_site_options_html = function(values) {
1225 return UpdraftCentral.template_replace('sites-advanced-site-options', values);
1226 }
1227
1228 /**
1229 * Get serialized options within a specified selector. Includes making sure that checkboxes are included when not checked.
1230 *
1231 * @param {String} selector - the jQuery selector to use to locate the options
1232 *
1233 * @returns {String} - the serialized options
1234 */
1235 this.get_serialized_options = function(selector) {
1236 var form_data = $(selector).serialize();
1237 $.each($(selector+' input[type=checkbox]')
1238 .filter(function(idx){
1239 return $(this).prop('checked') == false
1240 }),
1241 function(idx, el){
1242 //attach matched element names to the form_data with chosen value.
1243 var empty_val = '0';
1244 form_data += '&' + $(el).attr('name') + '=' + empty_val;
1245 });
1246 return form_data;
1247 }
1248
1249 /**
1250 * Opens the site connection test dialog for the specified site
1251 *
1252 * @param {Object} - the jQuery row object for the site whose configuration is to be edited
1253 */
1254 this.open_connection_test = function($row_parent) {
1255
1256 var site_url = $row_parent.data('site_url');
1257 var site_id = $row_parent.data('site_id');
1258
1259 var current_connection_method = $row_parent.data('connection_method');
1260
1261 if ('via_mothership_encrypting' == current_connection_method) {
1262 UpdraftCentral.dialog.alert('<h2>'+udclion.test_connection_methods+'</h2><p>'+udclion.test_not_possible_in_current_mode+'</p>');
1263 // <p><a href="#" class="updraftcentral_site_editdescription">'+udclion.open_site_configuration+'...</a></p>
1264 return;
1265 }
1266
1267 var current_method_simplified = ('direct_jquery_auth' == current_connection_method || 'direct_default_auth' == current_connection_method || 'direct_default_auth' == current_connection_method) ? 'direct' : current_connection_method;
1268
1269 UpdraftCentral.open_modal(udclion.test_connection_methods, UpdraftCentral.template_replace('sites-connection-test', { site_url: site_url }), true, false, function() {
1270
1271 var direct_method_can_be_attempted = true;
1272 if ('https:'== document.location.protocol) {
1273 if (site_url.substring(0, 5).toLowerCase() == 'http:') {
1274 direct_method_can_be_attempted = false;
1275 }
1276 }
1277
1278 if (direct_method_can_be_attempted) {
1279
1280 $('#updraftcentral_modal .connection-test-direct .connection-test-result').html('');
1281
1282 send_ajax('ping', null, $row_parent, 'direct_default_auth', '#updraftcentral_modal .connection-test-direct .connection-test-result', function(response, code, error_code) {
1283 if (updraftcentral_debug_level > 0) {
1284 console.log("Result follows for 'direct_default_auth' method:");
1285 console.log(response);
1286 }
1287 if ('ok' == code) {
1288 var new_html = '<span class="connection-test-succeeded">'+udclion.succeeded+'</span> ';
1289 if ('direct' == current_method_simplified) {
1290 new_html += udclion.current_method+' '+udclion.best_method+' '+udclion.recommend_keep;
1291 } else {
1292 new_html += udclion.best_method+' '+udclion.recommend_use+' <a href="#" class="connection-test-switch" data-site_id="'+site_id+'" data-connection_method="direct_default_auth">'+udclion.switch_to+'...</a>';
1293 }
1294 $('#updraftcentral_modal .connection-test-direct .connection-test-result').html(new_html);
1295 } else {
1296 $('#updraftcentral_modal .connection-test-direct .connection-test-result').html('<span class="connection-test-failed">'+udclion.failed+' ('+error_code+')</span>');
1297 }
1298 }, 30, false);
1299
1300 } else {
1301 $('#updraftcentral_modal .connection-test-direct .connection-test-result').html(udclion.not_possible_browser_restrictions);
1302 }
1303
1304 $('#updraftcentral_modal .connection-test-via_mothership .connection-test-result').html('');
1305 send_ajax('ping', null, $row_parent, 'via_mothership', '#updraftcentral_modal .connection-test-via_mothership .connection-test-result', function(response, code, error_code) {
1306 $('#updraftcentral_modal .connection-test-via_mothership .connection-test-result').html(code);
1307 if (updraftcentral_debug_level > 0) {
1308 console.log("Result follows for 'via_mothership' method:");
1309 console.log(response);
1310 }
1311 if ('ok' == code) {
1312 var new_html = '<span class="connection-test-succeeded">'+udclion.succeeded+'</span> ';
1313 if ('via_mothership' != current_connection_method) {
1314 new_html += '<a href="#" class="connection-test-switch" data-site_id="'+site_id+'" data-connection_method="via_mothership">'+udclion.switch_to+'...</a>';
1315 } else {
1316 new_html += udclion.current_method;
1317 }
1318 $('#updraftcentral_modal .connection-test-via_mothership .connection-test-result').html(new_html);
1319 } else {
1320 $('#updraftcentral_modal .connection-test-via_mothership .connection-test-result').html('<span class="connection-test-failed">'+udclion.failed+' ('+error_code+')</span>');
1321 }
1322 }, 30, false);
1323
1324 $('#updraftcentral_modal .connection-test-via_mothership_encrypting .connection-test-result').html('');
1325 send_ajax('ping', null, $row_parent, 'via_mothership_encrypting', '#updraftcentral_modal .connection-test-via_mothership_encrypting .connection-test-result', function(response, code, error_code) {
1326 if (updraftcentral_debug_level > 0) {
1327 console.log("Result follows for 'via_mothership_encrypting' method:");
1328 console.log(response);
1329 }
1330 if ('ok' == code) {
1331 var new_html = '<span class="connection-test-succeeded">'+udclion.succeeded+'</span> ';
1332 if ('via_mothership_encrypting' != current_connection_method) {
1333 new_html += '<a href="#" class="connection-test-switch" data-site_id="'+site_id+'" data-connection_method="via_mothership_encrypting">'+udclion.switch_to+'...</a>';
1334 } else {
1335 new_html += udclion.current_method;
1336 }
1337 $('#updraftcentral_modal .connection-test-via_mothership_encrypting .connection-test-result').html(new_html);
1338 } else {
1339 $('#updraftcentral_modal .connection-test-via_mothership_encrypting .connection-test-result').html('<span class="connection-test-failed">'+udclion.failed+' ('+error_code+')</span>');
1340 }
1341 }, 30, false);
1342
1343 }, true, 'modal-lg');
1344 }
1345
1346 /**
1347 * Opens the site configuration dialog for the specified site
1348 *
1349 * @param {Object} - the jQuery row object for the site whose configuration is to be edited
1350 */
1351 this.open_site_configuration = function($row_parent) {
1352 var site_url = $row_parent.data('site_url');
1353 var http_username = $row_parent.data('http_username');
1354 if ('undefined' === typeof http_username) { http_username = ''; }
1355
1356 var http_password = $row_parent.data('http_password');
1357 if ('undefined' === typeof http_password) { http_password = ''; }
1358
1359 var connection_method = $row_parent.data('connection_method');
1360 if ('undefined' === typeof connection_method) { connection_method = 'direct_default_auth'; }
1361
1362 var http_authentication_method = $row_parent.data('http_authentication_method');
1363 if ('undefined' === typeof http_authentication_method) { http_authentication_method = 'basic'; }
1364
1365 var existing_description = $row_parent.data('site_description');
1366 if (existing_description == site_url) { existing_description = ''; }
1367
1368 var send_cors_headers = $row_parent.data('send_cors_headers');
1369 if ('undefined' === typeof send_cors_headers || send_cors_headers) { send_cors_headers = 1; }
1370
1371 var advanced_site_options_html = UpdraftCentral.get_advanced_site_options_html({http_username: http_username, http_password: http_password});
1372
1373 UpdraftCentral.open_modal(udclion.edit_site_configuration, UpdraftCentral.template_replace('sites-edit-configuration', { site_url: site_url, advanced_options: advanced_site_options_html }, { existing_description: existing_description } ), function() {
1374
1375 var description = $('#updraftcentral-edit-site-description').val();
1376
1377 var send_cors_headers = $('#updraftcentral_modal #updraftcentral_site_send_cors_headers').is(':checked') ? 1 : 0;
1378
1379 var connection_method = $('#updraftcentral_modal #updraftcentral_site_connection_method').val();
1380
1381 var site_id = $row_parent.data('site_id');
1382 if (!site_id) { return; }
1383
1384 UpdraftCentral.close_modal();
1385
1386 var extra_site_info = UpdraftCentral.get_serialized_options('#updraftcentral_modal .expert_option');
1387
1388 send_ajax('edit_site_configuration', { site_id: site_id, description: description, extra_site_info: extra_site_info, send_cors_headers: send_cors_headers, connection_method: connection_method }, null, 'via_mothership_encrypting', '#updraftcentral_dashboard_existingsites', function(resp, code, error_code) {
1389
1390 if ('ok' == code) {
1391
1392 if (resp.hasOwnProperty('message')) { add_dashboard_notice(resp.message); }
1393
1394 if (resp.hasOwnProperty('sites_html')) {
1395 set_existing_sites_to(resp.sites_html);
1396 setup_menunav();
1397 } else {
1398 console.log(resp);
1399 add_dashboard_notice(udclion.unknown_response, 'error');
1400 }
1401 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
1402 }
1403 });
1404
1405 }, udclion.edit, function() {
1406 $('#updraftcentral_modal #updraftcentral_site_connection_method').val(connection_method).change();
1407 if (send_cors_headers) { $('#updraftcentral_modal #updraftcentral_site_send_cors_headers').prop('checked', true); }
1408 $('#updraftcentral_modal #updraftcentral_addsite_http_authentication_method').val(http_authentication_method);
1409 }, false);
1410 }
1411
1412 this.register_row_clicker('.updraftcentral_site_delete', function($row_parent) {
1413 UpdraftCentral.dialog.confirm('<h2>'+udclion.remove_site+'</h2><p>'+$row_parent.data('site_url')+'</p><p>'+udclion.really_delete_site+'</p>', function(result) {
1414 if (!result) return;
1415 var site_id = UpdraftCentral.$row_parent.data('site_id');
1416 if (!site_id) { return; }
1417 $row_parent.slideUp('slow');
1418
1419 send_ajax('delete_site', { site_id: site_id }, null, 'via_mothership_encrypting', '#updraftcentral_dashboard_existingsites', function(resp, code, error_code) {
1420 if ('ok' == code) {
1421 if (resp.hasOwnProperty('message')) {
1422 add_dashboard_notice(resp.message);
1423 }
1424 if (resp.hasOwnProperty('sites_html')) {
1425 set_existing_sites_to(resp.sites_html);
1426 } else {
1427 console.log(resp);
1428 add_dashboard_notice(udclion.unknown_response, 'error');
1429 }
1430 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
1431 }
1432 });
1433
1434 });
1435 });
1436
1437 /**
1438 * @callable RPCCallback
1439 * @param {Object} response - the data returned by the RPC call
1440 * @param {string} code - the code returned by the RPC call
1441 * @param {string|null} error_code - the error code returned by the RPC call (if any)
1442 */
1443
1444 /**
1445 * Send a command to the remote site. This is a very thin wrapper around send_ajax.
1446 *
1447 * @param {string} rpc_command - the command to send
1448 * @param {*} data - the data to send with the command
1449 * @param {Object} $site_row - the jQuery object for the row of the site that the request is being sent to
1450 * @param {RPCCallback} - function to call with the results
1451 * @param {Object|null} spinner_where - jQuery object indicating where any spinner should be shown
1452 * @param {Number} [timeout=30] - the number of seconds for the timeout on the HTTP call
1453 *
1454 * @uses send_ajax
1455 */
1456 this.send_site_rpc = function(rpc_command, data, $site_row, callback, spinner_where, timeout) {
1457 timeout = 'undefined' !== typeof timeout ? timeout : 30;
1458 var site_id = $site_row.data('site_id');
1459 if (!site_id) {
1460 console.log("UpdraftCentral: sent_site_rpc("+rpc_command+") command sent, but site ID could not be identified from the row (follows)");
1461 console.log($site_row);
1462 }
1463
1464 var connection_method = $site_row.data('connection_method');
1465
1466 if ('undefined' === typeof spinner_where || null === spinner_where || false === spinner_where) { spinner_where = $site_row; }
1467
1468 try {
1469 return send_ajax(rpc_command, data, $site_row, connection_method, spinner_where, callback);
1470 } catch (e) {
1471 if (spinner_where) {
1472 $(spinner_where).children('.updraftcentral_spinner').remove();
1473 }
1474 // add_dashboard_notice(udclion.js_exception_occurred+'<br>'+e.toString(), 'error');
1475 UpdraftCentral.dialog.alert('<h2>'+udclion.error+'</h2>'+udclion.js_exception_occurred+'<br>'+e.toString());
1476 console.log(e);
1477 }
1478 }
1479
1480 this.register_row_clicker('.row_siteinfo', function($row_parent) {
1481 UpdraftCentral.send_site_rpc('core.site_info', null, $row_parent, function(response, code, error_code) {
1482 if (updraftcentral_debug_level > 1) {
1483 console.log("send_site_rpc(site_info): parsed response follows");
1484 console.log(response);
1485 }
1486 if ('ok' == code) {
1487 if (false !== response) {
1488 var versions = response.data.versions;
1489 var bloginfo = response.data.bloginfo;
1490 var url = UpdraftCentral.sanitize_html(bloginfo.url);
1491 var name = UpdraftCentral.sanitize_html(bloginfo.name);
1492 // 'This site is running WordPress version %s (PHP %s, MySQL %s) and UpdraftPlus version %s (UDRPC version %s)'
1493 // var message = sprintf(udclion.what_remote_running, UpdraftCentral.sanitize_html(versions.wp), UpdraftCentral.sanitize_html(versions.php), UpdraftCentral.sanitize_html(versions.mysql), UpdraftCentral.sanitize_html(versions.ud), UpdraftCentral.sanitize_html(versions.udrpc_php));
1494 // add_dashboard_notice(message, 'info');
1495 // N.B. By default, open_modal() sanitizes the body.
1496 var ud_version = versions.ud;
1497 if ('none' == ud_version) { ud_version = udclion.updraftplus.version_none; }
1498 var message = sprintf(udclion.what_remote_running, versions.wp, versions.php, versions.mysql, ud_version, versions.udrpc_php);
1499 UpdraftCentral.open_modal(
1500 UpdraftCentral.sanitize_html(bloginfo.name),
1501 UpdraftCentral.template_replace('dashboard-siteinfo', { url: url, message: message, phpinfo: udclion.phpinfo }),
1502 null,
1503 false
1504 );
1505 }
1506 }
1507 });
1508 });
1509
1510 $('#updraftcentral_modal').on('click', '.updraftcentral_siteinfo_results .phpinfo', function(e) {
1511 e.preventDefault();
1512 UpdraftCentral.send_site_rpc('core.phpinfo', null, UpdraftCentral.$row_parent, function(response, code, error_code) {
1513
1514 if ('ok' == code && response.data) {
1515 var output = '';
1516 $.each(response.data, function(name, section) {
1517 output += "<h3>"+name+"</h3>\n"+'<table>'+"\n";
1518 $.each(section, function(key, val) {
1519 if (val.constructor === Array) {
1520 output += "<tr><td>"+key+"</td><td>"+val[0]+"</td><td>"+val[1]+"</td></tr>\n";
1521 } else if (typeof val === 'string') {
1522 if ($.isNumeric(key)) {
1523 output += "<tr><td></td><td>"+val+"</td></tr>\n";
1524 } else {
1525 output += "<tr><td>"+key+"</td><td>"+val+"</td></tr>\n";
1526 }
1527 } else {
1528 console.log("UpdraftCentral: phpinfo: Unrecognised output for key "+key+" (follows)");
1529 console.log(val);
1530 }
1531 });
1532 output += "</table>\n";
1533 });
1534
1535 // N.B. open_modal() by default sanitizes the body data
1536 UpdraftCentral.open_modal(udclion.phpinfo, '<div id="updraftcentral_phpinfo_results">'+output+'</div>', null, false, null, true, 'modal-lg');
1537 }
1538 }, $(this));
1539 });
1540
1541 /**
1542 * Opens a new browser portal at the specified URL
1543 * @param {Object} $row_parent - jQuery object for the site row
1544 * @param {Object|string|null} [redirect_to=null] - where to redirect to (defaults to the network admin)
1545 * @param {Object} [spinner_where=$row_parent] - jQuery object indicating where to put the site row.
1546 */
1547 this.open_browser_at = function($row_parent, redirect_to, spinner_where) {
1548 redirect_to = typeof redirect_to !== 'undefined' ? redirect_to : null;
1549 spinner_where = ('undefined' === typeof spinner_where) ? $row_parent : spinner_where;
1550 UpdraftCentral.send_site_rpc('core.get_login_url', redirect_to, $row_parent, function(response, code, error_code) {
1551 if ('ok' == code && false !== response && response.hasOwnProperty('data')) {
1552 var login_url = response.data.login_url;
1553 var win = window.open(login_url, '_blank');
1554 if ('undefined' != typeof win) {
1555 win.focus();
1556 } else {
1557 UpdraftCentral.dialog.alert('<h2>'+udclion.open_new_window+'</h2>'+udclion.window_may_be_blocked);
1558 }
1559 }
1560 }, spinner_where);
1561 }
1562
1563 $('#updraftcentral_dashboard_existingsites_container').on('click', '.updraftcentral_site_dashboard', function() {
1564 var $row_parent = $(this).parents('.updraftcentral_site_row');
1565 UpdraftCentral.open_browser_at($row_parent);
1566 });
1567
1568 /**
1569 * Toggle whether or not UpdraftCentral is in "full screen" mode
1570 */
1571 this.toggle_fullscreen = function() {
1572 // https://github.com/private-face/jquery.fullscreen
1573 if ($.fullscreen.isFullScreen()) {
1574 $('footer').show();
1575 $.fullscreen.exit();
1576 } else {
1577 $('footer').hide();
1578 $('#updraftcentral_dashboard').fullscreen({overflow: 'scroll', toggleClass: 'updraft-fullscreen' });
1579 }
1580 }
1581
1582 $('#updraftcentral_dashboard .updraft-central-logo img').dblclick(function() { UpdraftCentral.toggle_fullscreen(); });
1583
1584 $('#updraft-central-navigation .updraft-full-screen').on('click', function() { UpdraftCentral.toggle_fullscreen(); });
1585
1586 $('#updraft-central-navigation .updraftcentral-help').on('click', function() {
1587 UpdraftCentral.dialog.alert(UpdraftCentral.template_replace('dashboard-help', { uc_version: udclion.updraftcentral_version+': '+udclion.udc_version, running_on: UpdraftCentral.version_info_as_text() }));
1588 });
1589
1590 /**
1591 * Return a string with information on the current installation
1592 */
1593 this.version_info_as_text = function() {
1594 return 'WP/'+udclion.wp_version+' PHP/'+udclion.php_version+' MySQL/'+udclion.mysql_version+' Curl/'+udclion.curl_version;
1595 }
1596
1597 $('#updraft-central-navigation .updraftcentral-settings').on('click', function() {
1598
1599 UpdraftCentral.open_modal(udclion.settings, UpdraftCentral.template_replace('dashboard-settings', { uc_version: udclion.updraftcentral_version+': '+udclion.udc_version, running_on: UpdraftCentral.version_info_as_text() }), function() {
1600 var new_debugging_level = $('#updraftcentral_debug_level').val();
1601 if (new_debugging_level >= 0 && new_debugging_level <=3) {
1602 UpdraftCentral.set_debug_level(new_debugging_level);
1603 }
1604 UpdraftCentral.close_modal();
1605 }, udclion.save_settings, function() {
1606 $('#updraftcentral_debug_level').val(updraftcentral_debug_level);
1607 });
1608
1609 });
1610
1611 // Refresh dashicon rotates after it has been clicked - stops when the settings are refreshed.
1612 $('.updraftcentral_row_extracontents').on('click', '.dashicons-image-rotate', function() {
1613 $('.dashicons-image-rotate').addClass('dashicon-image-rotating');
1614 });
1615
1616 /**
1617 * Returns the result of filling in the specified Handlebars (http://handlebarsjs.com) template with the provided values
1618 *
1619 * @param {String} template_name - the name of the Handlebars template, based (though it is filterable) on the path within the 'templates' directory, with slashes replaced by dashes. e.g. templates/dashboard/something.handlebars.html is accessed via a name of 'dashboard-something'
1620 * @param {Object} [vars] - an object with properties (and corresponding values) corresponding to the named variables in the template and the values to replace them. N.B. The udclion object is always passed through (as udclion).
1621 * @param {Object} [attr_vars] - an optional object with properties (and corresponding values) corresponding to the named variables in the template and the values to replace them, but for which the values will first be sanitized for use in HTML attributes. This may not be necessary on input which isn't user-supplied (e.g. its format may already be known to be attribute-safe).
1622 *
1623 * @returns {String} The template with values filled in
1624 */
1625 this.template_replace = function(template_name, vars, attr_vars) {
1626 vars = ('undefined' === typeof vars) ? {} : vars;
1627 if (!UpdraftCentral_Handlebars.hasOwnProperty(template_name)) {
1628 console.log("UDCentral: UpdraftCentral_Handlebars template not found: "+template_name);
1629 console.log(UpdraftCentral_Handlebars);
1630 }
1631 if ('undefined' !== typeof attr_vars) {
1632 $.each(attr_vars, function(k, v) {
1633 vars[k] = quote_attribute(v);
1634 });
1635 }
1636 vars.udclion = udclion;
1637 return UpdraftCentral_Handlebars[template_name](vars);
1638 }
1639
1640 // Use a new browser portal for any clicks to updraftplus.com
1641 $('#updraftcentral_modal').on('click', 'a', function(e) {
1642 var href = $(this).attr('href');
1643 if ('undefined' === typeof href) { return; }
1644 if (href.substring(0, 23) == 'https://updraftplus.com' || href.substring(0, 23) == 'http://updraftplus.com') {
1645 e.preventDefault();
1646 var win = window.open(href, '_blank');
1647 win.focus();
1648 }
1649 });
1650
1651 /**
1652 * Compiles any Handlebars templates that have been passed into the page. By default, they are pre-compiled; but compilation happens in-browser when in developer mode.
1653 */
1654 function compile_handlebars_templates() {
1655 // Initialise Handlebars.templates - it may not already exist
1656 UpdraftCentral_Handlebars = (typeof UpdraftCentral_Handlebars === 'undefined') ? {} : UpdraftCentral_Handlebars;
1657 if (!udclion.hasOwnProperty('handlebars')) return;
1658 if (udclion.handlebars.hasOwnProperty('compile')) {
1659 $.each(udclion.handlebars.compile, function(template_name, source) {
1660 console.log("UpdraftCentral: in developer mode: compile template: "+template_name);
1661 UpdraftCentral_Handlebars[template_name] = Handlebars.compile(source);
1662 });
1663 }
1664 }
1665
1666 compile_handlebars_templates();
1667
1668 setup_menunav();
1669
1670 set_dashboard_mode('sites');
1671
1672 if ('undefined' !== typeof Modernizr && !Modernizr.lastchild) {
1673 console.log("UDCentral: Unsupported web browser");
1674 $('#updraftcentral_dashboard_loading').fadeOut();
1675 $('#updraftcentral_updraftplus_actions, #updraftcentral_sites_actions, #updraftcentral_dashboard_existingsites_container').remove();
1676 this.add_dashboard_notice(udclion.unsupported_browser, 'error', false);
1677 } else {
1678
1679 $('#updraftcentral_dashboard_loading').fadeOut();
1680 $('#updraftcentral_dashboard_existingsites_container').fadeIn();
1681
1682 if (udclion.hasOwnProperty('show_licence_counts') && udclion.show_licence_counts) { $('.updraftcentral_licence_info').show(); }
1683
1684 // Refresh the sites list every 24 hours
1685 setInterval(function(){
1686 send_ajax('sites_html', null, null, 'via_mothership_encrypting', '#updraftcentral_dashboard_existingsites', function(resp, code, error_code) {
1687 if ('ok' == code) {
1688 if (resp.hasOwnProperty('sites_html')) {
1689 set_existing_sites_to(resp.sites_html);
1690 } else {
1691 console.log("Expected sites_html data not found:");
1692 console.log(resp);
1693 }
1694 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
1695 }
1696 });
1697 }, 86400000);
1698
1699 }
1700
1701 // Remove any indicated notices that came pre-printed on the page
1702 $('#updraftcentral_notice_container .updraftcentral_notice.remove_after_load').delay(30000).slideUp('slow', function() {
1703 $(this).remove();
1704 });
1705
1706 return this;
1707 }
1708
1709 // https://github.com/richadams/jquery-tripleclick/
1710 // @author Rich Adams <rich@richadams.me>
1711 // Implements a triple-click event. Click (or touch) three times within 1s on the element to trigger.
1712 // Licence: https://creativecommons.org/licenses/by/3.0/ (verified at 2-Feb-2016)
1713
1714 ;(function($)
1715 {
1716 // Default options
1717 var defaults = {
1718 threshold: 1000, // ms
1719 }
1720
1721 function tripleHandler(event)
1722 {
1723 var $elem = jQuery(this);
1724
1725 // Merge the defaults and any user defined settings.
1726 settings = jQuery.extend({}, defaults, event.data);
1727
1728 // Get current values, or 0 if they don't yet exist.
1729 var clicks = $elem.data("triclick_clicks") || 0;
1730 var start = $elem.data("triclick_start") || 0;
1731
1732 // If first click, register start time.
1733 if (clicks === 0) { start = event.timeStamp; }
1734
1735 // If we have a start time, check it's within limit
1736 if (start != 0
1737 && event.timeStamp > start + settings.threshold)
1738 {
1739 // Tri-click failed, took too long.
1740 clicks = 0;
1741 start = event.timeStamp;
1742 }
1743
1744 // Increment counter, and do finish action.
1745 clicks += 1;
1746 if (clicks === 3)
1747 {
1748 clicks = 0;
1749 start = 0;
1750 event.type = "tripleclick";
1751
1752 // Let jQuery handle the triggering of "tripleclick" event handlers
1753 if (jQuery.event.handle === undefined) {
1754 jQuery.event.dispatch.apply(this, arguments);
1755 }
1756 else {
1757 // for jQuery before 1.9
1758 jQuery.event.handle.apply(this, arguments);
1759 }
1760 }
1761
1762 // Update object data
1763 $elem.data("triclick_clicks", clicks);
1764 $elem.data("triclick_start", start);
1765 }
1766
1767 var tripleclick = $.event.special.tripleclick =
1768 {
1769 setup: function(data, namespaces)
1770 {
1771 $(this).bind("touchstart click.triple", data, tripleHandler);
1772 },
1773 teardown: function(namespaces)
1774 {
1775 $(this).unbind("touchstart click.triple", data, tripleHandler);
1776 }
1777 };
1778 })(jQuery);
1779