1<?php
2
3require_once dirname(__FILE__).'/accesscheck.php';
4
5if (!$GLOBALS['commandline']) {
6    @ob_end_flush();
7} else {
8    //# when on cl, doit immediately
9    $_GET['doit'] = 'yes';
10}
11$force = isset($cline['f']) || isset($_GET['force']);
12
13function output($message)
14{
15    if ($GLOBALS['commandline']) {
16        @ob_end_clean();
17        echo strip_tags($message)."\n";
18        ob_start();
19    } else {
20        echo $message;
21        // output some stuff to make sure it's not buffered in the browser, hmm, would be nice to find a better way for this
22        for ($i = 0; $i < 10000; ++$i) {
23            echo '  '."\n";
24        }
25        flush();
26        @ob_end_flush();
27    }
28    flush();
29}
30
31output("");
32$dbversion = getConfig('version');
33$releaseDBversion = getConfig('releaseDBversion'); // release version check
34$inUpgrade = getConfig('in-upgrade-to');
35if (!empty($inUpgrade) && $inUpgrade == VERSION) {
36    if (!$force) {
37        if ($GLOBALS['commandline']) {
38            output(s('Another process is already upgrading this installation. Use -f to force upgrade, but only if you are sure the other process is no longer active.'));
39        } else {
40            output(s('Another process is already upgrading this installation. Click %s to force upgrade, but only if you are sure the other process is no longer active.', PageLinkButton('upgrade&doit=yes&force=1', s('Force Upgrade'))));
41        }
42
43        return;
44    }
45}
46
47if (!$dbversion) {
48    $dbversion = 'Older than 1.4.1';
49}
50output('<p class="information">'.$GLOBALS['I18N']->get('Your database version').': '.$dbversion.'</p>');
51
52if ($GLOBALS['database_module'] == 'mysql.inc') {
53    echo Warn(s('Please edit your config file and change "mysql.inc" to "mysqli.inc" to avoid future PHP incompatibility').
54        resourceLink('http://resources.phplist.com/system/mysql-mysqli-update')
55    );
56}
57
58if (!empty($GLOBALS['mysql_database_engine'])) {
59  $engines_count = Sql_Fetch_Row_Query(sprintf('select count(table_name) from information_schema.tables where engine != \'%s\' and table_schema = \'%s\'',$GLOBALS['mysql_database_engine'],$GLOBALS['database_name']));
60  if (!empty($engines_count[0])) {
61    if ($GLOBALS['commandline']) {
62      cl_output(s('Converting tables to preferred database engine'));
63      $engines = Sql_Query(sprintf('select table_name from information_schema.tables where engine != \'%s\' and table_schema = \'%s\'',$GLOBALS['mysql_database_engine'],$GLOBALS['database_name']));
64      while ($engine = Sql_Fetch_Assoc($engines)) {
65        cl_output(s('Converting table %s',$engine['table_name']));
66        Sql_Query(sprintf('alter table %s Engine %s',$engine['table_name'],$GLOBALS['mysql_database_engine']));
67      }
68    } else {
69      echo Warn(s('You have %d tables that do not use your preferred database engine',$engines_count[0]).'<br/>'.s('Use the commandline upgrade method to convert them'));
70    }
71  }
72}
73
74if (!versionCompare($dbversion,'2.11.11') && $dbversion!=='dev') {
75    Fatal_Error(s('Your version is older than 3.2.0 and cannot be upgraded to this version. Please upgrade to 3.2.0 first and then try again.'));
76    return;
77}
78
79// only action upgrade if necessary
80if ($force && $dbversion == VERSION  && defined('RELEASEDATE') && RELEASEDATE <= $releaseDBversion) {
81    output(s('Your database is already the correct version (%s), including release date version (%s), there is no need to upgrade',$dbversion, $releaseDBversion));
82    clearMaintenanceMode();
83   unset($_GET['doit']);
84}
85
86if ($dbversion == VERSION && !$force) {
87    output($GLOBALS['I18N']->get('Your database is already the correct version, there is no need to upgrade'));
88
89    echo '<p>'.PageLinkAjax('upgrade&update=tlds', s('update Top Level Domains'), '', 'button').'</p>';
90    clearMaintenanceMode();
91
92    echo subscribeToAnnouncementsForm();
93} elseif (isset($_GET['doit']) && $_GET['doit'] == 'yes') {
94    $success = 1;
95    // once we are off, this should not be interrupted
96
97    // mark the system to be in maintenance mode
98    setMaintenanceMode(s('Upgrading phpList to version '.VERSION));
99
100    // force send and other processes to stop
101    Sql_Query(sprintf('delete from %s ',$GLOBALS['tables']['sendprocess']));
102
103    ignore_user_abort(1);
104    // rename tables if we are using the prefix
105    include dirname(__FILE__).'/structure.php';
106    foreach ($DBstruct as $table => $value) {
107        set_time_limit(500);
108        if (isset($table_prefix)) {
109            if (Sql_Table_exists($table) && !Sql_Table_Exists($tables[$table])) {
110                Sql_Verbose_Query("alter table $table rename $tables[$table]", 1);
111            }
112        }
113    }
114    @ob_end_flush();
115    @ob_start();
116
117    output('<p class="information">'.$GLOBALS['I18N']->get('Please wait, upgrading your database, do not interrupt').'</p>');
118
119    flush();
120
121    if (preg_match('/(.*?)-/', $dbversion, $regs)) {
122        $dbversion = $regs[1];
123    }
124
125    //# lock this process
126    SaveConfig('in-upgrade-to', VERSION, 1);
127
128    if (version_compare($dbversion, '3.3.4','<')) {
129
130        Sql_Query("alter table {$GLOBALS['tables']['bounce']} modify data mediumblob ");
131
132        $indexesToRecreate = array(
133            'urlcache' => array(
134                'urlindex' => array('value' => 'url(255)', 'unique' => false),
135            ),
136            'linktrack_forward' => array(
137                'urlindex' => array('value' => 'url(255)', 'unique' => false),
138                'urlunique' => array('value' => 'urlhash', 'unique' => true),
139            ),
140            'bounceregex' =>  array(
141                'regex' => array('value' => 'regexhash', 'unique'=> true),
142            ),
143        );
144
145        $tablesToAlter = array(
146            'urlcache' => array('url'),
147            'linktrack_forward' => array('url'),
148            'bounceregex' => array('regex'),
149        );
150
151        //add columns for hash values
152
153        Sql_Query("alter table {$GLOBALS['tables']['linktrack_forward']} add  urlhash char(32) ");
154        Sql_Query("alter table {$GLOBALS['tables']['bounceregex']} add  regexhash char(32) ");
155
156        // add hash values
157
158        Sql_Query("update {$GLOBALS['tables']['linktrack_forward']} set urlhash = md5(url) where urlhash is NULL ");
159        Sql_Query("update {$GLOBALS['tables']['bounceregex']} set regexhash = md5(regex) where regexhash is NULL ");
160
161
162
163        foreach($indexesToRecreate as $table => $indexes) {
164
165
166            foreach($indexes as $indexName => $settings) {
167
168                $exists = table_index_exists($GLOBALS['tables'][$table],$indexName);
169                 if ($exists) {
170                     Sql_Query("drop index $indexName on {$GLOBALS['tables'][$table]} ");
171                 }
172            }
173
174            $alteringOperations = $tablesToAlter[$table];
175            foreach($alteringOperations as $operation) {
176                Sql_Query("alter table {$GLOBALS['tables'][$table]} modify $operation varchar(2083) ");
177            }
178
179            foreach($indexes as $indexName => $settings) {
180                $createStmt = '';
181                if($settings['unique'] === true) {
182                    $createStmt = 'create unique index';
183                } else {
184                    $createStmt = 'create index';
185                }
186
187                Sql_Query("$createStmt $indexName on {$GLOBALS['tables'][$table]}({$settings['value']})");
188            }
189
190        }
191    }
192
193    // Update jQuery version referenced in public page HTML stored in the database
194    if (version_compare($dbversion, '3.4.1', '<')) {
195
196        // The new filename does not hard-code the jQuery version number
197        $replacement = "jquery.min.js";
198
199        // Replace jQuery version public page footers in config table
200        $oldConfigFooter = getConfig('pagefooter');
201        $matches = null;
202
203        // Find and replace all references to version-specific jQuery files
204        preg_match('/jquery-3.3.1.min.js/', $oldConfigFooter, $matches);
205        if ($matches[0] == "jquery-3.3.1.min.js") {
206            $pattern = "jquery-3.3.1.min.js";
207        } else {
208            $pattern = "jquery-1.12.1.min.js";
209        }
210
211        $newConfigFooter = str_replace($pattern, $replacement, $oldConfigFooter);
212        SaveConfig('pagefooter', $newConfigFooter);
213
214        //Replace jQuery version for each subscribe page data.
215        $req = Sql_Query(sprintf('select data from %s where name = "footer"', $GLOBALS['tables']['subscribepage_data']));
216        $footersArray = array();
217        while ($row = Sql_Fetch_Assoc($req)) {
218            $footersArray[] = $row['data'];
219        }
220
221        // Find and replace all references to version-specific jQuery files
222        foreach ($footersArray as $key => $value) {
223            preg_match('/jquery-3.3.1.min.js/', $value, $matches);
224            if ($matches[0] == "jquery-3.3.1.min.js") {
225                $pattern = "jquery-3.3.1.min.js";
226            } else {
227                $pattern = "jquery-1.12.1.min.js";
228            }
229            $newFooter = str_replace($pattern, $replacement, $value);
230            Sql_Query(sprintf('update %s set data = "%s" where data = "%s" ', $GLOBALS['tables']['subscribepage_data'], sql_escape($newFooter), addslashes($value)));
231        }
232    }
233    //# remember whether we've done this, to avoid doing it every time
234    //# even thought that's not such a big deal
235    $isUTF8 = getConfig('UTF8converted');
236
237    if (empty($isUTF8)) {
238        $maxsize = 0;
239        $req = Sql_Query('select (data_length+index_length) tablesize
240      from information_schema.tables
241      where table_schema="' .$GLOBALS['database_name'].'"');
242
243        while ($row = Sql_Fetch_Assoc($req)) {
244            if ($row['tablesize'] > $maxsize) {
245                $maxsize = $row['tablesize'];
246            }
247        }
248        $maxsize = (int) ($maxsize * 1.2); //# add another 20%
249        $row = Sql_Fetch_Row_Query('select @@datadir');
250        $dataDir = $row[0];
251        $avail = disk_free_space($dataDir);
252
253        //# convert to UTF8
254        $dbname = $GLOBALS['database_name'];
255        if ($maxsize < $avail && !empty($dbname)) {
256            //# the conversion complains about a key length
257            Sql_Query(sprintf('alter table '.$GLOBALS['tables']['user_blacklist_data'].' change column email email varchar(150) not null unique'));
258
259            $req = Sql_Query('select * from information_schema.columns where table_schema = "'.$dbname.'" and CHARACTER_SET_NAME != "utf8"');
260
261            $dbcolumns = array();
262            $dbtables = array();
263            while ($row = Sql_Fetch_Assoc($req)) {
264                //# make sure to only change our own tables, in case we share with other applications
265                if (in_array($row['TABLE_NAME'], array_values($GLOBALS['tables']))) {
266                    $dbcolumns[] = $row;
267                    $dbtables[$row['TABLE_NAME']] = $row['TABLE_NAME'];
268                }
269            }
270
271            Sql_Query('use '.$dbname);
272
273            output($GLOBALS['I18N']->get('Upgrading the database to use UTF-8, please wait').'<br/>');
274            foreach ($dbtables as $dbtable) {
275                set_time_limit(3600);
276                output($GLOBALS['I18N']->get('Upgrading table ').' '.$dbtable.'<br/>');
277                Sql_Query(sprintf('alter table %s default charset utf8', $dbtable), 1);
278            }
279
280            foreach ($dbcolumns as $dbcolumn) {
281                set_time_limit(600);
282                output($GLOBALS['I18N']->get('Upgrading column ').' '.$dbcolumn['COLUMN_NAME'].'<br/>');
283                Sql_Query(sprintf('alter table %s change column %s %s %s default character set utf8',
284                    $dbcolumn['TABLE_NAME'], $dbcolumn['COLUMN_NAME'], $dbcolumn['COLUMN_NAME'],
285                    $dbcolumn['COLUMN_TYPE']), 1);
286            }
287            output($GLOBALS['I18N']->get('upgrade to UTF-8, done').'<br/>');
288            saveConfig('UTF8converted', date('Y-m-d H:i'), 0);
289        } else {
290            echo '<div class="error">'.s('Database requires converting to UTF-8.').'<br/>';
291            echo s('However, there is too little diskspace for this conversion').'<br/>';
292            echo s('Please do a manual conversion.').' '.PageLinkButton('converttoutf8',
293                    s('Run manual conversion to UTF8'));
294            echo '</div>';
295        }
296    }
297
298    //# 2.11.7 and up
299    Sql_Query(sprintf('alter table %s add column privileges text', $tables['admin']), 1);
300    Sql_Query('alter table '.$tables['list'].' add column category varchar(255) default ""', 1);
301    Sql_Query('alter table '.$tables['user_attribute'].' change column value value text');
302    Sql_Query('alter table '.$tables['message'].' change column textmessage textmessage longtext');
303    Sql_Query('alter table '.$tables['message'].' change column message message longtext');
304    Sql_Query('alter table '.$tables['messagedata'].' change column data data longtext');
305    Sql_Query('alter table '.$tables['bounce'].' add index statusidx (status(20))', 1);
306
307    //# fetch the list of TLDs, if possible
308    if (defined('TLD_AUTH_LIST')) {
309        refreshTlds(true);
310    }
311
312    //# changed terminology
313    Sql_Query(sprintf('update %s set status = "invalid email address" where status = "invalid email"',
314        $tables['usermessage']));
315
316    //# for some reason there are some config entries marked non-editable, that should be
317    include_once dirname(__FILE__).'/defaultconfig.php';
318    foreach ($default_config as $configItem => $configDetails) {
319        if (empty($configDetails['hidden'])) {
320            Sql_Query(sprintf('update %s set editable = 1 where item = "%s"', $tables['config'], $configItem));
321        } else {
322            Sql_Query(sprintf('update %s set editable = 0 where item = "%s"', $tables['config'], $configItem));
323        }
324    }
325
326    //# replace old header and footer with the new one
327    //# but only if there are untouched from the default, which seems fairly common
328    $oldPH = @file_get_contents(dirname(__FILE__).'/ui/old_public_header.inc');
329    $oldPH2 = preg_replace("/\n/", "\r\n", $oldPH); //# version with \r\n instead of \n
330
331    $oldPF = @file_get_contents(dirname(__FILE__).'/ui/old_public_footer.inc');
332    $oldPF2 = preg_replace("/\n/", "\r\n", $oldPF); //# version with \r\n instead of \n
333    Sql_Query(sprintf('update %s set value = "%s" where item = "pageheader" and (value = "%s" or value = "%s")',
334        $tables['config'], sql_escape($defaultheader), addslashes($oldPH), addslashes($oldPH2)));
335    Sql_Query(sprintf('update %s set value = "%s" where item = "pagefooter" and (value = "%s" or value = "%s")',
336        $tables['config'], sql_escape($defaultfooter), addslashes($oldPF), addslashes($oldPF2)));
337
338    //# and the same for subscribe pages
339    Sql_Query(sprintf('update %s set data = "%s" where name = "header" and (data = "%s" or data = "%s")',
340        $tables['subscribepage_data'], sql_escape($defaultheader), addslashes($oldPH), addslashes($oldPH2)));
341    Sql_Query(sprintf('update %s set data = "%s" where name = "footer" and (data = "%s" or data = "%s")',
342        $tables['subscribepage_data'], sql_escape($defaultfooter), addslashes($oldPF), addslashes($oldPF2)));
343
344    if (is_file(dirname(__FILE__).'/ui/'.$GLOBALS['ui'].'/old_public_header.inc')) {
345        $oldPH = file_get_contents(dirname(__FILE__).'/ui/'.$GLOBALS['ui'].'/old_public_header.inc');
346        $oldPH2 = preg_replace("/\n/", "\r\n", $oldPH); //# version with \r\n instead of \n
347        $oldPF = file_get_contents(dirname(__FILE__).'/ui/'.$GLOBALS['ui'].'/old_public_footer.inc');
348        $oldPF2 = preg_replace("/\n/", "\r\n", $oldPF); //# version with \r\n instead of \n
349        $currentPH = getConfig('pageheader');
350        $currentPF = getConfig('pagefooter');
351
352        if (($currentPH == $oldPH2 || $currentPH."\r\n" == $oldPH2) && !empty($defaultheader)) {
353            SaveConfig('pageheader', $defaultheader, 1);
354            Sql_Query(sprintf('update %s set data = "%s" where name = "header" and data = "%s"',
355                $tables['subscribepage_data'], sql_escape($defaultheader), addslashes($currentPH)));
356            //# only try to change footer when header has changed
357            if ($currentPF == $oldPF2 && !empty($defaultfooter)) {
358                SaveConfig('pagefooter', $defaultfooter, 1);
359                Sql_Query(sprintf('update %s set data = "%s" where name = "footer" and data = "%s"',
360                    $tables['subscribepage_data'], sql_escape($defaultfooter), addslashes($currentPF)));
361            }
362        }
363    }
364
365    //# #17328 - remove list categories with quotes
366    Sql_Query(sprintf("update %s set category = replace(category,\"\\\\'\",\" \")", $tables['list']));
367
368    //# add uuid columns
369    if (!Sql_Table_Column_Exists($GLOBALS['tables']['message'], 'uuid')) {
370        Sql_Query(sprintf('alter table %s add column uuid varchar(36) default ""',
371            $GLOBALS['tables']['message']));
372        Sql_Query(sprintf('alter table %s add index uuididx (uuid)',
373            $GLOBALS['tables']['message']));
374    }
375    if (!Sql_Table_Column_Exists($GLOBALS['tables']['linktrack_forward'], 'uuid')) {
376        Sql_Query(sprintf('alter table %s add column uuid varchar(36) default ""',
377            $GLOBALS['tables']['linktrack_forward']));
378        Sql_Query(sprintf('alter table %s add index uuididx (uuid)',
379            $GLOBALS['tables']['linktrack_forward']));
380    }
381    if (!Sql_Table_Column_Exists($GLOBALS['tables']['user'], 'uuid')) {
382        Sql_Query(sprintf('alter table %s add column uuid varchar(36) default ""',
383            $GLOBALS['tables']['user']));
384        Sql_Query(sprintf('alter table %s add index uuididx (uuid)',
385            $GLOBALS['tables']['user']));
386    }
387    // add uuids to those that do not have it
388    $req = Sql_Query(sprintf('select id from %s where uuid = ""', $GLOBALS['tables']['user']));
389    $numS = Sql_Affected_Rows();
390    if ($numS > 500 && empty($GLOBALS['commandline'])) {
391
392        // with a lot of subscrirbers this can take a very long time, causing a blank page for a long time (I had one system where it took almost an hour)
393        //.This really needs to be loaded in Async mode, therefore I'm removing this for now
394        // it is not strictly necessary to do this here, because processqueue does it as well.
395        // that does mean that the first process queue may take a while.
396
397        //   output(s('Giving a UUID to your subscribers and campaigns. If you have a lot of them, this may take a while.'));
398        //   output(s('If the page times out, you can reload. Or otherwise try to run the upgrade from commandline instead.').' '.resourceLink('https://resources.phplist.com/system/commandline', s('Documentation how to set up phpList commandline')));
399    } else {
400        output(s('Giving a UUID to your subscribers and campaigns. If you have a lot of them, this may take a while.'));
401        output(s('If the page times out, you can reload. Or otherwise try to run the upgrade from commandline instead.').' '.resourceLink('https://resources.phplist.com/system/commandline', s('Documentation how to set up phpList commandline')));
402        while ($row = Sql_Fetch_Row($req)) {
403            Sql_Query(sprintf('update %s set uuid = "%s" where id = %d', $GLOBALS['tables']['user'], (string)uuid::generate(4), $row[0]));
404        }
405    }
406
407    // let's hope there aren't too many campaigns or links, otherwise the same timeout would apply.
408
409    $req = Sql_Query(sprintf('select id from %s where uuid = ""', $GLOBALS['tables']['message']));
410    while ($row = Sql_Fetch_Row($req)) {
411        Sql_Query(sprintf('update %s set uuid = "%s" where id = %d', $GLOBALS['tables']['message'], (string) uuid::generate(4), $row[0]));
412    }
413    $req = Sql_Query(sprintf('select id from %s where uuid = ""', $GLOBALS['tables']['linktrack_forward']));
414    while ($row = Sql_Fetch_Row($req)) {
415        Sql_Query(sprintf('update %s set uuid = "%s" where id = %d', $GLOBALS['tables']['linktrack_forward'], (string) uuid::generate(4), $row[0]));
416    }
417
418    if (!Sql_Table_Exists($tables['admin_password_request'])) {
419        createTable('admin_password_request');
420    }
421
422    if (!Sql_Table_exists($GLOBALS['tables']['user_message_view'])) {
423        cl_output(s('Creating new table "user_message_view"'));
424        createTable('user_message_view');
425    }
426
427    if (version_compare($dbversion, '3.3.3','<')) {
428        // add a draft campaign for invite plugin
429        addInviteCampaign();
430    }
431
432    if (version_compare($dbversion, '3.3.4','<')) {
433        Sql_Query("alter table {$GLOBALS['tables']['bounce']} modify data mediumblob ");
434    }
435
436    if (version_compare($dbversion, '3.4.0-RC1','<')) {
437        SaveConfig('secret', bin2hex(random_bytes(20)));
438    }
439
440    if (version_compare($dbversion, '3.6.0','<')) {
441        Sql_Query("alter table {$GLOBALS['tables']['message']} change column processed processed integer ");
442    }
443
444    if (!Sql_Table_Column_Exists($GLOBALS['tables']['template'], 'template_text')) {
445        Sql_Query(sprintf('alter table %s add column template_text longblob after template',
446            $GLOBALS['tables']['template']));
447        //# no change in behavior for existing templates
448        Sql_Query(sprintf('update %s set template_text="[CONTENT]"',
449            $GLOBALS['tables']['template']));
450    }
451
452    //# longblobs are better at mixing character encoding. We don't know the encoding of anything we may want to store in cache
453    //# before converting, it's quickest to clear the cache
454    clearPageCache();
455    Sql_Query(sprintf('alter table %s change column content content longblob', $tables['urlcache']));
456
457    //# unlock the upgrade process
458    Sql_Query(sprintf('delete from %s where item = "in-upgrade-to"', $tables['config']));
459    // mark the database to be our current version
460    if ($success) {
461        SaveConfig('version', VERSION, 0);
462        if (defined('RELEASEDATE')) {
463            SaveConfig('releaseDBversion', RELEASEDATE, 0);
464        }
465        // mark now to be the last time we checked for an update
466        SaveConfig('lastcheckupdate', date('m/d/Y h:i:s', time()), 0, true);
467        //# also clear any possible value for "updateavailable"
468        Sql_Query(sprintf('delete from %s where item = "updateavailable"', $tables['config']));
469
470        Info(s('Success'), 1);
471
472        upgradePlugins(array_keys($GLOBALS['plugins']));
473
474        echo subscribeToAnnouncementsForm();
475
476//#  check for old click track data
477        $num = Sql_Fetch_Row_Query(sprintf('select count(*) from %s', $GLOBALS['tables']['linktrack']));
478        if ($num[0] > 0) {
479            echo '<p class="information">'.$GLOBALS['I18N']->get('The clicktracking system has changed').'</p>';
480            printf($GLOBALS['I18N']->get('You have %s entries in the old statistics table'), $num[0]).' ';
481            echo ' '.PageLinkButton('convertstats', $GLOBALS['I18N']->get('Convert Old data to new'));
482        }
483
484        if ($GLOBALS['commandline']) {
485            output($GLOBALS['I18N']->get('Upgrade successful'));
486        }
487    } else {
488        Error('An error occurred while upgrading your database');
489        if ($GLOBALS['commandline']) {
490            output($GLOBALS['I18N']->get('Upgrade failed'));
491        }
492    }
493    clearMaintenanceMode();
494} else {
495    echo '<p>'.s('Your database requires upgrading, please make sure to create a backup of your database first.').'</p>';
496    echo '<p>'.s('If you have a large database, make sure you have sufficient diskspace available for upgrade.').'</p>';
497    echo '<p>'.s('When you are ready click %s Depending on the size of your database, this may take quite a while. Please make sure not to interrupt the process, once it started.',
498            PageLinkButton('upgrade&doit=yes', s('Upgrade'))).'</p>';
499}
500