upgrademodx.class.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. <?php
  2. /**
  3. * UpgradeMODX class file for UpgradeMODX Widget snippet for extra
  4. *
  5. * Copyright 2015-2018 Bob Ray <https://bobsguides.com>
  6. * Created on 08-16-2015
  7. *
  8. * UpgradeMODX is free software; you can redistribute it and/or modify it under the
  9. * terms of the GNU General Public License as published by the Free Software
  10. * Foundation; either version 2 of the License, or (at your option) any later
  11. * version.
  12. *
  13. * UpgradeMODX is distributed in the hope that it will be useful, but WITHOUT ANY
  14. * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  15. * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License along with
  18. * UpgradeMODX; if not, write to the Free Software Foundation, Inc., 59 Temple
  19. * Place, Suite 330, Boston, MA 02111-1307 USA
  20. *
  21. * @package upgrademodx
  22. */
  23. /**
  24. * Description
  25. * -----------
  26. * UpgradeMODX Dashboard widget
  27. * This package was inspired by the work of a number of people and I have borrowed some of their code.
  28. * Dmytro Lukianenko (dmi3yy) is the original author of the MODX install script. Susan Sottwell, Sharapov,
  29. * Bumkaka, Inreti, Zaigham Rana, frischnetz, and AgelxNash, also contributed and I'd like to thank all
  30. * of them for laying the groundwork.
  31. *
  32. * Variables
  33. * ---------
  34. * @var $modx modX
  35. * @var $scriptProperties array
  36. *
  37. * @package upgrademodx
  38. **/
  39. /* Properties
  40. * @property &groups textfield -- group, or commma-separated list of groups, who will see the widget; Default: (empty)..
  41. * @property &hideWhenNoUpgrade combo-boolean -- Hide widget when no upgrade is available; Default: No.
  42. * @property &interval textfield -- Interval between checks -- Examples: 1 week, 3 days, 6 hours; Default: 1 week.
  43. * @property &language textfield -- Two-letter code of language to user; Default: en.
  44. * @property &lastCheck textfield -- Date and time of last check -- set automatically; Default: (empty)..
  45. * @property &latestVersion textfield -- Latest version (at last check) -- set automatically; Default: (empty)..
  46. * @property &plOnly combo-boolean -- Show only pl (stable) versions; Default: yes.
  47. * @property &versionsToShow textfield -- Number of versions to show in upgrade form (not widget); Default: 5.
  48. */
  49. if (!class_exists('UpgradeMODX')) {
  50. class UpgradeMODX {
  51. /** @var $versionArray string - array of versions to display if upgrade is available as a string
  52. * to inject into upgrade script */
  53. public $versionArray = '';
  54. /** @var $versionListPath string - location of versionlist file */
  55. public $versionListPath;
  56. /** @var $modx modX - modx object */
  57. public $modx = null;
  58. /** @var $latestVersion string - latest version available */
  59. public $latestVersion = '';
  60. /** @var $errors array - array of error message (non-fatal errors only) */
  61. public $errors = array();
  62. /** @var $forcePclZip boolean */
  63. public $forcePclZip = false;
  64. /** @var $forceFopen boolean */
  65. public $forceFopen = false;
  66. /** @var $githubTimeout int */
  67. public $gitHubTimeout = 6;
  68. /** @var $modxTimeout int */
  69. public $modxTimeout = 6;
  70. /** @var $attempts int */
  71. public $attempts = 2;
  72. /** @var $verifyPeer int */
  73. public $verifyPeer = true;
  74. /** @var $github_username string */
  75. public $github_username;
  76. /** @var $github_token string */
  77. public $github_token;
  78. public function __construct($modx) {
  79. /** @var $modx modX */
  80. $this->modx = $modx;
  81. }
  82. public function init($props) {
  83. /** @var $InstallData array */
  84. $language = $this->modx->getOption('language', $props, 'en', true);
  85. $this->modx->lexicon->load($language . ':upgrademodx:default');
  86. $this->forcePclZip = $this->modx->getOption('forcePclZip', $props, false);
  87. $this->forceFopen = $this->modx->getOption('forceFopen', $props, false);
  88. $this->plOnly = $this->modx->getOption('plOnly', $props);
  89. $this->gitHubTimeout = $this->modx->getOption('githubTimeout', $props, 6, true);
  90. $this->modxTimeout = $this->modx->getOption('modxTimeout', $props, 6, true);
  91. $this->attempts = $this->modx->getOption('attempts', $props, 2, true);
  92. $this->errors = array();
  93. $this->latestVersion = $this->modx->getOption('latestVersion', $props, '', true);
  94. $path = $this->modx->getOption('versionListPath', $props, MODX_CORE_PATH . 'cache/upgrademodx/', true);
  95. $path = str_replace('{core_path}', MODX_CORE_PATH, $path);
  96. $this->versionListPath = str_replace('{assets_path}', MODX_ASSETS_PATH, $path);
  97. $this->verifyPeer = $this->modx->getOption('ssl_verify_peer', $props, true);
  98. /* Next two use System Setting if property is empty */
  99. $this->github_username = $this->modx->getOption('github_username',
  100. $props, $this->modx->getOption('github_username', null), true);
  101. $this->github_token = $this->modx->getOption('github_token', $props,
  102. $this->modx->getOption('github_token', null), true);
  103. }
  104. public function writeScriptFile() {
  105. /** @var $InstallData array */
  106. $fp = @fopen(MODX_BASE_PATH . 'upgrade.php', 'w');
  107. if ($fp) {
  108. @include $this->versionListPath . 'versionlist';
  109. $versionArray = $InstallData;
  110. if (! is_array($versionArray) || empty($versionArray)) {
  111. $this->setError($this->modx->lexicon('ugm_no_version_list') . '@ ' . $this->versionListPath);
  112. } else {
  113. $versionList = '$InstallData = ' . var_export($versionArray, true) . ';';
  114. $forcePclZipString = '$forcePclZip = ';
  115. $forcePclZipString .= $this->forcePclZip ? 'true' : 'false';
  116. $forcePclZipString .= ';';
  117. $forceFopenString = '$forceFopen = ';
  118. $forceFopenString .= $this->forceFopen ? 'true' : 'false';
  119. $forceFopenString .= ';';
  120. $fields = array(
  121. '/* [[+ForcePclZip]] */' => $forcePclZipString,
  122. '/* [[+ForceFopen]] */' => $forceFopenString,
  123. '/* [[+InstallData]] */' => $versionList,
  124. );
  125. $fileContent = $this->modx->getChunk('UpgradeMODXSnippetScriptSource');
  126. $fileContent = str_replace(array_keys($fields), array_values($fields), $fileContent);
  127. fwrite($fp, $fileContent);
  128. fclose($fp);
  129. }
  130. } else {
  131. $this->setError($this->modx->lexicon('ugm_could_not_open') . ' ' . MODX_BASE_PATH . 'upgrade.php' .
  132. ' ' .
  133. $this->modx->lexicon('ugm_for_writing'));
  134. }
  135. }
  136. public function getJSONFromGitHub($method, $timeout = 6, $tries = 2) {
  137. $this->clearErrors();
  138. $data = '';
  139. $url = 'https://api.github.com/repos/modxcms/revolution/tags';
  140. // ini_set('user_agent', 'Mozilla/4.0 (compatible; MSIE 6.0)');
  141. if ($method == 'curl') {
  142. $data = $this->curlGetData($url, true, $timeout, $tries);
  143. } else {
  144. $data = $this->fopenGetData($url, true, $timeout, $tries);
  145. }
  146. $pos = strpos($data, 'API rate limit exceeded for');
  147. if ($pos !== false) {
  148. $this->setError('(GitHub -- ' . $method . ') ' . substr($data, $pos, 38));
  149. $data = false;
  150. }
  151. return $data === false? false : strip_tags($data);
  152. }
  153. public function finalizeVersionArray($contents, $plOnly = true, $versionsToShow = 5) {
  154. $contents = utf8_encode($contents);
  155. $contents = $this->modx->fromJSON($contents);
  156. if (empty($contents)) {
  157. $this->setError($this->modx->lexicon('ugm_json_decode_failed'));
  158. return false;
  159. }
  160. /* remove non-pl version objects if plOnly is set, and remove MODX 2.5.3 */
  161. foreach ($contents as $key => $content) {
  162. $name = substr($content['name'], 1);
  163. if ($plOnly && strpos($name, 'pl') === false) {
  164. unset($contents[$key]);
  165. continue;
  166. }
  167. if (strpos($name, '2.5.3-pl') !== false) {
  168. unset($contents[$key]);
  169. }
  170. }
  171. $contents = array_values($contents); // 'reindex' array
  172. /* GitHub won't necessarily have them in the correct order.
  173. Sort them with a Custom insertion sort since they will
  174. be almost sorted already */
  175. /* Make sure we don't access an invalid index */
  176. $versionsToShow = min($versionsToShow, count($contents));
  177. /* Make sure we show at least one */
  178. $versionsToShow = !empty($versionsToShow) ? $versionsToShow : 1;
  179. /* Sort by version */
  180. for ($i = 1; $i < $versionsToShow; $i++) {
  181. $element = $contents[$i];
  182. $j = $i;
  183. while ($j > 0 && (version_compare($contents[$j - 1]['name'], $element['name']) < 0)) {
  184. $contents[$j] = $contents[$j - 1];
  185. $j = $j - 1;
  186. }
  187. $contents[$j] = $element;
  188. }
  189. /* Truncate to $versionsToShow */
  190. $contents = array_splice($contents, 0, $versionsToShow);
  191. $versionArray = array();
  192. $i = 1;
  193. foreach ($contents as $version) {
  194. $name = substr($version['name'], 1);
  195. $url = 'https://modx.com/download/direct?id=modx-' . $name . '.zip';
  196. $versionArray[$name] = array(
  197. 'tree' => 'Revolution',
  198. 'name' => 'MODX Revolution ' . htmlentities($name),
  199. 'link' => $url,
  200. 'location' => 'setup/index.php',
  201. );
  202. $i++;
  203. if ($i > $versionsToShow) {
  204. break;
  205. }
  206. }
  207. $this->versionArray = $versionArray;
  208. return $this->versionArray;
  209. }
  210. public function updateLatestVersion($versionArray) {
  211. $latest = reset($versionArray);
  212. $this->latestVersion = substr($latest['name'], 16);
  213. }
  214. public function updateSnippetProperties($lastCheck, $latestVersion ) {
  215. $snippet = $this->modx->getObject('modSnippet', array('name' => 'UpgradeMODXWidget'));
  216. if ($snippet) {
  217. $properties = $snippet->get('properties');
  218. $properties['lastCheck']['value'] = strftime('%Y-%m-%d %H:%M:%S', $lastCheck);
  219. $properties['latestVersion']['value'] = $latestVersion;
  220. $snippet->setProperties($properties);
  221. $snippet->save();
  222. }
  223. }
  224. public function updateVersionListFile() {
  225. $path = $this->versionListPath;
  226. $this->mmkDir($path);
  227. $versionList = var_export($this->versionArray, true);
  228. $fp = @fopen($this->versionListPath . 'versionlist', 'w');
  229. if ($fp) {
  230. fwrite($fp, '<' . '?p' . "hp\n" . '$InstallData = ' . $versionList . ';');
  231. fclose($fp);
  232. } else {
  233. $this->setError($this->modx->lexicon('ugm_could_not_open') .
  234. ' ' . $path . 'versionlist ' . ' ' .
  235. $this->modx->lexicon('ugm_for_writing'));
  236. }
  237. }
  238. public function getVersionListPath() {
  239. return $this->versionListPath;
  240. }
  241. public function curlGetData($url, $returnData = false, $timeout = 6, $tries = 6 ) {
  242. $username = $this->github_username;
  243. $token = $this->github_token;
  244. $retVal = false;
  245. $errorMsg = '(' . $url . ' - curl) ' . $this->modx->lexicon('failed');
  246. $ch = curl_init();
  247. if ($this->verifyPeer) {
  248. $certPath = MODX_CORE_PATH . 'components/upgrademodx/cacert.pem';
  249. curl_setopt($ch, CURLOPT_CAINFO, $certPath);
  250. }
  251. curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
  252. curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 6.0)");
  253. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
  254. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verifyPeer);
  255. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
  256. curl_setopt($ch, CURLOPT_URL, $url);
  257. curl_setopt($ch, CURLOPT_RETURNTRANSFER, $returnData);
  258. curl_setopt($ch, CURLOPT_HEADER, false);
  259. curl_setopt($ch, CURLOPT_NOBODY, !$returnData);
  260. if (strpos($url, 'github') !== false) {
  261. if (!empty($username) && !empty($token)) {
  262. curl_setopt($ch, CURLOPT_USERPWD, $username . ':' . $token);
  263. }
  264. }
  265. $i = $tries;
  266. while ($i--) {
  267. $retVal = @curl_exec($ch);
  268. if (!empty($retVal)) {
  269. break;
  270. }
  271. }
  272. if (empty($retVal) || ($retVal === false)) {
  273. $e = curl_error($ch);
  274. if (!empty($e)) {
  275. $errorMsg = $e;
  276. }
  277. $this->setError($errorMsg);
  278. } elseif (! $returnData) { /* Just checking for existence */
  279. $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  280. $retVal = $statusCode == 200 || $statusCode == 301 || $statusCode == 302;
  281. }
  282. curl_close($ch);
  283. return $retVal;
  284. }
  285. public function fopenGetData($url, $returnData = false, $timeout = 6, $tries = 6) {
  286. $username = $this->modx->getOption('github_username');
  287. $token = $this->modx->getOption('github_token');
  288. $errorMsg = '(' . $url . ' - fopen) ' . $this->modx->lexicon('failed');
  289. $retVal = false;
  290. $opts = array(
  291. 'http' => array(
  292. 'method' => 'GET',
  293. 'timeout' => $timeout,
  294. 'max_redirects' => 1,
  295. 'ignore_errors' => true,
  296. 'user_agent' => 'Mozilla/4.0 (compatible; MSIE 6.0)',
  297. )
  298. );
  299. if (!empty($username) && !empty($token)) {
  300. $opts['http']['header'] = "Authorization: Basic " . base64_encode($username . ':' . $token);
  301. }
  302. $ctx = stream_context_create($opts);
  303. $i = $tries;
  304. $old = @ini_set('default_socket_timeout', $timeout);
  305. while ($i--) {
  306. if (!$returnData) {
  307. $retVal = @fopen($url, 'r');
  308. // $x = $http_response_header;
  309. if ($retVal) {
  310. @fclose($retVal);
  311. $retVal = true;
  312. break;
  313. } else {
  314. $timeout += 2;
  315. ini_set('default_socket_timeout', $timeout);
  316. }
  317. } else {
  318. $retVal = @file_get_contents($url, false, $ctx);
  319. // $x = $http_response_header;
  320. }
  321. }
  322. @ini_set('default_socket_timeout', $old);
  323. if (!$retVal) {
  324. $this->setError($errorMsg);
  325. }
  326. return $retVal;
  327. }
  328. public function downloadable($version, $method = 'curl', $timeout = 6, $tries = 2) {
  329. $this->clearErrors();
  330. $downloadUrl = 'https://modx.com/download/direct/modx-' . $version . '.zip';
  331. if ($method == 'curl') {
  332. $downloadable = $this->curlGetData($downloadUrl, false, $timeout, $tries);
  333. } else {
  334. $downloadable = $this->fopenGetData($downloadUrl, false, $timeout, $tries);
  335. }
  336. return $downloadable;
  337. }
  338. /**
  339. * @param $lastCheck string = time of previous check
  340. * @param $interval - interval between checks
  341. * @return bool true if time to check, false if not
  342. */
  343. public function timeToCheck($lastCheck, $interval = '+1 week') {
  344. if (empty($lastCheck)) {
  345. $retVal = true;
  346. } else {
  347. $interval = strpos($interval, '+') === false ? '+' . $interval : $interval;
  348. $retVal = time() > strtotime($lastCheck . ' ' . $interval);
  349. }
  350. return $retVal;
  351. }
  352. public function clearErrors() {
  353. $this->errors = array();
  354. }
  355. public function getLatestVersion() {
  356. return $this->latestVersion;
  357. }
  358. public function setError($msg) {
  359. $this->errors[] = $msg;
  360. }
  361. public function getErrors() {
  362. return $this->errors;
  363. }
  364. public function upgradeAvailable($currentVersion, $plOnly = false, $versionsToShow = 5, $method = 'curl') {
  365. $retVal = $this->getJSONFromGitHub($method, $this->gitHubTimeout, $this->attempts);
  366. if ($retVal !== false) {
  367. $retVal = $this->finalizeVersionArray($retVal, $plOnly, $versionsToShow);
  368. if ($retVal !== false) {
  369. $this->updateLatestVersion($retVal);
  370. $this->updateSnippetProperties(time(), $this->latestVersion);
  371. $this->updateVersionListFile();
  372. }
  373. }
  374. if ($retVal === false) {
  375. $this->setError($this->modx->lexicon('ugm_no_version_list_from_github'));
  376. }
  377. $latestVersion = $this->latestVersion;
  378. if (!empty($this->errors)) {
  379. $upgradeAvailable = false;
  380. } else {
  381. /* See if the latest version is newer than the current version */
  382. $newVersion = version_compare($currentVersion, $latestVersion) < 0;
  383. $downloadable = $this->downloadable($latestVersion, $method, $this->modxTimeout, $this->attempts);
  384. $upgradeAvailable = $newVersion && $downloadable;
  385. }
  386. return $upgradeAvailable;
  387. }
  388. public function mmkDir($folder, $perm = 0755) {
  389. if (!is_dir($folder)) {
  390. mkdir($folder, $perm, true);
  391. }
  392. }
  393. }
  394. }