4 * Copyright 2004,2005,2006,2007 $ThePhpWikiProgrammingTeam
5 * Copyright 2008 Marc-Etienne Vargenau, Alcatel-Lucent
7 * This file is part of PhpWiki.
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.
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.
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.
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.
29 * Installation on an existing PhpWiki database needs some
30 * additional worksteps. Each step will require multiple pages.
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
46 * Done: overwrite=1 link on edit conflicts at first occurence "Overwrite all".
48 * @author: Reini Urban
50 require_once 'lib/loadsave.php';
54 public $current_db_version;
56 public $_configUpdates;
59 function __construct(&$request)
61 $this->request =& $request;
62 $this->dbi =& $request->_dbi; // no reference for dbadmin ?
63 $this->phpwiki_version = $this->current_db_version = phpwiki_version();
64 //$this->current_db_version = 1030.13; // should be stored in the db. should be phpwiki_version
66 $this->db_version = $this->dbi->get_db_version();
67 $this->isSQL = $this->dbi->_backend->isSQL();
70 private function doPgsrcUpdate($pagename, $path, $filename)
72 // don't ever update the HomePage
73 if ((defined(HOME_PAGE) and ($pagename == HOME_PAGE))
74 or ($pagename == _("HomePage"))
75 or ($pagename == "HomePage")
77 echo "$path/$pagename: " . _("always skip the HomePage.") . " " . _("Skipped."), "<br />\n";
81 $page = $this->dbi->getPage($pagename);
82 if ($page->exists()) {
83 // check mtime: update automatically if pgsrc is newer
84 $rev = $page->getCurrentRevision();
85 $page_mtime = $rev->get('mtime');
86 $data = implode("", file($path . "/" . $filename));
87 if (($parts = ParseMimeifiedPages($data))) {
88 usort($parts, 'SortByPageVersion');
90 $pageinfo = $parts[0];
91 $stat = stat($path . "/" . $filename);
93 if (isset($pageinfo['versiondata']['mtime']))
94 $new_mtime = $pageinfo['versiondata']['mtime'];
95 if (!$new_mtime and isset($pageinfo['versiondata']['lastmodified']))
96 $new_mtime = $pageinfo['versiondata']['lastmodified'];
97 if (!$new_mtime and isset($pageinfo['pagedata']['date']))
98 $new_mtime = $pageinfo['pagedata']['date'];
100 $new_mtime = $stat[9];
101 if ($new_mtime > $page_mtime) {
102 echo "$path/$pagename" . _(": ") . _("newer than the existing page.")
103 . " " . _("Replace") . " " . "($new_mtime > $page_mtime)" . "<br />\n";
104 LoadAny($this->request, $path . "/" . $filename);
107 echo "$path/$pagename" . _(": ") . _("older than the existing page.")
108 . " " . _("Skipped."), "<br />\n";
111 echo "$path/$pagename" . _(": ") . _("unknown format.") . " " . _("Skipped.") . "<br />\n";
114 echo sprintf(_("%s does not exist"), $pagename), "<br />\n";
115 LoadAny($this->request, $path . "/" . $filename);
120 public function CheckActionPageUpdate()
122 echo "<h2>", sprintf(_("Check for necessary %s updates"), _("Action Pages")), "</h2>\n";
123 // 1.3.13 before we pull in all missing pages, we rename existing ones
124 $this->_rename_page_helper("_AuthInfo", "DebugAuthInfo");
125 $this->_rename_page_helper("Help/_AuthInfoPlugin", "Help/DebugAuthInfoPlugin");
126 $this->_rename_page_helper("_GroupInfo", "DebugGroupInfo");
127 $this->_rename_page_helper("Help/_GroupInfoPlugin", "Help/DebugGroupInfoPlugin");
128 $this->_rename_page_helper("_BackendInfo", "DebugBackendInfo");
129 $this->_rename_page_helper("Help/_BackendInfoPlugin", "Help/DebugBackendInfoPlugin");
130 $this->_rename_page_helper("Help/_WikiTranslationPlugin", "Help/WikiTranslationPlugin");
131 $this->_rename_page_helper("Help/Advice Mediawiki users", "Help/Advice for Mediawiki users");
132 // this is in some templates. so we keep the old name
133 //$this->_rename_page_helper($this->dbi, _("DebugInfo"), _("DebugBackendInfo"));
134 $this->_rename_page_helper("_GroupInfo", "GroupAuthInfo"); //never officially existed
135 $this->_rename_page_helper("InterWikiKarte", "InterWikiListe"); // german only
137 $path = FindFile('pgsrc');
138 $pgsrc = new fileSet($path);
139 // most actionpages have the same name as the plugin
140 $loc_path = FindLocalizedFile('pgsrc');
141 foreach ($pgsrc->getFiles() as $filename) {
142 if (substr($filename, -1, 1) == '~') continue;
143 if (substr($filename, -5, 5) == '.orig') continue;
144 $pagename = urldecode($filename);
145 if (isActionPage($pagename)) {
146 $translation = __($pagename);
147 if ($translation == $pagename)
148 $this->doPgsrcUpdate($pagename, $path, $filename);
149 elseif (FindLocalizedFile('pgsrc/' . urlencode($translation), 1))
150 $this->doPgsrcUpdate($translation, $loc_path, urlencode($translation)); else
151 $this->doPgsrcUpdate($pagename, $path, $filename);
156 // see loadsave.php for saving new pages.
157 public function CheckPgsrcUpdate()
159 // Check some theme specific pgsrc files (blog, wikilens, fusionforge, custom).
160 // We check theme specific pgsrc first in case the page is present in both
161 // theme specific and global pgsrc
163 $path = $WikiTheme->file("pgsrc");
164 // TBD: the call to fileSet prints a warning:
165 // Notice: Unable to open directory 'themes/MonoBook/pgsrc' for reading
166 $themepgsrc = array();
167 $pgsrc = new fileSet($path);
168 if ($pgsrc->getFiles()) {
169 echo "<h2>", sprintf(_("Check for necessary theme %s updates"),
171 foreach ($pgsrc->getFiles() as $filename) {
172 if (substr($filename, -1, 1) == '~') continue;
173 if (substr($filename, -5, 5) == '.orig') continue;
174 $pagename = urldecode($filename);
175 $themepgsrc[] = $pagename;
176 $this->doPgsrcUpdate($pagename, $path, $filename);
180 echo "<h2>", sprintf(_("Check for necessary %s updates"),
182 if ($this->db_version < 1030.12200612) {
183 echo "<h4>", _("rename to Help: pages"), "</h4>\n";
185 $translation = __("HomePage");
186 if ($translation == "HomePage") {
187 $path = FindFile(WIKI_PGSRC);
189 $path = FindLocalizedFile(WIKI_PGSRC);
191 $pgsrc = new fileSet($path);
192 // fixme: verification, ...
193 foreach ($pgsrc->getFiles() as $filename) {
194 if (substr($filename, -1, 1) == '~') continue;
195 if (substr($filename, -5, 5) == '.orig') continue;
196 $pagename = urldecode($filename);
197 if (!isActionPage($filename)) {
198 // There're a lot of now unneeded pages around.
199 // At first rename the BlaPlugin pages to Help/<pagename> and then to the update.
200 if ($this->db_version < 1030.12200612) {
201 $this->_rename_to_help_page($pagename);
203 if (in_array($pagename, $themepgsrc)) {
204 echo sprintf(_('%s already checked in theme pgsrc.'), $pagename).' '._('Skipped.').'<br />';
206 $this->doPgsrcUpdate($pagename, $path, $filename);
212 private function _rename_page_helper($oldname, $pagename)
214 echo sprintf(_("rename %s to %s"), $oldname, $pagename), " ...";
215 if ($this->dbi->isWikiPage($oldname) and !$this->dbi->isWikiPage($pagename)) {
216 if ($this->dbi->_backend->rename_page($oldname, $pagename)) {
217 echo _("OK"), " <br />\n";
219 echo ' <span style="color: red; font-weight: bold;">' . _("FAILED") . "</span><br />\n";
222 echo " " . _("Skipped.") . "<br />\n";
226 private function _rename_to_help_page($pagename)
228 $newprefix = _("Help") . "/";
229 if (substr($pagename, 0, strlen($newprefix)) != $newprefix)
231 $oldname = substr($pagename, strlen($newprefix));
232 $this->_rename_page_helper($oldname, $pagename);
236 * TODO: Search table definition in appropriate schema
238 * Supported: mysql and generic SQL, for ADODB and PearDB.
240 private function installTable($table, $backend_type)
245 echo _("MISSING"), " ... \n";
247 $schema = findFile("schemas/${backend_type}.sql");
249 echo " ",_("FAILED"),": ",sprintf(_("no schema %s found"),
250 "schemas/${backend_type}.sql")," ... <br />\n";
254 extract($this->dbi->_backend->_table_names);
255 $prefix = isset($DBParams['prefix']) ? $DBParams['prefix'] : '';
258 assert($session_tbl);
259 if ($backend_type == 'mysql') {
260 $this->dbi->genericSqlQuery("
261 CREATE TABLE $session_tbl (
262 sess_id CHAR(32) NOT NULL DEFAULT '',
263 sess_data BLOB NOT NULL,
264 sess_date INT UNSIGNED NOT NULL,
265 sess_ip CHAR(15) NOT NULL,
266 PRIMARY KEY (sess_id),
270 $this->dbi->genericSqlQuery("
271 CREATE TABLE $session_tbl (
272 sess_id CHAR(32) NOT NULL DEFAULT '',
273 sess_data " . ($backend_type == 'pgsql' ? 'TEXT' : 'BLOB') . " NOT NULL,
275 sess_ip CHAR(15) NOT NULL
277 $this->dbi->genericSqlQuery("CREATE UNIQUE INDEX sess_id ON $session_tbl (sess_id)");
279 $this->dbi->genericSqlQuery("CREATE INDEX sess_date on session (sess_date)");
280 echo " ", _("CREATED");
283 $pref_tbl = $prefix . 'pref';
284 if ($backend_type == 'mysql') {
285 $this->dbi->genericSqlQuery("
286 CREATE TABLE $pref_tbl (
287 userid CHAR(48) BINARY NOT NULL UNIQUE,
288 prefs TEXT NULL DEFAULT '',
292 $this->dbi->genericSqlQuery("
293 CREATE TABLE $pref_tbl (
294 userid CHAR(48) NOT NULL,
295 prefs TEXT NULL DEFAULT ''
297 $this->dbi->genericSqlQuery("CREATE UNIQUE INDEX userid ON $pref_tbl (userid)");
299 echo " ", _("CREATED");
302 $member_tbl = $prefix . 'member';
303 if ($backend_type == 'mysql') {
304 $this->dbi->genericSqlQuery("
305 CREATE TABLE $member_tbl (
306 userid CHAR(48) BINARY NOT NULL,
307 groupname CHAR(48) BINARY NOT NULL DEFAULT 'users',
312 $this->dbi->genericSqlQuery("
313 CREATE TABLE $member_tbl (
314 userid CHAR(48) NOT NULL,
315 groupname CHAR(48) NOT NULL DEFAULT 'users'
317 $this->dbi->genericSqlQuery("CREATE INDEX userid ON $member_tbl (userid)");
318 $this->dbi->genericSqlQuery("CREATE INDEX groupname ON $member_tbl (groupname)");
320 echo " ", _("CREATED");
323 $rating_tbl = $prefix . 'rating';
324 if ($backend_type == 'mysql') {
325 $this->dbi->genericSqlQuery("
326 CREATE TABLE $rating_tbl (
327 dimension INT(4) NOT NULL,
328 raterpage INT(11) NOT NULL,
329 rateepage INT(11) NOT NULL,
330 ratingvalue FLOAT NOT NULL,
331 rateeversion INT(11) NOT NULL,
332 tstamp TIMESTAMP(14) NOT NULL,
333 PRIMARY KEY (dimension, raterpage, rateepage)
336 $this->dbi->genericSqlQuery("
337 CREATE TABLE $rating_tbl (
338 dimension INT(4) NOT NULL,
339 raterpage INT(11) NOT NULL,
340 rateepage INT(11) NOT NULL,
341 ratingvalue FLOAT NOT NULL,
342 rateeversion INT(11) NOT NULL,
343 tstamp TIMESTAMP(14) NOT NULL
345 $this->dbi->genericSqlQuery("CREATE UNIQUE INDEX rating"
346 . " ON $rating_tbl (dimension, raterpage, rateepage)");
348 echo " ", _("CREATED");
351 $log_tbl = $prefix . 'accesslog';
352 // fields according to http://www.outoforder.cc/projects/apache/mod_log_sql/docs-2.0/#id2756178
354 A User Agent agent varchar(255) Mozilla/4.0 (compat; MSIE 6.0; Windows)
355 a CGi request arguments request_args varchar(255) user=Smith&cart=1231&item=532
356 b Bytes transfered bytes_sent int unsigned 32561
357 c??? Text of cookie cookie varchar(255) Apache=sdyn.fooonline.net 1300102700823
358 f Local filename requested request_file varchar(255) /var/www/html/books-cycroad.html
359 H HTTP request_protocol request_protocol varchar(10) HTTP/1.1
360 h Name of remote host remote_host varchar(50) blah.foobar.com
361 I Request ID (from modd_unique_id) id char(19) POlFcUBRH30AAALdBG8
362 l Ident user info remote_logname varcgar(50) bobby
363 M Machine ID??? machine_id varchar(25) web01
364 m HTTP request method request_method varchar(10) GET
365 P httpd cchild PID child_pid smallint unsigned 3215
366 p http port server_port smallint unsigned 80
367 R Referer referer varchar(255) http://www.biglinks4u.com/linkpage.html
368 r Request in full form request_line varchar(255) GET /books-cycroad.html HTTP/1.1
369 S Time of request in UNIX time_t format time_stamp int unsigned 1005598029
370 T Seconds to service request request_duration smallint unsigned 2
371 t Time of request in human format request_time char(28) [02/Dec/2001:15:01:26 -0800]
372 U Request in simple form request_uri varchar(255) /books-cycroad.html
373 u User info from HTTP auth remote_user varchar(50) bobby
374 v Virtual host servicing the request virtual_host varchar(255)
376 $this->dbi->genericSqlQuery("
377 CREATE TABLE $log_tbl (
378 time_stamp int unsigned,
379 remote_host varchar(100),
380 remote_user varchar(50),
381 request_method varchar(10),
382 request_line varchar(255),
383 request_args varchar(255),
384 request_uri varchar(255),
385 request_time char(28),
386 status smallint unsigned,
387 bytes_sent smallint unsigned,
388 referer varchar(255),
390 request_duration float
392 $this->dbi->genericSqlQuery("CREATE INDEX log_time ON $log_tbl (time_stamp)");
393 $this->dbi->genericSqlQuery("CREATE INDEX log_host ON $log_tbl (remote_host)");
394 echo " ", _("CREATED");
401 * Update from ~1.3.4 to current.
402 * tables: Only session, user, pref and member
403 * jeffs-hacks database api (around 1.3.2) later:
404 * people should export/import their pages if using that old versions.
406 public function CheckDatabaseUpdate()
410 echo "<h2>", sprintf(_("Check for necessary %s updates"),
412 " - ", DATABASE_TYPE, "</h2>\n";
413 $dbadmin = $this->request->getArg('dbadmin');
416 if (isset($dbadmin['cancel'])) {
417 echo _("Cancel"), " <br />\n";
421 echo _("db version: we want "), $this->current_db_version, "\n<br />";
422 echo _("db version: we have "), $this->db_version, "\n<br />";
423 if ($this->db_version >= $this->current_db_version) {
424 echo _("OK"), "<br />\n";
428 $backend_type = $this->dbi->_backend->backendType();
430 echo "<h4>", _("Backend type: "), $backend_type, "</h4>\n";
431 $prefix = isset($DBParams['prefix']) ? $DBParams['prefix'] : '';
432 $tables = $this->dbi->_backend->listOfTables();
433 foreach (explode(':', 'session:pref:member') as $table) {
434 echo sprintf(_("Check for table %s"), $table), " ...";
435 if (!in_array($prefix . $table, $tables)) {
436 $this->installTable($table, $backend_type);
438 echo _("OK"), " <br />\n";
443 if ($this->phpwiki_version >= 1030.12200612 and $this->db_version < 1030.13) {
444 if ($this->isSQL and preg_match("/(pgsql|postgres)/", $backend_type)) {
445 trigger_error(_("You need to upgrade to schema/psql-initialize.sql manually!"),
447 // $this->_upgrade_psql_tsearch2();
449 $this->_upgrade_relation_links();
452 if (ACCESS_LOG_SQL and $this->isSQL) {
453 $table = "accesslog";
454 echo sprintf(_("Check for table %s"), $table), " ...";
455 if (!in_array($prefix . $table, $tables)) {
456 $this->installTable($table, $backend_type);
458 echo _("OK"), " <br />\n";
461 if ($this->isSQL and (class_exists("RatingsUserFactory") or $this->dbi->isWikiPage(_("RateIt")))) {
463 echo sprintf(_("Check for table %s"), $table), " ...";
464 if (!in_array($prefix . $table, $tables)) {
465 $this->installTable($table, $backend_type);
467 echo _("OK"), " <br />\n";
470 $backend = &$this->dbi->_backend->_dbh;
472 extract($this->dbi->_backend->_table_names);
474 // 1.3.8 added session.sess_ip
475 if ($this->isSQL and $this->phpwiki_version >= 1030.08 and USE_DB_SESSION
476 and isset($this->request->_dbsession)
478 echo _("Check for new session.sess_ip column"), " ... ";
479 $database = $this->dbi->_backend->database();
480 assert(!empty($DBParams['db_session_table']));
481 $session_tbl = $prefix . $DBParams['db_session_table'];
482 $sess_fields = $this->dbi->_backend->listOfFields($database, $session_tbl);
485 } elseif (!strstr(strtolower(join(':', $sess_fields)), "sess_ip")) {
486 // TODO: postgres test (should be able to add columns at the end, but not in between)
487 echo "<b>", _("ADDING"), "</b>", " ... ";
488 $this->dbi->genericSqlQuery("ALTER TABLE $session_tbl ADD sess_ip CHAR(15) NOT NULL");
489 $this->dbi->genericSqlQuery("CREATE INDEX sess_date ON $session_tbl (sess_date)");
494 if (substr($backend_type, 0, 5) == 'mysql') {
495 // upgrade to 4.1.8 destroyed my session table:
496 // sess_id => varchar(10), sess_data => varchar(5). For others obviously also.
497 echo _("Check for mysql session.sess_id sanity"), " ... ";
498 $result = $this->dbi->genericSqlQuery("DESCRIBE $session_tbl");
499 if (DATABASE_TYPE == 'SQL') {
500 $iter = new WikiDB_backend_PearDB_generic_iter($backend, $result);
501 } elseif (DATABASE_TYPE == 'ADODB') {
502 $iter = new WikiDB_backend_ADODB_generic_iter($backend, $result,
503 array("Field", "Type", "Null", "Key", "Default", "Extra"));
504 } elseif (DATABASE_TYPE == 'PDO') {
505 $iter = new WikiDB_backend_PDO_generic_iter($backend, $result);
507 while ($col = $iter->next()) {
508 if ($col["Field"] == 'sess_id' and !strstr(strtolower($col["Type"]), 'char(32)')) {
509 $this->dbi->genericSqlQuery("ALTER TABLE $session_tbl CHANGE sess_id"
510 . " sess_id CHAR(32) NOT NULL");
511 echo "sess_id ", $col["Type"], " ", _("fixed"), " => CHAR(32) ";
513 if ($col["Field"] == 'sess_ip' and !strstr(strtolower($col["Type"]), 'char(15)')) {
514 $this->dbi->genericSqlQuery("ALTER TABLE $session_tbl CHANGE sess_ip"
515 . " sess_ip CHAR(15) NOT NULL");
516 echo "sess_ip ", $col["Type"], " ", _("fixed"), " => CHAR(15) ";
519 echo _("OK"), "<br />\n";
524 ALTER TABLE link ADD relation INT DEFAULT 0;
525 CREATE INDEX linkrelation ON link (relation);
528 // mysql >= 4.0.4 requires LOCK TABLE privileges
529 if (substr($backend_type, 0, 5) == 'mysql') {
530 echo _("Check for mysql LOCK TABLE privilege"), " ...";
531 $mysql_version = $this->dbi->_backend->_serverinfo['version'];
532 if ($mysql_version > 400.40) {
533 if (!empty($this->dbi->_backend->_parsedDSN))
534 $parseDSN = $this->dbi->_backend->_parsedDSN;
535 elseif (function_exists('parseDSN')) // ADODB or PDO
536 $parseDSN = parseDSN($DBParams['dsn']); else // pear
537 $parseDSN = DB::parseDSN($DBParams['dsn']);
538 $username = $this->dbi->_backend->qstr($parseDSN['username']);
540 $query = "SELECT lock_tables_priv FROM mysql.db WHERE user='$username'";
541 //mysql_select_db("mysql", $this->dbi->_backend->connection());
542 $db_fields = $this->dbi->_backend->listOfFields("mysql", "db");
543 if (!strstr(strtolower(join(':', $db_fields)), "lock_tables_priv")) {
544 echo join(':', $db_fields);
545 die("lock_tables_priv missing. The DB Admin must run mysql_fix_privilege_tables");
547 $row = $this->dbi->_backend->getRow($query);
548 if (isset($row[0]) and $row[0] == 'N') {
549 $this->dbi->genericSqlQuery("UPDATE mysql.db SET lock_tables_priv='Y'"
550 . " WHERE mysql.user='$username'");
551 $this->dbi->genericSqlQuery("FLUSH PRIVILEGES");
552 echo "mysql.db user='$username'", _("fixed"), "<br />\n";
555 $query = "SELECT lock_tables_priv FROM mysql.user WHERE user='$username'";
556 $row = $this->dbi->_backend->getRow($query);
557 if ($row and $row[0] == 'N') {
558 $this->dbi->genericSqlQuery("UPDATE mysql.user SET lock_tables_priv='Y'"
559 . " WHERE mysql.user='$username'");
560 $this->dbi->genericSqlQuery("FLUSH PRIVILEGES");
561 echo "mysql.user user='$username'", _("fixed"), "<br />\n";
563 echo ' <span style="color: red; font-weight: bold;">' . _("FAILED") . "</span>"
564 . " Neither mysql.db nor mysql.user has a user='$username'"
565 . " or the lock_tables_priv field",
568 echo _("OK"), "<br />\n";
571 echo _("OK"), "<br />\n";
573 //mysql_select_db($this->dbi->_backend->database(), $this->dbi->_backend->connection());
575 echo sprintf(_("version <em>%s</em> not affected"), $mysql_version), "<br />\n";
579 // 1.3.10 mysql requires page.id auto_increment
580 // mysql, mysqli or mysqlt
581 if ($this->phpwiki_version >= 1030.099 and substr($backend_type, 0, 5) == 'mysql'
582 and DATABASE_TYPE != 'PDO'
584 echo _("Check for mysql page.id auto_increment flag"), " ...";
585 assert(!empty($page_tbl));
586 $database = $this->dbi->_backend->database();
587 $fields = mysql_list_fields($database, $page_tbl, $this->dbi->_backend->connection());
588 $columns = mysql_num_fields($fields);
589 for ($i = 0; $i < $columns; $i++) {
590 if (mysql_field_name($fields, $i) == 'id') {
591 $flags = mysql_field_flags($fields, $i);
592 //DONE: something was wrong with ADODB here.
593 if (!strstr(strtolower($flags), "auto_increment")) {
594 echo "<b>", _("ADDING"), "</b>", " ... ";
595 // MODIFY col_def valid since mysql 3.22.16,
596 // older mysql's need CHANGE old_col col_def
597 $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl CHANGE id"
598 . " id INT NOT NULL AUTO_INCREMENT");
599 $fields = mysql_list_fields($database, $page_tbl);
600 if (!strstr(strtolower(mysql_field_flags($fields, $i)), "auto_increment"))
601 echo ' <span style="color: red; font-weight: bold;">' . _("FAILED") . "</span><br />\n";
603 echo _("OK"), "<br />\n";
605 echo _("OK"), "<br />\n";
610 mysql_free_result($fields);
613 // Check for mysql 4.1.x/5.0.0a binary search problem.
614 // http://bugs.mysql.com/bug.php?id=4398
615 // "select * from page where LOWER(pagename) like '%search%'" does not apply LOWER!
616 // Confirmed for 4.1.0alpha,4.1.3-beta,5.0.0a; not yet tested for 4.1.2alpha,
617 // On windows only, though utf8 would be useful elsewhere also.
618 // Illegal mix of collations (latin1_bin,IMPLICIT) and
619 // (utf8_general_ci, COERCIBLE) for operation '='])
620 if (isWindows() and substr($backend_type, 0, 5) == 'mysql') {
621 echo _("Check for mysql 4.1.x/5.0.0 binary search on Windows problem"), " ...";
622 $mysql_version = $this->dbi->_backend->_serverinfo['version'];
623 if ($mysql_version < 401.0) {
624 echo sprintf(_("version <em>%s</em>"), $mysql_version), " ",
625 _("not affected"), "<br />\n";
626 } elseif ($mysql_version >= 401.6) { // FIXME: since which version?
627 $row = $this->dbi->_backend->getRow("SHOW CREATE TABLE $page_tbl");
628 $result = join(" ", $row);
629 if (strstr(strtolower($result), "character set")
630 and strstr(strtolower($result), "collate")
632 echo _("OK"), "<br />\n";
635 $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl CHANGE pagename "
636 . "pagename VARCHAR(100) "
637 . "CHARACTER SET '$charset' COLLATE '$charset" . "_bin' NOT NULL");
638 echo sprintf(_("version <em>%s</em>"), $mysql_version),
639 " <b>", _("FIXED"), "</b>",
642 } elseif (DATABASE_TYPE != 'PDO') {
643 // check if already fixed
644 extract($this->dbi->_backend->_table_names);
645 assert(!empty($page_tbl));
646 $database = $this->dbi->_backend->database();
647 $fields = mysql_list_fields($database, $page_tbl, $this->dbi->_backend->connection());
648 $columns = mysql_num_fields($fields);
649 for ($i = 0; $i < $columns; $i++) {
650 if (mysql_field_name($fields, $i) == 'pagename') {
651 $flags = mysql_field_flags($fields, $i);
652 // I think it was fixed with 4.1.6, but I tested it only with 4.1.8
653 if ($mysql_version > 401.0 and $mysql_version < 401.6) {
654 // remove the binary flag
655 if (strstr(strtolower($flags), "binary")) {
656 // FIXME: on duplicate pagenames this will fail!
657 $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl CHANGE pagename"
658 . " pagename VARCHAR(100) NOT NULL");
659 echo sprintf(_("version <em>%s</em>"), $mysql_version),
660 "<b>", _("FIXED"), "</b>"
669 if ($this->isSQL and ACCESS_LOG_SQL & 2) {
670 echo _("Check for ACCESS_LOG_SQL passwords in POST requests"), " ...";
671 // Don't display passwords in POST requests (up to 2005-02-04 12:03:20)
672 $res = $this->dbi->genericSqlIter("SELECT time_stamp, remote_host, " .
673 "request_args FROM ${prefix}accesslog WHERE request_args LIKE " .
674 "'%s:6:\"passwd\"%' AND request_args NOT LIKE '%s:6:\"passwd\";" .
675 "s:15:\"<not displayed>\"%'");
677 while ($row = $res->next()) {
678 $args = preg_replace("/(s:6:\"passwd\";s:15:\").*(\")/",
679 "$1<not displayed>$2", $row["request_args"]);
680 $ts = $row["time_stamp"];
681 $rh = $row["remote_host"];
682 $this->dbi->genericSqlQuery("UPDATE ${prefix}accesslog SET " .
683 "request_args='$args' WHERE time_stamp=$ts AND " .
684 "remote_host='$rh'");
688 echo "<b>", _("FIXED"), "</b>", "<br />\n";
690 echo _("OK"), "<br />\n";
692 if ($this->phpwiki_version >= 1030.13) {
693 echo _("Check for ACCESS_LOG_SQL remote_host varchar(50)"), " ...";
694 $database = $this->dbi->_backend->database();
695 $accesslog_tbl = $prefix . 'accesslog';
696 $fields = $this->dbi->_backend->listOfFields($database, $accesslog_tbl);
699 } elseif (strstr(strtolower(join(':', $sess_fields)), "remote_host")) {
700 // TODO: how to check size, already done?
701 echo "<b>", _("FIXING"), "remote_host</b>", " ... ";
702 $this->dbi->genericSqlQuery("ALTER TABLE $accesslog_tbl CHANGE remote_host VARCHAR(100)");
709 $this->_upgrade_cached_html();
711 if ($this->db_version < $this->current_db_version) {
712 $this->dbi->set_db_version($this->current_db_version);
713 $this->db_version = $this->dbi->get_db_version();
714 echo "db version: upgrade to ", $this->db_version, " ";
715 echo _("OK"), "<br />\n";
721 * Filter SQL missing permissions errors.
723 * A wrong DBADMIN user will not be able to connect
724 * @see _is_false_error, ErrorManager
726 public function _dbpermission_filter($err)
728 if ($err->isWarning()) {
729 global $ErrorManager;
730 $this->error_caught = 1;
731 $ErrorManager->_postponed_errors[] = $err;
737 private function _try_dbadmin_user($user, $passwd)
740 $AdminParams = $DBParams;
741 if (DATABASE_TYPE == 'SQL')
742 $dsn = DB::parseDSN($AdminParams['dsn']);
744 $dsn = parseDSN($AdminParams['dsn']);
746 $AdminParams['dsn'] = sprintf("%s://%s:%s@%s/%s",
752 $AdminParams['_tryroot_from_upgrade'] = 1;
753 // add error handler to warn about missing permissions for DBADMIN_USER
754 global $ErrorManager;
755 $ErrorManager->pushErrorHandler(new WikiMethodCb($this, '_dbpermission_filter'));
756 $this->error_caught = 0;
757 $this->dbi = WikiDB::open($AdminParams);
758 if (!$this->error_caught)
760 // FAILED: redo our connection with the wikiuser
761 $this->dbi = WikiDB::open($DBParams);
762 $ErrorManager->flushPostponedErrors();
763 $ErrorManager->popErrorHandler();
767 private function _db_init()
772 /* SQLite never needs admin params */
773 $backend_type = $this->dbi->_backend->backendType();
774 if (substr($backend_type, 0, 6) == "sqlite") {
777 $dbadmin_user = 'root';
778 if ($dbadmin = $this->request->getArg('dbadmin')) {
779 $dbadmin_user = $dbadmin['user'];
780 if (isset($dbadmin['cancel'])) {
782 } elseif (!empty($dbadmin_user)) {
783 if ($this->_try_dbadmin_user($dbadmin['user'], $dbadmin['passwd']))
786 } elseif (DBADMIN_USER) {
787 if ($this->_try_dbadmin_user(DBADMIN_USER, DBADMIN_PASSWD)) {
791 // Check if the privileges are enough. Need CREATE and ALTER perms.
792 // And on Windows: SELECT FROM mysql, possibly: UPDATE mysql.
793 $form = HTML::form(array("method" => "post",
794 "action" => $this->request->getPostURL(),
795 "accept-charset" => 'UTF-8'),
796 HTML::p(_("Upgrade requires database privileges to CREATE and ALTER the phpwiki database."),
798 _("And on Windows at least the privilege to SELECT FROM mysql, and possibly UPDATE mysql")),
799 HiddenInputs(array('action' => 'upgrade',
800 'overwrite' => $this->request->getArg('overwrite'))),
801 HTML::table(array("cellspacing" => 4),
802 HTML::tr(HTML::td(array('align' => 'right'),
803 _("DB admin user:")),
804 HTML::td(HTML::input(array('name' => "dbadmin[user]",
807 'value' => $dbadmin_user)))),
808 HTML::tr(HTML::td(array('align' => 'right'),
809 _("DB admin password:")),
810 HTML::td(HTML::input(array('name' => "dbadmin[passwd]",
811 'type' => 'password',
813 'maxlength' => 256)))),
814 HTML::tr(HTML::td(array('align' => 'center', 'colspan' => 2),
815 Button("submit:", _("Submit"), 'wikiaction'),
817 Button("submit:dbadmin[cancel]", _("Cancel"),
820 echo "</div><!-- content -->\n";
821 echo asXML(Template("bottom"));
822 echo "</body></html>\n";
823 $this->request->finish();
828 * if page.cached_html does not exists:
829 * put _cached_html from pagedata into a new separate blob,
830 * not into the huge serialized string.
832 * It is only rarelely needed: for current page only, if-not-modified,
833 * but was extracted for every simple page iteration.
835 private function _upgrade_cached_html($verbose = true)
840 if ($this->phpwiki_version >= 1030.10) {
842 echo _("Check for extra page.cached_html column"), " ... ";
843 $database = $this->dbi->_backend->database();
844 extract($this->dbi->_backend->_table_names);
845 $fields = $this->dbi->_backend->listOfFields($database, $page_tbl);
847 echo _("SKIP"), "<br />\n";
850 if (!strstr(strtolower(join(':', $fields)), "cached_html")) {
852 echo "<b>", _("ADDING"), "</b>", " ... ";
853 $backend_type = $this->dbi->_backend->backendType();
854 if (substr($backend_type, 0, 5) == 'mysql')
855 $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl ADD cached_html MEDIUMBLOB");
857 $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl ADD cached_html BLOB");
859 echo "<b>", _("CONVERTING"), "</b>", " ... ";
860 $count = $this->_convert_cached_html();
862 echo $count, " ", _("OK"), "<br />\n";
865 echo _("OK"), "<br />\n";
872 * move _cached_html for all pages from pagedata into a new separate blob.
873 * decoupled from action=upgrade, so that it can be used by a WikiAdminUtils button also.
875 private function _convert_cached_html()
880 $pages = $this->dbi->getAllPages();
881 $cache =& $this->dbi->_cache;
883 extract($this->dbi->_backend->_table_names);
884 while ($page = $pages->next()) {
885 $pagename = $page->getName();
886 $data = $this->dbi->_backend->get_pagedata($pagename);
887 if (!empty($data['_cached_html'])) {
888 $cached_html = $data['_cached_html'];
889 $data['_cached_html'] = '';
890 $cache->update_pagedata($pagename, $data);
891 // store as blob, not serialized
892 $this->dbi->genericSqlQuery("UPDATE $page_tbl SET cached_html=? WHERE pagename=?",
893 array($cached_html, $pagename));
901 * upgrade to 1.3.13 link structure.
903 private function _upgrade_relation_links()
905 if ($this->phpwiki_version >= 1030.12200610 and $this->isSQL) {
906 echo _("Check for relation field in link table"), " ...";
907 $database = $this->dbi->_backend->database();
908 $prefix = isset($DBParams['prefix']) ? $DBParams['prefix'] : '';
909 $link_tbl = $prefix . 'link';
910 $fields = $this->dbi->_backend->listOfFields($database, $link_tbl);
913 } elseif (strstr(strtolower(join(':', $fields)), "link")) {
914 echo "<b>", _("ADDING"), " relation</b>", " ... ";
915 $this->dbi->genericSqlQuery("ALTER TABLE $link_tbl ADD relation INT DEFAULT 0;");
916 $this->dbi->genericSqlQuery("CREATE INDEX link_relation ON $link_tbl (relation);");
922 if ($this->phpwiki_version >= 1030.12200610) {
923 echo _("Rebuild entire database to upgrade relation links"), " ... ";
924 if (DATABASE_TYPE == 'dba') {
925 echo "<b>", _("CONVERTING"), " dba linktable</b>", "(~2 min, max 4 min) ... ";
928 $this->dbi->_backend->_linkdb->rebuild();
932 $this->dbi->_backend->rebuild();
934 echo _("OK"), "<br />\n";
939 * preg_replace over local file.
940 * Only line-orientated matches possible.
942 public function fixLocalFile($match, $replace, $filename)
944 $o_filename = $filename;
945 if (!file_exists($filename))
946 $filename = FindFile($filename);
947 if (!file_exists($filename))
948 return array(false, sprintf(_("File “%s” not found."), $o_filename));
950 if (is_writable($filename)) {
951 $in = fopen($filename, "rb");
952 $out = fopen($tmp = tempnam(getUploadFilePath(), "cfg"), "wb");
954 $tmp = str_replace("/", "\\", $tmp);
955 // Detect the existing linesep at first line. fgets strips it even if 'rb'.
956 // Before we simply assumed \r\n on Windows local files.
957 $s = fread($in, 1024);
959 $linesep = (substr_count($s, "\r\n") > substr_count($s, "\n")) ? "\r\n" : "\n";
960 //$linesep = isWindows() ? "\r\n" : "\n";
961 while ($s = fgets($in)) {
962 // =>php-5.0.1 can fill count
963 //$new = preg_replace($match, $replace, $s, -1, $count);
964 $new = preg_replace($match, $replace, $s);
966 $s = $new . $linesep;
975 $reason = sprintf(_("%s not found in %s"), $match, $filename);
977 return array($found, $reason);
979 @unlink("$file.bak");
980 @rename($file, "$file.bak");
981 if (!rename($tmp, $file))
982 return array(false, sprintf(_("couldn't move %s to %s"), $tmp, $filename));
986 return array(false, sprintf(_("file %s is not writable"), $filename));
990 public function CheckConfigUpdate()
992 echo "<h2>", sprintf(_("Check for necessary %s updates"),
993 "config.ini"), "</h2>\n";
994 $entry = new UpgradeConfigEntry
995 ($this, array('key' => 'cache_control_none',
996 'fixed_with' => 1012.0,
997 'header' => sprintf(_("Check for %s"), "CACHE_CONTROL = NONE"),
998 'applicable_args' => 'CACHE_CONTROL',
999 'notice' => _("CACHE_CONTROL is set to 'NONE', and must be changed to 'NO_CACHE'"),
1000 'check_args' => array("/^\s*CACHE_CONTROL\s*=\s*NONE/", "CACHE_CONTROL = NO_CACHE")));
1001 $entry->setApplicableCb(new WikiMethodCb($entry, '_applicable_defined_and_empty'));
1002 $this->_configUpdates[] = $entry;
1004 $entry = new UpgradeConfigEntry
1005 ($this, array('key' => 'group_method_none',
1006 'fixed_with' => 1012.0,
1007 'header' => sprintf(_("Check for %s"), "GROUP_METHOD = NONE"),
1008 'applicable_args' => 'GROUP_METHOD',
1009 'notice' => _("GROUP_METHOD is set to NONE, and must be changed to \"NONE\""),
1010 'check_args' => array("/^\s*GROUP_METHOD\s*=\s*NONE/", "GROUP_METHOD = \"NONE\"")));
1011 $entry->setApplicableCb(new WikiMethodCb($entry, '_applicable_defined_and_empty'));
1012 $this->_configUpdates[] = $entry;
1014 $entry = new UpgradeConfigEntry
1015 ($this, array('key' => 'blog_empty_default_prefix',
1016 'fixed_with' => 1013.0,
1017 'header' => sprintf(_("Check for %s"), "BLOG_EMPTY_DEFAULT_PREFIX"),
1018 'applicable_args' => 'BLOG_EMPTY_DEFAULT_PREFIX',
1019 'notice' => _("fix BLOG_EMPTY_DEFAULT_PREFIX into BLOG_DEFAULT_EMPTY_PREFIX"),
1020 'check_args' => array("/BLOG_EMPTY_DEFAULT_PREFIX\s*=/", "BLOG_DEFAULT_EMPTY_PREFIX =")));
1021 $entry->setApplicableCb(new WikiMethodCb($entry, '_applicable_defined'));
1022 $this->_configUpdates[] = $entry;
1024 // TODO: find extra file updates
1025 if (empty($this->_configUpdates))
1027 foreach ($this->_configUpdates as $update) {
1036 public $applicable_cb;
1044 * Add an upgrade item to be checked.
1046 * @param object $parent The parent Upgrade class to inherit the version properties
1047 * @param array $params
1049 function __construct(&$parent, $params)
1051 $this->parent =& $parent; // get the properties db_version
1052 foreach (array('key' => 'required',
1053 // the wikidb stores the version when we actually fixed that.
1054 'fixed_with' => 'required',
1055 'header' => '', // always printed
1056 'applicable_cb' => null, // method to check if applicable
1057 'applicable_args' => array(), // might be the config name
1059 'check_cb' => null, // method to apply
1060 'check_args' => array())
1062 if (!isset($params[$k])) { // default
1063 if ($v == 'required') trigger_error("Required arg $k missing", E_USER_ERROR);
1064 else $this->{$k} = $v;
1066 $this->{$k} = $params[$k];
1069 if (!is_array($this->applicable_args)) // single arg convenience shortcut
1070 $this->applicable_args = array($this->applicable_args);
1071 if (!is_array($this->check_args)) // single arg convenience shortcut
1072 $this->check_args = array($this->check_args);
1073 if ($this->notice === '' and count($this->applicable_args) > 0)
1074 $this->notice = 'Check for ' . join(', ', $this->applicable_args);
1075 $this->_db_key = "_upgrade";
1076 $this->upgrade = $this->parent->dbi->get($this->_db_key);
1080 public function setApplicableCb($object)
1082 $this->applicable_cb =& $object;
1085 private function _check_if_already_fixed()
1088 if (!isset($this->upgrade['name']))
1090 // override with force?
1091 if ($this->parent->request->getArg('force'))
1093 // already fixed and with an ok version
1094 if ($this->upgrade['name'] >= $this->fixed_with)
1095 return $this->upgrade['name'];
1096 // already fixed but with an older version. do it again.
1100 public function pass()
1102 // store in db no to fix again
1103 $this->upgrade['name'] = $this->parent->phpwiki_version;
1104 $this->parent->dbi->set($this->_db_key, $this->upgrade);
1105 echo "<b>", _("FIXED"), "</b>";
1106 if (isset($this->reason))
1107 echo _(": "), $this->reason;
1113 public function fail()
1115 echo '<span style="color: red; font-weight: bold; ">' . _("FAILED") . "</span>";
1116 if (isset($this->reason))
1117 echo _(": "), $this->reason;
1123 private function skip()
1125 if (isset($this->silent_skip))
1127 echo " " . _("Skipped.") . "<br />\n";
1132 public function check($args = null)
1134 if ($this->header) echo $this->header, ' ... ';
1135 if ($when = $this->_check_if_already_fixed()) {
1136 // be totally silent if no header is defined.
1137 if ($this->header) echo _("fixed with"), " ", $when, "<br />\n";
1141 if (is_object($this->applicable_cb)) {
1142 if (!$this->applicable_cb->call_array($this->applicable_args))
1143 return $this->skip();
1145 if ($this->notice) {
1148 echo $this->notice, " ";
1151 if (!is_null($args)) $this->check_args =& $args;
1152 if (is_object($this->check_cb))
1153 $do = $this->method_cb->call_array($this->check_args);
1155 $do = $this->default_method($this->check_args);
1156 if (is_array($do)) {
1157 $this->reason = $do[1];
1160 return $do ? $this->pass() : $this->fail();
1162 } // class UpgradeEntry
1164 class UpgradeConfigEntry extends UpgradeEntry
1166 public function _applicable_defined()
1168 return (boolean)defined($this->applicable_args[0]);
1171 public function _applicable_defined_and_empty()
1173 $const = $this->applicable_args[0];
1174 return (boolean)(defined($const) and !constant($const));
1177 public function default_method($args)
1180 $replace = $args[1];
1181 return $this->parent->fixLocalFile($match, $replace, "config/config.ini");
1183 } // class UpdateConfigEntry
1185 /** entry function from lib/main.php
1187 function DoUpgrade(&$request)
1190 if (!$request->_user->isAdmin()) {
1191 $request->_notAuthorized(WIKIAUTH_ADMIN);
1193 HTML::div(array('class' => 'disabled-plugin'),
1194 fmt("Upgrade disabled: user != isAdmin")));
1197 // TODO: StartLoadDump should turn on implicit_flush.
1198 @ini_set("implicit_flush", true);
1199 StartLoadDump($request, _("Upgrading this PhpWiki"));
1200 $upgrade = new Upgrade($request);
1201 if (!$request->getArg('nodb')) {
1202 $upgrade->CheckDatabaseUpdate($request); // first check cached_html and friends
1204 if (!$request->getArg('nopgsrc')) {
1205 $upgrade->CheckPgsrcUpdate($request);
1206 $upgrade->CheckActionPageUpdate($request);
1208 if (!$request->getArg('noconfig')) {
1209 $upgrade->CheckConfigUpdate($request);
1211 EndLoadDump($request);
1217 // c-basic-offset: 4
1218 // c-hanging-comment-ender-p: nil
1219 // indent-tabs-mode: nil