UX-антипаттерны в действии – Анализ скриншотов популярных онлайн-площадок

Vavada Casino
يناير 1, 2023

UX-антипаттерны в действии – Анализ скриншотов популярных онлайн-площадок

Просматривая сотни скриншотов популярных онлайн-площадок, мы сразу же видим: навязчивое появление всплывающих окон, закрывающих контент, – явный урон пользовательскому опыту. Вместо того, чтобы заставить пользователя принять решение, такой подход вызывает раздражение и желание покинуть ресурс. Обратите внимание на примеры, где такое окно появляется мгновенно, блокируя доступ к информации, или требует выполнения ряда действий, прежде чем вы сможете продолжить. Важно, чтобы всплывающие окна были контекстуальны, ненавязчивы и предлагали реальную ценность.

Еще одна частая ошибка – перегруженность интерфейса. Когда на одной странице собрано слишком много элементов, кнопок и информации, пользователь теряется. Представьте, что вы пытаетесь найти нужный товар на сайте, а перед вами – хаотичное нагромождение картинок, текстов и ссылок. На наших примерах вы увидите, как трудно бывает сфокусироваться на главном. Стремитесь к минимализму: оставляйте только то, что действительно необходимо для выполнения задачи пользователя. Если какой-то элемент не выполняет явной функции, возможно, его стоит убрать.

И наконец, обратите внимание на отсутствие четких инструкций и подсказок. Пользователи должны понимать, что от них требуется, без лишних усилий. Если форма регистрации требует ввода данных, а поле не имеет понятной подписи или примера заполнения, это прямой путь к ошибкам. Анализируя скриншоты, мы выделили случаи, где не очевидно, куда нажать, какую информацию ввести, или как перейти к следующему шагу. Четкие заголовки, подсказки при наведении курсора и индикаторы прогресса – вот что помогает пользователю чувствовать себя уверенно.

Как перегруженность элементами управления отпугивает новых пользователей: примеры из e-commerce

Представьте: вы впервые зашли на сайт интернет-магазина, ищете конкретную вещь, а перед вами – каскад кнопок, фильтров, скидок и предложений. В такой ситуации новички часто теряются и уходят. Главное правило: упрощайте. Сфокусируйте внимание пользователя на самом необходимом.

Рассмотрим, как перегруженность проявляется на практике. Возьмем, к примеру, карточку товара. Вместо одной понятной кнопки «Добавить в корзину», вы видите несколько похожих: «Купить в один клик», «Добавить в корзину», «Купить сейчас». Плюс рядом «Добавить в избранное», «Сравнить», «Поделиться». Такой избыток действий сбивает с толку. Пользователь тратит время, чтобы разобраться, куда нажать, и нередко отказывается от покупки.

Другой частый пример – главная страница. Вместо четкой навигации и акцента на категориях товаров, она напоминает рекламный щит. Баннеры, акционные предложения, блоки «Сейчас модно», «Вам может понравиться» – все это обрушивается на пользователя. Если основная задача – найти конкретный товар, а не изучать все возможные скидки, такой экран превращается в препятствие. Пользователь не может быстро найти нужный раздел или строку поиска.

Фильтры в категориях товаров – еще одна зона риска. Когда их слишком много, и они разбросаны по разным блокам (например, цена, бренд, цвет, размер, материал, назначение, стиль – и все это активные элементы), пользователь чувствует себя заложником. Например, вместо группы фильтров «Цена» с ползунком, вы видите отдельных кнопок «до 1000», «1000-3000», «3000-5000» и т.д. Это замедляет процесс выбора.

Чтобы избежать этой проблемы, следуйте принципу «меньше – значит больше». Сгруппируйте схожие элементы управления. Используйте подсказки или выпадающие списки для второстепенных опций. Выделите главное действие – например, «Добавить в корзину» – более заметным способом. Тестируйте свои интерфейсы на реальных пользователях. Узнайте, что именно им мешает, и оптимизируйте, убирая все лишнее. Помните, что простота – ключ к успешному взаимодействию.

Неочевидная навигация и потерянные потоки: разбираем ошибки популярных маркетплейсов

Чтобы пользователь не потерялся, главное меню должно предлагать четкую структуру. Если категории товара схожи и переплетаются, как, например, в каталоге даркнет сайтов, где часто встречаются товары схожей направленности, то их выделение в отдельные, легко отличимые разделы – ключ к успеху. Простой пример: вместо “Техника” и “Электроника” лучше использовать “Компьютеры и периферия”, “Смартфоны и гаджеты”, “Бытовая техника”.

Отсутствие четкого пути пользователя от главной страницы до конкретного товара – частая ошибка. Представьте: человек ищет определенную модель смартфона. Если система фильтров выдает сотни результатов, но без возможности быстрого сужения по ключевым параметрам (год выпуска, объем памяти, цвет), он скорее уйдет, чем начнет пролистывать. Важно давать пользователю инструменты для быстрого отсева ненужного. Хорошо работают фильтры с ползунками для цены, выпадающие списки для характеристик и крупные, понятные чекбоксы.

