luteDirectoryPath() {
			return $this->package->getAbsoluteDirectoryPath();
		}
		/**
		 * Trigger a PHP error, but only when $debugMode is enabled.
		 *
		 * @param string $message
		 * @param int $errorType
		 */
		public function triggerError($message, $errorType) {
			if ( $this->isDebugModeEnabled() ) {
				trigger_error($message, $errorType);
			}
		}
		/**
		 * @return bool
		 */
		protected function isDebugModeEnabled() {
			if ( $this->debugMode === null ) {
				$this->debugMode = (bool)(constant('WP_DEBUG'));
			}
			return $this->debugMode;
		}
		/**
		 * Get the full name of an update checker filter, action or DB entry.
		 *
		 * This method adds the "puc_" prefix and the "-$slug" suffix to the filter name.
		 * For example, "pre_inject_update" becomes "puc_pre_inject_update-plugin-slug".
		 *
		 * @param string $baseTag
		 * @return string
		 */
		public function getUniqueName($baseTag) {
			$name = 'puc_' . $baseTag;
			if ( $this->filterSuffix !== '' ) {
				$name .= '_' . $this->filterSuffix;
			}
			return $name . '-' . $this->slug;
		}
		/**
		 * Store API errors that are generated when checking for updates.
		 *
		 * @internal
		 * @param WP_Error $error
		 * @param array|null $httpResponse
		 * @param string|null $url
		 * @param string|null $slug
		 */
		public function collectApiErrors($error, $httpResponse = null, $url = null, $slug = null) {
			if ( isset($slug) && ($slug !== $this->slug) ) {
				return;
			}
			$this->lastRequestApiErrors[] = array(
				'error'        => $error,
				'httpResponse' => $httpResponse,
				'url'          => $url,
			);
		}
		/**
		 * @return array
		 */
		public function getLastRequestApiErrors() {
			return $this->lastRequestApiErrors;
		}
		/* -------------------------------------------------------------------
		 * PUC filters and filter utilities
		 * -------------------------------------------------------------------
		 */
		/**
		 * Register a callback for one of the update checker filters.
		 *
		 * Identical to add_filter(), except it automatically adds the "puc_" prefix
		 * and the "-$slug" suffix to the filter name. For example, "request_info_result"
		 * becomes "puc_request_info_result-your_plugin_slug".
		 *
		 * @param string $tag
		 * @param callable $callback
		 * @param int $priority
		 * @param int $acceptedArgs
		 */
		public function addFilter($tag, $callback, $priority = 10, $acceptedArgs = 1) {
			add_filter($this->getUniqueName($tag), $callback, $priority, $acceptedArgs);
		}
		/* -------------------------------------------------------------------
		 * Inject updates
		 * -------------------------------------------------------------------
		 */
		/**
		 * Insert the latest update (if any) into the update list maintained by WP.
		 *
		 * @param stdClass $updates Update list.
		 * @return stdClass Modified update list.
		 */
		public function injectUpdate($updates) {
			//Is there an update to insert?
			$update = $this->getUpdate();
			if ( !$this->shouldShowUpdates() ) {
				$update = null;
			}
			if ( !empty($update) ) {
				//Let plugins filter the update info before it's passed on to WordPress.
				$update = apply_filters($this->getUniqueName('pre_inject_update'), $update);
				$updates = $this->addUpdateToList($updates, $update->toWpFormat());
			} else {
				//Clean up any stale update info.
				$updates = $this->removeUpdateFromList($updates);
			}
			return $updates;
		}
		/**
		 * @param stdClass|null $updates
		 * @param stdClass|array $updateToAdd
		 * @return stdClass
		 */
		protected function addUpdateToList($updates, $updateToAdd) {
			if ( !is_object($updates) ) {
				$updates = new stdClass();
				$updates->response = array();
			}
			$updates->response[$this->getUpdateListKey()] = $updateToAdd;
			return $updates;
		}
		/**
		 * @param stdClass|null $updates
		 * @return stdClass|null
		 */
		protected function removeUpdateFromList($updates) {
			if ( isset($updates, $updates->response) ) {
				unset($updates->response[$this->getUpdateListKey()]);
			}
			return $updates;
		}
		/**
		 * Get the key that will be used when adding updates to the update list that's maintained
		 * by the WordPress core. The list is always an associative array, but the key is different
		 * for plugins and themes.
		 *
		 * @return string
		 */
		abstract protected function getUpdateListKey();
		/**
		 * Should we show available updates?
		 *
		 * Usually the answer is "yes", but there are exceptions. For example, WordPress doesn't
		 * support automatic updates installation for mu-plugins, so PUC usually won't show update
		 * notifications in that case. See the plugin-specific subclass for details.
		 *
		 * Note: This method only applies to updates that are displayed (or not) in the WordPress
		 * admin. It doesn't affect APIs like requestUpdate and getUpdate.
		 *
		 * @return bool
		 */
		protected function shouldShowUpdates() {
			return true;
		}
		/* -------------------------------------------------------------------
		 * JSON-based update API
		 * -------------------------------------------------------------------
		 */
		/**
		 * Retrieve plugin or theme metadata from the JSON document at $this->metadataUrl.
		 *
		 * @param string $metaClass Parse the JSON as an instance of this class. It must have a static fromJson method.
		 * @param string $filterRoot
		 * @param array $queryArgs Additional query arguments.
		 * @return array [Puc_v4p6_Metadata|null, array|WP_Error] A metadata instance and the value returned by wp_remote_get().
		 */
		protected function requestMetadata($metaClass, $filterRoot, $queryArgs = array()) {
			//Query args to append to the URL. Plugins can add their own by using a filter callback (see addQueryArgFilter()).
			$queryArgs = array_merge(
				array(
					'installed_version' => strval($this->getInstalledVersion()),
					'php' => phpversion(),
					'locale' => get_locale(),
				),
				$queryArgs
			);
			$queryArgs = apply_filters($this->getUniqueName($filterRoot . '_query_args'), $queryArgs);
			//Various options for the wp_remote_get() call. Plugins can filter these, too.
			$options = array(
				'timeout' => 10, //seconds
				'headers' => array(
					'Accept' => 'application/json',
				),
			);
			$options = apply_filters($this->getUniqueName($filterRoot . '_options'), $options);
			//The metadata file should be at 'http://your-api.com/url/here/$slug/info.json'
			$url = $this->metadataUrl;
			if ( !empty($queryArgs) ){
				$url = add_query_arg($queryArgs, $url);
			}
			$result = wp_remote_get($url, $options);
			$result = apply_filters($this->getUniqueName('request_metadata_http_result'), $result, $url, $options);
			
			//Try to parse the response
			$status = $this->validateApiResponse($result);
			$metadata = null;
			if ( !is_wp_error($status) ){
				$metadata = call_user_func(array($metaClass, 'fromJson'), $result['body']);
			} else {
				do_action('puc_api_error', $status, $result, $url, $this->slug);
				$this->triggerError(
					sprintf('The URL %s does not point to a valid metadata file. ', $url)
					. $status->get_error_message(),
					E_USER_WARNING
				);
			}
			return array($metadata, $result);
		}
		/**
		 * Check if $result is a successful update API response.
		 *
		 * @param array|WP_Error $result
		 * @return true|WP_Error
		 */
		protected function validateApiResponse($result) {
			if ( is_wp_error($result) ) { /** @var WP_Error $result */
				return new WP_Error($result->get_error_code(), 'WP HTTP Error: ' . $result->get_error_message());
			}
			if ( !isset($result['response']['code']) ) {
				return new WP_Error(
					'puc_no_response_code',
					'wp_remote_get() returned an unexpected result.'
				);
			}
			if ( $result['response']['code'] !== 200 ) {
				return new WP_Error(
					'puc_unexpected_response_code',
					'HTTP response code is ' . $result['response']['code'] . ' (expected: 200)'
				);
			}
			if ( empty($result['body']) ) {
				return new WP_Error('puc_empty_response', 'The metadata file appears to be empty.');
			}
			return true;
		}
		/* -------------------------------------------------------------------
		 * Language packs / Translation updates
		 * -------------------------------------------------------------------
		 */
		/**
		 * Filter a list of translation updates and return a new list that contains only updates
		 * that apply to the current site.
		 *
		 * @param array $translations
		 * @return array
		 */
		protected function filterApplicableTranslations($translations) {
			$languages = array_flip(array_values(get_available_languages()));
			$installedTranslations = $this->getInstalledTranslations();
			$applicableTranslations = array();
			foreach ($translations as $translation) {
				//Does it match one of the available core languages?
				$isApplicable = array_key_exists($translation->language, $languages);
				//Is it more recent than an already-installed translation?
				if ( isset($installedTranslations[$translation->language]) ) {
					$updateTimestamp = strtotime($translation->updated);
					$installedTimestamp = strtotime($installedTranslations[$translation->language]['PO-Revision-Date']);
					$isApplicable = $updateTimestamp > $installedTimestamp;
				}
				if ( $isApplicable ) {
					$applicableTranslations[] = $translation;
				}
			}
			return $applicableTranslations;
		}
		/**
		 * Get a list of installed translations for this plugin or theme.
		 *
		 * @return array
		 */
		protected function getInstalledTranslations() {
			if ( !function_exists('wp_get_installed_translations') ) {
				return array();
			}
			$installedTranslations = wp_get_installed_translations($this->translationType . 's');
			if ( isset($installedTranslations[$this->directoryName]) ) {
				$installedTranslations = $installedTranslations[$this->directoryName];
			} else {
				$installedTranslations = array();
			}
			return $installedTranslations;
		}
		/**
		 * Insert translation updates into the list maintained by WordPress.
		 *
		 * @param stdClass $updates
		 * @return stdClass
		 */
		public function injectTranslationUpdates($updates) {
			$translationUpdates = $this->getTranslationUpdates();
			if ( empty($translationUpdates) ) {
				return $updates;
			}
			//Being defensive.
			if ( !is_object($updates) ) {
				$updates = new stdClass();
			}
			if ( !isset($updates->translations) ) {
				$updates->translations = array();
			}
			//In case there's a name collision with a plugin or theme hosted on wordpress.org,
			//remove any preexisting updates that match our thing.
			$updates->translations = array_values(array_filter(
				$updates->translations,
				array($this, 'isNotMyTranslation')
			));
			//Add our updates to the list.
			foreach($translationUpdates as $update) {
				$convertedUpdate = array_merge(
					array(
						'type' => $this->translationType,
						'slug' => $this->directoryName,
						'autoupdate' => 0,
						//AFAICT, WordPress doesn't actually use the "version" field for anything.
						//But lets make sure it's there, just in case.
						'version' => isset($update->version) ? $update->version : ('1.' . strtotime($update->updated)),
					),
					(array)$update
				);
				$updates->translations[] = $convertedUpdate;
			}
			return $updates;
		}
		/**
		 * Get a list of available translation updates.
		 *
		 * This method will return an empty array if there are no updates.
		 * Uses cached update data.
		 *
		 * @return array
		 */
		public function getTranslationUpdates() {
			return $this->updateState->getTranslations();
		}
		/**
		 * Remove all cached translation updates.
		 *
		 * @see wp_clean_update_cache
		 */
		public function clearCachedTranslationUpdates() {
			$this->updateState->setTranslations(array());
		}
		/**
		 * Filter callback. Keeps only translations that *don't* match this plugin or theme.
		 *
		 * @param array $translation
		 * @return bool
		 */
		protected function isNotMyTranslation($translation) {
			$isMatch = isset($translation['type'], $translation['slug'])
				&& ($translation['type'] === $this->translationType)
				&& ($translation['slug'] === $this->directoryName);
			return !$isMatch;
		}
		/* -------------------------------------------------------------------
		 * Fix directory name when installing updates
		 * -------------------------------------------------------------------
		 */
		/**
		 * Rename the update directory to match the existing plugin/theme directory.
		 *
		 * When WordPress installs a plugin or theme update, it assumes that the ZIP file will contain
		 * exactly one directory, and that the directory name will be the same as the directory where
		 * the plugin or theme is currently installed.
		 *
		 * GitHub and other repositories provide ZIP downloads, but they often use directory names like
		 * "project-branch" or "project-tag-hash". We need to change the name to the actual plugin folder.
		 *
		 * This is a hook callback. Don't call it from a plugin.
		 *
		 * @access protected
		 *
		 * @param string $source The directory to copy to /wp-content/plugins or /wp-content/themes. Usually a subdirectory of $remoteSource.
		 * @param string $remoteSource WordPress has extracted the update to this directory.
		 * @param WP_Upgrader $upgrader
		 * @return string|WP_Error
		 */
		public function fixDirectoryName($source, $remoteSource, $upgrader) {
			global $wp_filesystem;
			/** @var WP_Filesystem_Base $wp_filesystem */
			//Basic sanity checks.
			if ( !isset($source, $remoteSource, $upgrader, $upgrader->skin, $wp_filesystem) ) {
				return $source;
			}
			//If WordPress is upgrading anything other than our plugin/theme, leave the directory name unchanged.
			if ( !$this->isBeingUpgraded($upgrader) ) {
				return $source;
			}
			//Rename the source to match the existing directory.
			$correctedSource = trailingslashit($remoteSource) . $this->directoryName . '/';
			if ( $source !== $correctedSource ) {
				//The update archive should contain a single directory that contains the rest of plugin/theme files.
				//Otherwise, WordPress will try to copy the entire working directory ($source == $remoteSource).
				//We can't rename $remoteSource because that would break WordPress code that cleans up temporary files
				//after update.
				if ( $this->isBadDirectoryStructure($remoteSource) ) {
					return new WP_Error(
						'puc-incorrect-directory-structure',
						sprintf(
							'The directory structure of the update is incorrect. All files should be inside ' .
							'a directory named %s, not at the root of the ZIP archive.',
							htmlentities($this->slug)
						)
					);
				}
				/** @var WP_Upgrader_Skin $upgrader ->skin */
				$upgrader->skin->feedback(sprintf(
					'Renaming %s to %s…',
					'' . basename($source) . '',
					'' . $this->directoryName . ''
				));
				if ( $wp_filesystem->move($source, $correctedSource, true) ) {
					$upgrader->skin->feedback('Directory successfully renamed.');
					return $correctedSource;
				} else {
					return new WP_Error(
						'puc-rename-failed',
						'Unable to rename the update to match the existing directory.'
					);
				}
			}
			return $source;
		}
		/**
		 * Is there an update being installed right now, for this plugin or theme?
		 *
		 * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
		 * @return bool
		 */
		abstract public function isBeingUpgraded($upgrader = null);
		/**
		 * Check for incorrect update directory structure. An update must contain a single directory,
		 * all other files should be inside that directory.
		 *
		 * @param string $remoteSource Directory path.
		 * @return bool
		 */
		protected function isBadDirectoryStructure($remoteSource) {
			global $wp_filesystem;
			/** @var WP_Filesystem_Base $wp_filesystem */
			$sourceFiles = $wp_filesystem->dirlist($remoteSource);
			if ( is_array($sourceFiles) ) {
				$sourceFiles = array_keys($sourceFiles);
				$firstFilePath = trailingslashit($remoteSource) . $sourceFiles[0];
				return (count($sourceFiles) > 1) || (!$wp_filesystem->is_dir($firstFilePath));
			}
			//Assume it's fine.
			return false;
		}
		/* -------------------------------------------------------------------
		 * DebugBar integration
		 * -------------------------------------------------------------------
		 */
		/**
		 * Initialize the update checker Debug Bar plugin/add-on thingy.
		 */
		public function maybeInitDebugBar() {
			if ( class_exists('Debug_Bar', false) && file_exists(dirname(__FILE__) . '/DebugBar') ) {
				$this->createDebugBarExtension();
			}
		}
		protected function createDebugBarExtension() {
			return new Puc_v4p6_DebugBar_Extension($this);
		}
		/**
		 * Display additional configuration details in the Debug Bar panel.
		 *
		 * @param Puc_v4p6_DebugBar_Panel $panel
		 */
		public function onDisplayConfiguration($panel) {
			//Do nothing. Subclasses can use this to add additional info to the panel.
		}
	}
endif;