Factory.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. <?php
  2. if ( !class_exists('Puc_v4p11_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_v4p11_Factory {
  14. protected static $classVersions = array();
  15. protected static $sorted = false;
  16. protected static $myMajorVersion = '';
  17. protected static $latestCompatibleVersion = '';
  18. /**
  19. * A wrapper method for buildUpdateChecker() that reads the metadata URL from the plugin or theme header.
  20. *
  21. * @param string $fullPath Full path to the main plugin file or the theme's style.css.
  22. * @param array $args Optional arguments. Keys should match the argument names of the buildUpdateChecker() method.
  23. * @return Puc_v4p11_Plugin_UpdateChecker|Puc_v4p11_Theme_UpdateChecker|Puc_v4p11_Vcs_BaseChecker
  24. */
  25. public static function buildFromHeader($fullPath, $args = array()) {
  26. $fullPath = self::normalizePath($fullPath);
  27. //Set up defaults.
  28. $defaults = array(
  29. 'metadataUrl' => '',
  30. 'slug' => '',
  31. 'checkPeriod' => 12,
  32. 'optionName' => '',
  33. 'muPluginFile' => '',
  34. );
  35. $args = array_merge($defaults, array_intersect_key($args, $defaults));
  36. extract($args, EXTR_SKIP);
  37. //Check for the service URI
  38. if ( empty($metadataUrl) ) {
  39. $metadataUrl = self::getServiceURI($fullPath);
  40. }
  41. /** @noinspection PhpUndefinedVariableInspection These variables are created by extract(), above. */
  42. return self::buildUpdateChecker($metadataUrl, $fullPath, $slug, $checkPeriod, $optionName, $muPluginFile);
  43. }
  44. /**
  45. * Create a new instance of the update checker.
  46. *
  47. * This method automatically detects if you're using it for a plugin or a theme and chooses
  48. * the appropriate implementation for your update source (JSON file, GitHub, BitBucket, etc).
  49. *
  50. * @see Puc_v4p11_UpdateChecker::__construct
  51. *
  52. * @param string $metadataUrl The URL of the metadata file, a GitHub repository, or another supported update source.
  53. * @param string $fullPath Full path to the main plugin file or to the theme directory.
  54. * @param string $slug Custom slug. Defaults to the name of the main plugin file or the theme directory.
  55. * @param int $checkPeriod How often to check for updates (in hours).
  56. * @param string $optionName Where to store book-keeping info about update checks.
  57. * @param string $muPluginFile The plugin filename relative to the mu-plugins directory.
  58. * @return Puc_v4p11_Plugin_UpdateChecker|Puc_v4p11_Theme_UpdateChecker|Puc_v4p11_Vcs_BaseChecker
  59. */
  60. public static function buildUpdateChecker($metadataUrl, $fullPath, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {
  61. $fullPath = self::normalizePath($fullPath);
  62. $id = null;
  63. //Plugin or theme?
  64. $themeDirectory = self::getThemeDirectoryName($fullPath);
  65. if ( self::isPluginFile($fullPath) ) {
  66. $type = 'Plugin';
  67. $id = $fullPath;
  68. } else if ( $themeDirectory !== null ) {
  69. $type = 'Theme';
  70. $id = $themeDirectory;
  71. } else {
  72. throw new RuntimeException(sprintf(
  73. 'The update checker cannot determine if "%s" is a plugin or a theme. ' .
  74. 'This is a bug. Please contact the PUC developer.',
  75. htmlentities($fullPath)
  76. ));
  77. }
  78. //Which hosting service does the URL point to?
  79. $service = self::getVcsService($metadataUrl);
  80. $apiClass = null;
  81. if ( empty($service) ) {
  82. //The default is to get update information from a remote JSON file.
  83. $checkerClass = $type . '_UpdateChecker';
  84. } else {
  85. //You can also use a VCS repository like GitHub.
  86. $checkerClass = 'Vcs_' . $type . 'UpdateChecker';
  87. $apiClass = $service . 'Api';
  88. }
  89. $checkerClass = self::getCompatibleClassVersion($checkerClass);
  90. if ( $checkerClass === null ) {
  91. trigger_error(
  92. sprintf(
  93. 'PUC %s does not support updates for %ss %s',
  94. htmlentities(self::$latestCompatibleVersion),
  95. strtolower($type),
  96. $service ? ('hosted on ' . htmlentities($service)) : 'using JSON metadata'
  97. ),
  98. E_USER_ERROR
  99. );
  100. return null;
  101. }
  102. //Add the current namespace to the class name(s).
  103. if ( version_compare(PHP_VERSION, '5.3', '>=') ) {
  104. $checkerClass = __NAMESPACE__ . '\\' . $checkerClass;
  105. }
  106. if ( !isset($apiClass) ) {
  107. //Plain old update checker.
  108. return new $checkerClass($metadataUrl, $id, $slug, $checkPeriod, $optionName, $muPluginFile);
  109. } else {
  110. //VCS checker + an API client.
  111. $apiClass = self::getCompatibleClassVersion($apiClass);
  112. if ( $apiClass === null ) {
  113. trigger_error(sprintf(
  114. 'PUC %s does not support %s',
  115. htmlentities(self::$latestCompatibleVersion),
  116. htmlentities($service)
  117. ), E_USER_ERROR);
  118. return null;
  119. }
  120. if ( version_compare(PHP_VERSION, '5.3', '>=') && (strpos($apiClass, '\\') === false) ) {
  121. $apiClass = __NAMESPACE__ . '\\' . $apiClass;
  122. }
  123. return new $checkerClass(
  124. new $apiClass($metadataUrl),
  125. $id,
  126. $slug,
  127. $checkPeriod,
  128. $optionName,
  129. $muPluginFile
  130. );
  131. }
  132. }
  133. /**
  134. *
  135. * Normalize a filesystem path. Introduced in WP 3.9.
  136. * Copying here allows use of the class on earlier versions.
  137. * This version adapted from WP 4.8.2 (unchanged since 4.5.0)
  138. *
  139. * @param string $path Path to normalize.
  140. * @return string Normalized path.
  141. */
  142. public static function normalizePath($path) {
  143. if ( function_exists('wp_normalize_path') ) {
  144. return wp_normalize_path($path);
  145. }
  146. $path = str_replace('\\', '/', $path);
  147. $path = preg_replace('|(?<=.)/+|', '/', $path);
  148. if ( substr($path, 1, 1) === ':' ) {
  149. $path = ucfirst($path);
  150. }
  151. return $path;
  152. }
  153. /**
  154. * Check if the path points to a plugin file.
  155. *
  156. * @param string $absolutePath Normalized path.
  157. * @return bool
  158. */
  159. protected static function isPluginFile($absolutePath) {
  160. //Is the file inside the "plugins" or "mu-plugins" directory?
  161. $pluginDir = self::normalizePath(WP_PLUGIN_DIR);
  162. $muPluginDir = self::normalizePath(WPMU_PLUGIN_DIR);
  163. if ( (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0) ) {
  164. return true;
  165. }
  166. //Is it a file at all? Caution: is_file() can fail if the parent dir. doesn't have the +x permission set.
  167. if ( !is_file($absolutePath) ) {
  168. return false;
  169. }
  170. //Does it have a valid plugin header?
  171. //This is a last-ditch check for plugins symlinked from outside the WP root.
  172. if ( function_exists('get_file_data') ) {
  173. $headers = get_file_data($absolutePath, array('Name' => 'Plugin Name'), 'plugin');
  174. return !empty($headers['Name']);
  175. }
  176. return false;
  177. }
  178. /**
  179. * Get the name of the theme's directory from a full path to a file inside that directory.
  180. * E.g. "/abc/public_html/wp-content/themes/foo/whatever.php" => "foo".
  181. *
  182. * Note that subdirectories are currently not supported. For example,
  183. * "/xyz/wp-content/themes/my-theme/includes/whatever.php" => NULL.
  184. *
  185. * @param string $absolutePath Normalized path.
  186. * @return string|null Directory name, or NULL if the path doesn't point to a theme.
  187. */
  188. protected static function getThemeDirectoryName($absolutePath) {
  189. if ( is_file($absolutePath) ) {
  190. $absolutePath = dirname($absolutePath);
  191. }
  192. if ( file_exists($absolutePath . '/style.css') ) {
  193. return basename($absolutePath);
  194. }
  195. return null;
  196. }
  197. /**
  198. * Get the service URI from the file header.
  199. *
  200. * @param string $fullPath
  201. * @return string
  202. */
  203. private static function getServiceURI($fullPath) {
  204. //Look for the URI
  205. if ( is_readable($fullPath) ) {
  206. $seek = array(
  207. 'github' => 'GitHub URI',
  208. 'gitlab' => 'GitLab URI',
  209. 'bucket' => 'BitBucket URI',
  210. );
  211. $seek = apply_filters('puc_get_source_uri', $seek);
  212. $data = get_file_data($fullPath, $seek);
  213. foreach ($data as $key => $uri) {
  214. if ( $uri ) {
  215. return $uri;
  216. }
  217. }
  218. }
  219. //URI was not found so throw an error.
  220. throw new RuntimeException(
  221. sprintf('Unable to locate URI in header of "%s"', htmlentities($fullPath))
  222. );
  223. }
  224. /**
  225. * Get the name of the hosting service that the URL points to.
  226. *
  227. * @param string $metadataUrl
  228. * @return string|null
  229. */
  230. private static function getVcsService($metadataUrl) {
  231. $service = null;
  232. //Which hosting service does the URL point to?
  233. $host = parse_url($metadataUrl, PHP_URL_HOST);
  234. $path = parse_url($metadataUrl, PHP_URL_PATH);
  235. //Check if the path looks like "/user-name/repository".
  236. //For GitLab.com it can also be "/user/group1/group2/.../repository".
  237. $repoRegex = '@^/?([^/]+?)/([^/#?&]+?)/?$@';
  238. if ( $host === 'gitlab.com' ) {
  239. $repoRegex = '@^/?(?:[^/#?&]++/){1,20}(?:[^/#?&]++)/?$@';
  240. }
  241. if ( preg_match($repoRegex, $path) ) {
  242. $knownServices = array(
  243. 'github.com' => 'GitHub',
  244. 'bitbucket.org' => 'BitBucket',
  245. 'gitlab.com' => 'GitLab',
  246. );
  247. if ( isset($knownServices[$host]) ) {
  248. $service = $knownServices[$host];
  249. }
  250. }
  251. return apply_filters('puc_get_vcs_service', $service, $host, $path, $metadataUrl);
  252. }
  253. /**
  254. * Get the latest version of the specified class that has the same major version number
  255. * as this factory class.
  256. *
  257. * @param string $class Partial class name.
  258. * @return string|null Full class name.
  259. */
  260. protected static function getCompatibleClassVersion($class) {
  261. if ( isset(self::$classVersions[$class][self::$latestCompatibleVersion]) ) {
  262. return self::$classVersions[$class][self::$latestCompatibleVersion];
  263. }
  264. return null;
  265. }
  266. /**
  267. * Get the specific class name for the latest available version of a class.
  268. *
  269. * @param string $class
  270. * @return null|string
  271. */
  272. public static function getLatestClassVersion($class) {
  273. if ( !self::$sorted ) {
  274. self::sortVersions();
  275. }
  276. if ( isset(self::$classVersions[$class]) ) {
  277. return reset(self::$classVersions[$class]);
  278. } else {
  279. return null;
  280. }
  281. }
  282. /**
  283. * Sort available class versions in descending order (i.e. newest first).
  284. */
  285. protected static function sortVersions() {
  286. foreach ( self::$classVersions as $class => $versions ) {
  287. uksort($versions, array(__CLASS__, 'compareVersions'));
  288. self::$classVersions[$class] = $versions;
  289. }
  290. self::$sorted = true;
  291. }
  292. protected static function compareVersions($a, $b) {
  293. return -version_compare($a, $b);
  294. }
  295. /**
  296. * Register a version of a class.
  297. *
  298. * @access private This method is only for internal use by the library.
  299. *
  300. * @param string $generalClass Class name without version numbers, e.g. 'PluginUpdateChecker'.
  301. * @param string $versionedClass Actual class name, e.g. 'PluginUpdateChecker_1_2'.
  302. * @param string $version Version number, e.g. '1.2'.
  303. */
  304. public static function addVersion($generalClass, $versionedClass, $version) {
  305. if ( empty(self::$myMajorVersion) ) {
  306. $nameParts = explode('_', __CLASS__, 3);
  307. self::$myMajorVersion = substr(ltrim($nameParts[1], 'v'), 0, 1);
  308. }
  309. //Store the greatest version number that matches our major version.
  310. $components = explode('.', $version);
  311. if ( $components[0] === self::$myMajorVersion ) {
  312. if (
  313. empty(self::$latestCompatibleVersion)
  314. || version_compare($version, self::$latestCompatibleVersion, '>')
  315. ) {
  316. self::$latestCompatibleVersion = $version;
  317. }
  318. }
  319. if ( !isset(self::$classVersions[$generalClass]) ) {
  320. self::$classVersions[$generalClass] = array();
  321. }
  322. self::$classVersions[$generalClass][$version] = $versionedClass;
  323. self::$sorted = false;
  324. }
  325. }
  326. endif;