├── ProcessWireUpgrade.css ├── ProcessWireUpgrade.module ├── ProcessWireUpgradeCheck.config.php ├── ProcessWireUpgradeCheck.module └── README.md /ProcessWireUpgrade.css: -------------------------------------------------------------------------------- 1 | body.AdminThemeDefault .content ul.bullets li, 2 | body.AdminThemeReno .content ul.bullets li { 3 | display: list-item; 4 | list-style: disc; 5 | margin-left: 2em; 6 | padding-left: 0; 7 | margin-top: 0; 8 | margin-bottom: 0; 9 | } 10 | span.pro { 11 | font-size: 12px; 12 | color: #999; 13 | } 14 | span.links { 15 | white-space: nowrap; 16 | } -------------------------------------------------------------------------------- /ProcessWireUpgrade.module: -------------------------------------------------------------------------------- 1 | 'Upgrades', 23 | 'summary' => 'Tool that helps you identify and install core and module upgrades.', 24 | 'version' => 11, 25 | 'author' => 'Ryan Cramer', 26 | 'installs' => 'ProcessWireUpgradeCheck', 27 | 'requires' => 'ProcessWire>=3.0.0', 28 | 'icon' => 'coffee' 29 | ); 30 | } 31 | 32 | const debug = false; 33 | const pageName = 'upgrades'; 34 | const minVersionPHP = '5.3.8'; 35 | 36 | /** 37 | * Path to /wire/ 38 | * 39 | */ 40 | protected $wirePath = ''; 41 | 42 | /** 43 | * Temporary path used by installer for download and ZIP extraction 44 | * 45 | */ 46 | protected $tempPath = ''; 47 | 48 | /** 49 | * Temporary path used by installer for file storage when file system isn't writable 50 | * 51 | */ 52 | protected $cachePath = ''; 53 | 54 | /** 55 | * Array of renames (oldPath => newPath) scheduled for __destruct 56 | * 57 | */ 58 | protected $renames = array(); // scheduled renames to occur after page render 59 | 60 | /** 61 | * Instance of ProcessWireUpgradeCheck 62 | * 63 | * @var ProcessWireUpgradeCheck 64 | * 65 | */ 66 | protected $checker = null; 67 | 68 | /** 69 | * Construct 70 | * 71 | */ 72 | public function __construct() { 73 | $this->initPaths(); 74 | parent::__construct(); 75 | } 76 | 77 | /** 78 | * API wired 79 | * 80 | */ 81 | public function wired() { 82 | $this->initPaths(); // duplication from construct intended 83 | parent::wired(); 84 | } 85 | 86 | /** 87 | * Initialize and perform access checks 88 | * 89 | */ 90 | public function init() { 91 | if($this->config->demo) { 92 | throw new WireException("This module cannot be used in demo mode"); 93 | } 94 | if(!$this->user->isSuperuser()) { 95 | throw new WireException("This module requires superuser"); 96 | } 97 | set_time_limit(3600); 98 | $this->checker = $this->modules->getInstall('ProcessWireUpgradeCheck'); 99 | if(!$this->checker) { 100 | throw new WireException("Please go to Modules and click 'Check for new modules' - this will auto-update ProcessWireUpgrade."); 101 | } 102 | parent::init(); 103 | } 104 | 105 | /** 106 | * Initialize paths used by this module 107 | * 108 | */ 109 | protected function initPaths() { 110 | $config = $this->wire()->config; 111 | $this->wirePath = $config->paths->root . 'wire/'; 112 | $this->tempPath = $config->paths->cache . $this->className() . '/'; 113 | $this->cachePath = $config->paths->cache . 'core-upgrade/'; 114 | } 115 | 116 | /** 117 | * Get info for either a specific core branch, or for the currently selected core branch 118 | * 119 | * @param string $name 120 | * @return array 121 | * 122 | */ 123 | protected function getBranch($name = '') { 124 | $branches = $this->checker->getCoreBranches(); 125 | if(empty($name)) $name = $this->session->getFor($this, 'branch'); 126 | return isset($branches[$name]) ? $branches[$name] : array(); 127 | } 128 | 129 | /** 130 | * Set the current core branch 131 | * 132 | * @param string $name 133 | * 134 | */ 135 | protected function setBranch($name) { 136 | $this->session->setFor($this, 'branch', $name); 137 | } 138 | 139 | /** 140 | * Preflight checks before listing modules 141 | * 142 | * @return string 143 | * 144 | */ 145 | protected function preflight() { 146 | 147 | $phpVersion = PHP_VERSION; 148 | 149 | if(version_compare($phpVersion, self::minVersionPHP) >= 0) { 150 | // good 151 | } else { 152 | $this->error("Please note that your current PHP version ($phpVersion) is not adequate to upgrade to the latest ProcessWire."); 153 | } 154 | 155 | if(!extension_loaded('pdo_mysql')) { 156 | $this->error("Your PHP is not compiled with PDO support. PDO is required by ProcessWire."); 157 | } 158 | 159 | if(!class_exists('\ZipArchive')) { 160 | $this->warning( 161 | "Your PHP does not have ZipArchive support. This is required to install core or module upgrades with this tool. " . 162 | "You can still use this tool to identify new versions and install them manually." 163 | ); 164 | } 165 | 166 | $upgradePaths = array($this->cachePath, $this->tempPath); 167 | 168 | foreach($upgradePaths as $key => $path) { 169 | if(!file_exists($path)) unset($upgradePaths[$key]); 170 | } 171 | 172 | if(count($upgradePaths)) { 173 | $btn = $this->modules->get('InputfieldButton'); /** @var InputfieldButton $btn */ 174 | $btn->href = "./remove"; 175 | $btn->value = $this->_('Remove'); 176 | $btn->icon = 'trash-o'; 177 | return 178 | $this->h('Upgrade files are already present. Please remove them before continuing.') . $this->ul($upgradePaths) . 179 | $this->p($btn->render()); 180 | } 181 | 182 | $lastRefresh = $this->session->getFor($this, 'lastRefresh'); 183 | 184 | if(!$lastRefresh || $lastRefresh < (time() - 86400)) { 185 | $btn = $this->refreshButton(); 186 | $btn->value = $this->_('Continue'); 187 | $btn->icon = 'angle-right'; 188 | return 189 | $this->h('We will load the latest core and module versions from the ProcessWire modules directory.') . 190 | $this->p('Please be patient, this may take a few seconds to complete.') . 191 | $this->p($btn->render()) . 192 | $this->lastRefreshInfo(); 193 | } 194 | 195 | return ''; 196 | } 197 | 198 | /** 199 | * Ask user to select branch or make them remove existing installation files 200 | * 201 | */ 202 | public function execute() { 203 | 204 | $sanitizer = $this->sanitizer; 205 | $config = $this->config; 206 | $modules = $this->modules; 207 | 208 | $preflight = $this->preflight(); 209 | if($preflight) return $preflight; 210 | 211 | /** @var MarkupAdminDataTable $table */ 212 | $table = $modules->get('MarkupAdminDataTable'); 213 | $table->setEncodeEntities(false); 214 | $table->headerRow(array( 215 | $this->_('Module title'), 216 | $this->_('Module name'), 217 | $this->_('Installed'), 218 | $this->_('Latest'), 219 | $this->_('Status'), 220 | $this->_('Links'), 221 | )); 222 | 223 | $items = $this->checker->getVersions(); 224 | $numPro = 0; 225 | 226 | if(count($items)) { 227 | foreach($items as $name => $item) { 228 | if(empty($item['local'])) continue; 229 | if(empty($item['remote'])) { /* not in directory */ } 230 | 231 | $remote = $sanitizer->entities($item['remote']); 232 | $installer = empty($item['installer']) ? '' : $sanitizer->entities($item['installer']); 233 | $upgradeLabel = $this->_('Up-to-date'); 234 | $links = []; 235 | 236 | if($item['new'] > 0) { 237 | $upgradeLabel = $this->_('Upgrade available'); 238 | $remote = $this->b($remote); 239 | } else if($item['new'] < 0) { 240 | $upgradeLabel .= '+'; 241 | } 242 | if(empty($remote)) { 243 | $remote = ""; 244 | $upgradeLabel = ""; 245 | } 246 | if(empty($item['branch'])) { 247 | $upgradeURL = $config->urls->admin . "module/?update=" . ($installer ? $installer : $name); 248 | } else { 249 | $upgradeURL = "./check?branch=$item[branch]"; 250 | } 251 | if($item['new'] > 0) { 252 | $upgradeLabel = $this->icon('lightbulb-o') . $upgradeLabel; 253 | $upgrade = $this->a($upgradeURL, $upgradeLabel); 254 | } else { 255 | $upgrade = $this->span($upgradeLabel, 'detail'); 256 | } 257 | $urls = isset($item['urls']) ? $item['urls'] : array(); 258 | foreach($urls as $key => $url) $urls[$key] = $sanitizer->entities($url); 259 | if(!empty($urls['support'])) { 260 | $links[] = $this->iconLink('life-ring', $urls['support'], 'Support'); 261 | } 262 | if(!empty($urls['repo']) && strpos($urls['repo'], 'github.com')) { 263 | $links[] = $this->iconLink('github-alt', $urls['repo'], 'GitHub'); 264 | } else if(!empty($urls['repo'])) { 265 | $links[] = $this->iconLink('info-circle', $urls['repo'], 'Details'); 266 | } else if(!empty($item['href'])) { 267 | $links[] = $this->iconLink('info-circle', $item['href'], 'Info'); 268 | } 269 | if(!empty($urls['dir'])) { 270 | $links[] = $this->iconLink('share-alt', $urls['dir'], 'Directory'); 271 | } 272 | 273 | if(empty($remote) && empty($links)) continue; 274 | 275 | // else if(!$item['remote']) $upgrade = "" . $this->_('Not in directory') . ""; 276 | $icon = empty($item['icon']) ? wireIconMarkup('plug', 'fw') : wireIconMarkup($this->sanitizer->entities($item['icon']), 'fw'); 277 | $title = $icon . ' ' . $sanitizer->entities($item['title']); 278 | $proLabel = ' ' . $this->span('PRO', 'pro'); 279 | 280 | if($installer && empty($remote)) { 281 | $title = $this->tooltip("Upgraded with $installer", $title); 282 | if(!empty($item['pro'])) $title .= $proLabel; 283 | 284 | } else if(!empty($item['pro'])) { 285 | $title = $this->a($upgradeURL, $title) . $proLabel; 286 | if($item['new'] > 0) { 287 | if(!empty($urls['support'])) $upgradeURL = $urls['support']; 288 | $protip = 'PRO module upgrade available in ProcessWire VIP support board (login required)'; 289 | $upgrade = $this->tooltip($protip, $this->aa($upgradeURL, $upgradeLabel)); 290 | // $title = $this->tooltip($protip, $this->aa($upgradeURL, $title) . $proLabel); 291 | } 292 | $numPro++; 293 | 294 | } else { 295 | $title = $this->a($upgradeURL, $title); 296 | } 297 | 298 | $table->row(array( 299 | $title, 300 | $sanitizer->entities($name), 301 | $sanitizer->entities($item['local']), 302 | $remote, 303 | $upgrade, 304 | $this->span(implode(' ', $links), 'links') 305 | )); 306 | } 307 | } 308 | 309 | return 310 | $table->render() . 311 | $this->p($this->refreshButton(true)->render()) . 312 | $this->lastRefreshInfo(); 313 | } 314 | 315 | /** 316 | * Refresh module versions data 317 | * 318 | */ 319 | public function executeRefresh() { 320 | $this->session->setFor($this, 'lastRefresh', time()); 321 | if(method_exists($this->modules, 'resetCache')) $this->modules->resetCache(); 322 | $this->checker->getVersions(true); 323 | $this->message($this->_('Refreshed module versions data')); 324 | $this->session->redirect('./'); 325 | } 326 | 327 | /** 328 | * Remove existing installation files 329 | * 330 | */ 331 | public function executeRemove() { 332 | $paths = array( 333 | $this->cachePath, 334 | $this->tempPath, 335 | ); 336 | foreach($paths as $path) { 337 | if(!file_exists($path)) continue; 338 | if(wireRmdir($path, true)) { 339 | $this->session->message(sprintf($this->_('Removed: %s'), $path)); 340 | } else { 341 | $this->session->error(sprintf($this->_('Permission error removing path (please remove manually): %s'), $path)); 342 | } 343 | } 344 | $this->session->redirect('./'); 345 | } 346 | 347 | /** 348 | * Check the selected branch and compare to current version to see if user wants to continue 349 | * 350 | */ 351 | public function executeCheck() { 352 | 353 | $this->coreUpgrade(); 354 | 355 | $config = $this->config; 356 | $input = $this->input; 357 | $modules = $this->modules; 358 | 359 | if(!$config->debug) { 360 | $this->error( 361 | "While optional, we recommend that you enable debug mode during the upgrade so that you will see detailed error messages, should they occur. " . 362 | "Do this by editing /site/config.php and setting the debug option to true. Example: \$config->debug = true;" 363 | ); 364 | } 365 | 366 | $name = $input->get('branch'); 367 | if(empty($name)) throw new WireException("No branch selected"); 368 | 369 | $branch = $this->getBranch($name); 370 | if(!count($branch)) throw new WireException("Unknown branch"); 371 | 372 | $this->headline("ProcessWire Core ($name)"); 373 | 374 | $this->setBranch($name); 375 | $result = version_compare($config->version, $branch['version']); 376 | 377 | if($result < 0) { 378 | $msg = "The available version ($branch[version]) is newer than the one you currently have ($config->version)."; 379 | 380 | } else if($result > 0) { 381 | $msg = "The available version ($branch[version]) is older than the one you currently have ($config->version)."; 382 | 383 | } else { 384 | $msg = "The available version is the same as the one you currently have."; 385 | } 386 | 387 | $out = $this->h("Do you want to download and install ProcessWire $branch[name] version $branch[version]?") . $this->p($msg); 388 | 389 | /** @var InputfieldButton $btn */ 390 | $btn = $modules->get('InputfieldButton'); 391 | $btn->href = "./download"; 392 | $btn->value = $this->_('Download Now'); 393 | $btn->icon = 'cloud-download'; 394 | $out .= $btn->render(); 395 | 396 | $btn = $modules->get('InputfieldButton'); 397 | $btn->href = "./"; 398 | $btn->value = $this->_('Abort'); 399 | $btn->icon = 'times-circle'; 400 | $btn->addClass('ui-priority-secondary'); 401 | $out .= $btn->render(); 402 | 403 | $out .= $this->p("After clicking the download button, be patient, as this may take a minute.", 'detail'); 404 | 405 | return $out; 406 | } 407 | 408 | /** 409 | * Download the selected branch ZIP file 410 | * 411 | */ 412 | public function executeDownload() { 413 | 414 | $branch = $this->getBranch(); 415 | if(empty($branch)) throw new WireException("No branch selected"); 416 | 417 | wireMkdir($this->tempPath); 418 | $http = new WireHttp(); 419 | $this->wire($http); 420 | $zipfile = $http->download($branch['zipURL'], $this->tempPath . "$branch[name].zip"); 421 | 422 | if(!$zipfile || !file_exists($zipfile) || !filesize($zipfile)) { 423 | throw new WireException("Unable to download $branch[zipURL]"); 424 | } 425 | 426 | $this->message("Downloaded version $branch[version] (" . number_format(filesize($zipfile)) . " bytes)"); 427 | $this->session->redirect('./database'); 428 | } 429 | 430 | /** 431 | * Export database or instruct them to do it 432 | * 433 | */ 434 | public function executeDatabase() { 435 | 436 | $this->coreUpgrade(); 437 | 438 | $config = $this->config; 439 | $input = $this->input; 440 | $session = $this->session; 441 | $modules = $this->modules; 442 | 443 | $session->removeFor($this, 'database'); 444 | $branch = $this->getBranch(); 445 | $canBackup = class_exists('\\ProcessWire\\WireDatabaseBackup'); 446 | 447 | if($input->get('backup') && $canBackup) { 448 | $options = array( 449 | 'filename' => $config->dbName . "-" . $config->version . "-" . date('Y-m-d_H-i-s') . ".sql", 450 | 'description' => "Automatic backup made by ProcessWireUpgrade before upgrading from {$config->version} to $branch[version]." 451 | ); 452 | $backups = $this->database->backups(); 453 | $file = $backups->backup($options); 454 | $errors = $backups->errors(); 455 | if(count($errors)) { 456 | foreach($errors as $error) $this->error($error); 457 | } 458 | if($file) { 459 | clearstatcache(); 460 | $bytes = filesize($file); 461 | $file = str_replace($config->paths->root, '/', $file); 462 | $this->message("Backup saved to $file ($bytes bytes) - Please note this location should you later need to restore it."); 463 | $session->setFor($this, 'database', $file); 464 | } else { 465 | $this->error("Database backup failed"); 466 | } 467 | $session->redirect('./prepare'); 468 | } 469 | 470 | $out = $this->h('Database Backup'); 471 | 472 | if($canBackup) { 473 | $out .= $this->p('Your version of ProcessWire supports automatic database backups.*'); 474 | 475 | /** @var InputfieldButton $btn */ 476 | $btn = $modules->get('InputfieldButton'); 477 | $btn->href = "./database?backup=1"; 478 | $btn->value = $this->_('Backup Database Now'); 479 | $btn->icon = 'database'; 480 | $out .= $btn->render(); 481 | 482 | $btn = $modules->get('InputfieldButton'); 483 | $btn->href = "./prepare"; 484 | $btn->icon = 'angle-right'; 485 | $btn->value = $this->_('Skip Database Backup'); 486 | $btn->addClass('ui-priority-secondary'); 487 | $out .= $btn->render(); 488 | 489 | $out .= $this->p('*We also recommend creating an independent database backup, for instance with PhpMyAdmin.', 'detail'); 490 | 491 | } else { 492 | $out .= $this->p( 493 | "Your current version of ProcessWire does not support automatic database backups. " . 494 | "We recommend making a backup of database `$config->dbName` using a tool like PhpMyAdmin. " . 495 | "Click the button below once you have saved a backup." 496 | ); 497 | 498 | /** @var InputfieldButton $btn */ 499 | $btn = $modules->get('InputfieldButton'); 500 | $btn->href = "./prepare"; 501 | $btn->icon = 'angle-right'; 502 | $btn->value = $this->_('Confirm'); 503 | $out .= $btn->render(); 504 | } 505 | 506 | return $this->out($out); 507 | } 508 | 509 | /** 510 | * Unzip files and prepare them for installation 511 | * 512 | */ 513 | public function executePrepare() { 514 | 515 | $config = $this->config; 516 | $this->coreUpgrade(); 517 | 518 | $error = ''; 519 | $branch = $this->getBranch(); 520 | if(empty($branch)) throw new WireException("No branch selected"); 521 | $zipfile = $this->tempPath . "$branch[name].zip"; // site/assets/cache/ProcessWireUpgrade/branch-dev.zip 522 | 523 | if(!file_exists($zipfile)) throw new WireException("Unable to locate ZIP: $zipfile"); 524 | $files = wireUnzipFile($zipfile, $this->tempPath); 525 | if(!count($files)) $error = "No files were found in $zipfile"; 526 | 527 | $oldVersion = $config->version; 528 | $newVersion = $branch['version']; 529 | 530 | $rootPath = dirname(rtrim($this->wirePath, '/')) . '/'; 531 | $rootTempPath = $this->tempPath; // site/assets/cache/ProcessWireUpgrade/ 532 | $wireTempPath = $this->tempPath . 'wire/'; // site/assets/cache/ProcessWireUpgrade/wire/ 533 | $wireNewName = "wire-$newVersion"; // wire-2.5.0 534 | 535 | if(!$error && !is_dir($wireTempPath)) { 536 | // adjust paths according to where they were unzipped, as needed 537 | // need to drill down a level from extracted archive 538 | // i.e. files[0] may be a dir like /ProcessWire-dev/ 539 | $rootTempPath = $this->tempPath . trim($files[0], '/') . '/'; 540 | $wireTempPath = $rootTempPath . "wire/"; 541 | if(!is_dir($wireTempPath)) $error = "Unable to find /wire/ directory in archive"; 542 | } 543 | 544 | $indexNewName = "index-$newVersion.php"; // index-2.5.0.php 545 | $htaccessNewName = "htaccess-$newVersion.txt"; // htaccess-2.5.0.txt 546 | 547 | $newIndexData = file_get_contents($rootTempPath . 'index.php'); 548 | $newIndexVersion = preg_match('/define\("PROCESSWIRE", (\d+)\);/', $newIndexData, $matches) ? (int) $matches[1] : 0; 549 | $oldIndexData = file_get_contents($rootPath . 'index.php'); 550 | $oldIndexVersion = preg_match('/define\("PROCESSWIRE", (\d+)\);/', $oldIndexData, $matches) ? (int) $matches[1] : 0; 551 | $indexIsOk = $newIndexVersion === $oldIndexVersion; 552 | 553 | $oldHtaccessData = file_get_contents($rootTempPath . 'htaccess.txt'); 554 | $oldHtaccessVersion = preg_match('/@htaccessVersion\s+(\d+)/', $oldHtaccessData, $matches) ? (int) $matches[1] : 0; 555 | $newHtaccessData = file_get_contents($rootPath . '.htaccess'); 556 | $newHtaccessVersion = preg_match('/@htaccessVersion\s+(\d+)/', $newHtaccessData, $matches) ? (int) $matches[1] : 0; 557 | $htaccessIsOk = $oldHtaccessVersion === $newHtaccessVersion; 558 | 559 | $rootWritable = is_writable($rootPath) && is_writable($rootPath . "wire/"); 560 | 561 | // determine where we will be moving upgrade files to 562 | if($rootWritable) { 563 | // if root path is writable, we can place new dirs/files in the same 564 | // location as what they are replacing, i.e. /wire/ and /wire-2.5.0/ 565 | $wireNewPath = $rootPath . $wireNewName . "/"; 566 | $htaccessNewFile = $rootPath . $htaccessNewName; 567 | $indexNewFile = $rootPath . $indexNewName; 568 | 569 | } else { 570 | // if root is not writable, we will place dirs/files in /site/assets/cache/core-upgrade/ instead. 571 | $cacheUpgradePath = $this->cachePath; 572 | $cacheUpgradeURL = str_replace($config->paths->root, '/', $cacheUpgradePath); 573 | $this->warning( 574 | "Your file system is not writable, so we are installing files to $cacheUpgradeURL instead. " . 575 | "You will have to copy them manually to your web root." 576 | ); 577 | wireMkdir($cacheUpgradePath); 578 | $wireNewPath = $cacheUpgradePath . 'wire/'; 579 | $htaccessNewFile = $cacheUpgradePath . 'htaccess.txt'; 580 | $indexNewFile = $cacheUpgradePath . 'index.php'; 581 | } 582 | 583 | if(!$error) { 584 | $this->renameNow($wireTempPath, $wireNewPath); // /temp/path/wire/ => /wire-2.5.0/ 585 | $this->renameNow($rootTempPath . "index.php", $indexNewFile); // /temp/path/index.php => /index-2.5.0.php 586 | $this->renameNow($rootTempPath . "htaccess.txt", $htaccessNewFile); // /temp/path/htaccess.txt => /htaccess-2.5.0.txt 587 | // remove /temp/path/ as no longer needed since we've taken everything we need out of it above 588 | wireRmdir($this->tempPath, true); 589 | } 590 | 591 | if($error) throw new WireException($error); 592 | 593 | $wireNewLabel = str_replace($config->paths->root, '/', $wireNewPath); 594 | $indexNewLabel = $rootWritable ? basename($indexNewFile) : str_replace($config->paths->root, '/', $indexNewFile); 595 | $htaccessNewLabel = $rootWritable ? basename($htaccessNewFile) : str_replace($config->paths->root, '/', $htaccessNewFile); 596 | 597 | $out = 598 | $this->h('Upgrade files copied') . 599 | $this->p( 600 | "We have prepared copies of upgrade files for installation. At this point, " . 601 | "you may install them yourself by replacing the existing `/wire/` directory with `$wireNewLabel`, " . 602 | "and optionally `index.php` with `$indexNewLabel`, and `.htaccess` with `$htaccessNewLabel`. " . 603 | ($rootWritable ? "Or, since your file system is writable, this tool can install them." : "Full instructions are below.") 604 | ); 605 | 606 | $items = array(); 607 | 608 | if($indexIsOk) { 609 | $items[] = "Your `index.php` file appears to be up-to-date, it should be okay to leave your existing one in place."; 610 | } else { 611 | $items[] = "Your `index.php` file appears to need an update, check that you haven’t made any site-specific customizations to it before replacing it."; 612 | } 613 | 614 | if($htaccessIsOk) { 615 | $items[] = "Your `.htaccess` file appears to be up-to-date, it should be okay to leave your existing one in place."; 616 | } else { 617 | $items[] = "Your `.htaccess` file appears to need an update, check for any site-specific customizations before replacing it. You may want to apply updates to it manually."; 618 | } 619 | 620 | $out .= $this->ul($items); 621 | 622 | if($rootWritable) { 623 | 624 | $htaccessNote = ''; 625 | if(!$htaccessIsOk) { 626 | $htaccessNote = 'Since the `.htaccess` file can often have site-specific customizations, it would be best to handle that one manually, unless you know it is unmodified.'; 627 | } 628 | 629 | $out .= 630 | $this->h('Want this tool to install the upgrade?') . 631 | $this->p("Check the boxes below for what you’d like us to install. $htaccessNote"); 632 | 633 | 634 | /** @var InputfieldSubmit $btn */ 635 | $btn = $this->modules->get('InputfieldSubmit'); 636 | $btn->attr('name', 'submit_install'); 637 | $btn->value = $this->_('Install'); 638 | $btn->icon = 'angle-right'; 639 | 640 | $out .= 641 | $this->form('./install/', 642 | $this->p( 643 | $this->checkbox('wire', 'Install new core `/wire/` directory', "(old will be renamed to /.wire-$oldVersion/)", true) . 644 | $this->checkbox('index', 'Install new `index.php` file', "(old will be renamed to .index-$oldVersion.php)", !$indexIsOk) . 645 | $this->checkbox('htaccess', 'Install new `.htaccess` file', "(old will be renamed to .htaccess-$oldVersion)", false) 646 | ) . 647 | $this->p($btn->render()) 648 | ); 649 | 650 | } else { 651 | // root not writable 652 | 653 | $backupInfo = array( 654 | "/wire" => "/.wire-$oldVersion", 655 | "/index.php" => "/.index-$oldVersion.php", 656 | "/.htaccess" => "/.htaccess-$oldVersion", 657 | ); 658 | 659 | $renameInfo = array( 660 | rtrim($wireNewPath, '/') => "/wire", 661 | "$indexNewFile" => "/index.php", 662 | "$htaccessNewFile" => "/.htaccess", 663 | ); 664 | 665 | $out .= $this->p( 666 | "Your file system is not writable so we can’t automatically install the upgrade files for you. " . 667 | "However, the files are ready for you to move to their destinations." 668 | ); 669 | 670 | $out .= 671 | $this->h('Backup your existing files') . 672 | $this->p( 673 | "While optional, we strongly recommend making backups of everything replaced so that you can always revert back to it if needed. " . 674 | "We recommend doing this by performing the following file rename operations:" 675 | ); 676 | 677 | $items = array(); 678 | foreach($backupInfo as $old => $new) { 679 | $items[] = "Rename `$old` to `$new`"; 680 | } 681 | $out .= $this->ul($items); 682 | 683 | $out .= 684 | $this->h('Migrate the new files') . 685 | $this->p('Now you can migrate the new files, renaming them from their temporary location to their destination.'); 686 | 687 | $items = array(); 688 | foreach($renameInfo as $old => $new) { 689 | $old = str_replace($config->paths->root, '/', $old); 690 | $items[] = "Rename `$old` to `$new`"; 691 | } 692 | $out .= 693 | $this->ul($items) . 694 | $this->p('Once you’ve completed the above steps, your upgrade will be complete.') . 695 | $this->completionNotes(); 696 | } 697 | 698 | $out .= $this->p('*In many cases, it is not necessary to upgrade the index.php and .htaccess files since they don’t always change between versions.', 'detail'); 699 | 700 | return $this->out($out); 701 | } 702 | 703 | /** 704 | * Install prepared files 705 | * 706 | */ 707 | public function executeInstall() { 708 | 709 | $this->coreUpgrade(); 710 | 711 | $input = $this->input; 712 | $config = $this->config; 713 | 714 | if(!$input->post('submit_install')) throw new WireException('No form received'); 715 | 716 | $branch = $this->getBranch(); 717 | if(empty($branch)) throw new WireException("No branch selected"); 718 | 719 | $oldVersion = $config->version; 720 | $newVersion = $branch['version']; 721 | $rootPath = dirname(rtrim($this->wirePath, '/')) . '/'; 722 | $renames = array(); 723 | 724 | if($input->post('wire')) { 725 | $renames["wire"] = ".wire-$oldVersion"; 726 | $renames["wire-$newVersion"] = "wire"; 727 | } 728 | if($input->post('index')) { 729 | $renames["index.php"] = ".index-$oldVersion.php"; 730 | $renames["index-$newVersion.php"] = "index.php"; 731 | } 732 | if($input->post('htaccess')) { 733 | $renames[".htaccess"] = ".htaccess-$oldVersion"; 734 | $renames["htaccess-$newVersion.txt"] = ".htaccess"; 735 | } 736 | 737 | foreach($renames as $old => $new) { 738 | $this->renameLater($rootPath . $old, $rootPath . $new); 739 | } 740 | 741 | $out = 742 | $this->h('Upgrade completed') . 743 | $this->p($this->b('Double check that everything works before you leave this page')) . 744 | $this->completionNotes(); 745 | 746 | return $this->out($out); 747 | } 748 | 749 | /** 750 | * Completion notes 751 | * 752 | */ 753 | protected function completionNotes() { 754 | 755 | $config = $this->config; 756 | $branch = $this->getBranch(); 757 | $newVersion = $branch['version']; 758 | $oldVersion = $config->version; 759 | $dbFile = $this->session->getFor($this, 'database'); 760 | $dbNote = ''; 761 | 762 | if($dbFile) $dbNote = "Your database was backed up to this file:
`$dbFile`"; 763 | 764 | $frontEndLink = $this->aa($config->urls->root, 'front-end'); 765 | $adminLink = $this->aa($config->urls->admin, 'admin'); 766 | $items = array(); 767 | 768 | $items[] = "Test out both the $frontEndLink and $adminLink of your site in full to make sure everything works."; 769 | $items[] = "Installed files have permission `$config->chmodFile` and directories `$config->chmodDir`. Double check that these are safe with your web host, especially in a shared hosting environment."; 770 | 771 | if($config->debug) { 772 | $items[] = "For production sites, remember to turn off debug mode once your upgrade is complete."; 773 | } 774 | 775 | $out = $this->ul($items); 776 | 777 | $out .= 778 | $this->p($this->b('If your upgrade did not work…')) . 779 | $this->p( 780 | 'If you encounter fatal error messages (in the front-end or admin links above), hit reload/refresh in that browser tab until it clears (2-3 times). ' . 781 | 'It may take a few requests for ProcessWire to apply any necessary database schema changes. ' . 782 | 'Should you determine that the upgrade failed for some reason and you want to revert back to the previous version, ' . 783 | 'below are the steps to undo what was just applied.' 784 | ) . 785 | $this->p('Step 1: Remove the installed updates by renaming or deleting them') . 786 | $this->ul(array( 787 | "Delete `/wire/` directory OR rename it to `/.wire-$newVersion/`", 788 | "If you replaced the `.htaccess` file: Delete `.htaccess` OR rename it to `.htaccess-$newVersion`", 789 | "If you replaced the `index.php` file: Delete `index.php` OR rename it to `.index-$newVersion.php`" 790 | )) . 791 | $this->p('Step 2: Restore backed up files') . 792 | $this->ul(array( 793 | "Rename directory `/.wire-$oldVersion/` to `/wire/`", 794 | "If applicable: Rename `.htaccess-$oldVersion` to `.htaccess`", 795 | "If applicable: Rename `.index-$oldVersion.php` to `index.php`", 796 | )); 797 | 798 | if($dbNote) $out .= 799 | $this->p('Step 3: Restore backed up database (if necessary)') . 800 | $this->ul(array($dbNote)); 801 | 802 | 803 | return $out; 804 | } 805 | 806 | /** 807 | * Schedule a rename operation, which will occur at __destruct 808 | * 809 | * @param string $oldPath 810 | * @param string $newPath 811 | * 812 | */ 813 | protected function renameLater($oldPath, $newPath) { 814 | $this->renames[$oldPath] = $newPath; 815 | $old = basename(rtrim($oldPath, '/')); 816 | $new = basename(rtrim($newPath, '/')); 817 | $this->message("Rename $old => $new"); 818 | } 819 | 820 | /** 821 | * Perform a rename now 822 | * 823 | * @param string $old 824 | * @param string $new 825 | * @return bool 826 | * @throws WireException 827 | * 828 | */ 829 | protected function renameNow($old, $new) { 830 | 831 | $result = true; 832 | 833 | // for labels 834 | $_old = str_replace($this->config->paths->root, '/', $old); 835 | $_new = str_replace($this->config->paths->root, '/', $new); 836 | 837 | if(!file_exists($old)) { 838 | $this->error("$_old does not exist"); 839 | return $result; 840 | } 841 | 842 | if(file_exists($new)) { 843 | $this->message("$_new already exists (we left it untouched)"); 844 | return $result; 845 | } 846 | 847 | $result = rename($old, $new); 848 | 849 | if($result) { 850 | $this->message("Renamed $_old => $_new"); 851 | } else { 852 | $this->error("Unable to rename $_old => $_new"); 853 | if(basename(rtrim($new, '/')) == 'wire') { 854 | throw new WireException("Upgrade aborted. Unable to rename $_old => $_new"); 855 | } 856 | } 857 | 858 | return $result; 859 | } 860 | 861 | protected function coreUpgrade() { 862 | $this->headline($this->_('Core upgrade')); 863 | } 864 | 865 | /** 866 | * @param bool $showInHeader 867 | * @return InputfieldButton 868 | * 869 | */ 870 | protected function refreshButton($showInHeader = false) { 871 | /** @var InputfieldButton $btn */ 872 | $btn = $this->modules->get('InputfieldButton'); 873 | $btn->href = './refresh'; 874 | $btn->value = $this->_('Refresh'); 875 | $btn->icon = 'refresh'; 876 | if($showInHeader && method_exists($btn, 'showInHeader')) $btn->showInHeader(true); 877 | return $btn; 878 | } 879 | 880 | /** 881 | * @param bool $paragraph 882 | * @return string 883 | * 884 | */ 885 | protected function lastRefreshInfo($paragraph = true) { 886 | $lastRefresh = $this->session->getFor($this, 'lastRefresh'); 887 | $lastRefresh = $lastRefresh ? wireRelativeTimeStr($lastRefresh) : $this->_('N/A'); 888 | $out = sprintf($this->_('Last refresh: %s'), $lastRefresh); 889 | if($paragraph) $out = $this->p($out, 'detail last-refresh-info'); 890 | return $out; 891 | } 892 | 893 | /** 894 | * Process rename operations 895 | * 896 | */ 897 | public function __destruct() { 898 | if(!count($this->renames)) return; 899 | //$rootPath = dirname(rtrim($this->wirePath, '/')) . '/'; 900 | foreach($this->renames as $oldPath => $newPath) { 901 | if(file_exists($newPath)) { 902 | $n = 0; 903 | do { 904 | $newPath2 = $newPath . "-" . (++$n); 905 | } while(file_exists($newPath2)); 906 | if(rename($newPath, $newPath2)) { 907 | $this->message("Renamed $newPath => $newPath2"); 908 | } 909 | } 910 | $old = basename(rtrim($oldPath, '/')); 911 | $new = basename(rtrim($newPath, '/')); 912 | if(rename($oldPath, $newPath)) { 913 | $this->message("Renamed $old => $new"); 914 | } else { 915 | $this->error("Unable to rename $old => $new"); 916 | } 917 | } 918 | $this->renames = array(); 919 | } 920 | 921 | public function h($str, $h = 2) { return "$str"; } 922 | public function p($str, $class = '') { return $class ? "

$str

" : "

$str

"; } 923 | public function a($href, $label, $class = '') { return ($class ? "$label"; } 924 | public function aa($href, $label, $class = '') { return str_replace('a($href, $label, $class)); } 925 | public function span($str, $class = '') { return $class ? "$str" : "$str"; } 926 | public function b($str, $class = '') { return $class ? "$str" : "$str"; } 927 | public function ul(array $items) { return "'; } 928 | public function form($action, $content) { return "
$content
"; } 929 | public function icon($name, $fw = true) { return wireIconMarkup($name, ($fw ? 'fw' : '')); } 930 | public function iconLink($icon, $href, $tooltip) { return str_replace('aa($href, $this->icon($icon), 'pw-tooltip')); } 931 | public function tooltip($tooltip, $markup) { return "$markup"; } 932 | 933 | public function checkbox($name, $label, $note = '', $checked = false) { 934 | $adminTheme = $this->wire()->adminTheme; 935 | $checkboxClass = $adminTheme ? $this->wire()->adminTheme->getClass('input-checkbox') : ''; 936 | $checked = $checked ? 'checked' : ''; 937 | $note = $note ? $this->span($note, 'detail') : ''; 938 | return "
"; 939 | } 940 | 941 | protected function out($out) { 942 | if(strpos($out, '`') === false) return $out; 943 | $out = preg_replace('/[`]([^<\n`]+?)[`]/', '$1', $out); 944 | return $out; 945 | } 946 | 947 | /** 948 | * Install 949 | * 950 | */ 951 | public function ___install() { 952 | 953 | // create the page our module will be assigned to 954 | $page = new Page(); 955 | $page->template = 'admin'; 956 | $page->name = self::pageName; 957 | 958 | // installs to the admin "Setup" menu ... change as you see fit 959 | $page->parent = $this->pages->get($this->config->adminRootPageID)->child('name=setup'); 960 | $page->process = $this; 961 | 962 | // we will make the page title the same as our module title 963 | // but you can make it whatever you want 964 | $info = self::getModuleInfo(); 965 | $page->title = $info['title']; 966 | 967 | // save the page 968 | $page->save(); 969 | 970 | // tell the user we created this page 971 | $this->message("Created Page: {$page->path}"); 972 | } 973 | 974 | /** 975 | * Uninstall 976 | * 977 | */ 978 | public function ___uninstall() { 979 | 980 | // find the page we installed, locating it by the process field (which has the module ID) 981 | // it would probably be sufficient just to locate by name, but this is just to be extra sure. 982 | $moduleID = $this->modules->getModuleID($this); 983 | $page = $this->pages->get("template=admin, process=$moduleID, name=" . self::pageName . "|core-upgrade"); 984 | 985 | if($page->id) { 986 | // if we found the page, let the user know and delete it 987 | $this->message("Deleting Page: {$page->path}"); 988 | $page->delete(); 989 | } 990 | 991 | wireRmdir($this->tempPath, true); 992 | } 993 | 994 | } 995 | 996 | -------------------------------------------------------------------------------- /ProcessWireUpgradeCheck.config.php: -------------------------------------------------------------------------------- 1 | add(array( 6 | array( 7 | 'name' => 'useLoginHook', 8 | 'type' => 'radios', 9 | 'label' => $this->_('Check for upgrades on superuser login?'), 10 | 'description' => $this->_('If "No" is selected, then upgrades will only be checked manually when you click to Setup > Upgrades.'), 11 | 'notes' => $this->_('Automatic upgrade check requires ProcessWire 3.0.123 or newer.'), 12 | 'options' => array( 13 | 1 => $this->_('Yes'), 14 | 0 => $this->_('No') 15 | ), 16 | 'optionColumns' => 1, 17 | 'value' => 0 18 | ) 19 | )); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ProcessWireUpgradeCheck.module: -------------------------------------------------------------------------------- 1 | 'Upgrades Checker', 28 | 'summary' => 'Automatically checks for core and installed module upgrades at routine intervals.', 29 | 'version' => 9, 30 | 'autoload' => "template=admin", 31 | 'singular' => true, 32 | 'author' => 'Ryan Cramer', 33 | 'icon' => 'coffee', 34 | 'requires' => 'ProcessWireUpgrade', 35 | ); 36 | } 37 | 38 | const tagsURL = 'https://api.github.com/repos/processwire/processwire/tags'; 39 | const branchesURL = 'https://api.github.com/repos/processwire/processwire/branches'; 40 | const versionURL = 'https://raw.githubusercontent.com/processwire/processwire/{branch}/wire/core/ProcessWire.php'; 41 | const zipURL = 'https://github.com/processwire/processwire/archive/{branch}.zip'; 42 | const timeout = 4.5; 43 | 44 | /** 45 | * Repository information indexed by branch 46 | * 47 | * @var array 48 | * 49 | */ 50 | protected $repos = array(); 51 | 52 | /** 53 | * Construct 54 | * 55 | */ 56 | public function __construct() { 57 | $this->set('useLoginHook', 1); 58 | $this->repos = array( 59 | 'main' => array( 60 | 'branchesURL' => self::branchesURL, 61 | 'zipURL' => self::zipURL, 62 | 'versionURL' => self::versionURL, 63 | 'tagsURL' => self::tagsURL, 64 | ), 65 | ); 66 | } 67 | 68 | /** 69 | * Initialize and perform access checks 70 | * 71 | */ 72 | public function init() { 73 | if($this->useLoginHook) { 74 | $this->session->addHookAfter('login', $this, 'loginHook'); 75 | } 76 | } 77 | 78 | /** 79 | * Check for upgrades at login (superuser only) 80 | * 81 | * @param null|HookEvent $e 82 | * 83 | */ 84 | public function loginHook($e = null) { 85 | 86 | if($e) {} // ignored HookEvent 87 | if(!$this->user->isSuperuser()) return; // only show messages to superuser 88 | 89 | $moduleVersions = array(); 90 | $cache = $this->cache; 91 | $cacheName = $this->className() . "_loginHook"; 92 | $cacheData = $cache ? $cache->get($cacheName) : null; 93 | 94 | if(!empty($cacheData) && is_string($cacheData)) $cache = json_decode($cacheData, true); 95 | 96 | $branches = empty($cacheData) ? $this->getCoreBranches(false) : $cacheData['branches']; 97 | if(empty($branches)) return; 98 | $master = $branches['master']; 99 | $branch = null; 100 | $new = version_compare($master['version'], $this->config->version); 101 | 102 | if($new > 0) { 103 | // master is newer than current 104 | $branch = $master; 105 | } else if($new < 0 && isset($branches['dev'])) { 106 | // we will assume dev branch 107 | $dev = $branches['dev']; 108 | $new = version_compare($dev['version'], $this->config->version); 109 | if($new > 0) $branch = $dev; 110 | } 111 | 112 | if($branch) { 113 | $versionStr = "$branch[name] $branch[version]"; 114 | $msg = $this->_('A ProcessWire core upgrade is available') . " ($versionStr)"; 115 | $this->message($msg); 116 | } else { 117 | $this->message($this->_('Your ProcessWire core is up-to-date')); 118 | } 119 | 120 | if($this->config->moduleServiceKey) { 121 | $n = 0; 122 | if(empty($cacheData) || empty($cacheData['moduleVersions'])) { 123 | $moduleVersions = $this->getModuleVersions(true); 124 | } else { 125 | $moduleVersions = $cacheData['moduleVersions']; 126 | } 127 | foreach($moduleVersions as $name => $info) { 128 | $msg = sprintf($this->_('An upgrade for %s is available'), $name) . " ($info[remote])"; 129 | $this->message($msg); 130 | $n++; 131 | } 132 | if(!$n) $this->message($this->_('Your modules are up-to-date')); 133 | } 134 | 135 | if($cache) { 136 | $cacheData = array( 137 | 'branches' => $branches, 138 | 'moduleVersions' => $moduleVersions 139 | ); 140 | $cache->save($cacheName, $cacheData, 43200); // 43200=12hr 141 | } 142 | } 143 | 144 | /** 145 | * Get versions of core or modules 146 | * 147 | * @param bool $refresh 148 | * @return array of array( 149 | * 'ModuleName' => array( 150 | * 'title' => 'Module Title', 151 | * 'local' => '1.2.3', // current installed version 152 | * 'remote' => '1.2.4', // directory version available, or boolean false if not found in directory 153 | * 'new' => true|false, // true if newer version available, false if not 154 | * 'requiresVersions' => array('ModuleName' => array('>', '1.2.3')), // module requirements (for modules only) 155 | * 'branch' => 'master', // branch name (for core only) 156 | * ) 157 | * ) 158 | * 159 | */ 160 | public function getVersions($refresh = false) { 161 | 162 | $versions = array(); 163 | $branches = $this->getCoreBranches(false, $refresh); 164 | 165 | foreach($branches as $branchName => $branch) { 166 | $name = "ProcessWire $branchName"; 167 | $new = version_compare($branch['version'], $this->config->version); 168 | $versions[$name] = array( 169 | 'title' => "ProcessWire Core ($branch[title])", 170 | 'icon' => 'microchip', 171 | 'local' => $this->config->version, 172 | 'remote' => $branch['version'], 173 | 'new' => $new, 174 | 'branch' => $branch['name'], 175 | 'author' => 'ProcessWire', 176 | 'href' => '', 177 | 'summary' => '', 178 | 'urls' => isset($branch['urls']) ? $branch['urls'] : array() 179 | ); 180 | } 181 | 182 | if($this->config->moduleServiceKey) { 183 | foreach($this->getModuleVersions(false, $refresh) as $name => $info) { 184 | $versions[$name] = $info; 185 | } 186 | } 187 | 188 | return $versions; 189 | } 190 | 191 | protected function getModuleInfoVerbose($name) { 192 | $info = $this->modules->getModuleInfoVerbose($name); 193 | return $info; 194 | } 195 | 196 | /** 197 | * Check all site modules for newer versions from the directory 198 | * 199 | * @param bool $onlyNew Only return array of modules with new versions available 200 | * @param bool $refresh 201 | * @return array of array( 202 | * 'ModuleName' => array( 203 | * 'title' => 'Module Title', 204 | * 'local' => '1.2.3', // current installed version 205 | * 'remote' => '1.2.4', // directory version available, or boolean false if not found in directory 206 | * 'new' => true|false, // true if newer version available, false if not 207 | * 'requiresVersions' => array('ModuleName' => array('>', '1.2.3')), // module requirements 208 | * ) 209 | * ) 210 | * @throws WireException 211 | * 212 | */ 213 | public function getModuleVersions($onlyNew = false, $refresh = false) { 214 | 215 | $modules = $this->modules; 216 | $config = $this->config; 217 | 218 | if(!$this->config->moduleServiceKey) { 219 | throw new WireException('Missing /site/config.php: value for $config->moduleServiceKey'); 220 | } 221 | 222 | $url = 223 | $config->moduleServiceURL . 224 | "?apikey=" . $config->moduleServiceKey . 225 | "&version=2" . 226 | "&limit=100" . 227 | "&field=module_version,version,requires,urls" . 228 | "&class_name="; 229 | 230 | $names = array(); 231 | $versions = array(); 232 | 233 | foreach($modules as $module) { 234 | $name = $module->className(); 235 | $info = $this->getModuleInfoVerbose($name); 236 | if($info['core']) continue; 237 | $names[] = $name; 238 | $versions[$name] = array( 239 | 'title' => $info['title'], 240 | 'summary' => empty($info['summary']) ? '' : $info['summary'], 241 | 'icon' => empty($info['icon']) ? '' : $info['icon'], 242 | 'author' => empty($info['author']) ? '' : $info['author'], 243 | 'href' => empty($info['href']) ? '' : $info['href'], 244 | 'local' => $modules->formatVersion($info['version']), 245 | 'remote' => false, 246 | 'new' => 0, 247 | 'requiresVersions' => $info['requiresVersions'], 248 | 'installs' => $info['installs'], 249 | ); 250 | } 251 | 252 | if(!count($names)) return array(); 253 | 254 | ksort($versions); 255 | 256 | $url .= implode(',', $names); 257 | 258 | $data = $refresh ? null : $this->session->getFor($this, 'moduleVersionsData'); 259 | 260 | if(empty($data)) { 261 | // if not cached 262 | $http = new WireHttp(); 263 | $this->wire($http); 264 | $http->setTimeout(self::timeout); 265 | $data = $http->getJSON($url); 266 | $this->session->setFor($this, 'moduleVersionsData', $data); 267 | 268 | if(!is_array($data)) { 269 | $error = $http->getError(); 270 | if(!$error) $error = $this->_('Error retrieving modules directory data'); 271 | $this->error($error . " (" . $this->className() . ")"); 272 | return array(); 273 | } 274 | } 275 | 276 | $newVersions = array(); 277 | $installedBy = array(); // moduleName => installedByModuleName 278 | 279 | foreach($data['items'] as $item) { 280 | $name = $item['class_name']; 281 | if(empty($versions[$name]) || empty($versions[$name]['local'])) continue; 282 | $versions[$name]['remote'] = $item['version']; 283 | $new = version_compare($versions[$name]['remote'], $versions[$name]['local']); 284 | $versions[$name]['new'] = $new; 285 | $versions[$name]['urls'] = $item['urls']; 286 | $versions[$name]['installer'] = ''; 287 | $versions[$name]['pro'] = !empty($item['pro']); 288 | 289 | if(!empty($versions[$name]['installs'])) { 290 | foreach($versions[$name]['installs'] as $installsName) { 291 | $installedBy[$installsName] = $name; 292 | } 293 | } 294 | 295 | if($new <= 0) { 296 | // local is up-to-date or newer than remote 297 | if($onlyNew) unset($versions[$name]); 298 | continue; 299 | } 300 | 301 | // remote is newer than local 302 | $versions[$name]['requiresVersions'] = $item['requires']; 303 | 304 | if($new > 0 && !$onlyNew) { 305 | $newVersions[$name] = $versions[$name]; 306 | unset($versions[$name]); 307 | } 308 | } 309 | 310 | if($onlyNew) { 311 | foreach($versions as $name => $item) { 312 | if($item['remote'] === false) unset($versions[$name]); 313 | } 314 | } else if(count($newVersions)) { 315 | $versions = $newVersions + $versions; 316 | } 317 | 318 | foreach($versions as $name => $item) { 319 | if(!empty($versions[$name]['remote'])) continue; 320 | if(!isset($installedBy[$name])) continue; 321 | $versions[$name]['installer'] = $installedBy[$name]; 322 | $installer = $versions[$installedBy[$name]]; 323 | if(!empty($installer['pro'])) $versions[$name]['pro'] = true; 324 | } 325 | 326 | return $versions; 327 | } 328 | 329 | /** 330 | * Get all available branches with info for each 331 | * 332 | * @param bool $throw Whether or not to throw exceptions on error (default=true) 333 | * @param bool $refresh Specify true to refresh data from web service 334 | * @return array of branches each with: 335 | * - name (string) i.e. dev 336 | * - title (string) i.e. Development 337 | * - zipURL (string) URL to zip download file 338 | * - version (string) i.e. 2.5.0 339 | * - versionURL (string) URL to we pull version from 340 | * @throws WireException 341 | * 342 | */ 343 | public function getCoreBranches($throw = true, $refresh = false) { 344 | 345 | if(!$refresh) { 346 | $branches = $this->session->getFor($this, 'branches'); 347 | if($branches && count($branches)) return $branches; 348 | } 349 | 350 | $branches = array(); 351 | 352 | foreach($this->repos as $repoName => $repo) { 353 | 354 | $http = new WireHttp(); 355 | $this->wire($http); 356 | $http->setTimeout(self::timeout); 357 | $http->setHeader('User-Agent', 'ProcessWireUpgrade'); 358 | $json = $http->get($repo['branchesURL']); 359 | 360 | $loadError = $this->_('Error loading GitHub branches') . ' - ' . $repo['branchesURL']; 361 | 362 | if(!$json) { 363 | $error = $loadError; 364 | $error .= ' - ' . $this->_('HTTP error(s):') . ' ' . $http->getError(); 365 | $error .= ' - ' . $this->_('Check that HTTP requests are not blocked by your server.'); 366 | if($throw) throw new WireException($error); 367 | $this->error($error); 368 | return array(); 369 | } 370 | 371 | $data = json_decode($json, true); 372 | 373 | if(!$data) { 374 | $error = $loadError; 375 | if($throw) throw new WireException($error); 376 | $this->error($error); 377 | return array(); 378 | } 379 | 380 | foreach($data as $key => $info) { 381 | 382 | if(empty($info['name'])) continue; 383 | 384 | $name = $info['name']; 385 | 386 | $branch = array( 387 | 'name' => $name, 388 | 'title' => ucfirst($name), 389 | 'zipURL' => str_replace('{branch}', $name, $repo['zipURL']), 390 | 'version' => '', 391 | 'versionURL' => str_replace('{branch}', $name, $repo['versionURL']), 392 | ); 393 | 394 | $content = $http->get($branch['versionURL']); 395 | if(!preg_match_all('/const\s+version(Major|Minor|Revision)\s*=\s*(\d+)/', $content, $matches)) { 396 | $branch['version'] = '?'; 397 | continue; 398 | } 399 | 400 | $version = array(); 401 | foreach($matches[1] as $k => $var) { 402 | $version[$var] = (int) $matches[2][$k]; 403 | } 404 | 405 | $branch['version'] = "$version[Major].$version[Minor].$version[Revision]"; 406 | 407 | $url = 'https://github.com/processwire/processwire'; 408 | $branch['urls'] = array( 409 | 'repo' => ($name === 'dev' ? "$url/tree/dev" : $url), 410 | 'download' => "$url/archive/refs/heads/$name.zip", 411 | 'support' => 'https://processwire.com/talk/', 412 | ); 413 | 414 | if($repoName != 'main') { 415 | $name = "$repoName-$name"; 416 | $branch['name'] = $name; 417 | $branch['title'] = ucfirst($repoName) . "/$branch[title]"; 418 | } 419 | 420 | $branches[$name] = $branch; 421 | } 422 | } 423 | 424 | $this->session->setFor($this, 'branches', $branches); 425 | 426 | return $branches; 427 | } 428 | 429 | } 430 | 431 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProcessWire Upgrade 2 | 3 | Provides core and module upgrade notifications and optionally 4 | installation from the admin. 5 | 6 | Can be used to upgrade your ProcessWire core or any module that 7 | is available from . 8 | 9 | Requires ProcessWire 3.x 10 | 11 | ## Please note before using this tool 12 | 13 | Files installed by this tool are readable and writable by Apache, 14 | which may be a security problem in some hosting environments. 15 | Especially shared hosting environments where Apache runs as the 16 | same user across all hosting accounts. If in doubt, you should 17 | instead install core upgrades and/or modules and module upgrades 18 | manually through your hosting account (FTP, SSH, etc.), which 19 | is already very simple to do. This ensures that any installed files 20 | are owned and writable by your user account rather than Apache. 21 | 22 | Even if you don't use this tool to install the upgrades, this tool 23 | is still useful in identifying when upgrades are available. 24 | 25 | ## Core Upgrades 26 | 27 | This tool checks if upgrades are available for your ProcessWire installation. 28 | If available, it will download the update. If your file system is 29 | writable, it will install the update for you. If your file system is 30 | not writable, then it will install upgrade files in a writable 31 | location (under /site/assets/cache/) and give you instructions on 32 | what files to move. 33 | 34 | Options to upgrade from the master or dev branch are available. 35 | 36 | This tool makes versioned backup copies of any files it 37 | overwrites during the upgrade. Should an upgrade fail for some 38 | reason, you can manually restore from the backups should you 39 | need to. 40 | 41 | After installing a core upgrade, you may want to manually update 42 | the permissions of installed files to be non-writable to Apache, 43 | depending on your environment. 44 | 45 | 46 | ## Module Upgrades 47 | 48 | Uses web services from modules.processwire.com to compare your 49 | current installed versions of modules to the latest remote 50 | versions available. Provides upgrade links when it finds newer 51 | versions of modules you have installed. 52 | 53 | After installing module upgrades, you may want to manually update 54 | the permissions of installed files to be non-writable to Apache, 55 | depending on your environment. 56 | 57 | 58 | --------------------------------------------------------------------------------