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

2,890 lines 118.8 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
208 /**
209 * Initializes UpdraftCentral's functions
210 *
211 * @returns {void}
212 */
213 this.init = function() {
214 // Initialize and load recorder
215 UpdraftCentral.init_recorder();
216
217 // Initialize keyboard shortcuts
218 UpdraftCentral.init_keyboard_shortcuts();
219
220 // Initialize ajax request listener
221 UpdraftCentral.init_process_listener();
222
223 // Initialize collapse/expand menu tooltips
224 UpdraftCentral.init_tooltip();
225 }
226
227 /**
228 * Initilizes the collapse/expand menu tooltips
229 *
230 * @returns {void}
231 */
232 this.init_tooltip = function() {
233 var collapse_icon = $('div#updraft-central-sidebar-button span.dashicons-arrow-left-alt2');
234 collapse_icon.data('animation', false);
235 collapse_icon.tooltip({
236 trigger: 'hover',
237 placement: 'right',
238 title: udclion.collapse_menu
239 });
240
241 var expand_icon = $('div#updraft-central-sidebar-button span.dashicons-arrow-right-alt2');
242 expand_icon.data('animation', false);
243 expand_icon.tooltip({
244 trigger: 'hover',
245 placement: 'right',
246 title: udclion.expand_menu
247 });
248 }
249
250 /**
251 * Checks whether the user has "write" privilege to the remote website's directory. If not, then we'll ask the user
252 * for their FTP Credentials to successfully install the desired entity (e.g plugin, theme or WP core).
253 *
254 * N.B. Calling this API will automatically display the request credentials dialog where the user
255 * is asked to provide his FTP credentials. So, there's no longer need to create or load the form manually.
256 *
257 * @param {Object} $site_row The jQuery object representing the current site selected.
258 * @param {String} directory Directory entity that we need to check if credentials is required (e.g 'plugins', 'themes' or 'core')
259 * @return {object} - A jQuery promise object
260 */
261 this.maybe_ask_credentials = function($site_row, directory) {
262 var deferred = jQuery.Deferred();
263 var credentials = new UpdraftCentral_Credentials();
264 var site = new UpdraftCentral_Site($site_row);
265
266 credentials.load_credentials(site).then(function(response) {
267 var requests = response.request_filesystem_credentials;
268
269 if ('undefined' !== typeof requests[directory] && requests[directory]) {
270 credentials.get_credentials(site).then(function(response) {
271 deferred.resolve({
272 site: site,
273 credentials_required: true,
274 credentials: response.site_credentials,
275 store_credentials: response.save_credentials_in_browser
276 });
277 }).fail(function(result) {
278 deferred.reject(result);
279 });
280 } else {
281 deferred.resolve({
282 site: site,
283 credentials_required: false
284 });
285 }
286 }).fail(function(result) {
287 deferred.reject(result);
288 });
289
290 return deferred.promise();
291 }
292
293 /**
294 * Checks whether the plugin is installed and activated on the remote website
295 *
296 * @param {Object} $site_row The jQuery object representing the current site selected.
297 * @param {String} plugin_name The name of the plugin to check
298 * @return {Object} A jQuery promise
299 */
300 this.is_plugin_active = function($site_row, plugin_name) {
301 var deferred = $.Deferred();
302 var param = {
303 plugin: plugin_name
304 }
305
306 UpdraftCentral.send_site_rpc('plugin.is_plugin_installed', param, $site_row, function(response, code, error_code) {
307 if ('ok' === code && !response.data.error) {
308 deferred.resolve(response.data);
309 } else {
310 deferred.reject(response);
311 }
312 });
313
314 return deferred.promise();
315 }
316
317 /**
318 * Activates the plugin on the remote website
319 *
320 * @param {Object} $site_row The jQuery object representing the current site selected.
321 * @param {String} plugin_name The name of the plugin to activate
322 * @return {Object} A jQuery promise
323 */
324 this.activate_plugin = function($site_row, plugin_name) {
325 var deferred = $.Deferred();
326 var param = {
327 plugin: plugin_name
328 }
329
330 UpdraftCentral.send_site_rpc('plugin.activate_plugin', param, $site_row, function(response, code, error_code) {
331 if ('ok' === code && !response.data.error) {
332 deferred.resolve(response.data);
333 } else {
334 deferred.reject(response);
335 }
336 });
337
338 return deferred.promise();
339 }
340
341 /**
342 * Download, install and activates the plugin on the remote website
343 *
344 * @param {Object} $site_row The jQuery object representing the current site selected.
345 * @param {String} plugin_name The name of the plugin to install and activate
346 * @param {String} plugin_slug The slug of the plugin to install and activate
347 * @return {Object} A jQuery promise
348 */
349 this.install_activate_plugin = function($site_row, plugin_name, plugin_slug) {
350 var deferred = $.Deferred();
351
352 UpdraftCentral.maybe_ask_credentials($site_row, 'plugins').then(function(response) {
353
354 // Store newly entered credentials to the browser if the user opted to.
355 if (response.credentials_required && response.store_credentials) {
356 UpdraftCentral.storage_set('filesystem_credentials_'+response.site.site_hash, response.credentials, true);
357 }
358
359 var param = {
360 plugin: plugin_name,
361 slug: plugin_slug,
362 filesystem_credentials: response.credentials
363 }
364
365 UpdraftCentral.send_site_rpc('plugin.install_activate_plugin', param, $site_row, function(response, code, error_code) {
366 if ('ok' === code && !response.data.error) {
367 deferred.resolve(response.data);
368 } else {
369 deferred.reject(response);
370 }
371 });
372
373 }).fail(function(response) {
374 deferred.reject(response);
375 });
376
377 return deferred.promise();
378 }
379
380 /**
381 * Registers listener for ajax processing events
382 *
383 * @returns {void}
384 */
385 this.init_process_listener = function() {
386 $(document).ajaxStop(function() {
387 if (0 === $.active) {
388 self.ajax_request_processing = false;
389 }
390 });
391
392 $('#updraftcentral_dashboard_existingsites').on('updraftcentral_dashboard_mode_pre_set', function(event, data) {
393 if (0 < $.active && (!data.force || data.reset)) {
394 self.ajax_request_processing = true;
395 }
396 });
397
398 $('#updraft-central-navigation > .top_menu_right > span.dashicons-editor-help, #updraft-central-navigation > .top_menu_right > span.dashicons-admin-tools, #updraftcentral_updraftplus_actions > button, .updraftcentral_row_site_buttons > .updraftcentral_row_container > .btn-group > button').on('click', function(event, data) {
399 // For actions that executes cascading/multiple processes in a single click
400 // we add them in the exceptions collection to avoid showing the message when running
401 // a legitimate multi-process action.
402 var exceptions = ['updraftcentral_site_analytics_show'];
403 var element = $(this);
404
405 exceptions = $.map(exceptions, function(value, index) {
406 if (element.hasClass(value)) {
407 return value;
408 }
409 });
410
411 if (0 < $.active && 0 === exceptions.length) {
412 self.ajax_request_processing = true;
413 }
414 });
415
416 $('#updraft-central-navigation').on('updraftcentral_nav_button_click', function(event, data) {
417 if (0 < $.active) {
418 self.ajax_request_processing = true;
419 }
420 });
421
422 $('#updraftcentral_dashboard').on('updraftcentral_dialog_opened', function(event) {
423 if ($.fullscreen.isFullScreen()) {
424 var dashboard_fullscreen = $('#updraftcentral_dashboard.updraft-fullscreen');
425 var backdrop = dashboard_fullscreen.find('div.modal-backdrop');
426 if (0 === backdrop.length) {
427 $(document.body).find('.modal-backdrop.show').appendTo(dashboard_fullscreen);
428 }
429 }
430 });
431
432 $('#updraftcentral_dashboard').on('updraftcentral_dialog_closed', function(event) {
433 if ($.fullscreen.isFullScreen()) {
434 var bootbox_modal = $('#updraftcentral_dashboard.updraft-fullscreen div.bootbox.modal.show');
435 var modal = $('#updraftcentral_dashboard.updraft-fullscreen #updraftcentral_modal_dialog.modal.show');
436 if (0 === bootbox_modal.length && 0 === modal.length) {
437 var backdrop = $('#updraftcentral_dashboard.updraft-fullscreen div.modal-backdrop.show');
438 if (backdrop.length) backdrop.remove();
439 }
440 }
441 });
442
443 $(window).resize(function() {
444 var w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
445 if (w <= mobile_width) {
446 var init_width = $("#updraft-central-navigation-sidebar").css('width');
447 var init_left = $("#updraft-central-navigation-sidebar").position().left;
448
449 if (0 <= init_left) {
450 $('#updraft-mobile-menu').trigger('click');
451 }
452
453 if (default_collapse_width+'px' === init_width) {
454 $('#updraft-central-sidebar-button').trigger('click');
455 }
456
457 if ($("#updraft-central-navigation-sidebar").hasClass('active')) {
458 $('#updraft-central-content').prepend('<div class="mobile-menu-backdrop"></div>');
459 }
460
461 $('#updraftcentral_dashboard').on('click', function(event) {
462 if ('updraft-central-navigation-sidebar' === $(event.target).attr('id') || $(event.target).hasClass('updraft-mobile-menu'))
463 return;
464
465 if ($("#updraft-central-navigation-sidebar").hasClass('active')) {
466 $("#updraft-central-navigation-sidebar").toggleClass("active");
467 $('#updraft-central-content > .mobile-menu-backdrop').remove();
468 }
469 });
470
471 } else {
472 $("#updraft-central-navigation-sidebar").removeClass("active");
473 $('#updraft-central-content > .mobile-menu-backdrop').remove();
474 }
475 });
476
477 // Trigger the resize function initially to do the routine that handles the visibility
478 // of the sidebar navigation elements
479 $(window).trigger('resize');
480 }
481
482 /**
483 * Saves user-defined keyboard shortcut entered by the user
484 *
485 * @params {$shortcut_name} The name of the shortcut to be overriden
486 * @params {$shortcut_key} The new shortcut key entered by the user
487 * @returns {object} jQuery promise object
488 */
489 this.save_shortcut = function (shortcut_name, shortcut_key) {
490 var deferred = $.Deferred();
491
492 UpdraftCentral.send_ajax('shortcuts', { name: shortcut_name, key: shortcut_key }, null, 'via_mothership_encrypting', null, function(resp, code, error_code) {
493 if ('ok' === code) {
494 if (resp.hasOwnProperty('message')) {
495 if ('success' === resp.message) {
496 deferred.resolve();
497 } else {
498 deferred.reject();
499 }
500 }
501 }
502 });
503
504 return deferred.promise();
505 }
506
507 /**
508 * Loads user-defined keyboard shortcuts
509 *
510 * @returns {object} jQuery promise object
511 */
512 this.load_shortcuts = function () {
513 var deferred = $.Deferred();
514
515 UpdraftCentral.send_ajax('shortcuts', {}, null, 'via_mothership_encrypting', null, function(resp, code, error_code) {
516 if ('ok' === code) {
517 if (resp.hasOwnProperty('shortcuts')) {
518 deferred.resolve(resp.shortcuts);
519 }
520 }
521 });
522
523 return deferred.promise();
524 }
525
526 /**
527 * Add sortable feature to div "updraftcentral_dashboard_existingsites"
528 *
529 * Send a final site order as an indexed array of id's in sorted order to manage_site_order in backend.
530 *
531 * returns 'failure message as response'
532 */
533 this.site_order = function () {
534 $("#updraftcentral_dashboard_existingsites").sortable({
535 axis: 'y',
536
537 // handle the start event (end of drag/sort)
538 start: function (event, ui) {
539 // close menu
540 $(".updraft_site_actions").removeClass("open");
541 },
542 // handle the stop event (end of drag/sort)
543 stop: function (event, ui) {
544 site_order_array = $(this).sortable("toArray",{attribute: "data-site_id"});
545 UpdraftCentral.send_ajax('manage_site_order', {site_order: site_order_array}, null, 'via_mothership_encrypting', null, function(resp, code, error_code) {
546
547 if ('ok' == code) {
548 if (resp.hasOwnProperty('message')) {
549 // only need to trap fail as success and nochange require no action
550 if (resp.message === "fail" ) {
551 UpdraftCentral_Library.dialog.alert(udclion.error_saving_site_order);
552 }
553 }
554 } else {
555 console.log("Expected site order data not found:");
556 console.log(resp);
557 }
558 });
559 }
560 });
561 }
562 this.site_order();
563
564 /**
565 * A Handlebarsjs helper function that is used to compare
566 * two values if they are equal. Please refer to the example below.
567 * Assuming "comment_status" contains the value of "spam".
568 *
569 * @param {mixed} a The first value to compare
570 * @param {mixed} b The second value to compare
571 *
572 * @example
573 * // returns "<span>I am spam!</span>", otherwise "<span>I am not a spam!</span>"
574 * {{#ifeq "spam" comment_status}}
575 * <span>I am spam!</span>
576 * {{else}}
577 * <span>I am not a spam!</span>
578 * {{/ifeq}}
579 *
580 * @return {string}
581 */
582 Handlebars.registerHelper('ifeq', function (a, b, opts) {
583 if ('string' !== typeof a && 'undefined' !== typeof a && null !== a) a = a.toString();
584 if ('string' !== typeof b && 'undefined' !== typeof b && null !== b) b = b.toString();
585 if (a === b) {
586 return opts.fn(this);
587 } else {
588 return opts.inverse(this);
589 }
590 });
591
592 /**
593 * A Handlebarsjs helper function that is used to compare
594 * two values if they are not equal. Please refer to the example below.
595 * Assuming "user_id" contains the value of "123".
596 *
597 * @param {mixed} a The first value to compare
598 * @param {mixed} b The second value to compare
599 *
600 * @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}}
601 *
602 * @return {string}
603 */
604 Handlebars.registerHelper('ifneq', function (a, b, opts) {
605 if (typeof a !== 'string') a = a.toString();
606 if (typeof b !== 'string') b = b.toString();
607 if (a !== b) {
608 return opts.fn(this);
609 } else {
610 return opts.inverse(this);
611 }
612 });
613
614 /**
615 * A Handlebarsjs helper function that is used to compare two values
616 * if they are equal. Specifically use to render a "selected" or "checked"
617 * attribute to a dropdown option or checkbox element. Please refer to the example below.
618 * Assuming "default_pingback_flag" contains the value of "1".
619 *
620 * @param {mixed} a The first value to compare
621 * @param {mixed} b The second value to compare
622 * @param {string} attr The attribute to render
623 *
624 * @example returns 'checked="checked"', otherwise "" <input name="default_pingback_flag" type="checkbox" value="1" {{ifset default_pingback_flag 1 'checked'}}>
625 *
626 * @return {string}
627 */
628 Handlebars.registerHelper('ifset', function (a, b, attr) {
629 if (typeof a !== 'string') a = a.toString();
630 if (typeof b !== 'string') b = b.toString();
631 if (a === b) {
632 return new Handlebars.SafeString(attr + '="' + attr + '"');
633 } else {
634 return '';
635 }
636 });
637
638 /**
639 * A Handlebarsjs helper function that is used to check if a certain
640 * value is empty, if so then add the specified attribute(s).
641 *
642 * @param {mixed} a The value to check
643 * @param {string} attrs The attribute(s) to render
644 *
645 * @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'}}>
646 *
647 * @return {string}
648 */
649 Handlebars.registerHelper('ifempty', function (a, attrs) {
650 if ('undefined' === typeof a || !a || !a.length) {
651 return new Handlebars.SafeString(attrs);
652 } else {
653 return '';
654 }
655 });
656
657 /**
658 * Set the current site row
659 *
660 * N.B. - primarily used for mass updates, needed by the automatic backup process but
661 * can always be used for whatever purpose it may serve.
662 *
663 * @param {Object} $site_row - A jQuery object representing the site row of the currently
664 * process site.
665 * @returns {void}
666 */
667 this.set_current_site_row = function($site_row) {
668 UpdraftCentral.$site_row = $site_row;
669 }
670
671 /**
672 * Registers an event handler for a particular event
673 *
674 * N.B. - Ensures that we don't register the same event handler twice
675 * by unbinding the same event attached to the selector/element.
676 *
677 * @param {string} event - A string representation of the event to bind (e.g. 'click', 'change', etc.).
678 * @param {string} selector - Any valid jQuery selector where you want to bound the event.
679 * @param {function} callback - A callback function to trigger when the event is raised on the given selector/element.
680 * @returns {void}
681 */
682 this.register_event_handler = function(event, selector, callback) {
683 jQuery(document).off(event, selector).on(event, selector, function(e) {
684 // Check and verify that a process is currently not running before
685 // executing the below code to prevent from abruptly aborting the current process
686 // which may lead to JS errors or/and inconsistency of information displayed to the user
687 if (self.check_processing_state(e)) return;
688
689 var params = [];
690 if ('undefined' !== typeof callback.arguments && callback.arguments && callback.arguments.length) params = callback.arguments;
691
692 callback.apply(this, params);
693 });
694 }
695
696 /**
697 * Sets an area to a loading style
698 *
699 * @param {Object} $container - the jQuery object of the area to be set as loading
700 * @returns {void}
701 */
702 this.set_loading = function ($container) {
703 $container.css('opacity', '0.3');
704 $container.find('button').attr('disabled', true);
705 $container.find('input[type="button"]').attr('disabled', true);
706 }
707
708 /**
709 * Removes the loading style from an area
710 *
711 * @param {Object} $container - the jQuery object of the area to be set as loading
712 * @param {string} html - a string of html to place into the finished loaded area
713 * @returns {Object} a jQuery promsise with The response from the server
714 */
715 this.done_loading = function ($container, html) {
716 var deferred = jQuery.Deferred();
717 $container.css('opacity', '1.0');
718 if (html) {
719 $container.slideUp(500, function () {
720 $container.html(html);
721 deferred.resolve();
722 }).slideDown(500);
723 } else {
724 $container.find('button').attr('disabled', false);
725 $container.find('input[type="button"]').attr('disabled', false);
726 deferred.resolve();
727 }
728 return deferred.promise();
729 }
730
731
732 /**
733 * Set the debugging level
734 *
735 * @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
736 * @returns {void}
737 */
738 this.set_debug_level = function(debug_level) {
739 updraftcentral_debug_level = debug_level;
740 }
741
742 /**
743 * Get the current debugging level
744 *
745 * @returns {number} - the debugging level (@see set_debug_level)
746 */
747 this.get_debug_level = function() {
748 return updraftcentral_debug_level;
749 }
750
751 /**
752 * Triggers the callback function for the modal's close event
753 *
754 * @param {callback|null} callback - a callback function to be called when the close button (either the "Close" or "X" button) is clicked
755 * @returns {void}
756 */
757 this.initiate_modal_close_listener = function(callback) {
758 // Listener for modal close and x buttons.
759 $('.modal-dialog button[data-dismiss="modal"]').on('click', function() {
760 if ('function' === typeof callback && callback) {
761 callback.apply(null, []);
762
763 // We'll make sure that after the callback is called we must invalidate
764 // the listener since this is only applicable when the close_callback is
765 // set or defined under the UpdraftCentral.open_modal.
766 $('.modal-dialog button[data-dismiss="modal"]').off('click');
767 }
768
769 // Trigger dashboard-wide dialog closed event (applies to both bootbox and bootstrap modal)
770 $('#updraftcentral_dashboard').trigger('updraftcentral_dialog_closed');
771 });
772 }
773
774 /**
775 * 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.
776 *
777 * @param {string} title - the title to use for the modal window
778 * @param {string} body - the HTML contents to place in the modal window
779 * @param {callback|true} action_button_callback - a callback to call when the main action button is pressed; or just true to close the modal
780 * @param {string|false} [action_button_text="Go"] - text for the action button; or, if false, an indication that there should be no action button
781 * @param {callback|null} [pre_open_callback=null] - an optional callback to call immediately before opening the modal
782 * @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
783 * @param {string} [extra_classes=''] - extra CSS classes for the modal dialog (e.g. modal-lg)
784 * @param {callback|null} close_callback - an optional callback to be called when the modal is closed
785 * @param {callback|null} [post_open_callback=null] - an optional callback to call immediately after opening the modal
786 * @returns {void}
787 */
788 this.open_modal = function(title, body, action_button_callback, action_button_text, pre_open_callback, sanitize_body, extra_classes, close_callback, post_open_callback) {
789 action_button_text = typeof action_button_text !== 'undefined' ? action_button_text : udclion.go;
790 // By default, we assume that the input is potentially evil, and sanitize it
791 sanitize_body = typeof sanitize_body !== 'undefined' ? sanitize_body : true;
792 extra_classes = typeof extra_classes !== 'undefined' ? extra_classes : '';
793
794 // Reset the modal's CSS classes
795 $('#updraftcentral_modal_dialog .modal-dialog').removeClass().addClass('modal-dialog '+extra_classes);
796
797 $('#updraftcentral_modal_dialog .modal-title').html(title);
798 if (sanitize_body) body = UpdraftCentral_Library.sanitize_html(body);
799 $('#updraftcentral_modal_dialog .modal-body').html(body);
800 if (false === action_button_text) {
801 $('#updraftcentral_modal_dialog button.updraft_modal_button_goahead').hide();
802 } else {
803 $('#updraftcentral_modal_dialog button.updraft_modal_button_goahead').html(action_button_text).show();
804 }
805 modal_action_callback = action_button_callback;
806 if (typeof pre_open_callback !== 'undefined' && null !== pre_open_callback) pre_open_callback.call(this);
807
808 // Add listener and callback handler for the modal's close buttons
809 UpdraftCentral.initiate_modal_close_listener(close_callback);
810
811 $('#updraftcentral_modal_dialog').modal();
812
813 if ($('#updraftcentral_modal_dialog #updraftcentral_addsite_tabs').length) {
814 $('#updraftcentral_addsite_tabs').tabs().addClass('ui-tabs-vertical ui-helper-clearfix');
815 }
816
817 // Trigger dashboard-wide dialog opened event (applies to both bootbox and bootstrap modal)
818 $('#updraftcentral_dashboard').trigger('updraftcentral_dialog_opened');
819
820 if ('undefined' !== typeof post_open_callback && null !== post_open_callback) post_open_callback.call(this);
821 }
822
823 /**
824 * Given a site row, send back a suitable HTML site description
825 *
826 * @param {Object} $site_row - the jQuery object for the row of the site
827 *
828 * @returns {string} - an HTML string describing the site
829 */
830 this.get_site_heading = function($site_row) {
831
832 var site_description = $site_row.data('site_description');
833 var site_url = $site_row.data('site_url');
834 if (site_description == site_url) { site_description = ''; }
835
836 var site_heading;
837 if (site_description) {
838 site_heading = '<a href="'+site_url+'">'+site_description+'</a>';
839 } else {
840 site_heading = '<a href="'+site_url+'">'+site_url+'</a>';
841 }
842
843 return site_heading;
844 }
845
846 /**
847 * Close the modal dialog
848 *
849 * @returns {void}
850 */
851 this.close_modal = function() {
852 $('#updraftcentral_modal_dialog').modal('hide');
853
854 // Trigger dialog closed event (applies to both bootbox and bootstrap modal)
855 $('#updraftcentral_dashboard').trigger('updraftcentral_dialog_closed');
856 }
857
858 /**
859 * A jQuery callback for row click events
860 *
861 * @callable rowclickerCallback
862 * @param {string} $site_row - the jQuery row object for the site that the click was for
863 * @param {Number} site_id - the site ID for the site that the click was for
864 * @param {Object} event - the event received from jQuery
865 *
866 * @return {*} prevent_default - if anything other than (boolean)true, then event.preventDefault() is called
867 */
868
869 /**
870 * De-register all row-clickers. The normal use of this is when switching tabs.
871 *
872 * @returns {void}
873 */
874 function deregister_row_clickers() {
875 $('#updraftcentral_dashboard_existingsites_container').off();
876 }
877
878 /**
879 * De-register all events on the modal. The normal use of this is when switching tabs.
880 *
881 * @returns {void}
882 */
883 function deregister_modal_listeners() {
884 $('#updraftcentral_modal').off();
885 }
886
887 /**
888 * Register click events for specified items in the UpdraftCentral site list (prevents repeating lots of jQuery boilerplate).
889 * 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).
890 *
891 * @param {string} selector - the selector to use
892 * @param {rowclickerCallback} callback - callback function that will be called upon the click event
893 * @param {boolean} [hide_other_sites=false] - if set, then the click will cause other sites in the tab to be hidden
894 * @param {string} [on_event='click'] - the event type to listen for. In the special case of 'keypress', the default event will not be prevented
895 * @returns {void}
896 */
897 this.register_row_clicker = function(selector, callback, hide_other_sites, on_event) {
898 on_event = typeof on_event !== 'undefined' ? on_event : 'click';
899 hide_other_sites = typeof hide_other_sites !== 'undefined' ? hide_other_sites : false;
900 params = {};
901 $('#updraftcentral_dashboard_existingsites_container').on(on_event, '.updraftcentral_site_row '+selector, params, function(event) {
902 // Check and verify that a process is currently not running before
903 // executing the below code to prevent from abruptly aborting the current process
904 // which may lead to JS errors or/and inconsistency of information displayed to the user
905 if (self.check_processing_state(event)) return;
906
907 if (on_event != 'keypress') { event.preventDefault(); }
908 UpdraftCentral.$site_row = $(this).closest('.updraftcentral_site_row');
909 var site_id = UpdraftCentral.$site_row.data('site_id');
910 if (hide_other_sites) {
911 $('#updraftcentral_dashboard_existingsites .updraftcentral_site_row:not([data-site_id="'+site_id+'"]), #updraftcentral_dashboard_existingsites .updraftcentral_row_divider').slideUp();
912 $('.updraftcentral_mode_actions .updraftcentral_action_choose_another_site').show();
913 }
914 callback.call(this, UpdraftCentral.$site_row, site_id, event);
915 });
916 }
917 var register_row_clicker = this.register_row_clicker;
918
919 /**
920 * Register click events for specified items in the UpdraftCentral modal (prevents repeating lots of jQuery boilerplate).
921 * 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).
922 *
923 * @param {string} selector - the selector to use
924 * @param {rowclickerCallback} callback - callback function that will be called upon the click event
925 * @param {string} [on_event='click'] - the event type to listen for. In the special case of 'keypress', the default event will not be prevented
926 *
927 * @returns {void}
928 */
929 this.register_modal_listener = function(selector, callback, on_event) {
930 on_event = typeof on_event !== 'undefined' ? on_event : 'click';
931 params = {};
932 $('#updraftcentral_modal').on(on_event, selector, params, function(event) {
933 callback.call(this, event);
934 });
935 }
936
937 $('#updraftcentral_modal_dialog button.updraft_modal_button_goahead').click(function() {
938 if (true === modal_action_callback) {
939 this.close_modal();
940 } else {
941 modal_action_callback.call(this);
942 }
943 });
944
945 /**
946 * JQuery callback for row click events
947 *
948 * @param {Object} $listener_row - the jQuery object of the listener itself
949 * @param {Object} $site_row - the jQuery row object for the site that the click was for
950 * @param {Number} site_id - the site ID for the site that this is a listener for
951 * @param {*} [data] - the returned data from the polling operation (if it is that sort of listener)
952 *
953 * @callable listenerCallback
954 *
955 * @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.
956 */
957
958 var listener_processors = {};
959 /**
960 * Register a listener callback - a callback function to be used in association with dashboard notices which poll and update
961 *
962 * @param {string} listener_type - an identifying string, indicating the listener type
963 * @param {listenerCallback} callback - a listener callback function
964 *
965 * @see create_dashboard_listener
966 *
967 * @returns {void}
968 */
969 this.register_listener_processor = function(listener_type, callback) {
970 listener_processors[listener_type] = callback;
971 }
972
973 /**
974 * Poll all listener rows on the dashboard for activity
975 *
976 * @returns {void}
977 */
978 function poll_listeners() {
979
980 // var listener_calls = {};
981
982 $('#updraftcentral_notice_container .updraftcentral_listener').each(function(ind) {
983 var site_id = $(this).data('site_id');
984 var listener_type = $(this).data('type');
985 var $listener_row = this;
986 var $site_row = $('#updraftcentral_dashboard_existingsites .updraftcentral_site_row[data-site_id="'+site_id+'"');
987 var finished = $(this).data('finished');
988
989 if (finished) { return; }
990
991 if (updraftcentral_debug_level > 1) {
992 console.log("poll_listeners(): site_id="+site_id+", listener_type="+listener_type);
993 }
994
995 if ($site_row.length > 0 && listener_processors.hasOwnProperty(listener_type)) {
996 // if (typeof listener_calls[site_id] === 'undefined') listener_calls[site_id] = [];
997 var call_this = listener_processors[listener_type].call(this, $listener_row, $site_row, site_id);
998 // 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.
999 if (0 === call_this) {
1000 $(this).data('finished', true);
1001 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_listener_finished_'+listener_type, {
1002 site_id: site_id,
1003 site_row: $site_row,
1004 listener_row: $listener_row,
1005 listener_type: listener_type
1006 });
1007 $($listener_row).clearQueue().delay(10000).slideUp('slow', function() {
1008 $(this).remove();
1009 });
1010 } else if (1 === call_this) {
1011 $(this).data('finished', true);
1012 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_listener_finished_'+listener_type, {
1013 site_id: site_id,
1014 site_row: $site_row,
1015 listener_row: $listener_row,
1016 listener_type: listener_type
1017 });
1018 } else if (null != call_this && call_this.hasOwnProperty('call')) {
1019 var call_type = call_this.call;
1020 UpdraftCentral.send_site_rpc(call_this.call, call_this.data, $site_row, function(response, code, error_code) {
1021 if ('ok' == code && false !== response && response.hasOwnProperty('data')) {
1022 if (listener_processors.hasOwnProperty(call_type)) {
1023 listener_processors[call_type].call(this, $listener_row, $site_row, site_id, response.data);
1024 } else {
1025 console.log("UpdraftCentral: listener type "+call_type+" has no registered processor (dump of all registered processors follows)");
1026 console.log(listener_processors);
1027 }
1028 }
1029 });
1030 }
1031 } else if ($site_row.length > 0) {
1032 console.log("UpdraftCentral: listener type "+listener_type+" has no registered processor (dump of all registered processors follows)");
1033 console.log(listener_processors);
1034 } else {
1035 console.log("UpdraftCentral: listener for site_id="+site_id+" with type "+listener_type+": site row not found");
1036 }
1037 });
1038
1039 }
1040
1041 setInterval(function() {
1042 poll_listeners();
1043 }, listener_poll_interval);
1044
1045 // A separate ud_rpc object for each site
1046 var ud_rpcs = [];
1047
1048 /**
1049 * 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.
1050 *
1051 * @param {Object} $site_row - the jQuery object for the site row
1052 *
1053 * @returns {string} - the URL
1054 */
1055 this.get_contact_url = function($site_row) {
1056 // 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.
1057 var admin_url = $site_row.data('admin_url').replace(/\/+$/, '');
1058 return admin_url+'/admin-ajax.php';
1059 }
1060
1061 /**
1062 * 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()
1063 *
1064 * @param {Object} $site_row - the jQuery object for the site row
1065 * @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.
1066 *
1067 * @returns {Object} - the UpdraftPlus_Remote_Communications object
1068 *
1069 * @uses get_udrpc
1070 */
1071 function get_site_udrpc($site_row, connection_method_config) {
1072
1073 var site_remote_public_key = $site_row.data('site_remote_public_key');
1074 var site_local_private_key = $site_row.data('site_local_private_key');
1075 var site_url = this.get_contact_url($site_row);
1076 var site_id = $site_row.data('site_id');
1077 var key_name_indicator = $site_row.data('key_name_indicator');
1078 var remote_user_id = $site_row.data('remote_user_id');
1079
1080 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'; }
1081
1082 // The connection method ought not to be via_mothership - such sites shouldn't be being routed into here (but via_mothership_encrypting is allowed)
1083 if ('via_mothership_encrypting' == connection_method_config) {
1084 console.warn("UpdraftCentral: A site ("+site_id+", "+site_url+") routed via_mothership_encrypting was passed into get_site_udrpc");
1085 console.log($site_row);
1086 }
1087
1088 var message_wrapper = false;
1089
1090 if ('direct_default_auth' == connection_method_config) {
1091 // 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.
1092 var is_firefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
1093 // N.B. If they're set to use Digest authentication, this should not use manual - should switch back
1094 // connection_method = (is_firefox) ? 'direct_manual_auth' : 'direct_jquery_auth';
1095 // Actually, 'jQuery method' also works in Firefox
1096 connection_method = 'direct_jquery_auth';
1097 } else {
1098 connection_method = connection_method_config;
1099
1100 // 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.
1101
1102 if ('via_mothership' == connection_method) {
1103
1104 message_wrapper = {
1105 action: 'updraftcentral_dashboard_ajax',
1106 subaction: 'site_rpc',
1107 component: 'dashboard',
1108 nonce: udclion.updraftcentral_dashboard_nonce,
1109 site_id: site_id,
1110 site_rpc_preencrypted: 1
1111 };
1112
1113 }
1114
1115 }
1116
1117 var send_cors_headers = $site_row.data('send_cors_headers');
1118 if ('undefined' === typeof send_cors_headers) { send_cors_headers = 1; }
1119
1120 var auth_method = ('direct_manual_auth' == connection_method) ? 'manual' : 'jquery';
1121
1122 var http_credentials = {};
1123
1124 var comms_url = site_url;
1125
1126 // When routing via the mothership, don't put in credentials, as the mothership will do that
1127 if ('via_mothership_encrypting' != connection_method && 'via_mothership' != connection_method) {
1128 var http_username = $site_row.data('http_username');
1129 if ('undefined' !== typeof http_username && http_username) {
1130 http_credentials.username = http_username;
1131 var http_password = $site_row.data('http_password');
1132 if ('undefined' !== typeof http_password) {
1133 http_credentials.password = http_password;
1134 }
1135 }
1136 } else {
1137 comms_url = udclion.ajaxurl;
1138 }
1139
1140 if (updraftcentral_debug_level > 0) {
1141 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);
1142 if (updraftcentral_debug_level > 1) {
1143 console.log("Remote public key follows");
1144 console.log(site_remote_public_key);
1145 }
1146 }
1147
1148 var reuse_id = site_id+' '+connection_method;
1149
1150 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);
1151
1152 }
1153
1154 /**
1155 * 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.
1156 *
1157 * @param {number} reuse_id - A unique ID, that can be used for re-using of the result
1158 * @param {number} remote_user_id - The ID of the user on the remote WP site that the keys are for
1159 * @param {string} key_name_indicator - The key name indicator (which indicates to the remote site which key to use to decrypt the message)
1160 * @param {string} site_remote_public_key - The RSA public key for contacting the remote site, in PEM format
1161 * @param {string} site_local_private_key - The RSA private key for the local site, in PEM format
1162 * @param {string} site_url - The URL for the remote site
1163 * @param {boolean} [cors_headers_wanted=true] - Whether to request that the remote application sets CORS headers with its reply
1164 * @param {Object} [http_credentials={}] - an object with any HTTP credentials to be set (useful properties: username, password)
1165 * @param {string} [auth_method] - the authentication method to use ('jquery' or 'manual')
1166 * @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.
1167 *
1168 * @returns {Object} - the UpdraftPlus_Remote_Communications object
1169 */
1170 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) {
1171 if ('undefined' != typeof ud_rpcs[reuse_id]) {
1172 ud_rpc = ud_rpcs[reuse_id];
1173 } else {
1174 cors_headers_wanted = (typeof cors_headers_wanted === 'undefined') ? true : cors_headers_wanted;
1175 var ud_rpc = new UpdraftPlus_Remote_Communications(key_name_indicator);
1176 ud_rpc.set_key_local(site_local_private_key);
1177 ud_rpc.set_key_remote(site_remote_public_key);
1178 ud_rpc.activate_replay_protection();
1179
1180 var url_match = /\/admin-ajax.php$/;
1181 if (url_match.test(site_url)) {
1182 // 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.
1183 site_url = site_url + '?action=updraft_central';
1184 }
1185
1186 ud_rpc.set_destination_url(site_url);
1187 if ('undefined' != typeof http_credentials) { ud_rpc.set_http_credentials(http_credentials); }
1188 if ('undefined' != typeof auth_method) { ud_rpc.set_auth_method(auth_method); }
1189 if ('undefined' != typeof message_wrapper && false !== message_wrapper) {
1190 ud_rpc.set_message_wrapper(message_wrapper);
1191 ud_rpc.set_message_unwrapper(function(response) {
1192 var processed = process_direct_ajax_response(response, 2, false);
1193 if (true === processed) {
1194 if (response.hasOwnProperty('wrapped_response')) {
1195 return response.wrapped_response;
1196 } else {
1197 processed = 'wrapped_response_not_found';
1198 }
1199 }
1200 console.error("UDRPC: Attempt to unwrap the message failed (code: "+processed+")");
1201 // This is usually redundant - something further down the line will log it
1202 if (updraftcentral_debug_level > 1) {
1203 console.log(response);
1204 }
1205 return false;
1206 });
1207 }
1208 ud_rpc.set_cors_headers_wanted(cors_headers_wanted);
1209 ud_rpcs[reuse_id] = ud_rpc;
1210 }
1211 if (updraftcentral_debug_level > 0) {
1212 // UDRPC, at debug level 2, console.log()s lots of cryptographic internals which are only really needed when debugging that
1213 var ud_rpc_debug_level = (updraftcentral_debug_level > 2) ? 2 : 1;
1214 ud_rpc.set_debug_level(ud_rpc_debug_level);
1215 }
1216 return ud_rpc;
1217 }
1218
1219 /**
1220 * An ajaxCallback
1221 *
1222 * @callable ajaxCallback
1223 * @param {*} response - the response data for the result of the call
1224 * @param {String} [code] - the response code; can be 'error' in the case of an error
1225 * @param {String} [error_code] - in the case of code being 'error', this contains the error code
1226 */
1227
1228 /**
1229 * 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.
1230 *
1231 * @param {ajaxCallback} response - callback that will be called with the results of the AJAX call
1232 * @param {string} [code] - the response code; can be 'error' in the case of an error
1233 * @param {string} [error_code] - in the case of code being 'error', this contains the error code
1234 * @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)
1235 * @param {ajaxCallback} response_callback - callback that will be called with the results of the AJAX call
1236 * @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)
1237 * @param {Object} $site_row - the jQuery object for the row of the site that the request is being sent to
1238 * @returns {void}
1239 */
1240 function process_ajax_response(response, code, error_code, is_site_rpc, response_callback, allow_visual_responses, $site_row) {
1241
1242 var website = ('undefined' !== typeof $site_row && $site_row && $site_row.length) ? $site_row.data('site_description')+' - ' : '';
1243
1244 allow_visual_responses = ('undefined' === typeof allow_visual_responses) ? true : allow_visual_responses;
1245
1246 // 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.
1247
1248 if ('error' == code) {
1249 console.error("process_ajax_response: return code: "+code+", error_code: "+error_code+" - parsed response follows");
1250 console.log(response);
1251 } else if (updraftcentral_debug_level > 0) {
1252 console.log("process_ajax_response: return code: "+code+" - parsed response follows");
1253 console.log(response);
1254 }
1255
1256 if (is_site_rpc && 'ok' == code && response.hasOwnProperty('response') && 'rpcerror' == response.response) {
1257 code = 'error';
1258 error_code = 'rpc_unknown_error';
1259
1260 if (response.hasOwnProperty('data') && response.data.hasOwnProperty('code')) {
1261 error_code = response.data.code;
1262 console.error("UpdraftCentral: RPC: Error occurred ("+error_code+"); data follows");
1263 console.log(response.data);
1264 response = response.data.data;
1265
1266 var handled = response_callback.call(this, response, code, error_code);
1267
1268 if (true !== handled) {
1269 // A default message for if we don't recognise the code
1270 var dash_message = udclion.js_exception_occurred+' ('+error_code+')';
1271 // Get the error's own message, if we know about it
1272 if (udclion.rpcerrors.hasOwnProperty(error_code)) { dash_message = udclion.rpcerrors[error_code]; }
1273
1274 if (allow_visual_responses) { UpdraftCentral_Library.dialog.alert('<h2>'+website+udclion.communications_error+'</h2>'+dash_message); }
1275 }
1276
1277 return;
1278 }
1279 }
1280
1281 if (code == 'error') {
1282
1283 var msg = udclion.general_js_comms_failure;
1284 // 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
1285 var is_comms_failure = true;
1286 var title = udclion.error;
1287
1288 // If the response didn't unwrap, it may be an error response.
1289 if (2 == is_site_rpc && 'unwrapper_failure' == error_code && response.hasOwnProperty('code')) { error_code = response.code; }
1290
1291 if ('json_parse_fail' == error_code) {
1292 if (response.indexOf('<html') > -1) {
1293 console.error("UpdraftCentral: JSON parse fail: looks like html was returned - remote plugin is probably not installed/inactive/blocked");
1294 msg = udclion.general_js_comms_failure;
1295 title = udclion.communications_error;
1296 }
1297 } else if ('response_empty' == error_code || 'http_post_fail' == error_code) {
1298 msg = udclion.general_js_comms_failure;
1299 title = udclion.communications_error;
1300 } else if ('timeout' == error_code) {
1301 msg = udclion.comms_failure_timeout;
1302 title = udclion.communications_error+' - '+udclion.timeout;
1303 } else if ('unauthorized' == error_code) {
1304 msg = udclion.comms_failure_unauthorised;
1305 title = udclion.communications_error;
1306 } else if ('unknown_response' == error_code) {
1307 msg = udclion.unknown_response;
1308 title = udclion.communications_error;
1309 } else if ('cannot_contact_localdev' == error_code) {
1310 title = udclion.communications_error;
1311 msg = response.message;
1312 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) {
1313 msg += '<br>'+udclion.localdev_can_work_better_with_https;
1314 }
1315 } 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) {
1316 msg = response.message;
1317 msg += "<br>"+udclion.digest_auth_not_supported;
1318 } 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) {
1319 msg = udclion.comms_failure_unauthorised+' <a href="#" class="updraftcentral_site_editdescription">'+udclion.open_site_configuration+'...</a>';
1320 } else if (response.hasOwnProperty('message')) {
1321 msg = response.message;
1322 if (!is_site_rpc) { is_comms_failure = false; }
1323 } else {
1324 is_comms_failure = false;
1325 msg += '<br>'+udclion.error_code+': '+error_code;
1326 }
1327
1328 // ns_error_dom_bad_uri: access to restricted uri denied - Firefox
1329 if (response.hasOwnProperty('status') && 401 == response.status) {
1330 msg = udclion.comms_failure_unauthorised+' <a href="#" class="updraftcentral_site_editdescription">'+udclion.open_site_configuration+'...</a>';
1331 } else if ('ns_error_dom_bad_uri: access to restricted uri denied' == error_code) {
1332 msg = udclion.comms_failure_unauthorised_by_browser+' <a href="#" class="updraftcentral_site_editdescription">'+udclion.open_site_configuration+'...</a>';
1333 }
1334
1335 msg = '<p>'+msg+'</p>';
1336
1337 if (is_comms_failure) {
1338 msg += '<p><a href="'+udclion.common_urls.connection_checklist+'">'+udclion.go_here_for_connection_help+'</a></p>';
1339 msg += '<p><a href="#" class="updraftcentral_test_other_connection_methods">'+udclion.test_other_connection_methods+'</a></p>';
1340 }
1341
1342 if (response.hasOwnProperty('status') && 200 != response.status && 0 != response.status) {
1343 msg += '<p>'+udclion.http_response_status+': '+response.status+'</p>';
1344 }
1345
1346 if (allow_visual_responses) { UpdraftCentral_Library.dialog.alert('<h2>'+website+title+'</h2>'+msg); }
1347 }
1348
1349 if (is_site_rpc && response.hasOwnProperty('data') && null != response.data) {
1350 if (response.data.hasOwnProperty('php_events')) {
1351 $.each(response.data.php_events, function(index, logline) {
1352 console.log("UpdraftCentral: PHP event on remote side: "+logline);
1353 });
1354 }
1355 if (response.data.hasOwnProperty('caught_output')) {
1356 console.log("UpdraftCentral: direct output on remote side: "+response.data.caught_output);
1357 }
1358 if (response.data.hasOwnProperty('php_events') || response.data.hasOwnProperty('caught_output')) {
1359 response.data = response.data.previous_data;
1360 }
1361 }
1362
1363 response_callback.call(this, response, code, error_code);
1364 }
1365
1366 /**
1367 * Process responses received back from the mothership over AJAX. This will do some processing, and then call process_ajax_response()
1368 *
1369 * @param {string} response - the response received
1370 * @param {boolean} is_site_rpc - whether it was command to a remote site or not.
1371 * @param {ajaxCallback|boolean} response_callback - callback that will be called with the results of the AJAX call - or, to not call, false
1372 * @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
1373 * @param {Object} $site_row - the jQuery object for the row of the site that the request is being sent to
1374 *
1375 * @returns {boolean|string} - If the parsing did not turn up any errors, true is return; otherwise, an error code.
1376 */
1377 function process_direct_ajax_response(response, is_site_rpc, response_callback, allow_visual_responses, $site_row) {
1378
1379 allow_visual_responses = ('undefined' === typeof allow_visual_responses) ? true : allow_visual_responses;
1380
1381 // AJAX via the mothership comes with its results wrapped
1382
1383 if (response.hasOwnProperty('responsetype') && 'error' == response.responsetype) {
1384 if (response.hasOwnProperty('message')) { console.error("UpdraftCentral error via AJAX: "+response.message); }
1385 if ('cannot_contact_localdev' == response.code) { response.request_info = { method: method, use_method: use_method} }
1386 if (false !== response_callback) {
1387 process_ajax_response(response, 'error', response.code, is_site_rpc, response_callback, allow_visual_responses, $site_row);
1388 }
1389 return response.code;
1390 }
1391
1392 if (!response.hasOwnProperty('message') && !response.hasOwnProperty('code')) {
1393 console.log(response);
1394 if (false !== response_callback) {
1395 process_ajax_response(response, 'error', 'unknown_response', is_site_rpc, response_callback, allow_visual_responses, $site_row);
1396 }
1397 return 'unknown_response';
1398 }
1399
1400 if (updraftcentral_debug_level > 1) {
1401 console.log(response.responsetype+': '+response.message);
1402 }
1403
1404 // When doing site RPC, the remote site's reply is in the 'data' attribute
1405 if (is_site_rpc) {
1406
1407 if (response.hasOwnProperty('php_events')) {
1408 $.each(response.php_events, function(index, logline) {
1409 console.info("UpdraftCentral: PHP event on remote side: "+logline);
1410 });
1411 }
1412
1413 if (response.hasOwnProperty('mothership_caught_output')) {
1414 console.info("UpdraftCentral: direct output on remote side: "+response.caught_output);
1415 }
1416
1417 // This is set for a successful communication
1418 if (response.hasOwnProperty('rpc_response')) {
1419 response = response.rpc_response;
1420 }
1421 }
1422
1423 if (false !== response_callback) {
1424 process_ajax_response(response, 'ok', null, is_site_rpc, response_callback, allow_visual_responses, $site_row);
1425 }
1426
1427 return true;
1428 }
1429
1430 /**
1431 * Sends a remote command via AJAX - either directly, or via the site that this plugin is installed upon.
1432 *
1433 * @param {String} command - the command to send
1434 * @param {*} data - data to send with the remote request
1435 * @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
1436 * @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.
1437 * @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
1438 * @param {ajaxCallback} response_callback - callback that will be called with the results of the AJAX call
1439 * @param {Number} [timeout=30] - the number of seconds to allow before the call times out
1440 * @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
1441 *
1442 * @uses process_ajax_response
1443 */
1444
1445 this.send_ajax = function(command, data, $site_row, connection_method, spinner_where, response_callback, timeout, allow_visual_responses) {
1446
1447 var website = ('undefined' !== typeof $site_row && $site_row && $site_row.length) ? $site_row.data('site_description')+' - ' : '';
1448
1449 connection_method = typeof connection_method !== 'undefined' ? connection_method : 'direct_default_auth';
1450 timeout = typeof timeout !== 'undefined' ? timeout : 30;
1451 spinner_where = typeof spinner_where !== 'undefined' ? spinner_where : null;
1452 allow_visual_responses = ('undefined' === typeof allow_visual_responses) ? true : allow_visual_responses;
1453
1454 // Boil it down to one of 'direct', 'server', 'server_proxies' (i.e. factor out the sub-methods)
1455 var ajax_method = ('via_mothership' == connection_method || 'via_mothership_encrypting' == connection_method) ? ('via_mothership' == connection_method ? 'server_proxies' : 'server') : 'direct';
1456
1457 var is_site_rpc = (null === $site_row) ? false : true;
1458
1459 if (is_site_rpc) {
1460 var unlicensed = $site_row.data('site_unlicensed');
1461 if ('undefined' !== typeof unlicensed && unlicensed) {
1462 UpdraftCentral_Library.dialog.alert('<h2>'+website+udclion.error+'</h2>'+udclion.site_unlicensed_message);
1463 return;
1464 }
1465 }
1466
1467 if (spinner_where) {
1468 // $(spinner_where).addClass('updraftcentral_spinner');
1469 $(spinner_where).prepend('<div class="updraftcentral_spinner"></div>');
1470 }
1471
1472 if ('direct' == ajax_method && 'https:'== document.location.protocol) {
1473 var site_url = this.get_contact_url($site_row);
1474 if (site_url.substring(0, 5).toLowerCase() == 'http:') {
1475 // Mixed content policy in all mainstream desktop browsers forbids requests to HTTP from HTTPS domains
1476 ajax_method = 'server';
1477 }
1478 }
1479
1480 if (updraftcentral_debug_level > 0) {
1481 console.log("send_message(ajax_method="+ajax_method+", requested_method="+connection_method+", command="+command+", data(follows))");
1482 console.log(data);
1483 }
1484
1485 if ('direct' == ajax_method || 'server_proxies' == ajax_method) {
1486
1487 if (!is_site_rpc) { throw 'send_ajax() called with direct method ('+connection_method+'), but no site row object passed in'; }
1488
1489 var ud_rpc = get_site_udrpc($site_row, connection_method);
1490 ud_rpc.send_message(command, data, timeout, function(response, code, error_code) {
1491
1492 if (spinner_where) {
1493 $(spinner_where).removeClass('updraftcentral_spinner');
1494 $(spinner_where).children('.updraftcentral_spinner').remove();
1495 }
1496
1497 if (updraftcentral_debug_level > 2) {
1498 console.log("Raw response, pre-processing, follows");
1499 console.log(response);
1500 }
1501
1502 var is_site_rpc_flag = ('server_proxies' == ajax_method) ? 2 : 1;
1503
1504 try {
1505 process_ajax_response(response, code, error_code, is_site_rpc_flag, response_callback, allow_visual_responses, $site_row);
1506 } catch (e) {
1507 UpdraftCentral_Library.dialog.alert('<h2>'+website+udclion.error+'</h2>'+udclion.js_exception_occurred+'<br>'+e.toString());
1508 console.log(e);
1509 }
1510 });
1511
1512
1513 } else {
1514 // 'server' == ajax_method
1515
1516 var site_id = 0;
1517 if (null !== $site_row) {
1518 site_id = $site_row.data('site_id');
1519 }
1520
1521 var ajax_subaction = (is_site_rpc) ? 'site_rpc' : command;
1522
1523 var ajax_data = (is_site_rpc) ? { command: command, data: data } : data;
1524
1525 var ajax_options = {
1526 type: 'POST',
1527 url: udclion.ajaxurl,
1528 timeout: (timeout * 1000), // In ms
1529 headers: {
1530 'X-Secondary-User-Agent': 'UpdraftCentral-dashboard.js/'+udclion.udc_version
1531 },
1532 data: {
1533 action: 'updraftcentral_dashboard_ajax',
1534 subaction: ajax_subaction,
1535 component: 'dashboard',
1536 nonce: udclion.updraftcentral_dashboard_nonce,
1537 site_id: site_id,
1538 data: ajax_data
1539 },
1540 dataType: 'text',
1541 success: function(response) {
1542
1543 if (spinner_where) {
1544 $(spinner_where).children('.updraftcentral_spinner').remove();
1545 // $(spinner_where).removeClass('updraftcentral_spinner');
1546 }
1547
1548 if ('undefined' === typeof response || '' === response) {
1549 console.log("UDRPC: the response from the remote site was empty");
1550 process_ajax_response(response, 'error', 'response_empty', is_site_rpc, response_callback, allow_visual_responses, $site_row);
1551 return;
1552 }
1553
1554 try {
1555 var parsed_response = JSON.parse(response);
1556 } catch (e) {
1557
1558 var valid_json = response.match(/\{"format":.*}/);
1559
1560 if (null === valid_json) {
1561 console.log(e);
1562 console.log(response);
1563 process_ajax_response(response, 'error', 'json_parse_fail', is_site_rpc, response_callback, allow_visual_responses, $site_row);
1564 return;
1565 } else {
1566 response = valid_json[0];
1567 try {
1568 var parsed_response = JSON.parse(response);
1569 console.log("UpdraftCentral: successfully parsed JSON after removing unwanted elements");
1570 console.log(response);
1571 } catch (e) {
1572 console.log(e);
1573 console.log(response);
1574 process_ajax_response(response, 'error', 'json_parse_fail', is_site_rpc, response_callback, allow_visual_responses, $site_row);
1575 return;
1576 }
1577 }
1578
1579 }
1580
1581 response = parsed_response;
1582
1583 process_direct_ajax_response(response, is_site_rpc, response_callback, allow_visual_responses, $site_row);
1584
1585 },
1586 error: function(request, status, error_thrown) {
1587
1588 if (spinner_where) {
1589 $(spinner_where).children('.updraftcentral_spinner').remove();
1590 // $(spinner_where).removeClass('updraftcentral_spinner');
1591 }
1592
1593 console.error("UpdraftCentral: Error in AJAX operation");
1594 console.log(request);
1595 console.log(status);
1596 // 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."'
1597 // "Unauthorized" is what you get when HTTP authentication is required. "Timeout" when there's a timeout.
1598 console.error(error_thrown);
1599
1600 if ('' == error_thrown) { error_thrown = 'http_post_fail'; }
1601
1602 if (error_thrown.hasOwnProperty('statusText')) {
1603 error_thrown = error_thrown.statusText.toString();
1604 }
1605
1606 if ('function' === typeof error_thrown.toLowerCase) {
1607 error_thrown = error_thrown.toLowerCase();
1608 } else {
1609 try {
1610 var tmp = error_thrown.toString().toLowerCase();
1611 if (tmp) { error_thrown = tmp; }
1612 } catch (e) {
1613 }
1614 }
1615
1616 process_ajax_response(request, 'error', error_thrown, is_site_rpc, response_callback, allow_visual_responses, $site_row);
1617 }
1618 }
1619
1620 if (updraftcentral_debug_level > 1) {
1621 console.log("UpdraftCentral: jQuery POST: options follow:");
1622 console.log(ajax_options);
1623 }
1624
1625 jQuery.ajax(ajax_options);
1626
1627 }
1628
1629 }
1630
1631 /**
1632 * Set up menu navigation for each site row item. This should be called after any actions that replace the HTML of row items
1633 *
1634 * @returns {void}
1635 */
1636 function setup_menunav() {
1637 // This is no longer needed.
1638 // $('#updraftcentral_dashboard .updraft-dropdown-menu').dropit();
1639 var how_many_sites = $('#updraftcentral_dashboard_existingsites .updraftcentral_site_row:not(.updraft_site_unlicensed)').length;
1640 $('#updraftcentral_licences_in_use').html(how_many_sites);
1641 }
1642
1643 $(window).resize(function() {
1644 var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
1645 if (width > mobile_width) {
1646 $('#updraftcentral_dashboard #updraft-central-navigation-sidebar').show();
1647 }
1648 });
1649
1650 // Toggle the mobile menu on/off, if at a relevant width
1651 $('#updraftcentral_dashboard .updraft-mobile-menu').on('click', function() {
1652 // Currently only using the width.
1653 // var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
1654 var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
1655 if (width <= mobile_width) {
1656 $("#updraft-central-navigation-sidebar").toggleClass("active");
1657 if ($("#updraft-central-navigation-sidebar").hasClass('active')) {
1658 $('#updraft-central-content').prepend('<div class="mobile-menu-backdrop"></div>');
1659 } else {
1660 $('#updraft-central-content > .mobile-menu-backdrop').remove();
1661 }
1662 }
1663 });
1664
1665
1666
1667 /**
1668 * 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.
1669 *
1670 * @param {string} html - the HTML to place within the site list container in the dashboard
1671 * @returns {void}
1672 */
1673 this.set_existing_sites_to = function(html) {
1674 // Reset the connection objects, as the IDs and credentials/options may have changed
1675 ud_rpcs = [];
1676 $('#updraftcentral_dashboard_existingsites').html(html);
1677 // Show/hide the relevant buttons/sections for the current tab
1678 UpdraftCentral.set_dashboard_mode(true, true);
1679 setup_menunav();
1680 }
1681
1682 /**
1683 * Adds a dashboard notice only if a notice doesnt exist with the same identifier
1684 *
1685 * @param {string} message - The message text to display
1686 * @param {string} [level="notice"] - The level for the notice. Can also start with 'listener_', which is styled as if it were 'info'
1687 * @param {Number|bool} [remove_after=30000] - The number of milliseconds to remove the notice after; or, 0|false to not remove
1688 * @param {Object} [extra_data={}] - Extra data to store with the dashboard notice (via data attributes)
1689 * @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
1690 *
1691 * @returns {Object|false} the jQuery object for the newly created notice or false if a notice with this identifier already exists
1692 */
1693 this.add_dashboard_notice_singleton = function(message, level, remove_after, extra_data, identifier) {
1694 level = ('undefined' === typeof level) ? 'notice' : level;
1695 remove_after = ('undefined' === typeof remove_after) ? 30000 : remove_after;
1696 extra_data = ('undefined' === typeof extradata) ? {} : extradata;
1697 identifier = ('undefined' === typeof identifier) ? '' : identifier;
1698 extra_data.identifier = identifier;
1699
1700 if (0 === $('#updraftcentral_notice_container .updraftcentral_notice[data-identifier="'+identifier+'"]').length) {
1701 return this.add_dashboard_notice(message, level, remove_after, extra_data);
1702 }
1703 return false;
1704 }
1705
1706 /**
1707 * Adds a dashboard notice
1708 *
1709 * @param {string} message - The message text to display
1710 * @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)
1711 * @param {Number|bool} [remove_after=30000] - The number of milliseconds to remove the notice after; or, 0|false to not remove
1712 * @param {Object} [extra_data={}] - Extra data to store with the dashboard notice (via data attributes)
1713 *
1714 * @returns {Object} the jQuery object for the newly created notice
1715 */
1716 this.add_dashboard_notice = function(message, level, remove_after, extra_data) {
1717 remove_after = typeof remove_after !== 'undefined' ? remove_after : 30000;
1718 extra_data = typeof extra_data !== 'undefined' ? extra_data : { };
1719 level = typeof level !== 'undefined' ? level : 'notice';
1720 var type = 'notice';
1721 var extra_classes = '';
1722
1723 if ('listener_' == level.substr(0, 9)) {
1724 type = 'listener';
1725 extra_classes = 'updraftcentral_listener updraftcentral_listener_'+level.substr(9);
1726 extra_data.type = level.substr(9);
1727 level = 'info';
1728 }
1729
1730 $container = $('#updraftcentral_notice_container');
1731
1732 var newnotice_container_opener = '<div class="updraftcentral_notice updraftcentral_notice_new updraftcentral_notice_level_'+level+' '+extra_classes+'"';
1733 $.each(extra_data, function(key, val) {
1734 newnotice_container_opener += 'data-'+key+'="'+UpdraftCentral_Library.quote_attribute(val)+'"';
1735 });
1736
1737 var $newnotice = $(newnotice_container_opener+'><button type="button" class="updraftcentral_notice_dismiss"></button><div class="updraftcentral_notice_contents">'+message+'</div></div>');
1738 $container.append($newnotice);
1739 if (remove_after) {
1740 $newnotice.slideDown('medium').delay(30000).slideUp('slow', function() {
1741 $(this).remove();
1742 });
1743 } else {
1744 $newnotice.slideDown('medium');
1745 }
1746
1747 return $newnotice;
1748 }
1749
1750 /**
1751 * Creates a special type of dashboard notice which polls for status updates
1752 *
1753 * @param {string} type - Listener type (an identifying string) (not shown; stored and used for CSS classes)
1754 * @param {Object} $site_row - a jQuery object identifying the site row that the listener is associated with
1755 * @param {string} message - HTML to be placed in the dashboard notice
1756 * @param {*} [data={}] - Data associated with the listener (which will be stored in an HTML data attribute)
1757 * @param {string} [title] - HTML to be used as the notice title. If not specified, a default will be used.
1758 *
1759 * @see register_listener_processor
1760 *
1761 * @returns {Object} the jQuery object for the newly created notice
1762 */
1763 this.create_dashboard_listener = function(type, $site_row, message, data, title) {
1764 data = ('undefined' === typeof data) ? {} : data;
1765 data.site_url = $site_row.data('site_url');
1766 data.site_id = $site_row.data('site_id');
1767 var listener_title = (typeof title === 'undefined') ? '<h2>'+$site_row.data('site_description')+'</h2>' : title;
1768 return this.add_dashboard_notice(listener_title+message, 'listener_'+type, false, data);
1769 }
1770
1771 // Only trigger a removal if the close button is directly in the notice. This allows other sub-elements to re-use the style class.
1772 $('#updraftcentral_notice_container').on('click', '.updraftcentral_notice > .updraftcentral_notice_dismiss', function() {
1773 $(this).parents('.updraftcentral_notice').clearQueue().slideUp('slow', function() {
1774 (this).remove();
1775 });
1776 });
1777
1778 /**
1779 * Get the current dashboard mode
1780 *
1781 * @returns {string} - the current dashboard mode
1782 */
1783 this.get_dashboard_mode = function() {
1784 return $('#updraftcentral_dashboard').data('updraftcentral_mode');
1785 }
1786
1787 /**
1788 * Checks whether an ajax request is currently processing
1789 *
1790 * @param {object} [e] - An optional event object passed by the callee to prevent further action
1791 * @returns {boolean}
1792 */
1793 this.check_processing_state = function(e) {
1794 // Prevent going into another section or area while a process is
1795 // currently running.
1796 if (self.ajax_request_processing) {
1797 if ('undefined' !== typeof e) e.preventDefault();
1798
1799 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.notice_heading+'</h2>'+udclion.currently_processing);
1800 return true;
1801 }
1802
1803 return false;
1804 }
1805
1806 /**
1807 * Set up the dashboard, by hiding things that don't belong in the currently active tab
1808 *
1809 * @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).
1810 * @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)
1811 * @param {boolean} [reset=false] - indicates whether the site list has been reset and was triggered by the "choose another site" (reset) button
1812 * @returns {void}
1813 */
1814 this.set_dashboard_mode = function (new_mode, force, reset) {
1815
1816 force = ('undefined' === typeof force) ? false : true;
1817 reset = ('undefined' === typeof reset) ? false : true;
1818 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_dashboard_mode_pre_set', { force: force, new_mode: new_mode, reset: reset });
1819
1820 // Check and verify that a process is currently not running before
1821 // executing the below code to prevent from abruptly aborting the current process
1822 // which may lead to JS errors or/and inconsistency of information displayed to the user
1823 if (self.check_processing_state()) return;
1824
1825
1826 var current_mode = this.get_dashboard_mode();
1827
1828 if (true === new_mode) { new_mode = current_mode; }
1829
1830 if (!force && new_mode == current_mode) { return; }
1831
1832 var extra_contents = $('#updraftcentral_dashboard_existingsites_container .updraftcentral_row_extracontents');
1833 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_dashboard_mode_set_before', { new_mode: new_mode, previous_mode: current_mode, force: force, extra_contents: extra_contents });
1834
1835 if (current_mode) { $('#updraftcentral_dashboard').removeClass('updraftcentral_mode_'+current_mode); }
1836
1837 $('#updraftcentral_dashboard_existingsites_container .updraftcentral_row_extracontents').empty();
1838
1839 // Show all sites again
1840 $('#updraftcentral_dashboard_existingsites .updraftcentral_site_row, #updraftcentral_dashboard_existingsites .updraftcentral_row_divider').show();
1841
1842 $('#updraftcentral_dashboard').data('updraftcentral_mode', new_mode);
1843 $('#updraftcentral_dashboard').addClass('updraftcentral_mode_'+new_mode);
1844 $('#updraft-menu-item-'+current_mode).removeClass('updraft-menu-item-links-active');
1845 $('#updraft-menu-item-'+new_mode).addClass('updraft-menu-item-links-active');
1846
1847 // 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.
1848 $('#updraftcentral_dashboard .updraftcentral-hide-in-other-tabs:not(.updraftcentral-show-in-tab-'+new_mode+'), #updraftcentral_dashboard .updraftcentral-hide-in-tab-'+new_mode).hide();
1849 $('#updraftcentral_dashboard .updraftcentral-show-in-tab-'+new_mode+' .updraftcentral-hide-in-tab-initially').hide();
1850 $('#updraftcentral_dashboard .updraftcentral-show-in-tab-'+new_mode+', #updraftcentral_dashboard .updraftcentral-show-in-other-tabs:not(.updraftcentral-hide-in-tab-'+new_mode+')').slideDown(1);
1851
1852 deregister_row_clickers();
1853 deregister_modal_listeners();
1854
1855 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_dashboard_mode_set', { new_mode: new_mode, previous_mode: current_mode });
1856 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_dashboard_mode_set_'+new_mode, { new_mode: new_mode, previous_mode: current_mode });
1857
1858 $('#updraftcentral_dashboard_existingsites').trigger('updraftcentral_dashboard_mode_set_after', { new_mode: new_mode, previous_mode: current_mode });
1859 }
1860
1861 $('.updraftcentral_mode_actions .updraftcentral_action_choose_another_site').click(function() {
1862 UpdraftCentral.set_dashboard_mode(true, true, true);
1863 });
1864
1865 $('#updraft-central-navigation-sidebar').off('click', '.updraft-menu-item').on('click', '.updraft-menu-item', function(e) {
1866 e.stopPropagation();
1867
1868 var item_dom_id = $(this).attr('id');
1869 if ('undefined' === typeof item_dom_id) { return; }
1870 if ('updraft-menu-item-' != item_dom_id.substring(0, 18)) {
1871 console.log("UDCentral: menu item without the ID in the expected format");
1872 console.log(this);
1873 return;
1874 }
1875
1876 var new_mode = item_dom_id.substring(18);
1877 UpdraftCentral.set_dashboard_mode(new_mode);
1878
1879 var w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
1880 if (w <= mobile_width) {
1881 $("#updraft-central-navigation-sidebar").toggleClass("active");
1882 if ($("#updraft-central-navigation-sidebar").hasClass("active")) {
1883 $('#updraft-central-content').prepend('<div class="mobile-menu-backdrop"></div>');
1884 } else {
1885 $('#updraft-central-content > .mobile-menu-backdrop').remove();
1886 }
1887 } else {
1888 $('#updraft-central-content > .mobile-menu-backdrop').remove();
1889 }
1890 });
1891
1892 $("#updraft-central-sidebar-button").click(function() {
1893 var defaultWidth = 200,
1894 collapse = false;
1895 var toggleWidth = $("#updraft-central-navigation-sidebar").width() > default_collapse_width ? default_collapse_width+"px" : defaultWidth + "px";
1896
1897 $("#updraft-central-navigation-sidebar").animate({
1898 width: toggleWidth
1899 }, {
1900 step: function( now, fx ) {
1901 var $label = $('#'+fx.elem.id).find('div.updraft-menu-item > span.menu-label');
1902 var $visibility_icon = $('#'+fx.elem.id).find('.module-visibility');
1903 var $hidden_modules_label = $('#hidden-modules-container').find('.uc-hidden-modules-label');
1904 var $show_all = $('#updraft-menu-item-all');
1905 if (120 > now) {
1906 $label.hide();
1907 $visibility_icon.hide();
1908 $hidden_modules_label.hide();
1909 $show_all.html('<span class="dashicons dashicons-visibility"></span>');
1910 collapse = true;
1911 } else {
1912 $label.show();
1913 $visibility_icon.show();
1914 $hidden_modules_label.show();
1915 $show_all.html(udclion.show_all);
1916 collapse = false;
1917 $('span.module-visibility > span.dashicons-hidden').show();
1918 $('span.module-visibility > span.dashicons-visibility').show();
1919 }
1920 },
1921 complete: function() {
1922 if (collapse) {
1923 $('[data-toggle="tooltip"]').tooltip('enable');
1924 } else {
1925 $('[data-toggle="tooltip"]').tooltip('disable');
1926 }
1927 }
1928 });
1929 $(".updraft-central-sidebar-button-icon").toggle();
1930 });
1931
1932 $('#updraftcentral_dashboard .updraftcentral_action_box .updraftcentral_action_manage_sites').click(function() {
1933 UpdraftCentral.set_dashboard_mode('sites');
1934 });
1935
1936
1937
1938 /**
1939 * Do any processing necessary with the passed information about current status
1940 *
1941 * @param {Object} status_info - any recognised properties will be processed
1942 * @returns {void}
1943 */
1944 function process_sites_status_info(status_info) {
1945 if (status_info.hasOwnProperty('how_many_licences_in_use')) {
1946 $('.updraftcentral_licences_in_use').html(status_info.how_many_licences_in_use);
1947 }
1948 if (status_info.hasOwnProperty('how_many_licences_available')) {
1949 var display = (status_info.how_many_licences_available < 0) ? '&#8734;' : status_info.how_many_licences_available;
1950 $('.updraftcentral_licences_total').html(display);
1951 }
1952 }
1953
1954 /**
1955 * Handle any links to updraftplus.com/updraftcentral.com in a new window
1956 *
1957 * @param {string} href - The URL
1958 * @param {Object} [e] - a jQuery event to cancel if opening a new window
1959 */
1960 function redirect_updraft_website_links(href, e) {
1961 if ('undefined' === typeof href) { return; }
1962 if (null !== href.match(/https?:\/\/updraft(plus|central)\.com/)) {
1963 if ('undefined' !== typeof e) { e.preventDefault(); }
1964 var win = window.open(href, '_blank');
1965 UpdraftCentral_Library.focus_window_or_error(win);
1966 }
1967 }
1968
1969 $('#updraftcentral_dashboard_newsite').click(function() {
1970
1971 var advanced_site_options_html = UpdraftCentral.get_advanced_site_options_html({ http_username: '', http_password: ''});
1972
1973 UpdraftCentral.open_modal(udclion.add_site, UpdraftCentral.template_replace('sites-add-new-modal', { advanced_options: advanced_site_options_html }), function() {
1974
1975 var key = $('#updraftcentral_addsite_key').val();
1976 UpdraftCentral.close_modal();
1977
1978 if ('undefined' === typeof key || key === null || key === '') { return; }
1979
1980 var extra_site_info = UpdraftCentral_Library.get_serialized_options('#updraftcentral_modal #updraftcentral_editsite_expertoptions .expert_option');
1981 var send_cors_headers = $('#updraftcentral_modal #updraftcentral_site_send_cors_headers').is(':checked') ? 1 : 0;
1982 var connection_method = $('#updraftcentral_modal #updraftcentral_site_connection_method').val();
1983
1984 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) {
1985
1986 if ('ok' == code) {
1987
1988 if (resp.hasOwnProperty('message')) {
1989 add_dashboard_notice(resp.message, 'info');
1990 if (resp.hasOwnProperty('sites_html')) {
1991 UpdraftCentral.set_existing_sites_to(resp.sites_html);
1992 } else {
1993 console.log("Expected sites_html data not found:");
1994 console.log(resp);
1995 }
1996 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
1997 }
1998
1999 if (resp.hasOwnProperty('key_needs_sending')) {
2000
2001 var site_id = resp.key_needs_sending.key_site_id;
2002 var site_ajax_url = resp.key_needs_sending.url;
2003 var $site_row = $('#updraftcentral_dashboard_existingsites .updraftcentral_site_row[data-site_id="'+site_id+'"');
2004 var site_remote_public_key = resp.key_needs_sending.remote_public_key;
2005
2006 $($site_row).prepend('<div class="updraftcentral_spinner"></div>');
2007
2008 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));
2009 var win = window.open(send_key_url, '_blank', 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=600,height=320');
2010 UpdraftCentral_Library.focus_window_or_error(win);
2011 return;
2012
2013 }
2014 }
2015 });
2016 }, udclion.add_site, function() {
2017 $('#updraftcentral_modal #updraftcentral_site_send_cors_headers').prop('checked', true);
2018 } , false, 'addsite_dialog', null, function() {
2019 $('#updraftcentral_addsite_key').focus();
2020 });
2021 });
2022
2023 // Register the modal events which are active in the 'Sites' tab
2024 $('#updraftcentral_dashboard_existingsites').on('updraftcentral_dashboard_mode_set_sites', function(e) {
2025
2026 register_modal_listener('#updraftcentral_addsite_expertoptions_show', function(e) {
2027 $(this).slideUp();
2028 $('#updraftcentral_modal #updraftcentral_editsite_expertoptions .initially-hidden').show();
2029 e.preventDefault();
2030 });
2031
2032 });
2033
2034 // Put clicked links within the settings sections into their own tab
2035 $('#updraftcentral_notice_container').on('click', 'a', function(e) {
2036 var href = $(this).attr('href');
2037 redirect_updraft_website_links(href, e);
2038 });
2039
2040 // Register the row clickers and modal listeners which are active in every tab
2041 $('#updraftcentral_dashboard_existingsites').on('updraftcentral_dashboard_mode_set', function(event, data) {
2042
2043 var menu_label = $('#updraft-menu-item-'+data.new_mode).find('.menu-label').html();
2044 var actions_container = $('.updraftcentral_mode_actions');
2045 if (0 === actions_container.find('h2.screen-title').length) {
2046 actions_container.prepend('<h2 class="screen-title"></h2>');
2047 }
2048 actions_container.find('h2.screen-title').html(menu_label);
2049
2050 // Use a new browser portal for any clicks to updraftplus.com
2051 register_modal_listener('a', function(e) {
2052 var href = $(this).attr('href');
2053 redirect_updraft_website_links(href, e);
2054 });
2055
2056 // Put clicked links within the settings sections into their own tab
2057 $('#updraftcentral_dashboard_existingsites_container').on('click', '.updraftcentral_site_row a', function(e) {
2058 var href = $(this).attr('href');
2059 redirect_updraft_website_links(href, e);
2060 });
2061
2062 register_modal_listener('#updraft_debug_empty_browser_cache', function(e) {
2063
2064 var how_many = 0;
2065 var verbose = (updraftcentral_debug_level > 0) ? true : false;
2066
2067 for (var i = localStorage.length; i >= 0; --i) {
2068 var key = localStorage.key(i);
2069 if (key !== null && key.substr(0, 15) == 'updraftcentral_') {
2070 if (verbose) { console.log("UpdraftCentral: Removing key from local storage: "+key); }
2071 localStorage.removeItem(key);
2072 how_many++;
2073 }
2074 }
2075 if (how_many > 0) {
2076 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.empty+' '+udclion.browser_cache+'</h2>'+sprintf(udclion.cache_emptied, how_many));
2077 } else {
2078 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.empty+' '+udclion.browser_cache+'</h2>'+udclion.cache_no_contents);
2079 }
2080 });
2081
2082 register_modal_listener('#updraft_debug_show_browser_cache', function(e) {
2083 var how_many = 0;
2084 for (var i = 0, len = localStorage.length; i < len; ++i) {
2085 var key = localStorage.key(i);
2086 var value = localStorage.getItem(key);
2087 if (key.substr(0, 15) == 'updraftcentral_') {
2088 how_many++;
2089 console.log(key+": "+value);
2090 }
2091 }
2092 if (how_many > 0) {
2093 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.log_contents+'</h2>'+udclion.cache_contents_logged);
2094 } else {
2095 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.log_contents+'</h2>'+udclion.cache_no_contents);
2096 }
2097 });
2098
2099 // The 'upgrade' tab has no sites rows visible
2100 if (data && data.hasOwnProperty('new_mode') && data.new_mode == 'notices') { return; }
2101
2102 register_modal_listener('.updraftcentral_site_editdescription', function(e) {
2103 e.preventDefault();
2104 open_site_configuration(UpdraftCentral.$site_row);
2105 });
2106
2107 register_modal_listener('.updraftcentral_test_other_connection_methods', function(e) {
2108 e.preventDefault();
2109 UpdraftCentral_Library.open_connection_test(UpdraftCentral.$site_row);
2110 });
2111
2112 register_modal_listener('a.connection-test-switch', function(e) {
2113 e.preventDefault();
2114 var connection_method = $(this).data('connection_method');
2115
2116 UpdraftCentral.close_modal();
2117
2118 var site_id = $(this).data('site_id');
2119
2120 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) {
2121
2122 if ('ok' == code) {
2123
2124 if (resp.hasOwnProperty('message')) { add_dashboard_notice(resp.message); }
2125
2126 if (resp.hasOwnProperty('sites_html')) {
2127 UpdraftCentral.set_existing_sites_to(resp.sites_html);
2128 setup_menunav();
2129 } else {
2130 console.log(resp);
2131 add_dashboard_notice(udclion.unknown_response, 'error');
2132 }
2133 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
2134 }
2135 });
2136 });
2137
2138 register_modal_listener('.updraftcentral_siteinfo_results .phpinfo', function(e) {
2139 e.preventDefault();
2140 UpdraftCentral.send_site_rpc('core.phpinfo', null, UpdraftCentral.$site_row, function(response, code, error_code) {
2141 if ('ok' == code && response.data) {
2142 var output = '';
2143 $.each(response.data, function(name, section) {
2144 output += "<h3>"+name+"</h3>\n"+'<table>'+"\n";
2145 $.each(section, function(key, val) {
2146 if (val.constructor === Array) {
2147 output += "<tr><td>"+key+"</td><td>"+val[0]+"</td><td>"+val[1]+"</td></tr>\n";
2148 } else if (typeof val === 'string') {
2149 if ($.isNumeric(key)) {
2150 output += "<tr><td></td><td>"+val+"</td></tr>\n";
2151 } else {
2152 output += "<tr><td>"+key+"</td><td>"+val+"</td></tr>\n";
2153 }
2154 } else {
2155 console.log("UpdraftCentral: phpinfo: Unrecognised output for key "+key+" (follows)");
2156 console.log(val);
2157 }
2158 });
2159 output += "</table>\n";
2160 });
2161
2162 // N.B. open_modal() by default sanitizes the body data
2163 UpdraftCentral.open_modal(udclion.phpinfo, '<div id="updraftcentral_phpinfo_results">'+output+'</div>', null, false, null, true, 'modal-lg');
2164 }
2165 }, $(this));
2166 });
2167
2168 register_modal_listener('#updraftcentral_site_connection_method', function() {
2169 var site_connection_method = $('#updraftcentral_site_connection_method').val();
2170
2171 if (null == site_connection_method) { return; }
2172
2173 if (site_connection_method.substring(0, 7) == 'direct_' && 'https:' == document.location.protocol) {
2174 $('#updraftcentral_site_connection_method_message').show().html(udclion.http_must_go_via_mothership);
2175 } else {
2176 $('#updraftcentral_site_connection_method_message').hide();
2177 }
2178 }, 'change');
2179
2180 register_row_clicker('.updraftcentral_site_adddescription', function($site_row) {
2181 open_site_configuration($site_row);
2182 });
2183
2184 register_row_clicker('.updraftcentral_site_delete', function($site_row) {
2185 UpdraftCentral_Library.dialog.confirm('<h2>'+udclion.remove_site+'</h2><p>'+$site_row.data('site_url')+'</p><p>'+udclion.really_delete_site+'</p>', function(result) {
2186 if (!result) return;
2187 var site_id = UpdraftCentral.$site_row.data('site_id');
2188 if (!site_id) { return; }
2189 $site_row.slideUp('slow');
2190
2191 UpdraftCentral.send_ajax('delete_site', { site_id: site_id }, null, 'via_mothership_encrypting', '#updraftcentral_dashboard_existingsites', function(resp, code, error_code) {
2192 if ('ok' == code) {
2193 if (resp.hasOwnProperty('message')) {
2194 add_dashboard_notice(resp.message);
2195 }
2196 if (resp.hasOwnProperty('sites_html')) {
2197 UpdraftCentral.set_existing_sites_to(resp.sites_html);
2198 } else {
2199 console.log(resp);
2200 add_dashboard_notice(udclion.unknown_response, 'error');
2201 }
2202 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
2203 }
2204 });
2205
2206 });
2207 });
2208
2209 register_row_clicker('.row_siteinfo', function($site_row) {
2210 UpdraftCentral.send_site_rpc('core.site_info', null, $site_row, function(response, code, error_code) {
2211 if (updraftcentral_debug_level > 1) {
2212 console.log("send_site_rpc(site_info): parsed response follows");
2213 console.log(response);
2214 }
2215 if ('ok' == code) {
2216 if (false !== response) {
2217 var versions = response.data.versions;
2218 var bloginfo = response.data.bloginfo;
2219 var url = UpdraftCentral_Library.sanitize_html(bloginfo.url);
2220 var name = UpdraftCentral_Library.sanitize_html(bloginfo.name);
2221 // 'This site is running WordPress version %s (PHP %s, MySQL %s) and UpdraftPlus version %s (UDRPC version %s)'
2222 // 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));
2223 // add_dashboard_notice(message, 'info');
2224 // N.B. By default, open_modal() sanitizes the body.
2225 var ud_version = versions.ud;
2226 if ('none' == ud_version) { ud_version = udclion.updraftplus.version_none; }
2227 var message = sprintf(udclion.what_remote_running, versions.wp, versions.php, versions.mysql, ud_version, versions.udrpc_php);
2228 UpdraftCentral.open_modal(
2229 UpdraftCentral_Library.sanitize_html(bloginfo.name),
2230 UpdraftCentral.template_replace('dashboard-siteinfo', { url: url, message: message, phpinfo: udclion.phpinfo }),
2231 null,
2232 false
2233 );
2234 }
2235 }
2236 });
2237 });
2238
2239 register_row_clicker('.updraftcentral_site_dashboard', function($site_row) {
2240 UpdraftCentral_Library.open_browser_at($site_row);
2241 });
2242
2243 });
2244
2245 /**
2246 * Gets the HTML fragment for advanced site editing options
2247 *
2248 * @param {Object} values - the values to pass to the template
2249 *
2250 * @returns {string} - the HTML
2251 */
2252 this.get_advanced_site_options_html = function(values) {
2253 return UpdraftCentral.template_replace('sites-advanced-site-options', values);
2254 }
2255
2256 /**
2257 * Opens the site configuration dialog for the specified site
2258 *
2259 * @param {Object} $site_row - the jQuery row object for the site whose configuration is to be edited
2260 * @returns {void}
2261 */
2262 this.open_site_configuration = function($site_row) {
2263
2264 var site_url = $site_row.data('site_url');
2265
2266 var http_username = $site_row.data('http_username');
2267 if ('undefined' === typeof http_username) { http_username = ''; }
2268
2269 var http_password = $site_row.data('http_password');
2270 if ('undefined' === typeof http_password) { http_password = ''; }
2271
2272 var connection_method = $site_row.data('connection_method');
2273 if ('undefined' === typeof connection_method) { connection_method = 'direct_default_auth'; }
2274
2275 var http_authentication_method = $site_row.data('http_authentication_method');
2276 if ('undefined' === typeof http_authentication_method) { http_authentication_method = 'basic'; }
2277
2278 var existing_description = $site_row.data('site_description');
2279 if (existing_description == site_url) { existing_description = ''; }
2280
2281 var send_cors_headers = $site_row.data('send_cors_headers');
2282 if ('undefined' === typeof send_cors_headers || send_cors_headers) { send_cors_headers = 1; }
2283
2284 var advanced_site_options_html = UpdraftCentral.get_advanced_site_options_html({http_username: http_username, http_password: http_password});
2285
2286 UpdraftCentral.open_modal(udclion.edit_site_configuration, UpdraftCentral.template_replace('sites-edit-configuration', { site_url: site_url, advanced_options: advanced_site_options_html }, { existing_description: existing_description }), function() {
2287
2288 var description = $('#updraftcentral-edit-site-description').val();
2289
2290 var send_cors_headers = $('#updraftcentral_modal #updraftcentral_site_send_cors_headers').is(':checked') ? 1 : 0;
2291
2292 var connection_method = $('#updraftcentral_modal #updraftcentral_site_connection_method').val();
2293
2294 var site_id = $site_row.data('site_id');
2295 if (!site_id) { return; }
2296
2297 UpdraftCentral.close_modal();
2298
2299 var extra_site_info = UpdraftCentral_Library.get_serialized_options('#updraftcentral_modal .expert_option');
2300
2301 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) {
2302
2303 if ('ok' == code) {
2304
2305 if (resp.hasOwnProperty('message')) { add_dashboard_notice(resp.message); }
2306
2307 if (resp.hasOwnProperty('sites_html')) {
2308 UpdraftCentral.set_existing_sites_to(resp.sites_html);
2309 setup_menunav();
2310 } else {
2311 console.log(resp);
2312 add_dashboard_notice(udclion.unknown_response, 'error');
2313 }
2314 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
2315 }
2316 });
2317
2318 }, udclion.edit, function() {
2319 $('#updraftcentral_modal #updraftcentral_site_connection_method').val(connection_method).change();
2320 if (send_cors_headers) { $('#updraftcentral_modal #updraftcentral_site_send_cors_headers').prop('checked', true); }
2321 $('#updraftcentral_modal #updraftcentral_addsite_http_authentication_method').val(http_authentication_method);
2322 }, false);
2323 }
2324
2325 /**
2326 * RPCCallback
2327 *
2328 * @callable RPCCallback
2329 * @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.
2330 * @param {string} code - the code returned by the RPC call; currently possible values are 'ok' or 'error'
2331 * @param {string|null} error_code - the error code returned by the RPC call (if any).
2332 *
2333 * @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
2334 */
2335
2336 /**
2337 * 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
2338 *
2339 * @param {string} rpc_command - the command to send
2340 * @param {*} data - the data to send with the command
2341 * @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)
2342 * @param {number} [timeout=30] - the number of seconds for the timeout on the HTTP call
2343 */
2344 this.debugging_send_command = function(rpc_command, data, site_url, timeout) {
2345 var $site_row = $('#updraftcentral_dashboard_existingsites').find('.updraftcentral_site_row[data-site_url="'+site_url+'"]').first();
2346 if ($site_row.length < 1) {
2347 console.log("debugging_send_command: no corresponding row found for the specified URL");
2348 return;
2349 }
2350
2351 timeout = 'undefined' !== typeof timeout ? timeout : 30;
2352
2353 UpdraftCentral.send_site_rpc(rpc_command, data, $site_row, function(response, code, error_code) {
2354 // Nothing needs logging here, as other parts of the stack will already do that.
2355 }, null, timeout);
2356 }
2357
2358 /**
2359 * Helper method for the UpdraftCentral.is_serializable function which
2360 * checks whether the submitted object or property has plain/simple data types
2361 *
2362 * @see {UpdraftCentral.is_serializable}
2363 * @param {*} data - Any type of data for checking or validation.
2364 * @returns {boolean} - "true" if data has plain/simple type, "false" otherwise.
2365 */
2366 var is_plain_type = function (data) {
2367
2368 // N.B. We're not comparing the type for "null" since it will always return as object. Thus,
2369 // Giving a false positive when running the check against this method (is_plain_type), instead
2370 // We're comparing it by value. If a "null" value is encountered we consider it as plain
2371 // Since it doesn't reference any complex hierarchy other than being a "null".
2372
2373 if (data === null || typeof data === 'string' || typeof data === 'boolean' || typeof data === 'number' || typeof data === 'undefined' || jQuery.isPlainObject(data) || Array.isArray(data)) {
2374 return true;
2375 }
2376 return false;
2377 }
2378
2379 /**
2380 * Checks whether the submitted data is valid for serialization
2381 *
2382 * @borrows {UpdraftCentral#is_plain_type}
2383 * @param {*} data - Any type of data for checking or validation.
2384 * @param {undefined|null} field - An optional field passed around during the loop.
2385 * @param {string} path - An string representing the path where the error occurred within the data parameter hierarchy.
2386 * @returns {boolean} - "true" if data is valid for serialization, "false" otherwise.
2387 */
2388 this.is_serializable = function(data, field, path) {
2389 if ('undefined' === typeof path) path = 'data';
2390 var origin = path;
2391
2392 if (!is_plain_type(data)) {
2393 if ('undefined' !== typeof field && field) {
2394 return {
2395 status: false,
2396 data: data,
2397 error_path: path,
2398 error_field: field,
2399 error_type: typeof data[field],
2400 error_value: data[field]
2401 };
2402 } else {
2403 return { status: false };
2404 }
2405 }
2406
2407 for (var field in data) {
2408 path += (path && path.length) ? ' -> '+field : field;
2409
2410 if (!is_plain_type(data[field])) {
2411 return {
2412 status: false,
2413 data: data,
2414 error_path: path,
2415 error_field: field,
2416 error_type: typeof data[field],
2417 error_value: data[field]
2418 };
2419 }
2420 if ("object" === typeof data[field]) {
2421 var result = UpdraftCentral.is_serializable(data[field], field, path);
2422 if (result.hasOwnProperty('status') && !result.status) {
2423 return {
2424 status: false,
2425 data: result.data,
2426 error_path: result.error_path,
2427 error_field: result.error_field,
2428 error_type: typeof result.data[result.error_field],
2429 error_value: result.data[result.error_field]
2430 };
2431 } else {
2432 // Reset path if we received a valid data during iteration.
2433 path = origin;
2434 }
2435 }
2436 }
2437 return true;
2438 }
2439
2440 /**
2441 * Send a command to the remote site. This is a very thin wrapper around send_ajax.
2442 *
2443 * @param {string} rpc_command - the command to send
2444 * @param {*} data - the data to send with the command
2445 * @param {Object} $site_row - the jQuery object for the row of the site that the request is being sent to
2446 * @param {RPCCallback} callback - function to call with the results
2447 * @param {Object|null|false} spinner_where - jQuery object indicating where any spinner should be shown
2448 * @param {number} [timeout=30] - the number of seconds for the timeout on the HTTP call
2449 *
2450 * @uses send_ajax
2451 *
2452 * @returns {void}
2453 */
2454 this.send_site_rpc = function(rpc_command, data, $site_row, callback, spinner_where, timeout) {
2455
2456 var result = UpdraftCentral.is_serializable(data);
2457 if (data !== null && result.hasOwnProperty('status') && !result.status) {
2458 console.log('UpdraftCentral: send_site_rpc(' + rpc_command + ') - the submitted data parameter contains unserializable types (follows)');
2459 if (result.hasOwnProperty('error_field') && result.error_field) {
2460 console.log('Error path: '+result.error_path);
2461 console.log('Error field: '+result.error_field);
2462 console.log('Error type: '+result.error_type);
2463 console.log('Error value follows:');
2464 console.log(result.error_value);
2465 } else {
2466 console.log(data);
2467 }
2468
2469 callback.call(this, {
2470 error: udclion.js_exception_occurred
2471 }, 'error', null);
2472 } else {
2473 timeout = 'undefined' !== typeof timeout ? timeout : 30;
2474 var site_id = $site_row.data('site_id');
2475 if (!site_id) {
2476 console.log("UpdraftCentral: sent_site_rpc("+rpc_command+") command sent, but site ID could not be identified from the row (follows)");
2477 console.log($site_row);
2478 }
2479
2480 var connection_method = $site_row.data('connection_method');
2481
2482 if ('undefined' === typeof spinner_where || null === spinner_where) { spinner_where = $site_row; }
2483
2484 try {
2485 return UpdraftCentral.send_ajax(rpc_command, data, $site_row, connection_method, spinner_where, callback, timeout);
2486 } catch (e) {
2487 if (spinner_where) {
2488 $(spinner_where).children('.updraftcentral_spinner').remove();
2489 }
2490
2491 // Here, we're triggering the callback with a code 'error' and passing in
2492 // the error that was catched by the try-catch block. This should help the caller to handle the error by itself. By
2493 // returning "true" (boolean) it will bypass the default error dialog to display,
2494 // meaning, the error was already handled by the caller (e.g. displayed, etc.), otherwise, the default
2495 // dialog will be shown to the user.
2496 var is_error_handled = callback.call(this, {
2497 error: e.toString()
2498 }, 'error', null);
2499
2500 if (typeof is_error_handled === 'undefined' || !is_error_handled) {
2501 // add_dashboard_notice(udclion.js_exception_occurred+'<br>'+e.toString(), 'error');
2502 var website = ('undefined' !== typeof $site_row && $site_row.length) ? $site_row.data('site_description')+' - ' : '';
2503
2504 UpdraftCentral_Library.dialog.alert('<h2>'+website+udclion.error+'</h2>'+udclion.js_exception_occurred+'<br>'+e.toString());
2505 console.log(e);
2506 }
2507 }
2508 }
2509 }
2510
2511 $('#updraftcentral_dashboard .updraft-central-logo img').dblclick(function() {
2512 UpdraftCentral_Library.toggle_fullscreen();
2513 });
2514
2515 $('#updraft-central-navigation .updraft-full-screen').on('click', function() {
2516 UpdraftCentral_Library.toggle_fullscreen();
2517 });
2518
2519 $('#updraft-central-navigation .updraftcentral-help').on('click', function() {
2520 UpdraftCentral_Library.dialog.alert(UpdraftCentral.template_replace('dashboard-help', { uc_version: udclion.updraftcentral_version+': '+udclion.udc_version, running_on: UpdraftCentral.version_info_as_text() }));
2521 });
2522
2523 /**
2524 * Return a string with information on the current installation
2525 *
2526 * @returns {string} information on the current installation
2527 */
2528 this.version_info_as_text = function() {
2529 return 'WP/'+udclion.wp_version+' PHP/'+udclion.php_version+' MySQL/'+udclion.mysql_version+' Curl/'+udclion.curl_version;
2530 }
2531
2532 $('#updraft-central-navigation .updraftcentral-settings').on('click', function() {
2533 $('#updraft-central-navigation').trigger('updraftcentral_nav_button_click', { name: 'settings' });
2534
2535 // Check and verify that a process is currently not running before
2536 // executing the below code to prevent from abruptly aborting the current process
2537 // which may lead to JS errors or/and inconsistency of information displayed to the user
2538 if (self.check_processing_state()) return;
2539
2540 UpdraftCentral.open_modal(udclion.settings, UpdraftCentral.template_replace('dashboard-settings', {
2541 uc_version: udclion.updraftcentral_version+': '+udclion.udc_version,
2542 running_on: UpdraftCentral.version_info_as_text()
2543 }), function() {
2544 var new_debugging_level = $('#updraftcentral_debug_level').val();
2545 if (new_debugging_level >= 0 && new_debugging_level <=3) {
2546 UpdraftCentral.set_debug_level(new_debugging_level);
2547 }
2548 UpdraftCentral.close_modal();
2549 }, udclion.save_settings, function() {
2550 $('#updraftcentral_debug_level').val(updraftcentral_debug_level);
2551 });
2552
2553 });
2554
2555 // Refresh dashicon rotates after it has been clicked - stops when the settings are refreshed.
2556 $('.updraftcentral_row_extracontents').on('click', '.dashicons-image-rotate', function() {
2557 $('.dashicons-image-rotate').addClass('dashicon-image-rotating');
2558 });
2559
2560 /**
2561 * Returns the result of filling in the specified Handlebars (http://handlebarsjs.com) template with the provided values
2562 *
2563 * @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'
2564 * @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).
2565 * @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).
2566 *
2567 * @returns {string} The template with values filled in
2568 */
2569 this.template_replace = function(template_name, vars, attr_vars) {
2570 vars = ('undefined' === typeof vars) ? {} : vars;
2571 if (!UpdraftCentral_Handlebars.hasOwnProperty(template_name)) {
2572 console.log("UDCentral: UpdraftCentral_Handlebars template not found: "+template_name);
2573 console.log(UpdraftCentral_Handlebars);
2574 }
2575 if ('undefined' !== typeof attr_vars) {
2576 $.each(attr_vars, function(k, v) {
2577 vars[k] = UpdraftCentral_Library.quote_attribute(v);
2578 });
2579 }
2580 vars.udclion = udclion;
2581
2582 // Checks if the template was compiled by gulp-handlebars and not the default node compiler
2583 if ("object" === typeof UpdraftCentral_Handlebars[template_name]) {
2584 return UpdraftCentral_Handlebars[template_name].handlebars(vars)
2585 }
2586 return UpdraftCentral_Handlebars[template_name](vars);
2587 }
2588
2589 UpdraftCentral_Handlebars = (typeof UpdraftCentral_Handlebars === 'undefined') ? {} : UpdraftCentral_Handlebars;
2590
2591 Handlebars.registerHelper('uc_each', function(context, options) {
2592 var ret = "";
2593 if ('undefined' === typeof context) { return ret; }
2594 for (var i=0, j=context.length; i<j; i++) {
2595 var vars = context[i];
2596 vars.as_json = JSON.stringify(vars);
2597 vars.udclion = udclion;
2598 ret = ret + options.fn(vars);
2599 }
2600 return ret;
2601 });
2602
2603 /**
2604 * 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.
2605 *
2606 * @returns {void}
2607 */
2608 function compile_handlebars_templates() {
2609 // Initialise Handlebars.templates - it may not already exist
2610 if (!udclion.hasOwnProperty('handlebars')) return;
2611 if (udclion.handlebars.hasOwnProperty('compile')) {
2612 $.each(udclion.handlebars.compile, function(template_name, source) {
2613 console.log("UpdraftCentral: in developer mode: compile template: "+template_name);
2614 UpdraftCentral_Handlebars[template_name] = Handlebars.compile(source);
2615 });
2616 }
2617 }
2618
2619 compile_handlebars_templates();
2620
2621 setup_menunav();
2622
2623 set_dashboard_mode('sites');
2624
2625 if ('undefined' !== typeof Modernizr && !Modernizr.lastchild) {
2626 console.log("UDCentral: Unsupported web browser");
2627 $('#updraftcentral_dashboard_loading').fadeOut();
2628 $('#updraftcentral_updraftplus_actions, #updraftcentral_sites_actions, #updraftcentral_dashboard_existingsites_container').remove();
2629 this.add_dashboard_notice(udclion.unsupported_browser, 'error', false);
2630 } else {
2631
2632 $('#updraftcentral_dashboard_loading').fadeOut();
2633 $('#updraftcentral_dashboard_existingsites_container').fadeIn();
2634
2635 if (udclion.hasOwnProperty('show_licence_counts') && udclion.show_licence_counts) { $('.updraftcentral_licence_info').show(); }
2636
2637 // Refresh the sites list every 24 hours
2638 setInterval(function() {
2639 UpdraftCentral.send_ajax('sites_html', null, null, 'via_mothership_encrypting', '#updraftcentral_dashboard_existingsites', function(resp, code, error_code) {
2640 if ('ok' == code) {
2641 if (resp.hasOwnProperty('sites_html')) {
2642 UpdraftCentral.set_existing_sites_to(resp.sites_html);
2643 } else {
2644 console.log("Expected sites_html data not found:");
2645 console.log(resp);
2646 }
2647 if (resp.hasOwnProperty('status_info')) { process_sites_status_info(resp.status_info); }
2648 }
2649 });
2650 }, 86400000);
2651
2652 }
2653
2654 // Remove any indicated notices that came pre-printed on the page
2655 $('#updraftcentral_notice_container .updraftcentral_notice.remove_after_load').delay(30000).slideUp('slow', function() {
2656 $(this).remove();
2657 });
2658
2659 // 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)
2660 $('#updraftcentral_modal_dialog').appendTo(document.body);
2661
2662 /**
2663 * Stores persistent data in the browser, using the HTML5 local storage API. Uses a fixed prefix of 'updraftcentral_' to avoid clashing with other applications.
2664 *
2665 * 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.
2666 *
2667 * @param {string} key - storage key
2668 * @param {*} data - data to store; must be data than can be turned into JSON
2669 * @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.
2670 * @returns {void}
2671 */
2672 this.storage_set = function(key, data, can_expire) {
2673 if ('undefined' !== typeof can_expire && can_expire) {
2674 var epoch_time = Math.floor(Date.now() / 1000);
2675 localStorage.setItem('updraftcentral_saved_at_'+key, epoch_time);
2676 }
2677 if (updraftcentral_debug_level > 1) {
2678 console.log("UpdraftCentral.storage_set(key="+key+")");
2679 }
2680 localStorage.setItem('updraftcentral_'+key, JSON.stringify(data));
2681 }
2682
2683 /**
2684 * Retrieves stored data from the browser, using the HTML5 local storage API. Uses a fixed prefix of 'updraftcentral_' to avoid clashing with other applications.
2685 *
2686 * @param {string} key - storage key
2687 * @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.
2688 * @returns {*} - stored data. Returns null if the age check fails. The result for never-stored data is undefined.
2689 */
2690 this.storage_get = function(key, maximum_age) {
2691 if ('undefined' !== typeof maximum_age && maximum_age > 0) {
2692 var stored_at = localStorage.getItem('updraftcentral_saved_at_'+key);
2693 if (!stored_at) { return null; }
2694 var epoch_time = Math.floor(Date.now() / 1000);
2695 var stored_ago = epoch_time - stored_at;
2696 if (UpdraftCentral.updraftcentral_debug_level > 1) {
2697 console.log("UpdraftCentral.storage_get(key="+key+", maximum_age="+maximum_age+"): stored_at="+stored_at+", epoch_time="+epoch_time+", stored_ago="+stored_ago);
2698 }
2699
2700 if (stored_ago > maximum_age) { return null; }
2701 }
2702 var item = localStorage.getItem('updraftcentral_'+key);
2703 if ('undefined' === typeof item) { return null; }
2704 try {
2705 var parsed = JSON.parse(item);
2706 return parsed;
2707 } catch (e) {
2708 }
2709 return null;
2710 }
2711
2712 /**
2713 * Retrieves stored data from the browser, using the HTML5 local storage API. Uses a fixed prefix of 'updraftcentral_' to avoid clashing with other applications.
2714 *
2715 * @param {string} key - storage key
2716 * @returns {string} the stored value
2717 */
2718 this.storage_remove = function(key) {
2719 localStorage.removeItem('updraftcentral_saved_at_'+key);
2720 return localStorage.removeItem('updraftcentral_'+key);
2721 }
2722
2723 /**
2724 * Records currently selected site and its current content
2725 *
2726 * N.B. Basically, this function records the content associated by
2727 * the selected site before clicking another menu or selecting
2728 * another website to work on. Implemented for ease of use and avoid
2729 * or minimize redundant or repeatitive clicking.
2730 */
2731 this.init_recorder = function() {
2732 var recorder = new UpdraftCentral_Recorder();
2733 recorder.load();
2734 }
2735
2736
2737 /**
2738 * Initializes the UpdraftCentral_Keyboard_Shortcuts class and
2739 * its functionalities
2740 *
2741 * @returns {void}
2742 */
2743 this.init_keyboard_shortcuts = function() {
2744 var shortcuts = new UpdraftCentral_Keyboard_Shortcuts();
2745 shortcuts.init();
2746 }
2747
2748 /**
2749 * Handles Modules Visibility. User can choose which modules to be visible / hidden in the sidebar
2750 */
2751 var $modules = $('#visible-modules-container .updraft-menu-item-container');
2752 var $hidden_modules_container = $('#hidden-modules-container');
2753 var $module_visibility = $('.module-visibility');
2754 var visible_modules = [];
2755 var hidden_modules = [];
2756 var module_id = '';
2757 var $show_all = $('<div class="updraft-menu-item-container"><div id="updraft-menu-item-all" class="updraft-menu-item">' + udclion.show_all + '</div></div>');
2758
2759 var $menu = $('#hidden-modules-menu');
2760 $menu.hide();
2761
2762 var $hamburger = $('.uc-hidden-modules-menu');
2763 var $close = $('.uc-hidden-modules-close');
2764 $close.hide();
2765
2766 /**
2767 * Initializes modules visibility based on what is stored in Database (usermeta table)
2768 */
2769 function initialize_module_visibility() {
2770 if ($(this).find('.dashicons-visibility').length > 0) {
2771 module_id = $(this).children('.updraft-menu-item').prop('id');
2772 module_id = module_id.replace('updraft-menu-item-', '');
2773 hidden_modules.push(module_id);
2774 $(this).hide();
2775 } else {
2776 module_id = $(this).children('.updraft-menu-item').prop('id');
2777 module_id = module_id.replace('updraft-menu-item-', '');
2778 visible_modules.push(module_id);
2779 }
2780 }
2781
2782 $modules.each(initialize_module_visibility);
2783
2784 if (0 === hidden_modules.length) {
2785 $hidden_modules_container.hide();
2786 } else if (hidden_modules.length > 1) {
2787 $show_all.appendTo($hidden_modules_container.next());
2788 }
2789 $hidden_modules_container.find('.uc-hidden-modules-label').text(udclion.hidden_modules + '(' + hidden_modules.length + ')');
2790
2791 $module_visibility.hover(function() {
2792 $(this).parent().find('.updraft-menu-item-links').addClass('updraft-menu-item-hover');
2793 },function() {
2794 $(this).parent().find('.updraft-menu-item-links').removeClass('updraft-menu-item-hover');
2795 });
2796
2797 /**
2798 * Upon clicking module visibility icon, the visibility is toggled and stored in DB
2799 */
2800 $('#updraft-central-navigation-sidebar').on('click', $module_visibility, function(evt) {
2801 if (!$(evt.target).parent().hasClass('module-visibility')) return;
2802 var $clicked_module = $(evt.target).parent();
2803 module_id = $clicked_module.prev().attr('id');
2804 module_id = module_id.replace('updraft-menu-item-', '');
2805 var visibility = false;
2806 if (0 < $clicked_module.find('.dashicons-visibility').length) {
2807 hidden_modules = $.grep(hidden_modules, function(value) {
2808 return value != module_id;
2809 });
2810 visible_modules.push(module_id);
2811 visibility = true;
2812 } else {
2813 $clicked_module.html('<span class="dashicons dashicons-visibility"></span>');
2814 visible_modules = $.grep(visible_modules, function(value) {
2815 return value != module_id;
2816 });
2817 hidden_modules.push(module_id);
2818 }
2819
2820 /**
2821 * Sends ajax request to store toggled visibility. Also toggles visibility in front end upon successful ajax call.
2822 */
2823 UpdraftCentral.send_ajax('module_visibility', {module_id: module_id, visibility: visibility}, null, 'via_mothership_encrypting', null, function (resp, code, error_code) {
2824 if ('ok' === code) {
2825 if (false === visibility) {
2826 $clicked_module.parent().clone().appendTo($menu);
2827 $clicked_module.prev().removeClass('updraft-menu-item-hover').parent().slideUp();
2828 $menu.find('.updraft-menu-item-links').removeClass('updraft-menu-item-hover').removeClass('updraft-menu-item-links-active');
2829 if ($menu.find('.updraft-menu-item-container').length > 0) {
2830 $hidden_modules_container.slideDown();
2831 if ($menu.find('.updraft-menu-item-container').length > 1) {
2832 $show_all.appendTo($hidden_modules_container.next());
2833 }
2834 }
2835 } else {
2836 $clicked_module.parent().remove();
2837 $('.updraft-menu-item-container').find('.updraft-menu-item-' + module_id).next().html('<span class="dashicons dashicons-hidden"></span>').parent().slideDown();
2838 if (0 === $menu.find('.updraft-menu-item-container').length) {
2839 $hidden_modules_container.slideUp();
2840 } else if ($menu.find('.updraft-menu-item-container').length < 2) {
2841 $show_all.remove();
2842 }
2843 }
2844 $hidden_modules_container.find('.uc-hidden-modules-label').text(udclion.hidden_modules + '(' + hidden_modules.length + ')');
2845 }
2846 });
2847
2848 });
2849
2850 /**
2851 * Resets all module visibility. Make all modules it visible
2852 */
2853 $('#updraft-central-navigation-sidebar').on('click', '#updraft-menu-item-all', function() {
2854 UpdraftCentral.send_ajax('reset_modules_visibility', 'all', null, 'via_mothership_encrypting', null, function (resp, code, error_code) {
2855 if ('ok' === code) {
2856 $modules.each(initialize_module_visibility);
2857 $modules.each(function() {
2858 $(this).find('.updraft-menu-item').removeClass('updraft-menu-item-hover');
2859 $(this).slideDown();
2860 $('.module-visibility', this).html('<span class="dashicons dashicons-hidden"></span>');
2861 });
2862 $hidden_modules_container.slideUp('normal', function() {
2863 $close.hide();
2864 $hamburger.show();
2865 $menu.hide();
2866 $menu.find('.updraft-menu-item-container').remove();
2867 });
2868 hidden_modules.length = 0;
2869 }
2870 });
2871 });
2872
2873 $hamburger.click(function() {
2874 $menu.slideToggle('normal', function() {
2875 $close.show();
2876 $hamburger.hide();
2877 });
2878 });
2879
2880 $close.click(function() {
2881 $menu.slideToggle('normal', function() {
2882 $close.hide();
2883 $hamburger.show();
2884 });
2885 });
2886
2887 return this;
2888 };
2889
2890