Testing update server communications
authorChuck Scott <cscott@gaslightmedia.com>
Fri, 7 Oct 2016 14:16:13 +0000 (10:16 -0400)
committerChuck Scott <cscott@gaslightmedia.com>
Fri, 7 Oct 2016 14:16:13 +0000 (10:16 -0400)
defines.php
index.php
lib/opentools-update-checker/css/opentools-updatecheck.css [new file with mode: 0644]
lib/opentools-update-checker/js/opentools-updatecheck.js [new file with mode: 0644]
lib/opentools-update-checker/opentools-update-checker.php [new file with mode: 0644]
lib/plugin-update-checker/plugin-update-checker.php

index 8647c5e..29d59bf 100644 (file)
@@ -31,6 +31,12 @@ $pageUri = explode('?', $_SERVER['REQUEST_URI']);               // Bust this up
 // Enable to display smarty template debug.
 define('GLM_SERVERSTATS_PLUGIN_DEBUG_SMARTY', false);
 
+/*
+ *  Create a copy of the plugin slug that can be used as a variable prefix used to keep
+ *  global instances from clashing with instances in other plugins.
+ */
+$vprefix = str_replace('-', '_', GLM_SERVERSTATS_PLUGIN_SLUG);
+define('GLM_SERVERSTATS_PREFIX', $vprefix.'_');
 
 // URLs
 define('GLM_SERVERSTATS_SITE_BASE_URL', home_url('/') );
index fd90db8..6231550 100644 (file)
--- a/index.php
+++ b/index.php
@@ -161,13 +161,20 @@ function serverStatsController($model)
 /*
  * Plugin Update Support - uses Gaslight Media update server
  */
