]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/upgrade.php
Wiki page --> wiki page
[SourceForge/phpwiki.git] / lib / upgrade.php
1 <?php
2
3 /*
4  * Copyright 2004,2005,2006,2007 $ThePhpWikiProgrammingTeam
5  * Copyright 2008 Marc-Etienne Vargenau, Alcatel-Lucent
6  *
7  * This file is part of PhpWiki.
8  *
9  * PhpWiki is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * PhpWiki is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License along
20  * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
21  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22  */
23
24 /**
25  * Upgrade existing WikiDB and config settings after installing a new PhpWiki sofwtare version.
26  * Status: almost no queries for verification.
27  *         simple merge conflict resolution, or Overwrite All.
28  *
29  * Installation on an existing PhpWiki database needs some
30  * additional worksteps. Each step will require multiple pages.
31  *
32  * This is the plan:
33  *  1. Check for new or changed database schema and update it
34  *     according to some predefined upgrade tables. (medium, complete)
35  *  2. Check for new or changed (localized) pgsrc/ pages and ask
36  *     for upgrading these. Check timestamps, upgrade silently or
37  *     show diffs if existing. Overwrite or merge (easy, complete)
38  *  3. Check for new or changed or deprecated index.php/config.ini settings
39  *     and help in upgrading these. (for newer changes since 1.3.11, not yet)
40  *   3a. Convert old-style index.php into config/config.ini. (easy, not yet)
41  *  4. Check for changed plugin invocation arguments. (medium, done)
42  *  5. Check for changed theme variables. (hard, not yet)
43  *  6. Convert the single-request upgrade to a class-based multi-page
44  *     version. (hard)
45  *
46  * Done: overwrite=1 link on edit conflicts at first occurence "Overwrite all".
47  *
48  * @author: Reini Urban
49  */
50 require_once 'lib/loadsave.php';
51
52 class Upgrade
53 {
54
55     function Upgrade(&$request)
56     {
57         $this->request =& $request;
58         $this->dbi =& $request->_dbi; // no reference for dbadmin ?
59         $this->phpwiki_version = $this->current_db_version = phpwiki_version();
60         //$this->current_db_version = 1030.13; // should be stored in the db. should be phpwiki_version
61
62         $this->db_version = $this->dbi->get_db_version();
63         $this->isSQL = $this->dbi->_backend->isSQL();
64     }
65
66     function doPgsrcUpdate($pagename, $path, $filename)
67     {
68         // don't ever update the HomePage
69         if ((defined(HOME_PAGE) and ($pagename == HOME_PAGE))
70             or ($pagename == _("HomePage"))
71             or ($pagename == "HomePage")
72         ) {
73             echo "$path/$pagename: " . _("always skip the HomePage.") . " " . _("Skipped."), "<br />\n";
74             return;
75         }
76
77         $page = $this->dbi->getPage($pagename);
78         if ($page->exists()) {
79             // check mtime: update automatically if pgsrc is newer
80             $rev = $page->getCurrentRevision();
81             $page_mtime = $rev->get('mtime');
82             $data = implode("", file($path . "/" . $filename));
83             if (($parts = ParseMimeifiedPages($data))) {
84                 usort($parts, 'SortByPageVersion');
85                 reset($parts);
86                 $pageinfo = $parts[0];
87                 $stat = stat($path . "/" . $filename);
88                 $new_mtime = 0;
89                 if (isset($pageinfo['versiondata']['mtime']))
90                     $new_mtime = $pageinfo['versiondata']['mtime'];
91                 if (!$new_mtime and isset($pageinfo['versiondata']['lastmodified']))
92                     $new_mtime = $pageinfo['versiondata']['lastmodified'];
93                 if (!$new_mtime and isset($pageinfo['pagedata']['date']))
94                     $new_mtime = $pageinfo['pagedata']['date'];
95                 if (!$new_mtime)
96                     $new_mtime = $stat[9];
97                 if ($new_mtime > $page_mtime) {
98                     echo "$path/$pagename" . _(": ") . _("newer than the existing page.")
99                          . " " . _("Replace") . " " . "($new_mtime &gt; $page_mtime)" . "<br />\n";
100                     LoadAny($this->request, $path . "/" . $filename);
101                     echo "<br />\n";
102                 } else {
103                     echo "$path/$pagename" . _(": ") . _("older than the existing page.") 
104                          . " " . _("Skipped."), "<br />\n";
105                 }
106             } else {
107                 echo "$path/$pagename" . _(": ") . _("unknown format.") . " " . _("Skipped.") . "<br />\n";
108             }
109         } else {
110             echo sprintf(_("%s does not exist"), $pagename), "<br />\n";
111             LoadAny($this->request, $path . "/" . $filename);
112             echo "<br />\n";
113         }
114     }
115
116     function CheckActionPageUpdate()
117     {
118         echo "<h2>", sprintf(_("Check for necessary %s updates"), _("ActionPage")), "</h2>\n";
119         // 1.3.13 before we pull in all missing pages, we rename existing ones
120         $this->_rename_page_helper(_("_AuthInfo"), _("DebugAuthInfo"));
121         // this is in some templates. so we keep the old name
122         //$this->_rename_page_helper($this->dbi, _("DebugInfo"), _("DebugBackendInfo"));
123         $this->_rename_page_helper(_("_GroupInfo"), _("GroupAuthInfo")); //never officially existed
124         $this->_rename_page_helper("InterWikiKarte", "InterWikiListe"); // german only
125
126         $path = FindFile('pgsrc');
127         $pgsrc = new fileSet($path);
128         // most actionpages have the same name as the plugin
129         $loc_path = FindLocalizedFile('pgsrc');
130         foreach ($pgsrc->getFiles() as $filename) {
131             if (substr($filename, -1, 1) == '~') continue;
132             if (substr($filename, -5, 5) == '.orig') continue;
133             $pagename = urldecode($filename);
134             if (isActionPage($pagename)) {
135                 $translation = gettext($pagename);
136                 if ($translation == $pagename)
137                     $this->doPgsrcUpdate($pagename, $path, $filename);
138                 elseif (FindLocalizedFile('pgsrc/' . urlencode($translation), 1))
139                     $this->doPgsrcUpdate($translation, $loc_path, urlencode($translation)); else
140                     $this->doPgsrcUpdate($pagename, $path, $filename);
141             }
142         }
143     }
144
145     // see loadsave.php for saving new pages.
146     function CheckPgsrcUpdate()
147     {
148         // Check some theme specific pgsrc files (blog, wikilens, fusionforge, custom).
149         // We check theme specific pgsrc first in case the page is present in both
150         // theme specific and global pgsrc
151         global $WikiTheme;
152         $path = $WikiTheme->file("pgsrc");
153         // TBD: the call to fileSet prints a warning:
154         // Notice: Unable to open directory 'themes/MonoBook/pgsrc' for reading
155         $pgsrc = new fileSet($path);
156         if ($pgsrc->getFiles()) {
157             echo "<h2>", sprintf(_("Check for necessary theme %s updates"),
158                 "pgsrc"), "</h2>\n";
159             foreach ($pgsrc->getFiles() as $filename) {
160                 if (substr($filename, -1, 1) == '~') continue;
161                 if (substr($filename, -5, 5) == '.orig') continue;
162                 $pagename = urldecode($filename);
163                 $this->doPgsrcUpdate($pagename, $path, $filename);
164             }
165         }
166
167         echo "<h2>", sprintf(_("Check for necessary %s updates"),
168             "pgsrc"), "</h2>\n";
169         if ($this->db_version < 1030.12200612) {
170             echo "<h4>", _("rename to Help: pages"), "</h4>\n";
171         }
172         $path = FindLocalizedFile(WIKI_PGSRC);
173         $pgsrc = new fileSet($path);
174         // fixme: verification, ...
175         foreach ($pgsrc->getFiles() as $filename) {
176             if (substr($filename, -1, 1) == '~') continue;
177             if (substr($filename, -5, 5) == '.orig') continue;
178             $pagename = urldecode($filename);
179             if (!isActionPage($filename)) {
180                 // There're a lot of now unneeded pages around.
181                 // At first rename the BlaPlugin pages to Help/<pagename> and then to the update.
182                 if ($this->db_version < 1030.12200612) {
183                     $this->_rename_to_help_page($pagename);
184                 }
185                 $this->doPgsrcUpdate($pagename, $path, $filename);
186             }
187         }
188     }
189
190     function _rename_page_helper($oldname, $pagename)
191     {
192         echo sprintf(_("rename %s to %s"), $oldname, $pagename), " ...";
193         if ($this->dbi->isWikiPage($oldname) and !$this->dbi->isWikiPage($pagename)) {
194             if ($this->dbi->_backend->rename_page($oldname, $pagename)) {
195                 echo _("OK"), " <br />\n";
196             } else {
197                 echo ' <span style="color: red; font-weight: bold;">' . _("FAILED") . "</span><br />\n";
198             }
199         } else {
200             echo " " . _("Skipped.") . "<br />\n";
201         }
202     }
203
204     function _rename_to_help_page($pagename)
205     {
206         $newprefix = _("Help") . "/";
207         if (substr($pagename, 0, strlen($newprefix)) != $newprefix) return;
208         $oldname = substr($pagename, strlen($newprefix));
209         $this->_rename_page_helper($oldname, $pagename);
210     }
211
212     /**
213      * TODO: Search table definition in appropriate schema
214      *       and create it.
215      * Supported: mysql and generic SQL, for ADODB and PearDB.
216      */
217     function installTable($table, $backend_type)
218     {
219         global $DBParams;
220         if (!$this->isSQL) return;
221         echo _("MISSING"), " ... \n";
222         $backend = &$this->dbi->_backend->_dbh;
223         /*
224           $schema = findFile("schemas/${backend_type}.sql");
225           if (!$schema) {
226           echo "  ",_("FAILED"),": ",sprintf(_("no schema %s found"),
227           "schemas/${backend_type}.sql")," ... <br />\n";
228           return false;
229           }
230         */
231         extract($this->dbi->_backend->_table_names);
232         $prefix = isset($DBParams['prefix']) ? $DBParams['prefix'] : '';
233         switch ($table) {
234             case 'session':
235                 assert($session_tbl);
236                 if ($backend_type == 'mysql') {
237                     $this->dbi->genericSqlQuery("
238 CREATE TABLE $session_tbl (
239         sess_id     CHAR(32) NOT NULL DEFAULT '',
240         sess_data     BLOB NOT NULL,
241         sess_date     INT UNSIGNED NOT NULL,
242         sess_ip     CHAR(15) NOT NULL,
243         PRIMARY KEY (sess_id),
244     INDEX (sess_date)
245 )");
246                 } else {
247                     $this->dbi->genericSqlQuery("
248 CREATE TABLE $session_tbl (
249     sess_id     CHAR(32) NOT NULL DEFAULT '',
250         sess_data     " . ($backend_type == 'pgsql' ? 'TEXT' : 'BLOB') . " NOT NULL,
251         sess_date     INT,
252         sess_ip     CHAR(15) NOT NULL
253 )");
254                     $this->dbi->genericSqlQuery("CREATE UNIQUE INDEX sess_id ON $session_tbl (sess_id)");
255                 }
256                 $this->dbi->genericSqlQuery("CREATE INDEX sess_date on session (sess_date)");
257                 echo "  ", _("CREATED");
258                 break;
259             case 'pref':
260                 $pref_tbl = $prefix . 'pref';
261                 if ($backend_type == 'mysql') {
262                     $this->dbi->genericSqlQuery("
263 CREATE TABLE $pref_tbl (
264       userid     CHAR(48) BINARY NOT NULL UNIQUE,
265       prefs      TEXT NULL DEFAULT '',
266       PRIMARY KEY (userid)
267 )");
268                 } else {
269                     $this->dbi->genericSqlQuery("
270 CREATE TABLE $pref_tbl (
271       userid     CHAR(48) NOT NULL,
272       prefs      TEXT NULL DEFAULT ''
273 )");
274                     $this->dbi->genericSqlQuery("CREATE UNIQUE INDEX userid ON $pref_tbl (userid)");
275                 }
276                 echo "  ", _("CREATED");
277                 break;
278             case 'member':
279                 $member_tbl = $prefix . 'member';
280                 if ($backend_type == 'mysql') {
281                     $this->dbi->genericSqlQuery("
282 CREATE TABLE $member_tbl (
283     userid    CHAR(48) BINARY NOT NULL,
284        groupname CHAR(48) BINARY NOT NULL DEFAULT 'users',
285        INDEX (userid),
286        INDEX (groupname)
287 )");
288                 } else {
289                     $this->dbi->genericSqlQuery("
290 CREATE TABLE $member_tbl (
291     userid    CHAR(48) NOT NULL,
292        groupname CHAR(48) NOT NULL DEFAULT 'users'
293 )");
294                     $this->dbi->genericSqlQuery("CREATE INDEX userid ON $member_tbl (userid)");
295                     $this->dbi->genericSqlQuery("CREATE INDEX groupname ON $member_tbl (groupname)");
296                 }
297                 echo "  ", _("CREATED");
298                 break;
299             case 'rating':
300                 $rating_tbl = $prefix . 'rating';
301                 if ($backend_type == 'mysql') {
302                     $this->dbi->genericSqlQuery("
303 CREATE TABLE $rating_tbl (
304         dimension INT(4) NOT NULL,
305         raterpage INT(11) NOT NULL,
306         rateepage INT(11) NOT NULL,
307         ratingvalue FLOAT NOT NULL,
308         rateeversion INT(11) NOT NULL,
309         tstamp TIMESTAMP(14) NOT NULL,
310         PRIMARY KEY (dimension, raterpage, rateepage)
311 )");
312                 } else {
313                     $this->dbi->genericSqlQuery("
314 CREATE TABLE $rating_tbl (
315         dimension INT(4) NOT NULL,
316         raterpage INT(11) NOT NULL,
317         rateepage INT(11) NOT NULL,
318         ratingvalue FLOAT NOT NULL,
319         rateeversion INT(11) NOT NULL,
320         tstamp TIMESTAMP(14) NOT NULL
321 )");
322                     $this->dbi->genericSqlQuery("CREATE UNIQUE INDEX rating"
323                         . " ON $rating_tbl (dimension, raterpage, rateepage)");
324                 }
325                 echo "  ", _("CREATED");
326                 break;
327             case 'accesslog':
328                 $log_tbl = $prefix . 'accesslog';
329                 // fields according to http://www.outoforder.cc/projects/apache/mod_log_sql/docs-2.0/#id2756178
330                 /*
331                   A    User Agent agent    varchar(255)    Mozilla/4.0 (compat; MSIE 6.0; Windows)
332                   a    CGi request arguments    request_args    varchar(255)    user=Smith&cart=1231&item=532
333                   b    Bytes transfered    bytes_sent    int unsigned    32561
334                   c???    Text of cookie    cookie    varchar(255)    Apache=sdyn.fooonline.net 1300102700823
335                   f    Local filename requested    request_file    varchar(255)    /var/www/html/books-cycroad.html
336                   H    HTTP request_protocol    request_protocol    varchar(10)    HTTP/1.1
337                   h    Name of remote host    remote_host    varchar(50)    blah.foobar.com
338                   I    Request ID (from modd_unique_id)    id    char(19)    POlFcUBRH30AAALdBG8
339                   l    Ident user info    remote_logname    varcgar(50)    bobby
340                   M    Machine ID???    machine_id    varchar(25)    web01
341                   m    HTTP request method    request_method    varchar(10)    GET
342                   P    httpd cchild PID    child_pid    smallint unsigned    3215
343                   p    http port    server_port    smallint unsigned    80
344                   R    Referer    referer    varchar(255)    http://www.biglinks4u.com/linkpage.html
345                   r    Request in full form    request_line    varchar(255)    GET /books-cycroad.html HTTP/1.1
346                   S    Time of request in UNIX time_t format    time_stamp    int unsigned    1005598029
347                   T    Seconds to service request    request_duration    smallint unsigned    2
348                   t    Time of request in human format    request_time    char(28)    [02/Dec/2001:15:01:26 -0800]
349                   U    Request in simple form    request_uri    varchar(255)    /books-cycroad.html
350                   u    User info from HTTP auth    remote_user    varchar(50)    bobby
351                   v    Virtual host servicing the request    virtual_host    varchar(255)
352                 */
353                 $this->dbi->genericSqlQuery("
354 CREATE TABLE $log_tbl (
355         time_stamp    int unsigned,
356     remote_host   varchar(100),
357     remote_user   varchar(50),
358         request_method varchar(10),
359     request_line  varchar(255),
360     request_args  varchar(255),
361     request_uri   varchar(255),
362     request_time  char(28),
363     status           smallint unsigned,
364     bytes_sent    smallint unsigned,
365         referer       varchar(255),
366     agent         varchar(255),
367     request_duration float
368 )");
369                 $this->dbi->genericSqlQuery("CREATE INDEX log_time ON $log_tbl (time_stamp)");
370                 $this->dbi->genericSqlQuery("CREATE INDEX log_host ON $log_tbl (remote_host)");
371                 echo "  ", _("CREATED");
372                 break;
373         }
374         echo "<br />\n";
375     }
376
377     /**
378      * Update from ~1.3.4 to current.
379      * tables: Only session, user, pref and member
380      * jeffs-hacks database api (around 1.3.2) later:
381      *   people should export/import their pages if using that old versions.
382      */
383     function CheckDatabaseUpdate()
384     {
385         global $DBAuthParams, $DBParams;
386
387         echo "<h2>", sprintf(_("Check for necessary %s updates"),
388             _("database")),
389         " - ", DATABASE_TYPE, "</h2>\n";
390         $dbadmin = $this->request->getArg('dbadmin');
391         if ($this->isSQL) {
392             $this->_db_init();
393             if (isset($dbadmin['cancel'])) {
394                 echo _("Cancel"), " <br />\n";
395                 return;
396             }
397         }
398         echo "db version: we want ", $this->current_db_version, "\n<br />";
399         echo "db version: we have ", $this->db_version, "\n<br />";
400         if ($this->db_version >= $this->current_db_version) {
401             echo _("OK"), "<br />\n";
402             return;
403         }
404
405         $backend_type = $this->dbi->_backend->backendType();
406         if ($this->isSQL) {
407             echo "<h4>", _("Backend type: "), $backend_type, "</h4>\n";
408             $prefix = isset($DBParams['prefix']) ? $DBParams['prefix'] : '';
409             $tables = $this->dbi->_backend->listOfTables();
410             foreach (explode(':', 'session:pref:member') as $table) {
411                 echo sprintf(_("Check for table %s"), $table), " ...";
412                 if (!in_array($prefix . $table, $tables)) {
413                     $this->installTable($table, $backend_type);
414                 } else {
415                     echo _("OK"), " <br />\n";
416                 }
417             }
418         }
419
420         if ($this->phpwiki_version >= 1030.12200612 and $this->db_version < 1030.13) {
421             if ($this->isSQL and preg_match("/(pgsql|postgres)/", $backend_type)) {
422                 trigger_error("You need to upgrade to schema/psql-initialize.sql manually!",
423                     E_USER_WARNING);
424                 // $this->_upgrade_psql_tsearch2();
425             }
426             $this->_upgrade_relation_links();
427         }
428
429         if (ACCESS_LOG_SQL and $this->isSQL) {
430             $table = "accesslog";
431             echo sprintf(_("Check for table %s"), $table), " ...";
432             if (!in_array($prefix . $table, $tables)) {
433                 $this->installTable($table, $backend_type);
434             } else {
435                 echo _("OK"), " <br />\n";
436             }
437         }
438         if ($this->isSQL and (class_exists("RatingsUserFactory") or $this->dbi->isWikiPage(_("RateIt")))) {
439             $table = "rating";
440             echo sprintf(_("Check for table %s"), $table), " ...";
441             if (!in_array($prefix . $table, $tables)) {
442                 $this->installTable($table, $backend_type);
443             } else {
444                 echo _("OK"), " <br />\n";
445             }
446         }
447         $backend = &$this->dbi->_backend->_dbh;
448         if ($this->isSQL)
449             extract($this->dbi->_backend->_table_names);
450
451         // 1.3.8 added session.sess_ip
452         if ($this->isSQL and $this->phpwiki_version >= 1030.08 and USE_DB_SESSION
453             and isset($this->request->_dbsession)
454         ) {
455             echo _("Check for new session.sess_ip column"), " ... ";
456             $database = $this->dbi->_backend->database();
457             assert(!empty($DBParams['db_session_table']));
458             $session_tbl = $prefix . $DBParams['db_session_table'];
459             $sess_fields = $this->dbi->_backend->listOfFields($database, $session_tbl);
460             if (!$sess_fields) {
461                 echo _("SKIP");
462             } elseif (!strstr(strtolower(join(':', $sess_fields)), "sess_ip")) {
463                 // TODO: postgres test (should be able to add columns at the end, but not in between)
464                 echo "<b>", _("ADDING"), "</b>", " ... ";
465                 $this->dbi->genericSqlQuery("ALTER TABLE $session_tbl ADD sess_ip CHAR(15) NOT NULL");
466                 $this->dbi->genericSqlQuery("CREATE INDEX sess_date ON $session_tbl (sess_date)");
467             } else {
468                 echo _("OK");
469             }
470             echo "<br />\n";
471             if (substr($backend_type, 0, 5) == 'mysql') {
472                 // upgrade to 4.1.8 destroyed my session table:
473                 // sess_id => varchar(10), sess_data => varchar(5). For others obviously also.
474                 echo _("Check for mysql session.sess_id sanity"), " ... ";
475                 $result = $this->dbi->genericSqlQuery("DESCRIBE $session_tbl");
476                 if (DATABASE_TYPE == 'SQL') {
477                     $iter = new WikiDB_backend_PearDB_generic_iter($backend, $result);
478                 } elseif (DATABASE_TYPE == 'ADODB') {
479                     $iter = new WikiDB_backend_ADODB_generic_iter($backend, $result,
480                         array("Field", "Type", "Null", "Key", "Default", "Extra"));
481                 } elseif (DATABASE_TYPE == 'PDO') {
482                     $iter = new WikiDB_backend_PDO_generic_iter($backend, $result);
483                 }
484                 while ($col = $iter->next()) {
485                     if ($col["Field"] == 'sess_id' and !strstr(strtolower($col["Type"]), 'char(32)')) {
486                         $this->dbi->genericSqlQuery("ALTER TABLE $session_tbl CHANGE sess_id"
487                             . " sess_id CHAR(32) NOT NULL");
488                         echo "sess_id ", $col["Type"], " ", _("fixed"), " =&gt; CHAR(32) ";
489                     }
490                     if ($col["Field"] == 'sess_ip' and !strstr(strtolower($col["Type"]), 'char(15)')) {
491                         $this->dbi->genericSqlQuery("ALTER TABLE $session_tbl CHANGE sess_ip"
492                             . " sess_ip CHAR(15) NOT NULL");
493                         echo "sess_ip ", $col["Type"], " ", _("fixed"), " =&gt; CHAR(15) ";
494                     }
495                 }
496                 echo _("OK"), "<br />\n";
497             }
498         }
499
500         /* TODO:
501            ALTER TABLE link ADD relation INT DEFAULT 0;
502            CREATE INDEX linkrelation ON link (relation);
503         */
504
505         // mysql >= 4.0.4 requires LOCK TABLE privileges
506         if (substr($backend_type, 0, 5) == 'mysql') {
507             echo _("Check for mysql LOCK TABLE privilege"), " ...";
508             $mysql_version = $this->dbi->_backend->_serverinfo['version'];
509             if ($mysql_version > 400.40) {
510                 if (!empty($this->dbi->_backend->_parsedDSN))
511                     $parseDSN = $this->dbi->_backend->_parsedDSN;
512                 elseif (function_exists('parseDSN')) // ADODB or PDO
513                     $parseDSN = parseDSN($DBParams['dsn']); else // pear
514                     $parseDSN = DB::parseDSN($DBParams['dsn']);
515                 $username = $this->dbi->_backend->qstr($parseDSN['username']);
516                 // on db level
517                 $query = "SELECT lock_tables_priv FROM mysql.db WHERE user='$username'";
518                 //mysql_select_db("mysql", $this->dbi->_backend->connection());
519                 $db_fields = $this->dbi->_backend->listOfFields("mysql", "db");
520                 if (!strstr(strtolower(join(':', $db_fields)), "lock_tables_priv")) {
521                     echo join(':', $db_fields);
522                     die("lock_tables_priv missing. The DB Admin must run mysql_fix_privilege_tables");
523                 }
524                 $row = $this->dbi->_backend->getRow($query);
525                 if (isset($row[0]) and $row[0] == 'N') {
526                     $this->dbi->genericSqlQuery("UPDATE mysql.db SET lock_tables_priv='Y'"
527                         . " WHERE mysql.user='$username'");
528                     $this->dbi->genericSqlQuery("FLUSH PRIVILEGES");
529                     echo "mysql.db user='$username'", _("fixed"), "<br />\n";
530                 } elseif (!$row) {
531                     // or on user level
532                     $query = "SELECT lock_tables_priv FROM mysql.user WHERE user='$username'";
533                     $row = $this->dbi->_backend->getRow($query);
534                     if ($row and $row[0] == 'N') {
535                         $this->dbi->genericSqlQuery("UPDATE mysql.user SET lock_tables_priv='Y'"
536                             . " WHERE mysql.user='$username'");
537                         $this->dbi->genericSqlQuery("FLUSH PRIVILEGES");
538                         echo "mysql.user user='$username'", _("fixed"), "<br />\n";
539                     } elseif (!$row) {
540                         echo ' <span style="color: red; font-weight: bold;">' . _("FAILED") . "</span>"
541                             . " Neither mysql.db nor mysql.user has a user='$username'"
542                             . " or the lock_tables_priv field",
543                         "<br />\n";
544                     } else {
545                         echo _("OK"), "<br />\n";
546                     }
547                 } else {
548                     echo _("OK"), "<br />\n";
549                 }
550                 //mysql_select_db($this->dbi->_backend->database(), $this->dbi->_backend->connection());
551             } else {
552                 echo sprintf(_("version <em>%s</em> not affected"), $mysql_version), "<br />\n";
553             }
554         }
555
556         // 1.3.10 mysql requires page.id auto_increment
557         // mysql, mysqli or mysqlt
558         if ($this->phpwiki_version >= 1030.099 and substr($backend_type, 0, 5) == 'mysql'
559             and DATABASE_TYPE != 'PDO'
560         ) {
561             echo _("Check for mysql page.id auto_increment flag"), " ...";
562             assert(!empty($page_tbl));
563             $database = $this->dbi->_backend->database();
564             $fields = mysql_list_fields($database, $page_tbl, $this->dbi->_backend->connection());
565             $columns = mysql_num_fields($fields);
566             for ($i = 0; $i < $columns; $i++) {
567                 if (mysql_field_name($fields, $i) == 'id') {
568                     $flags = mysql_field_flags($fields, $i);
569                     //DONE: something was wrong with ADODB here.
570                     if (!strstr(strtolower($flags), "auto_increment")) {
571                         echo "<b>", _("ADDING"), "</b>", " ... ";
572                         // MODIFY col_def valid since mysql 3.22.16,
573                         // older mysql's need CHANGE old_col col_def
574                         $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl CHANGE id"
575                             . " id INT NOT NULL AUTO_INCREMENT");
576                         $fields = mysql_list_fields($database, $page_tbl);
577                         if (!strstr(strtolower(mysql_field_flags($fields, $i)), "auto_increment"))
578                             echo ' <span style="color: red; font-weight: bold;">' . _("FAILED") . "</span><br />\n";
579                         else
580                             echo _("OK"), "<br />\n";
581                     } else {
582                         echo _("OK"), "<br />\n";
583                     }
584                     break;
585                 }
586             }
587             mysql_free_result($fields);
588         }
589
590         // Check for mysql 4.1.x/5.0.0a binary search problem.
591         //   http://bugs.mysql.com/bug.php?id=4398
592         // "select * from page where LOWER(pagename) like '%search%'" does not apply LOWER!
593         // Confirmed for 4.1.0alpha,4.1.3-beta,5.0.0a; not yet tested for 4.1.2alpha,
594         // On windows only, though utf8 would be useful elsewhere also.
595         // Illegal mix of collations (latin1_bin,IMPLICIT) and
596         // (utf8_general_ci, COERCIBLE) for operation '='])
597         if (isWindows() and substr($backend_type, 0, 5) == 'mysql') {
598             echo _("Check for mysql 4.1.x/5.0.0 binary search on Windows problem"), " ...";
599             $mysql_version = $this->dbi->_backend->_serverinfo['version'];
600             if ($mysql_version < 401.0) {
601                 echo sprintf(_("version <em>%s</em>"), $mysql_version), " ",
602                 _("not affected"), "<br />\n";
603             } elseif ($mysql_version >= 401.6) { // FIXME: since which version?
604                 $row = $this->dbi->_backend->getRow("SHOW CREATE TABLE $page_tbl");
605                 $result = join(" ", $row);
606                 if (strstr(strtolower($result), "character set")
607                     and strstr(strtolower($result), "collate")
608                 ) {
609                     echo _("OK"), "<br />\n";
610                 } else {
611                     //SET CHARACTER SET latin1
612                     $charset = CHARSET;
613                     if ($charset == 'iso-8859-1') $charset = 'latin1';
614                     $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl CHANGE pagename "
615                         . "pagename VARCHAR(100) "
616                         . "CHARACTER SET '$charset' COLLATE '$charset" . "_bin' NOT NULL");
617                     echo sprintf(_("version <em>%s</em>"), $mysql_version),
618                     " <b>", _("FIXED"), "</b>",
619                     "<br />\n";
620                 }
621             } elseif (DATABASE_TYPE != 'PDO') {
622                 // check if already fixed
623                 extract($this->dbi->_backend->_table_names);
624                 assert(!empty($page_tbl));
625                 $database = $this->dbi->_backend->database();
626                 $fields = mysql_list_fields($database, $page_tbl, $this->dbi->_backend->connection());
627                 $columns = mysql_num_fields($fields);
628                 for ($i = 0; $i < $columns; $i++) {
629                     if (mysql_field_name($fields, $i) == 'pagename') {
630                         $flags = mysql_field_flags($fields, $i);
631                         // I think it was fixed with 4.1.6, but I tested it only with 4.1.8
632                         if ($mysql_version > 401.0 and $mysql_version < 401.6) {
633                             // remove the binary flag
634                             if (strstr(strtolower($flags), "binary")) {
635                                 // FIXME: on duplicate pagenames this will fail!
636                                 $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl CHANGE pagename"
637                                     . " pagename VARCHAR(100) NOT NULL");
638                                 echo sprintf(_("version <em>%s</em>"), $mysql_version),
639                                 "<b>", _("FIXED"), "</b>"
640                                 , "<br />\n";
641                             }
642                         }
643                         break;
644                     }
645                 }
646             }
647         }
648         if ($this->isSQL and ACCESS_LOG_SQL & 2) {
649             echo _("Check for ACCESS_LOG_SQL passwords in POST requests"), " ...";
650             // Don't display passwords in POST requests (up to 2005-02-04 12:03:20)
651             $res = $this->dbi->genericSqlIter("SELECT time_stamp, remote_host, " .
652                 "request_args FROM ${prefix}accesslog WHERE request_args LIKE " .
653                 "'%s:6:\"passwd\"%' AND request_args NOT LIKE '%s:6:\"passwd\";" .
654                 "s:15:\"<not displayed>\"%'");
655             $count = 0;
656             while ($row = $res->next()) {
657                 $args = preg_replace("/(s:6:\"passwd\";s:15:\").*(\")/",
658                     "$1<not displayed>$2", $row["request_args"]);
659                 $ts = $row["time_stamp"];
660                 $rh = $row["remote_host"];
661                 $this->dbi->genericSqlQuery("UPDATE ${prefix}accesslog SET " .
662                     "request_args='$args' WHERE time_stamp=$ts AND " .
663                     "remote_host='$rh'");
664                 $count++;
665             }
666             if ($count > 0)
667                 echo "<b>", _("FIXED"), "</b>", "<br />\n";
668             else
669                 echo _("OK"), "<br />\n";
670
671             if ($this->phpwiki_version >= 1030.13) {
672                 echo _("Check for ACCESS_LOG_SQL remote_host varchar(50)"), " ...";
673                 $database = $this->dbi->_backend->database();
674                 $accesslog_tbl = $prefix . 'accesslog';
675                 $fields = $this->dbi->_backend->listOfFields($database, $accesslog_tbl);
676                 if (!$fields) {
677                     echo _("SKIP");
678                 } elseif (strstr(strtolower(join(':', $sess_fields)), "remote_host")) {
679                     // TODO: how to check size, already done?
680                     echo "<b>", _("FIXING"), "remote_host</b>", " ... ";
681                     $this->dbi->genericSqlQuery("ALTER TABLE $accesslog_tbl CHANGE remote_host VARCHAR(100)");
682                 } else {
683                     echo _("FAILED");
684                 }
685                 echo "<br />\n";
686             }
687         }
688         $this->_upgrade_cached_html();
689
690         if ($this->db_version < $this->current_db_version) {
691             $this->dbi->set_db_version($this->current_db_version);
692             $this->db_version = $this->dbi->get_db_version();
693             echo "db version: upgrade to ", $this->db_version, "  ";
694             echo _("OK"), "<br />\n";
695             flush();
696         }
697
698         return;
699     }
700
701     /**
702      * Filter SQL missing permissions errors.
703      *
704      * A wrong DBADMIN user will not be able to connect
705      * @see _is_false_error, ErrorManager
706      * @access private
707      */
708     function _dbpermission_filter($err)
709     {
710         if ($err->isWarning()) {
711             global $ErrorManager;
712             $this->error_caught = 1;
713             $ErrorManager->_postponed_errors[] = $err;
714             return true;
715         }
716         return false;
717     }
718
719     function _try_dbadmin_user($user, $passwd)
720     {
721         global $DBParams, $DBAuthParams;
722         $AdminParams = $DBParams;
723         if (DATABASE_TYPE == 'SQL')
724             $dsn = DB::parseDSN($AdminParams['dsn']);
725         else {
726             $dsn = parseDSN($AdminParams['dsn']);
727         }
728         $AdminParams['dsn'] = sprintf("%s://%s:%s@%s/%s",
729             $dsn['phptype'],
730             $user,
731             $passwd,
732             $dsn['hostspec'],
733             $dsn['database']);
734         $AdminParams['_tryroot_from_upgrade'] = 1;
735         // add error handler to warn about missing permissions for DBADMIN_USER
736         global $ErrorManager;
737         $ErrorManager->pushErrorHandler(new WikiMethodCb($this, '_dbpermission_filter'));
738         $this->error_caught = 0;
739         $this->dbi = WikiDB::open($AdminParams);
740         if (!$this->error_caught) return true;
741         // FAILED: redo our connection with the wikiuser
742         $this->dbi = WikiDB::open($DBParams);
743         $ErrorManager->flushPostponedErrors();
744         $ErrorManager->popErrorHandler();
745         return false;
746     }
747
748     function _db_init()
749     {
750         if (!$this->isSQL) return;
751
752         /* SQLite never needs admin params */
753         $backend_type = $this->dbi->_backend->backendType();
754         if (substr($backend_type, 0, 6) == "sqlite") {
755             return;
756         }
757         $dbadmin_user = 'root';
758         if ($dbadmin = $this->request->getArg('dbadmin')) {
759             $dbadmin_user = $dbadmin['user'];
760             if (isset($dbadmin['cancel'])) {
761                 return;
762             } elseif (!empty($dbadmin_user)) {
763                 if ($this->_try_dbadmin_user($dbadmin['user'], $dbadmin['passwd']))
764                     return;
765             }
766         } elseif (DBADMIN_USER) {
767             if ($this->_try_dbadmin_user(DBADMIN_USER, DBADMIN_PASSWD)) {
768                 return;
769             }
770         }
771         // Check if the privileges are enough. Need CREATE and ALTER perms.
772         // And on Windows: SELECT FROM mysql, possibly: UPDATE mysql.
773         $form = HTML::form(array("method" => "post",
774                 "action" => $this->request->getPostURL(),
775                 "accept-charset" => $GLOBALS['charset']),
776             HTML::p(_("Upgrade requires database privileges to CREATE and ALTER the phpwiki database."),
777                 HTML::br(),
778                 _("And on Windows at least the privilege to SELECT FROM mysql, and possibly UPDATE mysql")),
779             HiddenInputs(array('action' => 'upgrade',
780                 'overwrite' => $this->request->getArg('overwrite'))),
781             HTML::table(array("cellspacing" => 4),
782                 HTML::tr(HTML::td(array('align' => 'right'),
783                         _("DB admin user:")),
784                     HTML::td(HTML::input(array('name' => "dbadmin[user]",
785                         'size' => 12,
786                         'maxlength' => 256,
787                         'value' => $dbadmin_user)))),
788                 HTML::tr(HTML::td(array('align' => 'right'),
789                         _("DB admin password:")),
790                     HTML::td(HTML::input(array('name' => "dbadmin[passwd]",
791                         'type' => 'password',
792                         'size' => 12,
793                         'maxlength' => 256)))),
794                 HTML::tr(HTML::td(array('align' => 'center', 'colspan' => 2),
795                     Button("submit:", _("Submit"), 'wikiaction'),
796                     HTML::raw('&nbsp;'),
797                     Button("submit:dbadmin[cancel]", _("Cancel"),
798                         'button')))));
799         $form->printXml();
800         echo "</div><!-- content -->\n";
801         echo asXML(Template("bottom"));
802         echo "</body></html>\n";
803         $this->request->finish();
804         exit();
805     }
806
807     /**
808      * if page.cached_html does not exists:
809      *   put _cached_html from pagedata into a new seperate blob,
810      *   not into the huge serialized string.
811      *
812      * It is only rarelely needed: for current page only, if-not-modified,
813      * but was extracetd for every simple page iteration.
814      */
815     function _upgrade_cached_html($verbose = true)
816     {
817         global $DBParams;
818         if (!$this->isSQL) return 0;
819         $count = 0;
820         if ($this->phpwiki_version >= 1030.10) {
821             if ($verbose)
822                 echo _("Check for extra page.cached_html column"), " ... ";
823             $database = $this->dbi->_backend->database();
824             extract($this->dbi->_backend->_table_names);
825             $fields = $this->dbi->_backend->listOfFields($database, $page_tbl);
826             if (!$fields) {
827                 echo _("SKIP"), "<br />\n";
828                 return 0;
829             }
830             if (!strstr(strtolower(join(':', $fields)), "cached_html")) {
831                 if ($verbose)
832                     echo "<b>", _("ADDING"), "</b>", " ... ";
833                 $backend_type = $this->dbi->_backend->backendType();
834                 if (substr($backend_type, 0, 5) == 'mysql')
835                     $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl ADD cached_html MEDIUMBLOB");
836                 else
837                     $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl ADD cached_html BLOB");
838                 if ($verbose)
839                     echo "<b>", _("CONVERTING"), "</b>", " ... ";
840                 $count = _convert_cached_html();
841                 if ($verbose)
842                     echo $count, " ", _("OK"), "<br />\n";
843             } else {
844                 if ($verbose)
845                     echo _("OK"), "<br />\n";
846             }
847         }
848         return $count;
849     }
850
851     /**
852      * move _cached_html for all pages from pagedata into a new seperate blob.
853      * decoupled from action=upgrade, so that it can be used by a WikiAdminUtils button also.
854      */
855     function _convert_cached_html()
856     {
857         global $DBParams;
858         if (!$this->isSQL) return 0;
859         //if (!in_array(DATABASE_TYPE, array('SQL','ADODB'))) return;
860
861         $pages = $this->dbi->getAllPages();
862         $cache =& $this->dbi->_cache;
863         $count = 0;
864         extract($this->dbi->_backend->_table_names);
865         while ($page = $pages->next()) {
866             $pagename = $page->getName();
867             $data = $this->dbi->_backend->get_pagedata($pagename);
868             if (!empty($data['_cached_html'])) {
869                 $cached_html = $data['_cached_html'];
870                 $data['_cached_html'] = '';
871                 $cache->update_pagedata($pagename, $data);
872                 // store as blob, not serialized
873                 $this->dbi->genericSqlQuery("UPDATE $page_tbl SET cached_html=? WHERE pagename=?",
874                     array($cached_html, $pagename));
875                 $count++;
876             }
877         }
878         return $count;
879     }
880
881     /**
882      * upgrade to 1.3.13 link structure.
883      */
884     function _upgrade_relation_links($verbose = true)
885     {
886         if ($this->phpwiki_version >= 1030.12200610 and $this->isSQL) {
887             echo _("Check for relation field in link table"), " ...";
888             $database = $this->dbi->_backend->database();
889             $prefix = isset($DBParams['prefix']) ? $DBParams['prefix'] : '';
890             $link_tbl = $prefix . 'link';
891             $fields = $this->dbi->_backend->listOfFields($database, $link_tbl);
892             if (!$fields) {
893                 echo _("SKIP");
894             } elseif (strstr(strtolower(join(':', $fields)), "link")) {
895                 echo "<b>", _("ADDING"), " relation</b>", " ... ";
896                 $this->dbi->genericSqlQuery("ALTER TABLE $link_tbl ADD relation INT DEFAULT 0;");
897                 $this->dbi->genericSqlQuery("CREATE INDEX link_relation ON $link_tbl (relation);");
898             } else {
899                 echo _("FAILED");
900             }
901             echo "<br />\n";
902         }
903         if ($this->phpwiki_version >= 1030.12200610) {
904             echo _("Rebuild entire database to upgrade relation links"), " ... ";
905             if (DATABASE_TYPE == 'dba') {
906                 echo "<b>", _("CONVERTING"), " dba linktable</b>", "(~2 min, max 4 min) ... ";
907                 flush();
908                 longer_timeout(240);
909                 $this->dbi->_backend->_linkdb->rebuild();
910             } else {
911                 flush();
912                 longer_timeout(180);
913                 $this->dbi->_backend->rebuild();
914             }
915             echo _("OK"), "<br />\n";
916         }
917     }
918
919     function CheckPluginUpdate()
920     {
921         echo "<h2>", sprintf(_("Check for necessary %s updates"),
922             _("plugin argument")), "</h2>\n";
923
924         $this->_configUpdates = array();
925         $this->_configUpdates[] = new UpgradePluginEntry
926         ($this, array('key' => 'plugin_randompage_numpages',
927             'fixed_with' => 1012.0,
928             //'header' => _("change RandomPage pages => numpages"),
929             //'notice'  =>_("found RandomPage plugin"),
930             'check_args' => array("plugin RandomPage pages",
931                 "/(<\?\s*plugin\s+ RandomPage\s+)pages/",
932                 "\\1numpages")));
933         $this->_configUpdates[] = new UpgradePluginEntry
934         ($this, array('key' => 'plugin_createtoc_position',
935             'fixed_with' => 1013.0,
936             //'header' => _("change CreateToc align => position"),
937             //'notice'  =>_("found CreateToc plugin"),
938             'check_args' => array("plugin CreateToc align",
939                 "/(<\?\s*plugin\s+ CreateToc[^\?]+)align/",
940                 "\\1position")));
941
942         if (empty($this->_configUpdates)) return;
943         foreach ($this->_configUpdates as $update) {
944             $pages = $this->dbi->fullSearch($this->check_args[0]);
945             while ($page = $allpages->next()) {
946                 $current = $page->getCurrentRevision();
947                 $pagetext = $current->getPackedContent();
948                 $update->check($this->check_args[1], $this->check_args[2], $pagetext, $page, $current);
949             }
950         }
951         free($allpages);
952         unset($pagetext);
953         unset($current);
954         unset($page);
955     }
956
957     /**
958      * preg_replace over local file.
959      * Only line-orientated matches possible.
960      */
961     function fixLocalFile($match, $replace, $filename)
962     {
963         $o_filename = $filename;
964         if (!file_exists($filename))
965             $filename = FindFile($filename);
966         if (!file_exists($filename))
967             return array(false, sprintf(_("File “%s” not found."), $o_filename));
968         $found = false;
969         if (is_writable($filename)) {
970             $in = fopen($filename, "rb");
971             $out = fopen($tmp = tempnam(getUploadFilePath(), "cfg"), "wb");
972             if (isWindows())
973                 $tmp = str_replace("/", "\\", $tmp);
974             // Detect the existing linesep at first line. fgets strips it even if 'rb'.
975             // Before we simply assumed \r\n on Windows local files.
976             $s = fread($in, 1024);
977             rewind($in);
978             $linesep = (substr_count($s, "\r\n") > substr_count($s, "\n")) ? "\r\n" : "\n";
979             //$linesep = isWindows() ? "\r\n" : "\n";
980             while ($s = fgets($in)) {
981                 // =>php-5.0.1 can fill count
982                 //$new = preg_replace($match, $replace, $s, -1, $count);
983                 $new = preg_replace($match, $replace, $s);
984                 if ($new != $s) {
985                     $s = $new . $linesep;
986                     $found = true;
987                 }
988                 fputs($out, $s);
989             }
990             fclose($in);
991             fclose($out);
992             if (!$found) {
993                 // todo: skip
994                 $reason = sprintf(_("%s not found in %s"), $match, $filename);
995                 unlink($out);
996                 return array($found, $reason);
997             } else {
998                 @unlink("$file.bak");
999                 @rename($file, "$file.bak");
1000                 if (!rename($tmp, $file))
1001                     return array(false, sprintf(_("couldn't move %s to %s"), $tmp, $filename));
1002                 return true;
1003             }
1004         } else {
1005             return array(false, sprintf(_("file %s is not writable"), $filename));
1006         }
1007     }
1008
1009     function CheckConfigUpdate()
1010     {
1011         echo "<h2>", sprintf(_("Check for necessary %s updates"),
1012             "config.ini"), "</h2>\n";
1013         $entry = new UpgradeConfigEntry
1014         ($this, array('key' => 'cache_control_none',
1015             'fixed_with' => 1012.0,
1016             'header' => sprintf(_("Check for %s"), "CACHE_CONTROL = NONE"),
1017             'applicable_args' => 'CACHE_CONTROL',
1018             'notice' => _("CACHE_CONTROL is set to 'NONE', and must be changed to 'NO_CACHE'"),
1019             'check_args' => array("/^\s*CACHE_CONTROL\s*=\s*NONE/", "CACHE_CONTROL = NO_CACHE")));
1020         $entry->setApplicableCb(new WikiMethodCb($entry, '_applicable_defined_and_empty'));
1021         $this->_configUpdates[] = $entry;
1022
1023         $entry = new UpgradeConfigEntry
1024         ($this, array('key' => 'group_method_none',
1025             'fixed_with' => 1012.0,
1026             'header' => sprintf(_("Check for %s"), "GROUP_METHOD = NONE"),
1027             'applicable_args' => 'GROUP_METHOD',
1028             'notice' => _("GROUP_METHOD is set to NONE, and must be changed to \"NONE\""),
1029             'check_args' => array("/^\s*GROUP_METHOD\s*=\s*NONE/", "GROUP_METHOD = \"NONE\"")));
1030         $entry->setApplicableCb(new WikiMethodCb($entry, '_applicable_defined_and_empty'));
1031         $this->_configUpdates[] = $entry;
1032
1033         $entry = new UpgradeConfigEntry
1034         ($this, array('key' => 'blog_empty_default_prefix',
1035             'fixed_with' => 1013.0,
1036             'header' => sprintf(_("Check for %s"), "BLOG_EMPTY_DEFAULT_PREFIX"),
1037             'applicable_args' => 'BLOG_EMPTY_DEFAULT_PREFIX',
1038             'notice' => _("fix BLOG_EMPTY_DEFAULT_PREFIX into BLOG_DEFAULT_EMPTY_PREFIX"),
1039             'check_args' => array("/BLOG_EMPTY_DEFAULT_PREFIX\s*=/", "BLOG_DEFAULT_EMPTY_PREFIX =")));
1040         $entry->setApplicableCb(new WikiMethodCb($entry, '_applicable_defined'));
1041         $this->_configUpdates[] = $entry;
1042
1043         // TODO: find extra file updates
1044         if (empty($this->_configUpdates)) return;
1045         foreach ($this->_configUpdates as $update) {
1046             $update->check();
1047         }
1048     }
1049
1050 } // class Upgrade
1051
1052 class UpgradeEntry
1053 {
1054     /**
1055      * Add an upgrade item to be checked.
1056      *
1057      * @param $parent object The parent Upgrade class to inherit the version properties
1058      * @param $key string    A short unique key to store success in the WikiDB
1059      * @param $fixed_with double @see phpwiki_version() number
1060      * @param $header string Optional header to be printed always even if not applicable
1061      * @param $applicable WikiCallback Optional callback boolean applicable()
1062      * @param $notice string Description of the check
1063      * @param $method WikiCallback Optional callback array method(array)
1064      * //param All other args are passed to $method
1065      */
1066     function UpgradeEntry(&$parent, $params)
1067     {
1068         $this->parent =& $parent; // get the properties db_version
1069         foreach (array('key' => 'required',
1070                      // the wikidb stores the version when we actually fixed that.
1071                      'fixed_with' => 'required',
1072                      'header' => '', // always printed
1073                      'applicable_cb' => null, // method to check if applicable
1074                      'applicable_args' => array(), // might be the config name
1075                      'notice' => '',
1076                      'check_cb' => null, // method to apply
1077                      'check_args' => array())
1078                  as $k => $v) {
1079             if (!isset($params[$k])) { // default
1080                 if ($v == 'required') trigger_error("Required arg $k missing", E_USER_ERROR);
1081                 else $this->{$k} = $v;
1082             } else {
1083                 $this->{$k} = $params[$k];
1084             }
1085         }
1086         if (!is_array($this->applicable_args)) // single arg convenience shortcut
1087             $this->applicable_args = array($this->applicable_args);
1088         if (!is_array($this->check_args)) // single arg convenience shortcut
1089             $this->check_args = array($this->check_args);
1090         if ($this->notice === '' and count($this->applicable_args) > 0)
1091             $this->notice = 'Check for ' . join(', ', $this->applicable_args);
1092         $this->_db_key = "_upgrade";
1093         $this->upgrade = $this->parent->dbi->get($this->_db_key);
1094     }
1095
1096     /* needed ? */
1097     function setApplicableCb($object)
1098     {
1099         $this->applicable_cb =& $object;
1100     }
1101
1102     function _check_if_already_fixed()
1103     {
1104         // not yet fixed?
1105         if (!isset($this->upgrade['name'])) return false;
1106         // override with force?
1107         if ($this->parent->request->getArg('force')) return false;
1108         // already fixed and with an ok version
1109         if ($this->upgrade['name'] >= $this->fixed_with) return $this->upgrade['name'];
1110         // already fixed but with an older version. do it again.
1111         return false;
1112     }
1113
1114     function pass()
1115     {
1116         // store in db no to fix again
1117         $this->upgrade['name'] = $this->parent->phpwiki_version;
1118         $this->parent->dbi->set($this->_db_key, $this->upgrade);
1119         echo "<b>", _("FIXED"), "</b>";
1120         if (isset($this->reason))
1121             echo _(": "), $this->reason;
1122         echo "<br />\n";
1123         flush();
1124         return true;
1125     }
1126
1127     function fail()
1128     {
1129         echo '<span style="color: red; font-weight: bold; ">' . _("FAILED") . "</span>";
1130         if (isset($this->reason))
1131             echo _(": "), $this->reason;
1132         echo "<br />\n";
1133         flush();
1134         return false;
1135     }
1136
1137     function skip()
1138     { // not applicable
1139         if (isset($this->silent_skip)) return true;
1140         echo " " . _("Skipped.") . "<br />\n";
1141         flush();
1142         return true;
1143     }
1144
1145     function check($args = null)
1146     {
1147         if ($this->header) echo $this->header, ' ... ';
1148         if ($when = $this->_check_if_already_fixed()) {
1149             // be totally silent if no header is defined.
1150             if ($this->header) echo _("fixed with"), " ", $when, "<br />\n";
1151             flush();
1152             return true;
1153         }
1154         if (is_object($this->applicable_cb)) {
1155             if (!$this->applicable_cb->call_array($this->applicable_args))
1156                 return $this->skip();
1157         }
1158         if ($this->notice) {
1159             if ($this->header)
1160                 echo "<br />\n";
1161             echo $this->notice, " ";
1162             flush();
1163         }
1164         if (!is_null($args)) $this->check_args =& $args;
1165         if (is_object($this->check_cb))
1166             $do = $this->method_cb->call_array($this->check_args);
1167         else
1168             $do = $this->default_method($this->check_args);
1169         if (is_array($do)) {
1170             $this->reason = $do[1];
1171             $do = $do[0];
1172         }
1173         return $do ? $this->pass() : $this->fail();
1174     }
1175 } // class UpgradeEntry
1176
1177 class UpgradeConfigEntry extends UpgradeEntry
1178 {
1179     function _applicable_defined()
1180     {
1181         return (boolean)defined($this->applicable_args[0]);
1182     }
1183
1184     function _applicable_defined_and_empty()
1185     {
1186         $const = $this->applicable_args[0];
1187         return (boolean)(defined($const) and !constant($const));
1188     }
1189
1190     function default_method($args)
1191     {
1192         $match = $args[0];
1193         $replace = $args[1];
1194         return $this->parent->fixLocalFile($match, $replace, "config/config.ini");
1195     }
1196 } // class UpdateConfigEntry
1197
1198 /* This is different */
1199 class UpgradePluginEntry extends UpgradeEntry
1200 {
1201
1202     /**
1203      * check all pages for a plugin match
1204      */
1205     var $silent_skip = 1;
1206
1207     function default_method(&$args)
1208     {
1209         $match = $args[0];
1210         $replace = $args[1];
1211         $pagetext =& $args[2];
1212         $page =& $args[3];
1213         $current =& $args[4];
1214         if (preg_match($match, $pagetext)) {
1215             echo $page->getName(), " ", $this->notice, " ... ";
1216             if ($newtext = preg_replace($match, $replace, $pagetext)) {
1217                 $meta = $current->_data;
1218                 $meta['summary'] = "upgrade: " . $this->header;
1219                 $page->save($newtext, $current->getVersion() + 1, $meta);
1220                 $this->pass();
1221             } else {
1222                 $this->fail();
1223             }
1224         }
1225     }
1226 } // class UpdatePluginEntry
1227
1228 /**
1229  * fix custom themes which are not in our distribution
1230  * this should be optional
1231  */
1232 class UpgradeThemeEntry extends UpgradeEntry
1233 {
1234
1235     function default_method(&$args)
1236     {
1237         $match = $args[0];
1238         $replace = $args[1];
1239         $template = $args[2];
1240     }
1241
1242     function fixThemeTemplate($match, $new, $template)
1243     {
1244         // for all custom themes
1245         $ourthemes = explode(":", "blog:Crao:default:Hawaiian:MacOSX:MonoBook:Portland:shamino_com:SpaceWiki:wikilens:Wordpress");
1246         $themedir = NormalizeLocalFileName("themes");
1247         $dh = opendir($themedir);
1248         while ($r = readdir($dh)) {
1249             if (filetype($r) == 'dir' and $r[0] != '.' and !is_array($r, $ourthemes))
1250                 $customthemes[] = $r;
1251         }
1252         $success = true;
1253         $errors = '';
1254         foreach ($customthemes as $customtheme) {
1255             $template = FindFile("themes/$customtheme/templates/$template");
1256             $do = $this->parent->fixLocalFile($match, $new, template);
1257             if (!$do[0]) {
1258                 $success = false;
1259                 $errors .= $do[1] . " ";
1260                 echo $do[1];
1261             }
1262         }
1263         return array($success, $errors);
1264     }
1265 }
1266
1267 /**
1268  * TODO:
1269  *
1270  * Upgrade: Base class for multipage worksteps
1271  * identify, validate, display options, next step
1272  */
1273 /*
1274 */
1275
1276 // TODO: At which step are we?
1277 // validate and do it again or go on with next step.
1278
1279 /** entry function from lib/main.php
1280  */
1281 function DoUpgrade(&$request)
1282 {
1283
1284     if (!$request->_user->isAdmin()) {
1285         $request->_notAuthorized(WIKIAUTH_ADMIN);
1286         $request->finish(
1287             HTML::div(array('class' => 'disabled-plugin'),
1288                 fmt("Upgrade disabled: user != isAdmin")));
1289         return;
1290     }
1291     // TODO: StartLoadDump should turn on implicit_flush.
1292     @ini_set("implicit_flush", true);
1293     StartLoadDump($request, _("Upgrading this PhpWiki"));
1294     $upgrade = new Upgrade($request);
1295     //if (!$request->getArg('noindex'))
1296     //    CheckOldIndexUpdate($request); // index.php => config.ini to upgrade from < 1.3.10
1297     if (!$request->getArg('nodb')) {
1298         $upgrade->CheckDatabaseUpdate($request); // first check cached_html and friends
1299     }
1300     if (!$request->getArg('nopgsrc')) {
1301         $upgrade->CheckPgsrcUpdate($request);
1302         $upgrade->CheckActionPageUpdate($request);
1303     }
1304     // if (!$request->getArg('noplugin')) {
1305         // $upgrade->CheckPluginUpdate($request);
1306     // }
1307     if (!$request->getArg('noconfig')) {
1308         $upgrade->CheckConfigUpdate($request);
1309     }
1310     // This is optional and should be linked. In EndLoadDump or PhpWikiAdministration?
1311     //if ($request->getArg('theme'))
1312     //      $upgrade->CheckThemeUpdate($request);
1313     EndLoadDump($request);
1314 }
1315
1316 // Local Variables:
1317 // mode: php
1318 // tab-width: 8
1319 // c-basic-offset: 4
1320 // c-hanging-comment-ender-p: nil
1321 // indent-tabs-mode: nil
1322 // End: