An updated theme (based on Atticus Finch) for ClassicPress
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

270 lines
8.7 KiB

3 years ago
  1. <?php
  2. if ( !class_exists('Puc_v4_Factory', false) ):
  3. /**
  4. * A factory that builds update checker instances.
  5. *
  6. * When multiple versions of the same class have been loaded (e.g. PluginUpdateChecker 4.0
  7. * and 4.1), this factory will always use the latest available minor version. Register class
  8. * versions by calling {@link PucFactory::addVersion()}.
  9. *
  10. * At the moment it can only build instances of the UpdateChecker class. Other classes are
  11. * intended mainly for internal use and refer directly to specific implementations.
  12. */
  13. class Puc_v4_Factory {
  14. protected static $classVersions = array();
  15. protected static $sorted = false;
  16. protected static $myMajorVersion = '';
  17. protected static $latestCompatibleVersion = '';
  18. /**
  19. * Create a new instance of the update checker.
  20. *
  21. * This method automatically detects if you're using it for a plugin or a theme and chooses
  22. * the appropriate implementation for your update source (JSON file, GitHub, BitBucket, etc).
  23. *
  24. * @see Puc_v4p1_UpdateChecker::__construct
  25. *
  26. * @param string $metadataUrl The URL of the metadata file, a GitHub repository, or another supported update source.
  27. * @param string $fullPath Full path to the main plugin file or to the theme directory.
  28. * @param string $slug Custom slug. Defaults to the name of the main plugin file or the theme directory.
  29. * @param int $checkPeriod How often to check for updates (in hours).
  30. * @param string $optionName Where to store book-keeping info about update checks.
  31. * @param string $muPluginFile The plugin filename relative to the mu-plugins directory.
  32. * @return Puc_v4p1_Plugin_UpdateChecker|Puc_v4p1_Theme_UpdateChecker|Puc_v4p1_Vcs_BaseChecker
  33. */
  34. public static function buildUpdateChecker($metadataUrl, $fullPath, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {
  35. $fullPath = wp_normalize_path($fullPath);
  36. $id = null;
  37. //Plugin or theme?
  38. $themeDirectory = self::getThemeDirectoryName($fullPath);
  39. if ( self::isPluginFile($fullPath) ) {
  40. $type = 'Plugin';
  41. $id = $fullPath;
  42. } else if ( $themeDirectory !== null ) {
  43. $type = 'Theme';
  44. $id = $themeDirectory;
  45. } else {
  46. throw new RuntimeException(sprintf(
  47. 'The update checker cannot determine if "%s" is a plugin or a theme. ' .
  48. 'This is a bug. Please contact the PUC developer.',
  49. htmlentities($fullPath)
  50. ));
  51. }
  52. //Which hosting service does the URL point to?
  53. $service = self::getVcsService($metadataUrl);
  54. $apiClass = null;
  55. if ( empty($service) ) {
  56. //The default is to get update information from a remote JSON file.
  57. $checkerClass = $type . '_UpdateChecker';
  58. } else {
  59. //You can also use a VCS repository like GitHub.
  60. $checkerClass = 'Vcs_' . $type . 'UpdateChecker';
  61. $apiClass = $service . 'Api';
  62. }
  63. $checkerClass = self::getCompatibleClassVersion($checkerClass);
  64. if ( $checkerClass === null ) {
  65. trigger_error(
  66. sprintf(
  67. 'PUC %s does not support updates for %ss %s',
  68. htmlentities(self::$latestCompatibleVersion),
  69. strtolower($type),
  70. $service ? ('hosted on ' . htmlentities($service)) : 'using JSON metadata'
  71. ),
  72. E_USER_ERROR
  73. );
  74. return null;
  75. }
  76. if ( !isset($apiClass) ) {
  77. //Plain old update checker.
  78. return new $checkerClass($metadataUrl, $id, $slug, $checkPeriod, $optionName, $muPluginFile);
  79. } else {
  80. //VCS checker + an API client.
  81. $apiClass = self::getCompatibleClassVersion($apiClass);
  82. if ( $apiClass === null ) {
  83. trigger_error(sprintf(
  84. 'PUC %s does not support %s',
  85. htmlentities(self::$latestCompatibleVersion),
  86. htmlentities($service)
  87. ), E_USER_ERROR);
  88. return null;
  89. }
  90. return new $checkerClass(
  91. new $apiClass($metadataUrl),
  92. $id,
  93. $slug,
  94. $checkPeriod,
  95. $optionName,
  96. $muPluginFile
  97. );
  98. }
  99. }
  100. /**
  101. * Check if the path points to a plugin file.
  102. *
  103. * @param string $absolutePath Normalized path.
  104. * @return bool
  105. */
  106. protected static function isPluginFile($absolutePath) {
  107. //Is the file inside the "plugins" or "mu-plugins" directory?
  108. $pluginDir = wp_normalize_path(WP_PLUGIN_DIR);
  109. $muPluginDir = wp_normalize_path(WPMU_PLUGIN_DIR);
  110. if ( (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0) ) {
  111. return true;
  112. }
  113. //Is it a file at all? Caution: is_file() can fail if the parent dir. doesn't have the +x permission set.
  114. if ( !is_file($absolutePath) ) {
  115. return false;
  116. }
  117. //Does it have a valid plugin header?
  118. //This is a last-ditch check for plugins symlinked from outside the WP root.
  119. if ( function_exists('get_file_data') ) {
  120. $headers = get_file_data($absolutePath, array('Name' => 'Plugin Name'), 'plugin');
  121. return !empty($headers['Name']);
  122. }
  123. return false;
  124. }
  125. /**
  126. * Get the name of the theme's directory from a full path to a file inside that directory.
  127. * E.g. "/abc/public_html/wp-content/themes/foo/whatever.php" => "foo".
  128. *
  129. * Note that subdirectories are currently not supported. For example,
  130. * "/xyz/wp-content/themes/my-theme/includes/whatever.php" => NULL.
  131. *
  132. * @param string $absolutePath Normalized path.
  133. * @return string|null Directory name, or NULL if the path doesn't point to a theme.
  134. */
  135. protected static function getThemeDirectoryName($absolutePath) {
  136. if ( is_file($absolutePath) ) {
  137. $absolutePath = dirname($absolutePath);
  138. }
  139. if ( file_exists($absolutePath . '/style.css') ) {
  140. return basename($absolutePath);
  141. }
  142. return null;
  143. }
  144. /**
  145. * Get the name of the hosting service that the URL points to.
  146. *
  147. * @param string $metadataUrl
  148. * @return string|null
  149. */
  150. private static function getVcsService($metadataUrl) {
  151. $service = null;
  152. //Which hosting service does the URL point to?
  153. $host = @parse_url($metadataUrl, PHP_URL_HOST);
  154. $path = @parse_url($metadataUrl, PHP_URL_PATH);
  155. //Check if the path looks like "/user-name/repository".
  156. $usernameRepoRegex = '@^/?([^/]+?)/([^/#?&]+?)/?$@';
  157. if ( preg_match($usernameRepoRegex, $path) ) {
  158. $knownServices = array(
  159. 'github.com' => 'GitHub',
  160. 'bitbucket.org' => 'BitBucket',
  161. 'gitlab.com' => 'GitLab',
  162. );
  163. if ( isset($knownServices[$host]) ) {
  164. $service = $knownServices[$host];
  165. }
  166. }
  167. return $service;
  168. }
  169. /**
  170. * Get the latest version of the specified class that has the same major version number
  171. * as this factory class.
  172. *
  173. * @param string $class Partial class name.
  174. * @return string|null Full class name.
  175. */
  176. protected static function getCompatibleClassVersion($class) {
  177. if ( isset(self::$classVersions[$class][self::$latestCompatibleVersion]) ) {
  178. return self::$classVersions[$class][self::$latestCompatibleVersion];
  179. }
  180. return null;
  181. }
  182. /**
  183. * Get the specific class name for the latest available version of a class.
  184. *
  185. * @param string $class
  186. * @return null|string
  187. */
  188. public static function getLatestClassVersion($class) {
  189. if ( !self::$sorted ) {
  190. self::sortVersions();
  191. }
  192. if ( isset(self::$classVersions[$class]) ) {
  193. return reset(self::$classVersions[$class]);
  194. } else {
  195. return null;
  196. }
  197. }
  198. /**
  199. * Sort available class versions in descending order (i.e. newest first).
  200. */
  201. protected static function sortVersions() {
  202. foreach ( self::$classVersions as $class => $versions ) {
  203. uksort($versions, array(__CLASS__, 'compareVersions'));
  204. self::$classVersions[$class] = $versions;
  205. }
  206. self::$sorted = true;
  207. }
  208. protected static function compareVersions($a, $b) {
  209. return -version_compare($a, $b);
  210. }
  211. /**
  212. * Register a version of a class.
  213. *
  214. * @access private This method is only for internal use by the library.
  215. *
  216. * @param string $generalClass Class name without version numbers, e.g. 'PluginUpdateChecker'.
  217. * @param string $versionedClass Actual class name, e.g. 'PluginUpdateChecker_1_2'.
  218. * @param string $version Version number, e.g. '1.2'.
  219. */
  220. public static function addVersion($generalClass, $versionedClass, $version) {
  221. if ( empty(self::$myMajorVersion) ) {
  222. $nameParts = explode('_', __CLASS__, 3);
  223. self::$myMajorVersion = substr(ltrim($nameParts[1], 'v'), 0, 1);
  224. }
  225. //Store the greatest version number that matches our major version.
  226. $components = explode('.', $version);
  227. if ( $components[0] === self::$myMajorVersion ) {
  228. if (
  229. empty(self::$latestCompatibleVersion)
  230. || version_compare($version, self::$latestCompatibleVersion, '>')
  231. ) {
  232. self::$latestCompatibleVersion = $version;
  233. }
  234. }
  235. if ( !isset(self::$classVersions[$generalClass]) ) {
  236. self::$classVersions[$generalClass] = array();
  237. }
  238. self::$classVersions[$generalClass][$version] = $versionedClass;
  239. self::$sorted = false;
  240. }
  241. }
  242. endif;