Issues (1176)

admin_site_upgrade.php (3 issues)

1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2019 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
namespace Fisharebest\Webtrees;
17
18
use Exception;
19
use Fisharebest\Webtrees\Controller\PageController;
20
use Fisharebest\Webtrees\Functions\Functions;
21
use Fisharebest\Webtrees\Functions\FunctionsDate;
22
use PclZip;
23
24
define('WT_SCRIPT_NAME', 'admin_site_upgrade.php');
25
26
require './includes/session.php';
27
28
// Check for updates
29
$latest_version_txt = Functions::fetchLatestVersion();
30
if (preg_match('/^[0-9.]+\|[0-9.]+\|/', $latest_version_txt)) {
0 ignored issues
show
It seems like $latest_version_txt can also be of type null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

30
if (preg_match('/^[0-9.]+\|[0-9.]+\|/', /** @scrutinizer ignore-type */ $latest_version_txt)) {
Loading history...
31
    list($latest_version, $earliest_version, $download_url) = explode('|', $latest_version_txt);
0 ignored issues
show
It seems like $latest_version_txt can also be of type null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

31
    list($latest_version, $earliest_version, $download_url) = explode('|', /** @scrutinizer ignore-type */ $latest_version_txt);
Loading history...
32
} else {
33
    // Cannot determine the latest version
34
    list($latest_version, $earliest_version, $download_url) = explode('|', '||');
35
}
36
37
$latest_version_html = '<span dir="ltr">' . $latest_version . '</span>';
38
39
// Show a friendly message while the site is being upgraded
40
$lock_file           = __DIR__ . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'offline.txt';
41
$lock_file_text      = I18N::translate('This website is being upgraded. Try again in a few minutes.') . PHP_EOL . FunctionsDate::formatTimestamp(WT_TIMESTAMP) . /* I18N: Timezone - http://en.wikipedia.org/wiki/UTC */ I18N::translate('UTC');
42
43
// Success/failure indicators
44
$icon_success        = '<i class="icon-yes"></i>';
45
$icon_failure        = '<i class="icon-failure"></i>';
46
47
// Need confirmation for various actions
48
$continue            = Filter::post('continue', '1') && Filter::checkCsrf();
49
$modules_action      = Filter::post('modules', 'ignore|disable');
50
$themes_action       = Filter::post('themes', 'ignore|disable');
51
52
$controller = new PageController;
53
$controller
54
    ->restrictAccess(Auth::isAdmin())
55
    ->setPageTitle(I18N::translate('Upgrade wizard'))
56
    ->pageHeader();
57
58
echo '<h1>', $controller->getPageTitle(), '</h1>';
59
60
if ($latest_version == '') {
61
    echo '<p>', I18N::translate('No upgrade information is available.'), '</p>';
62
63
    return;
64
}
65
66
if (version_compare(WT_VERSION, $latest_version) >= 0) {
67
    echo '<p>', I18N::translate('This is the latest version of webtrees. No upgrade is available.'), '</p>';
68
69
    return;
70
}
71
72
echo '<form method="post" action="admin_site_upgrade.php">';
73
echo Filter::getCsrf();
74
75
if ($continue) {
76
    echo '<input type="hidden" name="continue" value="1">';
77
    echo '<p>', I18N::translate('It can take several minutes to download and install the upgrade. Be patient.'), '</p>';
78
} else {
79
    echo '<p>', I18N::translate('A new version of webtrees is available.'), '</p>';
80
    echo '<p>', I18N::translate('Depending on your server configuration, you may be able to upgrade automatically.'), '</p>';
81
    echo '<p>', I18N::translate('It can take several minutes to download and install the upgrade. Be patient.'), '</p>';
82
    echo '<button type="submit" name="continue" value="1">', /* I18N: %s is a version number, such as 1.2.3 */ I18N::translate('Upgrade to webtrees %s.', $latest_version_html), '</button>';
83
    echo '</form>';
84
85
    return;
86
}
87
88
echo '<ul>';
89
90
////////////////////////////////////////////////////////////////////////////////
91
// Cannot upgrade until pending changes are accepted/rejected
92
////////////////////////////////////////////////////////////////////////////////
93
94
echo '<li>', /* I18N: The system is about to… */ I18N::translate('Check for pending changes…');
95
96
$changes = Database::prepare("SELECT 1 FROM `##change` WHERE status='pending' LIMIT 1")->fetchOne();
97
98
if ($changes) {
99
    echo '<br>', I18N::translate('You should accept or reject all pending changes before upgrading.'), $icon_failure;
100
    echo '<br><button onclick="window.open(\'edit_changes.php\',\'_blank\', chan_window_specs); return false;"">', I18N::translate('Pending changes'), '</button>';
101
    echo '</li></ul></form>';
102
103
    return;
104
} else {
105
    echo '<br>', I18N::translate('There are no pending changes.'), $icon_success;
106
}
107
108
echo '</li>';
109
110
////////////////////////////////////////////////////////////////////////////////
111
// Custom modules may not work with the new version.
112
////////////////////////////////////////////////////////////////////////////////
113
114
echo '<li>', /* I18N: The system is about to [...] */ I18N::translate('Check for custom modules…');
115
116
$custom_modules = false;
117
foreach (Module::getInstalledModules('disabled') as $module) {
118
    if (!in_array($module->getName(), Module::getCoreModuleNames())) {
119
        switch ($modules_action) {
120
            case 'disable':
121
                Database::prepare(
122
                "UPDATE `##module` SET status = 'disabled' WHERE module_name = ?"
123
                )->execute(array($module->getName()));
124
                break;
125
            case 'ignore':
126
                echo '<br>', I18N::translate('Custom module'), ' — ', WT_MODULES_DIR, $module->getName(), ' — ', $module->getTitle(), $icon_success;
127
                break;
128
            default:
129
                echo '<br>', I18N::translate('Custom module'), ' — ', WT_MODULES_DIR, $module->getName(), ' — ', $module->getTitle(), $icon_failure;
130
                $custom_modules = true;
131
                break;
132
        }
133
    }
134
}
135
if ($custom_modules) {
136
    echo '<br>', I18N::translate('You should consult the module’s author to confirm compatibility with this version of webtrees.');
137
    echo '<br>', '<button type="submit" name="modules" value="disable">', I18N::translate('Disable these modules'), '</button> — ', I18N::translate('You can re-enable these modules after the upgrade.');
138
    echo '<br>', '<button type="submit" name="modules" value="ignore">', /* I18N: Ignore the warnings, and… */ I18N::translate('Upgrade anyway'), '</button> — ', I18N::translate('Caution: old modules may not work, or they may prevent webtrees from working.');
139
    echo '</li></ul></form>';
140
141
    return;
142
} else {
143
    if ($modules_action != 'ignore') {
144
        echo '<br>', I18N::translate('No custom modules are enabled.'), $icon_success;
145
    }
146
    echo '<input type="hidden" name="modules" value="', Filter::escapeHtml($modules_action), '">';
147
}
148
149
echo '</li>';
150
151
////////////////////////////////////////////////////////////////////////////////
152
// Custom themes may not work with the new version.
153
////////////////////////////////////////////////////////////////////////////////
154
155
echo '<li>', /* I18N: The system is about to… */ I18N::translate('Check for custom themes…');
156
157
$custom_themes = false;
158
foreach (Theme::themeNames() as $theme_id => $theme_name) {
159
    switch ($theme_id) {
160
        case 'clouds':
161
        case 'colors':
162
        case 'fab':
163
        case 'minimal':
164
        case 'webtrees':
165
        case 'xenea':
166
            break;
167
        default:
168
            $theme_used = Database::prepare(
169
            "SELECT EXISTS (SELECT 1 FROM `##site_setting`   WHERE setting_name='THEME_DIR' AND setting_value=?)" .
170
            " OR    EXISTS (SELECT 1 FROM `##gedcom_setting` WHERE setting_name='THEME_DIR' AND setting_value=?)" .
171
            " OR    EXISTS (SELECT 1 FROM `##user_setting`   WHERE setting_name='theme'     AND setting_value=?)"
172
            )->execute(array($theme_id, $theme_id, $theme_id))->fetchOne();
173
            if ($theme_used) {
174
                switch ($themes_action) {
175
                    case 'disable':
176
                        Database::prepare(
177
                            "DELETE FROM `##site_setting`   WHERE setting_name = 'THEME_DIR' AND setting_value = ?"
178
                            )->execute(array($theme_id));
179
                            Database::prepare(
180
                            "DELETE FROM `##gedcom_setting` WHERE setting_name = 'THEME_DIR' AND setting_value = ?"
181
                            )->execute(array($theme_id));
182
                            Database::prepare(
183
                            "DELETE FROM `##user_setting`   WHERE setting_name = 'theme'     AND setting_value = ?"
184
                            )->execute(array($theme_id));
185
                        break;
186
                    case 'ignore':
187
                        echo '<br>', I18N::translate('Custom theme'), ' — ', $theme_id, ' — ', $theme_name, $icon_success;
188
                        break;
189
                    default:
190
                        echo '<br>', I18N::translate('Custom theme'), ' — ', $theme_id, ' — ', $theme_name, $icon_failure;
191
                        $custom_themes = true;
192
                    break;
193
                }
194
            }
195
            break;
196
    }
197
}
198
199
if ($custom_themes) {
200
    echo '<br>', I18N::translate('You should consult the theme’s author to confirm compatibility with this version of webtrees.');
201
    echo '<br>', '<button type="submit" name="themes" value="disable">', I18N::translate('Disable these themes'), '</button> — ', I18N::translate('You can re-enable these themes after the upgrade.');
202
    echo '<br>', '<button type="submit" name="themes" value="ignore">', I18N::translate('Upgrade anyway'), '</button> — ', I18N::translate('Caution: old themes may not work, or they may prevent webtrees from working.');
203
    echo '</li></ul></form>';
204
205
    return;
206
} else {
207
    if ($themes_action != 'ignore') {
208
        echo '<br>', I18N::translate('No custom themes are enabled.'), $icon_success;
209
    }
210
    echo '<input type="hidden" name="themes" value="', Filter::escapeHtml($themes_action), '">';
211
}
212
213
echo '</li>';
214
215
////////////////////////////////////////////////////////////////////////////////
216
// Make a backup of genealogy data
217
////////////////////////////////////////////////////////////////////////////////
218
219
echo '<li>', /* I18N: The system is about to… */ I18N::translate('Export all the family trees to GEDCOM files…');
220
221
foreach (Tree::getAll() as $tree) {
222
    reset_timeout();
223
    $filename = WT_DATA_DIR . $tree->getName() . date('-Y-m-d') . '.ged';
224
225
    try {
226
        // To avoid partial trees on timeout/diskspace/etc, write to a temporary file first
227
        $stream = fopen($filename . '.tmp', 'w');
228
        $tree->exportGedcom($stream);
229
        fclose($stream);
230
        rename($filename . '.tmp', $filename);
231
        echo '<br>', I18N::translate('The family tree has been exported to %s.', Html::filename($filename)), $icon_success;
232
    } catch (\ErrorException $ex) {
233
        echo '<br>', I18N::translate('The file %s could not be created.', Html::filename($filename)), $icon_failure;
234
    }
235
}
236
237
echo '</li>';
238
239
////////////////////////////////////////////////////////////////////////////////
240
// Download a .ZIP file containing the new code
241
////////////////////////////////////////////////////////////////////////////////
242
243
echo '<li>', /* I18N: The system is about to…; %s is a URL. */ I18N::translate('Download %s…', Html::filename($download_url));
244
245
$zip_file   = WT_DATA_DIR . basename($download_url);
246
$zip_dir    = WT_DATA_DIR . basename($download_url, '.zip');
247
$zip_stream = fopen($zip_file, 'w');
248
reset_timeout();
249
$start_time = microtime(true);
250
File::fetchUrl($download_url, $zip_stream);
251
$end_time   = microtime(true);
252
$zip_size   = filesize($zip_file);
253
fclose($zip_stream);
254
255
echo '<br>', /* I18N: %1$s is a number of KB, %2$s is a (fractional) number of seconds */ I18N::translate('%1$s KB were downloaded in %2$s seconds.', I18N::number($zip_size / 1024), I18N::number($end_time - $start_time, 2));
256
if ($zip_size) {
257
    echo $icon_success;
258
} else {
259
    echo $icon_failure;
260
    // Guess why we might have failed...
261
    if (preg_match('/^https:/', $download_url) && !in_array('ssl', stream_get_transports())) {
262
        echo '<br>', /* I18N: http://en.wikipedia.org/wiki/Https */ I18N::translate('This server does not support secure downloads using HTTPS.');
263
    }
264
}
265
266
echo '</li>';
267
268
////////////////////////////////////////////////////////////////////////////////
269
// Unzip the file - this checks we have enough free disk space, that the .zip
270
// file is valid, etc.
271
////////////////////////////////////////////////////////////////////////////////
272
273
echo '<li>', /* I18N: The system is about to…; %s is a .ZIP file. */ I18N::translate('Unzip %s to a temporary folder…', Html::filename(basename($download_url)));
274
275
File::delete($zip_dir);
276
File::mkdir($zip_dir);
277
278
$archive = new PclZip($zip_file);
279
280
$res = $archive->properties();
281
if (!is_array($res) || $res['status'] != 'ok') {
282
    echo '<br>', I18N::translate('An error occurred when unzipping the file.'), $icon_failure;
283
    echo '<br>', $archive->errorInfo(true);
284
    echo '</li></ul></form>';
285
286
    return;
287
}
288
289
$num_files = $res['nb'];
290
291
reset_timeout();
292
$start_time = microtime(true);
293
$res        = $archive->extract(
294
    \PCLZIP_OPT_PATH, $zip_dir,
295
    \PCLZIP_OPT_REMOVE_PATH, 'webtrees',
296
    \PCLZIP_OPT_REPLACE_NEWER
297
);
298
$end_time = microtime(true);
299
300
if (is_array($res)) {
301
    foreach ($res as $result) {
302
        // Note that we're stripping the initial "webtrees/", so the top folder will fail.
303
        if ($result['status'] != 'ok' && $result['filename'] != 'webtrees/') {
304
            echo '<br>', I18N::translate('An error occurred when unzipping the file.'), $icon_failure;
305
            echo '<pre>', $result['status'], '</pre>';
306
            echo '<pre>', $result['filename'], '</pre>';
307
            echo '</li></ul></form>';
308
309
            return;
310
        }
311
    }
312
    echo '<br>', /* I18N: …from the .ZIP file, %2$s is a (fractional) number of seconds */ I18N::plural('%1$s file was extracted in %2$s seconds.', '%1$s files were extracted in %2$s seconds.', count($res), count($res), I18N::number($end_time - $start_time, 2)), $icon_success;
313
} else {
314
    echo '<br>', I18N::translate('An error occurred when unzipping the file.'), $icon_failure;
315
    echo '<pre>', $archive->errorInfo(true), '</pre>';
316
    echo '</li></ul></form>';
317
318
    return;
319
}
320
321
echo '</li>';
322
323
////////////////////////////////////////////////////////////////////////////////
324
// This is it - take the site offline first
325
////////////////////////////////////////////////////////////////////////////////
326
327
echo '<li>', /* I18N: The system is about to… */ I18N::translate('Check file permissions…');
328
329
reset_timeout();
330
$iterator = new \RecursiveDirectoryIterator($zip_dir);
331
$iterator->setFlags(\RecursiveDirectoryIterator::SKIP_DOTS);
332
foreach (new \RecursiveIteratorIterator($iterator) as $file) {
333
    $file = WT_ROOT . substr($file, strlen($zip_dir) + 1);
334
    if (file_exists($file) && (!is_readable($file) || !is_writable($file))) {
335
        echo '<br>', I18N::translate('The file %s could not be updated.', Html::filename($file)), $icon_failure;
336
        echo '</li></ul>';
337
        echo '<p class="error">', I18N::translate('To complete the upgrade, you should install the files manually.'), '</p>';
338
        echo '<p>', I18N::translate('The new files are currently located in the folder %s.', Html::filename($zip_dir)), '</p>';
339
        echo '<p>', I18N::translate('Copy these files to the folder %s, replacing any that have the same name.', Html::filename(WT_ROOT)), '</p>';
340
        echo '<p>', I18N::translate('To prevent visitors from accessing the website while you are in the middle of copying files, you can temporarily create a file %s on the server. If it contains a message, it will be displayed to visitors.', Html::filename($lock_file)), '</p>';
341
342
        return;
343
    }
344
}
345
346
echo '<br>', I18N::translate('All files have read and write permission.'), $icon_success;
347
348
echo '</li>';
349
350
////////////////////////////////////////////////////////////////////////////////
351
// This is it - take the site offline first
352
////////////////////////////////////////////////////////////////////////////////
353
354
echo '<li>', I18N::translate('Place the website offline, by creating the file %s…', $lock_file);
355
356
try {
357
    file_put_contents($lock_file, $lock_file_text);
358
    echo '<br>', I18N::translate('The file %s has been created.', Html::filename($lock_file)), $icon_success;
359
} catch (\ErrorException $ex) {
360
    echo '<br>', I18N::translate('The file %s could not be created.', Html::filename($lock_file)), $icon_failure;
361
}
362
363
echo '</li>';
364
365
////////////////////////////////////////////////////////////////////////////////
366
// Copy files
367
////////////////////////////////////////////////////////////////////////////////
368
369
echo '<li>', /* I18N: The system is about to… */ I18N::translate('Copy files…');
370
371
// The wiki tells people how to customize webtrees by modifying various files.
372
// Create a backup of these, just in case the user forgot!
373
try {
374
    copy('app/GedcomCode/GedcomCode/Rela.php', WT_DATA_DIR . 'GedcomCodeRela' . date('-Y-m-d') . '.php');
375
    copy('app/GedcomTag.php', WT_DATA_DIR . 'GedcomTag' . date('-Y-m-d') . '.php');
376
} catch (\ErrorException $ex) {
377
    // No problem if we cannot do this.
378
}
379
380
reset_timeout();
381
$start_time = microtime(true);
382
$res        = $archive->extract(
383
    \PCLZIP_OPT_PATH, WT_ROOT,
384
    \PCLZIP_OPT_REMOVE_PATH, 'webtrees',
385
    \PCLZIP_OPT_REPLACE_NEWER
386
);
387
$end_time = microtime(true);
388
389
if (is_array($res)) {
390
    foreach ($res as $result) {
391
        // Note that most of the folders will already exist, so it is not an error if we cannot create them
392
        if ($result['status'] != 'ok' && !substr($result['filename'], -1) == '/') {
393
            echo '<br>', I18N::translate('The file %s could not be created.', Html::filename($result['filename'])), $icon_failure;
394
        }
395
    }
396
    echo '<br>', /* I18N: …from the .ZIP file, %2$s is a (fractional) number of seconds */ I18N::plural('%1$s file was extracted in %2$s seconds.', '%1$s files were extracted in %2$s seconds.', count($res), count($res), I18N::number($end_time - $start_time, 2)), $icon_success;
397
} else {
398
    echo '<br>', I18N::translate('An error occurred when unzipping the file.'), $icon_failure;
399
    echo '</li></ul></form>';
400
401
    return;
402
}
403
404
echo '</li>';
405
406
////////////////////////////////////////////////////////////////////////////////
407
// All done - put the site back online
408
////////////////////////////////////////////////////////////////////////////////
409
410
echo '<li>', I18N::translate('Place the website online, by deleting the file %s…', Html::filename($lock_file));
411
412
if (File::delete($lock_file)) {
413
    echo '<br>', I18N::translate('The file %s has been deleted.', Html::filename($lock_file)), $icon_success;
414
} else {
415
    echo '<br>', I18N::translate('The file %s could not be deleted.', Html::filename($lock_file)), $icon_failure;
416
}
417
418
echo '</li>';
419
420
////////////////////////////////////////////////////////////////////////////////
421
// Clean up
422
////////////////////////////////////////////////////////////////////////////////
423
424
echo '<li>', /* I18N: The system is about to… */ I18N::translate('Delete temporary files…');
425
426
reset_timeout();
427
if (File::delete($zip_dir)) {
428
    echo '<br>', I18N::translate('The folder %s has been deleted.', Html::filename($zip_dir)), $icon_success;
429
} else {
430
    echo '<br>', I18N::translate('The folder %s could not be deleted.', Html::filename($zip_dir)), $icon_failure;
431
}
432
433
if (File::delete($zip_file)) {
434
    echo '<br>', I18N::translate('The file %s has been deleted.', Html::filename($zip_file)), $icon_success;
435
} else {
436
    echo '<br>', I18N::translate('The file %s could not be deleted.', Html::filename($zip_file)), $icon_failure;
437
}
438
439
echo '</li>';
440
echo '</ul>';
441
442
// We have updated the language files.
443
foreach (glob(WT_DATA_DIR . 'cache/language-*') as $file) {
444
    File::delete($file);
445
}
446
447
echo '<p>', I18N::translate('The upgrade is complete.'), '</p>';
448
449
echo '<p><a href="index.php" class="btn btn-primary">', I18N::translate('continue'), '</a></p>';
450
451
/**
452
 * Reset the time limit, as timeouts in this script could leave the upgrade incomplete.
453
 */
454
function reset_timeout()
455
{
456
    if (!ini_get('safe_mode') && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
457
        try {
458
            set_time_limit(ini_get('max_execution_time'));
0 ignored issues
show
ini_get('max_execution_time') of type string is incompatible with the type integer expected by parameter $seconds of set_time_limit(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

458
            set_time_limit(/** @scrutinizer ignore-type */ ini_get('max_execution_time'));
Loading history...
459
        } catch (Exception $ex) {
460
            // "set_time_limt(): Cannot set max execution time limit due to system policy"
461
        }
462
    }
463
}
464