-require GLM_SERVERSTATS_PLUGIN_LIB_PATH.'/plugin-update-checker/plugin-update-checker.php';
-$MyUpdateChecker = PucFactory::buildUpdateChecker(
-     'http://gaslightmedia.gaslightmedia.com/update_server/?action=get_metadata&slug='.GLM_SERVERSTATS_PLUGIN_SLUG,
+require GLM_SERVERSTATS_PLUGIN_LIB_PATH.'/opentools-update-checker/opentools-update-checker.php';
+${GLM_SERVERSTATS_PREFIX."updateChecker"} = new OpenToolsPluginUpdateChecker(
+//     'http://gaslightmedia.gaslightmedia.com/GlmUpdateServer/?action=get_metadata&slug='.GLM_SERVERSTATS_PLUGIN_SLUG,
+    'http://192.168.44.51/update_server/?action=get_metadata&slug='.GLM_SERVERSTATS_PLUGIN_SLUG,
     __FILE__,
     GLM_SERVERSTATS_PLUGIN_SLUG
 );
 
+${GLM_SERVERSTATS_PREFIX."updateChecker"}->declareCredentials(array(
+  'license_key' => __('License Key:')
+));
+
+
+
 /*
 *
 * Activate and Deactivate hooks
diff --git a/lib/opentools-update-checker/css/opentools-updatecheck.css b/lib/opentools-update-checker/css/opentools-updatecheck.css
new file mode 100644 (file)
index 0000000..e0ed451
--- /dev/null
@@ -0,0 +1,23 @@
+
+tr.otup_update_credentials .update-credentials {
+    margin: 0 10px 8px 31px;
+    padding: 6px 12px 8px 40px;
+    background-color: #f7f7f7;
+    background-color: rgba(0,0,0,.03);
+}
+
+.widefat tr.otup_update_credentials th input, .widefat tr.otup_update_credentials thead td input {
+    /* margin: 0 0 0 8px; */
+    padding: inherit;
+    vertical-align: inherit;
+}
+
+tr.otup_update_credentials div.message-fail {
+    background-color: #FFBFBF;
+    padding: 3px;
+}
+
+tr.otup_update_credentials div.message-success {
+    background-color: #BFFFBF;
+    padding: 3px;
+}
diff --git a/lib/opentools-update-checker/js/opentools-updatecheck.js b/lib/opentools-update-checker/js/opentools-updatecheck.js
new file mode 100644 (file)
index 0000000..662a5cf
--- /dev/null
@@ -0,0 +1,76 @@
+/**
+ * OpenTools Update Check Admin JS
+ */
+var showUpdateCredentialsRow = function (btn) {
+    if (showUpdateCredentialsRow.credentialsRowDisplayed) {
+        return;
+    }
+    showUpdateCredentialsRow.credentialsRowDisplayed = true;
+    var ajaxurl = jQuery(btn).data('ajaxurl');
+    var slug = jQuery(btn).data("slug");
+    var nonce = jQuery(btn).data("nonce");
+    var ajaxargs = {
+        type: "POST",
+        url: ajaxurl,
+        data: {
+            action: 'getUpdateCredentialsRow',
+            slug: slug,
+            _ajax_nonce: nonce
+        },
+        success: function ( json ) {
+            jQuery(btn).closest('tr').after(json['row']);
+        },
+        error: function() {  },
+        complete: function() {  },
+    };
+    jQuery.ajax(ajaxargs);
+    return false;
+};
+
+var submitUpdateCredentials = function(btn) {
+    var ajaxurl = jQuery(btn).data('ajaxurl');
+    var slug = jQuery(btn).data("slug");
+    var nonce = jQuery(btn).data("nonce");
+    // the credentialvars data field contains a json-encoded array of variables!
+    var credentialvars = jQuery(btn).data("credentialvars");
+    
+    var tr = jQuery(btn).closest('tr');
+    var data = { 
+            action: 'submitUpdateCredentials',
+            slug: slug,
+            _ajax_nonce: nonce,
+        };
+    
+    var index;
+    for (index = 0; index < credentialvars.length; index++) {
+        var credname = credentialvars[index];
+        data[credname] = jQuery(tr).find("input[name='otup_update_credentials["+slug+"]["+credname+"]']").val();
+    }
+    
+    var ajaxargs = {
+        type: "POST",
+        url: ajaxurl,
+        data: data,
+        success: function ( json ) {
+            if (json['success']) {
+                jQuery(tr).find('div.update-credentials-message').html(json['message']);
+                jQuery(tr).find('div.update-credentials').removeClass('message-fail').addClass('message-success')
+                jQuery(tr).find('div.update-credentials-form').fadeOut( 500, function() { jQuery(this).remove(); });
+                jQuery(tr).closest('table').find('a.otup_credentials_link_'+slug).removeClass('dashicons-no').addClass('dashicons-yes');
+                jQuery(tr).delay(5000).fadeOut(1000, function() { jQuery(this).remove(); });
+                
+            } else {
+                jQuery(tr).find('div.update-credentials-message').html(json['message']);
+                jQuery(tr).find('div.update-credentials').addClass('message-fail').removeClass('message-success');
+                jQuery(tr).closest('table').find('a.otup_credentials_link_'+slug).removeClass('dashicons-yes').addClass('dashicons-no');
+            }
+        },
+        error: function() { 
+            jQuery(tr).find('div.update-credentials-message').html("Unable to validate the update credentials. Please make sure the server is available.");
+            jQuery(tr).find('div.update-credentials').addClass('message-fail').removeClass('message-success');
+        },
+        complete: function() {  },
+    };
+    jQuery.ajax(ajaxargs);
+    return false;
+}
\ No newline at end of file
diff --git a/lib/opentools-update-checker/opentools-update-checker.php b/lib/opentools-update-checker/opentools-update-checker.php
new file mode 100644 (file)
index 0000000..a4f4ad5
--- /dev/null
@@ -0,0 +1,250 @@
+<?php
+/**
+ * OpenTools Plugin Update Checker Library
+ *
+ * Copyright 2016 Reinhold Kainhofer
+ * Extends the plugin-update-checker by Janis Elsts
+ * Released under the MIT license.
+ */
+
+
+// *****************************************************************
+// * PLUGIN UPDATES (using plugin-update-checker and a self-written update server script)
+// * http://w-shadow.com/blog/2010/09/02/automatic-updates-for-any-plugin/
+// *****************************************************************
+
+if (!class_exists('OpenToolsPluginUpdateChecker')):
+
+require dirname(__FILE__).'/../plugin-update-checker/plugin-update-checker.php';
+
+class OpenToolsPluginUpdateChecker extends PluginUpdateChecker_3_1 {
+       protected $credvars = array();
+       protected $ajaxurl = '';
+       public function __construct($metadataUrl, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '')
+       {
+               parent::__construct($metadataUrl, $pluginFile, $slug, $checkPeriod, $optionName, $muPluginFile);
+               $this->installOTHooks();
+       }
+       public function declareCredentials($credential_def) {
+               $this->credvars = $credential_def;
+               // Append the update credentials to the update server link
+               $this->addQueryArgFilter(array($this, 'appendQueryArgsCredentials'));
+
+       }
+
+       protected function installOTHooks()
+       {
+               $this->ajaxurl = is_network_admin()?network_admin_url( 'admin-ajax.php' ): admin_url( 'admin-ajax.php' );
+
+               add_action('admin_print_scripts-plugins.php', array($this, 'addCredentialCheckScripts'));
+               add_action('admin_print_styles-plugins.php',  array($this, 'addCredentialCheckStyles'));
+
+//             add_filter('plugin_row_meta', array($this, 'displayUpdateCredentialsLink'), 9, 2);
+               add_filter('plugin_action_links_'.$this->pluginFile, array($this, 'displayUpdateCredentialsLink'), 9, 2);
+
+               add_action( 'wp_ajax_getUpdateCredentialsRow',          array( &$this, 'getUpdateCredentialsRow') );
+               add_action( 'wp_ajax_submitUpdateCredentials',          array( &$this, 'submitUpdateCredentials') );
+       }
+
+       protected function getCredentials($slug)
+       {
+               $credentials = array('validated' => FALSE);
+               foreach ($this->credvars as $credkey => $credname) {
+                       $credentials[$credkey] = get_option('otup_credentials_'.$slug.'_'.$credkey);
+               }
+               $credentials['validated']    = get_option('otup_credentials_validated_'.$slug);
+               return $credentials;
+       }
+
+       protected function setCredentials($slug, $credentials, $validated = false)
+       {
+               foreach ($credentials as $credkey => $credvalue) {
+                       update_option('otup_credentials_'.$slug.'_'.$credkey, $credvalue, false);
+               }
+               update_option('otup_credentials_validated_'.$slug,    $validated,   false);
+       }
+
+
+       public function addCredentialCheckScripts() {
+               wp_register_script( 'opentools-updatecheck', plugins_url('js/opentools-updatecheck.js', __FILE__), array('jquery'));
+               wp_enqueue_script( 'opentools-updatecheck');
+       }
+
+       public function addCredentialCheckStyles() {
+               wp_register_style( 'opentools-updatecheck', plugins_url('css/opentools-updatecheck.css', __FILE__));
+               wp_enqueue_style( 'opentools-updatecheck');
+       }
+
+       /** Append the ordernumber and order password to the update server URL
+        */
+       public function appendQueryArgsCredentials($queryArgs) {
+               $credentials = $this->getCredentials($this->slug);
+               foreach ($credentials as $credkey => $credvalue) {
+                       $queryArgs[$credkey] = $credvalue;
+               }
+               return $queryArgs;
+       }
+
+       /**
+        * Add a "Update Credentials" link to the plugin row in the "Plugins" page. By default,
+        * the new link will appear after the "Visit plugin site" link.
+        *
+        * You can change the link text by using the "otup_enter_update_credentials-$slug" filter.
+        * Returning an empty string from the filter will disable the link.
+        *
+        * @param array $pluginMeta Array of meta links.
+        * @param string $pluginFile
+        * @return array
+        */
+       public function displayUpdateCredentialsLink($links, $pluginFile) {
+               $isRelevant = ($pluginFile == $this->pluginFile)
+                             || (!empty($this->muPluginFile) && $pluginFile == $this->muPluginFile);
+               $isRelevant = $isRelevant && !empty($this->credvars);
+
+               if ( $isRelevant && current_user_can('update_plugins') ) {
+                       $credentials = $this->getCredentials($this->slug);
+                       $textcolor = $credentials['validated']?'<span>':'<span style="color: #c50034;">';
+                       $linkText = apply_filters('otup_enter_update_credentials-' . $this->slug, $textcolor.__('Update License Key</span>', 'oton-updates').'</span>');
+                       if ( !empty($linkText) ) {
+                               $iconyesno = $credentials['validated']?'yes':'no';
+                               $link = sprintf('<a href="#" onClick=\'return showUpdateCredentialsRow(this);\' class="dashicons-before dashicons-'.$iconyesno.' otup_credentials_link_'.$this->slug.'" data-slug="%s" data-nonce="%s" data-ajaxurl="%s" >%s</a>', esc_attr($this->slug), esc_attr(wp_create_nonce( 'otup_enter_update_credentials' )), esc_attr($this->ajaxurl), $linkText);
+                               array_unshift($links, $link);
+                       }
+               }
+               return $links;
+       }
+
+
+       /**
+       * If the user has clicked on the "Update Credentials" link, display the input boxes after the plugin row.
+       *
+       * @param string $file
+       * @param array  $plugin_data
+       * @return false|void
+       */
+       public function getUpdateCredentialsRow() {
+               $json = array('row' => '', 'message'=>'Unsuccessful');
+
+               $showCredentials = isset($_REQUEST['slug'])
+                       && $_REQUEST['slug'] == $this->slug
+                       && current_user_can('update_plugins')
+                       && check_ajax_referer('otup_enter_update_credentials');
+               $showCredentials = $showCredentials && !empty($this->credvars);
+
+               if ( $showCredentials && (is_network_admin() || !is_multisite() )) {
+                       $slug = $this->slug;
+                       if ( is_network_admin() ) {
+                               $active_class = is_plugin_active_for_network( $this->pluginFile ) ? 'active': '';
+                       } else {
+                               $active_class = is_plugin_active( $this->pluginFile ) ? 'active' : '';
+                       }
+
+                       $current_credentials = $this->getCredentials($slug);
+
+                       $tr = '<tr class="' . $active_class . ' otup_update_credentials" id="' . esc_attr( $slug . '-credentials' ) . '" >';
+                       $tr .= '<th colspan="3" class="check-column colspanchange">';
+                       $tr .= '<div class="update-credentials">';
+                       $tr .= '<div class="update-credentials-message">';
+                       $tr .= '</div>';
+                       $tr .= '<div class="update-credentials-form">';
+
+                       foreach ($this->credvars as $credkey => $credname) {
+                               $tr .= $credname    . " <input type=\"text\" name=\"otup_update_credentials[$slug][$credkey]\" value=\"" . esc_attr($current_credentials[$credkey]) . "\">&nbsp;&nbsp;&nbsp;";
+                       }
+
+                       $tr .= sprintf('<input type="submit" class="button otup_update_credentials_submit" onclick="return submitUpdateCredentials(this);" data-slug="%s" data-nonce="%s" data-ajaxurl="%s" data-credentialvars=\'%s\'>',
+                               esc_attr($this->slug),
+                               esc_attr(wp_create_nonce( 'otup_enter_update_credentials_'.$slug )),
+                               esc_attr($this->ajaxurl),
+                               esc_attr(json_encode(array_keys($this->credvars)))
+                       );
+
+                       $tr .= '</div>';
+                       $tr .= '</div></th></tr>';
+                       $json['row'] = $tr;
+                       $json['message'] = '';
+               } else {
+                       $json['message'] = __("No permissions to modify update credentials", "opentools-updatecheck");
+               }
+               wp_send_json($json);
+       }
+
+
+       /**
+        * Check the submitted update credentials for correctness and save them
+        *
+        * @return void
+        */
+       public function submitUpdateCredentials() {
+               $json = array('message' => '', 'success' => FALSE);
+               $slug = isset($_REQUEST['slug'])?($_REQUEST['slug']):"INVALIDSLUG";
+
+               $submitCredentials = $slug == $this->slug
+                       && current_user_can('update_plugins')
+                       && check_ajax_referer('otup_enter_update_credentials_'.$slug);
+               $submitCredentials = $submitCredentials && !empty($this->credvars);
+
+               if ( $submitCredentials ) {
+                       $credentials = array();
+                       foreach ($this->credvars as $credkey=>$credname) {
+                               if (isset($_REQUEST[$credkey])) {
+                                       $credentials[$credkey] = $_REQUEST[$credkey];
+                               }
+                       }
+
+                       $message = "";
+                       $validated = $this->checkUpdateCredentials($credentials, $message);
+                       $this->setCredentials($this->slug, $credentials, $validated);
+
+                       $json['success'] = $validated;
+
+                       if ($validated) {
+                               if ( is_network_admin() ) {
+                                       $active_class = is_plugin_active_for_network( $this->pluginFile ) ? 'active': '';
+                               } else {
+                                       $active_class = is_plugin_active( $this->pluginFile ) ? 'active' : '';
+                               }
+
+                               $json['message'] .= __("License Key accepted. Automatic updates for this plugin are enabled.", "opentools-updatecheck");
+                       } else {
+                               $json['message'] = $message;
+                       }
+               } else {
+                       $json['message'] = __("No permissions to modify update credentials", "opentools-updatecheck");
+               }
+               wp_send_json($json);
+       }
+
+       public function checkUpdateCredentials($credentials, &$message)
+       {
+               $this->setCredentials($this->slug, $credentials);
+               $success = FALSE;
+               $updateinfo = $this->requestInfo(array());
+
+               if ($updateinfo && isset($updateinfo->download_url)) {
+                       $downloadurl = $updateinfo->download_url;
+                       $downloadurl = apply_filters('puc_check_download_query_args-'.$this->slug, $downloadurl);
+
+                       $headers = get_headers($downloadurl);
+                       list($version, $status_code, $msg) = explode(' ',$headers[0], 3);
+
+                       // Check the HTTP Status code
+                       $message = $msg;
+                       $success = ($status_code==200);
+               } else {
+                       $message = __('Unable to verify your License Key. Please check your License Key and try again.');
+               }
+               return $success;
+       }
+
+       public function addAccessCheckQueryArgFilter($callback){
+               add_filter('puc_check_download_query_args-'.$this->slug, $callback);
+       }
+
+};
+
+endif;
+
+
+
+// *****************************************************************
index a894844..0193de2 100644 (file)
@@ -2,7 +2,7 @@
 /**\r
  * Plugin Update Checker Library 3.1\r
  * http://w-shadow.com/\r
- * \r
+ *\r
  * Copyright 2016 Janis Elsts\r
  * Released under the MIT license. See license.txt for details.\r
  */\r
