Api.php 7.6 KB

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