PageRenderTime 61ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/modules/civicrm/CRM/Extension/Browser.php

https://github.com/nysenate/Bluebird-CRM
PHP | 269 lines | 128 code | 38 blank | 103 comment | 24 complexity | c9caea126742df0f2fd442235dcfdbc2 MD5 | raw file
Possible License(s): JSON, BSD-3-Clause, MPL-2.0-no-copyleft-exception, AGPL-1.0, GPL-2.0, AGPL-3.0, Apache-2.0, MIT, GPL-3.0, CC-BY-4.0, LGPL-2.1, BSD-2-Clause, LGPL-3.0
  1. <?php
  2. /*
  3. +--------------------------------------------------------------------+
  4. | Copyright CiviCRM LLC. All rights reserved. |
  5. | |
  6. | This work is published under the GNU AGPLv3 license with some |
  7. | permitted exceptions and without any warranty. For full license |
  8. | and copyright information, see https://civicrm.org/licensing |
  9. +--------------------------------------------------------------------+
  10. */
  11. /**
  12. * This class glues together the various parts of the extension
  13. * system.
  14. *
  15. * @package CRM
  16. * @copyright CiviCRM LLC https://civicrm.org/licensing
  17. */
  18. class CRM_Extension_Browser {
  19. /**
  20. * An URL for public extensions repository.
  21. *
  22. * Note: This default is now handled through setting/*.php.
  23. *
  24. * @deprecated
  25. */
  26. const DEFAULT_EXTENSIONS_REPOSITORY = 'https://civicrm.org/extdir/ver={ver}|cms={uf}';
  27. /**
  28. * Relative path below remote repository URL for single extensions file.
  29. */
  30. const SINGLE_FILE_PATH = '/single';
  31. /**
  32. * The name of the single JSON extension cache file.
  33. */
  34. const CACHE_JSON_FILE = 'extensions.json';
  35. // timeout for when the connection or the server is slow
  36. const CHECK_TIMEOUT = 5;
  37. /**
  38. * @param string $repoUrl
  39. * URL of the remote repository.
  40. * @param string $indexPath
  41. * Relative path of the 'index' file within the repository.
  42. * @param string $cacheDir
  43. * Local path in which to cache files.
  44. */
  45. public function __construct($repoUrl, $indexPath, $cacheDir) {
  46. $this->repoUrl = $repoUrl;
  47. $this->cacheDir = $cacheDir;
  48. $this->indexPath = empty($indexPath) ? self::SINGLE_FILE_PATH : $indexPath;
  49. if ($cacheDir && !file_exists($cacheDir) && is_dir(dirname($cacheDir)) && is_writable(dirname($cacheDir))) {
  50. CRM_Utils_File::createDir($cacheDir, FALSE);
  51. }
  52. }
  53. /**
  54. * Determine whether the system policy allows downloading new extensions.
  55. *
  56. * This is reflection of *policy* and *intent*; it does not indicate whether
  57. * the browser will actually *work*. For that, see checkRequirements().
  58. *
  59. * @return bool
  60. */
  61. public function isEnabled() {
  62. return (FALSE !== $this->getRepositoryUrl());
  63. }
  64. /**
  65. * @return string
  66. */
  67. public function getRepositoryUrl() {
  68. return $this->repoUrl;
  69. }
  70. /**
  71. * Refresh the cache of remotely-available extensions.
  72. */
  73. public function refresh() {
  74. $file = $this->getTsPath();
  75. if (file_exists($file)) {
  76. unlink($file);
  77. }
  78. }
  79. /**
  80. * Determine whether downloading is supported.
  81. *
  82. * @return array
  83. * List of error messages; empty if OK.
  84. */
  85. public function checkRequirements() {
  86. if (!$this->isEnabled()) {
  87. return [];
  88. }
  89. $errors = [];
  90. if (!$this->cacheDir || !is_dir($this->cacheDir) || !is_writable($this->cacheDir)) {
  91. $civicrmDestination = urlencode(CRM_Utils_System::url('civicrm/admin/extensions', 'reset=1'));
  92. $url = CRM_Utils_System::url('civicrm/admin/setting/path', "reset=1&civicrmDestination=${civicrmDestination}");
  93. $errors[] = array(
  94. 'title' => ts('Directory Unwritable'),
  95. 'message' => ts('Your extensions cache directory (%1) is not web server writable. Please go to the <a href="%2">path setting page</a> and correct it.<br/>',
  96. array(
  97. 1 => $this->cacheDir,
  98. 2 => $url,
  99. )
  100. ),
  101. );
  102. }
  103. return $errors;
  104. }
  105. /**
  106. * Get a list of all available extensions.
  107. *
  108. * @return array
  109. * ($key => CRM_Extension_Info)
  110. */
  111. public function getExtensions() {
  112. if (!$this->isEnabled() || count($this->checkRequirements())) {
  113. return [];
  114. }
  115. $exts = [];
  116. $remote = $this->_discoverRemote();
  117. if (is_array($remote)) {
  118. foreach ($remote as $dc => $e) {
  119. $exts[$e->key] = $e;
  120. }
  121. }
  122. return $exts;
  123. }
  124. /**
  125. * Get a description of a particular extension.
  126. *
  127. * @param string $key
  128. * Fully-qualified extension name.
  129. *
  130. * @return CRM_Extension_Info|NULL
  131. */
  132. public function getExtension($key) {
  133. // TODO optimize performance -- we don't need to fetch/cache the entire repo
  134. $exts = $this->getExtensions();
  135. if (array_key_exists($key, $exts)) {
  136. return $exts[$key];
  137. }
  138. else {
  139. return NULL;
  140. }
  141. }
  142. /**
  143. * @return array
  144. * @throws CRM_Extension_Exception_ParseException
  145. */
  146. private function _discoverRemote() {
  147. $tsPath = $this->getTsPath();
  148. $timestamp = FALSE;
  149. if (file_exists($tsPath)) {
  150. $timestamp = file_get_contents($tsPath);
  151. }
  152. // 3 minutes ago for now
  153. $outdated = (int) $timestamp < (time() - 180) ? TRUE : FALSE;
  154. if (!$timestamp || $outdated) {
  155. $remotes = json_decode($this->grabRemoteJson(), TRUE);
  156. }
  157. else {
  158. $remotes = json_decode($this->grabCachedJson(), TRUE);
  159. }
  160. $this->_remotesDiscovered = [];
  161. foreach ((array) $remotes as $id => $xml) {
  162. $ext = CRM_Extension_Info::loadFromString($xml);
  163. $this->_remotesDiscovered[] = $ext;
  164. }
  165. if (file_exists(dirname($tsPath))) {
  166. file_put_contents($tsPath, (string) time());
  167. }
  168. return $this->_remotesDiscovered;
  169. }
  170. /**
  171. * Loads the extensions data from the cache file. If it is empty
  172. * or doesn't exist, try fetching from remote instead.
  173. *
  174. * @return string
  175. */
  176. private function grabCachedJson() {
  177. $filename = $this->cacheDir . DIRECTORY_SEPARATOR . self::CACHE_JSON_FILE . '.' . md5($this->getRepositoryUrl());
  178. $json = NULL;
  179. if (file_exists($filename)) {
  180. $json = file_get_contents($filename);
  181. }
  182. if (empty($json)) {
  183. $json = $this->grabRemoteJson();
  184. }
  185. return $json;
  186. }
  187. /**
  188. * Connects to public server and grabs the list of publicly available
  189. * extensions.
  190. *
  191. * @return string
  192. * @throws \CRM_Extension_Exception
  193. */
  194. private function grabRemoteJson() {
  195. ini_set('default_socket_timeout', self::CHECK_TIMEOUT);
  196. set_error_handler(array('CRM_Extension_Browser', 'downloadError'));
  197. if (!ini_get('allow_url_fopen')) {
  198. ini_set('allow_url_fopen', 1);
  199. }
  200. if (FALSE === $this->getRepositoryUrl()) {
  201. // don't check if the user has configured civi not to check an external
  202. // url for extensions. See CRM-10575.
  203. return [];
  204. }
  205. $filename = $this->cacheDir . DIRECTORY_SEPARATOR . self::CACHE_JSON_FILE . '.' . md5($this->getRepositoryUrl());
  206. $url = $this->getRepositoryUrl() . $this->indexPath;
  207. $status = CRM_Utils_HttpClient::singleton()->fetch($url, $filename);
  208. ini_restore('allow_url_fopen');
  209. ini_restore('default_socket_timeout');
  210. restore_error_handler();
  211. if ($status !== CRM_Utils_HttpClient::STATUS_OK) {
  212. throw new CRM_Extension_Exception(ts('The CiviCRM public extensions directory at %1 could not be contacted - please check your webserver can make external HTTP requests or contact CiviCRM team on <a href="http://forum.civicrm.org/">CiviCRM forum</a>.', array(1 => $this->getRepositoryUrl())), 'connection_error');
  213. }
  214. // Don't call grabCachedJson here, that would risk infinite recursion
  215. return file_get_contents($filename);
  216. }
  217. /**
  218. * @return string
  219. */
  220. private function getTsPath() {
  221. return $this->cacheDir . DIRECTORY_SEPARATOR . 'timestamp.txt';
  222. }
  223. /**
  224. * A dummy function required for suppressing download errors.
  225. *
  226. * @param $errorNumber
  227. * @param $errorString
  228. */
  229. public static function downloadError($errorNumber, $errorString) {
  230. }
  231. }