Api.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. <?php
  2. namespace YahnisElsts\PluginUpdateChecker\v5p0\Vcs;
  3. use Parsedown;
  4. use PucReadmeParser;
  5. if ( !class_exists(Api::class, false) ):
  6. abstract class Api {
  7. const STRATEGY_LATEST_RELEASE = 'latest_release';
  8. const STRATEGY_LATEST_TAG = 'latest_tag';
  9. const STRATEGY_STABLE_TAG = 'stable_tag';
  10. const STRATEGY_BRANCH = 'branch';
  11. protected $tagNameProperty = 'name';
  12. protected $slug = '';
  13. /**
  14. * @var string
  15. */
  16. protected $repositoryUrl = '';
  17. /**
  18. * @var mixed Authentication details for private repositories. Format depends on service.
  19. */
  20. protected $credentials = null;
  21. /**
  22. * @var string The filter tag that's used to filter options passed to wp_remote_get.
  23. * For example, "puc_request_info_options-slug" or "puc_request_update_options_theme-slug".
  24. */
  25. protected $httpFilterName = '';
  26. /**
  27. * @var string The filter applied to the list of update detection strategies that
  28. * are used to find the latest version.
  29. */
  30. protected $strategyFilterName = '';
  31. /**
  32. * @var string|null
  33. */
  34. protected $localDirectory = null;
  35. /**
  36. * Api constructor.
  37. *
  38. * @param string $repositoryUrl
  39. * @param array|string|null $credentials
  40. */
  41. public function __construct($repositoryUrl, $credentials = null) {
  42. $this->repositoryUrl = $repositoryUrl;
  43. $this->setAuthentication($credentials);
  44. }
  45. /**
  46. * @return string
  47. */
  48. public function getRepositoryUrl() {
  49. return $this->repositoryUrl;
  50. }
  51. /**
  52. * Figure out which reference (i.e. tag or branch) contains the latest version.
  53. *
  54. * @param string $configBranch Start looking in this branch.
  55. * @return null|Reference
  56. */
  57. public function chooseReference($configBranch) {
  58. $strategies = $this->getUpdateDetectionStrategies($configBranch);
  59. if ( !empty($this->strategyFilterName) ) {
  60. $strategies = apply_filters(
  61. $this->strategyFilterName,
  62. $strategies,
  63. $this->slug
  64. );
  65. }
  66. foreach ($strategies as $strategy) {
  67. $reference = call_user_func($strategy);
  68. if ( !empty($reference) ) {
  69. return $reference;
  70. }
  71. }
  72. return null;
  73. }
  74. /**
  75. * Get an ordered list of strategies that can be used to find the latest version.
  76. *
  77. * The update checker will try each strategy in order until one of them
  78. * returns a valid reference.
  79. *
  80. * @param string $configBranch
  81. * @return array<callable> Array of callables that return Vcs_Reference objects.
  82. */
  83. abstract protected function getUpdateDetectionStrategies($configBranch);
  84. /**
  85. * Get the readme.txt file from the remote repository and parse it
  86. * according to the plugin readme standard.
  87. *
  88. * @param string $ref Tag or branch name.
  89. * @return array Parsed readme.
  90. */
  91. public function getRemoteReadme($ref = 'master') {
  92. $fileContents = $this->getRemoteFile($this->getLocalReadmeName(), $ref);
  93. if ( empty($fileContents) ) {
  94. return array();
  95. }
  96. $parser = new PucReadmeParser();
  97. return $parser->parse_readme_contents($fileContents);
  98. }
  99. /**
  100. * Get the case-sensitive name of the local readme.txt file.
  101. *
  102. * In most cases it should just be called "readme.txt", but some plugins call it "README.txt",
  103. * "README.TXT", or even "Readme.txt". Most VCS are case-sensitive so we need to know the correct
  104. * capitalization.
  105. *
  106. * Defaults to "readme.txt" (all lowercase).
  107. *
  108. * @return string
  109. */
  110. public function getLocalReadmeName() {
  111. static $fileName = null;
  112. if ( $fileName !== null ) {
  113. return $fileName;
  114. }
  115. $fileName = 'readme.txt';
  116. if ( isset($this->localDirectory) ) {
  117. $files = scandir($this->localDirectory);
  118. if ( !empty($files) ) {
  119. foreach ($files as $possibleFileName) {
  120. if ( strcasecmp($possibleFileName, 'readme.txt') === 0 ) {
  121. $fileName = $possibleFileName;
  122. break;
  123. }
  124. }
  125. }
  126. }
  127. return $fileName;
  128. }
  129. /**
  130. * Get a branch.
  131. *
  132. * @param string $branchName
  133. * @return Reference|null
  134. */
  135. abstract public function getBranch($branchName);
  136. /**
  137. * Get a specific tag.
  138. *
  139. * @param string $tagName
  140. * @return Reference|null
  141. */
  142. abstract public function getTag($tagName);
  143. /**
  144. * Get the tag that looks like the highest version number.
  145. * (Implementations should skip pre-release versions if possible.)
  146. *
  147. * @return Reference|null
  148. */
  149. abstract public function getLatestTag();
  150. /**
  151. * Check if a tag name string looks like a version number.
  152. *
  153. * @param string $name
  154. * @return bool
  155. */
  156. protected function looksLikeVersion($name) {
  157. //Tag names may be prefixed with "v", e.g. "v1.2.3".
  158. $name = ltrim($name, 'v');
  159. //The version string must start with a number.
  160. if ( !is_numeric(substr($name, 0, 1)) ) {
  161. return false;
  162. }
  163. //The goal is to accept any SemVer-compatible or "PHP-standardized" version number.
  164. return (preg_match('@^(\d{1,5}?)(\.\d{1,10}?){0,4}?($|[abrdp+_\-]|\s)@i', $name) === 1);
  165. }
  166. /**
  167. * Check if a tag appears to be named like a version number.
  168. *
  169. * @param \stdClass $tag
  170. * @return bool
  171. */
  172. protected function isVersionTag($tag) {
  173. $property = $this->tagNameProperty;
  174. return isset($tag->$property) && $this->looksLikeVersion($tag->$property);
  175. }
  176. /**
  177. * Sort a list of tags as if they were version numbers.
  178. * Tags that don't look like version number will be removed.
  179. *
  180. * @param \stdClass[] $tags Array of tag objects.
  181. * @return \stdClass[] Filtered array of tags sorted in descending order.
  182. */
  183. protected function sortTagsByVersion($tags) {
  184. //Keep only those tags that look like version numbers.
  185. $versionTags = array_filter($tags, array($this, 'isVersionTag'));
  186. //Sort them in descending order.
  187. usort($versionTags, array($this, 'compareTagNames'));
  188. return $versionTags;
  189. }
  190. /**
  191. * Compare two tags as if they were version number.
  192. *
  193. * @param \stdClass $tag1 Tag object.
  194. * @param \stdClass $tag2 Another tag object.
  195. * @return int
  196. */
  197. protected function compareTagNames($tag1, $tag2) {
  198. $property = $this->tagNameProperty;
  199. if ( !isset($tag1->$property) ) {
  200. return 1;
  201. }
  202. if ( !isset($tag2->$property) ) {
  203. return -1;
  204. }
  205. return -version_compare(ltrim($tag1->$property, 'v'), ltrim($tag2->$property, 'v'));
  206. }
  207. /**
  208. * Get the contents of a file from a specific branch or tag.
  209. *
  210. * @param string $path File name.
  211. * @param string $ref
  212. * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
  213. */
  214. abstract public function getRemoteFile($path, $ref = 'master');
  215. /**
  216. * Get the timestamp of the latest commit that changed the specified branch or tag.
  217. *
  218. * @param string $ref Reference name (e.g. branch or tag).
  219. * @return string|null
  220. */
  221. abstract public function getLatestCommitTime($ref);
  222. /**
  223. * Get the contents of the changelog file from the repository.
  224. *
  225. * @param string $ref
  226. * @param string $localDirectory Full path to the local plugin or theme directory.
  227. * @return null|string The HTML contents of the changelog.
  228. */
  229. public function getRemoteChangelog($ref, $localDirectory) {
  230. $filename = $this->findChangelogName($localDirectory);
  231. if ( empty($filename) ) {
  232. return null;
  233. }
  234. $changelog = $this->getRemoteFile($filename, $ref);
  235. if ( $changelog === null ) {
  236. return null;
  237. }
  238. return Parsedown::instance()->text($changelog);
  239. }
  240. /**
  241. * Guess the name of the changelog file.
  242. *
  243. * @param string $directory
  244. * @return string|null
  245. */
  246. protected function findChangelogName($directory = null) {
  247. if ( !isset($directory) ) {
  248. $directory = $this->localDirectory;
  249. }
  250. if ( empty($directory) || !is_dir($directory) || ($directory === '.') ) {
  251. return null;
  252. }
  253. $possibleNames = array('CHANGES.md', 'CHANGELOG.md', 'changes.md', 'changelog.md');
  254. $files = scandir($directory);
  255. $foundNames = array_intersect($possibleNames, $files);
  256. if ( !empty($foundNames) ) {
  257. return reset($foundNames);
  258. }
  259. return null;
  260. }
  261. /**
  262. * Set authentication credentials.
  263. *
  264. * @param $credentials
  265. */
  266. public function setAuthentication($credentials) {
  267. $this->credentials = $credentials;
  268. }
  269. public function isAuthenticationEnabled() {
  270. return !empty($this->credentials);
  271. }
  272. /**
  273. * @param string $url
  274. * @return string
  275. */
  276. public function signDownloadUrl($url) {
  277. return $url;
  278. }
  279. /**
  280. * @param string $filterName
  281. */
  282. public function setHttpFilterName($filterName) {
  283. $this->httpFilterName = $filterName;
  284. }
  285. /**
  286. * @param string $filterName
  287. */
  288. public function setStrategyFilterName($filterName) {
  289. $this->strategyFilterName = $filterName;
  290. }
  291. /**
  292. * @param string $directory
  293. */
  294. public function setLocalDirectory($directory) {
  295. if ( empty($directory) || !is_dir($directory) || ($directory === '.') ) {
  296. $this->localDirectory = null;
  297. } else {
  298. $this->localDirectory = $directory;
  299. }
  300. }
  301. /**
  302. * @param string $slug
  303. */
  304. public function setSlug($slug) {
  305. $this->slug = $slug;
  306. }
  307. }
  308. endif;