* Created on 08-16-2015 * * UpgradeMODX is free software; you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * UpgradeMODX is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * UpgradeMODX; if not, write to the Free Software Foundation, Inc., 59 Temple * Place, Suite 330, Boston, MA 02111-1307 USA * * @package upgrademodx */ /** * Description * ----------- * UpgradeMODX Dashboard widget * This package was inspired by the work of a number of people and I have borrowed some of their code. * Dmytro Lukianenko (dmi3yy) is the original author of the MODX install script. Susan Sottwell, Sharapov, * Bumkaka, Inreti, Zaigham Rana, frischnetz, and AgelxNash, also contributed and I'd like to thank all * of them for laying the groundwork. * * Variables * --------- * @var $modx modX * @var $scriptProperties array * * @package upgrademodx **/ /* Properties * @property &groups textfield -- group, or commma-separated list of groups, who will see the widget; Default: (empty).. * @property &hideWhenNoUpgrade combo-boolean -- Hide widget when no upgrade is available; Default: No. * @property &interval textfield -- Interval between checks -- Examples: 1 week, 3 days, 6 hours; Default: 1 week. * @property &language textfield -- Two-letter code of language to user; Default: en. * @property &lastCheck textfield -- Date and time of last check -- set automatically; Default: (empty).. * @property &latestVersion textfield -- Latest version (at last check) -- set automatically; Default: (empty).. * @property &plOnly combo-boolean -- Show only pl (stable) versions; Default: yes. * @property &versionsToShow textfield -- Number of versions to show in upgrade form (not widget); Default: 5. */ if (!class_exists('UpgradeMODX')) { class UpgradeMODX { /** @var $versionArray string - array of versions to display if upgrade is available as a string * to inject into upgrade script */ public $versionArray = ''; /** @var $versionListPath string - location of versionlist file */ public $versionListPath; /** @var $modx modX - modx object */ public $modx = null; /** @var $latestVersion string - latest version available */ public $latestVersion = ''; /** @var $errors array - array of error message (non-fatal errors only) */ public $errors = array(); /** @var $forcePclZip boolean */ public $forcePclZip = false; /** @var $forceFopen boolean */ public $forceFopen = false; /** @var $githubTimeout int */ public $gitHubTimeout = 6; /** @var $modxTimeout int */ public $modxTimeout = 6; /** @var $attempts int */ public $attempts = 2; /** @var $verifyPeer int */ public $verifyPeer = true; /** @var $github_username string */ public $github_username; /** @var $github_token string */ public $github_token; public function __construct($modx) { /** @var $modx modX */ $this->modx = $modx; } public function init($props) { /** @var $InstallData array */ $language = $this->modx->getOption('language', $props, 'en', true); $this->modx->lexicon->load($language . ':upgrademodx:default'); $this->forcePclZip = $this->modx->getOption('forcePclZip', $props, false); $this->forceFopen = $this->modx->getOption('forceFopen', $props, false); $this->plOnly = $this->modx->getOption('plOnly', $props); $this->gitHubTimeout = $this->modx->getOption('githubTimeout', $props, 6, true); $this->modxTimeout = $this->modx->getOption('modxTimeout', $props, 6, true); $this->attempts = $this->modx->getOption('attempts', $props, 2, true); $this->errors = array(); $this->latestVersion = $this->modx->getOption('latestVersion', $props, '', true); $path = $this->modx->getOption('versionListPath', $props, MODX_CORE_PATH . 'cache/upgrademodx/', true); $path = str_replace('{core_path}', MODX_CORE_PATH, $path); $this->versionListPath = str_replace('{assets_path}', MODX_ASSETS_PATH, $path); $this->verifyPeer = $this->modx->getOption('ssl_verify_peer', $props, true); /* Next two use System Setting if property is empty */ $this->github_username = $this->modx->getOption('github_username', $props, $this->modx->getOption('github_username', null), true); $this->github_token = $this->modx->getOption('github_token', $props, $this->modx->getOption('github_token', null), true); } public function writeScriptFile() { /** @var $InstallData array */ $fp = @fopen(MODX_BASE_PATH . 'upgrade.php', 'w'); if ($fp) { @include $this->versionListPath . 'versionlist'; $versionArray = $InstallData; if (! is_array($versionArray) || empty($versionArray)) { $this->setError($this->modx->lexicon('ugm_no_version_list') . '@ ' . $this->versionListPath); } else { $versionList = '$InstallData = ' . var_export($versionArray, true) . ';'; $forcePclZipString = '$forcePclZip = '; $forcePclZipString .= $this->forcePclZip ? 'true' : 'false'; $forcePclZipString .= ';'; $forceFopenString = '$forceFopen = '; $forceFopenString .= $this->forceFopen ? 'true' : 'false'; $forceFopenString .= ';'; $fields = array( '/* [[+ForcePclZip]] */' => $forcePclZipString, '/* [[+ForceFopen]] */' => $forceFopenString, '/* [[+InstallData]] */' => $versionList, ); $fileContent = $this->modx->getChunk('UpgradeMODXSnippetScriptSource'); $fileContent = str_replace(array_keys($fields), array_values($fields), $fileContent); fwrite($fp, $fileContent); fclose($fp); } } else { $this->setError($this->modx->lexicon('ugm_could_not_open') . ' ' . MODX_BASE_PATH . 'upgrade.php' . ' ' . $this->modx->lexicon('ugm_for_writing')); } } public function getJSONFromGitHub($method, $timeout = 6, $tries = 2) { $this->clearErrors(); $data = ''; $url = 'https://api.github.com/repos/modxcms/revolution/tags'; // ini_set('user_agent', 'Mozilla/4.0 (compatible; MSIE 6.0)'); if ($method == 'curl') { $data = $this->curlGetData($url, true, $timeout, $tries); } else { $data = $this->fopenGetData($url, true, $timeout, $tries); } $pos = strpos($data, 'API rate limit exceeded for'); if ($pos !== false) { $this->setError('(GitHub -- ' . $method . ') ' . substr($data, $pos, 38)); $data = false; } return $data === false? false : strip_tags($data); } public function finalizeVersionArray($contents, $plOnly = true, $versionsToShow = 5) { $contents = utf8_encode($contents); $contents = $this->modx->fromJSON($contents); if (empty($contents)) { $this->setError($this->modx->lexicon('ugm_json_decode_failed')); return false; } /* remove non-pl version objects if plOnly is set, and remove MODX 2.5.3 */ foreach ($contents as $key => $content) { $name = substr($content['name'], 1); if ($plOnly && strpos($name, 'pl') === false) { unset($contents[$key]); continue; } if (strpos($name, '2.5.3-pl') !== false) { unset($contents[$key]); } } $contents = array_values($contents); // 'reindex' array /* GitHub won't necessarily have them in the correct order. Sort them with a Custom insertion sort since they will be almost sorted already */ /* Make sure we don't access an invalid index */ $versionsToShow = min($versionsToShow, count($contents)); /* Make sure we show at least one */ $versionsToShow = !empty($versionsToShow) ? $versionsToShow : 1; /* Sort by version */ for ($i = 1; $i < $versionsToShow; $i++) { $element = $contents[$i]; $j = $i; while ($j > 0 && (version_compare($contents[$j - 1]['name'], $element['name']) < 0)) { $contents[$j] = $contents[$j - 1]; $j = $j - 1; } $contents[$j] = $element; } /* Truncate to $versionsToShow */ $contents = array_splice($contents, 0, $versionsToShow); $versionArray = array(); $i = 1; foreach ($contents as $version) { $name = substr($version['name'], 1); $url = 'https://modx.com/download/direct?id=modx-' . $name . '.zip'; $versionArray[$name] = array( 'tree' => 'Revolution', 'name' => 'MODX Revolution ' . htmlentities($name), 'link' => $url, 'location' => 'setup/index.php', ); $i++; if ($i > $versionsToShow) { break; } } $this->versionArray = $versionArray; return $this->versionArray; } public function updateLatestVersion($versionArray) { $latest = reset($versionArray); $this->latestVersion = substr($latest['name'], 16); } public function updateSnippetProperties($lastCheck, $latestVersion ) { $snippet = $this->modx->getObject('modSnippet', array('name' => 'UpgradeMODXWidget')); if ($snippet) { $properties = $snippet->get('properties'); $properties['lastCheck']['value'] = strftime('%Y-%m-%d %H:%M:%S', $lastCheck); $properties['latestVersion']['value'] = $latestVersion; $snippet->setProperties($properties); $snippet->save(); } } public function updateVersionListFile() { $path = $this->versionListPath; $this->mmkDir($path); $versionList = var_export($this->versionArray, true); $fp = @fopen($this->versionListPath . 'versionlist', 'w'); if ($fp) { fwrite($fp, '<' . '?p' . "hp\n" . '$InstallData = ' . $versionList . ';'); fclose($fp); } else { $this->setError($this->modx->lexicon('ugm_could_not_open') . ' ' . $path . 'versionlist ' . ' ' . $this->modx->lexicon('ugm_for_writing')); } } public function getVersionListPath() { return $this->versionListPath; } public function curlGetData($url, $returnData = false, $timeout = 6, $tries = 6 ) { $username = $this->github_username; $token = $this->github_token; $retVal = false; $errorMsg = '(' . $url . ' - curl) ' . $this->modx->lexicon('failed'); $ch = curl_init(); if ($this->verifyPeer) { $certPath = MODX_CORE_PATH . 'components/upgrademodx/cacert.pem'; curl_setopt($ch, CURLOPT_CAINFO, $certPath); } curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 6.0)"); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verifyPeer); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, $returnData); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_NOBODY, !$returnData); if (strpos($url, 'github') !== false) { if (!empty($username) && !empty($token)) { curl_setopt($ch, CURLOPT_USERPWD, $username . ':' . $token); } } $i = $tries; while ($i--) { $retVal = @curl_exec($ch); if (!empty($retVal)) { break; } } if (empty($retVal) || ($retVal === false)) { $e = curl_error($ch); if (!empty($e)) { $errorMsg = $e; } $this->setError($errorMsg); } elseif (! $returnData) { /* Just checking for existence */ $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $retVal = $statusCode == 200 || $statusCode == 301 || $statusCode == 302; } curl_close($ch); return $retVal; } public function fopenGetData($url, $returnData = false, $timeout = 6, $tries = 6) { $username = $this->modx->getOption('github_username'); $token = $this->modx->getOption('github_token'); $errorMsg = '(' . $url . ' - fopen) ' . $this->modx->lexicon('failed'); $retVal = false; $opts = array( 'http' => array( 'method' => 'GET', 'timeout' => $timeout, 'max_redirects' => 1, 'ignore_errors' => true, 'user_agent' => 'Mozilla/4.0 (compatible; MSIE 6.0)', ) ); if (!empty($username) && !empty($token)) { $opts['http']['header'] = "Authorization: Basic " . base64_encode($username . ':' . $token); } $ctx = stream_context_create($opts); $i = $tries; $old = @ini_set('default_socket_timeout', $timeout); while ($i--) { if (!$returnData) { $retVal = @fopen($url, 'r'); // $x = $http_response_header; if ($retVal) { @fclose($retVal); $retVal = true; break; } else { $timeout += 2; ini_set('default_socket_timeout', $timeout); } } else { $retVal = @file_get_contents($url, false, $ctx); // $x = $http_response_header; } } @ini_set('default_socket_timeout', $old); if (!$retVal) { $this->setError($errorMsg); } return $retVal; } public function downloadable($version, $method = 'curl', $timeout = 6, $tries = 2) { $this->clearErrors(); $downloadUrl = 'https://modx.com/download/direct/modx-' . $version . '.zip'; if ($method == 'curl') { $downloadable = $this->curlGetData($downloadUrl, false, $timeout, $tries); } else { $downloadable = $this->fopenGetData($downloadUrl, false, $timeout, $tries); } return $downloadable; } /** * @param $lastCheck string = time of previous check * @param $interval - interval between checks * @return bool true if time to check, false if not */ public function timeToCheck($lastCheck, $interval = '+1 week') { if (empty($lastCheck)) { $retVal = true; } else { $interval = strpos($interval, '+') === false ? '+' . $interval : $interval; $retVal = time() > strtotime($lastCheck . ' ' . $interval); } return $retVal; } public function clearErrors() { $this->errors = array(); } public function getLatestVersion() { return $this->latestVersion; } public function setError($msg) { $this->errors[] = $msg; } public function getErrors() { return $this->errors; } public function upgradeAvailable($currentVersion, $plOnly = false, $versionsToShow = 5, $method = 'curl') { $retVal = $this->getJSONFromGitHub($method, $this->gitHubTimeout, $this->attempts); if ($retVal !== false) { $retVal = $this->finalizeVersionArray($retVal, $plOnly, $versionsToShow); if ($retVal !== false) { $this->updateLatestVersion($retVal); $this->updateSnippetProperties(time(), $this->latestVersion); $this->updateVersionListFile(); } } if ($retVal === false) { $this->setError($this->modx->lexicon('ugm_no_version_list_from_github')); } $latestVersion = $this->latestVersion; if (!empty($this->errors)) { $upgradeAvailable = false; } else { /* See if the latest version is newer than the current version */ $newVersion = version_compare($currentVersion, $latestVersion) < 0; $downloadable = $this->downloadable($latestVersion, $method, $this->modxTimeout, $this->attempts); $upgradeAvailable = $newVersion && $downloadable; } return $upgradeAvailable; } public function mmkDir($folder, $perm = 0755) { if (!is_dir($folder)) { mkdir($folder, $perm, true); } } } }