PluginProbe
UpdraftCentral Dashboard / 0.8.13
UpdraftCentral Dashboard v0.8.13
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.8.13, at js/dashboard.js

3,944 lines 155.4 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 UpdraftCentral.init();
4 });
5
6 // Only needed for IE9 support - http://caniuse.com/#feat=console-basic
7 // Console-polyfill. MIT license.
8 // https://github.com/paulmillr/console-polyfill
9 // Make it safe to do console.log() always.
10 (function(global) {
11 'use strict';
12 global.console = global.console || {};
13 var con = global.console;
14 var prop, method;
15 var empty = {};
16 var dummy = function() {};
17 var properties = 'memory'.split(',');
18 var methods = ('assert,clear,count,debug,dir,dirxml,error,exception,group,' +
19 'groupCollapsed,groupEnd,info,log,markTimeline,profile,profiles,profileEnd,' +
20 'show,table,time,timeEnd,timeline,timelineEnd,timeStamp,trace,warn').split(',');
21 while (prop = properties.pop()) if (!con[prop]) con[prop] = empty;
22 while (method = methods.pop()) if ('function' !== typeof con[method]) con[method] = dummy;
23 // Using `this` for web workers & supports Browserify / Webpack.
24 })(typeof window === 'undefined' ? this : window);
25
26 /**
27 * This is the callback on the Paginator class
28 *
29 * @callback paginatorCallback
30 * @param {int} current_page
31 */
32 /**
33 * Create paginator markup and manages the active page.
34 *
35 * @constructor
36 * @param {Object} location - A jQuery DOM object used for placing the paginator
37 * @param {Object} page_info - an object describing the paginator
38 * @param {int} page_info.current_page - the current active page on the paginator
39 * @param {int} page_info.total_pages - the amount of pages the paginator should show
40 * @param {paginatorCallback} page_change - what should be done after there's a page change
41 * @returns {void}
42 */
43 function UpdraftCentral_Paginator(location, page_info, page_change) {
44 var self = this;
45
46 var current = page_info.current_page;
47 var total = page_info.total_pages;
48 var callback = page_change;
49
50 /**
51 * Appends the paginator to the DOM and sets the active page
52 *
53 * @returns {void}
54 */
55 function init(){
56 // Adding condition here to make sure that the paginator navigation
57 // will only be visible if the total pages is more than 1.
58 if (total > 1) {
59 append(location);
60 set_active(current);
61 }
62 }
63 init();
64
65 /**
66 * Sets the active page
67 *
68 * @param {int} page_number - the page number that is to be active
69 * @returns {void}
70 */
71 function set_active(page_number){
72 self.element.find('.page').each(function(index, element) {
73
74 jQuery(this).removeClass('page_active');
75 jQuery(this).attr('aria-selected', false);
76
77 if (jQuery(this).data('page') === page_number) {
78 jQuery(this).addClass('page_active');
79 jQuery(this).attr('aria-selected', true);
80 }
81 })
82
83 self.element.find('.page_prev').removeClass('disabled');
84 self.element.find('.page_next').removeClass('disabled');
85 if (page_number === 1) {
86 self.element.find('.page_prev').addClass('disabled');
87 } else if (page_number === total) {
88 self.element.find('.page_next').addClass('disabled');
89 }
90 trigger();
91 }
92
93 /**
94 * Sets the next page as active
95 *
96 * @returns {void}
97 */
98 function next(){
99 if (current < total) {
100 current++;
101 set_active(current);
102 }
103 }
104
105 /**
106 * Sets the previous page as active
107 *
108 * @returns {void}
109 */
110 function prev(){
111 if (current > 1) {
112 current--;
113 set_active(current);
114 }
115 }
116
117 /**
118 * Go to a page and set it as active
119 *
120 * @param {int} page_number - the page number that is to be active
121 * @returns {void}
122 */
123 function go_to(page_number){
124 if (page_number !== current) {
125 current = page_number;
126 set_active(page_number);
127 }
128 }
129
130 /**
131 * Inserts the paginator markup to the DOM
132 *
133 * @param {string} _location - a selector for where the paginator should be
134 * @returns {void}
135 */
136 function append(_location){
137 var pages = [];
138 for (var i = 1; i <= total; i++) {
139 pages.push(i);
140 }
141
142 self.element = jQuery(UpdraftCentral.template_replace('dashboard-paginator', { pages:pages }));
143 self.element.appendTo(_location);
144
145 self.element.on('click', 'a', function(e) {
146 e.preventDefault();
147
148 if (jQuery(this).hasClass('active')) {
149 return;
150 }
151
152 if (jQuery(this).hasClass('page_prev')) {
153 prev();
154 } else if (jQuery(this).hasClass('page_next')) {
155 next();
156 } else if (jQuery(this).hasClass('page')) {
157 var page_number = jQuery(this).data('page');
158 go_to(page_number);
159 }
160
161 });
162 }
163
164 /**
165 * Set what should happen on a page change
166 *
167 * @param {paginatorCallback} _callback - a call back that triggers after a page change
168 * @returns {void}
169 */
170 this.page_change = function(_callback) {
171 callback = _callback;
172 }
173 /**
174 * Triggers a jquery event on the paginator element and executes the page_change callback
175 *
176 * @fires page_change
177 * @returns {void}
178 */
179 function trigger(){
180 self.element.trigger("page_change", current);
181 if (jQuery.isFunction(callback)) {
182 callback(current);
183 }
184 }
185 }
186
187 var UpdraftCentral = function() {
188
189 // This is just used internally to log more things. Set to 0 to turn it off (which won't necessarily prevent all console logging).
190 // It's not completely systematic/consistent. Logging has been added in an ad hoc manner during development/testing to help with debugging.
191 // This gets passed through from the PHP constant UPDRAFTCENTRAL_DEBUG_LEVEL
192 var updraftcentral_debug_level = ('undefined' === typeof udclion || !udclion.hasOwnProperty('debug_level')) ? 0 : udclion.debug_level;
193 var listener_poll_interval = (updraftcentral_debug_level > 0) ? 5000 : 10000;
194
195 var mobile_width = 670;
196 var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
197 var $ = jQuery;
198
199 var modal_action_callback;
200
201 // 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)
202 var $site_row;
203
204 var self = this;
205 var default_collapse_width = 60;
206 this.ajax_request_processing = false;
207 this.uc_action_triggered = false;
208 this.uc_action_data = [];
209 this.event_trigger = new UpdraftCentral_Collection();
210 this.uc_module;
211
212 /**
213 * Initializes UpdraftCentral's functions
214 *
215 * @returns {void}
216 */
217 this.init = function() {
218 // Initialize and load recorder
219 UpdraftCentral.init_recorder();
220
221 // Initialize keyboard shortcuts
222 UpdraftCentral.init_keyboard_shortcuts();
223
224 // Initialize ajax request listener
225 UpdraftCentral.init_process_listener();
226
227 // Initialize collapse/expand menu tooltips
228 UpdraftCentral.init_tooltip();
229
230 // Purge localStorage of old data
231 UpdraftCentral.storage_purge();
232
233 // Set requested mode
234 UpdraftCentral.set_requested_mode();
235 }
236
237 /**
238 * Sets the initial mode on page load based from the requested module
239 *
240 * @returns {void}
241 */
242 this.set_requested_mode = function() {
243 var url = window.location.href;
244 var params = [],
245 menu_item = '';
246
247 if (-1 !== url.indexOf('?')) {
248 params = url.slice(url.indexOf('?')+1).split('&');
249 if (params.length) {
250 for (var i=0, param=''; i<params.length; i++) {
251 param = params[i].split('=');
252 if ('uc_module' === param[0] && param[1].length) {
253 // Check whether the requested module actually exists, if so then
254 // set the current dashboard mode to that particular area.
255 menu_item = $('#updraft-menu-item-'+param[1]);
256
257 if ('undefined' !== typeof menu_item && menu_item && menu_item.length) {
258 self.uc_module = param[1];
259 $('#updraftcentral_dashboard_existingsites').on('updraftcentral_sites_loaded', function(event, data) {
260 set_dashboard_mode(data.module);
261 });
262 }
263 }
264 }
265 }
266 }
267
268 if ('undefined' == typeof self.uc_module) self.uc_module = false;
269 }
270
271 this.plupload_init = function(module, site_filter) {
272
273 // We bail if we don't get either 'plugin' or 'theme' as the submitted module.
274 if ('undefined' == typeof module) return;
275
276 // Creates the uploader and pass the config
277 var config = JSON.parse(udclion[module].plupload_config);
278 var uploader = new plupload.Uploader(config);
279 var selected_sites = new UpdraftCentral_Collection();
280 var credentials = new UpdraftCentral_Credentials();
281
282 // Checks if browser supports drag and drop upload, makes some css adjustments if necessary
283 uploader.bind('Init', function(up) {
284 var uploaddiv = $('#plupload-upload-ui');
285 if (up.features.dragdrop) {
286 uploaddiv.addClass('drag-drop');
287 $('#drag-drop-area').bind('dragover.wp-uploader', function() {
288 uploaddiv.addClass('drag-over');
289 }).bind('dragleave.wp-uploader, drop.wp-uploader', function() {
290 uploaddiv.removeClass('drag-over');
291 });
292
293 } else {
294 uploaddiv.removeClass('drag-drop');
295 $('#drag-drop-area').unbind('.wp-uploader');
296 }
297 });
298
299 uploader.init();
300
301 // A file was added in the queue
302 uploader.bind('FilesAdded', function(up, files) {
303 // Limit number of files for upload set in config
304 if (config.hasOwnProperty('max_file_count') && config.max_file_count) {
305 if (files.length) {
306 var count = parseInt(config.max_file_count);
307 files = files.splice(0, count);
308 }
309
310 if (uploader.files.length) {
311 $.each(uploader.files, function(index, file) {
312 if (file.id !== files[0].id) {
313 uploader.removeFile(file);
314 up.removeFile(file);
315 }
316 });
317 $('#filelist').empty();
318 }
319 }
320
321 plupload.each(files, function(file) {
322 if (! /\.zip$/.test(file.name)) {
323 UpdraftCentral_Library.dialog.alert('<h2>'+udclion[module].install_zip_heading+'</h2><p>'+file.name+': '+udclion[module].notarchive+'</p>');
324 uploader.removeFile(file);
325 up.removeFile(file);
326 return;
327 }
328
329 // a file was added, you may want to update your DOM here...
330 $('#filelist').append('<div class="file" id="'+file.id+'"><b>'+file.name+'</b> (<span>'+plupload.formatSize(0)+'</span>/'+plupload.formatSize(file.size)+') <div class="fileprogress"></div></div>');
331 });
332
333 up.refresh();
334 up.start();
335 });
336
337 // Updates progress of the upload process
338 uploader.bind('UploadProgress', function(up, file) {
339 $('#' + file.id + " .fileprogress").width(file.percent + "%");
340 $('#' + file.id + " span").html(plupload.formatSize(parseInt(file.size * file.percent / 100)));
341 });
342
343 // Displays error when found
344 uploader.bind('Error', function(up, error) {
345 UpdraftCentral_Library.dialog.alert('<h2>'+udclion[module].install_zip_heading+'</h2><p>'+udclion[module].uploaderr+' (code '+error.code+') : '+error.message+' - '+udclion[module].makesure+'</p>');
346 });
347
348 /**
349 * Shows the activation options dialog. Primarily showing the user a message whether
350 * he or she wishes to activate the plugin or theme after a successful installation/upload
351 *
352 * @param {object} up Uploader instance
353 * @param {object} file An object currently representing the file in process
354 *
355 * @return {void}
356 */
357 function show_pre_install_message(up, file) {
358 var sites = selected_sites.get_items();
359 if (sites.length) {
360 UpdraftCentral_Library.dialog.confirm('<h2>'+udclion[module].install_zip_heading+'</h2><p>'+udclion[module].install_zip_message+'</p>', function(result) {
361 var activate = (!result) ? 0 : 1;
362
363 $.extend(up.settings.multipart_params, {
364 'site_id': UpdraftCentral.$site_row.data('site_id'),
365 'activate': activate,
366 'sites': UpdraftCentral_Library.base64_encode(JSON.stringify(sites)),
367 });
368
369 var $location = UpdraftCentral.$site_row.find('.updraftcentral_row_extracontents');
370 UpdraftCentral.set_loading($location);
371
372 // Continue upload
373 file.status = plupload.UPLOADING;
374 up.trigger('UploadFile', file);
375 }, null, { confirm: udclion.yes_activate, cancel: udclion.do_not_activate });
376 } else {
377 reset_uploader(up, file);
378 }
379 }
380
381 /**
382 * Resets the uploader UI
383 *
384 * @param {object} up Uploader instance
385 * @param {object} file An object currently representing the file in process
386 *
387 * @return {void}
388 */
389 function reset_uploader(up, file) {
390 $('#filelist').find('div#'+file.id).remove();
391 uploader.removeFile(file);
392 up.removeFile(file);
393 up.stop();
394 up.start();
395 }
396
397 /**
398 * Check site requirements and load credentials. Allows input of credentials
399 * if it is deemed needed when installing plugin or theme to the remote site
400 *
401 * @param {UpdraftCentral_Site} site The website to check and load credentials from
402 *
403 * @return {Promise}
404 */
405 function load_site_creds(site) {
406 var deferred = jQuery.Deferred();
407 // Check to see if credentials is needed when installing/uploading plugins/themes
408 credentials.load_credentials(site).then(function(response) {
409 var show_form = false;
410 if (response.hasOwnProperty('request_filesystem_credentials')) {
411 var sysfolders = response.request_filesystem_credentials;
412 if (sysfolders.hasOwnProperty(module+'s') && sysfolders[module+'s']) {
413 show_form = true;
414 }
415 }
416
417 if (show_form) {
418 // Shows form where user is asked to input his/her FTP credentials that is
419 // needed to install/upload the plugin file.
420 credentials.get_credentials(site).then(function(response) {
421 // If we now have a valid credentials, we copy the relevant fields/flags
422 // to the current site before we proceed with the process.
423 site.site_credentials = response.site_credentials;
424 site.save_credentials_in_browser = response.save_credentials_in_browser;
425
426 if (site.save_credentials_in_browser) {
427 UpdraftCentral.storage_set('filesystem_credentials_'+site.site_hash, site.site_credentials, true);
428 }
429
430 deferred.resolve(site);
431 }).fail(function(response) {
432 deferred.resolve({});
433 });
434 } else {
435 deferred.resolve(site);
436 }
437 }).fail(function(response) {
438 deferred.resolve(site);
439 });
440
441 return deferred.promise();
442 }
443
444 /**
445 * Checks and loads credentials of the selected sites
446 *
447 * @param {arrays} items An array containing the destination sites when installing plugins or themes
448 * @param {Deferred} deferred jQuery's Deferred object
449 *
450 * @return {Promise}
451 */
452 function check_site_creds(items, deferred) {
453 if ('undefined' == typeof deferred) var deferred = jQuery.Deferred();
454 if (items.length) {
455 var site_row = items.shift();
456
457 if ('undefined' !== typeof site_row) {
458 var site = new UpdraftCentral_Site(site_row);
459 if (!selected_sites.exists(site.id)) {
460 load_site_creds(site).then(function(response) {
461 // Add site id and creds to the selected sites collection to
462 // be pass along with the upload request
463 if (response.hasOwnProperty('id') && response.id) {
464 selected_sites.add(response.id, {
465 id: response.id,
466 description: response.site_description,
467 filesystem_credentials: response.site_credentials
468 });
469 }
470
471 check_site_creds(items, deferred);
472 });
473 }
474 }
475 } else {
476 deferred.resolve();
477 }
478
479 return deferred.promise();
480 }
481
482 // Pre-upload housekeeping
483 uploader.bind('BeforeUpload', function(up, file) {
484 var credentials = new UpdraftCentral_Credentials();
485 var selection = site_filter.get_selected_sites();
486
487 if (selection.hasOwnProperty('sites') && selection.sites.count()) {
488 var site_rows = selection.sites;
489 var up_status_container = $('#uc-install-status');
490
491 selected_sites.clear();
492 up_status_container.append('<span id="load-site-creds-msg">'+udclion[module].creds_check+'</span>');
493 up_status_container.append('<div class="injected-spinner updraftcentral_spinner"></div>');
494
495 check_site_creds(site_rows.get_items()).then(function(response) {
496 up_status_container.find('span#load-site-creds-msg').remove();
497 up_status_container.find('div.injected-spinner').remove();
498 show_pre_install_message(up, file);
499 });
500 } else {
501 // We normally won't reach this line since if no sites have been selected
502 // using the filter box it will automatically default to the currently manage site where the
503 // uploader interface is shown. We just add this line here just in case.
504 reset_uploader(up, file);
505 }
506
507 // Hold process until user confirms option
508 return false;
509 });
510
511 // Checks upload status. Primarily for files that were uploading in chunks.
512 uploader.bind('ChunkUploaded', function(up, file, response) {
513 // N.B. Uncomment below line if you want to track the number of bytes uploaded for every
514 // chunk request. Good for debugging purposes. I just added it just in case.
515 // console.log("Chunk uploaded.", result.offset, "of", result.total, "bytes.");
516
517 // Checking chunk result for error. If error is found then stop the upload process.
518 var result = JSON.parse(response.response);
519 var previous_status = file.status,
520 site_description, resp,
521 errors = '', error_count = 0,
522 error_display_limit = 3, message;
523
524 if (result.hasOwnProperty('e') && result.e) {
525 if (UpdraftCentral.get_debug_level() > 0) {
526 console.log(udclion.error+': '+udclion[module].install_zip_heading.toLowerCase()+' ('+file.name+'); '+udclion.message+' ('+result.e+');');
527 }
528 UpdraftCentral_Library.dialog.alert('<h2>'+udclion[module].install_zip_heading+'</h2><p>'+vsprintf(udclion[module].upload_failed, [file.name, '<br/>', result.e])+'</p>');
529
530 up.removeFile(file);
531 if (plupload.UPLOADING == previous_status && plupload.STARTED == up.state) {
532 up.stop();
533 up.start();
534 }
535 } else {
536 $.each(result, function(index, data) {
537 site_description = data.site_description;
538 resp = data.response;
539
540 if ((resp.hasOwnProperty('error') && resp.error) || (resp.hasOwnProperty('responsetype') && 'error' == resp.responsetype)) {
541 message = udclion[module].upload_cutoff;
542 if (resp.hasOwnProperty('message') && resp.message) {
543 message = resp.message;
544 } else if (resp.hasOwnProperty('data') && resp.data) {
545 message = resp.data;
546 }
547
548 if (UpdraftCentral.get_debug_level() > 0) {
549 console.log(udclion.error+': '+udclion[module].install_zip_heading.toLowerCase()+' ('+file.name+'); '+site_description+': '+message);
550 }
551
552 if (error_display_limit > error_count) {
553 errors += site_description+': '+message+'<br/>';
554 error_count++;
555 }
556
557 previous_status = file.status;
558 up.removeFile(file);
559
560 if (plupload.UPLOADING == previous_status && plupload.STARTED == up.state) {
561 up.stop();
562 up.start();
563 }
564 }
565 });
566
567 if (error_count > 0) {
568 UpdraftCentral_Library.dialog.alert('<h2>'+udclion[module].install_zip_heading+'</h2><p>'+vsprintf(udclion[module].upload_failed, [file.name, '<br/>', errors])+'</p>');
569 }
570 }
571 });
572
573 // A file was uploaded. Upload process has completed.
574 uploader.bind('FileUploaded', function(up, file, response) {
575 var $location = UpdraftCentral.$site_row.find('.updraftcentral_row_extracontents');
576 UpdraftCentral.done_loading($location);
577
578 if (response.status == '200') {
579 try {
580 var result = JSON.parse(response.response);
581 var success_count = 0,
582 installed_name = '',
583 errors = '',
584 error_count = 0,
585 error_display_limit = 3;
586
587 if (result.hasOwnProperty('e') && result.e) {
588 if (UpdraftCentral.get_debug_level() > 0) {
589 console.log(udclion.error+': '+udclion[module].install_zip_heading.toLowerCase()+' ('+file.name+'); '+udclion.message+' ('+result.e+');');
590 }
591 errors = result.e;
592 } else {
593 var site_description, resp, error_message, data;
594 $.each(result, function(index, data) {
595 site_description = data.site_description;
596 resp = data.response;
597
598 if (resp.hasOwnProperty('installed') && resp.installed) {
599 installed_name = resp.installed_data.Name;
600 success_count++;
601 } else {
602 if (resp.hasOwnProperty('message') && resp.message) {
603 if (UpdraftCentral.get_debug_level() > 0) {
604 console.log(udclion.error+': '+udclion[module].install_zip_heading.toLowerCase()+' ('+file.name+'); '+site_description+': '+resp.message);
605 }
606 error_message = resp.message;
607 } else {
608 error_message = sprintf(udclion[module].general_error, JSON.stringify(resp));
609 if (resp.hasOwnProperty('error') && resp.error && resp.hasOwnProperty('code') && resp.code) {
610 data = resp.data;
611
612 if (data && data.hasOwnProperty('message') && data.message) {
613 error_message = data.message;
614 } else if ('string' === typeof data && data.length) {
615 error_message = data;
616 } else if ('undefined' !== typeof udclion[module][resp.code]) {
617 error_message = udclion[module][resp.code];
618 } else {
619 error_message = sprintf(udclion[module].general_error, resp.code);
620 }
621 }
622
623 if (UpdraftCentral.get_debug_level() > 0) {
624 console.log(udclion.error+': '+udclion[module].install_zip_heading.toLowerCase()+' ('+file.name+'); '+site_description+': '+error_message);
625 }
626 }
627
628 if (error_display_limit > error_count) {
629 errors += site_description+': '+error_message+'<br/>';
630 error_count++;
631 }
632 }
633 });
634 }
635
636 if (success_count !== result.length) {
637 UpdraftCentral_Library.dialog.alert('<h2>'+udclion[module].install_zip_heading+'</h2><p>'+vsprintf(udclion[module].upload_failed, [file.name, '<br/>', errors])+'</p>');
638 } else {
639 var settings = up.settings.multipart_params;
640 var action_message = udclion[module].installed;
641
642 if (settings.hasOwnProperty('activate') && settings.activate) {
643 action_message = udclion[module].installed_activated;
644 }
645
646 var message = vsprintf(udclion[module].install_success_message, [action_message, '"'+installed_name+'"']);
647 UpdraftCentral_Library.dialog.alert('<h2>'+udclion[module].install_zip_heading+'</h2><p>'+message+'</p>');
648 }
649
650 reset_uploader(up, file);
651 } catch (err) {
652 // Log error and response objects in console. Helps in debugging on what went wrong.
653 if (UpdraftCentral.get_debug_level() > 0) {
654 console.log(udclion.error+': '+udclion[module].install_zip_heading.toLowerCase()+' ('+file.name+'); '+site_description+' - '+udclion[module].jsonnotunderstood);
655 console.log(err);
656 console.log(response);
657 }
658 reset_uploader(up, file);
659 }
660
661 } else {
662 // Log response objects in console. Helps in debugging on what went wrong.
663 if (UpdraftCentral.get_debug_level() > 0) {
664 console.log(udclion.error+': '+udclion[module].install_zip_heading.toLowerCase()+' ('+file.name+'); '+site_description+' ('+udclion[module].details_follows+'):');
665 console.log(response);
666 }
667 reset_uploader(up, file);
668 }
669 });
670 }
671
672 /**
673 * Initilizes the collapse/expand menu tooltips
674 *
675 * @returns {void}
676 */
677 this.init_tooltip = function() {
678 var collapse_icon = $('#updraft-central-sidebar-button span.arrow-left');
679 collapse_icon.data('animation', false);
680 collapse_icon.tooltip({
681 trigger: 'hover',
682 placement: 'right',
683 title: udclion.collapse_menu
684 });
685
686 var expand_icon = $('#updraft-central-sidebar-button span.arrow-right');
687 expand_icon.data('animation', false);
688 expand_icon.tooltip({
689 trigger: 'hover',
690 placement: 'right',
691 title: udclion.expand_menu
692 });
693 }
694
695 /**
696 * Fetches metadata from the wordpress.org API (https://codex.wordpress.org/WordPress.org_API), or via our local cache
697 *
698 * @param {string} type - either 'plugin' or 'theme' (otherwise, results are undefined)
699 * @param {string} slug - the plugin or theme slug (i.e. not the file path)
700 * @param {*} passback - this gets passed back to the callback function
701 * @param {metadata_callback} callback - in the event of a successful retrieval, this function is called with the results
702 * @return {void}
703 */
704 this.get_wporg_metadata = function(type, slug, passback, callback) {
705 var wp_org_plugin_json_api = 'https://api.wordpress.org/plugins/info/1.1/';
706 var wp_org_theme_json_api = 'https://api.wordpress.org/themes/info/1.1/';
707
708 // Cache for 10 minutes
709 var from_storage = UpdraftCentral.storage_get('wporg_api_'+type+'_'+slug, 600);
710
711 if (from_storage && from_storage.hasOwnProperty('name')) {
712 callback.call(this, from_storage, passback);
713 return;
714 }
715
716 var api_url = wp_org_plugin_json_api;
717 if ('theme' == type) {
718 api_url = wp_org_theme_json_api;
719 }
720
721 var fields = {
722 short_description: true,
723 icons: true
724 }
725 if ('theme' === type) {
726 fields = {
727 description: true,
728 sections: true,
729 rating: true,
730 ratings: true,
731 downloaded: true,
732 downloadlink: true,
733 last_updated: true,
734 screenshot_url: true,
735 parent: true,
736 }
737 }
738
739 jQuery.getJSON(api_url, {
740 action: type+'_information',
741 request: {
742 slug: slug,
743 fields: fields
744 }
745 }, function(data, status) {
746 if ('success' == status) {
747 UpdraftCentral.storage_set('wporg_api_'+type+'_'+slug, data, true);
748 callback.call(this, data, passback);
749 }
750 });
751 }
752
753 /**
754 * Searches wordpress.org for the given keyword
755 *
756 * @param {string} type - either 'plugin' or 'theme' (otherwise, results are undefined)
757 * @param {string} keyword - the string or keyword to search for
758 * @param {function} callback - in the event of a successful retrieval, this function is called with the results
759 * @param {integer} page - the current page to retrieve the items from
760 * @param {integer} limit - the number of items to return after the search
761 *
762 * @return {void}
763 */
764 this.search_wporg = function(type, keyword, callback, page, limit) {
765 var api_url = 'https://api.wordpress.org/'+type+'s/info/1.1/';
766 var fields = {
767 sections: false,
768 added: false,
769 tags: false,
770 compatibility: false,
771 donate_link: false,
772 icons: true
773 }
774
775 if ('theme' === type) {
776 fields = {
777 description: true,
778 sections: false,
779 rating: true,
780 ratings: true,
781 downloaded: true,
782 downloadlink: true,
783 last_updated: true,
784 screenshot_url: true,
785 parent: true
786 }
787 }
788
789 limit = 'undefined' !== typeof limit ? limit : 10;
790 page = 'undefined' !== typeof page ? page : 1;
791
792 jQuery.getJSON(api_url, {
793 action: 'query_'+type+'s',
794 request: {
795 search: keyword,
796 per_page: limit,
797 page: page,
798 fields: fields
799 }
800 }, function(data, status) {
801 if ('success' == status) {
802 callback.call(this, data);
803 }
804 });
805 }
806
807 /**
808 * Saves user defined timeout
809 *
810 * @param {integer} timeout The value to set as timeout (in seconds)
811 * @param {object} $location A jquery object representing the container where the spinner will be displayed
812 *
813 * @return {object} jQuery promise object
814 */
815 this.save_timeout = function(timeout, $location) {
816 var deferred = $.Deferred();
817
818 UpdraftCentral.send_ajax('save_timeout', { timeout: timeout }, null, 'via_mothership_encrypting', $location, function(resp, code, error_code) {
819 if ('ok' === code) {
820 if (resp.hasOwnProperty('message')) {
821 if ('success' === resp.message) {
822 deferred.resolve();
823 } else {
824 deferred.reject();
825 }
826 }
827 }
828 });
829
830 return deferred.promise();
831 }
832
833 /**
834 * Saves updraftcentral user defined settings
835 *
836 * @param {object} settings The settings to save.
837 * @param {object} $location A jquery object representing the container where the spinner will be displayed
838 *
839 * @return {object} jQuery promise object
840 */
841 this.save_settings = function(settings, $location) {
842 var deferred = $.Deferred();
843
844 UpdraftCentral.send_ajax('save_settings', settings, null, 'via_mothership_encrypting', $location, function(resp, code, error_code) {
845 if ('ok' === code) {
846 if (resp.hasOwnProperty('message')) {
847 if ('success' === resp.message) {
848 deferred.resolve();
849 } else {
850 deferred.reject();
851 }
852 }
853 }
854 });
855
856 return deferred.promise();
857 }
858
859 /**
860 * Checks whether the user has "write" privilege to the remote website's directory. If not, then we'll ask the user
861 * for their FTP Credentials to successfully install the desired entity (e.g plugin, theme or WP core).
862 *
863 * N.B. Calling this API will automatically display the request credentials dialog where the user
864 * is asked to provide his FTP credentials. So, there's no longer need to create or load the form manually.
865 *
866 * @param {Object} $site_row The jQuery object representing the current site selected.
867 * @param {String} directory Directory entity that we need to check if credentials is required (e.g 'plugins', 'themes' or 'core')
868 * @return {object} - A jQuery promise object
869 */
870 this.maybe_ask_credentials = function($site_row, directory) {
871 var deferred = jQuery.Deferred();
872 var credentials = new UpdraftCentral_Credentials();
873 var site = new UpdraftCentral_Site($site_row);
874
875 credentials.load_credentials(site).then(function(response) {
876 var requests = response.request_filesystem_credentials;
877
878 if ('undefined' !== typeof requests[directory] && requests[directory]) {
879 credentials.get_credentials(site).then(function(response) {
880 deferred.resolve({
881 site: site,
882 credentials_required: true,
883 credentials: response.site_credentials,
884 store_credentials: response.save_credentials_in_browser
885 });
886 }).fail(function(result) {
887 deferred.reject(result);
888 });
889 } else {
890 deferred.resolve({
891 site: site,
892 credentials_required: false
893 });
894 }
895 }).fail(function(result) {
896 deferred.reject(result);
897 });
898
899 return deferred.promise();
900 }
901
902 /**
903 * Checks whether the plugin is installed and activated on the remote website
904 *
905 * @param {Object} $site_row The jQuery object representing the current site selected.
906 * @param {String} plugin_name The name of the plugin to check
907 * @param {String} plugin_slug Optional. The slug of the plugin to check in case the name check fails
908 * @return {Object} A jQuery promise
909 */
910 this.is_plugin_active = function($site_row, plugin_name, plugin_slug) {
911 var deferred = $.Deferred();
912 var param = {
913 plugin: plugin_name
914 }
915
916 if ('undefined' !== typeof plugin_slug && plugin_slug) {
917 param.slug = plugin_slug;
918 }
919
920 UpdraftCentral.send_site_rpc('plugin.is_plugin_installed', param, $site_row, function(response, code, error_code) {
921 if ('ok' === code && !response.data.error) {
922 deferred.resolve(response.data);
923 } else {
924 deferred.reject(response);
925 }
926 });
927
928 return deferred.promise();
929 }
930
931 /**
932 * Activates the plugin on the remote website
933 *
934 * @param {Object} $site_row The jQuery object representing the current site selected.
935 * @param {String} plugin_name The name of the plugin to activate
936 * @param {String} plugin_slug The slug of the plugin to activate in case the name check fails
937 * @return {Object} A jQuery promise
938 */
939 this.activate_plugin = function($site_row, plugin_name, plugin_slug) {
940 var deferred = $.Deferred();
941 var param = {
942 plugin: plugin_name
943 }
944
945 if ('undefined' !== typeof plugin_slug && plugin_slug) {
946 param.slug = plugin_slug;
947 }
948
949 UpdraftCentral.send_site_rpc('plugin.activate_plugin', param, $site_row, function(response, code, error_code) {
950 if ('ok' === code && !response.data.error) {
951 deferred.resolve(response.data);
952 } else {
953 deferred.reject(response);
954 }
955 });
956
957 return deferred.promise();
958 }
959
960 /**
961 * Download, install and activates the plugin on the remote website
962 *
963 * @param {Object} $site_row The jQuery object representing the current site selected.
964 * @param {String} plugin_name The name of the plugin to install and activate
965 * @param {String} plugin_slug The slug of the plugin to install and activate
966 * @return {Object} A jQuery promise
967 */
968 this.install_activate_plugin = function($site_row, plugin_name, plugin_slug) {
969 var deferred = $.Deferred();
970
971 UpdraftCentral.maybe_ask_credentials($site_row, 'plugins').then(function(response) {
972
973 // Store newly entered credentials to the browser if the user opted to.
974 if (response.credentials_required && response.store_credentials) {
975 UpdraftCentral.storage_set('filesystem_credentials_'+response.site.site_hash, response.credentials, true);
976 }
977
978 var param = {
979 plugin: plugin_name,
980 slug: plugin_slug,
981 filesystem_credentials: response.credentials
982 }
983
984 UpdraftCentral.send_site_rpc('plugin.install_activate_plugin', param, $site_row, function(response, code, error_code) {
985 if ('ok' === code && !response.data.error) {
986 deferred.resolve(response.data);
987 } else {
988 deferred.reject(response);
989 }
990 });
991
992 }).fail(function(response) {
993 deferred.reject(response);
994 });
995
996 return deferred.promise();
997 }
998
999 /**
1000 * Registers listener for ajax processing events
1001 *
1002 * @returns {void}
1003 */
1004 this.init_process_listener = function() {
1005 $(document).ajaxStop(function() {
1006 if (0 === $.active) {
1007 self.ajax_request_processing = false;
1008 self.uc_action_data = [];
1009 self.event_trigger.clear();
1010 if (updraftcentral_debug_level > 0) {
1011 console.log('init_process_listener (ajaxStop): ajax_request_processing=false, uc_action_data=[], event_trigger=cleared; (reset flag/vars for in-progress blocking).');
1012 }
1013 }
1014 });
1015
1016 $(document).ajaxSend(function(event, request, settings) {
1017 self.ajax_request_processing = true;
1018 if (updraftcentral_debug_level > 0) {
1019 console.log('init_process_listener (ajaxSend): ajax_request_processing=true; (setting flag for in-progress blocking).');
1020 }
1021 });
1022
1023 $('#updraftcentral_dashboard').on('updraftcentral_dialog_opened', function(event) {
1024 if ($.fullscreen.isFullScreen()) {
1025 var dashboard_fullscreen = $('#updraftcentral_dashboard.updraft-fullscreen');
1026 var backdrop = dashboard_fullscreen.find('div.modal-backdrop');
1027 if (0 === backdrop.length) {
1028 $(document.body).find('.modal-backdrop.show').appendTo(dashboard_fullscreen);
1029 }
1030 }
1031 });
1032
1033 $('#updraftcentral_dashboard').on('updraftcentral_dialog_closed', function(event) {
1034 if ($.fullscreen.isFullScreen()) {
1035 var bootbox_modal = $('#updraftcentral_dashboard.updraft-fullscreen div.bootbox.modal.show');
1036 var modal = $('#updraftcentral_dashboard.updraft-fullscreen #updraftcentral_modal_dialog.modal.show');
1037 if (0 === bootbox_modal.length && 0 === modal.length) {
1038 var backdrop = $('#updraftcentral_dashboard.updraft-fullscreen div.modal-backdrop.show');
1039 if (backdrop.length) backdrop.remove();
1040 }
1041 }
1042 });
1043
1044 $(window).resize(function() {
1045 var w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
1046 if (w <= mobile_width) {
1047 var init_width = $("#updraft-central-navigation-sidebar").css('width');
1048 var init_left = $("#updraft-central-navigation-sidebar").position().left;
1049
1050 if (0 <= init_left) {
1051 $('#updraft-mobile-menu').trigger('click');
1052 }
1053
1054 if (default_collapse_width+'px' === init_width) {
1055 $('#updraft-central-sidebar-button').trigger('click');
1056 }
1057
1058 if ($("#updraft-central-navigation-sidebar").hasClass('active')) {
1059 $('#updraft-central-content').prepend('<div class="mobile-menu-backdrop"></div>');
1060 }
1061
1062 $('#updraftcentral_dashboard').on('click', function(event) {
1063 if ('updraft-central-navigation-sidebar' === $(event.target).attr('id') || $(event.target).hasClass('updraft-mobile-menu'))
1064 return;
1065
1066 if ($("#updraft-central-navigation-sidebar").hasClass('active')) {
1067 $("#updraft-central-navigation-sidebar").toggleClass("active");
1068 $('#updraft-central-content > .mobile-menu-backdrop').remove();
1069 }
1070 });
1071
1072 } else {
1073 $("#updraft-central-navigation-sidebar").removeClass("active");
1074 $('#updraft-central-content > .mobile-menu-backdrop').remove();
1075 }
1076 });
1077
1078 // Trigger the resize function initially to do the routine that handles the visibility
1079 // of the sidebar navigation elements
1080 $(window).trigger('resize');
1081 }
1082
1083 /**
1084 * Saves user-defined keyboard shortcut entered by the user
1085 *
1086 * @params {$shortcut_name} The name of the shortcut to be overriden
1087 * @params {$shortcut_key} The new shortcut key entered by the user
1088 * @returns {object} jQuery promise object
1089 */
1090 this.save_shortcut = function (shortcut_name, shortcut_key) {
1091 var deferred = $.Deferred();
1092
1093 UpdraftCentral.send_ajax('shortcuts', { name: shortcut_name, key: shortcut_key }, null, 'via_mothership_encrypting', null, function(resp, code, error_code) {
1094 if ('ok' === code) {
1095 if (resp.hasOwnProperty('message')) {
1096 if ('success' === resp.message) {
1097 deferred.resolve();
1098 } else {
1099 deferred.reject();
1100 }
1101 }
1102 }
1103 });
1104
1105 return deferred.promise();
1106 }
1107
1108 /**
1109 * Loads user-defined keyboard shortcuts
1110 *
1111 * @returns {object} jQuery promise object
1112 */
1113 this.load_shortcuts = function () {
1114 var deferred = $.Deferred();
1115
1116 UpdraftCentral.send_ajax('shortcuts', {}, null, 'via_mothership_encrypting', null, function(resp, code, error_code) {
1117 if ('ok' === code) {
1118 if (resp.hasOwnProperty('shortcuts')) {
1119 deferred.resolve(resp.shortcuts);
1120 }
1121 }
1122 });
1123
1124 return deferred.promise();
1125 }
1126
1127 /**
1128 * Add sortable feature to div "updraftcentral_dashboard_existingsites"
1129 *
1130 * Send a final site order as an indexed array of id's in sorted order to manage_site_order in backend.
1131 *
1132 * returns 'failure message as response'
1133 */
1134 this.site_order = function () {
1135 $("#updraftcentral_dashboard_existingsites").sortable({
1136 axis: 'y',
1137
1138 // handle the start event (end of drag/sort)
1139 start: function (event, ui) {
1140 // close menu
1141 $(".updraft_site_actions").removeClass("open");
1142 },
1143 // handle the stop event (end of drag/sort)
1144 stop: function (event, ui) {
1145 site_order_array = $(this).sortable("toArray",{attribute: "data-site_id"});
1146 UpdraftCentral.send_ajax('manage_site_order', {site_order: site_order_array}, null, 'via_mothership_encrypting', null, function(resp, code, error_code) {
1147
1148 if ('ok' == code) {
1149 if (resp.hasOwnProperty('message')) {
1150 // only need to trap fail as success and nochange require no action
1151 if (resp.message === "fail" ) {
1152 UpdraftCentral_Library.dialog.alert(udclion.error_saving_site_order);
1153 }
1154 }
1155 } else {
1156 console.log("Expected site order data not found:");
1157 console.log(resp);
1158 }
1159 });
1160 }
1161 });
1162 }
1163 this.site_order();
1164
1165 /**
1166 * A Handlebarsjs helper function that is used to format a date string to a specific time format
1167 *
1168 * @param {mixed} date_str The string to format
1169 * @param {mixed} date_format The format of the intended output
1170 *
1171 * @return {string}
1172 */
1173 Handlebars.registerHelper('timeago', function (date_str, date_format) {
1174 if ('undefined' === typeof date_str || 'undefined' === typeof date_format) return '';
1175
1176 if ('string' !== typeof date_format) {
1177 date_format = 'YYYY-MM-DD h:mma'; // Defaults to WordPress.org's datetime field (e.g. last_updated). e.g. 2018-12-01 7:30p
1178 }
1179
1180 return new Handlebars.SafeString(moment(date_str, date_format).fromNow());
1181 });
1182
1183 /**
1184 * A Handlebarsjs helper function that is used to shorten a long text, possibly
1185 * with multiple paragraphs (e.g. descriptions, narratives, etc.) by showing just the first
1186 * two sentences whenever applicable
1187 *
1188 * @param {string} value The string to shorten
1189 *
1190 * @return {string}
1191 */
1192 Handlebars.registerHelper('shorten', function (value) {
1193 if ('undefined' === typeof value) return '';
1194
1195 var temp = value.split('.');
1196 var shorter_value = temp[0];
1197
1198 // Adding the second sentence wouldn't hurt, in case the first sentence is too short.
1199 if (shorter_value.length < 150 && 'undefined' !== typeof temp[1]) shorter_value += '. ' + temp[1];
1200
1201 return new Handlebars.SafeString(shorter_value + '.');
1202 });
1203
1204 /**
1205 * A Handlebarsjs helper function that is used to remove all sorts of unwanted characters
1206 *
1207 * @param {mixed} value The value to normalize
1208 *
1209 * @return {string}
1210 */
1211 Handlebars.registerHelper('normalize', function (value) {
1212 if ('undefined' === typeof value) return '';
1213
1214 value = value.replace(/&#(\d+);/g, function(match, dec) {
1215 return String.fromCharCode(dec);
1216 }).replace(/&amp;/g, '&');
1217
1218 return new Handlebars.SafeString(value);
1219 });
1220
1221 /**
1222 * A Handlebarsjs helper function that is used to format a number string based
1223 * on the current locale
1224 *
1225 * @param {mixed} value The string to convert
1226 *
1227 * @return {string}
1228 */
1229 Handlebars.registerHelper('toLocale', function (value) {
1230 if ('undefined' === typeof value) return '';
1231
1232 return new Handlebars.SafeString(value.toLocaleString());
1233 });
1234
1235 /**
1236 * A Handlebarsjs helper function that is used to generate a star filled ratings that
1237 * is equivalent to wordpress.org's star (rating) system
1238 *
1239 * @param {mixed} rating The current rating in number
1240 * @param {mixed} number_ratings The rating domain for each star
1241 *
1242 * @return {string}
1243 */
1244 Handlebars.registerHelper('render_star', function (rating, number_ratings) {
1245 if ('undefined' === typeof rating || 'undefined' === typeof number_ratings) return '';
1246
1247 var rating = parseInt(rating);
1248 var divisor = 20; // max rating in WordPress is 100, thus we divide with 20 to get the 5 star rating domain
1249
1250 var result = rating / divisor;
1251 var temp = result.toString().split('.');
1252 var whole = parseInt(temp[0]);
1253 var remainder = ('undefined' !== typeof temp[1]) ? parseInt(temp[1]) : 0;
1254
1255 var star = '';
1256 var used = false;
1257 for (var i=1; i<=5; i++) {
1258 if (i <= whole) {
1259 star += '<span class="dashicons dashicons-star-filled"></span>';
1260 } else if (0 !== remainder && !used) {
1261 if (remainder >= 8) {
1262 star += '<span class="dashicons dashicons-star-filled"></span>';
1263 } else {
1264 if (remainder <= 3) {
1265 star += '<span class="dashicons dashicons-star-empty"></span>';
1266 } else {
1267 star += '<span class="dashicons dashicons-star-half"></span>';
1268 }
1269 }
1270 used = true;
1271 } else {
1272 star += '<span class="dashicons dashicons-star-empty"></span>';
1273 }
1274 }
1275
1276 return new Handlebars.SafeString(star+' ('+number_ratings.toLocaleString()+')');
1277 });
1278
1279 /**
1280 * A Handlebarsjs helper function that is used to invalidate all html tags
1281 * by getting the plain text version of the content
1282 *
1283 * @param {mixed} html The content to filter
1284 *
1285 * @return {string}
1286 */
1287 Handlebars.registerHelper('strip_tags', function (html) {
1288 if ('undefined' === typeof html || 'string' !== typeof html) return '';
1289
1290 var temp = document.createElement("DIV");
1291 temp.innerHTML = html;
1292
1293 return new Handlebars.SafeString(temp.innerText);
1294 });
1295
1296 /**
1297 * A Handlebarsjs helper function that is used to execute sprintf functionalities
1298 * in handlerbarsjs template
1299 *
1300 * @return {string}
1301 */
1302 Handlebars.registerHelper('sprintf', function () {
1303 var text = '';
1304 if ('function' === typeof sprintf && 'undefined' !== typeof arguments) {
1305 var args = [].slice.call(arguments);
1306 text = sprintf.apply(null, args);
1307 }
1308
1309 return new Handlebars.SafeString(text);
1310 });
1311
1312 /**
1313 * A Handlebarsjs helper function that is used to compare
1314 * two values if they are equal. Please refer to the example below.
1315 * Assuming "comment_status" contains the value of "spam".
1316 *
1317 * @param {mixed} a The first value to compare
1318 * @param {mixed} b The second value to compare
1319 *
1320 * @example
1321 * // returns "<span>I am spam!</span>", otherwise "<span>I am not a spam!</span>"
1322 * {{#ifeq "spam" comment_status}}
1323 * <span>I am spam!</span>
1324 * {{else}}
1325 * <span>I am not a spam!</span>
1326 * {{/ifeq}}
1327 *
1328 * @return {string}
1329 */
1330 Handlebars.registerHelper('ifeq', function (a, b, opts) {
1331 if ('string' !== typeof a && 'undefined' !== typeof a && null !== a) a = a.toString();
1332 if ('string' !== typeof b && 'undefined' !== typeof b && null !== b) b = b.toString();
1333 if (a === b) {
1334 return opts.fn(this);
1335 } else {
1336 return opts.inverse(this);
1337 }
1338 });
1339
1340 /**
1341 * A Handlebarsjs helper function that is used to compare
1342 * two values if they are not equal. Please refer to the example below.
1343 * Assuming "user_id" contains the value of "123".
1344 *
1345 * @param {mixed} a The first value to compare
1346 * @param {mixed} b The second value to compare
1347 *
1348 * @example returns "<span>Valid user!</span>", otherwise "<span>Invalid user!</span>" {{#ifneq user_id 0}} <span>Valid user!</span> {{else}} <span>Invalid user!</span> {{/ifneq}}
1349 *
1350 * @return {string}
1351 */
1352 Handlebars.registerHelper('ifneq', function (a, b, opts) {
1353 if (typeof a !== 'string') a = a.toString();
1354 if (typeof b !== 'string') b = b.toString();
1355 if (a !== b) {
1356 return opts.fn(this);
1357 } else {
1358 return opts.inverse(this);
1359 }
1360 });
1361
1362 /**
1363 * A Handlebarsjs helper function that is used to compare two values
1364 * if they are equal. Specifically use to render a "selected" or "checked"
1365 * attribute to a dropdown option or checkbox element. Please refer to the example below.
1366 * Assuming "default_pingback_flag" contains the value of "1".
1367 *
1368 * @param {mixed} a The first value to compare
1369 * @param {mixed} b The second value to compare
1370 * @param {string} attr The attribute to render
1371 *
1372 * @example returns 'checked="checked"', otherwise "" <input name="default_pingback_flag" type="checkbox" value="1" {{ifset default_pingback_flag 1 'checked'}}>
1373 *
1374 * @return {string}
1375 */
1376 Handlebars.registerHelper('ifset', function (a, b, attr) {
1377 if (typeof a !== 'string') a = a.toString();
1378 if (typeof b !== 'string') b = b.toString();
1379 if (a === b) {
1380 return new Handlebars.SafeString(attr + '="' + attr + '"');
1381 } else {
1382 return '';
1383 }
1384 });
1385
1386 /**
1387 * A Handlebarsjs helper function that is used to check if a certain
1388 * value is empty, if so then add the specified attribute(s).
1389 *
1390 * @param {mixed} a The value to check
1391 * @param {string} attrs The attribute(s) to render
1392 *
1393 * @example returns the attached attribute(s) 'disabled="disabled" data-unavailable="1"', otherwise "" <input type="checkbox" name="uc_updates_check_item" value="1" {{ifempty update.plugin 'disabled'}}>
1394 *
1395 * @return {string}
1396 */
1397 Handlebars.registerHelper('ifempty', function (a, attrs) {
1398 if ('undefined' === typeof a || !a || !a.length) {
1399 return new Handlebars.SafeString(attrs);
1400 } else {
1401 return '';
1402 }
1403 });
1404
1405 /**
1406 * Set the current site row
1407 *
1408 * N.B. - primarily used for mass updates, needed by the automatic backup process but
1409 * can always be used for whatever purpose it may serve.
1410 *
1411 * @param {Object} $site_row - A jQuery object representing the site row of the currently
1412 * process site.
1413 * @returns {void}
1414 */
1415 this.set_current_site_row = function($site_row) {
1416 UpdraftCentral.$site_row = $site_row;
1417 }
1418
1419 /**
1420 * Registers an event handler for a particular event
1421 *
1422 * N.B. - Ensures that we don't register the same event handler twice
1423 * by unbinding the same event attached to the selector/element.
1424 *
1425 * @param {string} event - A string representation of the event to bind (e.g. 'click', 'change', etc.).
1426 * @param {string} selector - Any valid jQuery selector where you want to bound the event.
1427 * @param {function} callback - A callback function to trigger when the event is raised on the given selector/element.
1428 * @returns {void}
1429 */
1430 this.register_event_handler = function(event, selector, callback) {
1431 jQuery(document).off(event, selector).on(event, selector, function(e) {
1432 // Check and verify that a process is currently not running before
1433 // executing the below code to prevent from abruptly aborting the current process
1434 // which may lead to JS errors or/and inconsistency of information displayed to the user
1435 if (self.check_processing_state(e)) return;
1436
1437 var params = [];
1438 if ('undefined' !== typeof callback.arguments && callback.arguments && callback.arguments.length) params = callback.arguments;
1439
1440 callback.apply(this, params);
1441 });
1442 }
1443
1444 /**
1445 * Sets an area to a loading style
1446 *
1447 * @param {Object} $container - the jQuery object of the area to be set as loading
1448 * @returns {void}
1449 */
1450 this.set_loading = function ($container) {
1451 $container.css('opacity', '0.3');
1452
1453 // Disable elements while process is on-going
1454 UpdraftCentral_Library.disable_actions();
1455
1456 // If we don't have a spinner visible while loading then we
1457 // append one for the current process.
1458 if ($('.updraftcentral_spinner').not(':visible')) {
1459 if (!$('.injected-spinner.updraftcentral_spinner').length) {
1460 $('#updraftcentral_dashboard').append('<div class="injected-spinner updraftcentral_spinner"></div>');
1461 }
1462 }
1463 }
1464
1465 /**
1466 * Removes the loading style from an area
1467 *
1468 * @param {Object} $container - the jQuery object of the area to be set as loading
1469 * @param {string} html - a string of html to place into the finished loaded area
1470 * @returns {Object} a jQuery promsise with The response from the server
1471 */
1472 this.done_loading = function ($container, html) {
1473 var deferred = jQuery.Deferred();
1474 $container.css('opacity', '1.0');
1475
1476 // Remove our injected spinner after the process has been completed.
1477 if ($('.injected-spinner.updraftcentral_spinner').length) {
1478 $('.injected-spinner.updraftcentral_spinner').remove();
1479 }
1480
1481 // Restore or enable back elements when process is complete
1482 UpdraftCentral_Library.enable_actions();
1483
1484 if (html) {
1485 $container.slideUp(500, function () {
1486 $container.html(html);
1487 deferred.resolve();
1488 }).slideDown(500);
1489 } else {
1490 deferred.resolve();
1491 }
1492
1493 return deferred.promise();
1494 }
1495
1496
1497 /**
1498 * Set the debugging level
1499 *
1500 * @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
1501 * @returns {void}
1502 */
1503 this.set_debug_level = function(debug_level) {
1504 updraftcentral_debug_level = debug_level;
1505 }
1506
1507 /**
1508 * Get the current debugging level
1509 *
1510 * @returns {number} - the debugging level (@see set_debug_level)
1511 */
1512 this.get_debug_level = function() {
1513 return parseInt(updraftcentral_debug_level);
1514 }
1515
1516 /**
1517 * Triggers the callback function for the modal's close event
1518 *
1519 * @param {callback|null} callback - a callback function to be called when the close button (either the "Close" or "X" button) is clicked
1520 * @returns {void}
1521 */
1522 this.initiate_modal_close_listener = function(callback) {
1523 // Listener for modal close and x buttons.
1524 $('.modal-dialog button[data-dismiss="modal"]').on('click', function() {
1525 if ('function' === typeof callback && callback) {
1526 callback.apply(null, []);
1527
1528 // We'll make sure that after the callback is called we must invalidate
1529 // the listener since this is only applicable when the close_callback is
1530 // set or defined under the UpdraftCentral.open_modal.
1531 $('.modal-dialog button[data-dismiss="modal"]').off('click');
1532 }
1533
1534 // Trigger dashboard-wide dialog closed event (applies to both bootbox and bootstrap modal)
1535 $('#updraftcentral_dashboard').trigger('updraftcentral_dialog_closed');
1536 });
1537 }
1538
1539 /**
1540 * 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.
1541 *
1542 * @param {string} title - the title to use for the modal window
1543 * @param {string} body - the HTML contents to place in the modal window
1544 * @param {callback|true} action_button_callback - a callback to call when the main action button is pressed; or just true to close the modal
1545 * @param {string|false} [action_button_text="Go"] - text for the action button; or, if false, an indication that there should be no action button
1546 * @param {callback|null} [pre_open_callback=null] - an optional callback to call immediately before opening the modal
1547 * @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
1548 * @param {string} [extra_classes=''] - extra CSS classes for the modal dialog (e.g. modal-lg)
1549 * @param {callback|null} close_callback - an optional callback to be called when the modal is closed
1550 * @param {callback|null} [post_open_callback=null] - an optional callback to call immediately after opening the modal
1551 * @returns {void}
1552 */
1553 this.open_modal = function(title, body, action_button_callback, action_button_text, pre_open_callback, sanitize_body, extra_classes, close_callback, post_open_callback) {
1554 action_button_text = typeof action_button_text !== 'undefined' ? action_button_text : udclion.go;
1555 // By default, we assume that the input is potentially evil, and sanitize it
1556 sanitize_body = typeof sanitize_body !== 'undefined' ? sanitize_body : true;
1557 extra_classes = typeof extra_classes !== 'undefined' ? extra_classes : '';
1558
1559 // Reset the modal's CSS classes
1560 $('#updraftcentral_modal_dialog .modal-dialog').removeClass().addClass('modal-dialog '+extra_classes);
1561
1562 $('#updraftcentral_modal_dialog .modal-title').html(title);
1563 if (sanitize_body) body = UpdraftCentral_Library.sanitize_html(body);
1564 $('#updraftcentral_modal_dialog .modal-body').html(body);
1565 if (false === action_button_text) {
1566 $('#updraftcentral_modal_dialog button.updraft_modal_button_goahead').hide();
1567 } else {
1568 $('#updraftcentral_modal_dialog button.updraft_modal_button_goahead').html(action_button_text).show();
1569 }
1570 modal_action_callback = action_button_callback;
1571 if (typeof pre_open_callback !== 'undefined' && null !== pre_open_callback) pre_open_callback.call(this);
1572
1573 // Add listener and callback handler for the modal's close buttons
1574 UpdraftCentral.initiate_modal_close_listener(close_callback);
1575
1576 $('#updraftcentral_modal_dialog').modal();
1577
1578 if ($('#updraftcentral_modal_dialog #updraftcentral_addsite_tabs').length) {
1579 $('#updraftcentral_addsite_tabs').tabs().addClass('ui-tabs-vertical ui-helper-clearfix');
1580 }
1581
1582 // Trigger dashboard-wide dialog opened event (applies to both bootbox and bootstrap modal)
1583 $('#updraftcentral_dashboard').trigger('updraftcentral_dialog_opened');
1584
1585 if ('undefined' !== typeof post_open_callback && null !== post_open_callback) post_open_callback.call(this);
1586 }
1587
1588 /**
1589 * Given a site row, send back a suitable HTML site description
1590 *
1591 * @param {Object} $site_row - the jQuery object for the row of the site
1592 *
1593 * @returns {string} - an HTML string describing the site
1594 */
1595 this.get_site_heading = function($site_row) {
1596
1597 var site_description = $site_row.data('site_description');
1598 var site_url = $site_row.data('site_url');
1599 if (site_description == site_url) { site_description = ''; }
1600
1601 var site_heading;
1602 if (site_description) {
1603 site_heading = '<a href="'+site_url+'">'+site_description+'</a>';
1604 } else {
1605 site_heading = '<a href="'+site_url+'">'+site_url+'</a>';
1606 }
1607
1608 return site_heading;
1609 }
1610
1611 /**
1612 * Close the modal dialog
1613 *
1614 * @returns {void}
1615 */
1616 this.close_modal = function() {
1617 $('#updraftcentral_modal_dialog').modal('hide');
1618
1619 // Trigger dialog closed event (applies to both bootbox and bootstrap modal)
1620 $('#updraftcentral_dashboard').trigger('updraftcentral_dialog_closed');
1621 }
1622
1623 /**
1624 * A jQuery callback for row click events
1625 *
1626 * @callable rowclickerCallback
1627 * @param {string} $site_row - the jQuery row object for the site that the click was for
1628 * @param {Number} site_id - the site ID for the site that the click was for
1629 * @param {Object} event - the event received from jQuery
1630 *
1631 * @return {*} prevent_default - if anything other than (boolean)true, then event.preventDefault() is called
1632 */
1633
1634 /**
1635 * De-register all row-clickers. The normal use of this is when switching tabs.
1636 *
1637 * @returns {void}
1638 */
1639 function deregister_row_clickers() {
1640 $('#updraftcentral_dashboard_existingsites_container').off();
1641 }
1642
1643 /**
1644 * De-register all events on the modal. The normal use of this is when switching tabs.
1645 *
1646 * @returns {void}
1647 */
1648 function deregister_modal_listeners() {
1649 $('#updraftcentral_modal').off();
1650 }
1651
1652 /**
1653 * Register click events for specified items in the UpdraftCentral site list (prevents repeating lots of jQuery boilerplate).
1654 * Note that all row clickers are always deregistered upon a mode change (i.e. tab change). So, the correct place to call this function is when updraftcentral_dashboard_mode_set_(your mode) is triggered (or updraftcentral_dashboard_mode_set for all tabs).
1655 *
1656 * @param {string} selector - the selector to use
1657 * @param {rowclickerCallback} callback - callback function that will be called upon the click event
1658 * @param {boolean} [hide_other_sites=false] - if set, then the click will cause other sites in the tab to be hidden
1659 * @param {string} [on_event='click'] - the event type to listen for. In the special case of 'keypress', the default event will not be prevented
1660 * @returns {void}
1661 */
1662 this.register_row_clicker = function(selector, callback, hide_other_sites, on_event) {
1663 on_event = typeof on_event !== 'undefined' ? on_event : 'click';
1664 hide_other_sites = typeof hide_other_sites !== 'undefined' ? hide_other_sites : false;
1665 params = {};
1666 $('#updraftcentral_dashboard_existingsites_container').on(on_event, '.updraftcentral_site_row '+selector, params, function(event) {
1667 var key = UpdraftCentral_Library.md5('_key_' + $(this).get(0).className + '_' + selector);
1668
1669 // Prevent multiple executions of the same action per request.
1670 // N.B. For non-ajax based action (e.g. preloaded filters, search, etc.) we bypass
1671 // them in this check as they don't involve any active connections
1672 if (self.event_trigger.exists(key) && 0 !== $.active) return;
1673
1674 // Check and verify that a process is currently not running before
1675 // executing the below code to prevent from abruptly aborting the current process
1676 // which may lead to JS errors or/and inconsistency of information displayed to the user.
1677 //
1678 // N.B. The ".btn-group > button" is primarily used by the UpdraftCentral_Recorder so we
1679 // make sure that it doesn't get booted out from the processing state check regardless of the outcome
1680 // whether it returns true or false, otherwise, we won't be able to cache content successfully.
1681 if ('.btn-group > button' !== selector && self.check_processing_state(event)) return;
1682
1683 // Add currently requested action to the event_trigger collection. We need
1684 // it to check for multiple execution per request later.
1685 self.event_trigger.add(key, 1);
1686
1687
1688 if (on_event != 'keypress') { event.preventDefault(); }
1689 UpdraftCentral.$site_row = $(this).closest('.updraftcentral_site_row');
1690 var site_id = UpdraftCentral.$site_row.data('site_id');
1691 if (hide_other_sites) {
1692 $('#updraftcentral_dashboard_existingsites .updraftcentral_site_row:not([data-site_id="'+site_id+'"]), #updraftcentral_dashboard_existingsites .updraftcentral_row_divider').slideUp();
1693 $('.updraftcentral_mode_actions .updraftcentral_action_choose_another_site').show();
1694 $("#updraftcentral_dashboard_existingsites").sortable('disable');
1695 $('#updraftcentral-search-area').hide();
1696 UpdraftCentral.$site_row.addClass('sortable-is-disabled');
1697 }
1698 callback.call(this, UpdraftCentral.$site_row, site_id, event);
1699 });
1700 }
1701 var register_row_clicker = this.register_row_clicker;
1702
1703 /**
1704 * Register click events for specified items in the UpdraftCentral modal (prevents repeating lots of jQuery boilerplate).
1705 * Note that all modal clickers are always deregistered upon a mode change (i.e. tab change). So, the correct place to call this function is when updraftcentral_dashboard_mode_set_(your mode) is triggered (or updraftcentral_dashboard_mode_set for all tabs).
1706 *
1707 * @param {string} selector - the selector to use
1708 * @param {rowclickerCallback} callback - callback function that will be called upon the click event
1709 * @param {string} [on_event='click'] - the event type to listen for. In the special case of 'keypress', the default event will not be prevented
1710 *
1711 * @returns {void}
1712 */
1713 this.register_modal_listener = function(selector, callback, on_event) {
1714 on_event = typeof on_event !== 'undefined' ? on_event : 'click';
1715 params = {};
1716 $('#updraftcentral_modal').on(on_event, selector, params, function(event) {
1717 callback.call(this, event);
1718 });
1719 }
1720
1721 $('#updraftcentral_modal_dialog button.updraft_modal_button_goahead').click(function() {
1722 if (true === modal_action_callback) {
1723 this.close_modal();
1724 } else {
1725 modal_action_callback.call(this);
1726 }
1727 });
1728
1729 /**
1730 * JQuery callback for row click events
1731 *
1732 * @param {Object} $listener_row - the jQuery object of the listener itself
1733 * @param {Object} $site_row - the jQuery row object for the site that the click was for
1734 * @param {Number} site_id - the site ID for the site that this is a listener for
1735 * @param {*} [data] - the returned data from the polling operation (if it is that sort of listener)
1736 *
1737 * @callable listenerCallback
1738 *
1739 * @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.
1740 */
1741
1742 var listener_processors = {};
1743 /**
1744 * Register a listener callback - a callback function to be used in association with dashboard notices which poll and update
1745 *
1746 * @param {string} listener_type - an identifying string, indicating the listener type
1747 * @param {listenerCallback} callback - a listener callback function
1748 *
1749 * @see create_dashboard_listener
1750 *
1751 * @returns {void}
1752 */
1753 this.register_listener_processor = function(listener_type, callback) {
1754 listener_processors[listener_type] = callback;
1755 }
1756
1757 /**
1758 * Poll all listener rows on the dashboard for activity
1759 *
1760 * @returns {void}
1761 */
1762 function poll_listeners() {
1763
1764 // var listener_calls = {};
1765
1766 $('#updraftcentral_notice_container .updraftcentral_listener').each(function(ind) {
1767 var site_id = $(this).data('site_id');
1768 var listener_type = $(this).data('type');
1769 var $listener_row = this;
1770 var $site_row = $('#updraftcentral_dashboard_existingsites .updraftcentral_site_row[data-site_id="'+site_id+'"');
1771 var finished = $(this).data('finished');
1772
1773 if (finished) { return; }
1774
1775 if (updraftcentral_debug_level > 1) {
1776 console.log("poll_listeners(): site_id="+site_id+", listener_type="+listener_type);
1777 }
1778
1779 if ($site_row.length > 0 && listener_processors.hasOwnProperty(listener_type)) {
1780 // if (typeof listener_calls[site_id] === 'undefined') listener_calls[site_id] = [];
1781 var call_this = listener_processors[listener_type].call(this, $listener_row, $site_row, site_id);
1782 // 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.
1783 // A return value of null is also supported; this means "do nothing this time (but not finished)". Allows for throttling.
1784 if (0 === call_this) {
1785 // Finish and close
1786 $(this).data('finished', true);
1787 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_listener_finished_'+listener_type, {
1788 site_id: site_id,
1789 site_row: $site_row,
1790 listener_row: $listener_row,
1791 listener_type: listener_type
1792 });
1793 $($listener_row).clearQueue().delay(10000).slideUp('slow', function() {
1794 $(this).remove();
1795 });
1796 } else if (1 === call_this) {
1797 // Finish but don't close
1798 $(this).data('finished', true);
1799 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_listener_finished_'+listener_type, {
1800 site_id: site_id,
1801 site_row: $site_row,
1802 listener_row: $listener_row,
1803 listener_type: listener_type
1804 });
1805 } else if (null != call_this && call_this.hasOwnProperty('call')) {
1806 var call_type = call_this.call;
1807 UpdraftCentral.send_site_rpc(call_this.call, call_this.data, $site_row, function(response, code, error_code) {
1808 if ('ok' == code && false !== response && response.hasOwnProperty('data')) {
1809 if (listener_processors.hasOwnProperty(call_type)) {
1810 listener_processors[call_type].call(this, $listener_row, $site_row, site_id, response.data);
1811 } else {
1812 console.log("UpdraftCentral: listener type "+call_type+" has no registered processor (dump of all registered processors follows)");
1813 console.log(listener_processors);
1814 }
1815 }
1816 });
1817 }
1818 } else if ($site_row.length > 0) {
1819 console.log("UpdraftCentral: listener type "+listener_type+" has no registered processor (dump of all registered processors follows)");
1820 console.log(listener_processors);
1821 } else {
1822 console.log("UpdraftCentral: listener for site_id="+site_id+" with type "+listener_type+": site row not found");
1823 }
1824 });
1825
1826 }
1827
1828 setInterval(function() {
1829 poll_listeners();
1830 }, listener_poll_interval);
1831
1832 // A separate ud_rpc object for each site
1833 var ud_rpcs = [];
1834
1835 /**
1836 * Given a site (identified by its row), get the URL to send HTTP requests to. This is abstracted for convenience and maintainability if there need to be future changes.
1837 *
1838 * @param {Object} $site_row - the jQuery object for the site row
1839 *
1840 * @returns {string} - the URL
1841 */
1842 this.get_contact_url = function($site_row) {
1843 // Used to be site_url; we changed to using the admin_url because when checking updates (e.g.), some sites are only registering their hooks on the back-end. Since UC is typically providing wp-admin-like functions, it makes sense to go for the back-end.
1844 var admin_url = $site_row.data('admin_url').replace(/\/+$/, '');
1845 return admin_url+'/admin-ajax.php';
1846 }
1847
1848 /**
1849 * 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()
1850 *
1851 * @param {Object} $site_row - the jQuery object for the site row
1852 * @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.
1853 *
1854 * @returns {Object} - the UpdraftPlus_Remote_Communications object
1855 *
1856 * @uses get_udrpc
1857 */
1858 function get_site_udrpc($site_row, connection_method_config) {
1859
1860 var site_remote_public_key = $site_row.data('site_remote_public_key');
1861 var site_local_private_key = $site_row.data('site_local_private_key');
1862 var site_url = this.get_contact_url($site_row);
1863 var site_id = $site_row.data('site_id');
1864 var key_name_indicator = $site_row.data('key_name_indicator');
1865 var remote_user_id = $site_row.data('remote_user_id');
1866
1867 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'; }
1868
1869 // The connection method ought not to be via_mothership_encrypting - such sites shouldn't be being routed into here (but via_mothership is allowed)
1870 if ('via_mothership_encrypting' == connection_method_config) {
1871 console.warn("UpdraftCentral: A site ("+site_id+", "+site_url+") routed via_mothership_encrypting was passed into get_site_udrpc");
1872 console.log($site_row);
1873 }
1874
1875 var message_wrapper = false;
1876
1877 if ('direct_default_auth' == connection_method_config) {
1878 // 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.
1879 var is_firefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
1880 // N.B. If they're set to use Digest authentication, this should not use manual - should switch back
1881 // connection_method = (is_firefox) ? 'direct_manual_auth' : 'direct_jquery_auth';
1882 // Actually, 'jQuery method' also works in Firefox
1883 connection_method = 'direct_jquery_auth';
1884 } else {
1885 connection_method = connection_method_config;
1886
1887 // 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.
1888
1889 if ('via_mothership' == connection_method) {
1890
1891 message_wrapper = {
1892 action: 'updraftcentral_dashboard_ajax',
1893 subaction: 'site_rpc',
1894 component: 'dashboard',
1895 nonce: udclion.updraftcentral_dashboard_nonce,
1896 site_id: site_id,
1897 site_rpc_preencrypted: 1
1898 };
1899
1900 }
1901
1902 }
1903
1904 var send_cors_headers = $site_row.data('send_cors_headers');
1905 if ('undefined' === typeof send_cors_headers) { send_cors_headers = 1; }
1906
1907 var auth_method = ('direct_manual_auth' == connection_method) ? 'manual' : 'jquery';
1908
1909 var http_credentials = {};
1910
1911 var comms_url = site_url;
1912
1913 // When routing via the mothership, don't put in credentials, as the mothership will do that
1914 if ('via_mothership_encrypting' != connection_method && 'via_mothership' != connection_method) {
1915 var http_username = $site_row.data('http_username');
1916 if ('undefined' !== typeof http_username && http_username) {
1917 http_credentials.username = http_username;
1918 var http_password = $site_row.data('http_password');
1919 if ('undefined' !== typeof http_password) {
1920 http_credentials.password = http_password;
1921 }
1922 }
1923 } else {
1924 comms_url = udclion.ajaxurl;
1925 }
1926
1927 if (updraftcentral_debug_level > 0) {
1928 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);
1929 if (updraftcentral_debug_level > 1) {
1930 console.log("Remote public key follows");
1931 console.log(site_remote_public_key);
1932 }
1933 }
1934
1935 var reuse_id = site_id+' '+connection_method;
1936
1937 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);
1938
1939 }
1940
1941 /**
1942 * 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.
1943 *
1944 * @param {number} reuse_id - A unique ID, that can be used for re-using of the result
1945 * @param {number} remote_user_id - The ID of the user on the remote WP site that the keys are for
1946 * @param {string} key_name_indicator - The key name indicator (which indicates to the remote site which key to use to decrypt the message)
1947 * @param {string} site_remote_public_key - The RSA public key for contacting the remote site, in PEM format
1948 * @param {string} site_local_private_key - The RSA private key for the local site, in PEM format
1949 * @param {string} site_url - The URL for the remote site
1950 * @param {boolean} [cors_headers_wanted=true] - Whether to request that the remote application sets CORS headers with its reply
1951 * @param {Object} [http_credentials={}] - an object with any HTTP credentials to be set (useful properties: username, password)
1952 * @param {string} [auth_method] - the authentication method to use ('jquery' or 'manual')
1953 * @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.
1954 *
1955 * @returns {Object} - the UpdraftPlus_Remote_Communications object
1956 */
1957 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) {
1958 if ('undefined' != typeof ud_rpcs[reuse_id]) {
1959 ud_rpc = ud_rpcs[reuse_id];
1960 } else {
1961 cors_headers_wanted = (typeof cors_headers_wanted === 'undefined') ? true : cors_headers_wanted;
1962 var ud_rpc = new UpdraftPlus_Remote_Communications(key_name_indicator);
1963 ud_rpc.set_key_local(site_local_private_key);
1964 ud_rpc.set_key_remote(site_remote_public_key);
1965 ud_rpc.activate_replay_protection();
1966
1967 var url_match = /\/admin-ajax.php$/;
1968 if (url_match.test(site_url)) {
1969 // wp-admin/admin-ajax.php before WP 3.5 will die() if $_REQUEST['action'] is not set (3.2) or is empty (3.4). Later WP versions also check that, but after (instead of before) wp-load.php, which is where we are ultimately hooked in.
1970 site_url = site_url + '?action=updraft_central';
1971 }
1972
1973 ud_rpc.set_destination_url(site_url);
1974 if ('undefined' != typeof http_credentials) { ud_rpc.set_http_credentials(http_credentials); }
1975 if ('undefined' != typeof auth_method) { ud_rpc.set_auth_method(auth_method); }
1976 if ('undefined' != typeof message_wrapper && false !== message_wrapper) {
1977 ud_rpc.set_message_wrapper(message_wrapper);
1978 ud_rpc.set_message_unwrapper(function(response) {
1979 var processed = process_direct_ajax_response(response, 2, false);
1980 if (true === processed) {
1981 if (response.hasOwnProperty('wrapped_response')) {
1982 return response.wrapped_response;
1983 } else {
1984 processed = 'wrapped_response_not_found';
1985 }
1986 }
1987 console.error("UDRPC: Attempt to unwrap the message failed (code: "+processed+")");
1988 // This is usually redundant - something further down the line will log it
1989 if (updraftcentral_debug_level > 1) {
1990 console.log(response);
1991 }
1992 return false;
1993 });
1994 }
1995 ud_rpc.set_cors_headers_wanted(cors_headers_wanted);
1996 ud_rpcs[reuse_id] = ud_rpc;
1997 }
1998 if (updraftcentral_debug_level > 0) {
1999 // UDRPC, at debug level 2, console.log()s lots of cryptographic internals which are only really needed when debugging that
2000 var ud_rpc_debug_level = (updraftcentral_debug_level > 2) ? 2 : 1;
2001 ud_rpc.set_debug_level(ud_rpc_debug_level);
2002 }
2003 return ud_rpc;
2004 }
2005
2006 /**
2007 * An ajaxCallback
2008 *
2009 * @callable ajaxCallback
2010 * @param {*} response - the response data for the result of the call
2011 * @param {String} [code] - the response code; can be 'error' in the case of an error
2012 * @param {String} [error_code] - in the case of code being 'error', this contains the error code
2013 */
2014
2015 /**
2016 * 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.
2017 *
2018 * @param {ajaxCallback} response - callback that will be called with the results of the AJAX call
2019 * @param {string} [code] - the response code; can be 'error' in the case of an error
2020 * @param {string} [error_code] - in the case of code being 'error', this contains the error code
2021 * @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)
2022 * @param {ajaxCallback} response_callback - callback that will be called with the results of the AJAX call
2023 * @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)
2024 * @param {Object} $site_row - the jQuery object for the row of the site that the request is being sent to
2025 * @returns {void}
2026 */
2027 function process_ajax_response(response, code, error_code, is_site_rpc, response_callback, allow_visual_responses, $site_row) {
2028
2029 var website = ('undefined' !== typeof $site_row && $site_row && $site_row.length) ? $site_row.data('site_description')+' - ' : '';
2030
2031 allow_visual_responses = ('undefined' === typeof allow_visual_responses) ? true : allow_visual_responses;
2032
2033 // 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.
2034
2035 if ('error' == code) {
2036 console.error("process_ajax_response: return code: "+code+", error_code: "+error_code+" - parsed response follows");
2037 console.log(response);
2038 } else if (updraftcentral_debug_level > 0) {
2039 console.log("process_ajax_response: return code: "+code+" - parsed response follows");
2040 console.log(response);
2041 }
2042
2043 if (is_site_rpc && 'ok' == code && response.hasOwnProperty('response') && 'rpcerror' == response.response) {
2044 code = 'error';
2045 error_code = 'rpc_unknown_error';
2046
2047 if (response.hasOwnProperty('data') && response.data.hasOwnProperty('code')) {
2048 error_code = response.data.code;
2049 console.error("UpdraftCentral: RPC: Error occurred ("+error_code+" - "+$site_row.data('site_description')+"); data follows");
2050 console.log(response.data);
2051 response = response.data.data;
2052
2053 var handled = response_callback.call(this, response, code, error_code);
2054
2055 if (true !== handled) {
2056 // A default message for if we don't recognise the code
2057 var dash_message = udclion.js_exception_occurred+' ('+error_code+')';
2058 // Get the error's own message, if we know about it
2059 if (udclion.rpcerrors.hasOwnProperty(error_code)) {
2060 if ('rpc_fatal_error' == error_code && response.hasOwnProperty('message')) {
2061 dash_message = sprintf(udclion.rpcerrors[error_code], response.message);
2062 } else {
2063 dash_message = udclion.rpcerrors[error_code];
2064 }
2065 }
2066
2067 if (allow_visual_responses) { UpdraftCentral_Library.dialog.alert('<h2>'+website+udclion.communications_error+'</h2>'+dash_message); }
2068 }
2069
2070 return;
2071 }
2072 }
2073
2074 if (code == 'error') {
2075
2076 var msg = udclion.general_js_comms_failure;
2077 // 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
2078 var is_comms_failure = true;
2079 var title = udclion.error;
2080
2081 // If the response didn't unwrap, it may be an error response.
2082 if (2 == is_site_rpc && 'unwrapper_failure' == error_code && response.hasOwnProperty('code')) { error_code = response.code; }
2083
2084 if ('json_parse_fail' == error_code) {
2085 if (response.indexOf('<html') > -1) {
2086 console.error("UpdraftCentral: JSON parse fail: looks like html was returned - remote plugin is probably not installed/inactive/blocked");
2087 msg = udclion.general_js_comms_failure;
2088 title = udclion.communications_error;
2089 }
2090 } else if ('response_empty' == error_code || 'http_post_fail' == error_code) {
2091 msg = udclion.general_js_comms_failure;
2092 title = udclion.communications_error;
2093 } else if ('timeout' == error_code) {
2094 msg = udclion.comms_failure_timeout;
2095 title = udclion.communications_error+' - '+udclion.timeout;
2096 } else if ('unauthorized' == error_code) {
2097 msg = udclion.comms_failure_unauthorised;
2098 title = udclion.communications_error;
2099 } else if ('unknown_response' == error_code) {
2100 msg = udclion.unknown_response;
2101 title = udclion.communications_error;
2102 } else if ('cannot_contact_localdev' == error_code) {
2103 title = udclion.communications_error;
2104 msg = response.message;
2105 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) {
2106 msg += '<br>'+udclion.localdev_can_work_better_with_https;
2107 }
2108 } 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) {
2109 msg = response.message;
2110 msg += "<br>"+udclion.digest_auth_not_supported;
2111 } 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) {
2112 msg = udclion.comms_failure_unauthorised+' <a href="#" class="updraftcentral_site_editdescription">'+udclion.open_site_configuration+'...</a>';
2113 } else if (response.hasOwnProperty('message')) {
2114 msg = response.message;
2115 if (!is_site_rpc) { is_comms_failure = false; }
2116 } else {
2117 is_comms_failure = false;
2118 msg += '<br>'+udclion.error_code+': '+error_code;
2119 }
2120
2121 // ns_error_dom_bad_uri: access to restricted uri denied - Firefox
2122 if (response.hasOwnProperty('status') && 401 == response.status) {
2123 msg = udclion.comms_failure_unauthorised+' <a href="#" class="updraftcentral_site_editdescription">'+udclion.open_site_configuration+'...</a>';
2124 } else if ('ns_error_dom_bad_uri: access to restricted uri denied' == error_code) {
2125 msg = udclion.comms_failure_unauthorised_by_browser+' <a href="#" class="updraftcentral_site_editdescription">'+udclion.open_site_configuration+'...</a>';
2126 }
2127
2128 msg = '<p>'+msg+'</p>';
2129
2130 if (is_comms_failure) {
2131 msg += '<p><a href="'+udclion.common_urls.connection_checklist+'">'+udclion.go_here_for_connection_help+'</a></p>';
2132 msg += '<p><a href="#" class="updraftcentral_test_other_connection_methods">'+udclion.test_other_connection_methods+'</a></p>';
2133 }
2134
2135 if (response.hasOwnProperty('status') && 200 != response.status && 0 != response.status) {
2136 msg += '<p>'+udclion.http_response_status+': '+response.status+'</p>';
2137 }
2138
2139 $('#updraftcentral_dashboard').trigger('updraftcentral_response_error', { code: code, message: msg, title: title});
2140 if (allow_visual_responses) { UpdraftCentral_Library.dialog.alert('<h2>'+website+title+'</h2>'+msg); }
2141 }
2142
2143 if (is_site_rpc && response.hasOwnProperty('data') && null != response.data) {
2144 if (response.data.hasOwnProperty('php_events')) {
2145 $.each(response.data.php_events, function(index, logline) {
2146 console.log("UpdraftCentral: PHP event on remote side: "+logline);
2147 });
2148 }
2149 if (response.data.hasOwnProperty('caught_output')) {
2150 console.log("UpdraftCentral: direct output on remote side: "+response.data.caught_output);
2151 }
2152 if (response.data.hasOwnProperty('php_events') || response.data.hasOwnProperty('caught_output')) {
2153 response.data = response.data.previous_data;
2154 }
2155 }
2156
2157 response_callback.call(this, response, code, error_code);
2158 }
2159
2160 /**
2161 * Process responses received back from the mothership over AJAX. This will do some processing, and then call process_ajax_response()
2162 *
2163 * @param {string} response - the response received
2164 * @param {boolean} is_site_rpc - whether it was command to a remote site or not.
2165 * @param {ajaxCallback|boolean} response_callback - callback that will be called with the results of the AJAX call - or, to not call, false
2166 * @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
2167 * @param {Object} $site_row - the jQuery object for the row of the site that the request is being sent to
2168 *
2169 * @returns {boolean|string} - If the parsing did not turn up any errors, true is return; otherwise, an error code.
2170 */
2171 function process_direct_ajax_response(response, is_site_rpc, response_callback, allow_visual_responses, $site_row) {
2172
2173 allow_visual_responses = ('undefined' === typeof allow_visual_responses) ? true : allow_visual_responses;
2174
2175 // AJAX via the mothership comes with its results wrapped
2176
2177 if (response.hasOwnProperty('responsetype') && 'error' == response.responsetype) {
2178 if (response.hasOwnProperty('message')) { console.error("UpdraftCentral error via AJAX: "+response.message); }
2179 if ('cannot_contact_localdev' == response.code) { response.request_info = { method: method, use_method: use_method} }
2180 if (false !== response_callback) {
2181 process_ajax_response(response, 'error', response.code, is_site_rpc, response_callback, allow_visual_responses, $site_row);
2182 }
2183 return response.code;
2184 }
2185
2186 if (!response.hasOwnProperty('message') && !response.hasOwnProperty('code')) {
2187 console.log(response);
2188 if (false !== response_callback) {
2189 process_ajax_response(response, 'error', 'unknown_response', is_site_rpc, response_callback, allow_visual_responses, $site_row);
2190 }
2191 return 'unknown_response';
2192 }
2193
2194 if (updraftcentral_debug_level > 1) {
2195 console.log(response.responsetype+': '+response.message);
2196 }
2197
2198 // When doing site RPC, the remote site's reply is in the 'data' attribute
2199 if (is_site_rpc) {
2200
2201 if (response.hasOwnProperty('php_events')) {
2202 $.each(response.php_events, function(index, logline) {
2203 console.info("UpdraftCentral: PHP event on remote side: "+logline);
2204 });
2205 }
2206
2207 if (response.hasOwnProperty('mothership_caught_output')) {
2208 console.info("UpdraftCentral: direct output on remote side: "+response.caught_output);
2209 }
2210
2211 // This is set for a successful communication
2212 if (response.hasOwnProperty('rpc_response')) {
2213 response = response.rpc_response;
2214 }
2215 }
2216
2217 if (false !== response_callback) {
2218 process_ajax_response(response, 'ok', null, is_site_rpc, response_callback, allow_visual_responses, $site_row);
2219 }
2220
2221 return true;
2222 }
2223
2224 /**
2225 * Sends a remote command via AJAX - either directly, or via the site that this plugin is installed upon.
2226 *
2227 * @param {String} command - the command to send
2228 * @param {*} data - data to send with the remote request
2229 * @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
2230 * @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.
2231 * @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
2232 * @param {ajaxCallback} response_callback - callback that will be called with the results of the AJAX call
2233 * @param {Number} [timeout=30] - the number of seconds to allow before the call times out
2234 * @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
2235 *
2236 * @uses process_ajax_response
2237 */
2238
2239 this.send_ajax = function(command, data, $site_row, connection_method, spinner_where, response_callback, timeout, allow_visual_responses) {
2240
2241 var website = ('undefined' !== typeof $site_row && $site_row && $site_row.length) ? $site_row.data('site_description')+' - ' : '';
2242
2243 connection_method = typeof connection_method !== 'undefined' ? connection_method : 'direct_default_auth';
2244 timeout = typeof timeout !== 'undefined' ? timeout : 30;
2245
2246 // Override submitted timeout if "user defined timeout" is set
2247 if ('undefined' !== typeof udclion.user_defined_timeout && udclion.user_defined_timeout) {
2248 timeout = udclion.user_defined_timeout;
2249 }
2250
2251 spinner_where = typeof spinner_where !== 'undefined' ? spinner_where : null;
2252 allow_visual_responses = ('undefined' === typeof allow_visual_responses) ? true : allow_visual_responses;
2253
2254 // Boil it down to one of 'direct', 'server', 'server_proxies' (i.e. factor out the sub-methods)
2255 var ajax_method = ('via_mothership' == connection_method || 'via_mothership_encrypting' == connection_method) ? ('via_mothership' == connection_method ? 'server_proxies' : 'server') : 'direct';
2256
2257 var is_site_rpc = (null === $site_row) ? false : true;
2258
2259 if (is_site_rpc) {
2260 var unlicensed = $site_row.data('site_unlicensed');
2261 if ('undefined' !== typeof unlicensed && unlicensed) {
2262 UpdraftCentral_Library.dialog.alert('<h2>'+website+udclion.error+'</h2>'+udclion.site_unlicensed_message);
2263 return;
2264 }
2265 }
2266
2267 if (spinner_where) {
2268 if (!$('.updraftcentral_spinner').is(':visible')) {
2269 $(spinner_where).prepend('<div class="updraftcentral_spinner"></div>');
2270 }
2271 }
2272
2273 if ('direct' == ajax_method && 'https:'== document.location.protocol) {
2274 var site_url = this.get_contact_url($site_row);
2275 if (site_url.substring(0, 5).toLowerCase() == 'http:') {
2276 // Mixed content policy in all mainstream desktop browsers forbids requests to HTTP from HTTPS domains
2277 ajax_method = 'server';
2278 }
2279 }
2280
2281 if (updraftcentral_debug_level > 0) {
2282 console.log("send_message(ajax_method="+ajax_method+", requested_method="+connection_method+", command="+command+", data(follows))");
2283 console.log(data);
2284 }
2285
2286 // In case, the in-progress dialog is shown this information will be dumped
2287 // into the console to make any debugging tasks much more easier.
2288 self.uc_action_data.push({
2289 command: command,
2290 data: data,
2291 website: website,
2292 connection_method: connection_method
2293 });
2294
2295 if ('direct' == ajax_method || 'server_proxies' == ajax_method) {
2296
2297 if (!is_site_rpc) { throw 'send_ajax() called with direct method ('+connection_method+'), but no site row object passed in'; }
2298
2299 var ud_rpc = get_site_udrpc($site_row, connection_method);
2300 ud_rpc.send_message(command, data, timeout, function(response, code, error_code) {
2301
2302 if (spinner_where) {
2303 $(spinner_where).removeClass('updraftcentral_spinner');
2304 $(spinner_where).children('.updraftcentral_spinner').remove();
2305 }
2306
2307 if (updraftcentral_debug_level > 2) {
2308 console.log("Raw response, pre-processing, follows");
2309 console.log(response);
2310 }
2311
2312 var is_site_rpc_flag = ('server_proxies' == ajax_method) ? 2 : 1;
2313
2314 try {
2315 process_ajax_response(response, code, error_code, is_site_rpc_flag, response_callback, allow_visual_responses, $site_row);
2316 } catch (e) {
2317 UpdraftCentral_Library.dialog.alert('<h2>'+website+udclion.error+'</h2>'+udclion.js_exception_occurred+'<br>'+e.toString());
2318 console.log(e);
2319 }
2320 });
2321
2322
2323 } else {
2324 // 'server' == ajax_method
2325
2326 var site_id = 0;
2327 if (null !== $site_row) {
2328 site_id = $site_row.data('site_id');
2329 }
2330
2331 var ajax_subaction = (is_site_rpc) ? 'site_rpc' : command;
2332
2333 var ajax_data = (is_site_rpc) ? { command: command, data: data } : data;
2334
2335 var ajax_options = {
2336 type: 'POST',
2337 url: udclion.ajaxurl,
2338 timeout: (timeout * 1000), // In ms
2339 headers: {
2340 'X-Secondary-User-Agent': 'UpdraftCentral-dashboard.js/'+udclion.udc_version
2341 },
2342 data: {
2343 action: 'updraftcentral_dashboard_ajax',
2344 subaction: ajax_subaction,
2345 component: 'dashboard',
2346 nonce: udclion.updraftcentral_dashboard_nonce,
2347 site_id: site_id,
2348 data: ajax_data
2349 },
2350 dataType: 'text',
2351 success: function(response) {
2352
2353 if (spinner_where) {
2354 $(spinner_where).children('.updraftcentral_spinner').remove();
2355 // $(spinner_where).removeClass('updraftcentral_spinner');
2356 }
2357
2358 if ('undefined' === typeof response || '' === response) {
2359 console.log("UDRPC: the response from the remote site was empty");
2360 process_ajax_response(response, 'error', 'response_empty', is_site_rpc, response_callback, allow_visual_responses, $site_row);
2361 return;
2362 }
2363
2364 try {
2365 var parsed_response = JSON.parse(response);
2366 } catch (e) {
2367
2368 var valid_json = response.match(/\{"format":.*}/);
2369
2370 if (null === valid_json) {
2371 console.log(e);
2372 console.log(response);
2373 process_ajax_response(response, 'error', 'json_parse_fail', is_site_rpc, response_callback, allow_visual_responses, $site_row);
2374 return;
2375 } else {
2376 response = valid_json[0];
2377 try {
2378 var parsed_response = JSON.parse(response);
2379 console.log("UpdraftCentral: successfully parsed JSON after removing unwanted elements");
2380 console.log(response);
2381 } catch (e) {
2382 console.log(e);
2383 console.log(response);
2384 process_ajax_response(response, 'error', 'json_parse_fail', is_site_rpc, response_callback, allow_visual_responses, $site_row);
2385 return;
2386 }
2387 }
2388
2389 }
2390
2391 response = parsed_response;
2392
2393 process_direct_ajax_response(response, is_site_rpc, response_callback, allow_visual_responses, $site_row);
2394
2395 },
2396 error: function(request, status, error_thrown) {
2397
2398 if (spinner_where) {
2399 $(spinner_where).children('.updraftcentral_spinner').remove();
2400 // $(spinner_where).removeClass('updraftcentral_spinner');
2401 }
2402
2403 console.error("UpdraftCentral: Error in AJAX operation");
2404 console.log(request);
2405 console.log(status);
2406 // 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."'
2407 // "Unauthorized" is what you get when HTTP authentication is required. "Timeout" when there's a timeout.
2408 console.error(error_thrown);
2409
2410 if ('' == error_thrown) { error_thrown = 'http_post_fail'; }
2411
2412 if (error_thrown.hasOwnProperty('statusText')) {
2413 error_thrown = error_thrown.statusText.toString();
2414 }
2415
2416 if ('function' === typeof error_thrown.toLowerCase) {
2417 error_thrown = error_thrown.toLowerCase();
2418 } else {
2419 try {
2420 var tmp = error_thrown.toString().toLowerCase();
2421 if (tmp) { error_thrown = tmp; }
2422 } catch (e) {
2423 }
2424 }
2425
2426 process_ajax_response(request, 'error', error_thrown, is_site_rpc, response_callback, allow_visual_responses, $site_row);
2427 }
2428 }
2429
2430 if (updraftcentral_debug_level > 1) {
2431 console.log("UpdraftCentral: jQuery POST: options follow:");
2432 console.log(ajax_options);
2433 }
2434
2435 jQuery.ajax(ajax_options);
2436
2437 }
2438
2439 }
2440
2441 /**
2442 * Set up menu navigation for each site row item. This should be called after any actions that replace the HTML of row items
2443 *
2444 * @returns {void}
2445 */
2446 function setup_menunav() {
2447 // This is no longer needed.
2448 // $('#updraftcentral_dashboard .updraft-dropdown-menu').dropit();
2449 var how_many_sites = $('#updraftcentral_dashboard_existingsites .updraftcentral_site_row:not(.updraft_site_unlicensed)').length;
2450 $('#updraftcentral_licences_in_use').html(how_many_sites);
2451 }
2452
2453 /**
2454 * Fixes layout issues on the backup settings/configure page where content (template) is being
2455 * pulled from the control site dynamically.
2456 *
2457 * @return {void}
2458 */
2459 function adapt_screen_layout() {
2460 var container = $('#updraftcentral_dashboard');
2461
2462 // Get the main container's width
2463 var current_width = container.outerWidth();
2464
2465 // Add "media query"-like class
2466 $('body').toggleClass('updraftcentral-small', current_width <= 800);
2467 $('body').toggleClass('updraftcentral-medium', current_width > 800 && current_width <= 1200);
2468 $('body').toggleClass('updraftcentral-large', current_width > 1200);
2469 }
2470 adapt_screen_layout();
2471 $('#updraft-central-navigation-sidebar').on('collapse_expand_complete', adapt_screen_layout);
2472
2473 $(window).resize(function() {
2474 var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
2475 if (width > mobile_width) {
2476 $('#updraftcentral_dashboard #updraft-central-navigation-sidebar').show();
2477 }
2478 adapt_screen_layout();
2479 });
2480
2481 // Toggle the mobile menu on/off, if at a relevant width
2482 $('#updraftcentral_dashboard .updraft-mobile-menu').on('click', function() {
2483 // Currently only using the width.
2484 // var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
2485 var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
2486 if (width <= mobile_width) {
2487 $("#updraft-central-navigation-sidebar").toggleClass("active");
2488 if ($("#updraft-central-navigation-sidebar").hasClass('active')) {
2489 $('#updraft-central-content').prepend('<div class="mobile-menu-backdrop"></div>');
2490 } else {
2491 $('#updraft-central-content > .mobile-menu-backdrop').remove();
2492 }
2493 }
2494 });
2495
2496 /**
2497 * 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.
2498 *
2499 * @param {string} html - the HTML to place within the site list container in the dashboard
2500 * @returns {void}
2501 */
2502 this.set_existing_sites_to = function(html) {
2503 var checker = setInterval(function() {
2504 // Make sure that no active connections are present in order not to interfere
2505 // when loading the sites and their underlying buttons successfully (most especially
2506 // when refreshing the sites list after site deletion).
2507 if (0 === $.active) {
2508 clearInterval(checker);
2509
2510 // Reset the connection objects, as the IDs and credentials/options may have changed
2511 ud_rpcs = [];
2512 $('#updraftcentral_dashboard_existingsites').html(html);
2513 // Show/hide the relevant buttons/sections for the current tab
2514 UpdraftCentral.set_dashboard_mode(true, true);
2515 setup_menunav();
2516 }
2517 }, 500);
2518 }
2519
2520 /**
2521 * Adds a dashboard notice only if a notice doesnt exist with the same identifier
2522 *
2523 * @param {string} message - The message text to display
2524 * @param {string} [level="notice"] - The level for the notice. Can also start with 'listener_', which is styled as if it were 'info'
2525 * @param {Number|bool} [remove_after=30000] - The number of milliseconds to remove the notice after; or, 0|false to not remove
2526 * @param {Object} [extra_data={}] - Extra data to store with the dashboard notice (via data attributes)
2527 * @param {string} identifier - a string passed to give an id to the notice this stops other notices with the same id being displayed at the same time
2528 *
2529 * @returns {Object|false} the jQuery object for the newly created notice or false if a notice with this identifier already exists
2530 */
2531 this.add_dashboard_notice_singleton = function(message, level, remove_after, extra_data, identifier) {
2532 level = ('undefined' === typeof level) ? 'notice' : level;
2533 remove_after = ('undefined' === typeof remove_after) ? 30000 : remove_after;
2534 extra_data = ('undefined' === typeof extradata) ? {} : extradata;
2535 identifier = ('undefined' === typeof identifier) ? '' : identifier;
2536 extra_data.identifier = identifier;
2537
2538 if (0 === $('#updraftcentral_notice_container .updraftcentral_notice[data-identifier="'+identifier+'"]').length) {
2539 return this.add_dashboard_notice(message, level, remove_after, extra_data);
2540 }
2541 return false;
2542 }
2543
2544 /**
2545 * Adds a dashboard notice
2546 *
2547 * @param {string} message - The message text to display
2548 * @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)
2549 * @param {Number|bool} [remove_after=30000] - The number of milliseconds to remove the notice after; or, 0|false to not remove
2550 * @param {Object} [extra_data={}] - Extra data to store with the dashboard notice (via data attributes)
2551 *
2552 * @returns {Object} the jQuery object for the newly created notice
2553 */
2554 this.add_dashboard_notice = function(message, level, remove_after, extra_data) {
2555 remove_after = typeof remove_after !== 'undefined' ? remove_after : 30000;
2556 extra_data = typeof extra_data !== 'undefined' ? extra_data : { };
2557 level = typeof level !== 'undefined' ? level : 'notice';
2558 var type = 'notice';
2559 var extra_classes = '';
2560
2561 if ('listener_' == level.substr(0, 9)) {
2562 type = 'listener';
2563 extra_classes = 'updraftcentral_listener updraftcentral_listener_'+level.substr(9);
2564 extra_data.type = level.substr(9);
2565 level = 'info';
2566 }
2567
2568 $container = $('#updraftcentral_notice_container');
2569
2570 var newnotice_container_opener = '<div class="updraftcentral_notice updraftcentral_notice_new updraftcentral_notice_level_'+level+' '+extra_classes+'"';
2571 $.each(extra_data, function(key, val) {
2572 newnotice_container_opener += 'data-'+key+'="'+UpdraftCentral_Library.quote_attribute(val)+'"';
2573 });
2574
2575 var $newnotice = $(newnotice_container_opener+'><button type="button" class="updraftcentral_notice_dismiss"></button><div class="updraftcentral_notice_contents">'+message+'</div></div>');
2576 $container.append($newnotice);
2577 if (remove_after) {
2578 $newnotice.slideDown('medium').delay(30000).slideUp('slow', function() {
2579 $(this).remove();
2580 });
2581 } else {
2582 $newnotice.slideDown('medium');
2583 }
2584
2585 return $newnotice;
2586 }
2587
2588 /**
2589 * Creates a special type of dashboard notice which polls for status updates
2590 *
2591 * @param {string} type - Listener type (an identifying string) (not shown; stored and used for CSS classes)
2592 * @param {Object} $site_row - a jQuery object identifying the site row that the listener is associated with
2593 * @param {string} message - HTML to be placed in the dashboard notice
2594 * @param {*} [data={}] - Data associated with the listener (which will be stored in an HTML data attribute)
2595 * @param {string} [title] - HTML to be used as the notice title. If not specified, a default will be used.
2596 *
2597 * @see register_listener_processor
2598 *
2599 * @returns {Object} the jQuery object for the newly created notice
2600 */
2601 this.create_dashboard_listener = function(type, $site_row, message, data, title) {
2602 data = ('undefined' === typeof data) ? {} : data;
2603 data.site_url = $site_row.data('site_url');
2604 data.site_id = $site_row.data('site_id');
2605 var listener_title = (typeof title === 'undefined') ? '<h2>'+$site_row.data('site_description')+'</h2>' : title;
2606 return this.add_dashboard_notice(listener_title+message, 'listener_'+type, false, data);
2607 }
2608
2609 // Only trigger a removal if the close button is directly in the notice. This allows other sub-elements to re-use the style class.
2610 $('#updraftcentral_notice_container').on('click', '.updraftcentral_notice > .updraftcentral_notice_dismiss', function() {
2611 $(this).parents('.updraftcentral_notice').clearQueue().slideUp('slow', function() {
2612 (this).remove();
2613 });
2614 });
2615
2616 /**
2617 * Get the current dashboard mode
2618 *
2619 * @returns {string} - the current dashboard mode
2620 */
2621 this.get_dashboard_mode = function() {
2622 return $('#updraftcentral_dashboard').data('updraftcentral_mode');
2623 }
2624
2625 /**
2626 * Checks whether an ajax request is currently processing
2627 *
2628 * @param {object} [e] - An optional event object passed by the callee to prevent further action
2629 * @returns {boolean}
2630 */
2631 this.check_processing_state = function(e) {
2632 var current_mode = this.get_dashboard_mode();
2633
2634 // Prevent going into another section or area while a process is
2635 // currently running.
2636 if (self.ajax_request_processing && self.uc_action_data.length) {
2637 if ('undefined' !== typeof e) e.preventDefault();
2638
2639 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.notice_heading+'</h2>'+udclion.currently_processing);
2640
2641 // Dump what was logged in the "uc_action_data" from a previous (latest) process
2642 // or action that blocks progress or the currently requested action.
2643 console.log('Action(s) that blocks progress (follows):');
2644 console.log(self.uc_action_data);
2645 return true;
2646 }
2647
2648 return false;
2649 }
2650
2651 /**
2652 * Set up the dashboard, by hiding things that don't belong in the currently active tab
2653 *
2654 * @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).
2655 * @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)
2656 * @param {boolean} [reset=false] - indicates whether the site list has been reset and was triggered by the "choose another site" (reset) button
2657 * @returns {void}
2658 */
2659 this.set_dashboard_mode = function (new_mode, force, reset) {
2660
2661 // Check and verify that a process is currently not running before
2662 // executing the below code to prevent from abruptly aborting the current process
2663 // which may lead to JS errors or/and inconsistency of information displayed to the user
2664 if (self.check_processing_state()) return;
2665
2666 force = ('undefined' === typeof force) ? false : true;
2667 reset = ('undefined' === typeof reset) ? false : true;
2668 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_dashboard_mode_pre_set', { force: force, new_mode: new_mode, reset: reset });
2669
2670
2671 var current_mode = this.get_dashboard_mode();
2672
2673 if (true === new_mode) { new_mode = current_mode; }
2674
2675 if (!force && new_mode == current_mode) { return; }
2676
2677 var extra_contents = $('#updraftcentral_dashboard_existingsites_container .updraftcentral_row_extracontents');
2678 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_dashboard_mode_set_before', { new_mode: new_mode, previous_mode: current_mode, force: force, extra_contents: extra_contents });
2679
2680 if (current_mode) { $('#updraftcentral_dashboard').removeClass('updraftcentral_mode_'+current_mode); }
2681
2682 $('#updraftcentral_dashboard_existingsites_container .updraftcentral_row_extracontents').empty();
2683
2684 // Show all sites again
2685 $('#updraftcentral_dashboard_existingsites .updraftcentral_site_row, #updraftcentral_dashboard_existingsites .updraftcentral_row_divider').show();
2686
2687 $('#updraftcentral_dashboard').data('updraftcentral_mode', new_mode);
2688 $('#updraftcentral_dashboard').addClass('updraftcentral_mode_'+new_mode);
2689 $('#updraft-menu-item-'+current_mode).removeClass('updraft-menu-item-links-active');
2690 $('#updraft-menu-item-'+new_mode).addClass('updraft-menu-item-links-active');
2691
2692 // 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.
2693 $('#updraftcentral_dashboard .updraftcentral-hide-in-other-tabs:not(.updraftcentral-show-in-tab-'+new_mode+'), #updraftcentral_dashboard .updraftcentral-hide-in-tab-'+new_mode).hide();
2694 $('#updraftcentral_dashboard .updraftcentral-show-in-tab-'+new_mode+' .updraftcentral-hide-in-tab-initially').hide();
2695 $('#updraftcentral_dashboard .updraftcentral-show-in-tab-'+new_mode+', #updraftcentral_dashboard .updraftcentral-show-in-other-tabs:not(.updraftcentral-hide-in-tab-'+new_mode+')').slideDown(1);
2696
2697 deregister_row_clickers();
2698 deregister_modal_listeners();
2699
2700 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_dashboard_mode_set', { new_mode: new_mode, previous_mode: current_mode });
2701 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_dashboard_mode_set_'+new_mode, { new_mode: new_mode, previous_mode: current_mode });
2702
2703 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_dashboard_mode_set_after', { new_mode: new_mode, previous_mode: current_mode });
2704
2705 $("#updraftcentral_dashboard_existingsites").sortable('enable');
2706 }
2707
2708 $('.updraftcentral_mode_actions .updraftcentral_action_choose_another_site').click(function() {
2709 if ('undefined' !== typeof UpdraftCentral.$site_row && UpdraftCentral.$site_row) {
2710 if (UpdraftCentral.$site_row.hasClass('sortable-is-disabled')) {
2711 UpdraftCentral.$site_row.removeClass('sortable-is-disabled');
2712 }
2713 }
2714 UpdraftCentral.set_dashboard_mode(true, true, true);
2715 });
2716
2717 $('#updraft-central-navigation-sidebar').off('click', '.updraft-menu-item').on('click', '.updraft-menu-item', function(e) {
2718 e.stopPropagation();
2719
2720 var item_dom_id = $(this).attr('id');
2721 if ('undefined' === typeof item_dom_id) { return; }
2722 if ('updraft-menu-item-' != item_dom_id.substring(0, 18)) {
2723 console.log("UDCentral: menu item without the ID in the expected format");
2724 console.log(this);
2725 return;
2726 }
2727
2728 var new_mode = item_dom_id.substring(18);
2729 UpdraftCentral.set_dashboard_mode(new_mode);
2730
2731 var w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
2732 if (w <= mobile_width) {
2733 $("#updraft-central-navigation-sidebar").toggleClass("active");
2734 if ($("#updraft-central-navigation-sidebar").hasClass("active")) {
2735 $('#updraft-central-content').prepend('<div class="mobile-menu-backdrop"></div>');
2736 } else {
2737 $('#updraft-central-content > .mobile-menu-backdrop').remove();
2738 }
2739 } else {
2740 $('#updraft-central-content > .mobile-menu-backdrop').remove();
2741 }
2742 });
2743
2744 $("#updraft-central-sidebar-button").click(function() {
2745 var defaultWidth = 200,
2746 collapse = false;
2747 var toggleWidth = $("#updraft-central-navigation-sidebar").width() > default_collapse_width ? default_collapse_width+"px" : defaultWidth + "px";
2748
2749 $("#updraft-central-navigation-sidebar").animate({
2750 width: toggleWidth
2751 }, {
2752 step: function( now, fx ) {
2753 var $label = $('#'+fx.elem.id).find('button.updraft-menu-item > span.menu-label');
2754 var $visibility_icon = $('#'+fx.elem.id).find('.module-visibility');
2755 var $hidden_modules_label = $('#hidden-modules-container').find('.uc-hidden-modules-label');
2756 var $show_all = $('#updraft-menu-item-all');
2757 if (120 > now) {
2758 $label.hide();
2759 $visibility_icon.hide();
2760 $hidden_modules_label.hide();
2761 $show_all.html('<span class="dashicons dashicons-visibility"></span>');
2762 collapse = true;
2763 } else {
2764 $label.show();
2765 $visibility_icon.show();
2766 $hidden_modules_label.show();
2767 $show_all.html(udclion.show_all);
2768 collapse = false;
2769 $('span.module-visibility > span.dashicons-hidden').show();
2770 $('span.module-visibility > span.dashicons-visibility').show();
2771 }
2772 },
2773 complete: function() {
2774 if (collapse) {
2775 $('[data-toggle="tooltip"]').tooltip('enable');
2776 } else {
2777 $('[data-toggle="tooltip"]').tooltip('disable');
2778 }
2779 $('#updraft-central-navigation-sidebar').trigger('collapse_expand_complete');
2780 }
2781 });
2782 $(".updraft-central-sidebar-button-icon").toggle();
2783 });
2784
2785 $('#updraftcentral_dashboard .updraftcentral_action_box .updraftcentral_action_manage_sites').click(function() {
2786 UpdraftCentral.set_dashboard_mode('sites');
2787 });
2788
2789 /**
2790 * Do any processing necessary with the passed information about current status
2791 *
2792 * @param {Object} status_info - any recognised properties will be processed
2793 * @returns {void}
2794 */
2795 function process_sites_status_info(status_info) {
2796 if (status_info.hasOwnProperty('how_many_licences_in_use')) {
2797 $('.updraftcentral_licences_in_use').html(status_info.how_many_licences_in_use);
2798 }
2799 if (status_info.hasOwnProperty('how_many_licences_available')) {
2800 var display = (status_info.how_many_licences_available < 0) ? '&#8734;' : status_info.how_many_licences_available;
2801 $('.updraftcentral_licences_total').html(display);
2802 }
2803 }
2804
2805 /**
2806 * Handle any links to updraftplus.com/updraftcentral.com in a new window
2807 *
2808 * @param {string} href - The URL
2809 * @param {Object} [e] - a jQuery event to cancel if opening a new window
2810 */
2811 function redirect_updraft_website_links(href, e) {
2812 if ('undefined' === typeof href) { return; }
2813 if (null !== href.match(/https?:\/\/updraft(plus|central)\.com/)) {
2814 if ('undefined' !== typeof e) { e.preventDefault(); }
2815 var win = window.open(href, '_blank');
2816 UpdraftCentral_Library.focus_window_or_error(win);
2817 }
2818 }
2819
2820 $('#updraftcentral_dashboard_newsite').click(function() {
2821
2822 var advanced_site_options_html = UpdraftCentral.get_advanced_site_options_html({ http_username: '', http_password: ''});
2823
2824 UpdraftCentral.open_modal(udclion.add_site, UpdraftCentral.template_replace('sites-add-new-modal', { advanced_options: advanced_site_options_html }), function() {
2825
2826 var key = $('#updraftcentral_addsite_key').val();
2827 UpdraftCentral.close_modal();
2828
2829 if ('undefined' === typeof key || key === null || key === '') { return; }
2830
2831 var extra_site_info = UpdraftCentral_Library.get_serialized_options('#updraftcentral_modal #updraftcentral_editsite_expertoptions .expert_option');
2832 var send_cors_headers = $('#updraftcentral_modal #updraftcentral_site_send_cors_headers').is(':checked') ? 1 : 0;
2833 var connection_method = $('#updraftcentral_modal #updraftcentral_site_connection_method').val();
2834
2835 UpdraftCentral.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) {
2836
2837 if ('ok' == code) {
2838
2839 if (resp.hasOwnProperty('message')) {
2840 add_dashboard_notice(resp.message, 'info');
2841 if (resp.hasOwnProperty('sites_html')) {
2842 UpdraftCentral.set_existing_sites_to(resp.sites_html);
2843 } else {
2844 console.log("Expected sites_html data not found:");
2845 console.log(resp);
2846 }
2847 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
2848 }
2849
2850 if (resp.hasOwnProperty('key_needs_sending')) {
2851
2852 var site_id = resp.key_needs_sending.key_site_id;
2853 var site_ajax_url = resp.key_needs_sending.url;
2854 var $site_row = $('#updraftcentral_dashboard_existingsites .updraftcentral_site_row[data-site_id="'+site_id+'"');
2855 var site_remote_public_key = resp.key_needs_sending.remote_public_key;
2856
2857 $($site_row).prepend('<div class="updraftcentral_spinner"></div>');
2858
2859 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_Library.base64_encode(site_remote_public_key));
2860 var win = window.open(send_key_url, '_blank', 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=600,height=320');
2861 UpdraftCentral_Library.focus_window_or_error(win);
2862 return;
2863
2864 }
2865 }
2866 });
2867 }, udclion.add_site, function() {
2868 $('#updraftcentral_modal #updraftcentral_site_send_cors_headers').prop('checked', true);
2869 } , false, 'addsite_dialog', null, function() {
2870 $('#updraftcentral_addsite_key').focus();
2871 });
2872 });
2873
2874 // Updates export dialog's button label upon selection of file for import
2875 $('body').on('change', '#updraftcentral_import_file', function() {
2876 if ($(this).val().length) {
2877 $('.exportsettings_dialog button.updraft_modal_button_goahead').html(udclion.import_settings);
2878 }
2879 });
2880
2881 /**
2882 * Sends the settings file to UpdraftCentral for import processing
2883 *
2884 * @param {string} file File path reference to the uploaded file
2885 * @param {object} form_data Any additional data to be submitted along with the file
2886 * @param {integer} timeout Timeout value for the current request
2887 * @returns {Promise}
2888 */
2889 function send_import_file(file, form_data, timeout) {
2890 var deferred = $.Deferred();
2891 timeout = ('undefined' !== typeof timeout) ? timeout : 30;
2892
2893 // Override submitted timeout if "user defined timeout" is set
2894 if ('undefined' !== typeof udclion.user_defined_timeout && udclion.user_defined_timeout) {
2895 timeout = udclion.user_defined_timeout;
2896 }
2897
2898 var data = new FormData();
2899 data.append('action', 'updraftcentral_dashboard_ajax');
2900 data.append('subaction', 'import_settings');
2901 data.append('component', 'dashboard');
2902 data.append('nonce', udclion.updraftcentral_dashboard_nonce);
2903 data.append('site_id', 0); // We're sending it to UpdraftCentral so, no remote ID needed here
2904
2905 if ('undefined' !== typeof file && file) {
2906 data.append('file', file);
2907 }
2908
2909 if ('undefined' !== typeof form_data && form_data) {
2910 data.append('data', JSON.stringify(form_data));
2911 }
2912
2913 var dialog = $('.exportsettings_dialog');
2914 dialog.prepend('<div class="updraftcentral_spinner"></div>');
2915
2916 var ajax_options = {
2917 type: 'POST',
2918 url: udclion.ajaxurl,
2919 timeout: (timeout * 1000),
2920 headers: {
2921 'X-Secondary-User-Agent': 'UpdraftCentral-dashboard.js/'+udclion.udc_version
2922 },
2923 data: data,
2924 contentType: false,
2925 processData: false,
2926 dataType: 'text',
2927 success: function(response) {
2928 dialog.children('.updraftcentral_spinner').remove();
2929 deferred.resolve(response);
2930 },
2931 error: function(request, status, error_thrown) {
2932 dialog.children('.updraftcentral_spinner').remove();
2933 deferred.reject(error_thrown);
2934 }
2935 }
2936
2937 jQuery.ajax(ajax_options);
2938 return deferred.promise();
2939 }
2940
2941 // Allows users to export their site settings along with some other informations
2942 $('#updraftcentral_dashboard_export_settings').click(function() {
2943
2944 UpdraftCentral.open_modal(udclion.export_import_settings, UpdraftCentral.template_replace('sites-export-modal', {}), function() {
2945 var encrypt_phrase = $('#updraftcentral_modal #updraftcentral_encryption_phrase').val();
2946 var import_file = $('#updraftcentral_modal #updraftcentral_import_file');
2947
2948 if ('undefined' !== typeof import_file && import_file && import_file.val().length) {
2949 send_import_file(import_file[0].files[0], { phrase: encrypt_phrase }).then(function(response) {
2950 var resp = JSON.parse(response);
2951 if (resp.data.hasOwnProperty('errors') && resp.data.errors.length) {
2952 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.import_response_heading+'</h2>'+resp.data.errors[0]);
2953 } else {
2954 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.import_response_heading+'</h2>'+udclion.import_successful, function() {
2955 UpdraftCentral.close_modal();
2956 location.reload();
2957 });
2958 }
2959 }).fail(function(error_thrown) {
2960 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.import_response_heading+'</h2>'+error_thrown);
2961 });
2962 } else {
2963 UpdraftCentral.send_ajax('export_settings', { phrase: encrypt_phrase }, null, 'via_mothership_encrypting', '#updraftcentral_dashboard_existingsites', function(resp, code, error_code) {
2964 if ('ok' == code) {
2965 if (resp.hasOwnProperty('data') && resp.data.hasOwnProperty('json_data')) {
2966 UpdraftCentral.close_modal();
2967
2968 // Attach this data to an anchor on page
2969 var link = document.body.appendChild(document.createElement('a'));
2970 link.setAttribute('download', resp.data.file_name);
2971 link.setAttribute('style', "display:none;");
2972 link.setAttribute('href', 'data:text/json' + ';charset=UTF-8,' + encodeURIComponent(resp.data.json_data));
2973 link.click();
2974 }
2975 }
2976 }, null, false);
2977 }
2978 }, udclion.export_settings, null, false, 'exportsettings_dialog', null, null);
2979 });
2980
2981 // Register the modal events which are active in the 'Sites' tab
2982 $('#updraftcentral_dashboard_existingsites').on('updraftcentral_dashboard_mode_set_sites', function(e) {
2983
2984 register_modal_listener('#updraftcentral_addsite_expertoptions_show', function(e) {
2985 $(this).slideUp();
2986 $('#updraftcentral_modal #updraftcentral_editsite_expertoptions .initially-hidden').show();
2987 e.preventDefault();
2988 });
2989
2990 });
2991
2992 // Put clicked links within the settings sections into their own tab
2993 $('#updraftcentral_notice_container').on('click', 'a', function(e) {
2994 var href = $(this).attr('href');
2995 redirect_updraft_website_links(href, e);
2996 });
2997
2998 // Register the row clickers and modal listeners which are active in every tab
2999 $('#updraftcentral_dashboard_existingsites').on('updraftcentral_dashboard_mode_set', function(event, data) {
3000
3001 var menu_label = $('#updraft-menu-item-'+data.new_mode).find('.menu-label').html();
3002 var actions_container = $('.updraftcentral_mode_actions');
3003 if (0 === actions_container.find('h2.screen-title').length) {
3004 actions_container.prepend('<h2 class="screen-title"></h2>');
3005 }
3006 actions_container.find('h2.screen-title').html(menu_label);
3007
3008 // Use a new browser portal for any clicks to updraftplus.com
3009 register_modal_listener('a', function(e) {
3010 var href = $(this).attr('href');
3011 redirect_updraft_website_links(href, e);
3012 });
3013
3014 // Put clicked links within the settings sections into their own tab
3015 $('#updraftcentral_dashboard_existingsites_container').on('click', '.updraftcentral_site_row a', function(e) {
3016 var href = $(this).attr('href');
3017 redirect_updraft_website_links(href, e);
3018 });
3019
3020 register_modal_listener('#updraft_debug_empty_browser_cache', function(e) {
3021
3022 var how_many = 0;
3023 var verbose = (updraftcentral_debug_level > 0) ? true : false;
3024
3025 for (var i = localStorage.length; i >= 0; --i) {
3026 var key = localStorage.key(i);
3027 if (key !== null && key.substr(0, 15) == 'updraftcentral_') {
3028 if (verbose) { console.log("UpdraftCentral: Removing key from local storage: "+key); }
3029 localStorage.removeItem(key);
3030 how_many++;
3031 }
3032 }
3033 if (how_many > 0) {
3034 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.empty+' '+udclion.browser_cache+'</h2>'+sprintf(udclion.cache_emptied, how_many));
3035 } else {
3036 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.empty+' '+udclion.browser_cache+'</h2>'+udclion.cache_no_contents);
3037 }
3038 });
3039
3040 register_modal_listener('#updraft_debug_show_browser_cache', function(e) {
3041 var how_many = 0;
3042 for (var i = 0, len = localStorage.length; i < len; ++i) {
3043 var key = localStorage.key(i);
3044 var value = localStorage.getItem(key);
3045 if (key.substr(0, 15) == 'updraftcentral_') {
3046 how_many++;
3047 console.log(key+": "+value);
3048 }
3049 }
3050 if (how_many > 0) {
3051 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.log_contents+'</h2>'+udclion.cache_contents_logged);
3052 } else {
3053 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.log_contents+'</h2>'+udclion.cache_no_contents);
3054 }
3055 });
3056
3057 // The 'upgrade' tab has no sites rows visible
3058 if (data && data.hasOwnProperty('new_mode') && data.new_mode == 'notices') { return; }
3059
3060 register_modal_listener('.updraftcentral_site_editdescription', function(e) {
3061 e.preventDefault();
3062 open_site_configuration(UpdraftCentral.$site_row);
3063 });
3064
3065 register_modal_listener('.updraftcentral_test_other_connection_methods', function(e) {
3066 e.preventDefault();
3067 UpdraftCentral_Library.open_connection_test(UpdraftCentral.$site_row);
3068 });
3069
3070 register_modal_listener('a.connection-test-switch', function(e) {
3071 e.preventDefault();
3072 var connection_method = $(this).data('connection_method');
3073
3074 UpdraftCentral.close_modal();
3075
3076 var site_id = $(this).data('site_id');
3077
3078 UpdraftCentral.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) {
3079
3080 if ('ok' == code) {
3081
3082 if (resp.hasOwnProperty('message')) { add_dashboard_notice(resp.message); }
3083
3084 if (resp.hasOwnProperty('sites_html')) {
3085 UpdraftCentral.set_existing_sites_to(resp.sites_html);
3086 setup_menunav();
3087 } else {
3088 console.log(resp);
3089 add_dashboard_notice(udclion.unknown_response, 'error');
3090 }
3091 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
3092 }
3093 });
3094 });
3095
3096 register_modal_listener('.updraftcentral_siteinfo_results .phpinfo', function(e) {
3097 e.preventDefault();
3098 UpdraftCentral.send_site_rpc('core.phpinfo', null, UpdraftCentral.$site_row, function(response, code, error_code) {
3099 if ('ok' == code && response.data) {
3100 var output = '';
3101 $.each(response.data, function(name, section) {
3102 output += "<h3>"+name+"</h3>\n"+'<table>'+"\n";
3103 $.each(section, function(key, val) {
3104 if (val.constructor === Array) {
3105 output += "<tr><td>"+key+"</td><td>"+val[0]+"</td><td>"+val[1]+"</td></tr>\n";
3106 } else if (typeof val === 'string') {
3107 if ($.isNumeric(key)) {
3108 output += "<tr><td></td><td>"+val+"</td></tr>\n";
3109 } else {
3110 output += "<tr><td>"+key+"</td><td>"+val+"</td></tr>\n";
3111 }
3112 } else {
3113 console.log("UpdraftCentral: phpinfo: Unrecognised output for key "+key+" (follows)");
3114 console.log(val);
3115 }
3116 });
3117 output += "</table>\n";
3118 });
3119
3120 // N.B. open_modal() by default sanitizes the body data
3121 UpdraftCentral.open_modal(udclion.phpinfo, '<div id="updraftcentral_phpinfo_results">'+output+'</div>', null, false, null, true, 'modal-lg');
3122 }
3123 }, $(this));
3124 });
3125
3126 register_modal_listener('#updraftcentral_site_connection_method', function() {
3127 var site_connection_method = $('#updraftcentral_site_connection_method').val();
3128
3129 if (null == site_connection_method) { return; }
3130
3131 if (site_connection_method.substring(0, 7) == 'direct_' && 'https:' == document.location.protocol) {
3132 $('#updraftcentral_site_connection_method_message').show().html(udclion.http_must_go_via_mothership);
3133 } else {
3134 $('#updraftcentral_site_connection_method_message').hide();
3135 }
3136 }, 'change');
3137
3138 register_row_clicker('.updraftcentral_site_adddescription', function($site_row) {
3139 open_site_configuration($site_row);
3140 });
3141
3142 register_row_clicker('.updraftcentral_site_delete', function($site_row) {
3143 UpdraftCentral_Library.dialog.confirm('<h2>'+udclion.remove_site+'</h2><p>'+UpdraftCentral_Library.escape_attrib($site_row.data('site_url'))+'</p><p>'+udclion.really_delete_site+'</p>', function(result) {
3144 if (!result) return;
3145 var site_id = UpdraftCentral.$site_row.data('site_id');
3146 if (!site_id) { return; }
3147 $site_row.slideUp('slow');
3148
3149 UpdraftCentral.send_ajax('delete_site', { site_id: site_id }, null, 'via_mothership_encrypting', '#updraftcentral_dashboard_existingsites', function(resp, code, error_code) {
3150 if ('ok' == code) {
3151 if (resp.hasOwnProperty('message')) {
3152 add_dashboard_notice(resp.message);
3153 }
3154 if (resp.hasOwnProperty('sites_html')) {
3155 UpdraftCentral.set_existing_sites_to(resp.sites_html);
3156 } else {
3157 console.log(resp);
3158 add_dashboard_notice(udclion.unknown_response, 'error');
3159 }
3160 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
3161 }
3162 });
3163
3164 });
3165 });
3166
3167 register_row_clicker('.row_siteinfo', function($site_row) {
3168 UpdraftCentral.send_site_rpc('core.site_info', null, $site_row, function(response, code, error_code) {
3169 if (updraftcentral_debug_level > 1) {
3170 console.log("send_site_rpc(site_info): parsed response follows");
3171 console.log(response);
3172 }
3173 if ('ok' == code) {
3174 if (false !== response) {
3175 var versions = response.data.versions;
3176 var bloginfo = response.data.bloginfo;
3177 var url = UpdraftCentral_Library.sanitize_html(bloginfo.url);
3178 var name = UpdraftCentral_Library.sanitize_html(bloginfo.name);
3179 // 'This site is running WordPress version %s (PHP %s, MySQL %s) and UpdraftPlus version %s (UDRPC version %s)'
3180 // var message = sprintf(udclion.what_remote_running, UpdraftCentral_Library.sanitize_html(versions.wp), UpdraftCentral_Library.sanitize_html(versions.php), UpdraftCentral_Library.sanitize_html(versions.mysql), UpdraftCentral_Library.sanitize_html(versions.ud), UpdraftCentral_Library.sanitize_html(versions.udrpc_php));
3181 // add_dashboard_notice(message, 'info');
3182 // N.B. By default, open_modal() sanitizes the body.
3183 var ud_version = versions.ud;
3184 if ('none' == ud_version) { ud_version = udclion.updraftplus.version_none; }
3185 var message = sprintf(udclion.what_remote_running, versions.wp, versions.php, versions.mysql, ud_version, versions.udrpc_php);
3186 UpdraftCentral.open_modal(
3187 UpdraftCentral_Library.sanitize_html(bloginfo.name),
3188 UpdraftCentral.template_replace('dashboard-siteinfo', { url: url, message: message, phpinfo: udclion.phpinfo }),
3189 null,
3190 false
3191 );
3192 }
3193 }
3194 });
3195 });
3196
3197 register_row_clicker('.updraftcentral_site_dashboard', function($site_row) {
3198 UpdraftCentral_Library.open_browser_at($site_row);
3199 });
3200
3201 });
3202
3203 /**
3204 * Gets the HTML fragment for advanced site editing options
3205 *
3206 * @param {Object} values - the values to pass to the template
3207 *
3208 * @returns {string} - the HTML
3209 */
3210 this.get_advanced_site_options_html = function(values) {
3211 return UpdraftCentral.template_replace('sites-advanced-site-options', values);
3212 }
3213
3214 /**
3215 * Opens the site configuration dialog for the specified site
3216 *
3217 * @param {Object} $site_row - the jQuery row object for the site whose configuration is to be edited
3218 * @returns {void}
3219 */
3220 this.open_site_configuration = function($site_row) {
3221
3222 var site_url = $site_row.data('site_url');
3223
3224 var http_username = $site_row.data('http_username');
3225 if ('undefined' === typeof http_username) { http_username = ''; }
3226
3227 var http_password = $site_row.data('http_password');
3228 if ('undefined' === typeof http_password) { http_password = ''; }
3229
3230 var connection_method = $site_row.data('connection_method');
3231 if ('undefined' === typeof connection_method) { connection_method = 'direct_default_auth'; }
3232
3233 var http_authentication_method = $site_row.data('http_authentication_method');
3234 if ('undefined' === typeof http_authentication_method) { http_authentication_method = 'basic'; }
3235
3236 var existing_description = $site_row.data('site_description');
3237 if (existing_description == site_url) { existing_description = ''; }
3238
3239 var send_cors_headers = $site_row.data('send_cors_headers');
3240 if ('undefined' === typeof send_cors_headers || send_cors_headers) { send_cors_headers = 1; }
3241
3242 var advanced_site_options_html = UpdraftCentral.get_advanced_site_options_html({http_username: http_username, http_password: http_password});
3243
3244 UpdraftCentral.open_modal(udclion.edit_site_configuration, UpdraftCentral.template_replace('sites-edit-configuration', { site_url: UpdraftCentral_Library.escape_attrib(site_url), advanced_options: advanced_site_options_html }, { existing_description: existing_description }), function() {
3245
3246 var description = $('#updraftcentral-edit-site-description').val();
3247
3248 var send_cors_headers = $('#updraftcentral_modal #updraftcentral_site_send_cors_headers').is(':checked') ? 1 : 0;
3249
3250 var connection_method = $('#updraftcentral_modal #updraftcentral_site_connection_method').val();
3251
3252 var site_id = $site_row.data('site_id');
3253 if (!site_id) { return; }
3254
3255 UpdraftCentral.close_modal();
3256
3257 var extra_site_info = UpdraftCentral_Library.get_serialized_options('#updraftcentral_modal .expert_option');
3258
3259 UpdraftCentral.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) {
3260
3261 if ('ok' == code) {
3262
3263 if (resp.hasOwnProperty('message')) { add_dashboard_notice(resp.message); }
3264
3265 if (resp.hasOwnProperty('sites_html')) {
3266 UpdraftCentral.set_existing_sites_to(resp.sites_html);
3267 setup_menunav();
3268 } else {
3269 console.log(resp);
3270 add_dashboard_notice(udclion.unknown_response, 'error');
3271 }
3272 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
3273 }
3274 });
3275
3276 }, udclion.edit, function() {
3277 $('#updraftcentral_modal #updraftcentral_site_connection_method').val(connection_method).change();
3278 if (send_cors_headers) { $('#updraftcentral_modal #updraftcentral_site_send_cors_headers').prop('checked', true); }
3279 $('#updraftcentral_modal #updraftcentral_addsite_http_authentication_method').val(http_authentication_method);
3280 }, false);
3281 }
3282
3283 /**
3284 * RPCCallback
3285 *
3286 * @callable RPCCallback
3287 * @param {Object} response - the data returned by the RPC call. The format of this object depends upon both code and (if code is not 'ok') on error_code, and so should not be processed before those variables have been inspected.
3288 * @param {string} code - the code returned by the RPC call; currently possible values are 'ok' or 'error'
3289 * @param {string|null} error_code - the error code returned by the RPC call (if any).
3290 *
3291 * @returns {*} - if true, then in the case of an error (code is 'error'), then no further action will be taken; otherwise, default actions (e.g. displaying an error) will be taken
3292 */
3293
3294 /**
3295 * This is intended for debugging use only, from the browser console. It sends a command to the site specified by URL. The results are logged in accordance with your debug settings
3296 *
3297 * @param {string} rpc_command - the command to send
3298 * @param {*} data - the data to send with the command
3299 * @param {string} site_url - the URL of the site to send to; this must exactly match site URL in one of the rows (N.B. normally you need to include the trailing slash)
3300 * @param {number} [timeout=30] - the number of seconds for the timeout on the HTTP call
3301 */
3302 this.debugging_send_command = function(rpc_command, data, site_url, timeout) {
3303 var $site_row = $('#updraftcentral_dashboard_existingsites').find('.updraftcentral_site_row[data-site_url="'+site_url+'"]').first();
3304 if ($site_row.length < 1) {
3305 console.log("debugging_send_command: no corresponding row found for the specified URL");
3306 return;
3307 }
3308
3309 timeout = 'undefined' !== typeof timeout ? timeout : 30;
3310
3311 UpdraftCentral.send_site_rpc(rpc_command, data, $site_row, function(response, code, error_code) {
3312 // Nothing needs logging here, as other parts of the stack will already do that.
3313 }, null, timeout);
3314 }
3315
3316 /**
3317 * Helper method for the UpdraftCentral.is_serializable function which
3318 * checks whether the submitted object or property has plain/simple data types
3319 *
3320 * @see {UpdraftCentral.is_serializable}
3321 * @param {*} data - Any type of data for checking or validation.
3322 * @returns {boolean} - "true" if data has plain/simple type, "false" otherwise.
3323 */
3324 var is_plain_type = function (data) {
3325
3326 // N.B. We're not comparing the type for "null" since it will always return as object. Thus,
3327 // Giving a false positive when running the check against this method (is_plain_type), instead
3328 // We're comparing it by value. If a "null" value is encountered we consider it as plain
3329 // Since it doesn't reference any complex hierarchy other than being a "null".
3330
3331 if (data === null || typeof data === 'string' || typeof data === 'boolean' || typeof data === 'number' || typeof data === 'undefined' || jQuery.isPlainObject(data) || Array.isArray(data)) {
3332 return true;
3333 }
3334 return false;
3335 }
3336
3337 /**
3338 * Checks whether the submitted data is valid for serialization
3339 *
3340 * @borrows {UpdraftCentral#is_plain_type}
3341 * @param {*} data - Any type of data for checking or validation.
3342 * @param {undefined|null} field - An optional field passed around during the loop.
3343 * @param {string} path - An string representing the path where the error occurred within the data parameter hierarchy.
3344 * @returns {boolean} - "true" if data is valid for serialization, "false" otherwise.
3345 */
3346 this.is_serializable = function(data, field, path) {
3347 if ('undefined' === typeof path) path = 'data';
3348 var origin = path;
3349
3350 if (!is_plain_type(data)) {
3351 if ('undefined' !== typeof field && field) {
3352 return {
3353 status: false,
3354 data: data,
3355 error_path: path,
3356 error_field: field,
3357 error_type: typeof data[field],
3358 error_value: data[field]
3359 };
3360 } else {
3361 return { status: false };
3362 }
3363 }
3364
3365 for (var field in data) {
3366 path += (path && path.length) ? ' -> '+field : field;
3367
3368 if (!is_plain_type(data[field])) {
3369 return {
3370 status: false,
3371 data: data,
3372 error_path: path,
3373 error_field: field,
3374 error_type: typeof data[field],
3375 error_value: data[field]
3376 };
3377 }
3378 if ("object" === typeof data[field]) {
3379 var result = UpdraftCentral.is_serializable(data[field], field, path);
3380 if (result.hasOwnProperty('status') && !result.status) {
3381 return {
3382 status: false,
3383 data: result.data,
3384 error_path: result.error_path,
3385 error_field: result.error_field,
3386 error_type: typeof result.data[result.error_field],
3387 error_value: result.data[result.error_field]
3388 };
3389 } else {
3390 // Reset path if we received a valid data during iteration.
3391 path = origin;
3392 }
3393 }
3394 }
3395 return true;
3396 }
3397
3398 /**
3399 * Send a command to the remote site. This is a very thin wrapper around send_ajax.
3400 *
3401 * @param {string} rpc_command - the command to send
3402 * @param {*} data - the data to send with the command
3403 * @param {Object} $site_row - the jQuery object for the row of the site that the request is being sent to
3404 * @param {RPCCallback} callback - function to call with the results
3405 * @param {Object|null|false} spinner_where - jQuery object indicating where any spinner should be shown
3406 * @param {number} [timeout=30] - the number of seconds for the timeout on the HTTP call
3407 * @param {string} connection_method - use this to over-ride the connection method from the default for the site
3408 *
3409 * @uses send_ajax
3410 *
3411 * @returns {void}
3412 */
3413 this.send_site_rpc = function(rpc_command, data, $site_row, callback, spinner_where, timeout, connection_method) {
3414
3415 var result = UpdraftCentral.is_serializable(data);
3416
3417 if (null !== data && result.hasOwnProperty('status') && !result.status) {
3418 console.log('UpdraftCentral: send_site_rpc(' + rpc_command + ') - the submitted data parameter contains unserializable types (follows)');
3419 if (result.hasOwnProperty('error_field') && result.error_field) {
3420 console.log('Error path: '+result.error_path);
3421 console.log('Error field: '+result.error_field);
3422 console.log('Error type: '+result.error_type);
3423 console.log('Error value follows:');
3424 console.log(result.error_value);
3425 } else {
3426 console.log(data);
3427 }
3428
3429 callback.call(this, {
3430 error: udclion.js_exception_occurred
3431 }, 'error', null);
3432
3433 return;
3434 }
3435
3436 timeout = 'undefined' !== typeof timeout ? timeout : 30;
3437
3438 var site_id = $site_row.data('site_id');
3439
3440 if (!site_id) {
3441 console.log("UpdraftCentral: sent_site_rpc("+rpc_command+") command sent, but site ID could not be identified from the row (follows)");
3442 console.log($site_row);
3443 }
3444
3445 connection_method = ('undefined' === typeof connection_method) ? $site_row.data('connection_method') : connection_method;
3446
3447 // Overwrite the connection_method if this is a updraftclone command
3448 connection_method = (0 === rpc_command.lastIndexOf('updraftclone.', 0)) ? 'via_mothership' : connection_method;
3449
3450 if ('undefined' === typeof spinner_where || null === spinner_where) { spinner_where = $site_row; }
3451
3452 try {
3453 return UpdraftCentral.send_ajax(rpc_command, data, $site_row, connection_method, spinner_where, callback, timeout);
3454 } catch (e) {
3455 if (spinner_where) {
3456 $(spinner_where).children('.updraftcentral_spinner').remove();
3457 }
3458
3459 // Here, we're triggering the callback with a code 'error' and passing in
3460 // the error that was catched by the try-catch block. This should help the caller to handle the error by itself. By
3461 // returning "true" (boolean) it will bypass the default error dialog to display,
3462 // meaning, the error was already handled by the caller (e.g. displayed, etc.), otherwise, the default
3463 // dialog will be shown to the user.
3464 var is_error_handled = callback.call(this, {
3465 error: e.toString()
3466 }, 'error', null);
3467
3468 if (typeof is_error_handled === 'undefined' || !is_error_handled) {
3469 // add_dashboard_notice(udclion.js_exception_occurred+'<br>'+e.toString(), 'error');
3470 var website = ('undefined' !== typeof $site_row && $site_row.length) ? $site_row.data('site_description')+' - ' : '';
3471
3472 UpdraftCentral_Library.dialog.alert('<h2>'+website+udclion.error+'</h2>'+udclion.js_exception_occurred+'<br>'+e.toString());
3473 console.log(e);
3474 }
3475 }
3476 }
3477
3478 $('#updraftcentral_dashboard .updraft-central-logo img').dblclick(function() {
3479 UpdraftCentral_Library.toggle_fullscreen();
3480 });
3481
3482 $('#updraft-central-navigation button.updraft-full-screen').on('click', function() {
3483 UpdraftCentral_Library.toggle_fullscreen();
3484 });
3485
3486 $('#updraft-central-navigation button.updraftcentral-help').on('click', function() {
3487 UpdraftCentral_Library.dialog.alert(UpdraftCentral.template_replace('dashboard-help', { uc_version: udclion.updraftcentral_version+': '+udclion.udc_version, running_on: UpdraftCentral.version_info_as_text() }));
3488 });
3489
3490 /**
3491 * Return a string with information on the current installation
3492 *
3493 * @returns {string} information on the current installation
3494 */
3495 this.version_info_as_text = function() {
3496 return 'WP/'+udclion.wp_version+' PHP/'+udclion.php_version+' MySQL/'+udclion.mysql_version+' Curl/'+udclion.curl_version;
3497 }
3498
3499 $('#updraft-central-navigation button.updraftcentral-settings').on('click', function() {
3500 // Check and verify that a process is currently not running before
3501 // executing the below code to prevent from abruptly aborting the current process
3502 // which may lead to JS errors or/and inconsistency of information displayed to the user
3503 if (self.check_processing_state()) return;
3504
3505 UpdraftCentral.open_modal(udclion.settings, UpdraftCentral.template_replace('dashboard-settings', {
3506 uc_version: udclion.updraftcentral_version+': '+udclion.udc_version,
3507 running_on: UpdraftCentral.version_info_as_text(),
3508 timeout: udclion.user_defined_timeout,
3509 shortcut_status: udclion.shortcut_status,
3510 }), function() {
3511 var timeout = $('#updraftcentral_settings_timeout').val();
3512 if (!timeout.length || !$.isNumeric(timeout) || timeout < 30) timeout = 30; // Default is 30 seconds
3513
3514 $location = $('#updraftcentral_modal > .uc-settings-container');
3515 var settings = {
3516 timeout: timeout,
3517 shortcut_status: $('input[name="uc-shortcuts-activate"]').is(":checked") ? 'active' : 'inactive',
3518 }
3519
3520 UpdraftCentral.save_settings(settings, $location).then(function(response) {
3521 // On success, reflect the recent settings changes without waiting
3522 // for the user to reload the page
3523 udclion.user_defined_timeout = settings.timeout;
3524 udclion.shortcut_status = settings.shortcut_status;
3525
3526 var new_debugging_level = $('#updraftcentral_debug_level').val();
3527 if (new_debugging_level >= 0 && new_debugging_level <=3) {
3528 UpdraftCentral.set_debug_level(new_debugging_level);
3529 }
3530
3531 UpdraftCentral.close_modal();
3532 });
3533 }, udclion.save_settings, function() {
3534 $('#updraftcentral_debug_level').val(updraftcentral_debug_level);
3535 });
3536
3537 });
3538
3539 // Refresh dashicon rotates after it has been clicked - stops when the settings are refreshed.
3540 $('.updraftcentral_row_extracontents').on('click', '.dashicons-image-rotate', function() {
3541 $('.dashicons-image-rotate').addClass('dashicon-image-rotating');
3542 });
3543
3544 /**
3545 * Returns the result of filling in the specified Handlebars (http://handlebarsjs.com) template with the provided values
3546 *
3547 * @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'
3548 * @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).
3549 * @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).
3550 *
3551 * @returns {string} The template with values filled in
3552 */
3553 this.template_replace = function(template_name, vars, attr_vars) {
3554 vars = ('undefined' === typeof vars) ? {} : vars;
3555 if (!UpdraftCentral_Handlebars.hasOwnProperty(template_name)) {
3556 console.log("UDCentral: UpdraftCentral_Handlebars template not found: "+template_name);
3557 console.log(UpdraftCentral_Handlebars);
3558 }
3559 if ('undefined' !== typeof attr_vars) {
3560 $.each(attr_vars, function(k, v) {
3561 vars[k] = UpdraftCentral_Library.quote_attribute(v);
3562 });
3563 }
3564 vars.udclion = udclion;
3565
3566 // Checks if the template was compiled by gulp-handlebars and not the default node compiler
3567 if ("object" === typeof UpdraftCentral_Handlebars[template_name]) {
3568 return UpdraftCentral_Handlebars[template_name].handlebars(vars)
3569 }
3570 return UpdraftCentral_Handlebars[template_name](vars);
3571 }
3572
3573 UpdraftCentral_Handlebars = (typeof UpdraftCentral_Handlebars === 'undefined') ? {} : UpdraftCentral_Handlebars;
3574
3575 Handlebars.registerHelper('uc_each', function(context, options) {
3576 var ret = "";
3577 if ('undefined' === typeof context) { return ret; }
3578 for (var i=0, j=context.length; i<j; i++) {
3579 var vars = context[i];
3580 if (!vars.hasOwnProperty('as_json')) vars.as_json = JSON.stringify(vars);
3581 if (!vars.hasOwnProperty('udclion')) vars.udclion = udclion;
3582 ret = ret + options.fn(vars);
3583 }
3584 return ret;
3585 });
3586
3587 /**
3588 * 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.
3589 *
3590 * @returns {void}
3591 */
3592 function compile_handlebars_templates() {
3593 // Initialise Handlebars.templates - it may not already exist
3594 if (!udclion.hasOwnProperty('handlebars')) return;
3595 if (udclion.handlebars.hasOwnProperty('compile')) {
3596 $.each(udclion.handlebars.compile, function(template_name, source) {
3597 console.log("UpdraftCentral: in developer mode: compile template: "+template_name);
3598 UpdraftCentral_Handlebars[template_name] = Handlebars.compile(source);
3599 });
3600 }
3601 }
3602
3603 compile_handlebars_templates();
3604
3605 setup_menunav();
3606
3607 var init_interval = setInterval(function() {
3608 // It is very important that the initial state fo the "uc_module" is
3609 // undefined. Do not initialize it, let the "set_requested_mode" populate it to avoid
3610 // unexpected behaviour when setting the initial mode during UpdraftCentral page load.
3611 //
3612 // N.B. This will help avoid setting and forcing the dashmode mode to "sites" on multiple
3613 // occasions in either UpdraftCentral or UpdraftCentral-Premium. Also, this will give the
3614 // loading of any target module directly through the "uc_module" parameter to load properly
3615 // without being overwritten.
3616 if ('undefined' !== self.uc_module) {
3617 clearInterval(init_interval);
3618
3619 set_dashboard_mode('sites');
3620 if (false !== self.uc_module) {
3621 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_sites_loaded', { module: self.uc_module });
3622 }
3623 }
3624 }, 100);
3625
3626
3627 if ('undefined' !== typeof Modernizr && !Modernizr.lastchild) {
3628 console.log("UDCentral: Unsupported web browser");
3629 $('#updraftcentral_dashboard_loading').fadeOut();
3630 $('#updraftcentral_updraftplus_actions, #updraftcentral_sites_actions, #updraftcentral_dashboard_existingsites_container').remove();
3631 this.add_dashboard_notice(udclion.unsupported_browser, 'error', false);
3632 } else {
3633
3634 $('#updraftcentral_dashboard_loading').fadeOut();
3635 $('#updraftcentral_dashboard_existingsites_container').fadeIn();
3636
3637 if (udclion.hasOwnProperty('show_licence_counts') && udclion.show_licence_counts) { $('.updraftcentral_licence_info').show(); }
3638
3639 // Refresh the sites list every 24 hours
3640 setInterval(function() {
3641 UpdraftCentral.send_ajax('sites_html', null, null, 'via_mothership_encrypting', '#updraftcentral_dashboard_existingsites', function(resp, code, error_code) {
3642 if ('ok' == code) {
3643 if (resp.hasOwnProperty('sites_html')) {
3644 UpdraftCentral.set_existing_sites_to(resp.sites_html);
3645 } else {
3646 console.log("Expected sites_html data not found:");
3647 console.log(resp);
3648 }
3649 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
3650 }
3651 });
3652 }, 86400000);
3653
3654 }
3655
3656 // Remove any indicated notices that came pre-printed on the page
3657 $('#updraftcentral_notice_container .updraftcentral_notice.remove_after_load').delay(30000).slideUp('slow', function() {
3658 $(this).remove();
3659 });
3660
3661 // Move this out of the hierarchy, so that any parent elements in the theme with z-indexes can't result in it being hidden under the grey-out (since the grey-out is not in the hierarchy)
3662 $('#updraftcentral_modal_dialog').appendTo(document.body);
3663
3664 /**
3665 * Stores persistent data in the browser, using the HTML5 local storage API. Uses a fixed prefix of 'updraftcentral_' to avoid clashing with other applications.
3666 *
3667 * We are abstracting this to allow not only for expiring data, but for other possible future enhancements; e.g. an option to duplicate some items persiently in the database. For now we are keeping our options open.
3668 *
3669 * @param {string} key - storage key
3670 * @param {*} data - data to store; must be data than can be turned into JSON
3671 * @param {boolean} [can_expire=false] - whether the time of updating the data should be stored (to allow assessing its age when retrieving it). Note that this should always be used consistently (either always on, or always off) with any particular key - otherwise, its results can be out of date.
3672 * @returns {void}
3673 */
3674 this.storage_set = function(key, data, can_expire) {
3675 if ('undefined' !== typeof can_expire && can_expire) {
3676 var epoch_time = Math.floor(Date.now() / 1000);
3677 localStorage.setItem('updraftcentral_saved_at_'+key, epoch_time);
3678 }
3679 if (updraftcentral_debug_level > 1) {
3680 console.log("UpdraftCentral.storage_set(key="+key+")");
3681 }
3682
3683 try {
3684 localStorage.setItem('updraftcentral_'+key, JSON.stringify(data));
3685 } catch (e) {
3686 console.log(e);
3687 var purged = this.storage_purge();
3688 if (purged > 0) {
3689 if (updraftcentral_debug_level > 1) {
3690 console.log("UpdraftCentral.storage_set(key="+key+") failed; but purged "+purged+" items, so trying again");
3691 }
3692 localStorage.setItem('updraftcentral_'+key, JSON.stringify(data));
3693 }
3694 }
3695
3696 }
3697
3698 /**
3699 * Retrieves stored data from the browser, using the HTML5 local storage API. Uses a fixed prefix of 'updraftcentral_' to avoid clashing with other applications.
3700 *
3701 * @param {string} key - storage key
3702 * @param {Number|Boolean} [maximum_age=false] - if set to a strictly positive numerical value, then only return the data if it was stored within the indicated number of seconds.
3703 * @returns {*} - stored data. Returns null if the age check fails. The result for never-stored data is undefined.
3704 */
3705 this.storage_get = function(key, maximum_age) {
3706 if ('undefined' !== typeof maximum_age && maximum_age > 0) {
3707 var stored_at = localStorage.getItem('updraftcentral_saved_at_'+key);
3708 if (!stored_at) { return null; }
3709 var epoch_time = Math.floor(Date.now() / 1000);
3710 var stored_ago = epoch_time - stored_at;
3711 if (UpdraftCentral.updraftcentral_debug_level > 1) {
3712 console.log("UpdraftCentral.storage_get(key="+key+", maximum_age="+maximum_age+"): stored_at="+stored_at+", epoch_time="+epoch_time+", stored_ago="+stored_ago);
3713 }
3714
3715 if (stored_ago > maximum_age) { return null; }
3716 }
3717 var item = localStorage.getItem('updraftcentral_'+key);
3718 if ('undefined' === typeof item) { return null; }
3719 try {
3720 var parsed = JSON.parse(item);
3721 return parsed;
3722 } catch (e) {
3723 }
3724 return null;
3725 }
3726
3727 /**
3728 * Clean local storage, to reduce the risk of it over-flowing.
3729 * The method used below is currently fairly naive, based on knowledge of how other componets user set_storage()
3730 *
3731 * @return {Integer} - the number of items purged
3732 */
3733 this.storage_purge = function() {
3734
3735 var purged = 0;
3736
3737 for (i = localStorage.length - 1; i >=0; i--) {
3738
3739 var key = localStorage.key(i);
3740
3741 if (null === key) { continue; }
3742
3743 var time_now = (new Date).getTime() / 1000;
3744
3745 if (key.substring(0, 34) == 'updraftcentral_saved_at_wporg_api_') {
3746
3747 var key_age = time_now - localStorage.getItem(key);
3748
3749 // This should match the maximum expiry used with storage_get()
3750 if (key_age > 600) {
3751 if (updraftcentral_debug_level > 0) {
3752 console.log("UpdraftCentral::storage_purge(): purging key: "+key.substring(24)+" (age: "+key_age+" s)");
3753 }
3754 this.storage_remove(key.substring(24));
3755 purged++;
3756 }
3757
3758 }
3759
3760 }
3761
3762 return purged;
3763
3764 }
3765
3766 /**
3767 * Retrieves stored data from the browser, using the HTML5 local storage API. Uses a fixed prefix of 'updraftcentral_' to avoid clashing with other applications.
3768 *
3769 * @param {string} key - storage key
3770 * @returns {void}
3771 */
3772 this.storage_remove = function(key) {
3773 localStorage.removeItem('updraftcentral_saved_at_'+key);
3774 localStorage.removeItem('updraftcentral_'+key);
3775 }
3776
3777 /**
3778 * Records currently selected site and its current content
3779 *
3780 * N.B. Basically, this function records the content associated by
3781 * the selected site before clicking another menu or selecting
3782 * another website to work on. Implemented for ease of use and avoid
3783 * or minimize redundant or repeatitive clicking.
3784 */
3785 this.init_recorder = function() {
3786 var recorder = new UpdraftCentral_Recorder();
3787 recorder.load();
3788 }
3789
3790 /**
3791 * Initializes the UpdraftCentral_Keyboard_Shortcuts class and
3792 * its functionalities
3793 *
3794 * @returns {void}
3795 */
3796 this.init_keyboard_shortcuts = function() {
3797 var shortcuts = new UpdraftCentral_Keyboard_Shortcuts();
3798 shortcuts.init();
3799 }
3800
3801 /**
3802 * Handles Modules Visibility. User can choose which modules to be visible / hidden in the sidebar
3803 */
3804 var $modules = $('#visible-modules-container .updraft-menu-item-container');
3805 var $hidden_modules_container = $('#hidden-modules-container');
3806 var $module_visibility = $('.module-visibility');
3807 var visible_modules = [];
3808 var hidden_modules = [];
3809 var module_id = '';
3810 var $show_all = $('<div class="updraft-menu-item-container"><button id="updraft-menu-item-all" class="updraft-menu-item">' + udclion.show_all + '</button></div>');
3811
3812 var $menu = $('#hidden-modules-menu');
3813 $menu.hide();
3814
3815 var $hamburger = $('.uc-hidden-modules-menu');
3816 var $close = $('.uc-hidden-modules-close');
3817 $close.hide();
3818
3819 /**
3820 * Initializes modules visibility based on what is stored in Database (usermeta table)
3821 */
3822 function initialize_module_visibility() {
3823 if ($(this).find('.dashicons-visibility').length > 0) {
3824 module_id = $(this).children('.updraft-menu-item').prop('id');
3825 module_id = module_id.replace('updraft-menu-item-', '');
3826 hidden_modules.push(module_id);
3827 $(this).hide();
3828 } else {
3829 module_id = $(this).children('.updraft-menu-item').prop('id');
3830 module_id = module_id.replace('updraft-menu-item-', '');
3831 visible_modules.push(module_id);
3832 }
3833 }
3834
3835 $modules.each(initialize_module_visibility);
3836
3837 if (0 === hidden_modules.length) {
3838 $hidden_modules_container.hide();
3839 } else if (hidden_modules.length > 1) {
3840 $show_all.appendTo($hidden_modules_container.next());
3841 }
3842 $hidden_modules_container.find('.uc-hidden-modules-label').text(udclion.hidden_modules + '(' + hidden_modules.length + ')');
3843
3844 $module_visibility.hover(function() {
3845 $(this).parent().find('.updraft-menu-item-links').addClass('updraft-menu-item-hover');
3846 },function() {
3847 $(this).parent().find('.updraft-menu-item-links').removeClass('updraft-menu-item-hover');
3848 });
3849
3850 /**
3851 * Upon clicking module visibility icon, the visibility is toggled and stored in DB
3852 */
3853 $('#updraft-central-navigation-sidebar').on('click', $module_visibility, function(evt) {
3854 if (!$(evt.target).parent().hasClass('module-visibility') && !$(evt.target).hasClass('module-visibility')) return;
3855
3856 var $clicked_module = $(evt.target).closest('.module-visibility');
3857 module_id = $clicked_module.prev().attr('id');
3858 module_id = module_id.replace('updraft-menu-item-', '');
3859 var visibility = false;
3860 if (0 < $clicked_module.find('.dashicons-visibility').length) {
3861 hidden_modules = $.grep(hidden_modules, function(value) {
3862 return value != module_id;
3863 });
3864 visible_modules.push(module_id);
3865 visibility = true;
3866 } else {
3867 $clicked_module.html('<span class="dashicons dashicons-visibility"></span>');
3868 visible_modules = $.grep(visible_modules, function(value) {
3869 return value != module_id;
3870 });
3871 hidden_modules.push(module_id);
3872 }
3873
3874 /**
3875 * Sends ajax request to store toggled visibility. Also toggles visibility in front end upon successful ajax call.
3876 */
3877 UpdraftCentral.send_ajax('module_visibility', {module_id: module_id, visibility: visibility}, null, 'via_mothership_encrypting', null, function (resp, code, error_code) {
3878 if ('ok' === code) {
3879 if (false === visibility) {
3880 $clicked_module.parent().clone().appendTo($menu);
3881 $clicked_module.prev().removeClass('updraft-menu-item-hover').parent().slideUp();
3882 $menu.find('.updraft-menu-item-links').removeClass('updraft-menu-item-hover').removeClass('updraft-menu-item-links-active');
3883 if ($menu.find('.updraft-menu-item-container').length > 0) {
3884 $hidden_modules_container.slideDown();
3885 if ($menu.find('.updraft-menu-item-container').length > 1) {
3886 $show_all.appendTo($hidden_modules_container.next());
3887 }
3888 }
3889 } else {
3890 $clicked_module.parent().remove();
3891 $('.updraft-menu-item-container').find('.updraft-menu-item-' + module_id).next().html('<span class="dashicons dashicons-hidden"></span>').parent().slideDown();
3892 if (0 === $menu.find('.updraft-menu-item-container').length) {
3893 $hidden_modules_container.slideUp();
3894 } else if ($menu.find('.updraft-menu-item-container').length < 2) {
3895 $show_all.remove();
3896 }
3897 }
3898 $hidden_modules_container.find('.uc-hidden-modules-label').text(udclion.hidden_modules + '(' + hidden_modules.length + ')');
3899 }
3900 });
3901
3902 });
3903
3904 /**
3905 * Resets all module visibility. Make all modules it visible
3906 */
3907 $('#updraft-central-navigation-sidebar').on('click', '#updraft-menu-item-all', function() {
3908 UpdraftCentral.send_ajax('reset_modules_visibility', 'all', null, 'via_mothership_encrypting', null, function (resp, code, error_code) {
3909 if ('ok' === code) {
3910 $modules.each(initialize_module_visibility);
3911 $modules.each(function() {
3912 $(this).find('.updraft-menu-item').removeClass('updraft-menu-item-hover');
3913 $(this).slideDown();
3914 $('.module-visibility', this).html('<span class="dashicons dashicons-hidden"></span>');
3915 });
3916 $hidden_modules_container.slideUp('normal', function() {
3917 $close.hide();
3918 $hamburger.show();
3919 $menu.hide();
3920 $menu.find('.updraft-menu-item-container').remove();
3921 });
3922 hidden_modules.length = 0;
3923 }
3924 });
3925 });
3926
3927 $hamburger.click(function() {
3928 $menu.slideToggle('normal', function() {
3929 $close.show();
3930 $hamburger.hide();
3931 });
3932 });
3933
3934 $close.click(function() {
3935 $menu.slideToggle('normal', function() {
3936 $close.hide();
3937 $hamburger.show();
3938 });
3939 });
3940
3941 return this;
3942 };
3943
3944