PluginProbe
WPComplete / 1.0
WPComplete v1.0
2.9.5.7 2.9.5.6 trunk 1.0 1.1 1.2 1.4 1.4.6 2.3 2.9.1 2.9.2 2.9.5 2.9.5.1 2.9.5.3 2.9.5.4 2.9.5.5
wpcomplete / includes / github-checker.php

github-checker.php in WPComplete 1.0, at includes/github-checker.php

459 lines 13.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if ( !class_exists('PucGitHubChecker_3_1', false) ):
4
5 class PucGitHubChecker_3_1 extends PluginUpdateChecker_3_1 {
6 /**
7 * @var string GitHub username.
8 */
9 protected $userName;
10 /**
11 * @var string GitHub repository name.
12 */
13 protected $repositoryName;
14
15 /**
16 * @var string Either a fully qualified repository URL, or just "user/repo-name".
17 */
18 protected $repositoryUrl;
19
20 /**
21 * @var string The branch to use as the latest version. Defaults to "master".
22 */
23 protected $branch;
24
25 /**
26 * @var string GitHub authentication token. Optional.
27 */
28 protected $accessToken;
29
30 public function __construct(
31 $repositoryUrl,
32 $pluginFile,
33 $branch = 'master',
34 $checkPeriod = 12,
35 $optionName = '',
36 $muPluginFile = ''
37 ) {
38
39 $this->repositoryUrl = $repositoryUrl;
40 $this->branch = empty($branch) ? 'master' : $branch;
41
42 $path = @parse_url($repositoryUrl, PHP_URL_PATH);
43 if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
44 $this->userName = $matches['username'];
45 $this->repositoryName = $matches['repository'];
46 } else {
47 throw new InvalidArgumentException('Invalid GitHub repository URL: "' . $repositoryUrl . '"');
48 }
49
50 parent::__construct($repositoryUrl, $pluginFile, '', $checkPeriod, $optionName, $muPluginFile);
51 }
52
53 /**
54 * Retrieve details about the latest plugin version from GitHub.
55 *
56 * @param array $unusedQueryArgs Unused.
57 * @return PluginInfo_3_1
58 */
59 public function requestInfo($unusedQueryArgs = array()) {
60 $info = new PluginInfo_3_1();
61 $info->filename = $this->pluginFile;
62 $info->slug = $this->slug;
63
64 $this->setInfoFromHeader($this->getPluginHeader(), $info);
65
66 //Figure out which reference (tag or branch) we'll use to get the latest version of the plugin.
67 $ref = $this->branch;
68 if ( $this->branch === 'master' ) {
69 //Use the latest release.
70 $release = $this->getLatestRelease();
71 if ( $release !== null ) {
72 $ref = $release->tag_name;
73 $info->version = ltrim($release->tag_name, 'v'); //Remove the "v" prefix from "v1.2.3".
74 $info->last_updated = $release->created_at;
75 $info->download_url = $release->zipball_url;
76
77 if ( !empty($release->body) ) {
78 $info->sections['changelog'] = $this->parseMarkdown($release->body);
79 }
80 if ( isset($release->assets[0]) ) {
81 $info->downloaded = $release->assets[0]->download_count;
82 }
83 } else {
84 //Failing that, use the tag with the highest version number.
85 $tag = $this->getLatestTag();
86 if ( $tag !== null ) {
87 $ref = $tag->name;
88 $info->version = $tag->name;
89 $info->download_url = $tag->zipball_url;
90 }
91 }
92 }
93
94 if ( empty($info->download_url) ) {
95 $info->download_url = $this->buildArchiveDownloadUrl($ref);
96 } else if ( !empty($this->accessToken) ) {
97 $info->download_url = add_query_arg('access_token', $this->accessToken, $info->download_url);
98 }
99
100 //Get headers from the main plugin file in this branch/tag. Its "Version" header and other metadata
101 //are what the WordPress install will actually see after upgrading, so they take precedence over releases/tags.
102 $mainPluginFile = basename($this->pluginFile);
103 $remotePlugin = $this->getRemoteFile($mainPluginFile, $ref);
104 if ( !empty($remotePlugin) ) {
105 $remoteHeader = $this->getFileHeader($remotePlugin);
106 $this->setInfoFromHeader($remoteHeader, $info);
107 }
108
109 //Try parsing readme.txt. If it's formatted according to WordPress.org standards, it will contain
110 //a lot of useful information like the required/tested WP version, changelog, and so on.
111 if ( $this->readmeTxtExistsLocally() ) {
112 $this->setInfoFromRemoteReadme($ref, $info);
113 }
114
115 //The changelog might be in a separate file.
116 if ( empty($info->sections['changelog']) ) {
117 $info->sections['changelog'] = $this->getRemoteChangelog($ref);
118 if ( empty($info->sections['changelog']) ) {
119 $info->sections['changelog'] = __('There is no changelog available.', 'plugin-update-checker');
120 }
121 }
122
123 if ( empty($info->last_updated) ) {
124 //Fetch the latest commit that changed the main plugin file and use it as the "last_updated" date.
125 //It's reasonable to assume that every update will change the version number in that file.
126 $latestCommit = $this->getLatestCommit($mainPluginFile, $ref);
127 if ( $latestCommit !== null ) {
128 $info->last_updated = $latestCommit->commit->author->date;
129 }
130 }
131
132 $info = apply_filters('puc_request_info_result-' . $this->slug, $info, null);
133 return $info;
134 }
135
136 /**
137 * Get the latest release from GitHub.
138 *
139 * @return StdClass|null
140 */
141 protected function getLatestRelease() {
142 $releases = $this->api('/repos/:user/:repo/releases');
143 if ( is_wp_error($releases) || !is_array($releases) || !isset($releases[0]) ) {
144 return null;
145 }
146
147 $latestRelease = $releases[0];
148 return $latestRelease;
149 }
150
151 /**
152 * Get the tag that looks like the highest version number.
153 *
154 * @return StdClass|null
155 */
156 protected function getLatestTag() {
157 $tags = $this->api('/repos/:user/:repo/tags');
158
159 if ( is_wp_error($tags) || empty($tags) || !is_array($tags) ) {
160 return null;
161 }
162
163 usort($tags, array($this, 'compareTagNames')); //Sort from highest to lowest.
164 return $tags[0];
165 }
166
167 /**
168 * Compare two GitHub tags as if they were version number.
169 *
170 * @param string $tag1
171 * @param string $tag2
172 * @return int
173 */
174 protected function compareTagNames($tag1, $tag2) {
175 if ( !isset($tag1->name) ) {
176 return 1;
177 }
178 if ( !isset($tag2->name) ) {
179 return -1;
180 }
181 return -version_compare($tag1->name, $tag2->name);
182 }
183
184 /**
185 * Get the latest commit that changed the specified file.
186 *
187 * @param string $filename
188 * @param string $ref Reference name (e.g. branch or tag).
189 * @return StdClass|null
190 */
191 protected function getLatestCommit($filename, $ref = 'master') {
192 $commits = $this->api(
193 '/repos/:user/:repo/commits',
194 array(
195 'path' => $filename,
196 'sha' => $ref,
197 )
198 );
199 if ( !is_wp_error($commits) && is_array($commits) && isset($commits[0]) ) {
200 return $commits[0];
201 }
202 return null;
203 }
204
205 protected function getRemoteChangelog($ref = '') {
206 $filename = $this->getChangelogFilename();
207 if ( empty($filename) ) {
208 return null;
209 }
210
211 $changelog = $this->getRemoteFile($filename, $ref);
212 if ( $changelog === null ) {
213 return null;
214 }
215 return $this->parseMarkdown($changelog);
216 }
217
218 protected function getChangelogFilename() {
219 $pluginDirectory = dirname($this->pluginAbsolutePath);
220 if ( empty($this->pluginAbsolutePath) || !is_dir($pluginDirectory) || ($pluginDirectory === '.') ) {
221 return null;
222 }
223
224 $possibleNames = array('CHANGES.md', 'CHANGELOG.md', 'changes.md', 'changelog.md');
225 $files = scandir($pluginDirectory);
226 $foundNames = array_intersect($possibleNames, $files);
227
228 if ( !empty($foundNames) ) {
229 return reset($foundNames);
230 }
231 return null;
232 }
233
234 /**
235 * Convert Markdown to HTML.
236 *
237 * @param string $markdown
238 * @return string
239 */
240 protected function parseMarkdown($markdown) {
241 if ( !class_exists('Parsedown', false) ) {
242 require_once(dirname(__FILE__) . '/vendor/Parsedown' . (version_compare(PHP_VERSION, '5.3.0', '>=') ? '' : 'Legacy') . '.php');
243 }
244
245 $instance = Parsedown::instance();
246 return $instance->text($markdown);
247 }
248
249 /**
250 * Perform a GitHub API request.
251 *
252 * @param string $url
253 * @param array $queryParams
254 * @return mixed|WP_Error
255 */
256 protected function api($url, $queryParams = array()) {
257 $variables = array(
258 'user' => $this->userName,
259 'repo' => $this->repositoryName,
260 );
261 foreach ($variables as $name => $value) {
262 $url = str_replace('/:' . $name, '/' . urlencode($value), $url);
263 }
264 $url = 'https://api.github.com' . $url;
265
266 if ( !empty($this->accessToken) ) {
267 $queryParams['access_token'] = $this->accessToken;
268 }
269 if ( !empty($queryParams) ) {
270 $url = add_query_arg($queryParams, $url);
271 }
272
273 $response = wp_remote_get($url, array('timeout' => 10));
274 if ( is_wp_error($response) ) {
275 return $response;
276 }
277
278 $code = wp_remote_retrieve_response_code($response);
279 $body = wp_remote_retrieve_body($response);
280 if ( $code === 200 ) {
281 $document = json_decode($body);
282 return $document;
283 }
284
285 return new WP_Error(
286 'puc-github-http-error',
287 'GitHub API error. HTTP status: ' . $code
288 );
289 }
290
291 /**
292 * Set the access token that will be used to make authenticated GitHub API requests.
293 *
294 * @param string $accessToken
295 */
296 public function setAccessToken($accessToken) {
297 $this->accessToken = $accessToken;
298 }
299
300 /**
301 * Get the contents of a file from a specific branch or tag.
302 *
303 * @param string $path File name.
304 * @param string $ref
305 * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
306 */
307 protected function getRemoteFile($path, $ref = 'master') {
308 $apiUrl = '/repos/:user/:repo/contents/' . $path;
309 $response = $this->api($apiUrl, array('ref' => $ref));
310
311 if ( is_wp_error($response) || !isset($response->content) || ($response->encoding !== 'base64') ) {
312 return null;
313 }
314 return base64_decode($response->content);
315 }
316
317 /**
318 * Parse plugin metadata from the header comment.
319 * This is basically a simplified version of the get_file_data() function from /wp-includes/functions.php.
320 *
321 * @param $content
322 * @return array
323 */
324 protected function getFileHeader($content) {
325 $headers = array(
326 'Name' => 'Plugin Name',
327 'PluginURI' => 'Plugin URI',
328 'Version' => 'Version',
329 'Description' => 'Description',
330 'Author' => 'Author',
331 'AuthorURI' => 'Author URI',
332 'TextDomain' => 'Text Domain',
333 'DomainPath' => 'Domain Path',
334 'Network' => 'Network',
335
336 //The newest WordPress version that this plugin requires or has been tested with.
337 //We support several different formats for compatibility with other libraries.
338 'Tested WP' => 'Tested WP',
339 'Requires WP' => 'Requires WP',
340 'Tested up to' => 'Tested up to',
341 'Requires at least' => 'Requires at least',
342 );
343
344 $content = str_replace("\r", "\n", $content); //Normalize line endings.
345 $results = array();
346 foreach ($headers as $field => $name) {
347 $success = preg_match('/^[ \t\/*#@]*' . preg_quote($name, '/') . ':(.*)$/mi', $content, $matches);
348 if ( ($success === 1) && $matches[1] ) {
349 $results[$field] = _cleanup_header_comment($matches[1]);
350 } else {
351 $results[$field] = '';
352 }
353 }
354
355 return $results;
356 }
357
358 /**
359 * Copy plugin metadata from a file header to a PluginInfo object.
360 *
361 * @param array $fileHeader
362 * @param PluginInfo_3_1 $pluginInfo
363 */
364 protected function setInfoFromHeader($fileHeader, $pluginInfo) {
365 $headerToPropertyMap = array(
366 'Version' => 'version',
367 'Name' => 'name',
368 'PluginURI' => 'homepage',
369 'Author' => 'author',
370 'AuthorName' => 'author',
371 'AuthorURI' => 'author_homepage',
372
373 'Requires WP' => 'requires',
374 'Tested WP' => 'tested',
375 'Requires at least' => 'requires',
376 'Tested up to' => 'tested',
377 );
378 foreach ($headerToPropertyMap as $headerName => $property) {
379 if ( isset($fileHeader[$headerName]) && !empty($fileHeader[$headerName]) ) {
380 $pluginInfo->$property = $fileHeader[$headerName];
381 }
382 }
383
384 if ( !empty($fileHeader['Description']) ) {
385 $pluginInfo->sections['description'] = $fileHeader['Description'];
386 }
387 }
388
389 /**
390 * Copy plugin metadata from the remote readme.txt file.
391 *
392 * @param string $ref GitHub tag or branch where to look for the readme.
393 * @param PluginInfo_3_1 $pluginInfo
394 */
395 protected function setInfoFromRemoteReadme($ref, $pluginInfo) {
396 $readmeTxt = $this->getRemoteFile('readme.txt', $ref);
397 if ( empty($readmeTxt) ) {
398 return;
399 }
400
401 $readme = $this->parseReadme($readmeTxt);
402
403 if ( isset($readme['sections']) ) {
404 $pluginInfo->sections = array_merge($pluginInfo->sections, $readme['sections']);
405 }
406 if ( !empty($readme['tested_up_to']) ) {
407 $pluginInfo->tested = $readme['tested_up_to'];
408 }
409 if ( !empty($readme['requires_at_least']) ) {
410 $pluginInfo->requires = $readme['requires_at_least'];
411 }
412
413 if ( isset($readme['upgrade_notice'], $readme['upgrade_notice'][$pluginInfo->version]) ) {
414 $pluginInfo->upgrade_notice = $readme['upgrade_notice'][$pluginInfo->version];
415 }
416 }
417
418 protected function parseReadme($content) {
419 if ( !class_exists('PucReadmeParser', false) ) {
420 require_once(dirname(__FILE__) . '/vendor/readme-parser.php');
421 }
422 $parser = new PucReadmeParser();
423 return $parser->parse_readme_contents($content);
424 }
425
426 /**
427 * Check if the currently installed version has a readme.txt file.
428 *
429 * @return bool
430 */
431 protected function readmeTxtExistsLocally() {
432 $pluginDirectory = dirname($this->pluginAbsolutePath);
433 if ( empty($this->pluginAbsolutePath) || !is_dir($pluginDirectory) || ($pluginDirectory === '.') ) {
434 return false;
435 }
436 return is_file($pluginDirectory . '/readme.txt');
437 }
438
439 /**
440 * Generate a URL to download a ZIP archive of the specified branch/tag/etc.
441 *
442 * @param string $ref
443 * @return string
444 */
445 protected function buildArchiveDownloadUrl($ref = 'master') {
446 $url = sprintf(
447 'https://api.github.com/repos/%1$s/%2$s/zipball/%3$s',
448 urlencode($this->userName),
449 urlencode($this->repositoryName),
450 urlencode($ref)
451 );
452 if ( !empty($this->accessToken) ) {
453 $url = add_query_arg('access_token', $this->accessToken, $url);
454 }
455 return $url;
456 }
457 }
458
459 endif;