@@ -10,8 +10,8 @@
 if ( !class_exists('PluginUpdateChecker_3_1', false) ):\r
 \r
 /**\r
- * A custom plugin update checker. \r
- * \r
+ * A custom plugin update checker.\r
+ *\r
  * @author Janis Elsts\r
  * @copyright 2016\r
  * @version 3.0\r
@@ -60,7 +60,7 @@ class PluginUpdateChecker_3_1 {
                if ( empty($this->slug) ){\r
                        $this->slug = basename($this->pluginFile, '.php');\r
                }\r
-               \r
+\r
                if ( empty($this->optionName) ){\r
                        $this->optionName = 'external_updates-' . $this->slug;\r
                }\r
@@ -89,17 +89,17 @@ class PluginUpdateChecker_3_1 {
        protected function createScheduler($checkPeriod) {\r
                return new PucScheduler_3_1($this, $checkPeriod);\r
        }\r
-       \r
+\r
        /**\r
-        * Install the hooks required to run periodic update checks and inject update info \r
-        * into WP data structures. \r
-        * \r
+        * Install the hooks required to run periodic update checks and inject update info\r
+        * into WP data structures.\r
+        *\r
         * @return void\r
         */\r
        protected function installHooks(){\r
                //Override requests for plugin information\r
                add_filter('plugins_api', array($this, 'injectInfo'), 20, 3);\r
-               \r
+\r
                //Insert our update info into the update array maintained by WP.\r
                add_filter('site_transient_update_plugins', array($this,'injectUpdate')); //WP 3.0+\r
                add_filter('transient_update_plugins', array($this,'injectUpdate')); //WP 2.8+\r
@@ -133,7 +133,7 @@ class PluginUpdateChecker_3_1 {
                $this->metadataHost = @parse_url($this->metadataUrl, PHP_URL_HOST);\r
                add_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10, 2);\r
        }\r
