diff --git a/README.md b/README.md index d16f9ca..defea89 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ The plugin is designed to work out of the box with minimal configuration. Howeve - ~~pagination~~ ## Changelog +### 1.5.0 - 2025-02-13 +- Added repo update feature + ### 1.4.0 - 2025-02-13 - Added admin configuration page diff --git a/includes/updater.php b/includes/updater.php new file mode 100644 index 0000000..640ae40 --- /dev/null +++ b/includes/updater.php @@ -0,0 +1,410 @@ + + * @link http://jkudish.com + * @package WP_GitHub_Updater + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @copyright Copyright (c) 2011-2013, Joachim Kudish + * + * GNU General Public License, Free Software Foundation + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +class WpGitHubUpdater { + /** + * GitHub Updater version + */ + const VERSION = 1.6; + + /** + * @var $config the config for the updater + * @access public + */ + public $config; + + /** + * @var $missingConfig any config that is missing from the initialization of this instance + * @access public + */ + public $missingConfig; + + /** + * @var $githubData temporiraly store the data fetched from GitHub, allows us to only load the data once per class instance + * @access private + */ + private $githubData; + + + /** + * Class Constructor + * + * @since 1.0 + * @param array $config the configuration required for the updater to work + * @see hasMinimumConfig() + * @return void + */ + public function __construct($config = array()) { + + $defaults = array( + 'slug' => plugin_basename(__FILE__), + 'proper_folder_name' => dirname(plugin_basename(__FILE__)), + 'sslverify' => true, + 'access_token' => '', + ); + + $this->config = wp_parse_args($config, $defaults); + + // if the minimum config isn't set, issue a warning and bail + if (!$this->hasMinimumConfig()) { + $message = 'The GitHub Updater was initialized without the minimum required configuration, please check the config in your plugin. The following params are missing: '; + $message .= implode(',', $this->missingConfig); + _doing_it_wrong(__CLASS__, $message , self::VERSION); + return; + } + + $this->setDefaults(); + + add_filter('pre_set_site_transient_update_plugins', array($this, 'apiCheck')); + + // Hook into the plugin details screen + add_filter('plugins_api', array($this, 'getPluginInfo'), 10, 3); + add_filter('upgraderPostInstall', array($this, 'upgraderPostInstall'), 10, 3); + + // set timeout + add_filter('httpRequestTimeout', array($this, 'httpRequestTimeout')); + + // set sslverify for zip download + add_filter('http_request_args', array($this, 'httpRequestSslverify'), 10, 2); + } + + public function hasMinimumConfig() { + + $this->missingConfig = array(); + + $required_config_params = array( + 'api_url', + 'raw_url', + 'github_url', + 'zip_url', + 'requires', + 'tested', + 'readme', + ); + + foreach ($required_config_params as $required_param) { + if (empty($this->config[$required_param])) { $this->missingConfig[] = $required_param; } + } + + return empty($this->missingConfig); + } + + + /** + * Check wether or not the transients need to be overruled and API needs to be called for every single page load + * + * @return bool overrule or not + */ + public function overruleTransients() { + return defined('WP_GITHUB_FORCE_UPDATE') && WP_GITHUB_FORCE_UPDATE; + } + + + /** + * Set defaults + * + * @since 1.2 + * @return void + */ + public function setDefaults() { + if (!empty($this->config['access_token'])) { + + // See Downloading a zipball (private repo) https://help.github.com/articles/downloading-files-from-the-command-line + extract(parse_url($this->config['zip_url'])); // $scheme, $host, $path + + $zip_url = $scheme . '://api.github.com/repos' . $path; + $zip_url = add_query_arg(array('access_token' => $this->config['access_token']), $zip_url); + + $this->config['zip_url'] = $zip_url; + } + + + if (!isset($this->config['new_version'])) { $this->config['new_version'] = $this->getNewVersion(); } + + if (!isset($this->config['last_updated'])) { $this->config['last_updated'] = $this->getDate(); } + + if (!isset($this->config['description'])) { $this->config['description'] = $this->getDescription(); } + + $plugin_data = $this->getPluginData(); + if (!isset($this->config['plugin_name'])) { $this->config['plugin_name'] = $plugin_data['Name']; } + + if (!isset($this->config['version'])) { $this->config['version'] = $plugin_data['Version']; } + + if (!isset($this->config['author'])) { $this->config['author'] = $plugin_data['Author']; } + + if (!isset($this->config['homepage'])) { $this->config['homepage'] = $plugin_data['PluginURI']; } + + if (!isset($this->config['readme'])) { $this->config['readme'] = 'README.md'; } + + } + + + /** + * Callback fn for the httpRequestTimeout filter + * + * @since 1.0 + * @return int timeout value + */ + public function httpRequestTimeout() { + return 2; + } + + /** + * Callback fn for the http_request_args filter + * + * @param unknown $args + * @param unknown $url + * + * @return mixed + */ + public function httpRequestSslverify($args, $url) { + if ($this->config[ 'zip_url' ] == $url) { $args[ 'sslverify' ] = $this->config[ 'sslverify' ]; } + + return $args; + } + + + /** + * Get New Version from GitHub + * + * @since 1.0 + * @return int $version the version number + */ + public function getNewVersion() { + $version = get_site_transient(md5($this->config['slug']).'_new_version'); + + if ($this->overruleTransients() || (!isset($version) || !$version || '' == $version)) { + $version = $this->fetchVersionFromGitHub(); + if (false === $version) { + $version = $this->fetchVersionFromReadme(); + } + if (false !== $version) { + set_site_transient(md5($this->config['slug']).'_new_version', $version, 60*60*6); + } + } + + return $version; + } + + private function fetchVersionFromGitHub() { + $raw_response = $this->remoteGet(trailingslashit($this->config['raw_url']) . basename($this->config['slug'])); + if (is_wp_error($raw_response)) { return false; } + + if (is_array($raw_response) && !empty($raw_response['body'])) { + preg_match('/.*Version\:\s*(.*)$/mi', $raw_response['body'], $matches); + } + + return empty($matches[1]) ? false : $matches[1]; + } + + private function fetchVersionFromReadme() { + $raw_response = $this->remoteGet(trailingslashit($this->config['raw_url']) . $this->config['readme']); + if (is_wp_error($raw_response)) { return false; } + + preg_match('#^\s*`*~Current Version\:\s*([^~]*)~#im', $raw_response['body'], $__version); + + return isset($__version[1]) ? $__version[1] : false; + } + + + /** + * Interact with GitHub + * + * @param string $query + * + * @since 1.6 + * @return mixed + */ + public function remoteGet($query) { + if (!empty($this->config['access_token'])) { $query = add_query_arg(array('access_token' => $this->config['access_token']), $query); } + + return wp_remote_get($query, array( + 'sslverify' => $this->config['sslverify'] + )); + } + + + /** + * Get GitHub Data from the specified repository + * + * @since 1.0 + * @return array $githubData the data + */ + public function getGithubData() { + if (isset($this->githubData) && !empty($this->githubData)) { + $githubData = $this->githubData; + } else { + $githubData = get_site_transient(md5($this->config['slug']).'_githubData'); + + if ($this->overruleTransients() || (!isset($githubData) || !$githubData || '' == $githubData)) { + $githubData = $this->remoteGet($this->config['api_url']); + + if (is_wp_error($githubData)) { return false; } + + $githubData = json_decode($githubData['body']); + + // refresh every 6 hours + set_site_transient(md5($this->config['slug']).'_githubData', $githubData, 60*60*6); + } + + // Store the data in this class instance for future calls + $this->githubData = $githubData; + } + + return $githubData; + } + + + /** + * Get update date + * + * @since 1.0 + * @return string $date the date + */ + public function getDate() { + $_date = $this->getGithubData(); + return (!empty($_date->updated_at)) ? date('Y-m-d', strtotime($_date->updated_at)) : false; + } + + + /** + * Get plugin description + * + * @since 1.0 + * @return string $description the description + */ + public function getDescription() { + $_description = $this->getGithubData(); + return (!empty($_description->description)) ? $_description->description : false; + } + + + /** + * Get Plugin data + * + * @since 1.0 + * @return object $data the data + */ + public function getPluginData() { + include_once ABSPATH.'/wp-admin/includes/plugin.php'; + return get_plugin_data(WP_PLUGIN_DIR.'/'.$this->config['slug']); + } + + + /** + * Hook into the plugin update check and connect to GitHub + * + * @since 1.0 + * @param object $transient the plugin data transient + * @return object $transient updated plugin data transient + */ + public function apiCheck($transient) { + + // Check if the transient contains the 'checked' information + // If not, just return its value without hacking it + if (empty($transient->checked)) { return $transient; } + + // check the version and decide if it's new + $update = version_compare($this->config['new_version'], $this->config['version']); + + if (1 === $update) { + $response = new stdClass; + $response->new_version = $this->config['new_version']; + $response->slug = $this->config['proper_folder_name']; + $response->url = add_query_arg(array('access_token' => $this->config['access_token']), $this->config['github_url']); + $response->package = $this->config['zip_url']; + + // If response is false, don't alter the transient + if (false !== $response) { $transient->response[ $this->config['slug'] ] = $response; } + } + + return $transient; + } + + + /** + * Get Plugin info + * + * @since 1.0 + * @param bool $false always false + * @param string $action the API function being performed + * @param object $args plugin arguments + * @return object $response the plugin info + */ + public function getPluginInfo($false, $action, $response) { + + // Check if this call API is for the right plugin + if (!isset($response->slug) || $response->slug != $this->config['slug']) { return false; } + + $response->slug = $this->config['slug']; + $response->plugin_name = $this->config['plugin_name']; + $response->version = $this->config['new_version']; + $response->author = $this->config['author']; + $response->homepage = $this->config['homepage']; + $response->requires = $this->config['requires']; + $response->tested = $this->config['tested']; + $response->downloaded = 0; + $response->last_updated = $this->config['last_updated']; + $response->sections = array('description' => $this->config['description']); + $response->download_link = $this->config['zip_url']; + + return $response; + } + + + /** + * Upgrader/Updater + * Move & activate the plugin, echo the update message + * + * @since 1.0 + * @param boolean $true always true + * @param mixed $hook_extra not used + * @param array $result the result of the move + * @return array $result the result of the move + */ + public function upgraderPostInstall($true, $hook_extra, $result) { + + global $wp_filesystem; + + // Move & Activate + $proper_destination = WP_PLUGIN_DIR.'/'.$this->config['proper_folder_name']; + $wp_filesystem->move($result['destination'], $proper_destination); + $result['destination'] = $proper_destination; + $activate = activate_plugin(WP_PLUGIN_DIR.'/'.$this->config['slug']); + + // Output the update message + $fail = __('The plugin has been updated, but could not be reactivated. Please reactivate it manually.', 'github_plugin_updater'); + $success = __('Plugin reactivated successfully.', 'github_plugin_updater'); + echo is_wp_error($activate) ? $fail : $success; + return $result; + + } +} diff --git a/resource-filter.php b/resource-filter.php index a5ea3ab..448fdd4 100644 --- a/resource-filter.php +++ b/resource-filter.php @@ -2,14 +2,37 @@ /** * Plugin Name: Content Filter * Description: Adds filtering for the content typed by various taxonomies. - * Version: 1.4.0 + * Version: 1.5.0 * Author: Keith Solomon */ if (!defined('ABSPATH')) { exit; } // Prevent direct access +require_once plugin_dir_path(__FILE__) . 'includes/updater.php'; require_once plugin_dir_path(__FILE__) . 'includes/template-loader.php'; +function cfInit() { + if (is_admin()) { // note the use of is_admin() to double check that this is happening in the admin + $config = array( + 'slug' => plugin_basename(__FILE__), // this is the slug of your plugin + 'proper_folder_name' => 'plugin-name', // this is the name of the folder your plugin lives in + 'api_url' => 'https://api.github.com/repos/username/repository-name', // the GitHub API url of your GitHub repo + 'raw_url' => 'https://raw.github.com/username/repository-name/master', // the GitHub raw url of your GitHub repo + 'github_url' => 'https://github.com/username/repository-name', // the GitHub url of your GitHub repo + 'zip_url' => 'https://github.com/username/repository-name/zipball/master', // the zip url of the GitHub repo + 'sslverify' => true, // whether WP should check the validity of the SSL cert when getting an update, see https://github.com/jkudish/WordPress-GitHub-Plugin-Updater/issues/2 and https://github.com/jkudish/WordPress-GitHub-Plugin-Updater/issues/4 for details + 'requires' => '3.0', // which version of WordPress does your plugin require? + 'tested' => '3.3', // which version of WordPress is your plugin tested up to? + 'readme' => 'README.md', // which file to use as the readme for the version number + 'access_token' => '', // Access private repositories by authorizing under Plugins > GitHub Updates when this example plugin is installed + ); + + new WpGitHubUpdater($config); + } +} + +add_action('init', 'cfInit'); + class ContentFilterPlugin { /** Registers the necessary actions and filters for the Content Filter plugin. *