├── 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
" : "$str
"; } 923 | public function a($href, $label, $class = '') { return ($class ? "sanitizer->entities($href) . "'>$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 "$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