-       \r
+\r
        /**\r
         * Explicitly allow HTTP requests to the metadata URL.\r
         *\r
@@ -161,9 +161,9 @@ class PluginUpdateChecker_3_1 {
 \r
        /**\r
         * Retrieve plugin info from the configured API endpoint.\r
-        * \r
+        *\r
         * @uses wp_remote_get()\r
-        * \r
+        *\r
         * @param array $queryArgs Additional query arguments to append to the request. Optional.\r
         * @return PluginInfo_3_1\r
         */\r
@@ -172,7 +172,7 @@ class PluginUpdateChecker_3_1 {
                $installedVersion = $this->getInstalledVersion();\r
                $queryArgs['installed_version'] = ($installedVersion !== null) ? $installedVersion : '';\r
                $queryArgs = apply_filters('puc_request_info_query_args-'.$this->slug, $queryArgs);\r
-               \r
+\r
                //Various options for the wp_remote_get() call. Plugins can filter these, too.\r
                $options = array(\r
                        'timeout' => 10, //seconds\r
@@ -181,13 +181,13 @@ class PluginUpdateChecker_3_1 {
                        ),\r
                );\r
                $options = apply_filters('puc_request_info_options-'.$this->slug, $options);\r
-               \r
+\r
                //The plugin info should be at 'http://your-api.com/url/here/$slug/info.json'\r
-               $url = $this->metadataUrl; \r
+               $url = $this->metadataUrl;\r
                if ( !empty($queryArgs) ){\r
                        $url = add_query_arg($queryArgs, $url);\r
                }\r
-               \r
+\r
                $result = wp_remote_get(\r
                        $url,\r
                        $options\r
@@ -221,6 +221,7 @@ class PluginUpdateChecker_3_1 {
         * @return true|WP_Error\r
         */\r
        private function validateApiResponse($result) {\r
+\r
                if ( is_wp_error($result) ) { /** @var WP_Error $result */\r
                        return new WP_Error($result->get_error_code(), 'WP HTTP Error: ' . $result->get_error_message());\r
                }\r
@@ -251,7 +252,7 @@ class PluginUpdateChecker_3_1 {
         * @return PluginUpdate_3_1 An instance of PluginUpdate, or NULL when no updates are available.\r
         */\r
        public function requestUpdate(){\r
-               //For the sake of simplicity, this function just calls requestInfo() \r
+               //For the sake of simplicity, this function just calls requestInfo()\r
                //and transforms the result accordingly.\r
                $pluginInfo = $this->requestInfo(array('checking_for_updates' => '1'));\r
                if ( $pluginInfo == null ){\r
@@ -299,10 +300,10 @@ class PluginUpdateChecker_3_1 {
 \r
                return $applicableTranslations;\r
        }\r
-       \r
+\r
        /**\r
         * Get the currently installed version of the plugin.\r
-        * \r
+        *\r
         * @return string Version number.\r
         */\r
        public function getInstalledVersion(){\r
@@ -376,20 +377,20 @@ class PluginUpdateChecker_3_1 {
                        $state->checkedVersion = '';\r
                        $state->update = null;\r
                }\r
-               \r
+\r
                $state->lastCheck = time();\r
                $state->checkedVersion = $installedVersion;\r
-               $this->setUpdateState($state); //Save before checking in case something goes wrong \r
-               \r
+               $this->setUpdateState($state); //Save before checking in case something goes wrong\r
+\r
                $state->update = $this->requestUpdate();\r
                $this->setUpdateState($state);\r
 \r
                return $this->getUpdate();\r
        }\r
-       \r
+\r
        /**\r
         * Load the update checker state from the DB.\r
-        *  \r
+        *\r
         * @return stdClass|null\r
         */\r
        public function getUpdateState() {\r
@@ -403,11 +404,11 @@ class PluginUpdateChecker_3_1 {
                }\r
                return $state;\r
        }\r
-       \r
-       \r
+\r
+\r
        /**\r
         * Persist the update checker state to the DB.\r
-        * \r
+        *\r
         * @param StdClass $state\r
         * @return void\r
         */\r
@@ -428,13 +429,13 @@ class PluginUpdateChecker_3_1 {
        public function resetUpdateState() {\r
                delete_site_option($this->optionName);\r
        }\r
-       \r
+\r
        /**\r
-        * Intercept plugins_api() calls that request information about our plugin and \r
-        * use the configured API endpoint to satisfy them. \r
-        * \r
+        * Intercept plugins_api() calls that request information about our plugin and\r
+        * use the configured API endpoint to satisfy them.\r
+        *\r
         * @see plugins_api()\r
-        * \r
+        *\r
         * @param mixed $result\r
         * @param string $action\r
         * @param array|object $args\r
@@ -447,19 +448,19 @@ class PluginUpdateChecker_3_1 {
                if ( !$relevant ) {\r
                        return $result;\r
                }\r
-               \r
+\r
                $pluginInfo = $this->requestInfo();\r
                $pluginInfo = apply_filters('puc_pre_inject_info-' . $this->slug, $pluginInfo);\r
                if ( $pluginInfo ) {\r
                        return $pluginInfo->toWpFormat();\r
                }\r
-                               \r
+\r
                return $result;\r
        }\r
-       \r
+\r
        /**\r
         * Insert the latest update (if any) into the update list maintained by WP.\r
-        * \r
+        *\r
         * @param StdClass $updates Update list.\r
         * @return StdClass Modified update list.\r
         */\r
@@ -859,48 +860,48 @@ class PluginUpdateChecker_3_1 {
        }\r
 \r
        /**\r
-        * Register a callback for filtering query arguments. \r
-        * \r
+        * Register a callback for filtering query arguments.\r
+        *\r
         * The callback function should take one argument - an associative array of query arguments.\r
         * It should return a modified array of query arguments.\r
-        * \r
+        *\r
         * @uses add_filter() This method is a convenience wrapper for add_filter().\r
-        * \r
+        *\r
         * @param callable $callback\r
         * @return void\r
         */\r
        public function addQueryArgFilter($callback){\r
                add_filter('puc_request_info_query_args-'.$this->slug, $callback);\r
        }\r
-       \r
+\r
        /**\r
         * Register a callback for filtering arguments passed to wp_remote_get().\r
-        * \r
+        *\r
         * The callback function should take one argument - an associative array of arguments -\r
         * and return a modified array or arguments. See the WP documentation on wp_remote_get()\r
-        * for details on what arguments are available and how they work. \r
-        * \r
+        * for details on what arguments are available and how they work.\r
+        *\r
         * @uses add_filter() This method is a convenience wrapper for add_filter().\r
-        * \r
+        *\r
         * @param callable $callback\r
         * @return void\r
         */\r
        public function addHttpRequestArgFilter($callback){\r
                add_filter('puc_request_info_options-'.$this->slug, $callback);\r
        }\r
-       \r
+\r
        /**\r
         * Register a callback for filtering the plugin info retrieved from the external API.\r
-        * \r
-        * The callback function should take two arguments. If the plugin info was retrieved \r
-        * successfully, the first argument passed will be an instance of  PluginInfo. Otherwise, \r
-        * it will be NULL. The second argument will be the corresponding return value of \r
+        *\r
+        * The callback function should take two arguments. If the plugin info was retrieved\r
+        * successfully, the first argument passed will be an instance of  PluginInfo. Otherwise,\r
+        * it will be NULL. The second argument will be the corresponding return value of\r
         * wp_remote_get (see WP docs for details).\r
-        *  \r
+        *\r
         * The callback function should return a new or modified instance of PluginInfo or NULL.\r
-        * \r
+        *\r
         * @uses add_filter() This method is a convenience wrapper for add_filter().\r
-        * \r
+        *\r
         * @param callable $callback\r
         * @return void\r
         */\r
@@ -955,7 +956,7 @@ if ( !class_exists('PluginInfo_3_1', false) ):
 \r
 /**\r
  * A container class for holding and transforming various plugin metadata.\r
- * \r
+ *\r
  * @author Janis Elsts\r
  * @copyright 2016\r
  * @version 3.0\r
@@ -963,7 +964,7 @@ if ( !class_exists('PluginInfo_3_1', false) ):
  */\r
 class PluginInfo_3_1 {\r
        //Most fields map directly to the contents of the plugin's info.json file.\r
-       //See the relevant docs for a description of their meaning.  \r
+       //See the relevant docs for a description of their meaning.\r
        public $name;\r
        public $slug;\r
        public $version;\r
@@ -975,25 +976,25 @@ class PluginInfo_3_1 {
 \r
        public $author;\r
        public $author_homepage;\r
-       \r
+\r
        public $requires;\r
        public $tested;\r
        public $upgrade_notice;\r
-       \r
+\r
        public $rating;\r
        public $num_ratings;\r
        public $downloaded;\r
        public $active_installs;\r
        public $last_updated;\r
-       \r
+\r
        public $id = 0; //The native WP.org API returns numeric plugin IDs, but they're not used for anything.\r
 \r
        public $filename; //Plugin filename relative to the plugins directory.\r
-               \r
+\r
        /**\r
-        * Create a new instance of PluginInfo from JSON-encoded plugin info \r
+        * Create a new instance of PluginInfo from JSON-encoded plugin info\r
         * returned by an external update API.\r
-        * \r
+        *\r
         * @param string $json Valid JSON string representing plugin info.\r
         * @return PluginInfo_3_1|null New instance of PluginInfo, or NULL on error.\r
         */\r
@@ -1002,18 +1003,18 @@ class PluginInfo_3_1 {
                $apiResponse = json_decode($json);\r
                if ( empty($apiResponse) || !is_object($apiResponse) ){\r
                        trigger_error(\r
-                               "Failed to parse plugin metadata. Try validating your .json file with http://jsonlint.com/",\r
+                               "Failed to parse plugin metadata. Try validating your .json file with http://jsonlint.com/\n".print_r($apiResponse,1),\r
                                E_USER_NOTICE\r
                        );\r
                        return null;\r
                }\r
-               \r
+\r
                $valid = self::validateMetadata($apiResponse);\r
                if ( is_wp_error($valid) ){\r
                        trigger_error($valid->get_error_message(), E_USER_NOTICE);\r
                        return null;\r
                }\r
-               \r
+\r
                $info = new self();\r
                foreach(get_object_vars($apiResponse) as $key => $value){\r
                        $info->$key = $value;\r
@@ -1021,8 +1022,8 @@ class PluginInfo_3_1 {
 \r
                //json_decode decodes assoc. arrays as objects. We want it as an array.\r
                $info->sections = (array)$info->sections;\r
-               \r
-               return $info;           \r
+\r
+               return $info;\r
        }\r
 \r
        /**\r
@@ -1045,17 +1046,17 @@ class PluginInfo_3_1 {
                return true;\r
        }\r
 \r
-       \r
+\r
        /**\r
         * Transform plugin info into the format used by the native WordPress.org API\r
-        * \r
+        *\r
         * @return object\r
         */\r
        public function toWpFormat(){\r
                $info = new stdClass;\r
-               \r
+\r
                //The custom update API is built so that many fields have the same name and format\r
-               //as those returned by the native WordPress.org API. These can be assigned directly. \r
+               //as those returned by the native WordPress.org API. These can be assigned directly.\r
                $sameFormat = array(\r
                        'name', 'slug', 'version', 'requires', 'tested', 'rating', 'upgrade_notice',\r
                        'num_ratings', 'downloaded', 'active_installs', 'homepage', 'last_updated',\r
@@ -1090,14 +1091,14 @@ class PluginInfo_3_1 {
                return $this->author;\r
        }\r
 }\r
-       \r
+\r
 endif;\r
 \r
 if ( !class_exists('PluginUpdate_3_1', false) ):\r
 \r
 /**\r
  * A simple container class for holding information about an available update.\r
- * \r
+ *\r
  * @author Janis Elsts\r
  * @copyright 2016\r
  * @version 3.0\r
@@ -1119,10 +1120,10 @@ class PluginUpdate_3_1 {
                'download_url', 'upgrade_notice', 'filename',\r
                'translations'\r
        );\r
-       \r
+\r
        /**\r
         * Create a new instance of PluginUpdate from its JSON-encoded representation.\r
-        * \r
+        *\r
         * @param string $json\r
         * @return PluginUpdate_3_1|null\r
         */\r
@@ -1141,18 +1142,18 @@ class PluginUpdate_3_1 {
        /**\r
         * Create a new instance of PluginUpdate based on an instance of PluginInfo.\r
         * Basically, this just copies a subset of fields from one object to another.\r
-        * \r
+        *\r
         * @param PluginInfo_3_1 $info\r
         * @return PluginUpdate_3_1\r
         */\r
        public static function fromPluginInfo($info){\r
                return self::fromObject($info);\r
        }\r
-       \r
+\r
        /**\r
-        * Create a new instance of PluginUpdate by copying the necessary fields from \r
+        * Create a new instance of PluginUpdate by copying the necessary fields from\r
         * another object.\r
-        *  \r
+        *\r
         * @param StdClass|PluginInfo_3_1|PluginUpdate_3_1 $object The source object.\r
         * @return PluginUpdate_3_1 The new copy.\r
         */\r
@@ -1169,13 +1170,13 @@ class PluginUpdate_3_1 {
                }\r
                return $update;\r
        }\r
-       \r
+\r
        /**\r
-        * Create an instance of StdClass that can later be converted back to \r
+        * Create an instance of StdClass that can later be converted back to\r
         * a PluginUpdate. Useful for serialization and caching, as it avoids\r
         * the "incomplete object" problem if the cached value is loaded before\r
         * this class.\r
-        * \r
+        *\r
         * @return StdClass\r
         */\r
        public function toStdClass() {\r
@@ -1191,11 +1192,11 @@ class PluginUpdate_3_1 {
                }\r
                return $object;\r
        }\r
-       \r
-       \r
+\r
+\r
        /**\r
         * Transform the update into the format used by WordPress native plugin API.\r
-        * \r
+        *\r
         * @return object\r
         */\r
        public function toWpFormat(){\r
@@ -1212,11 +1213,11 @@ class PluginUpdate_3_1 {
                if ( !empty($this->upgrade_notice) ){\r
                        $update->upgrade_notice = $this->upgrade_notice;\r
                }\r
-               \r
+\r
                return $update;\r
        }\r
 }\r
-       \r
+\r
 endif;\r
 \r
 if ( !class_exists('PucScheduler_3_1', false) ):\r