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

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

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