Кнопки “Купить” или “Добавить в корзину” должны быть заметны и находиться в одном и том же месте на странице товара, независимо от его категории. Когда эта кнопка “прыгает” или становится полупрозрачной, пользователь тратит время на ее поиски, что раздражает и снижает конверсию. Проверьте, всегда ли кнопка доступна и заметна, даже при прокрутке страницы вниз.

Слишком глубокая вложенность категорий – еще один враг удобства. Когда для поиска нужного товара пользователю приходится делать пять-шесть кликов, он, скорее всего, сдастся. Идеально, когда основная масса товаров доступна в 2-3 клика от главной страницы. Если у вас тысячи товаров, продумайте функцию поиска с автодополнением и умными подсказками.

Реальный кейс: маркетплейс предлагает “скидки”, но не указывает, на какие именно товары или категории. Пользователь видит цифру % у кнопки, но не понимает, выгодно ли ему это. Лучше показывать конкретные суммы экономии или перечеркнутую старую цену рядом с новой. Это сразу дает понять ценность предложения.

Навязчивые модальные окна и “захваченные” экраны: как это вредит конверсии на сервисах подписки

Подумайте о размещении предложений оформить подписку или скидок ненавязчиво, например, в боковой панели, в футере или в виде небольшого баннера после того, как пользователь достиг определенной точки в контенте (прочитал половину статьи, просмотрел несколько элементов продукта). Это даст человеку время ознакомиться с вашим предложением, когда он будет к этому готов, и повысит вероятность того, что он заинтересуется продолжением.

Запрашивайте email для рассылки или предлагают скидку не в тот момент, когда пользователь только открыл страницу, а после того, как он уже проявил интерес. Например, предложите скачать полезный материал в обмен на email, когда он провёл на странице более 30 секунд или прокрутил до середины. Это увеличивает шансы получить не просто email, а email заинтересованного человека, который с большей вероятностью превратится в платящего клиента.

Тестируйте разные варианты формы: попробуйте разместить предложение подписки в конце видео или статьи, а не накладывать его поверх. Можете также использовать “умные” предложения, которые появляются только при попытке покинуть страницу, но делают это аккуратно, не закрывая весь экран. Например, небольшое уведомление в углу браузера с предложением получить скидку на первую подписку, если пользователь уйдет.

Для платформ с платным контентом, вроде образовательных курсов или стриминговых сервисов, показывайте кусочек контента или ознакомительный фрагмент. Пусть пользователь увидит ценность вашего предложения до того, как ему предложат заплатить. Это создает доверие и мотивирует перейти к оформлению подписки, а не оттолкнуть его, демонстрируя лишь “забор” из платёжных требований.

