Как вы вероятно уже знаете, WordPress имеет механизм, который обнаруживает, что плагины, темы и само ядро WordPress имеют обновления. Уведомляет об этом пользователя, когда они имеются, получает информацию об этих обновлениях и позволяет автоматически устанавливать их. Однако всё это касается только плагинов и шаблонов тем размещённых в репозитариях WordPress.
Разумеется, существуют способы для расширения этого механизма на плагины и темы размещённые на других серверах. В сети есть немало скриптов для получения возможности обновления с GitHub и даже с собственного сервера. Есть скрипты и для обновления с Envato Market (CodeCanyon и ThemeForest). Однако все скрипты доступа к Envato используют API, который на сегодняшний день является устаревшим и с июня 2016 доступ к этому API будет закрыт. Теперь необходимо пользоваться новым API. Пришлось написать скрипт самому.
Немного об авто-обновлении
WordPress имеет отличную систему автоматического обновления, которая уведомляет Вас о том, что доступны новые версии ядра WordPress, установленные плагины или темы. Уведомления отображаются в админ-баре, а также на странице плагинов, где вы можете получить более подробную информацию о новой версии.
Для того, чтобы установить новую версию, вы просто должны нажать кнопку “Обновить автоматически”. WordPress автоматически загрузит новый пакет, распакует его и заменит старые файлы на новые. Нет необходимости использовать FTP для удаления и загрузки файлов.
Существует также специальная страница для обновлений, которая доступна в меню “Консоль”. Это полезно, когда вы хотите провести массовое обновление плагинов вместо обновления каждого из них в отдельности. На ней также имеется кнопка “Проверить снова”, с помощью которой можно принудительно проверить наличие новых обновлений. По умолчанию, WordPress делает это каждые 12 часов.
На обеих страницах (Плагины, Обновление) Вы можете посмотреть подробную информацию о новой версии, нажав на ссылку “Детали” или “Посмотреть информацию о версии Х.Х.Х”.
Скрипт
План работ
Все вышеперечисленные функции обеспечиваются сервером WordPress, т.е. при запросе с Вашего блога сервер wordpress.org возвращает всю необходимую информацию. Сервер Envato также позволяет получить всю необходимую информацию об обновлении плагина, продаваемого на CodeCanyon (плагины на Envato Market), но в другой структуре данных (отличной от WordPress). Кроме того, в скриптах WordPress нет ни одного, который бы отвечал за получение информации от каких либо серверов, кроме WordPress. Следовательно наша задача состоит в том, чтобы:
- Обеспечить получение информации о наличии новой версии плагина и уведомить о наличии оной WordPress
- Обеспечить получение информации о плагине для дальнейшего просмотра пользователем плагина (администратором блога)
- Обеспечить доступ к файлу новой версии плагина, размещённому на сервере Envato для загрузки и обновления
Все три пункта плана мы сможем реализовать с помощью трёх фильтров WordPress, а именно pre_set_site_transient_update_plugins, plugins_api и upgrader_package_options.
Подготовка данных
Скрипт реализован в виде класса (ООП) и принимает при запуске два необходимых для работы параметра. В принципе, Вы можете изменить класс так, чтобы принимаемые данные были заданы в самом классе, но при этом класс потеряет свою универсальность.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public function __construct( $id, $data ) { if ( ! empty( $id ) ) { $this->itemId = $id; $this->personalToken = ( isset( $data['token'] ) ) ? $data['token'] : null; $this->currentVersion = ( isset( $data['version'] ) ) ? $data['version'] : null; $this->slug = ( isset( $data['slug'] ) ) ? $data['slug'] : null; $this->pluginSlug = ( isset( $data['pluginSlug'] ) ) ? $data['pluginSlug'] : null; $this->name = ( isset( $data['name'] ) ) ? $data['name'] : null; } $this->enabled = self::is_enabled(); if($this->enabled) { add_filter( 'pre_set_site_transient_update_plugins', array( &$this, 'checkUpdate' ) ); add_filter( 'plugins_api', array( &$this, 'checkInfo' ), 10, 3 ); add_filter( 'upgrader_package_options', array( &$this, 'setUpdatePackage' ) ); } } |
Для обеспечения выполнения поставленной задачи, нам необходимы следующие данные:
- $id – идентификационный номер плагина в системе Envato
- $data[‘token’] – персональный ключ покупателя плагина (Personal Token)
- $data[‘version’] – текущая версия плагина
- $data[‘slug’] – псевдоним плагина (например: sam-pro-lite)
- $data[‘pluginSlug’] – полный псевдоним плагина (папка плагина + имя главного файла, например: sam-pro-lite/sam-pro-lite.php)
- $data[‘name’] – имя плагина
Покупатель плагина может сгенерировать Personal Token на сайте Envato API. Минимальный набор разрешений для ключа должен быть таким:
Конструктор плагина принимает эти данные в виде двух параметров: строкового $id и массива $data. После проверки наличия данных конструктор назначает в качестве обработчиков фильтров методы класса checkUpdate, checkInfo и setUpdatePackage.
- pre_set_site_transient_update_plugins – checkUpdate
- plugins_api – checkInfo
- upgrader_package_options – getUpdatePackage
Получение данных об обновлении
Так как Ваш плагин размещён не в репозитарии WordPress, запрос на получение информации об обновлениях Вашего плагина останется без ответа, если Вы не предпримете необходимых действий. Т.е. не вмешаетесь в процесс получения информации.
Каждый раз, когда WordPress опрашивает репозитарий о наличии новых версий, он записывает в специальный параметр _set_transient_update_plugins в таблицу базы данных wp_options. Фильтр pre_set_transient_update_plugins вызывается непосредственно перед записью параметра в таблицу. Необходимо вмешаться в этот процесс.
Фильтр передаёт на обработку только один параметр – массив данных, записываемый в базу данных.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public function checkUpdate( $transient ) { if ( empty( $transient->checked ) ) { return $transient; } $pluginInfo = self::requestInfo(); if ( isset( $pluginInfo['wordpress_plugin_metadata'] ) ) { $info = $pluginInfo['wordpress_plugin_metadata']; if ( version_compare( $this->currentVersion, $info['version'], '<' ) ) { $plugin = new stdClass(); $plugin->slug = $this->slug; $plugin->new_version = $info['version']; $plugin->url = ''; $plugin->package = $this->pluginSlug; $plugin->name = $info['plugin_name']; $plugin->plugin = $this->pluginSlug; $transient->response[ $this->pluginSlug ] = $plugin; } } return $transient; } |
Сначала проверяется наличие в массиве данных наличие поля “checked”. Если оно есть, это значит, что WordPress запросил и обработал данные об обновлении и сейчас самое время вставить в параметр свои данные. Если нет, значит 12 часов ещё не прошло… Ничего не делаем.
Если время работать, запрашиваем сервер Envato (вспомогательный метод класса requestInfo) и сравниваем текущую версию плагина с версией плагина полученной в ответ на запрос. Если полученная версия больше текущей, заполняем необходимые поля объекта описания плагина и добавляем объект в поле “response” результирующего массива. Всё. Теперь WordPress знает, что наш плагин требует обновления.
Что касается непосредственного получения информации о плагине, то для получения оной Envato API требуется только ID плагина (в системе Envato Market) и персонального ключа (Personal Token) покупателя:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public function requestInfo() { $args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $this->personalToken, ), 'timeout' => 30, ); $url = '//api.envato.com/v2/market/catalog/item?id=' . $this->itemId; $response = wp_remote_get( esc_url_raw( $url ), $args ); $response_code = wp_remote_retrieve_response_code( $response ); $response_message = wp_remote_retrieve_response_message( $response ); if ( 200 !== $response_code && ! empty( $response_message ) ) { return new WP_Error( $response_code, $response_message ); } elseif ( 200 !== $response_code ) { return new WP_Error( $response_code, __( 'An unknown API error occurred.', SAM_PRO_DOMAIN ) ); } else { $out = json_decode( wp_remote_retrieve_body( $response ), true ); if ( null === $out ) { return new WP_Error( 'api_error', __( 'An unknown API error occurred.', SAM_PRO_DOMAIN ) ); } return $out; } } |
Результат обработки фильтра будет таким:
Получение информации о плагине
Получение данных детальной информации о плагине лучше всего организовать обработав фильтр plugins_api. Само получение информации мало чем отличается от процедуры получения данных об обновлении. Только объёмом данных передаваемых в WordPress.
Фильтр передаёт обрабатывающей функции три параметра: $result, $action, $args, из которых нам потребуется только последний.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public function checkInfo( $result, $action, $args ) { if ( $args->slug === $this->slug ) { $pluginInfo = self::requestInfo(); if ( isset( $pluginInfo['wordpress_plugin_metadata'] ) ) { $info = $pluginInfo['wordpress_plugin_metadata']; $versions = self::getAttribute( $pluginInfo['attributes'], 'compatible-software' ); $sections = explode( '<h2 id="item-description__changelog">Changelog</h2>', $pluginInfo['description'] ); $description = ( isset( $sections[0] ) ) ? $sections[0] : ''; $changelog = ( isset( $sections[1] ) ) ? $sections[1] : ''; $plugin = new stdClass(); $plugin->name = $info['plugin_name']; $plugin->author = $info['author']; $plugin->slug = $this->slug; $plugin->version = $info['version']; $plugin->requires = $versions['required']; $plugin->tested = $versions['tested']; $plugin->rating = ( (int) $pluginInfo['rating']['count'] < 3 ) ? 100.0 : 20 * (float) $pluginInfo['rating']['rating']; $plugin->num_ratings = (int) $pluginInfo['rating']['count']; $plugin->active_installs = (int) $pluginInfo['number_of_sales']; $plugin->last_updated = $pluginInfo['updated_at']; $plugin->added = $pluginInfo['published_at']; $plugin->homepage = "URL домашней страницы Вашего плагина"; $plugin->sections = array( 'description' => $description, 'changelog' => $changelog ); $plugin->download_link = $pluginInfo['url']; $plugin->banners = array( 'high' => $pluginInfo['previews']['landscape_preview']['landscape_url'] ); return $plugin; } else { return false; } } else { return false; } } |
Если получен ответ метод возвращает массив данных содержащий детальную информацию о плагине, которая будет показана в модальном окне “Детали”:
Получение файла плагина
Для получения файла плагина и передачи его установщику WordPress лучше всего использовать фильтр upgrader_package_options. Есть другие варианты (например фильтр upgrader_pre_download), но этот лучший.
Так же как и для получения информации о плагине, для получения ссылки на файл плагина необходим ID плагина и персональный ключ покупателя. В ответе сервера будут получены две ссылки: одна на архив полного пакета плагина, другая на загружаемый архив, пригодный для передачи установщику WordPress.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public function requestDownloadUrl() { $args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $this->personalToken, ), 'timeout' => 30, ); $url = '//api.envato.com/v2/market/buyer/download?item_id=' . $this->itemId . '&shorten_url=true'; $response = wp_remote_get( esc_url_raw( $url ), $args ); $response_code = wp_remote_retrieve_response_code( $response ); $response_message = wp_remote_retrieve_response_message( $response ); if ( 200 !== $response_code && ! empty( $response_message ) ) { return new WP_Error( $response_code, $response_message ); } elseif ( 200 !== $response_code ) { return new WP_Error( $response_code, __( 'An unknown API error occurred.', SAM_PRO_DOMAIN ) ); } else { $out = json_decode( wp_remote_retrieve_body( $response ), true ); if ( null === $out ) { return new WP_Error( 'api_error', __( 'An unknown API error occurred.', SAM_PRO_DOMAIN ) ); } return $out; } } |
Фильтр передаёт обработчику только один параметр – массив, содержащий некоторые данные о загружаемом пакете, позволяющие установить путь к файлу пакета плагина. Чем мы и воспользуемся. К сожалению, нет никакой возможности определить какой именно плагин будет загружаться в данный момент с помощью легальных способов. Если бы в массиве параметров содержался псевдоним плагина, всё было бы совсем просто. Но, увы, нет. Именно для того, чтобы “поймать” свой плагин мы записали в поле “URL пакета” (package) полный псевдоним плагина.
1 2 3 4 5 6 7 8 9 10 11 12 |
public function setUpdatePackage( $options ) { $package = $options['package']; if ( $package === $this->pluginSlug ) { $response = self::requestDownloadUrl(); $options['package'] = ( is_wp_error( $response ) || empty( $response ) || ! empty( $response['error'] ) ) ? '' : $response['wordpress_plugin']; } return $options; } |
Если получение URL пакета завершилось успехом, установщику WordPress передаётся полученный URL, если нет – передаётся пустая строка, что приводит к отказу от обновления, но не приводит к краху системы.
Вот и всё. Осталось только включить скрипт в нужном месте и в нужное время.
Чтобы подключить скрипт воспользуемся событием (action) init.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public function __construct() { ... add_action( 'init', array( &$this, 'updatingService' ) ); ... } public function updatingService() { $settings = self::getSettings(); include_once ('tools/simplelib-plugin-upgrader.php'); $emToken = self::decryptData($settings['emToken']); $this->samProUpgrader = new SimpleLibPluginUpgrader('12721925', array( 'token' => $emToken, 'version' => SAM_PRO_VERSION, 'slug' => 'sam-pro-lite', 'pluginSlug' => basename( SAM_PRO_PATH ) . '/' . basename( SAM_PRO_MAIN_FILE ), 'name' => 'SAM Pro Lite' )); } |
Полный код скрипта
На сегодняшний день скрипт не полностью универсален, но…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
<?php /** * Class SimpleLibPluginUpgrader. * Version 1.4 * Author: minimus * Author URI: //simplelib.com */ if ( ! class_exists( 'SimpleLibPluginUpgrader' ) ) { class SimpleLibPluginUpgrader { private $itemId = null; private $personalToken = null; private $currentVersion = null; private $slug = null; private $pluginSlug = null; private $name = null; private $homepage = ''; private $errorString = ''; private $defaultSections = array( 'description', 'installation', 'faq', 'screenshots', 'changelog', 'reviews', 'other_notes' ); public $enabled = false; public $callback = null; /** * SimpleLibPluginUpgrader constructor. * * @param string $id Envato Item ID * @param array $data Contains the required parameters * token — Personal Token of buyer * version — plugin current version * slug — slug of plugin (i.e.: sam-pro-lite) * pluginSlug — full slug of plugin (plugin folder + name of main plugin file, * i.e.: sam-pro-lite/sam-pro-lite.php) * name — name of plugin * homepage – plugin homepage URL, not required * errorString - localized error string * @param null|callable $callback The function provides splitting of the content of the Envato plugin description * to the standard sections. */ public function __construct( $id, $data, $callback = null ) { if ( ! empty( $id ) ) { $this->itemId = $id; $this->personalToken = ( isset( $data['token'] ) ) ? $data['token'] : null; $this->currentVersion = ( isset( $data['version'] ) ) ? $data['version'] : null; $this->slug = ( isset( $data['slug'] ) ) ? $data['slug'] : null; $this->pluginSlug = ( isset( $data['pluginSlug'] ) ) ? $data['pluginSlug'] : null; $this->name = ( isset( $data['name'] ) ) ? $data['name'] : null; $this->homepage = ( isset( $data['homepage'] ) ) ? $data['homepage'] : ''; $this->errorString = ( isset( $data['errorString'] ) ) ? $data['errorString'] : 'An unknown API error occurred.'; $this->callback = $callback; } $this->enabled = self::is_enabled(); if ( $this->enabled ) { add_filter( 'pre_set_site_transient_update_plugins', array( &$this, 'checkUpdate' ) ); add_filter( 'plugins_api', array( &$this, 'checkInfo' ), 10, 3 ); add_filter( 'upgrader_package_options', array( &$this, 'setUpdatePackage' ) ); } } /** * Checking for all the transmitted data to the class * * @return bool */ private function is_enabled() { return ( ! is_null( $this->itemId ) && ! is_null( $this->personalToken ) && ! is_null( $this->currentVersion ) && ! is_null( $this->slug ) && ! is_null( $this->pluginSlug ) && ! is_null( $this->name ) ); } /** * Preparing of the part of received data * * @param array $data preparing data * @param string $name the name of input data part * * @return array|bool|string */ private function getAttribute( $data, $name ) { $out = ''; foreach ( $data as $key => $val ) { if ( $val['name'] === $name ) { switch ( $name ) { case 'compatible-software': $out = array( 'required' => str_replace( 'WordPress ', '', $val['value'][ count( $val['value'] ) - 1 ] ), 'tested' => str_replace( 'WordPress ', '', $val['value'][0] ) ); break; default: $out = false; } } } return $out; } /** * Default function for splitting content. If user function is not defined, provides splitting of the content * of the Envato plugin description to the standard sections. * Default sections: description, installation, faq, screenshots, changelog, reviews, other_notes. * * @param null|array $data content of the Envato plugin description * * @return array */ private function getSections( $data = null ) { if ( is_null( $data ) || empty( $data ) ) { return array(); } $out = array(); $m = preg_match_all( "/<h2(.*?)>(.+?)<\/h2>/", $data, $matches ); $sections = preg_split( "/<h2(.*?)>(.+?)<\/h2>/", $data ); $out['description'] = ( isset( $sections[0] ) ) ? $sections[0] : ''; foreach ( $matches[2] as $key => $match ) { $out[ strtolower( $match ) ] = $sections[ $key + 1 ]; } return $out; } /** * Request data from Envato API * * @param string $data type of data for request * * @return array|mixed|null|object|WP_Error */ public function request( $data = 'info' ) { $args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $this->personalToken, ), 'timeout' => 30, ); switch ( $data ) { case 'info': $url = '//api.envato.com/v3/market/catalog/item?id=' . $this->itemId; break; case 'link': $url = '//api.envato.com/v3/market/buyer/download?item_id=' . $this->itemId . '&shorten_url=true'; break; default: $url = '//api.envato.com/v3/market/catalog/item?id=' . $this->itemId; } $response = wp_remote_get( esc_url_raw( $url ), $args ); $response_code = wp_remote_retrieve_response_code( $response ); $response_message = wp_remote_retrieve_response_message( $response ); if ( 200 !== $response_code && ! empty( $response_message ) ) { return new WP_Error( $response_code, $response_message ); } elseif ( 200 !== $response_code ) { return new WP_Error( $response_code, $this->errorString ); } elseif ( 200 == $response_code ) { $out = json_decode( wp_remote_retrieve_body( $response ), true ); if ( null === $out ) { return new WP_Error( 'api_error', $this->errorString ); } return $out; } else { return null; } } /** * pre_set_site_transient_update_plugins filter handler. Checking the availability of an update of the plugin * on the CodeCanyon. * * @param object $transient * * @return object */ public function checkUpdate( $transient ) { if ( empty( $transient->checked ) ) { return $transient; } $pluginInfo = self::request(); if ( is_array( $pluginInfo ) && isset( $pluginInfo['wordpress_plugin_metadata'] ) ) { $info = $pluginInfo['wordpress_plugin_metadata']; if ( version_compare( $this->currentVersion, $info['version'], '<' ) ) { $plugin = new stdClass(); $plugin->slug = $this->slug; $plugin->new_version = $info['version']; $plugin->url = ''; $plugin->package = $this->pluginSlug; $plugin->name = $info['plugin_name']; $plugin->plugin = $this->pluginSlug; $transient->response[ $this->pluginSlug ] = $plugin; } } return $transient; } /** * plugins_api filter handler. Retrieving plugin information from Envato API. * * @param false|object|array $result The result object or array. Default false. * @param string $action The type of information being requested from the Plugin Install API. * @param object $args Plugin API arguments. * * @return bool|object */ public function checkInfo( $result, $action, $args ) { if ( $args->slug === $this->slug ) { $pluginInfo = self::request(); if ( is_array( $pluginInfo ) && isset( $pluginInfo['wordpress_plugin_metadata'] ) ) { $info = $pluginInfo['wordpress_plugin_metadata']; $versions = self::getAttribute( $pluginInfo['attributes'], 'compatible-software' ); $sections = ( is_null( $this->callback ) ) ? self::getSections( $pluginInfo['description'] ) : call_user_func( $this->callback, $pluginInfo['description'] ); $plugin = new stdClass(); $plugin->name = $info['plugin_name']; $plugin->author = $info['author']; $plugin->slug = $this->slug; $plugin->version = $info['version']; $plugin->requires = $versions['required']; $plugin->tested = $versions['tested']; $plugin->rating = ( (int) $pluginInfo['rating']['count'] < 3 ) ? 100.0 : 20 * (float) $pluginInfo['rating']['rating']; $plugin->num_ratings = (int) $pluginInfo['rating']['count']; $plugin->active_installs = (int) $pluginInfo['number_of_sales']; $plugin->last_updated = $pluginInfo['updated_at']; $plugin->added = $pluginInfo['published_at']; $plugin->homepage = $this->homepage; $plugin->sections = $sections; $plugin->download_link = $pluginInfo['url']; $plugin->banners = array( 'high' => $pluginInfo['previews']['landscape_preview']['landscape_url'] ); return $plugin; } else { return false; } } else { return false; } } /** * upgrader_package_options filter handler. Retrieving plugin package URI from Envato API. * * @param array $options The package options before running an update. * * @return array */ public function setUpdatePackage( $options ) { $package = $options['package']; if ( $package === $this->pluginSlug ) { $response = self::request( 'link' ); $options['package'] = ( is_wp_error( $response ) || empty( $response ) || ! empty( $response['error'] ) ) ? '' : $response['wordpress_plugin']; } return $options; } } } |
Теперь точно всё! 😉
© 2016, minimus. Все права защищены. При копировании и републикации статьи, ссылка на первоисточник обязательна.
Полезная информация для меня в частности.