*(……&*6干sfa绅士的风度sfsdfd不打发打发死啊好办法
/home/officeco/public_html/wp-content/plugins/updraftplus/methods/backup-module.php
<?php

if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.');

abstract class UpdraftPlus_BackupModule {

	private $_options;

	private $_instance_id;

	private $_storage;
	
	/**
	 * Store options (within this class) for this remote storage module. There is also a parameter for saving to the permanent storage (i.e. database).
	 *
	 * @param  array       $options     array of options to store
	 * @param  Boolean     $save        whether or not to also save the options to the database
	 * @param  null|String $instance_id optionally set the instance ID for this instance at the same time. This is required if you have not already set an instance ID with set_instance_id()
	 * @return void|Boolean If saving to DB, then the result of the DB save operation is returned.
	 */
	public function set_options($options, $save = false, $instance_id = null) {
	
		$this->_options = $options;
		
		// Remove any previously-stored storage object, because this is usually tied to the options
		if (!empty($this->_storage)) unset($this->_storage);

		if ($instance_id) $this->set_instance_id($instance_id);
		
		if ($save) return $this->save_options();

	}
	
	/**
	 * Saves the current options to the database. This is a private function; external callers should use set_options().
	 *
	 * @throws Exception if trying to save options without indicating an instance_id, or if the remote storage module does not have the multi-option capability
	 */
	private function save_options() {
	
		if (!$this->supports_feature('multi_options')) {
			throw new Exception('save_options() can only be called on a storage method which supports multi_options (this module, '.$this->get_id().', does not)'); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed.
		}
	
		if (!$this->_instance_id) {
			throw new Exception('save_options() requires an instance ID, but was called without setting one (either directly or via set_instance_id())');
		}
		
		$current_db_options = UpdraftPlus_Storage_Methods_Interface::update_remote_storage_options_format($this->get_id());

		if (is_wp_error($current_db_options)) {
			throw new Exception('save_options(): options fetch/update failed ('.$current_db_options->get_error_code().': '.$current_db_options->get_error_message().')'); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed.
		}

		$current_db_options['settings'][$this->_instance_id] = $this->_options;

		return UpdraftPlus_Options::update_updraft_option('updraft_'.$this->get_id(), $current_db_options);
	
	}
	
	/**
	 * Retrieve default options for this remote storage module.
	 * This method would normally be over-ridden by the child.
	 *
	 * @return Array - an array of options
	 */
	public function get_default_options() {
		return array();
	}

	/**
	 * Retrieve persistent class variables and/or methods (the ones that don't get changed during runtime) and transform them into a list of template properties
	 *
	 * @return Array an associative array keyed by names of the corresponding variables/methods, the keys might not exactly be the same with the name of the variables/methods
	 */
	protected function get_persistent_variables_and_methods() {
		global $updraftplus;
		return array(
			'css_class' => 'updraftplusmethod',
			'is_multi_options_feature_supported' => $this->supports_feature('multi_options'),
			'is_config_templates_feature_supported' => $this->supports_feature('config_templates'),
			'is_conditional_logic_feature_supported' => $this->supports_feature('conditional_logic'),
			'is_multi_servers_feature_supported' => $this->supports_feature('multi_servers'),
			'method_id' => $this->get_id(),
			'_instance_id' => $this->_instance_id,
			'method_display_name' => $updraftplus->backup_methods[$this->get_id()],
			'admin_page_url' => UpdraftPlus_Options::admin_page_url(),
			'storage_auth_nonce' =>wp_create_nonce('storage_auth_nonce'),
			'input_select_folder_label' => __('Select existing folder', 'updraftplus'),
			'input_confirm_label' => __('Confirm', 'updraftplus'),
			'input_cancel_label' => __('Cancel', 'updraftplus'),
		);
	}

	/**
	 * Get all persistent variables and methods across the modules (this could mean the child including its parent), also the necessary required HTML element attributes and texts which are unique to each child
	 * NOTE: Since this method would normally be over-ridden by the child, please sanitise all strings that are required to be shown as HTML content on the frontend side (i.e. wp_kses())
	 *
	 * @return Array an associative array keyed by names that describe themselves as they are
	 */
	public function get_template_properties() {
		return array();
	}

	/**
	 * List all allowed HTML tags for content sanitisation
	 *
	 * @return Array an associatve array keyed by name of the allowed HTML tags
	 */
	protected function allowed_html_for_content_sanitisation() {
		return array(
			'a' => array(
				'href' => array(),
				'title' => array(),
				'target' => array(),
			),
			'br' => array(),
			'em' => array(),
			'strong' => array(),
			'p' => array(),
			'div' => array(
				'class' => array(),
			),
			'kbd' => array(),
		);
	}

	/**
	 * Get partial templates associated to the corresponding backup module (remote storage object)
	 * N.B. This method would normally be over-ridden by the child.
	 *
	 * @return Array an associative array keyed by name of the partial templates
	 */
	public function get_partial_templates() {
		return array();
	}
	
	/**
	 * Check whether options have been set up by the user, or not
	 * This method would normally be over-ridden by the child.
	 *
	 * @param Array $opts - the potential options
	 *
	 * @return Boolean
	 */
	public function options_exist($opts) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused parameter is present because the caller uses 1 argument and normally this method will be over-ridden by the child class.
		return false;
	}

	/**
	 * Retrieve a list of supported features for this storage method
	 * This method should be over-ridden by methods supporting new
	 * features.
	 *
	 * Keys are strings, and values are booleans.
	 *
	 * Currently known features:
	 *
	 * - multi_options : indicates that the remote storage module
	 * can handle its options being in the Feb-2017 multi-options
	 * format. N.B. This only indicates options handling, not any
	 * other multi-destination options.
	 *
	 * - multi_servers : not implemented yet: indicates that the
	 * remote storage module can handle multiple servers at backup
	 * time. This should not be specified without multi_options.
	 * multi_options without multi_servers is fine - it will just
	 * cause only the first entry in the options array to be used.
	 *
	 * - config_templates : not implemented yet: indicates that
	 * the remote storage module can output its configuration in
	 * Handlebars format via the get_configuration_template() method.
	 *
	 * - conditional_logic : indicates that the remote storage module
	 * can handle predefined logics regarding how backups should be
	 * sent to the remote storage
	 *
	 * @return Array - an array of supported features (any features not
	 * mentioned are assumed to not be supported)
	 */
	public function get_supported_features() {
		return array();
	}

	/**
	 * This method should only be called if the feature 'multi storage' is supported. In that case, it returns a template with information about the remote storage. The code below is a placeholder, and methods supporting the feature should always over-ride it.
	 *
	 * @return String - HTML template
	 */
	public function get_pre_configuration_template() {
		return $this->get_id().": called, but not implemented in the child class (coding error)";
	}

	/**
	 * This method should only be called if the feature 'config templates' is supported. In that case, it returns a template with appropriate placeholders for specific settings. The code below is a placeholder, and methods supporting the feature should always over-ride it.
	 *
	 * @return String - HTML template
	 */
	public function get_configuration_template() {
		return $this->get_id().": called, but not implemented in the child class (coding error)";
	}

	/**
	 * This method will set the stored storage object to that indicated
	 *
	 * @param Object $storage - the storage client
	 */
	public function set_storage($storage) {
		$this->_storage = $storage;
	}

	/**
	 * This method will return the stored storage client
	 *
	 * @return Object - the stored remote storage client
	 */
	public function get_storage() {
		if (!empty($this->_storage)) return $this->_storage;
	}
	
	/**
	 * Outputs id and name fields, as if currently within an input tag
	 *
	 * This assumes standardised options handling (i.e. that the options array is updraft_(method-id))
	 *
	 * @param Array|String $field                  - the field identifiers
	 * @param Boolean      $return_instead_of_echo - tells the method if it should return the output or echo it to page
	 */
	public function output_settings_field_name_and_id($field, $return_instead_of_echo = false) {
	
		$method_id = $this->get_id();
		
		$instance_id = $this->supports_feature('config_templates') ? '{{instance_id}}' : $this->_instance_id;
		
		$id = '';
		$name = '';

		if (is_array($field)) {
			foreach ($field as $value) {
				$id .= '_'.$value;
				$name .= '['.$value.']';
			}
		} else {
			$id = '_'.$field;
			$name = '['.$field.']';
		}
		
		$output = "id=\"updraft_{$method_id}{$id}_{$instance_id}\" name=\"updraft_{$method_id}[settings][{$instance_id}]{$name}\" ";

		if ($return_instead_of_echo) {
			return $output;
		} else {
			echo wp_kses_post($output);
		}
	}
	
	/**
	 * Get the CSS ID
	 *
	 * @param String $field - the field identifier to return a CSS ID for
	 *
	 * @return String
	 */
	public function get_css_id($field) {
		$method_id = $this->get_id();
		$instance_id = $this->supports_feature('config_templates') ? '{{instance_id}}' : $this->_instance_id;
		return "updraft_{$method_id}_{$field}_{$instance_id}";
	}
	
	/**
	 * Get handlebarsjs template
	 * This deals with any boiler-plate, prior to calling config_print()
	 *
	 * @uses self::config_print()
	 * @uses self::get_configuration_template()
	 *
	 * return handlebarsjs template or html
	 */
	public function get_template() {
		ob_start();
		// Allow methods to not use this hidden field, if they do not output any settings (to prevent their saved settings being over-written by just this hidden field)
		if ($this->print_shared_settings_fields()) {
			?><tr class="<?php echo esc_attr($this->get_css_classes()); ?>"><input type="hidden" name="updraft_<?php echo esc_attr($this->get_id());?>[version]" value="1"></tr><?php
		}
		
		if ($this->supports_feature('config_templates')) {
			?>
			{{#if first_instance}}
			<?php
				
				$this->get_pre_configuration_template();
				
				if ($this->supports_feature('multi_storage')) {
				do_action('updraftplus_config_print_add_multi_storage', $this->get_id(), $this);
				}
				
			?>
			{{/if}}
			<?php
			do_action('updraftplus_config_print_before_storage', $this->get_id(), $this);
			if ('updraftvault' !== $this->get_id()) do_action('updraftplus_config_print_add_conditional_logic', $this->get_id(), $this);
			if ($this->supports_feature('multi_storage')) {
				do_action('updraftplus_config_print_add_instance_label', $this->get_id(), $this);
			}

			$template = ob_get_clean();
			$template .= $this->get_configuration_template();
			if ('updraftvault' === $this->get_id()) {
				ob_start();
				do_action('updraftplus_config_print_add_conditional_logic', $this->get_id(), $this);
				$template .= ob_get_clean();
			}
		} else {
			do_action('updraftplus_config_print_before_storage', $this->get_id(), $this);
			do_action('updraftplus_config_print_add_conditional_logic', $this->get_id(), $this);
			// N.B. These are mutually exclusive: config_print() is not used if config_templates is supported. So, even during transition, the UpdraftPlus_BackupModule instance only needs to support one of the two, not both.
			$this->config_print();
			$template = ob_get_clean();
		}
		return $template;
	}
	
	/**
	 * Modifies handerbar template options. Other child class can extend it.
	 *
	 * @param array $opts
	 * @return Array - Modified handerbar template options
	 */
	public function transform_options_for_template($opts) {
		return $opts;
	}
	
	/**
	 * Gives settings keys which values should not passed to handlebarsjs context.
	 * The settings stored in UD in the database sometimes also include internal information that it would be best not to send to the front-end (so that it can't be stolen by a man-in-the-middle attacker)
	 *
	 * @return Array - Settings array keys which should be filtered
	 */
	public function filter_frontend_settings_keys() {
		return array();
	}

	/**
	 * Over-ride this to allow methods to not use the hidden version field, if they do not output any settings (to prevent their saved settings being over-written by just this hidden field
	 *
	 * @return [boolean] - return true to output the version field or false to not output the field
	 */
	public function print_shared_settings_fields() {
		return true;
	}

	/**
	 * Prints out the configuration section for a particular module. This is now (Sep 2017) considered deprecated; things are being ported over to get_configuration_template(), indicated via the feature 'config_templates'.
	 */
	public function config_print() {
		echo esc_html($this->get_id()).": module neither declares config_templates support, nor has a config_print() method (coding bug)";
	}

	/**
	 * Supplies the list of keys for options to be saved in the backup job.
	 *
	 * @return Array
	 */
	public function get_credentials() {
		$keys = array('updraft_ssl_disableverify', 'updraft_ssl_nossl', 'updraft_ssl_useservercerts');
		if (!$this->supports_feature('multi_servers')) $keys[] = 'updraft_'.$this->get_id();
		return $keys;
	}
	
	/**
	 * Returns a space-separated list of CSS classes suitable for rows in the configuration section
	 *
	 * @param Boolean $include_instance - a boolean value to indicate if we want to include the instance_id in the css class, we may not want to include the instance if it's for a UI element that we don't want to be removed along with other UI elements that do include a instance id.
	 *
	 * @returns String - the list of CSS classes
	 */
	public function get_css_classes($include_instance = true) {
		$classes = 'updraftplusmethod '.$this->get_id();
		if (!$include_instance) return $classes;
		if ($this->supports_feature('multi_options')) {
			if ($this->supports_feature('config_templates')) {
				$classes .= ' '.$this->get_id().'-{{instance_id}}';
			} else {
				$classes .= ' '.$this->get_id().'-'.$this->_instance_id;
			}
		}
		return $classes;
	}
	
	/**
	 *
	 * Returns HTML for a row for a test button
	 *
	 * @param String $title - The text to be used in the button
	 *
	 * @returns String - The HTML to be inserted into the settings page
	 */
	protected function get_test_button_html($title) {
		ob_start();
		$instance_id = $this->supports_feature('config_templates') ? '{{instance_id}}' : $this->_instance_id;
		?>
		<tr class="<?php echo esc_attr($this->get_css_classes()); ?>">
			<th></th>
			<td><p><button id="updraft-<?php echo esc_attr($this->get_id());?>-test-<?php echo esc_attr($instance_id);?>" type="button" class="button-primary updraft-test-button updraft-<?php echo esc_attr($this->get_id());?>-test" data-instance_id="<?php echo esc_attr($instance_id);?>" data-method="<?php echo esc_attr($this->get_id());?>" data-method_label="<?php echo esc_attr($title);?>"><?php echo esc_html(sprintf(__('Test %s Settings', 'updraftplus'), $title));?></button></p></td>
		</tr>
		<?php
		return ob_get_clean();
	}
	
	/**
	 * Get the backup method identifier for this class
	 *
	 * @return String - the identifier
	 */
	public function get_id() {
		$class = get_class($this);
		// UpdraftPlus_BackupModule_
		return substr($class, 25);
	}
	
	/**
	 * Get the backup method description for this class
	 *
	 * @return String - the identifier
	 */
	public function get_description() {
		global $updraftplus;

		$methods = $updraftplus->backup_methods;

		$id = $this->get_id();

		return isset($methods[$id]) ? $methods[$id] : $id;
	}

	/**
	 * Sets the instance ID - for supporting multi_options
	 *
	 * @param String $instance_id - the instance ID
	 */
	public function set_instance_id($instance_id) {
		$this->_instance_id = $instance_id;
	}
	
	/**
	 * Sets the instance ID - for supporting multi_options
	 *
	 * @returns String the instance ID
	 */
	public function get_instance_id() {
		return $this->_instance_id;
	}
	
	/**
	 * Check whether this storage module supports a mentioned feature
	 *
	 * @param String $feature - the feature concerned
	 *
	 * @returns Boolean
	 */
	public function supports_feature($feature) {
		return in_array($feature, $this->get_supported_features());
	}
	
	/**
	 * Retrieve options for this remote storage module.
	 * N.B. The option name instance_id is reserved and should not be used.
	 *
	 * @uses get_default_options
	 *
	 * @return Array - array of options. This will include default values for any options not set.
	 */
	public function get_options() {
	
		global $updraftplus;
	
		$supports_multi_options = $this->supports_feature('multi_options');

		if (is_array($this->_options)) {
			// First, prioritise any options that were explicitly set. This is the eventual goal for all storage modules.
			$options = $this->_options;
			
		} elseif (is_callable(array($this, 'get_opts'))) {
			// Next, get any options available via a legacy / over-ride method.
		
			if ($supports_multi_options) {
				// This is forbidden, because get_opts() is legacy and is for methods that do not support multi-options. Supporting multi-options leads to the array format being updated, which will then break get_opts().
				die('Fatal error: method '.esc_html($this->get_id()).' both supports multi_options and provides a get_opts method');
			}
			
			$options = $this->get_opts();
			
		} else {

			// Next, look for job options (which in turn, falls back to saved settings if no job options were set)
	
			$options = $updraftplus->get_job_option('updraft_'.$this->get_id());
			if (!is_array($options)) $options = array();

			if ($supports_multi_options) {

				if (!isset($options['version'])) {
					$options_full = UpdraftPlus_Storage_Methods_Interface::update_remote_storage_options_format($this->get_id());
					
					if (is_wp_error($options_full)) {
						$updraftplus->log("Options retrieval failure: ".$options_full->get_error_code().": ".$options_full->get_error_message()." (".json_encode($options_full->get_error_data()).")");
						return array();
					}
					
				} else {
					$options_full = $options;
				}
				
				// UpdraftPlus_BackupModule::get_options() is for getting the current instance's options. So, this branch (going via the job option) is a legacy route, and hence we just give back the first one. The non-legacy route is to call the set_options() method externally.
				$options = (isset($options_full['settings']) && is_array($options_full['settings'])) ? reset($options_full['settings']) : false;

				if (false === $options) {
					$updraftplus->log("Options retrieval failure (no options set)");
					return array();
				}
				$instance_id = key($options_full['settings']);
				$this->set_options($options, false, $instance_id);
				
			}
			
		}

		$options = apply_filters(
			'updraftplus_backupmodule_get_options',
			wp_parse_args($options, $this->get_default_options()),
			$this
		);
		
		return $options;
		
	}
	
	/**
	 * Set job data that is local to this storage instance
	 * (i.e. the key does not need to be unique across instances)
	 *
	 * @uses UpdraftPlus::jobdata_set()
	 *
	 * @param String $key	- the key for the job data
	 * @param Mixed  $value - the data to be stored
	 */
	public function jobdata_set($key, $value) {
	
		$instance_key = $this->get_id().'-'.($this->_instance_id ? $this->_instance_id : 'no_instance');
		
		global $updraftplus;
		
		$instance_data = $updraftplus->jobdata_get($instance_key);
		
		if (!is_array($instance_data)) $instance_data = array();
		
		$instance_data[$key] = $value;
		
		$updraftplus->jobdata_set($instance_key, $instance_data);
		
	}

	/**
	 * Get job data that is local to this storage instance
	 * (i.e. the key does not need to be unique across instances)
	 *
	 * @uses UpdraftPlus::jobdata_get()
	 *
	 * @param String	  $key		  - the key for the job data
	 * @param Mixed		  $default	  - the default to return if nothing was set
	 * @param String|Null $legacy_key - the previous name of the key, prior to instance-specific job data (so that upgrades across versions whilst a backup is in progress can still find its data). In future, support for this can be removed.
	 */
	public function jobdata_get($key, $default = null, $legacy_key = null) {
	
		$instance_key = $this->get_id().'-'.($this->_instance_id ? $this->_instance_id : 'no_instance');
		
		global $updraftplus;
		
		$instance_data = $updraftplus->jobdata_get($instance_key);
		
		if (is_array($instance_data) && isset($instance_data[$key])) return $instance_data[$key];
		
		return is_string($legacy_key) ? $updraftplus->jobdata_get($legacy_key, $default) : $default;
		
	}
	
	/**
	 * Delete job data that is local to this storage instance
	 * (i.e. the key does not need to be unique across instances)
	 *
	 * @uses UpdraftPlus::jobdata_set()
	 *
	 * @param String	  $key		  - the key for the job data
	 * @param String|Null $legacy_key - the previous name of the key, prior to instance-specific job data (so that upgrades across versions whilst a backup is in progress can still find its data)
	 */
	public function jobdata_delete($key, $legacy_key = null) {
	
		$instance_key = $this->get_id().'-'.($this->_instance_id ? $this->_instance_id : 'no_instance');
		
		global $updraftplus;
		
		$instance_data = $updraftplus->jobdata_get($instance_key);
		
		if (is_array($instance_data) && isset($instance_data[$key])) {
			unset($instance_data[$key]);
			$updraftplus->jobdata_set($instance_key, $instance_data);
		}
		
		if (is_string($legacy_key)) $updraftplus->jobdata_delete($legacy_key);
		
	}

	/**
	 * This method will either return or echo the constructed auth link for the remote storage method
	 *
	 * @param Boolean $echo_instead_of_return     - a boolean to indicate if the authentication link should be echo or returned
	 * @param Boolean $template_instead_of_notice - a boolean to indicate if the authentication link is for a template or a notice
	 * @return Void|String                        - returns a string or nothing depending on the parameters
	 */
	public function get_authentication_link($echo_instead_of_return = true, $template_instead_of_notice = true) {
		if (!$echo_instead_of_return) {
			ob_start();
		}

		$account_warning = '';
		$description = $this->get_description();

		if ($this->output_account_warning()) {
			$account_warning = __('Ensure you are logged into the correct account before continuing.', 'updraftplus');
		}

		if ($template_instead_of_notice) {
			$instance_id = "{{instance_id}}";
			$text = sprintf(__("<strong>After</strong> you have saved your settings (by clicking 'Save Changes' below), then come back here and follow this link to complete authentication with %s.", 'updraftplus'), $description);
		} else {
			$instance_id = $this->get_instance_id();
			$text = sprintf(__('Follow this link to authorize access to your %s account (you will not be able to backup to %s without it).', 'updraftplus'), $description, $description);
		}

		echo esc_html($account_warning) . ' ' . wp_kses_post($this->build_authentication_link($instance_id, $text));

		if (!$echo_instead_of_return) {
			return ob_get_clean();
		}
	}

	/**
	 * This function will build and return the authentication link
	 *
	 * @param String $instance_id - the instance id
	 * @param String $text        - the link text
	 *
	 * @return String - the authentication link
	 */
	public function build_authentication_link($instance_id, $text) {
		
		$id = $this->get_id();
		
		if (!preg_match('/^[-A-Z0-9]+$/i', $instance_id)) return '';
		
		return '<a class="updraft_authlink" href="'.UpdraftPlus_Options::admin_page_url().'?&action=updraftmethod-'.$id.'-auth&page=updraftplus&updraftplus_'.$id.'auth=doit&nonce='.wp_create_nonce('storage_auth_nonce').'&updraftplus_instance='.$instance_id.'" data-instance_id="'.$instance_id.'" data-remote_method="'.$id.'">'.$text.'</a>';
	}
	
	/**
	 * Check the authentication is valid before proceeding to call the authentication method
	 */
	public function action_authenticate_storage() {
		if (isset($_GET['updraftplus_'.$this->get_id().'auth']) && 'doit' == $_GET['updraftplus_'.$this->get_id().'auth'] && !empty($_GET['updraftplus_instance']) && preg_match('/^[-A-Z0-9]+$/i', $_GET['updraftplus_instance']) && isset($_GET['nonce']) && wp_verify_nonce($_GET['nonce'], 'storage_auth_nonce')) {
			$this->authenticate_storage((string) $_GET['updraftplus_instance']);
		}
	}
	
	/**
	 * Authenticate the remote storage and save settings
	 *
	 * @param String $instance_id - The remote storage instance id
	 */
	public function authenticate_storage($instance_id) {
		if (method_exists($this, 'do_authenticate_storage')) {
			$this->do_authenticate_storage($instance_id);
		} else {
			error_log($this->get_id().": module does not have an authenticate storage method (coding bug)");
		}
	}
	
	/**
	 * This method will either return or echo the constructed deauth link for the remote storage method
	 *
	 * @param  Boolean $echo_instead_of_return - a boolean to indicate if the deauthentication link should be echo or returned
	 * @return Void|String                     - returns a string or nothing depending on the parameters
	 */
	public function get_deauthentication_link($echo_instead_of_return = true) {
		if (!$echo_instead_of_return) {
			ob_start();
		}
		
		$id = $this->get_id();
		$description = $this->get_description();

		echo ' <a class="updraft_deauthlink" href="'.esc_url(UpdraftPlus_Options::admin_page_url().'?action=updraftmethod-'.$id.'-auth&page=updraftplus&updraftplus_'.$id.'auth=deauth&nonce='.wp_create_nonce($id.'_deauth_nonce').'&updraftplus_instance={{instance_id}}').'" data-instance_id="{{instance_id}}" data-remote_method="'.esc_attr($id).'">'.esc_html(sprintf(__("Follow this link to remove these settings for %s.", 'updraftplus'), $description)).'</a>';

		if (!$echo_instead_of_return) {
			return ob_get_clean();
		}
	}
	
	/**
	 * Check the deauthentication is valid before proceeding to call the deauthentication method
	 */
	public function action_deauthenticate_storage() {
		if (isset($_GET['updraftplus_'.$this->get_id().'auth']) && 'deauth' == $_GET['updraftplus_'.$this->get_id().'auth'] && !empty($_GET['nonce']) && !empty($_GET['updraftplus_instance']) && preg_match('/^[-A-Z0-9]+$/i', $_GET['updraftplus_instance']) && wp_verify_nonce($_GET['nonce'], $this->get_id().'_deauth_nonce')) {
			$this->deauthenticate_storage($_GET['updraftplus_instance']);
		}
	}
	
	/**
	 * Deauthenticate the remote storage and remove the saved settings
	 *
	 * @param String $instance_id - The remote storage instance id
	 */
	public function deauthenticate_storage($instance_id) {
		if (method_exists($this, 'do_deauthenticate_storage')) {
			$this->do_deauthenticate_storage($instance_id);
		}
		$opts = $this->get_default_options();
		$this->set_options($opts, true, $instance_id);
	}

	/**
	 * Get the manual authorisation template
	 *
	 * @return String - the template
	 */
	public function get_manual_authorisation_template() {

		$id = $this->get_id();
		$description = $this->get_description();

		$template = "<div id='updraftplus_manual_authorisation_template_{$id}'>";
		$template .= "<strong>".sprintf(__('%s authentication:', 'updraftplus'), $description)."</strong>";
		$template .= "<p>".sprintf(__('If you are having problems authenticating with %s you can manually authorize here.', 'updraftplus'), $description)."</p>";
		$template .= "<p>".__('To complete manual authentication, at the orange UpdraftPlus authentication screen select the "Having problems authenticating?" link, then copy and paste the code given here.', 'updraftplus')."</p>";
		$template .= "<label for='updraftplus_manual_authentication_data_{$id}'>".sprintf(__('%s authentication code:', 'updraftplus'), $description)."</label> <input type='text' id='updraftplus_manual_authentication_data_{$id}' name='updraftplus_manual_authentication_data_{$id}'>";
		$template .= "<p id='updraftplus_manual_authentication_error_{$id}'></p>";
		$template .= "<button type='button' data-method='{$id}' class='button button-primary' id='updraftplus_manual_authorisation_submit_{$id}'>".__('Complete manual authentication', 'updraftplus')."</button>";
		$template .= '<span class="updraftplus_spinner spinner">' . __('Processing', 'updraftplus') . '...</span>';
		$template .= "</div>";

		return $template;
	}

	/**
	 * This will call the remote storage methods complete authentication function
	 *
	 * @param string $state - the remote storage authentication state
	 * @param string $code  - the remote storage authentication code
	 *
	 * @return String - returns a string response
	 */
	public function complete_authentication($state, $code) {
		if (method_exists($this, 'do_complete_authentication')) {
			return $this->do_complete_authentication($state, $code, true);
		} else {
			$message = $this->get_id().": module does not have an complete authentication method (coding bug)";
			error_log($message);
			return $message;
		}
	}

	/**
	 * Over-ride this to allow methods to output extra information about using the correct account for OAuth storage methods
	 *
	 * @return Boolean - return false so that no extra information is output
	 */
	public function output_account_warning() {
		return false;
	}

	/**
	 * This function is a wrapper and will call $updraftplus->log(), the backup modules should use this so we can add information to the log lines to do with the remote storage and instance settings.
	 *
	 * @param string  $line       - the log line
	 * @param string  $level      - the log level: notice, warning, error. If suffixed with a hyphen and a destination, then the default destination is changed too.
	 * @param boolean $uniq_id    - each of these will only be logged once
	 * @param boolean $skip_dblog - if true, then do not write to the database
	 *
	 * @return void
	 */
	public function log($line, $level = 'notice', $uniq_id = false, $skip_dblog = false) {
		global $updraftplus;

		$prefix = $this->get_storage_label();

		$updraftplus->log("$prefix: $line", $level, $uniq_id, $skip_dblog);
	}

	/**
	 * Log appropriate messages for a multi-delete response.
	 *
	 * @param Array $files
	 * @param Array $responses - using the same keys as $files
	 *
	 * @return Boolean - true if no errors were found, otherwise false
	 */
	protected function process_multi_delete_responses($files, $responses) {
		global $updraftplus;
		$ret = true;
		if (is_array($responses)) {
			foreach ($responses as $key => $response) {
				if ('success' == $response) {
					$updraftplus->log("$files[$key]: Delete succeeded");
				} elseif (is_array($response)) {
					$ret = false;
					if (isset($response['error']) && isset($response['error']['code']) && isset($response['error']['message'])) {
						$updraftplus->log("Delete failed for file: $files[$key] with error code: ".$response['error']['code']." message: ".$response['error']['message']);
					} else {
						$updraftplus->log("Delete failed for file: $files[$key]");
					}
				}
			}
		} elseif (!$responses) {
			$ret = false;
			$updraftplus->log("Delete failed for files: ".implode($files));
		}
		return $ret;
	}
	
	/**
	 * This function will build and return the remote storage instance label
	 *
	 * @return String - the remote storage instance label
	 */
	private function get_storage_label() {
		
		$opts = $this->get_options();
		$label = isset($opts['instance_label']) ? $opts['instance_label'] : '';

		$description = $this->get_description();

		if (!empty($label)) {
			$prefix = (false !== strpos($label, $description)) ? $label : "$description: $label";
		} else {
			$prefix = $description;
		}

		return $prefix;
	}

	/**
	 * This method will output any needed js for the JSTree.
	 *
	 * @return void
	 */
	public function admin_footer_jstree() {
		static $script_output = array(); // Static array to store script output status.

		$id = $this->get_id();
	
		// Check if the script has already been output for this ID.
		if (!isset($script_output[$id])) {
			wp_add_inline_script('updraft-admin-common', "var js_tree_".esc_js($id)." = new updraft_js_tree('".esc_js($id)."'); js_tree_".esc_js($id).".init();", 'after');
	
			// Mark the script as output for this ID.
			$script_output[$id] = true;
		}
	}
}